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