1use crate::error::{AamlError, ErrorDiagnostics};
8use crate::pipeline::parser::{AstNode, ValueNode};
9use crate::pipeline::tasks::ValidationTask;
10
11pub trait Validator: Send + Sync {
16 fn validate<'a>(&self, ast: &'a [AstNode<'a>]) -> Result<Vec<ValidationTask<'a>>, AamlError>;
29
30 fn check_syntax(&self, ast: &[AstNode<'_>]) -> Result<(), AamlError>;
34}
35
36pub 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 }
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 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 tasks.push(ValidationTask::CheckNoCircularReference {
127 key: key.to_string().into(),
128 line,
129 });
130
131 tasks
132 }
133
134 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 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 self.check_syntax(ast)?;
164
165 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()); }
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 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 let has_object_task = tasks
294 .iter()
295 .any(|t| matches!(t, ValidationTask::ValidateObjectStructure { .. }));
296 assert!(has_object_task);
297 }
298}