Skip to main content

systemprompt_api/services/static_content/
config.rs

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