Skip to main content

systemprompt_models/validators/
web.rs

1//! Web configuration validator.
2
3use super::ValidationConfigProvider;
4use super::validation_config_provider::{WebConfigRaw, WebMetadataRaw};
5use std::path::Path;
6use systemprompt_traits::validation_report::{
7    ValidationError, ValidationReport, ValidationWarning,
8};
9use systemprompt_traits::{ConfigProvider, DomainConfig, DomainConfigError};
10
11#[derive(Debug, Default)]
12pub struct WebConfigValidator {
13    config: Option<WebConfigRaw>,
14    metadata: Option<WebMetadataRaw>,
15    config_path: Option<String>,
16    system_path: Option<String>,
17}
18
19impl DomainConfig for WebConfigValidator {
20    fn domain_id(&self) -> &'static str {
21        "web"
22    }
23
24    fn priority(&self) -> u32 {
25        10
26    }
27
28    fn load(&mut self, config: &dyn ConfigProvider) -> Result<(), DomainConfigError> {
29        let provider = config
30            .as_any()
31            .downcast_ref::<ValidationConfigProvider>()
32            .ok_or_else(|| {
33                DomainConfigError::LoadError(
34                    "Expected ValidationConfigProvider with pre-loaded configs".into(),
35                )
36            })?;
37
38        self.config = provider.web_config().cloned();
39        self.metadata = provider.web_metadata().cloned();
40        self.config_path = config.get("web_config_path");
41        self.system_path = Some(config.system_path().to_owned());
42        Ok(())
43    }
44
45    fn validate(&self) -> Result<ValidationReport, DomainConfigError> {
46        let mut report = ValidationReport::new("web");
47
48        let Some(cfg) = self.config.as_ref() else {
49            return Ok(report);
50        };
51
52        if let Some(ref base_url) = cfg.base_url {
53            if !base_url.starts_with("http://") && !base_url.starts_with("https://") {
54                report.add_error(
55                    ValidationError::new(
56                        "web_config.base_url",
57                        format!("Invalid URL format: {}", base_url),
58                    )
59                    .with_suggestion("URL must start with http:// or https://"),
60                );
61            }
62        }
63
64        if let Some(ref site_name) = cfg.site_name {
65            if site_name.is_empty() {
66                report.add_error(ValidationError::new(
67                    "web_config.site_name",
68                    "Site name cannot be empty",
69                ));
70            }
71        }
72
73        if let Some(ref path) = self.config_path {
74            let parent = Path::new(path).parent();
75            if let Some(dir) = parent {
76                if !dir.exists() {
77                    report.add_error(
78                        ValidationError::new("web_config", "Web config directory does not exist")
79                            .with_path(dir),
80                    );
81                }
82            }
83        }
84
85        self.validate_branding(&mut report);
86        self.validate_paths(&mut report);
87
88        Ok(report)
89    }
90}
91
92impl WebConfigValidator {
93    pub fn new() -> Self {
94        Self::default()
95    }
96
97    fn validate_paths(&self, report: &mut ValidationReport) {
98        let Some(cfg) = self.config.as_ref() else {
99            return;
100        };
101
102        let Some(paths) = cfg.paths.as_ref() else {
103            report.add_warning(
104                ValidationWarning::new(
105                    "web_config.paths",
106                    "Missing 'paths' section - using defaults",
107                )
108                .with_suggestion("Add paths.templates and paths.assets to web_config.yaml"),
109            );
110            return;
111        };
112
113        if let Some(templates) = &paths.templates {
114            if !templates.is_empty() {
115                let resolved = self.resolve_path(templates);
116                let path = Path::new(&resolved);
117                if !path.exists() {
118                    report.add_error(
119                        ValidationError::new(
120                            "web_config.paths.templates",
121                            format!("Templates directory does not exist: {}", resolved),
122                        )
123                        .with_path(path)
124                        .with_suggestion(
125                            "Create the templates directory or update paths.templates in \
126                             web_config.yaml",
127                        ),
128                    );
129                } else if !path.is_dir() {
130                    report.add_error(
131                        ValidationError::new(
132                            "web_config.paths.templates",
133                            "Templates path is not a directory",
134                        )
135                        .with_path(path),
136                    );
137                }
138            }
139        }
140    }
141
142    fn resolve_path(&self, path: &str) -> String {
143        let p = Path::new(path);
144        if p.is_absolute() {
145            return path.to_owned();
146        }
147
148        if let Some(ref system_path) = self.system_path {
149            let base = Path::new(system_path);
150            let resolved = base.join(p);
151            return resolved.canonicalize().map_or_else(
152                |_| resolved.to_string_lossy().to_string(),
153                |c| c.to_string_lossy().to_string(),
154            );
155        }
156
157        path.to_owned()
158    }
159
160    fn validate_branding(&self, report: &mut ValidationReport) {
161        let Some(cfg) = self.config.as_ref() else {
162            return;
163        };
164
165        let Some(branding) = cfg.branding.as_ref() else {
166            report.add_error(
167                ValidationError::new(
168                    "web_config.branding",
169                    "Missing 'branding' section in web.yaml",
170                )
171                .with_suggestion(
172                    "Add a 'branding' section with copyright, logo, favicon, twitter_handle, and \
173                     display_sitename",
174                ),
175            );
176            return;
177        };
178
179        require_branding_field(
180            report,
181            branding.copyright.as_ref().is_none_or(String::is_empty),
182            "web_config.branding.copyright",
183            "Missing required field 'copyright'",
184            "Add 'copyright: \"© 2024 Your Company\"' under branding",
185        );
186        require_branding_field(
187            report,
188            branding
189                .twitter_handle
190                .as_ref()
191                .is_none_or(String::is_empty),
192            "web_config.branding.twitter_handle",
193            "Missing required field 'twitter_handle'",
194            "Add 'twitter_handle: \"@yourhandle\"' under branding",
195        );
196        require_branding_field(
197            report,
198            branding.display_sitename.is_none(),
199            "web_config.branding.display_sitename",
200            "Missing required field 'display_sitename'",
201            "Add 'display_sitename: true' under branding",
202        );
203        require_branding_field(
204            report,
205            branding.favicon.as_ref().is_none_or(String::is_empty),
206            "web_config.branding.favicon",
207            "Missing required field 'favicon'",
208            "Add 'favicon: \"/favicon.ico\"' under branding",
209        );
210
211        let logo_svg = branding
212            .logo
213            .as_ref()
214            .and_then(|l| l.primary.as_ref())
215            .and_then(|p| p.svg.as_ref());
216        require_branding_field(
217            report,
218            logo_svg.is_none_or(String::is_empty),
219            "web_config.branding.logo.primary.svg",
220            "Missing required field 'logo.primary.svg'",
221            "Add 'logo: { primary: { svg: \"/logo.svg\" } }' under branding",
222        );
223    }
224}
225
226fn require_branding_field(
227    report: &mut ValidationReport,
228    missing: bool,
229    field: &str,
230    message: &str,
231    suggestion: &str,
232) {
233    if missing {
234        report.add_error(ValidationError::new(field, message).with_suggestion(suggestion));
235    }
236}