Skip to main content

aam_core/pipeline/
validator.rs

1//! Validator stage: generates validation tasks from AST.
2//!
3//! The Validator analyzes AST nodes and generates declarative `ValidationTask` items.
4//! These tasks are later executed by `ValidateExecutor`, enabling lazy evaluation,
5//! error aggregation (critical for LSP), and potential parallel execution.
6
7use crate::error::{AamlError, ErrorDiagnostics};
8use crate::pipeline::parser::{AstNode, ValueNode};
9use crate::pipeline::tasks::ValidationTask;
10
11/// Trait for task-based semantic validation.
12///
13/// Instead of directly validating, the Validator generates `ValidationTask` items.
14/// This enables deferred execution and better error handling for LSP integration.
15pub trait Validator: Send + Sync {
16    /// Analyzes an AST and generates validation tasks.
17    ///
18    /// This method performs static analysis on the AST and produces a list of
19    /// validation tasks that will be executed later by a `ValidateExecutor`.
20    /// It does NOT mutate state or execute validations directly.
21    ///
22    /// # Arguments
23    /// - `ast`: AST nodes to analyze
24    ///
25    /// # Returns
26    /// - `Ok(Vec<ValidationTask>)` containing all validation tasks to be executed
27    /// - `Err(AamlError)` if AST analysis itself fails (e.g., syntax errors)
28    fn validate<'a>(&self, ast: &'a [AstNode<'a>]) -> Result<Vec<ValidationTask<'a>>, AamlError>;
29
30    /// Performs quick syntactic checks that don't require deferred validation.
31    ///
32    /// This is called immediately during parsing to catch obvious issues early.
33    fn check_syntax(&self, ast: &[AstNode<'_>]) -> Result<(), AamlError>;
34}
35
36/// Default implementation of the Validator stage.
37///
38/// This validator performs syntactic checks immediately and generates
39/// semantic validation tasks for deferred execution.
40pub struct DefaultValidator;
41
42impl DefaultValidator {
43    #[must_use]
44    pub const fn new() -> Self {
45        Self
46    }
47
48    fn infer_literal_type(value: &str) -> &'static str {
49        if value == "true" || value == "false" {
50            "bool"
51        } else if value.parse::<f64>().is_ok() {
52            "f64"
53        } else {
54            "string"
55        }
56    }
57
58    fn infer_value_type(value: &ValueNode<'_>) -> std::borrow::Cow<'static, str> {
59        match value {
60            ValueNode::Literal(literal) => Self::infer_literal_type(literal.as_ref()).into(),
61            ValueNode::Object(_) => "object".into(),
62            ValueNode::List(items) => {
63                let inner = Self::infer_list_element_type(items);
64                if inner == "any" {
65                    "list".into()
66                } else {
67                    format!("list<{inner}>").into()
68                }
69            }
70        }
71    }
72
73    fn infer_list_element_type(items: &[ValueNode<'_>]) -> std::borrow::Cow<'static, str> {
74        let mut inferred: Option<std::borrow::Cow<'static, str>> = None;
75        for item in items {
76            let current = Self::infer_value_type(item);
77            match &inferred {
78                None => inferred = Some(current),
79                Some(existing) if *existing == current => {}
80                _ => return "any".into(),
81            }
82        }
83
84        inferred.unwrap_or_else(|| "any".into())
85    }
86
87    fn build_value_tasks<'a>(
88        tasks: &mut Vec<ValidationTask<'a>>,
89        key: &str,
90        value: &'a ValueNode<'a>,
91        line: usize,
92    ) {
93        match value {
94            ValueNode::Literal(_s) => {
95                // Literal type checks are deferred until execution when schema/type context is available.
96            }
97            ValueNode::Object(pairs) => {
98                tasks.push(ValidationTask::ValidateObjectStructure {
99                    key: key.to_string().into(),
100                    pairs: pairs.clone(),
101                    line,
102                });
103            }
104            ValueNode::List(items) => {
105                let inferred_type = Self::infer_list_element_type(items);
106                tasks.push(ValidationTask::ValidateListElements {
107                    key: key.to_string().into(),
108                    items: items.clone(),
109                    element_type: inferred_type,
110                    line,
111                });
112            }
113        }
114    }
115
116    /// Generates validation tasks for an assignment node.
117    fn generate_assignment_tasks<'a>(
118        key: &std::borrow::Cow<'a, str>,
119        value: &'a ValueNode<'a>,
120        line: usize,
121    ) -> Vec<ValidationTask<'a>> {
122        let mut tasks = Vec::new();
123        Self::build_value_tasks(&mut tasks, key, value, line);
124
125        // Check for circular references
126        tasks.push(ValidationTask::CheckNoCircularReference {
127            key: key.to_string().into(),
128            line,
129        });
130
131        tasks
132    }
133
134    /// Generates validation tasks for a directive node.
135    fn generate_directive_tasks<'a>(
136        name: &std::borrow::Cow<'a, str>,
137        args: std::borrow::Cow<'a, str>,
138        line: usize,
139    ) -> Vec<ValidationTask<'a>> {
140        if name.as_ref() == "derive" {
141            return vec![ValidationTask::CheckDeriveCompleteness {
142                derive_path: args,
143                current_key: std::borrow::Cow::Borrowed(""),
144                line,
145            }];
146        }
147
148        // Directives are validated and executed in the execution phase.
149        // The validator stage only needs to add extra checks to derive.
150        Vec::new()
151    }
152}
153
154impl Default for DefaultValidator {
155    fn default() -> Self {
156        Self::new()
157    }
158}
159
160impl Validator for DefaultValidator {
161    fn validate<'a>(&self, ast: &'a [AstNode<'a>]) -> Result<Vec<ValidationTask<'a>>, AamlError> {
162        // First, perform syntactic checks
163        self.check_syntax(ast)?;
164
165        // Now generate validation tasks for deferred execution
166        let mut tasks = Vec::new();
167
168        for node in ast {
169            match node {
170                AstNode::Assignment { key, value, line } => {
171                    let node_tasks = Self::generate_assignment_tasks(key, value, *line);
172                    tasks.extend(node_tasks);
173                }
174                AstNode::Directive {
175                    name,
176                    args,
177                    line,
178                    body: _,
179                } => {
180                    let node_tasks = Self::generate_directive_tasks(name, args.clone(), *line);
181                    tasks.extend(node_tasks);
182                }
183            }
184        }
185
186        Ok(tasks)
187    }
188
189    fn check_syntax(&self, ast: &[AstNode<'_>]) -> Result<(), AamlError> {
190        for node in ast {
191            match node {
192                AstNode::Assignment { key, value, line } => {
193                    if key.is_empty() {
194                        return Err(AamlError::ParseError {
195                            line: *line,
196                            content: format!("= {value}"),
197                            details: "Empty key in assignment".to_string(),
198                            diagnostics: Some(Box::new(ErrorDiagnostics::new(
199                                "Empty key",
200                                "Assignment keys must be non-empty".to_string(),
201                                "Provide a valid key name".to_string(),
202                            ))),
203                        });
204                    }
205                }
206                AstNode::Directive { name, line, .. } => {
207                    if name.is_empty() {
208                        return Err(AamlError::ParseError {
209                            line: *line,
210                            content: "@".to_string(),
211                            details: "Empty directive name".to_string(),
212                            diagnostics: None,
213                        });
214                    }
215                }
216            }
217        }
218
219        Ok(())
220    }
221}
222
223#[cfg(test)]
224mod tests {
225    use super::*;
226
227    #[test]
228    fn test_validate_simple_assignment() {
229        let node = AstNode::Assignment {
230            key: "host".to_string().into(),
231            value: ValueNode::Literal("localhost".to_string().into()),
232            line: 1,
233        };
234        let validator = DefaultValidator::new();
235        let nodes = [node];
236        let tasks = validator.validate(&nodes).unwrap();
237        assert!(!tasks.is_empty()); // Should generate validation tasks
238    }
239
240    #[test]
241    fn test_validate_empty_key() {
242        let node = AstNode::Assignment {
243            key: String::new().into(),
244            value: ValueNode::Literal("value".to_string().into()),
245            line: 1,
246        };
247        let validator = DefaultValidator::new();
248        assert!(validator.check_syntax(&[node]).is_err());
249    }
250
251    #[test]
252    fn test_generate_tasks_for_list_value() {
253        let node = AstNode::Assignment {
254            key: "items".to_string().into(),
255            value: ValueNode::List(
256                vec![
257                    ValueNode::Literal("a".to_string().into()),
258                    ValueNode::Literal("b".to_string().into()),
259                ]
260                .into(),
261            ),
262            line: 1,
263        };
264        let validator = DefaultValidator::new();
265        let nodes = [node];
266        let tasks = validator.validate(&nodes).unwrap();
267
268        // Should have generated ValidateListElements task among others
269        let has_list_task = tasks
270            .iter()
271            .any(|t| matches!(t, ValidationTask::ValidateListElements { .. }));
272        assert!(has_list_task);
273    }
274
275    #[test]
276    fn test_generate_tasks_for_object_value() {
277        let node = AstNode::Assignment {
278            key: "config".to_string().into(),
279            value: ValueNode::Object(
280                vec![(
281                    "foo".to_string().into(),
282                    ValueNode::Literal("bar".to_string().into()),
283                )]
284                .into(),
285            ),
286            line: 1,
287        };
288        let validator = DefaultValidator::new();
289        let nodes = [node];
290        let tasks = validator.validate(&nodes).unwrap();
291
292        // Should have generated ValidateObjectStructure task among others
293        let has_object_task = tasks
294            .iter()
295            .any(|t| matches!(t, ValidationTask::ValidateObjectStructure { .. }));
296        assert!(has_object_task);
297    }
298}