Skip to main content

systemprompt_models/routing/
mod.rs

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