systemprompt_models/validators/
web.rs1use 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}