systemprompt_cli/commands/web/validate/
template_validation.rs1use std::collections::HashSet;
7use std::fs;
8
9use systemprompt_models::content_config::ContentConfigRaw;
10
11use super::super::paths::WebPaths;
12use super::super::types::{TemplatesConfig, ValidationIssue};
13
14pub fn validate_templates(
15 profile: &systemprompt_models::Profile,
16 web_paths: &WebPaths,
17 errors: &mut Vec<ValidationIssue>,
18 warnings: &mut Vec<ValidationIssue>,
19) {
20 let templates_dir = &web_paths.templates;
21 let templates_yaml_path = templates_dir.join("templates.yaml");
22
23 if !templates_yaml_path.exists() {
24 warnings.push(ValidationIssue {
25 source: "templates".to_owned(),
26 message: format!(
27 "templates.yaml not found at {}",
28 templates_yaml_path.display()
29 ),
30 suggestion: Some("Create a templates.yaml file".to_owned()),
31 });
32 return;
33 }
34
35 let Ok(content) = fs::read_to_string(&templates_yaml_path) else {
36 errors.push(ValidationIssue {
37 source: "templates".to_owned(),
38 message: "Failed to read templates.yaml".to_owned(),
39 suggestion: None,
40 });
41 return;
42 };
43
44 let Ok(templates_config) = serde_yaml::from_str::<TemplatesConfig>(&content) else {
45 errors.push(ValidationIssue {
46 source: "templates".to_owned(),
47 message: "Failed to parse templates.yaml".to_owned(),
48 suggestion: Some("Check YAML syntax".to_owned()),
49 });
50 return;
51 };
52
53 for name in templates_config.templates.keys() {
54 let html_path = templates_dir.join(format!("{}.html", name));
55 if !html_path.exists() {
56 errors.push(ValidationIssue {
57 source: "templates".to_owned(),
58 message: format!("Missing HTML file for template '{}'", name),
59 suggestion: Some(format!("Create {}", html_path.display())),
60 });
61 }
62 }
63
64 let content_config_path = profile.paths.content_config();
65 let Ok(content) = fs::read_to_string(&content_config_path) else {
66 return;
67 };
68 let Ok(content_config) = serde_yaml::from_str::<ContentConfigRaw>(&content) else {
69 return;
70 };
71
72 let content_type_names: HashSet<&String> = content_config.content_sources.keys().collect();
73
74 for (template_name, entry) in &templates_config.templates {
75 for ct in &entry.content_types {
76 if !content_type_names.contains(ct) {
77 warnings.push(ValidationIssue {
78 source: "templates".to_owned(),
79 message: format!(
80 "Template '{}' references unknown content type '{}'",
81 template_name, ct
82 ),
83 suggestion: Some("Add the content type to content config".to_owned()),
84 });
85 }
86 }
87 }
88
89 let template_content_types: HashSet<&String> = templates_config
90 .templates
91 .values()
92 .flat_map(|e| e.content_types.iter())
93 .collect();
94
95 for name in content_type_names {
96 if !template_content_types.contains(name) {
97 warnings.push(ValidationIssue {
98 source: "templates".to_owned(),
99 message: format!("Content type '{}' has no associated template", name),
100 suggestion: Some("Link a template to this content type".to_owned()),
101 });
102 }
103 }
104}