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_identifiers::SourceId;
9use systemprompt_models::ContentConfigRaw;
10
11#[derive(Debug, Clone)]
12pub struct StaticContentMatcher {
13    patterns: Vec<(String, SourceId)>,
14}
15
16impl StaticContentMatcher {
17    pub fn from_config(config_path: &str) -> Result<Self> {
18        let yaml_content = std::fs::read_to_string(config_path)?;
19        let config: ContentConfigRaw = serde_yaml::from_str(&yaml_content)?;
20
21        let patterns = config
22            .content_sources
23            .into_iter()
24            .filter(|(_, source)| source.enabled)
25            .filter_map(|(source_id, source)| {
26                source
27                    .sitemap
28                    .filter(|s| s.enabled)
29                    .map(|sitemap| (sitemap.url_pattern, SourceId::new(source_id)))
30            })
31            .collect();
32
33        Ok(Self { patterns })
34    }
35
36    pub const fn empty() -> Self {
37        Self {
38            patterns: Vec::new(),
39        }
40    }
41
42    pub fn matches(&self, path: &str) -> Option<(String, SourceId)> {
43        self.patterns.iter().find_map(|(pattern, source_id)| {
44            extract_slug(path, pattern).map(|slug| (slug, source_id.clone()))
45        })
46    }
47}
48
49fn extract_slug(path: &str, pattern: &str) -> Option<String> {
50    let pattern_parts: Vec<&str> = pattern.split('{').collect();
51    if pattern_parts.len() != 2 {
52        return None;
53    }
54
55    let prefix = pattern_parts[0];
56    if !path.starts_with(prefix) {
57        return None;
58    }
59
60    let slug = path.trim_start_matches(prefix).trim_end_matches('/');
61    (!slug.is_empty()).then(|| slug.to_owned())
62}