systemprompt_api/services/static_content/
config.rs1use 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}