Skip to main content

systemprompt_cli/commands/web/validate/
template_validation.rs

1//! Template validation for `web validate`.
2//!
3//! Copyright (c) systemprompt.io — Business Source License 1.1.
4//! See <https://systemprompt.io> for licensing details.
5
6use 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}