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