Skip to main content

systemprompt_models/validators/
web.rs

1//! Web configuration validator.
2
3use super::validation_config_provider::{WebConfigRaw, WebMetadataRaw};
4use super::ValidationConfigProvider;
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_string());
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_string();
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_string()
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        if branding.copyright.as_ref().is_none_or(String::is_empty) {
180            report.add_error(
181                ValidationError::new(
182                    "web_config.branding.copyright",
183                    "Missing required field 'copyright'",
184                )
185                .with_suggestion("Add 'copyright: \"© 2024 Your Company\"' under branding"),
186            );
187        }
188
189        if branding
190            .twitter_handle
191            .as_ref()
192            .is_none_or(String::is_empty)
193        {
194            report.add_error(
195                ValidationError::new(
196                    "web_config.branding.twitter_handle",
197                    "Missing required field 'twitter_handle'",
198                )
199                .with_suggestion("Add 'twitter_handle: \"@yourhandle\"' under branding"),
200            );
201        }
202
203        if branding.display_sitename.is_none() {
204            report.add_error(
205                ValidationError::new(
206                    "web_config.branding.display_sitename",
207                    "Missing required field 'display_sitename'",
208                )
209                .with_suggestion("Add 'display_sitename: true' under branding"),
210            );
211        }
212
213        if branding.favicon.as_ref().is_none_or(String::is_empty) {
214            report.add_error(
215                ValidationError::new(
216                    "web_config.branding.favicon",
217                    "Missing required field 'favicon'",
218                )
219                .with_suggestion("Add 'favicon: \"/favicon.ico\"' under branding"),
220            );
221        }
222
223        let logo_svg = branding
224            .logo
225            .as_ref()
226            .and_then(|l| l.primary.as_ref())
227            .and_then(|p| p.svg.as_ref());
228
229        if logo_svg.is_none_or(String::is_empty) {
230            report.add_error(
231                ValidationError::new(
232                    "web_config.branding.logo.primary.svg",
233                    "Missing required field 'logo.primary.svg'",
234                )
235                .with_suggestion("Add 'logo: { primary: { svg: \"/logo.svg\" } }' under branding"),
236            );
237        }
238    }
239}