systemprompt-models 0.1.22

Shared data models and types for systemprompt.io OS
Documentation
use crate::ContentRouting;
use crate::modules::ApiPaths;
use std::path::Path;
use std::sync::Arc;

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct EventMetadata {
    pub event_type: &'static str,
    pub event_category: &'static str,
    pub log_module: &'static str,
}

impl EventMetadata {
    pub const HTML_CONTENT: Self = Self {
        event_type: "page_view",
        event_category: "content",
        log_module: "page_view",
    };

    pub const API_REQUEST: Self = Self {
        event_type: "http_request",
        event_category: "api",
        log_module: "http_request",
    };

    pub const STATIC_ASSET: Self = Self {
        event_type: "asset_request",
        event_category: "static",
        log_module: "asset_request",
    };

    pub const NOT_FOUND: Self = Self {
        event_type: "not_found",
        event_category: "error",
        log_module: "not_found",
    };
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RouteType {
    HtmlContent { source: String },
    ApiEndpoint { category: ApiCategory },
    StaticAsset { asset_type: AssetType },
    NotFound,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ApiCategory {
    Content,
    Core,
    Agents,
    OAuth,
    Other,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AssetType {
    JavaScript,
    Stylesheet,
    Image,
    Font,
    SourceMap,
    Other,
}

pub struct RouteClassifier {
    content_routing: Option<Arc<dyn ContentRouting>>,
}

impl std::fmt::Debug for RouteClassifier {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("RouteClassifier")
            .field("content_routing", &self.content_routing.is_some())
            .finish()
    }
}

impl RouteClassifier {
    pub fn new(content_routing: Option<Arc<dyn ContentRouting>>) -> Self {
        Self { content_routing }
    }

    pub fn classify(&self, path: &str, _method: &str) -> RouteType {
        if Self::is_static_asset_path(path) {
            return RouteType::StaticAsset {
                asset_type: Self::determine_asset_type(path),
            };
        }

        if path.starts_with(ApiPaths::API_BASE) {
            return RouteType::ApiEndpoint {
                category: Self::determine_api_category(path),
            };
        }

        if path.starts_with(ApiPaths::TRACK_BASE) {
            return RouteType::ApiEndpoint {
                category: ApiCategory::Other,
            };
        }

        if let Some(routing) = &self.content_routing {
            if routing.is_html_page(path) {
                return RouteType::HtmlContent {
                    source: routing.determine_source(path),
                };
            }
        } else if !Self::is_static_asset_path(path) && !path.starts_with(ApiPaths::API_BASE) {
            return RouteType::HtmlContent {
                source: "unknown".to_string(),
            };
        }

        RouteType::NotFound
    }

    pub fn should_track_analytics(&self, path: &str, method: &str) -> bool {
        if method == "OPTIONS" {
            return false;
        }

        match self.classify(path, method) {
            RouteType::HtmlContent { .. } => true,
            RouteType::ApiEndpoint { category } => {
                matches!(
                    category,
                    ApiCategory::Core | ApiCategory::Content | ApiCategory::Other
                )
            },
            RouteType::StaticAsset { .. } | RouteType::NotFound => false,
        }
    }

    pub fn is_html(&self, path: &str) -> bool {
        matches!(self.classify(path, "GET"), RouteType::HtmlContent { .. })
    }

    pub fn get_event_metadata(&self, path: &str, method: &str) -> EventMetadata {
        match self.classify(path, method) {
            RouteType::HtmlContent { .. } => EventMetadata::HTML_CONTENT,
            RouteType::ApiEndpoint { .. } => EventMetadata::API_REQUEST,
            RouteType::StaticAsset { .. } => EventMetadata::STATIC_ASSET,
            RouteType::NotFound => EventMetadata::NOT_FOUND,
        }
    }

    fn is_static_asset_path(path: &str) -> bool {
        if path.starts_with(ApiPaths::ASSETS_BASE)
            || path.starts_with(ApiPaths::WELLKNOWN_BASE)
            || path.starts_with(ApiPaths::GENERATED_BASE)
            || path.starts_with(ApiPaths::FILES_BASE)
        {
            return true;
        }

        matches!(
            Path::new(path).extension().and_then(|e| e.to_str()),
            Some(
                "js" | "css"
                    | "map"
                    | "ttf"
                    | "woff"
                    | "woff2"
                    | "otf"
                    | "png"
                    | "jpg"
                    | "jpeg"
                    | "svg"
                    | "ico"
                    | "webp"
            )
        ) || path == "/favicon.ico"
    }

    fn determine_asset_type(path: &str) -> AssetType {
        match Path::new(path).extension().and_then(|e| e.to_str()) {
            Some("js") => AssetType::JavaScript,
            Some("css") => AssetType::Stylesheet,
            Some("png" | "jpg" | "jpeg" | "svg" | "ico" | "webp") => AssetType::Image,
            Some("ttf" | "woff" | "woff2" | "otf") => AssetType::Font,
            Some("map") => AssetType::SourceMap,
            _ => AssetType::Other,
        }
    }

    fn determine_api_category(path: &str) -> ApiCategory {
        if path.starts_with(ApiPaths::CONTENT_BASE) {
            ApiCategory::Content
        } else if path.starts_with(ApiPaths::CORE_BASE) {
            ApiCategory::Core
        } else if path.starts_with(ApiPaths::AGENTS_BASE) {
            ApiCategory::Agents
        } else if path.starts_with(ApiPaths::OAUTH_BASE) {
            ApiCategory::OAuth
        } else {
            ApiCategory::Other
        }
    }
}