From d77765cb6147d33721017d6e337ca1d93bbe9b07 Mon Sep 17 00:00:00 2001 From: Alex Groleau Date: Tue, 23 Jun 2026 17:03:27 -0400 Subject: [PATCH] jspg error refactoring checkpoint --- src/database/compile/collection.rs | 23 +++++---- src/database/compile/mod.rs | 13 +++-- src/database/compile/polymorphism.rs | 34 +++++++------ src/database/compose/mod.rs | 19 ++++--- src/database/mod.rs | 76 ++++++++++++++++------------ src/drop.rs | 5 +- src/lib.rs | 2 +- src/merger/mod.rs | 32 +++++++----- src/queryer/mod.rs | 42 +++++++++------ src/tests/mod.rs | 9 ++-- src/tests/runner.rs | 23 ++++++++- src/tests/types/case.rs | 13 +++-- src/validator/error.rs | 5 +- src/validator/mod.rs | 29 ++++++----- src/validator/rules/array.rs | 28 +++++++--- src/validator/rules/core.rs | 18 +++++-- src/validator/rules/extensible.rs | 10 +++- src/validator/rules/format.rs | 11 +++- src/validator/rules/mod.rs | 7 ++- src/validator/rules/not.rs | 2 +- src/validator/rules/numeric.rs | 27 ++++++++-- src/validator/rules/object.rs | 46 ++++++++++------- src/validator/rules/polymorphism.rs | 45 ++++++++-------- src/validator/rules/string.rs | 20 ++++++-- src/validator/rules/type.rs | 41 ++++++++------- 25 files changed, 362 insertions(+), 218 deletions(-) diff --git a/src/database/compile/collection.rs b/src/database/compile/collection.rs index fb88623..17530ad 100644 --- a/src/database/compile/collection.rs +++ b/src/database/compile/collection.rs @@ -1,4 +1,8 @@ use crate::database::schema::Schema; +#[allow(unused_imports)] +use crate::drop::{Error, ErrorDetails}; +#[allow(unused_imports)] +use std::collections::HashMap; use std::sync::Arc; impl Schema { @@ -8,18 +12,19 @@ impl Schema { field_name: &str, root_id: &str, path: &str, - errors: &mut Vec, + errors: &mut Vec, ) { #[cfg(not(test))] for c in id.chars() { if !c.is_ascii_lowercase() && !c.is_ascii_digit() && c != '_' && c != '.' && c != '$' { - errors.push(crate::drop::Error { + errors.push(Error { code: "INVALID_IDENTIFIER".to_string(), - message: format!( - "Invalid character '{}' in JSON Schema '{}' property: '{}'. Identifiers must exclusively contain [a-z0-9_.$]", - c, field_name, id - ), - details: crate::drop::ErrorDetails { + values: Some(HashMap::from([ + ("character".to_string(), c.to_string()), + ("field_name".to_string(), field_name.to_string()), + ("identifier".to_string(), id.to_string()), + ])), + details: ErrorDetails { path: Some(path.to_string()), schema: Some(root_id.to_string()), ..Default::default() @@ -35,7 +40,7 @@ impl Schema { root_id: &str, path: String, to_insert: &mut Vec<(String, Arc)>, - errors: &mut Vec, + errors: &mut Vec, ) { if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &schema_arc.obj.type_ { if t == "array" { @@ -70,7 +75,7 @@ impl Schema { root_id: &str, path: String, to_insert: &mut Vec<(String, Arc)>, - errors: &mut Vec, + errors: &mut Vec, ) { if let Some(props) = &schema_arc.obj.properties { for (k, v) in props.iter() { diff --git a/src/database/compile/mod.rs b/src/database/compile/mod.rs index c5c3ef9..55c67f5 100644 --- a/src/database/compile/mod.rs +++ b/src/database/compile/mod.rs @@ -5,7 +5,9 @@ pub mod filter; pub mod polymorphism; use crate::database::schema::Schema; +use crate::drop::{Error, ErrorDetails}; use indexmap::IndexMap; +use std::collections::HashMap; impl Schema { pub fn compile( @@ -13,7 +15,7 @@ impl Schema { db: &crate::database::Database, root_id: &str, path: String, - errors: &mut Vec, + errors: &mut Vec, ) { if self.obj.compiled_properties.get().is_some() { return; @@ -72,13 +74,10 @@ impl Schema { } if custom_type_count > 1 { - errors.push(crate::drop::Error { + errors.push(Error { code: "MULTIPLE_INHERITANCE_PROHIBITED".to_string(), - message: format!( - "Schema attempts to extend multiple custom object pointers in its type array {:?}. Use 'oneOf' for polymorphism and tagged unions.", - types - ), - details: crate::drop::ErrorDetails { + values: Some(HashMap::from([("types".to_string(), types.join(", "))])), + details: ErrorDetails { path: Some(path.clone()), schema: Some(root_id.to_string()), ..Default::default() diff --git a/src/database/compile/polymorphism.rs b/src/database/compile/polymorphism.rs index fce6bab..abb0e48 100644 --- a/src/database/compile/polymorphism.rs +++ b/src/database/compile/polymorphism.rs @@ -1,4 +1,6 @@ +use crate::drop::{Error, ErrorDetails}; use indexmap::IndexSet; +use std::collections::HashMap; use crate::database::schema::Schema; impl Schema { @@ -7,7 +9,7 @@ impl Schema { db: &crate::database::Database, root_id: &str, path: &str, - errors: &mut Vec, + errors: &mut Vec, ) { let mut options = indexmap::IndexMap::new(); let strategy: &str; @@ -118,16 +120,16 @@ impl Schema { }; if strategy.is_empty() { - errors.push(crate::drop::Error { - code: "AMBIGUOUS_POLYMORPHISM".to_string(), - message: format!("oneOf boundaries must map mathematically unique 'type' or 'kind' discriminators, or strictly contain disjoint primitive types."), - details: crate::drop::ErrorDetails { - path: Some(path.to_string()), - schema: Some(root_id.to_string()), - ..Default::default() - } - }); - return; + errors.push(Error { + code: "AMBIGUOUS_POLYMORPHISM".to_string(), + values: None, + details: ErrorDetails { + path: Some(path.to_string()), + schema: Some(root_id.to_string()), + ..Default::default() + } + }); + return; } for (i, c) in one_of.iter().enumerate() { @@ -140,15 +142,15 @@ impl Schema { if let Some(val) = c.obj.get_discriminator_value(&strategy, &child_id) { if options.contains_key(&val) { - errors.push(crate::drop::Error { + errors.push(Error { code: "POLYMORPHIC_COLLISION".to_string(), - message: format!("Polymorphic boundary defines multiple candidates mapped to the identical discriminator value '{}'.", val), - details: crate::drop::ErrorDetails { + values: Some(HashMap::from([("value".to_string(), val.to_string())])), + details: ErrorDetails { path: Some(path.to_string()), schema: Some(root_id.to_string()), ..Default::default() - } - }); + } + }); continue; } diff --git a/src/database/compose/mod.rs b/src/database/compose/mod.rs index 82f0439..815d054 100644 --- a/src/database/compose/mod.rs +++ b/src/database/compose/mod.rs @@ -1,7 +1,8 @@ +use crate::drop::{Error, ErrorDetails}; use serde_json::Value; use std::collections::{HashMap, HashSet}; -pub fn compose(val: &mut Value, errors: &mut Vec) -> Result<(), String> { +pub fn compose(val: &mut Value, errors: &mut Vec) { let mut traits = HashMap::new(); let mut schemas = HashMap::new(); @@ -56,15 +57,13 @@ pub fn compose(val: &mut Value, errors: &mut Vec) -> Result< } } } - - Ok(()) } fn resolve_in_place( current: &mut Value, traits: &HashMap, schemas: &HashMap, - errors: &mut Vec, + errors: &mut Vec, schema_id: &str, path: &str, visited: &mut HashSet, @@ -118,10 +117,10 @@ fn resolve_in_place( for inc in include_arr { if let Some(inc_name) = inc.as_str() { if visited.contains(inc_name) { - errors.push(crate::drop::Error { + errors.push(Error { code: "CIRCULAR_INCLUDE_DETECTED".to_string(), - message: format!("Circular inclusion detected for '{}'", inc_name), - details: crate::drop::ErrorDetails { + values: Some(HashMap::from([("include".to_string(), inc_name.to_string())])), + details: ErrorDetails { schema: Some(schema_id.to_string()), path: Some(path.to_string()), ..Default::default() @@ -232,10 +231,10 @@ fn resolve_in_place( } } } else { - errors.push(crate::drop::Error { + errors.push(Error { code: "TRAIT_NOT_FOUND".to_string(), - message: format!("Trait or schema '{}' not found for inclusion", inc_name), - details: crate::drop::ErrorDetails { + values: Some(HashMap::from([("include".to_string(), inc_name.to_string())])), + details: ErrorDetails { schema: Some(schema_id.to_string()), path: Some(path.to_string()), ..Default::default() diff --git a/src/database/mod.rs b/src/database/mod.rs index 0c47295..1934cf4 100644 --- a/src/database/mod.rs +++ b/src/database/mod.rs @@ -28,6 +28,8 @@ use serde_json::Value; use indexmap::IndexMap; use std::sync::Arc; use r#type::Type; +use std::collections::HashMap; +use crate::drop::{Drop, Error, ErrorDetails}; #[derive(serde::Serialize)] pub struct Database { @@ -57,13 +59,7 @@ impl Database { let mut errors = Vec::new(); - if let Err(e) = compose::compose(&mut val, &mut errors) { - errors.push(crate::drop::Error { - code: "COMPOSE_FAILED".to_string(), - message: format!("Fatal error during trait composition: {}", e), - details: crate::drop::ErrorDetails::default(), - }); - } + compose::compose(&mut val, &mut errors); if let serde_json::Value::Object(mut map) = val { if let Some(serde_json::Value::Array(arr)) = map.remove("enums") { @@ -78,10 +74,13 @@ impl Database { db.enums.insert(def.name.clone(), def); } Err(e) => { - errors.push(crate::drop::Error { + errors.push(Error { code: "DATABASE_ENUM_PARSE_FAILED".to_string(), - message: format!("Failed to parse database enum '{}': {}", name, e), - details: crate::drop::ErrorDetails { + values: Some(HashMap::from([ + ("enum".to_string(), name.clone()), + ("reason".to_string(), e.to_string()), + ])), + details: ErrorDetails { context: Some(serde_json::json!(name)), ..Default::default() }, @@ -103,10 +102,13 @@ impl Database { db.types.insert(def.name.clone(), def); } Err(e) => { - errors.push(crate::drop::Error { + errors.push(Error { code: "DATABASE_TYPE_PARSE_FAILED".to_string(), - message: format!("Failed to parse database type '{}': {}", name, e), - details: crate::drop::ErrorDetails { + values: Some(HashMap::from([ + ("type".to_string(), name.clone()), + ("reason".to_string(), e.to_string()), + ])), + details: ErrorDetails { context: Some(serde_json::json!(name)), ..Default::default() }, @@ -132,10 +134,13 @@ impl Database { } } Err(e) => { - errors.push(crate::drop::Error { + errors.push(Error { code: "DATABASE_RELATION_PARSE_FAILED".to_string(), - message: format!("Failed to parse database relation '{}': {}", constraint, e), - details: crate::drop::ErrorDetails { + values: Some(HashMap::from([ + ("relation".to_string(), constraint.clone()), + ("reason".to_string(), e.to_string()), + ])), + details: ErrorDetails { context: Some(serde_json::json!(constraint)), ..Default::default() }, @@ -157,10 +162,13 @@ impl Database { db.puncs.insert(def.name.clone(), def); } Err(e) => { - errors.push(crate::drop::Error { + errors.push(Error { code: "DATABASE_PUNC_PARSE_FAILED".to_string(), - message: format!("Failed to parse database punc '{}': {}", name, e), - details: crate::drop::ErrorDetails { + values: Some(HashMap::from([ + ("punc".to_string(), name.clone()), + ("reason".to_string(), e.to_string()), + ])), + details: ErrorDetails { context: Some(serde_json::json!(name)), ..Default::default() }, @@ -173,9 +181,9 @@ impl Database { db.compile(&mut errors); let drop = if errors.is_empty() { - crate::drop::Drop::success() + Drop::success() } else { - crate::drop::Drop::with_errors(errors) + Drop::with_errors(errors) }; (db, drop) } @@ -443,7 +451,7 @@ impl Database { // Abort relation discovery early if no hierarchical inheritance match was found if matching_rels.is_empty() { - let mut details = crate::drop::ErrorDetails { + let mut details = ErrorDetails { path: Some(path.to_string()), ..Default::default() }; @@ -451,12 +459,13 @@ impl Database { details.schema = Some(sid.to_string()); } - errors.push(crate::drop::Error { + errors.push(Error { code: "EDGE_MISSING".to_string(), - message: format!( - "No database relation exists between '{}' and '{}' for property '{}'", - parent_type, child_type, prop_name - ), + values: Some(HashMap::from([ + ("parent_type".to_string(), parent_type.to_string()), + ("child_type".to_string(), child_type.to_string()), + ("property_name".to_string(), prop_name.to_string()), + ])), details, }); return None; @@ -542,7 +551,7 @@ impl Database { // we must abort rather than silently guessing. Returning None prevents arbitrary SQL generation // and forces a clean structural error for the architect. if !resolved { - let mut details = crate::drop::ErrorDetails { + let mut details = ErrorDetails { path: Some(path.to_string()), context: serde_json::to_value(&matching_rels).ok(), cause: Some("Multiple conflicting constraints found matching prefixes".to_string()), @@ -552,12 +561,13 @@ impl Database { details.schema = Some(sid.to_string()); } - errors.push(crate::drop::Error { + errors.push(Error { code: "AMBIGUOUS_TYPE_RELATIONS".to_string(), - message: format!( - "Ambiguous database relation between '{}' and '{}' for property '{}'", - parent_type, child_type, prop_name - ), + values: Some(HashMap::from([ + ("parent_type".to_string(), parent_type.to_string()), + ("child_type".to_string(), child_type.to_string()), + ("property_name".to_string(), prop_name.to_string()), + ])), details, }); return None; diff --git a/src/drop.rs b/src/drop.rs index 3300de7..b9aa676 100644 --- a/src/drop.rs +++ b/src/drop.rs @@ -57,10 +57,13 @@ impl Drop { } } +use std::collections::HashMap; + #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Error { pub code: String, - pub message: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub values: Option>, pub details: ErrorDetails, } diff --git a/src/lib.rs b/src/lib.rs index bf0d0d0..4a273de 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -31,7 +31,7 @@ lazy_static::lazy_static! { fn jspg_failure() -> JsonB { let error = crate::drop::Error { code: "ENGINE_NOT_INITIALIZED".to_string(), - message: "JSPG extension has not been initialized via jspg_setup".to_string(), + values: None, details: crate::drop::ErrorDetails { path: None, cause: None, diff --git a/src/merger/mod.rs b/src/merger/mod.rs index 127d705..ce5fcb2 100644 --- a/src/merger/mod.rs +++ b/src/merger/mod.rs @@ -5,7 +5,9 @@ pub mod cache; use crate::database::Database; use crate::database::r#type::Type; +use crate::drop::{Drop, Error, ErrorDetails}; use serde_json::Value; +use std::collections::HashMap; use std::sync::Arc; pub struct Merger { @@ -21,20 +23,22 @@ impl Merger { } } - pub fn merge(&self, schema_id: &str, data: Value) -> crate::drop::Drop { + pub fn merge(&self, schema_id: &str, data: Value) -> Drop { let mut notifications_queue = Vec::new(); let target_schema = match self.db.schemas.get(schema_id) { Some(s) => Arc::clone(&s), None => { - return crate::drop::Drop::with_errors(vec![crate::drop::Error { - code: "MERGE_FAILED".to_string(), - message: format!("Unknown schema_id: {}", schema_id), - details: crate::drop::ErrorDetails { + return Drop::with_errors(vec![Error { + code: "SCHEMA_NOT_FOUND".to_string(), + values: Some(HashMap::from([ + ("schema".to_string(), schema_id.to_string()), + ])), + details: ErrorDetails { path: None, cause: None, context: Some(data), - schema: None, + schema: Some(schema_id.to_string()), }, }]); } @@ -72,10 +76,12 @@ impl Merger { } } - return crate::drop::Drop::with_errors(vec![crate::drop::Error { + return Drop::with_errors(vec![Error { code: final_code, - message: final_message, - details: crate::drop::ErrorDetails { + values: Some(HashMap::from([ + ("error".to_string(), final_message), + ])), + details: ErrorDetails { path: None, cause: final_cause, context: None, @@ -88,10 +94,12 @@ impl Merger { // Execute the globally collected, pre-ordered notifications last! for notify_sql in notifications_queue { if let Err(e) = self.db.execute(¬ify_sql, None) { - return crate::drop::Drop::with_errors(vec![crate::drop::Error { + return Drop::with_errors(vec![Error { code: "MERGE_FAILED".to_string(), - message: format!("Executor Error in pre-ordered notify: {:?}", e), - details: crate::drop::ErrorDetails { + values: Some(HashMap::from([ + ("error".to_string(), e.to_string()), + ])), + details: ErrorDetails { path: None, cause: None, context: None, diff --git a/src/queryer/mod.rs b/src/queryer/mod.rs index 114ff12..f733a53 100644 --- a/src/queryer/mod.rs +++ b/src/queryer/mod.rs @@ -1,4 +1,6 @@ use crate::database::Database; +use crate::drop::{Drop, Error, ErrorDetails}; +use std::collections::HashMap; use std::sync::Arc; pub mod compiler; @@ -22,17 +24,19 @@ impl Queryer { &self, schema_id: &str, filter: Option<&serde_json::Value>, - ) -> crate::drop::Drop { + ) -> Drop { let filters_map = filter.and_then(|f| f.as_object()); // 1. Process filters into structured $op keys and linear values let (filter_keys, args) = match self.parse_filter_entries(filters_map) { Ok(res) => res, Err(msg) => { - return crate::drop::Drop::with_errors(vec![crate::drop::Error { + return Drop::with_errors(vec![Error { code: "FILTER_PARSE_FAILED".to_string(), - message: msg.clone(), - details: crate::drop::ErrorDetails { + values: Some(HashMap::from([ + ("error".to_string(), msg.clone()), + ])), + details: ErrorDetails { path: None, // filters apply to the root query cause: Some(msg), context: filter.cloned(), @@ -134,10 +138,12 @@ impl Queryer { .insert(cache_key.to_string(), compiled_sql.clone()); Ok(compiled_sql) } - Err(e) => Err(crate::drop::Drop::with_errors(vec![crate::drop::Error { + Err(e) => Err(Drop::with_errors(vec![Error { code: "QUERY_COMPILATION_FAILED".to_string(), - message: e.clone(), - details: crate::drop::ErrorDetails { + values: Some(HashMap::from([ + ("error".to_string(), e.clone()), + ])), + details: ErrorDetails { path: None, cause: Some(e), context: None, @@ -152,29 +158,33 @@ impl Queryer { schema_id: &str, sql: &str, args: Vec, - ) -> crate::drop::Drop { + ) -> Drop { match self.db.query(sql, Some(args)) { Ok(serde_json::Value::Array(table)) => { if table.is_empty() { - crate::drop::Drop::success_with_val(serde_json::Value::Null) + Drop::success_with_val(serde_json::Value::Null) } else { - crate::drop::Drop::success_with_val(table.first().unwrap().clone()) + Drop::success_with_val(table.first().unwrap().clone()) } } - Ok(other) => crate::drop::Drop::with_errors(vec![crate::drop::Error { + Ok(other) => Drop::with_errors(vec![Error { code: "QUERY_FAILED".to_string(), - message: format!("Expected array from generic query, got: {:?}", other), - details: crate::drop::ErrorDetails { + values: Some(HashMap::from([ + ("error".to_string(), format!("Expected array from generic query, got: {:?}", other)), + ])), + details: ErrorDetails { path: None, cause: Some(format!("Expected array, got {}", other)), context: Some(serde_json::json!([sql])), schema: Some(schema_id.to_string()), }, }]), - Err(e) => crate::drop::Drop::with_errors(vec![crate::drop::Error { + Err(e) => Drop::with_errors(vec![Error { code: "QUERY_FAILED".to_string(), - message: format!("SPI error in queryer: {}", e), - details: crate::drop::ErrorDetails { + values: Some(HashMap::from([ + ("error".to_string(), e.to_string()), + ])), + details: ErrorDetails { path: None, cause: Some(format!("SPI error in queryer: {}", e)), context: Some(serde_json::json!([sql])), diff --git a/src/tests/mod.rs b/src/tests/mod.rs index d8d07b4..fcee8b2 100644 --- a/src/tests/mod.rs +++ b/src/tests/mod.rs @@ -18,7 +18,6 @@ fn test_library_api() { "type": "drop", "errors": [{ "code": "ENGINE_NOT_INITIALIZED", - "message": "JSPG extension has not been initialized via jspg_setup", "details": {} }] }) @@ -250,7 +249,9 @@ fn test_library_api() { "errors": [ { "code": "REQUIRED_FIELD_MISSING", - "message": "Missing name", + "values": { + "field_name": "name" + }, "details": { "path": "name", "schema": "source_schema" @@ -258,7 +259,9 @@ fn test_library_api() { }, { "code": "STRICT_PROPERTY_VIOLATION", - "message": "Unexpected property 'wrong'", + "values": { + "property_name": "wrong" + }, "details": { "path": "wrong", "schema": "source_schema" diff --git a/src/tests/runner.rs b/src/tests/runner.rs index 119e9f2..d26009c 100644 --- a/src/tests/runner.rs +++ b/src/tests/runner.rs @@ -86,7 +86,7 @@ pub fn run_test_case(path: &str, suite_idx: usize, case_idx: usize) -> Result<() let error_messages: Vec = drop .errors .iter() - .map(|e| format!("Error {} at path {}: {}", e.code, e.details.path.as_deref().unwrap_or("/"), e.message)) + .map(|e| format!("Error {} at path {}: {:?}", e.code, e.details.path.as_deref().unwrap_or("/"), e.values)) .collect(); failures.push(format!( "[{}] Cannot run '{}' test '{}': System Setup Compilation structurally failed:\n{}", @@ -117,7 +117,7 @@ pub fn run_test_case(path: &str, suite_idx: usize, case_idx: usize) -> Result<() } } "validate" => { - let result = test.run_validate(db_unwrapped.unwrap()); + let result = test.run_validate(db_unwrapped.unwrap(), path, suite_idx, case_idx); if let Err(e) = result { println!("TEST VALIDATE ERROR FOR '{}': {}", test.description, e); failures.push(format!( @@ -205,6 +205,25 @@ pub fn canonicalize_with_map(s: &str, uuid_map: &HashMap, gen_ma ts_re.replace_all(&s1, "{{timestamp}}").to_string() } +pub fn update_validation_fixture(path: &str, suite_idx: usize, case_idx: usize, errors: &[crate::drop::Error]) { + let content = fs::read_to_string(path).unwrap(); + let mut file_data: Value = serde_json::from_str(&content).unwrap(); + + if let Some(expect) = file_data[suite_idx]["tests"][case_idx].get_mut("expect") { + if let Some(obj) = expect.as_object_mut() { + if errors.is_empty() { + obj.remove("errors"); + } else { + let serialized_errors = serde_json::to_value(errors).unwrap(); + obj.insert("errors".to_string(), serialized_errors); + } + } + } + + let formatted_json = serde_json::to_string_pretty(&file_data).unwrap(); + fs::write(path, formatted_json).unwrap(); +} + pub fn update_sql_fixture(path: &str, suite_idx: usize, case_idx: usize, queries: &[String]) { use crate::tests::formatter::SqlFormatter; let content = fs::read_to_string(path).unwrap(); diff --git a/src/tests/types/case.rs b/src/tests/types/case.rs index 5f46b62..0677795 100644 --- a/src/tests/types/case.rs +++ b/src/tests/types/case.rs @@ -1,7 +1,10 @@ use super::expect::Expect; use crate::database::Database; +use crate::tests::runner::update_validation_fixture; +use crate::validator::Validator; use serde::Deserialize; use serde_json::Value; +use std::env; use std::sync::Arc; #[derive(Debug, Deserialize)] @@ -58,16 +61,16 @@ impl Case { Ok(()) } - pub fn run_validate(&self, db: Arc) -> Result<(), String> { - use crate::validator::Validator; - + pub fn run_validate(&self, db: Arc, path: &str, suite_idx: usize, case_idx: usize) -> Result<(), String> { let validator = Validator::new(db); - let schema_id = &self.schema_id; - let test_data = self.data.clone().unwrap_or(Value::Null); let result = validator.validate(schema_id, &test_data); + if env::var("UPDATE_EXPECT").is_ok() { + update_validation_fixture(path, suite_idx, case_idx, &result.errors); + } + if let Some(expect) = &self.expect { expect.assert_drop(&result)?; } diff --git a/src/validator/error.rs b/src/validator/error.rs index 9e756d9..459dbec 100644 --- a/src/validator/error.rs +++ b/src/validator/error.rs @@ -1,6 +1,9 @@ +use std::collections::HashMap; + #[derive(Debug, Clone, serde::Serialize)] pub struct ValidationError { pub code: String, - pub message: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub values: Option>, pub path: String, } diff --git a/src/validator/mod.rs b/src/validator/mod.rs index a338918..47eb29a 100644 --- a/src/validator/mod.rs +++ b/src/validator/mod.rs @@ -1,4 +1,4 @@ -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; pub mod context; pub mod error; @@ -13,6 +13,7 @@ use crate::database::Database; use crate::validator::rules::util::is_integer; use serde_json::Value; use std::sync::Arc; +use crate::drop::{Drop, Error, ErrorDetails}; pub struct Validator { pub db: Arc, @@ -57,15 +58,15 @@ impl Validator { match ctx.validate_scoped() { Ok(result) => { if result.is_valid() { - crate::drop::Drop::success() + Drop::success() } else { - let errors: Vec = result + let errors: Vec = result .errors .into_iter() - .map(|e| crate::drop::Error { + .map(|e| Error { code: e.code, - message: e.message, - details: crate::drop::ErrorDetails { + values: e.values, + details: ErrorDetails { path: Some(e.path), cause: None, context: None, @@ -73,13 +74,13 @@ impl Validator { }, }) .collect(); - crate::drop::Drop::with_errors(errors) + Drop::with_errors(errors) } } - Err(e) => crate::drop::Drop::with_errors(vec![crate::drop::Error { + Err(e) => Drop::with_errors(vec![Error { code: e.code, - message: e.message, - details: crate::drop::ErrorDetails { + values: e.values, + details: ErrorDetails { path: Some(e.path), cause: None, context: None, @@ -88,10 +89,12 @@ impl Validator { }]), } } else { - crate::drop::Drop::with_errors(vec![crate::drop::Error { + Drop::with_errors(vec![Error { code: "SCHEMA_NOT_FOUND".to_string(), - message: format!("Schema {} not found", schema_id), - details: crate::drop::ErrorDetails { + values: Some(HashMap::from([ + ("schema".to_string(), schema_id.to_string()), + ])), + details: ErrorDetails { path: Some("/".to_string()), cause: None, context: None, diff --git a/src/validator/rules/array.rs b/src/validator/rules/array.rs index f3d73c8..c2c5b29 100644 --- a/src/validator/rules/array.rs +++ b/src/validator/rules/array.rs @@ -1,4 +1,4 @@ -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use serde_json::Value; @@ -17,8 +17,11 @@ impl<'a> ValidationContext<'a> { && (arr.len() as f64) < min { result.errors.push(ValidationError { - code: "MIN_ITEMS".to_string(), - message: "Too few items".to_string(), + code: "MIN_ITEMS_VIOLATED".to_string(), + values: Some(HashMap::from([ + ("count".to_string(), arr.len().to_string()), + ("limit".to_string(), min.to_string()), + ])), path: self.path.to_string(), }); } @@ -26,8 +29,11 @@ impl<'a> ValidationContext<'a> { && (arr.len() as f64) > max { result.errors.push(ValidationError { - code: "MAX_ITEMS".to_string(), - message: "Too many items".to_string(), + code: "MAX_ITEMS_VIOLATED".to_string(), + values: Some(HashMap::from([ + ("count".to_string(), arr.len().to_string()), + ("limit".to_string(), max.to_string()), + ])), path: self.path.to_string(), }); } @@ -38,7 +44,7 @@ impl<'a> ValidationContext<'a> { if seen.contains(&item) { result.errors.push(ValidationError { code: "UNIQUE_ITEMS_VIOLATED".to_string(), - message: "Array has duplicate items".to_string(), + values: None, path: self.path.to_string(), }); break; @@ -71,7 +77,10 @@ impl<'a> ValidationContext<'a> { if _match_count < min { result.errors.push(ValidationError { code: "CONTAINS_VIOLATED".to_string(), - message: format!("Contains matches {} < min {}", _match_count, min), + values: Some(HashMap::from([ + ("count".to_string(), _match_count.to_string()), + ("limit".to_string(), min.to_string()), + ])), path: self.path.to_string(), }); } @@ -80,7 +89,10 @@ impl<'a> ValidationContext<'a> { { result.errors.push(ValidationError { code: "CONTAINS_VIOLATED".to_string(), - message: format!("Contains matches {} > max {}", _match_count, max), + values: Some(HashMap::from([ + ("count".to_string(), _match_count.to_string()), + ("limit".to_string(), max.to_string()), + ])), path: self.path.to_string(), }); } diff --git a/src/validator/rules/core.rs b/src/validator/rules/core.rs index c8ed6fe..6b7d9e6 100644 --- a/src/validator/rules/core.rs +++ b/src/validator/rules/core.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use crate::validator::Validator; use crate::validator::context::ValidationContext; use crate::validator::error::ValidationError; @@ -17,7 +19,9 @@ impl<'a> ValidationContext<'a> { if !Validator::check_type(t, current) { result.errors.push(ValidationError { code: "INVALID_TYPE".to_string(), - message: format!("Expected type '{}'", t), + values: Some(HashMap::from([ + ("expected".to_string(), t.to_string()), + ])), path: self.path.to_string(), }); } @@ -33,7 +37,9 @@ impl<'a> ValidationContext<'a> { if !valid { result.errors.push(ValidationError { code: "INVALID_TYPE".to_string(), - message: format!("Expected one of types {:?}", types), + values: Some(HashMap::from([ + ("expected".to_string(), format!("{:?}", types)), + ])), path: self.path.to_string(), }); } @@ -45,7 +51,9 @@ impl<'a> ValidationContext<'a> { if !equals(current, const_val) { result.errors.push(ValidationError { code: "CONST_VIOLATED".to_string(), - message: "Value does not match const".to_string(), + values: Some(HashMap::from([ + ("expected".to_string(), format!("{:?}", const_val)), + ])), path: self.path.to_string(), }); } else if let Some(obj) = current.as_object() { @@ -66,7 +74,9 @@ impl<'a> ValidationContext<'a> { if !found { result.errors.push(ValidationError { code: "ENUM_MISMATCH".to_string(), - message: "Value is not in enum".to_string(), + values: Some(HashMap::from([ + ("expected".to_string(), format!("{:?}", enum_vals)), + ])), path: self.path.to_string(), }); } else if let Some(obj) = current.as_object() { diff --git a/src/validator/rules/extensible.rs b/src/validator/rules/extensible.rs index 25cd4dc..928e0bb 100644 --- a/src/validator/rules/extensible.rs +++ b/src/validator/rules/extensible.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use crate::validator::context::ValidationContext; use crate::validator::error::ValidationError; use crate::validator::result::ValidationResult; @@ -27,7 +29,9 @@ impl<'a> ValidationContext<'a> { if !result.evaluated_keys.contains(key) && !self.overrides.contains(key) { result.errors.push(ValidationError { code: "STRICT_PROPERTY_VIOLATION".to_string(), - message: format!("Unexpected property '{}'", key), + values: Some(HashMap::from([ + ("property_name".to_string(), key.to_string()), + ])), path: self.join_path(key), }); } @@ -47,7 +51,9 @@ impl<'a> ValidationContext<'a> { } result.errors.push(ValidationError { code: "STRICT_ITEM_VIOLATION".to_string(), - message: format!("Unexpected item at index {}", i), + values: Some(HashMap::from([ + ("index".to_string(), i.to_string()), + ])), path: item_path, }); } diff --git a/src/validator/rules/format.rs b/src/validator/rules/format.rs index 4d169f2..f6e3776 100644 --- a/src/validator/rules/format.rs +++ b/src/validator/rules/format.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use crate::validator::context::ValidationContext; use crate::validator::error::ValidationError; use crate::validator::result::ValidationResult; @@ -19,7 +21,10 @@ impl<'a> ValidationContext<'a> { if should && let Err(e) = f(current) { result.errors.push(ValidationError { code: "FORMAT_MISMATCH".to_string(), - message: format!("Format error: {}", e), + values: Some(HashMap::from([ + ("format".to_string(), self.schema.format.clone().unwrap_or_default()), + ("error".to_string(), e.to_string()), + ])), path: self.path.to_string(), }); } @@ -30,7 +35,9 @@ impl<'a> ValidationContext<'a> { { result.errors.push(ValidationError { code: "FORMAT_MISMATCH".to_string(), - message: "Format regex mismatch".to_string(), + values: Some(HashMap::from([ + ("format".to_string(), self.schema.format.clone().unwrap_or_default()), + ])), path: self.path.to_string(), }); } diff --git a/src/validator/rules/mod.rs b/src/validator/rules/mod.rs index 33b0c66..cf52232 100644 --- a/src/validator/rules/mod.rs +++ b/src/validator/rules/mod.rs @@ -1,6 +1,7 @@ use crate::validator::context::ValidationContext; use crate::validator::error::ValidationError; use crate::validator::result::ValidationResult; +use std::collections::HashMap; pub mod array; pub mod cases; @@ -61,7 +62,9 @@ impl<'a> ValidationContext<'a> { if self.depth > 100 { Err(ValidationError { code: "RECURSION_LIMIT_EXCEEDED".to_string(), - message: "Recursion limit exceeded".to_string(), + values: Some(HashMap::from([ + ("limit".to_string(), 100.to_string()), + ])), path: self.path.to_string(), }) } else { @@ -73,7 +76,7 @@ impl<'a> ValidationContext<'a> { if self.schema.always_fail { result.errors.push(ValidationError { code: "FALSE_SCHEMA".to_string(), - message: "Schema is false".to_string(), + values: None, path: self.path.to_string(), }); // Short-circuit diff --git a/src/validator/rules/not.rs b/src/validator/rules/not.rs index e92002b..c852876 100644 --- a/src/validator/rules/not.rs +++ b/src/validator/rules/not.rs @@ -13,7 +13,7 @@ impl<'a> ValidationContext<'a> { if sub_res.is_valid() { result.errors.push(ValidationError { code: "NOT_VIOLATED".to_string(), - message: "Matched 'not' schema".to_string(), + values: None, path: self.path.to_string(), }); } diff --git a/src/validator/rules/numeric.rs b/src/validator/rules/numeric.rs index 9dcc0c6..2210c51 100644 --- a/src/validator/rules/numeric.rs +++ b/src/validator/rules/numeric.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use crate::validator::context::ValidationContext; use crate::validator::error::ValidationError; use crate::validator::result::ValidationResult; @@ -14,7 +16,10 @@ impl<'a> ValidationContext<'a> { { result.errors.push(ValidationError { code: "MINIMUM_VIOLATED".to_string(), - message: format!("Value {} < min {}", num, min), + values: Some(HashMap::from([ + ("value".to_string(), num.to_string()), + ("limit".to_string(), min.to_string()), + ])), path: self.path.to_string(), }); } @@ -23,7 +28,10 @@ impl<'a> ValidationContext<'a> { { result.errors.push(ValidationError { code: "MAXIMUM_VIOLATED".to_string(), - message: format!("Value {} > max {}", num, max), + values: Some(HashMap::from([ + ("value".to_string(), num.to_string()), + ("limit".to_string(), max.to_string()), + ])), path: self.path.to_string(), }); } @@ -32,7 +40,10 @@ impl<'a> ValidationContext<'a> { { result.errors.push(ValidationError { code: "EXCLUSIVE_MINIMUM_VIOLATED".to_string(), - message: format!("Value {} <= ex_min {}", num, ex_min), + values: Some(HashMap::from([ + ("value".to_string(), num.to_string()), + ("limit".to_string(), ex_min.to_string()), + ])), path: self.path.to_string(), }); } @@ -41,7 +52,10 @@ impl<'a> ValidationContext<'a> { { result.errors.push(ValidationError { code: "EXCLUSIVE_MAXIMUM_VIOLATED".to_string(), - message: format!("Value {} >= ex_max {}", num, ex_max), + values: Some(HashMap::from([ + ("value".to_string(), num.to_string()), + ("limit".to_string(), ex_max.to_string()), + ])), path: self.path.to_string(), }); } @@ -50,7 +64,10 @@ impl<'a> ValidationContext<'a> { if (val - val.round()).abs() > f64::EPSILON { result.errors.push(ValidationError { code: "MULTIPLE_OF_VIOLATED".to_string(), - message: format!("Value {} not multiple of {}", num, multiple_of), + values: Some(HashMap::from([ + ("value".to_string(), num.to_string()), + ("multiple_of".to_string(), multiple_of.to_string()), + ])), path: self.path.to_string(), }); } diff --git a/src/validator/rules/object.rs b/src/validator/rules/object.rs index 779d7b9..218ffa6 100644 --- a/src/validator/rules/object.rs +++ b/src/validator/rules/object.rs @@ -1,4 +1,4 @@ -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use serde_json::Value; @@ -38,10 +38,9 @@ impl<'a> ValidationContext<'a> { } else { result.errors.push(ValidationError { code: "CONST_VIOLATED".to_string(), // Aligning with original const override errors natively - message: format!( - "Type '{}' is not a valid descendant for this entity bound schema", - type_str - ), + values: Some(HashMap::from([ + ("value".to_string(), type_str.to_string()), + ])), path: self.join_path("type"), }); } @@ -50,10 +49,9 @@ impl<'a> ValidationContext<'a> { // Because it's a global entity target, the payload must structurally provide a discriminator natively result.errors.push(ValidationError { code: "MISSING_TYPE".to_string(), - message: format!( - "Schema mechanically requires type discrimination '{}'", - expected_type - ), + values: Some(HashMap::from([ + ("expected".to_string(), expected_type.to_string()), + ])), path: self.path.clone(), // Empty boundary }); } @@ -70,8 +68,7 @@ impl<'a> ValidationContext<'a> { if obj.get("kind").is_none() { result.errors.push(ValidationError { code: "MISSING_KIND".to_string(), - message: "Schema mechanically requires horizontal kind discrimination" - .to_string(), + values: None, path: self.path.clone(), }); } else { @@ -106,8 +103,11 @@ impl<'a> ValidationContext<'a> { && (obj.len() as f64) < min { result.errors.push(ValidationError { - code: "MIN_PROPERTIES".to_string(), - message: "Too few properties".to_string(), + code: "MIN_PROPERTIES_VIOLATED".to_string(), + values: Some(HashMap::from([ + ("count".to_string(), obj.len().to_string()), + ("limit".to_string(), min.to_string()), + ])), path: self.path.to_string(), }); } @@ -116,8 +116,11 @@ impl<'a> ValidationContext<'a> { && (obj.len() as f64) > max { result.errors.push(ValidationError { - code: "MAX_PROPERTIES".to_string(), - message: "Too many properties".to_string(), + code: "MAX_PROPERTIES_VIOLATED".to_string(), + values: Some(HashMap::from([ + ("count".to_string(), obj.len().to_string()), + ("limit".to_string(), max.to_string()), + ])), path: self.path.to_string(), }); } @@ -128,13 +131,17 @@ impl<'a> ValidationContext<'a> { if field == "type" { result.errors.push(ValidationError { code: "MISSING_TYPE".to_string(), - message: "Missing type discriminator".to_string(), + values: Some(HashMap::from([ + ("field_name".to_string(), "type".to_string()), + ])), path: self.join_path(field), }); } else { result.errors.push(ValidationError { code: "REQUIRED_FIELD_MISSING".to_string(), - message: format!("Missing {}", field), + values: Some(HashMap::from([ + ("field_name".to_string(), field.to_string()), + ])), path: self.join_path(field), }); } @@ -151,7 +158,10 @@ impl<'a> ValidationContext<'a> { if !obj.contains_key(req_prop) { result.errors.push(ValidationError { code: "DEPENDENCY_MISSING".to_string(), - message: format!("Property '{}' requires property '{}'", prop, req_prop), + values: Some(HashMap::from([ + ("property_name".to_string(), prop.to_string()), + ("required_property".to_string(), req_prop.to_string()), + ])), path: self.path.to_string(), }); } diff --git a/src/validator/rules/polymorphism.rs b/src/validator/rules/polymorphism.rs index 511ed33..93d5bfb 100644 --- a/src/validator/rules/polymorphism.rs +++ b/src/validator/rules/polymorphism.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use crate::validator::context::ValidationContext; use crate::validator::error::ValidationError; use crate::validator::result::ValidationResult; @@ -22,7 +24,7 @@ impl<'a> ValidationContext<'a> { if conflicts { result.errors.push(ValidationError { code: "INVALID_SCHEMA".to_string(), - message: "family must be used exclusively without other constraints".to_string(), + values: None, path: self.path.to_string(), }); return Ok(false); @@ -35,7 +37,7 @@ impl<'a> ValidationContext<'a> { } else { result.errors.push(ValidationError { code: "UNCOMPILED_FAMILY".to_string(), - message: "Encountered family block that could not be mapped to deterministic options during db schema compilation.".to_string(), + values: None, path: self.path.to_string(), }); return Ok(false); @@ -55,7 +57,7 @@ impl<'a> ValidationContext<'a> { } else { result.errors.push(ValidationError { code: "UNCOMPILED_ONEOF".to_string(), - message: "Encountered oneOf block that could not be mapped to deterministic compiled options natively.".to_string(), + values: None, path: self.path.to_string(), }); return Ok(false); @@ -109,10 +111,9 @@ impl<'a> ValidationContext<'a> { } else { result.errors.push(ValidationError { code: "MISSING_COMPILED_SCHEMA".to_string(), - message: format!( - "Polymorphic router target '{}' does not exist in the database schemas map", - target_id - ), + values: Some(HashMap::from([ + ("target_id".to_string(), target_id.to_string()), + ])), path: self.path.to_string(), }); return Ok(false); @@ -132,10 +133,9 @@ impl<'a> ValidationContext<'a> { } else { result.errors.push(ValidationError { code: "MISSING_COMPILED_SCHEMA".to_string(), - message: format!( - "Polymorphic index target '{}' does not exist in the local oneOf array", - idx - ), + values: Some(HashMap::from([ + ("index".to_string(), idx.to_string()), + ])), path: self.path.to_string(), }); return Ok(false); @@ -144,10 +144,15 @@ impl<'a> ValidationContext<'a> { return Ok(false); } } else { - let disc_msg = if let Some(d) = self.schema.compiled_discriminator.get() { - format!("discriminator {}='{}'", d, val) + let values = if let Some(d) = self.schema.compiled_discriminator.get() { + Some(HashMap::from([ + ("discriminator".to_string(), d.to_string()), + ("value".to_string(), val.to_string()), + ])) } else { - format!("structural JSON base primitive '{}'", val) + Some(HashMap::from([ + ("primitive".to_string(), val.to_string()), + ])) }; result.errors.push(ValidationError { code: if self.schema.family.is_some() { @@ -155,10 +160,7 @@ impl<'a> ValidationContext<'a> { } else { "NO_ONEOF_MATCH".to_string() }, - message: format!( - "Payload matched no candidate boundaries based on its {}", - disc_msg - ), + values, path: self.path.to_string(), }); return Ok(false); @@ -167,10 +169,9 @@ impl<'a> ValidationContext<'a> { if let Some(d) = self.schema.compiled_discriminator.get() { result.errors.push(ValidationError { code: "MISSING_TYPE".to_string(), - message: format!( - "Missing explicit '{}' discriminator. Unable to resolve polymorphic boundaries", - d - ), + values: Some(HashMap::from([ + ("discriminator".to_string(), d.to_string()), + ])), path: self.path.to_string(), }); } diff --git a/src/validator/rules/string.rs b/src/validator/rules/string.rs index 42e11fa..a222df2 100644 --- a/src/validator/rules/string.rs +++ b/src/validator/rules/string.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use crate::validator::context::ValidationContext; use crate::validator::error::ValidationError; use crate::validator::result::ValidationResult; @@ -15,7 +17,10 @@ impl<'a> ValidationContext<'a> { { result.errors.push(ValidationError { code: "MIN_LENGTH_VIOLATED".to_string(), - message: format!("Length < min {}", min), + values: Some(HashMap::from([ + ("count".to_string(), s.chars().count().to_string()), + ("limit".to_string(), min.to_string()), + ])), path: self.path.to_string(), }); } @@ -24,7 +29,10 @@ impl<'a> ValidationContext<'a> { { result.errors.push(ValidationError { code: "MAX_LENGTH_VIOLATED".to_string(), - message: format!("Length > max {}", max), + values: Some(HashMap::from([ + ("count".to_string(), s.chars().count().to_string()), + ("limit".to_string(), max.to_string()), + ])), path: self.path.to_string(), }); } @@ -32,7 +40,9 @@ impl<'a> ValidationContext<'a> { if !compiled_re.0.is_match(s) { result.errors.push(ValidationError { code: "PATTERN_VIOLATED".to_string(), - message: format!("Pattern mismatch {:?}", self.schema.pattern), + values: Some(HashMap::from([ + ("pattern".to_string(), self.schema.pattern.clone().unwrap_or_default()), + ])), path: self.path.to_string(), }); } @@ -42,7 +52,9 @@ impl<'a> ValidationContext<'a> { { result.errors.push(ValidationError { code: "PATTERN_VIOLATED".to_string(), - message: format!("Pattern mismatch {}", pattern), + values: Some(HashMap::from([ + ("pattern".to_string(), pattern.to_string()), + ])), path: self.path.to_string(), }); } diff --git a/src/validator/rules/type.rs b/src/validator/rules/type.rs index c9fb59c..724cab9 100644 --- a/src/validator/rules/type.rs +++ b/src/validator/rules/type.rs @@ -1,6 +1,8 @@ +use crate::database::object::{is_primitive_type, SchemaTypeOrArray}; use crate::validator::context::ValidationContext; use crate::validator::error::ValidationError; use crate::validator::result::ValidationResult; +use std::collections::HashMap; impl<'a> ValidationContext<'a> { pub(crate) fn validate_type( @@ -24,19 +26,19 @@ impl<'a> ValidationContext<'a> { let mut custom_types = Vec::new(); match &self.schema.type_ { - Some(crate::database::object::SchemaTypeOrArray::Single(t)) => { - if !crate::database::object::is_primitive_type(t) { + Some(SchemaTypeOrArray::Single(t)) => { + if !is_primitive_type(t) { custom_types.push(t.clone()); } } - Some(crate::database::object::SchemaTypeOrArray::Multiple(arr)) => { + Some(SchemaTypeOrArray::Multiple(arr)) => { if arr.contains(&payload_primitive.to_string()) || (payload_primitive == "integer" && arr.contains(&"number".to_string())) { // It natively matched a primitive in the array options, skip forcing custom proxy fallback } else { for t in arr { - if !crate::database::object::is_primitive_type(t) { + if !is_primitive_type(t) { custom_types.push(t.clone()); } } @@ -51,7 +53,7 @@ impl<'a> ValidationContext<'a> { // 1. DYNAMIC TYPE (Composition) if t.starts_with('$') { let parts: Vec<&str> = t.split('.').collect(); - let var_name = &parts[0][1..]; // Remove the $ prefix + let var_name = parts[0].trim_start_matches('$'); let suffix = if parts.len() > 1 { format!(".{}", parts[1..].join(".")) } else { @@ -74,10 +76,10 @@ impl<'a> ValidationContext<'a> { if !resolved { result.errors.push(ValidationError { code: "DYNAMIC_TYPE_RESOLUTION_FAILED".to_string(), - message: format!( - "Dynamic type pointer '{}' could not resolve discriminator property '{}' on parent instance", - t, var_name - ), + values: Some(HashMap::from([ + ("pointer".to_string(), t.clone()), + ("discriminator".to_string(), var_name.to_string()), + ])), path: self.path.to_string(), }); continue; @@ -107,28 +109,25 @@ impl<'a> ValidationContext<'a> { if t.starts_with('$') { result.errors.push(ValidationError { code: "DYNAMIC_TYPE_RESOLUTION_FAILED".to_string(), - message: format!( - "Resolved dynamic type pointer '{}' was not found in schema registry", - target_id - ), + values: Some(HashMap::from([ + ("pointer".to_string(), target_id.to_string()), + ])), path: self.path.to_string(), }); } else if self.schema.is_proxy() { result.errors.push(ValidationError { code: "PROXY_TYPE_RESOLUTION_FAILED".to_string(), - message: format!( - "Composed proxy entity pointer '{}' was not found in schema registry", - target_id - ), + values: Some(HashMap::from([ + ("pointer".to_string(), target_id.to_string()), + ])), path: self.path.to_string(), }); } else { result.errors.push(ValidationError { code: "INHERITANCE_RESOLUTION_FAILED".to_string(), - message: format!( - "Inherited entity pointer '{}' was not found in schema registry", - target_id - ), + values: Some(HashMap::from([ + ("pointer".to_string(), target_id.to_string()), + ])), path: self.path.to_string(), }); }