systemprompt_models/validators/
content.rs

1//! Content configuration validator.
2
3use super::ValidationConfigProvider;
4use crate::ContentConfigRaw;
5use std::path::{Path, PathBuf};
6use systemprompt_traits::validation_report::{ValidationError, ValidationReport};
7use systemprompt_traits::{ConfigProvider, DomainConfig, DomainConfigError};
8
9#[derive(Debug)]
10struct LoadedContentConfig {
11    config: ContentConfigRaw,
12    services_path: PathBuf,
13}
14
15impl LoadedContentConfig {
16    fn resolve_path(&self, path: &str) -> PathBuf {
17        let path = Path::new(path);
18        if path.is_absolute() {
19            path.to_path_buf()
20        } else {
21            self.services_path.join(path)
22        }
23    }
24}
25
26#[derive(Debug, Default)]
27pub struct ContentConfigValidator {
28    loaded: Option<LoadedContentConfig>,
29}
30
31impl ContentConfigValidator {
32    pub fn new() -> Self {
33        Self::default()
34    }
35}
36
37impl DomainConfig for ContentConfigValidator {
38    fn domain_id(&self) -> &'static str {
39        "content"
40    }
41
42    fn priority(&self) -> u32 {
43        20
44    }
45
46    fn load(&mut self, config: &dyn ConfigProvider) -> Result<(), DomainConfigError> {
47        let provider = config
48            .as_any()
49            .downcast_ref::<ValidationConfigProvider>()
50            .ok_or_else(|| {
51                DomainConfigError::LoadError(
52                    "Expected ValidationConfigProvider with pre-loaded configs".into(),
53                )
54            })?;
55
56        self.loaded = provider
57            .content_config()
58            .cloned()
59            .map(|config| LoadedContentConfig {
60                config,
61                services_path: PathBuf::from(&provider.config().services_path),
62            });
63        Ok(())
64    }
65
66    fn validate(&self) -> Result<ValidationReport, DomainConfigError> {
67        let mut report = ValidationReport::new("content");
68
69        let Some(loaded) = self.loaded.as_ref() else {
70            return Ok(report);
71        };
72
73        for (name, source) in &loaded.config.content_sources {
74            let source_path = loaded.resolve_path(&source.path);
75            if !source_path.exists() {
76                report.add_error(
77                    ValidationError::new(
78                        format!("content_sources.{}", name),
79                        "Content source directory does not exist",
80                    )
81                    .with_path(source_path)
82                    .with_suggestion("Create the directory or remove the source"),
83                );
84            }
85
86            if source.source_id.as_str().is_empty() {
87                report.add_error(ValidationError::new(
88                    format!("content_sources.{}.source_id", name),
89                    "Source ID cannot be empty",
90                ));
91            }
92
93            if source.category_id.as_str().is_empty() {
94                report.add_error(ValidationError::new(
95                    format!("content_sources.{}.category_id", name),
96                    "Category ID cannot be empty",
97                ));
98            }
99        }
100
101        for (name, source) in &loaded.config.content_sources {
102            if !loaded
103                .config
104                .categories
105                .contains_key(source.category_id.as_str())
106            {
107                report.add_error(
108                    ValidationError::new(
109                        format!("content_sources.{}.category_id", name),
110                        format!(
111                            "Referenced category '{}' not found in categories",
112                            source.category_id
113                        ),
114                    )
115                    .with_suggestion("Add the category to the categories section"),
116                );
117            }
118        }
119
120        Ok(report)
121    }
122}