Skip to main content

systemprompt_api/services/static_content/
config.rs

1//! `StaticContentMatcher` that maps request paths to content-source slugs via
2//! sitemap URL patterns.
3//!
4//! Copyright (c) systemprompt.io — Business Source License 1.1.
5//! See <https://systemprompt.io> for licensing details.
6
7use anyhow::Result;
8use systemprompt_models::ContentConfigRaw;
9
10#[derive(Debug, Clone)]
11pub struct StaticContentMatcher {
12    patterns: Vec<(String, String)>,
13}
14
15impl StaticContentMatcher {
16    pub fn from_config(config_path: &str) -> Result<Self> {
17        let yaml_content = std::fs::read_to_string(config_path)?;
18        let config: ContentConfigRaw = serde_yaml::from_str(&yaml_content)?;
19
20        let patterns = config
21            .content_sources
22            .into_iter()
23            .filter(|(_, source)| source.enabled)
24            .filter_map(|(source_id, source)| {
25                source
26                    .sitemap
27                    .filter(|s| s.enabled)
28                    .map(|sitemap| (sitemap.url_pattern, source_id))
29            })
30            .collect();
31
32        Ok(Self { patterns })
33    }
34
35    pub const fn empty() -> Self {
36        Self {
37            patterns: Vec::new(),
38        }
39    }
40
41    pub fn matches(&self, path: &str) -> Option<(String, String)> {
42        self.patterns.iter().find_map(|(pattern, source_id)| {
43            extract_slug(path, pattern).map(|slug| (slug, source_id.clone()))
44        })
45    }
46}
47
48fn extract_slug(path: &str, pattern: &str) -> Option<String> {
49    let pattern_parts: Vec<&str> = pattern.split('{').collect();
50    if pattern_parts.len() != 2 {
51        return None;
52    }
53
54    let prefix = pattern_parts[0];
55    if !path.starts_with(prefix) {
56        return None;
57    }
58
59    let slug = path.trim_start_matches(prefix).trim_end_matches('/');
60    (!slug.is_empty()).then(|| slug.to_owned())
61}