pub mod manifest;
pub mod merge;
pub mod sdl;
use crate::merge::conflict::MergeResult;
use crate::merge::sdl::SdlDocument;
use serde_json::Value;
#[derive(Debug, Clone)]
pub struct ValidationError {
pub message: String,
pub path: Option<String>,
}
impl std::fmt::Display for ValidationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &self.path {
Some(p) => write!(f, "{}: {}", p, self.message),
None => write!(f, "{}", self.message),
}
}
}
pub type ValidationResult = Result<(), Vec<ValidationError>>;
pub fn validate_before_persist(schema: &SdlDocument, merged: &MergeResult) -> ValidationResult {
match merged {
MergeResult::Conflicts(_) => Err(vec![ValidationError {
message: "cannot persist — merge produced unresolved conflicts".to_string(),
path: None,
}]),
MergeResult::Merged(value) => merge::validate_merged(schema, value),
}
}
pub fn check_required_paths(schema: &SdlDocument, value: &Value) -> Vec<ValidationError> {
let mut errors = Vec::new();
for required_path in &schema.schema.required {
match crate::merge::path::CanonicalPath::parse(required_path) {
Ok(path) => {
if crate::merge::path::resolve_path(value, &path).is_none() {
errors.push(ValidationError {
message: format!("required field '{}' is missing", required_path),
path: Some(required_path.clone()),
});
}
}
Err(_) => {
errors.push(ValidationError {
message: format!("invalid required path '{}'", required_path),
path: Some(required_path.clone()),
});
}
}
}
errors
}
pub fn check_type_match(
type_: &crate::merge::sdl::PropertyType,
value: &Value,
path: &str,
) -> Option<ValidationError> {
if value.is_null() {
return None;
}
let type_matches = match type_ {
crate::merge::sdl::PropertyType::String => value.is_string(),
crate::merge::sdl::PropertyType::Number => value.is_number(),
crate::merge::sdl::PropertyType::Boolean => value.is_boolean(),
crate::merge::sdl::PropertyType::Object => value.is_object(),
crate::merge::sdl::PropertyType::Array => value.is_array(),
};
if !type_matches {
Some(ValidationError {
message: format!(
"type mismatch: expected {:?}, got {}",
type_,
type_name(value)
),
path: Some(path.to_string()),
})
} else {
None
}
}
fn type_name(value: &Value) -> &'static str {
match value {
Value::Null => "null",
Value::Bool(_) => "boolean",
Value::Number(_) => "number",
Value::String(_) => "string",
Value::Array(_) => "array",
Value::Object(_) => "object",
}
}
impl ValidationError {
pub fn new(message: impl Into<String>, path: impl Into<String>) -> Self {
ValidationError {
message: message.into(),
path: Some(path.into()),
}
}
pub fn global(message: impl Into<String>) -> Self {
ValidationError {
message: message.into(),
path: None,
}
}
}
pub fn combine(results: Vec<ValidationResult>) -> ValidationResult {
let mut all_errors = Vec::new();
for result in results {
match result {
Ok(()) => {}
Err(errors) => all_errors.extend(errors),
}
}
if all_errors.is_empty() {
Ok(())
} else {
Err(all_errors)
}
}