systemprompt_models/routing/
mod.rs1use crate::ContentRouting;
12use crate::modules::ApiPaths;
13use std::path::Path;
14use std::sync::Arc;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub struct EventMetadata {
18 pub event_type: &'static str,
19 pub event_category: &'static str,
20 pub log_module: &'static str,
21}
22
23impl EventMetadata {
24 pub const HTML_CONTENT: Self = Self {
25 event_type: "page_view",
26 event_category: "content",
27 log_module: "page_view",
28 };
29
30 pub const API_REQUEST: Self = Self {
31 event_type: "http_request",
32 event_category: "api",
33 log_module: "http_request",
34 };
35
36 pub const STATIC_ASSET: Self = Self {
37 event_type: "asset_request",
38 event_category: "static",
39 log_module: "asset_request",
40 };
41
42 pub const NOT_FOUND: Self = Self {
43 event_type: "not_found",
44 event_category: "error",
45 log_module: "not_found",
46 };
47
48 pub const GATEWAY_REQUEST: Self = Self {
52 event_type: "gateway_request",
53 event_category: "gateway",
54 log_module: "gateway_request",
55 };
56}
57
58#[derive(Debug, Clone, PartialEq, Eq)]
59pub enum RouteType {
60 HtmlContent { source: String },
61 ApiEndpoint { category: ApiCategory },
62 StaticAsset { asset_type: AssetType },
63 NotFound,
64}
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
67pub enum ApiCategory {
68 Content,
69 Core,
70 Agents,
71 OAuth,
72 Gateway,
73 Other,
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77pub enum AssetType {
78 JavaScript,
79 Stylesheet,
80 Image,
81 Font,
82 SourceMap,
83 Other,
84}
85
86pub struct RouteClassifier {
87 content_routing: Option<Arc<dyn ContentRouting>>,
88}
89
90impl std::fmt::Debug for RouteClassifier {
91 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92 f.debug_struct("RouteClassifier")
93 .field("content_routing", &self.content_routing.is_some())
94 .finish()
95 }
96}
97
98impl RouteClassifier {
99 pub fn new(content_routing: Option<Arc<dyn ContentRouting>>) -> Self {
100 Self { content_routing }
101 }
102
103 pub fn classify(&self, path: &str, _method: &str) -> RouteType {
104 if Self::is_static_asset_path(path) {
105 return RouteType::StaticAsset {
106 asset_type: Self::determine_asset_type(path),
107 };
108 }
109
110 if path.starts_with(ApiPaths::GATEWAY_BASE)
114 || path.starts_with(ApiPaths::GATEWAY_PUBLIC_BASE)
115 {
116 return RouteType::ApiEndpoint {
117 category: ApiCategory::Gateway,
118 };
119 }
120
121 if path.starts_with(ApiPaths::API_BASE) {
122 return RouteType::ApiEndpoint {
123 category: Self::determine_api_category(path),
124 };
125 }
126
127 if path.starts_with(ApiPaths::TRACK_BASE) {
128 return RouteType::ApiEndpoint {
129 category: ApiCategory::Other,
130 };
131 }
132
133 if let Some(routing) = &self.content_routing {
134 if routing.is_html_page(path) {
135 return RouteType::HtmlContent {
136 source: routing.determine_source(path),
137 };
138 }
139 } else if !Self::is_static_asset_path(path) && !path.starts_with(ApiPaths::API_BASE) {
140 return RouteType::HtmlContent {
141 source: "unknown".to_owned(),
142 };
143 }
144
145 RouteType::NotFound
146 }
147
148 pub fn should_track_analytics(&self, path: &str, method: &str) -> bool {
149 if method == "OPTIONS" {
150 return false;
151 }
152
153 match self.classify(path, method) {
154 RouteType::HtmlContent { .. } => true,
155 RouteType::ApiEndpoint { category } => {
160 matches!(
161 category,
162 ApiCategory::Core | ApiCategory::Content | ApiCategory::Other
163 )
164 },
165 RouteType::StaticAsset { .. } | RouteType::NotFound => false,
166 }
167 }
168
169 pub fn is_html(&self, path: &str) -> bool {
170 matches!(self.classify(path, "GET"), RouteType::HtmlContent { .. })
171 }
172
173 pub fn get_event_metadata(&self, path: &str, method: &str) -> EventMetadata {
174 match self.classify(path, method) {
175 RouteType::HtmlContent { .. } => EventMetadata::HTML_CONTENT,
176 RouteType::ApiEndpoint {
177 category: ApiCategory::Gateway,
178 } => EventMetadata::GATEWAY_REQUEST,
179 RouteType::ApiEndpoint { .. } => EventMetadata::API_REQUEST,
180 RouteType::StaticAsset { .. } => EventMetadata::STATIC_ASSET,
181 RouteType::NotFound => EventMetadata::NOT_FOUND,
182 }
183 }
184
185 fn is_static_asset_path(path: &str) -> bool {
186 if path.starts_with(ApiPaths::ASSETS_BASE)
187 || path.starts_with(ApiPaths::WELLKNOWN_BASE)
188 || path.starts_with(ApiPaths::GENERATED_BASE)
189 || path.starts_with(ApiPaths::FILES_BASE)
190 {
191 return true;
192 }
193
194 matches!(
195 Path::new(path).extension().and_then(|e| e.to_str()),
196 Some(
197 "js" | "css"
198 | "map"
199 | "ttf"
200 | "woff"
201 | "woff2"
202 | "otf"
203 | "png"
204 | "jpg"
205 | "jpeg"
206 | "svg"
207 | "ico"
208 | "webp"
209 | "mp4"
210 | "webm"
211 )
212 ) || path == "/favicon.ico"
213 }
214
215 pub(crate) fn determine_asset_type(path: &str) -> AssetType {
216 match Path::new(path).extension().and_then(|e| e.to_str()) {
217 Some("js") => AssetType::JavaScript,
218 Some("css") => AssetType::Stylesheet,
219 Some("png" | "jpg" | "jpeg" | "svg" | "ico" | "webp") => AssetType::Image,
220 Some("ttf" | "woff" | "woff2" | "otf") => AssetType::Font,
221 Some("map") => AssetType::SourceMap,
222 _ => AssetType::Other,
223 }
224 }
225
226 fn determine_api_category(path: &str) -> ApiCategory {
227 if path.starts_with(ApiPaths::CONTENT_BASE) {
228 ApiCategory::Content
229 } else if path.starts_with(ApiPaths::CORE_BASE) {
230 ApiCategory::Core
231 } else if path.starts_with(ApiPaths::AGENTS_BASE) {
232 ApiCategory::Agents
233 } else if path.starts_with(ApiPaths::OAUTH_BASE) {
234 ApiCategory::OAuth
235 } else {
236 ApiCategory::Other
237 }
238 }
239}