use std::collections::HashSet;
use crate::diagnostics::{
codes, com_error, validation_error, Diagnostic, DiagnosticCategory, DiagnosticReport,
DiagnosticStage,
};
use crate::model::TransformationContract;
pub struct ValidationContext {
report: DiagnosticReport,
}
impl ValidationContext {
#[must_use]
pub fn new() -> Self {
Self {
report: DiagnosticReport::new(),
}
}
#[must_use]
pub fn into_report(self) -> DiagnosticReport {
self.report
}
pub fn push(&mut self, diagnostic: Diagnostic) {
self.report.push(diagnostic);
}
pub fn error(
&mut self,
id: &str,
category: DiagnosticCategory,
message: impl Into<String>,
object_ref: Option<&str>,
remediation: Option<&str>,
) {
self.error_with_stage(
id,
category,
message,
object_ref,
remediation,
DiagnosticStage::Validation,
);
}
pub fn error_with_stage(
&mut self,
id: &str,
category: DiagnosticCategory,
message: impl Into<String>,
object_ref: Option<&str>,
remediation: Option<&str>,
stage: DiagnosticStage,
) {
let mut diagnostic = match stage {
DiagnosticStage::CanonicalObjectModel => com_error(id, category, message),
_ => validation_error(id, category, message),
};
diagnostic.stage = stage;
if let Some(object_ref) = object_ref {
diagnostic = diagnostic.with_object_ref(object_ref);
}
if let Some(remediation) = remediation {
diagnostic = diagnostic.with_remediation(remediation);
}
self.push(diagnostic);
}
pub fn check_unique_ids(
&mut self,
ids: impl IntoIterator<Item = (String, String)>,
collection: &str,
) {
let mut seen = HashSet::new();
for (id, object_ref) in ids {
if id.trim().is_empty() {
self.error(
codes::MISSING_REQUIRED_FIELD,
DiagnosticCategory::Structure,
format!("{collection} entry is missing a non-empty id"),
Some(&object_ref),
Some("Provide a stable identifier for every object"),
);
continue;
}
if !seen.insert(id.clone()) {
self.error(
codes::DUPLICATE_IDENTIFIER,
DiagnosticCategory::Structure,
format!("duplicate identifier '{id}' in {collection}"),
Some(&object_ref),
Some("Use unique identifiers within each collection"),
);
}
}
}
}
impl Default for ValidationContext {
fn default() -> Self {
Self::new()
}
}
pub fn object_refs(contract: &TransformationContract) -> Vec<(String, String)> {
let mut refs = Vec::new();
for input in &contract.inputs {
refs.push((input.id.clone(), format!("inputs.{}", input.id)));
}
for output in &contract.outputs {
refs.push((output.id.clone(), format!("outputs.{}", output.id)));
}
for action in &contract.semantic_actions {
refs.push((action.id.clone(), format!("semanticActions.{}", action.id)));
}
for expression in &contract.expressions {
refs.push((
expression.id.clone(),
format!("expressions.{}", expression.id),
));
}
for function in &contract.functions {
refs.push((function.id.clone(), format!("functions.{}", function.id)));
}
for rule in &contract.rules {
refs.push((rule.id.clone(), format!("rules.{}", rule.id)));
}
refs
}
#[must_use]
pub fn is_namespaced_identifier(identifier: &str) -> bool {
identifier.contains(':')
}
pub fn validate_extension_keys(ctx: &mut ValidationContext, contract: &TransformationContract) {
for key in contract.extensions.keys() {
if !is_namespaced_identifier(key) {
ctx.error(
codes::UNKNOWN_FIELD,
DiagnosticCategory::Structure,
format!("unknown top-level field '{key}'"),
Some(key),
Some("Check for typos in standard field names such as inputs or outputs"),
);
}
}
}