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 let Some(routing) = &self.content_routing {
97            if routing.is_html_page(path) {
98                return RouteType::HtmlContent {
99                    source: routing.determine_source(path),
100                };
101            }
102        } else if !Self::is_static_asset_path(path) && !path.starts_with(ApiPaths::API_BASE) {
103            return RouteType::HtmlContent {
104                source: "unknown".to_string(),
105            };
106        }
107
108        RouteType::NotFound
109    }
110
111    pub fn should_track_analytics(&self, path: &str, method: &str) -> bool {
112        if method == "OPTIONS" {
113            return false;
114        }
115
116        match self.classify(path, method) {
117            RouteType::HtmlContent { .. } => true,
118            RouteType::ApiEndpoint { category } => {
119                matches!(category, ApiCategory::Core | ApiCategory::Content)
120            },
121            RouteType::StaticAsset { .. } | RouteType::NotFound => false,
122        }
123    }
124
125    pub fn is_html(&self, path: &str) -> bool {
126        matches!(self.classify(path, "GET"), RouteType::HtmlContent { .. })
127    }
128
129    pub fn get_event_metadata(&self, path: &str, method: &str) -> EventMetadata {
130        match self.classify(path, method) {
131            RouteType::HtmlContent { .. } => EventMetadata::HTML_CONTENT,
132            RouteType::ApiEndpoint { .. } => EventMetadata::API_REQUEST,
133            RouteType::StaticAsset { .. } => EventMetadata::STATIC_ASSET,
134            RouteType::NotFound => EventMetadata::NOT_FOUND,
135        }
136    }
137
138    fn is_static_asset_path(path: &str) -> bool {
139        if path.starts_with(ApiPaths::ASSETS_BASE)
140            || path.starts_with(ApiPaths::WELLKNOWN_BASE)
141            || path.starts_with(ApiPaths::GENERATED_BASE)
142            || path.starts_with(ApiPaths::FILES_BASE)
143        {
144            return true;
145        }
146
147        matches!(
148            Path::new(path).extension().and_then(|e| e.to_str()),
149            Some(
150                "js" | "css"
151                    | "map"
152                    | "ttf"
153                    | "woff"
154                    | "woff2"
155                    | "otf"
156                    | "png"
157                    | "jpg"
158                    | "jpeg"
159                    | "svg"
160                    | "ico"
161                    | "webp"
162            )
163        ) || path == "/vite.svg"
164            || path == "/favicon.ico"
165    }
166
167    fn determine_asset_type(path: &str) -> AssetType {
168        match Path::new(path).extension().and_then(|e| e.to_str()) {
169            Some("js") => AssetType::JavaScript,
170            Some("css") => AssetType::Stylesheet,
171            Some("png" | "jpg" | "jpeg" | "svg" | "ico" | "webp") => AssetType::Image,
172            Some("ttf" | "woff" | "woff2" | "otf") => AssetType::Font,
173            Some("map") => AssetType::SourceMap,
174            _ => AssetType::Other,
175        }
176    }
177
178    fn determine_api_category(path: &str) -> ApiCategory {
179        if path.starts_with(ApiPaths::CONTENT_BASE) {
180            ApiCategory::Content
181        } else if path.starts_with(ApiPaths::CORE_BASE) {
182            ApiCategory::Core
183        } else if path.starts_with(ApiPaths::AGENTS_BASE) {
184            ApiCategory::Agents
185        } else if path.starts_with(ApiPaths::OAUTH_BASE) {
186            ApiCategory::OAuth
187        } else {
188            ApiCategory::Other
189        }
190    }
191}