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