Skip to main content

systemprompt_models/routing/
mod.rs

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