Skip to main content

ralph/template/variables/
validate.rs

1//! Purpose: Scan template-backed task fields for supported and unknown
2//! variables.
3//!
4//! Responsibilities:
5//! - Extract `{{variable}}` placeholders from task fields.
6//! - Detect whether branch context is required.
7//! - Report unknown template variables as warnings.
8//!
9//! Scope:
10//! - Validation only; no git probing or string substitution.
11//!
12//! Usage:
13//! - Called by template loading before context detection and substitution.
14//!
15//! Invariants/Assumptions:
16//! - Variable syntax remains `{{variable_name}}`.
17//! - Unknown variables produce warnings rather than hard failures.
18//! - Validation behavior and warning ordering remain unchanged.
19
20use std::collections::HashSet;
21
22use regex::Regex;
23
24use crate::contracts::Task;
25
26use super::context::{TemplateValidation, TemplateWarning};
27
28/// The set of known/supported template variables.
29const KNOWN_VARIABLES: &[&str] = &["target", "module", "file", "branch"];
30
31/// Extract template variable occurrences from a string.
32///
33/// Returns a set of variable names found in the input (without braces).
34pub(super) fn extract_variables(input: &str) -> HashSet<String> {
35    let mut variables = HashSet::new();
36    // Use lazy_static or thread_local for regex if performance is critical,
37    // but for template loading (not hot path), we can compile on demand.
38    // This function is called infrequently during template loading.
39    let re = match Regex::new(r"\{\{(\w+)\}\}") {
40        Ok(re) => re,
41        Err(_) => return variables, // Should never happen with static pattern
42    };
43
44    for cap in re.captures_iter(input) {
45        if let Some(matched) = cap.get(1) {
46            variables.insert(matched.as_str().to_string());
47        }
48    }
49    variables
50}
51
52/// Check if the input contains the {{branch}} variable.
53pub(super) fn uses_branch_variable(input: &str) -> bool {
54    input.contains("{{branch}}")
55}
56
57/// Validate a template task and collect warnings.
58///
59/// This scans all string fields in the task for:
60/// - Unknown template variables (not in KNOWN_VARIABLES)
61/// - Presence of {{branch}} variable (to determine if git detection is needed)
62pub fn validate_task_template(task: &Task) -> TemplateValidation {
63    let mut validation = TemplateValidation::default();
64    let mut all_variables: HashSet<String> = HashSet::new();
65
66    // Collect variables from all string fields
67    let fields = [
68        ("title", task.title.clone()),
69        ("request", task.request.clone().unwrap_or_default()),
70    ];
71
72    for (field_name, value) in &fields {
73        if uses_branch_variable(value) {
74            validation.uses_branch = true;
75        }
76        let vars = extract_variables(value);
77        for var in &vars {
78            if !KNOWN_VARIABLES.contains(&var.as_str()) {
79                validation.warnings.push(TemplateWarning::UnknownVariable {
80                    name: var.clone(),
81                    field: Some(field_name.to_string()),
82                });
83            }
84            all_variables.insert(var.clone());
85        }
86    }
87
88    // Check array fields
89    let array_fields: [(&str, &[String]); 5] = [
90        ("tags", &task.tags),
91        ("scope", &task.scope),
92        ("evidence", &task.evidence),
93        ("plan", &task.plan),
94        ("notes", &task.notes),
95    ];
96
97    for (field_name, values) in &array_fields {
98        for value in *values {
99            if uses_branch_variable(value) {
100                validation.uses_branch = true;
101            }
102            let vars = extract_variables(value);
103            for var in &vars {
104                if !KNOWN_VARIABLES.contains(&var.as_str()) {
105                    validation.warnings.push(TemplateWarning::UnknownVariable {
106                        name: var.clone(),
107                        field: Some(field_name.to_string()),
108                    });
109                }
110                all_variables.insert(var.clone());
111            }
112        }
113    }
114
115    validation
116}