Skip to main content

systemprompt_models/routing/
mod.rs

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