use crate::error::{AamlError, ErrorDiagnostics};
use crate::pipeline::parser::{AstNode, ValueNode};
use crate::pipeline::tasks::ValidationTask;
pub trait Validator: Send + Sync {
fn validate<'a>(&self, ast: &[AstNode<'a>]) -> Result<Vec<ValidationTask<'a>>, AamlError>;
fn check_syntax<'a>(&self, ast: &[AstNode<'a>]) -> Result<(), AamlError>;
}
pub struct DefaultValidator;
impl DefaultValidator {
pub fn new() -> Self {
Self
}
fn infer_literal_type(value: &str) -> &'static str {
if value == "true" || value == "false" {
"bool"
} else if value.parse::<f64>().is_ok() {
"f64"
} else {
"string"
}
}
fn infer_value_type<'a>(value: &ValueNode<'a>) -> std::borrow::Cow<'static, str> {
match value {
ValueNode::Literal(literal) => Self::infer_literal_type(literal.as_ref()).into(),
ValueNode::Object(_) => "object".into(),
ValueNode::List(items) => {
let inner = Self::infer_list_element_type(items);
if inner == "any" {
"list".into()
} else {
format!("list<{}>", inner).into()
}
}
}
}
fn infer_list_element_type<'a>(items: &[ValueNode<'a>]) -> std::borrow::Cow<'static, str> {
let mut inferred: Option<std::borrow::Cow<'static, str>> = None;
for item in items {
let current = Self::infer_value_type(item);
match &inferred {
None => inferred = Some(current),
Some(existing) if *existing == current => {}
_ => return "any".into(),
}
}
inferred.unwrap_or_else(|| "any".into())
}
fn build_value_tasks<'a>(
tasks: &mut Vec<ValidationTask<'a>>,
key: &std::borrow::Cow<'a, str>,
value: &ValueNode<'a>,
line: usize,
) {
match value {
ValueNode::Literal(_s) => {
}
ValueNode::Object(pairs) => {
tasks.push(ValidationTask::ValidateObjectStructure {
key: key.to_string().into(),
pairs: pairs.clone(),
line,
});
}
ValueNode::List(items) => {
let inferred_type = Self::infer_list_element_type(items);
tasks.push(ValidationTask::ValidateListElements {
key: key.to_string().into(),
items: items.clone(),
element_type: inferred_type,
line,
});
}
}
}
fn generate_assignment_tasks<'a>(
key: std::borrow::Cow<'a, str>,
value: &ValueNode<'a>,
line: usize,
) -> Vec<ValidationTask<'a>> {
let mut tasks = Vec::new();
Self::build_value_tasks(&mut tasks, &key, value, line);
tasks.push(ValidationTask::CheckNoCircularReference {
key: key.to_string().into(),
line,
});
tasks
}
fn generate_directive_tasks<'a>(
name: std::borrow::Cow<'a, str>,
args: std::borrow::Cow<'a, str>,
line: usize,
) -> Vec<ValidationTask<'a>> {
let mut tasks = Vec::new();
match name.as_ref() {
"import" => {
if !args.is_empty() {
tasks.push(ValidationTask::VerifyFileExists {
path: args.to_string().into(),
line,
});
}
}
"derive" => {
tasks.push(ValidationTask::CheckDeriveCompleteness {
derive_path: args.to_string().into(),
current_key: std::borrow::Cow::Borrowed(""), line,
});
}
"schema" | "type" => {
}
_ => {
}
}
tasks
}
}
impl Default for DefaultValidator {
fn default() -> Self {
Self::new()
}
}
impl Validator for DefaultValidator {
fn validate<'a>(&self, ast: &[AstNode<'a>]) -> Result<Vec<ValidationTask<'a>>, AamlError> {
self.check_syntax(ast)?;
let mut tasks = Vec::new();
for node in ast {
match node {
AstNode::Assignment { key, value, line } => {
let node_tasks = Self::generate_assignment_tasks(key.clone(), value, *line);
tasks.extend(node_tasks);
}
AstNode::Directive {
name,
args,
line,
body: _,
} => {
let node_tasks =
Self::generate_directive_tasks(name.clone(), args.clone(), *line);
tasks.extend(node_tasks);
}
}
}
Ok(tasks)
}
fn check_syntax<'a>(&self, ast: &[AstNode<'a>]) -> Result<(), AamlError> {
for node in ast {
match node {
AstNode::Assignment { key, value, line } => {
if key.is_empty() {
return Err(AamlError::ParseError {
line: *line,
content: format!("= {}", value.to_string()),
details: "Empty key in assignment".to_string(),
diagnostics: Some(ErrorDiagnostics::new(
"Empty key",
"Assignment keys must be non-empty".to_string(),
"Provide a valid key name".to_string(),
)),
});
}
}
AstNode::Directive {
name,
args: _,
line,
body: _,
} => {
if name.is_empty() {
return Err(AamlError::ParseError {
line: *line,
content: "@".to_string(),
details: "Empty directive name".to_string(),
diagnostics: None,
});
}
}
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validate_simple_assignment() {
let node = AstNode::Assignment {
key: "host".to_string().into(),
value: ValueNode::Literal("localhost".to_string().into()),
line: 1,
};
let validator = DefaultValidator::new();
let tasks = validator.validate(&[node]).unwrap();
assert!(!tasks.is_empty()); }
#[test]
fn test_validate_empty_key() {
let node = AstNode::Assignment {
key: "".to_string().into(),
value: ValueNode::Literal("value".to_string().into()),
line: 1,
};
let validator = DefaultValidator::new();
assert!(validator.check_syntax(&[node]).is_err());
}
#[test]
fn test_generate_tasks_for_list_value() {
let node = AstNode::Assignment {
key: "items".to_string().into(),
value: ValueNode::List(
vec![
ValueNode::Literal("a".to_string().into()),
ValueNode::Literal("b".to_string().into()),
]
.into(),
),
line: 1,
};
let validator = DefaultValidator::new();
let tasks = validator.validate(&[node]).unwrap();
let has_list_task = tasks
.iter()
.any(|t| matches!(t, ValidationTask::ValidateListElements { .. }));
assert!(has_list_task);
}
#[test]
fn test_generate_tasks_for_object_value() {
let node = AstNode::Assignment {
key: "config".to_string().into(),
value: ValueNode::Object(
vec![(
"foo".to_string().into(),
ValueNode::Literal("bar".to_string().into()),
)]
.into(),
),
line: 1,
};
let validator = DefaultValidator::new();
let tasks = validator.validate(&[node]).unwrap();
let has_object_task = tasks
.iter()
.any(|t| matches!(t, ValidationTask::ValidateObjectStructure { .. }));
assert!(has_object_task);
}
}