use serde_json::Value;
use crate::merge::path::{CanonicalPath, build_path_map};
use crate::merge::sdl::{IdentityRule, PropertyType, SdlDocument};
use crate::validation::{
ValidationError, ValidationResult, check_required_paths, check_type_match,
};
pub fn validate_manifest(schema: &SdlDocument, manifest: &Value) -> ValidationResult {
let mut errors = Vec::new();
errors.extend(check_required_paths(schema, manifest));
for (path, def) in &schema.schema.properties {
match CanonicalPath::parse(path) {
Ok(canonical_path) => {
let value = crate::merge::path::resolve_path(manifest, &canonical_path);
if let Some(val) = value {
if let Some(err) = check_type_match(&def.type_, val, path) {
errors.push(err);
}
if matches!(def.type_, PropertyType::Array)
&& let Some(ref identity) = def.merge.identity
{
errors.extend(check_array_identity(val, path, identity));
}
}
}
Err(_) => {
let path_map = build_path_map(manifest, schema);
if let Some(val) = path_map.get(path)
&& let Some(err) = check_type_match(&def.type_, val, path)
{
errors.push(err);
}
}
}
}
if errors.is_empty() {
Ok(())
} else {
Err(errors)
}
}
fn check_array_identity(
value: &Value,
path: &str,
identity: &IdentityRule,
) -> Vec<ValidationError> {
let mut errors = Vec::new();
match identity {
IdentityRule::PrimitiveValue => {
}
IdentityRule::Key { key } => {
if let Value::Array(arr) = value {
for (idx, item) in arr.iter().enumerate() {
if let Value::Object(obj) = item {
if !obj.contains_key(key) {
errors.push(ValidationError::new(
format!(
"array item at index {} is missing identity key '{}'",
idx, key
),
format!("{}[{}]", path, idx),
));
}
} else if !item.is_null() {
errors.push(ValidationError::new(
format!(
"array item at index {} is not an object (expected identity key '{}')",
idx, key
),
format!("{}[{}]", path, idx),
));
}
}
}
}
}
errors
}
#[cfg(test)]
mod tests {
use super::*;
use crate::merge::sdl::SdlDocument;
use serde_json::json;
fn test_sdl() -> SdlDocument {
SdlDocument::from_yaml(
r#"
schema:
version: "1.0"
required:
- id
properties:
id:
type: string
merge:
type: replace
name:
type: string
merge:
type: replace
version:
type: number
merge:
type: atomic
tags:
type: array
merge:
type: ordered_unique
identity:
mode: primitive_value
characters:
type: array
merge:
type: ordered_unique
identity:
mode: key
key: id
"#,
)
.unwrap()
}
#[test]
fn test_validate_valid_manifest() {
let manifest = json!({
"id": "nap://test/char/luke",
"name": "Luke Skywalker",
"version": 1,
"tags": ["hero", "jedi"],
"characters": [
{"id": "obiwan", "name": "Obi-Wan"}
]
});
assert!(validate_manifest(&test_sdl(), &manifest).is_ok());
}
#[test]
fn test_validate_missing_required() {
let manifest = json!({
"name": "Luke"
});
let result = validate_manifest(&test_sdl(), &manifest);
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.message.contains("required")));
}
#[test]
fn test_validate_type_mismatch() {
let manifest = json!({
"id": "nap://test/char/luke",
"version": "not-a-number" });
let result = validate_manifest(&test_sdl(), &manifest);
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.message.contains("type mismatch")));
}
#[test]
fn test_validate_missing_identity_key_in_array() {
let manifest = json!({
"id": "nap://test/char/luke",
"characters": [
{"name": "Obi-Wan"} ]
});
let result = validate_manifest(&test_sdl(), &manifest);
assert!(result.is_err());
let errors = result.unwrap_err();
assert!(errors.iter().any(|e| e.message.contains("identity key")));
}
}