systemprompt_models/validators/
web.rs1use 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(|| DomainConfigError::LoadError {
33 message: "Expected ValidationConfigProvider with pre-loaded configs".into(),
34 })?;
35
36 self.config = provider.web_config().cloned();
37 self.metadata = provider.web_metadata().cloned();
38 self.config_path = config.get("web_config_path");
39 self.system_path = Some(config.system_path().to_owned());
40 Ok(())
41 }
42
43 fn validate(&self) -> Result<ValidationReport, DomainConfigError> {
44 let mut report = ValidationReport::new("web");
45
46 let Some(cfg) = self.config.as_ref() else {
47 return Ok(report);
48 };
49
50 if let Some(ref base_url) = cfg.base_url
51 && !base_url.starts_with("http://")
52 && !base_url.starts_with("https://")
53 {
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 if let Some(ref site_name) = cfg.site_name
64 && site_name.is_empty()
65 {
66 report.add_error(ValidationError::new(
67 "web_config.site_name",
68 "Site name cannot be empty",
69 ));
70 }
71
72 if let Some(ref path) = self.config_path {
73 let parent = Path::new(path).parent();
74 if let Some(dir) = parent
75 && !dir.exists()
76 {
77 report.add_error(
78 ValidationError::new("web_config", "Web config directory does not exist")
79 .with_path(dir),
80 );
81 }
82 }
83
84 self.validate_branding(&mut report);
85 self.validate_paths(&mut report);
86
87 Ok(report)
88 }
89}
90
91impl WebConfigValidator {
92 pub fn new() -> Self {
93 Self::default()
94 }
95
96 fn validate_paths(&self, report: &mut ValidationReport) {
97 let Some(cfg) = self.config.as_ref() else {
98 return;
99 };
100
101 let Some(paths) = cfg.paths.as_ref() else {
102 report.add_warning(
103 ValidationWarning::new(
104 "web_config.paths",
105 "Missing 'paths' section - using defaults",
106 )
107 .with_suggestion("Add paths.templates and paths.assets to web_config.yaml"),
108 );
109 return;
110 };
111
112 if let Some(templates) = &paths.templates
113 && !templates.is_empty()
114 {
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 fn resolve_path(&self, path: &str) -> String {
142 let p = Path::new(path);
143 if p.is_absolute() {
144 return path.to_owned();
145 }
146
147 if let Some(ref system_path) = self.system_path {
148 let base = Path::new(system_path);
149 let resolved = base.join(p);
150 return resolved.canonicalize().map_or_else(
151 |_| resolved.to_string_lossy().to_string(),
152 |c| c.to_string_lossy().to_string(),
153 );
154 }
155
156 path.to_owned()
157 }
158
159 fn validate_branding(&self, report: &mut ValidationReport) {
160 let Some(cfg) = self.config.as_ref() else {
161 return;
162 };
163
164 let Some(branding) = cfg.branding.as_ref() else {
165 report.add_error(
166 ValidationError::new(
167 "web_config.branding",
168 "Missing 'branding' section in web.yaml",
169 )
170 .with_suggestion(
171 "Add a 'branding' section with copyright, logo, favicon, twitter_handle, and \
172 display_sitename",
173 ),
174 );
175 return;
176 };
177
178 require_branding_field(
179 report,
180 branding.copyright.as_ref().is_none_or(String::is_empty),
181 "web_config.branding.copyright",
182 "Missing required field 'copyright'",
183 "Add 'copyright: \"© 2024 Your Company\"' under branding",
184 );
185 require_branding_field(
186 report,
187 branding
188 .twitter_handle
189 .as_ref()
190 .is_none_or(String::is_empty),
191 "web_config.branding.twitter_handle",
192 "Missing required field 'twitter_handle'",
193 "Add 'twitter_handle: \"@yourhandle\"' under branding",
194 );
195 require_branding_field(
196 report,
197 branding.display_sitename.is_none(),
198 "web_config.branding.display_sitename",
199 "Missing required field 'display_sitename'",
200 "Add 'display_sitename: true' under branding",
201 );
202 require_branding_field(
203 report,
204 branding.favicon.as_ref().is_none_or(String::is_empty),
205 "web_config.branding.favicon",
206 "Missing required field 'favicon'",
207 "Add 'favicon: \"/favicon.ico\"' under branding",
208 );
209
210 let logo_svg = branding
211 .logo
212 .as_ref()
213 .and_then(|l| l.primary.as_ref())
214 .and_then(|p| p.svg.as_ref());
215 require_branding_field(
216 report,
217 logo_svg.is_none_or(String::is_empty),
218 "web_config.branding.logo.primary.svg",
219 "Missing required field 'logo.primary.svg'",
220 "Add 'logo: { primary: { svg: \"/logo.svg\" } }' under branding",
221 );
222 }
223}
224
225fn require_branding_field(
226 report: &mut ValidationReport,
227 missing: bool,
228 field: &str,
229 message: &str,
230 suggestion: &str,
231) {
232 if missing {
233 report.add_error(ValidationError::new(field, message).with_suggestion(suggestion));
234 }
235}