jspg error refactoring checkpoint

This commit is contained in:
2026-06-23 17:03:27 -04:00
parent fb25224d22
commit d77765cb61
25 changed files with 362 additions and 218 deletions

View File

@ -1,4 +1,8 @@
use crate::database::schema::Schema; use crate::database::schema::Schema;
#[allow(unused_imports)]
use crate::drop::{Error, ErrorDetails};
#[allow(unused_imports)]
use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
impl Schema { impl Schema {
@ -8,18 +12,19 @@ impl Schema {
field_name: &str, field_name: &str,
root_id: &str, root_id: &str,
path: &str, path: &str,
errors: &mut Vec<crate::drop::Error>, errors: &mut Vec<Error>,
) { ) {
#[cfg(not(test))] #[cfg(not(test))]
for c in id.chars() { for c in id.chars() {
if !c.is_ascii_lowercase() && !c.is_ascii_digit() && c != '_' && c != '.' && c != '$' { 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(), code: "INVALID_IDENTIFIER".to_string(),
message: format!( values: Some(HashMap::from([
"Invalid character '{}' in JSON Schema '{}' property: '{}'. Identifiers must exclusively contain [a-z0-9_.$]", ("character".to_string(), c.to_string()),
c, field_name, id ("field_name".to_string(), field_name.to_string()),
), ("identifier".to_string(), id.to_string()),
details: crate::drop::ErrorDetails { ])),
details: ErrorDetails {
path: Some(path.to_string()), path: Some(path.to_string()),
schema: Some(root_id.to_string()), schema: Some(root_id.to_string()),
..Default::default() ..Default::default()
@ -35,7 +40,7 @@ impl Schema {
root_id: &str, root_id: &str,
path: String, path: String,
to_insert: &mut Vec<(String, Arc<Schema>)>, to_insert: &mut Vec<(String, Arc<Schema>)>,
errors: &mut Vec<crate::drop::Error>, errors: &mut Vec<Error>,
) { ) {
if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &schema_arc.obj.type_ { if let Some(crate::database::object::SchemaTypeOrArray::Single(t)) = &schema_arc.obj.type_ {
if t == "array" { if t == "array" {
@ -70,7 +75,7 @@ impl Schema {
root_id: &str, root_id: &str,
path: String, path: String,
to_insert: &mut Vec<(String, Arc<Schema>)>, to_insert: &mut Vec<(String, Arc<Schema>)>,
errors: &mut Vec<crate::drop::Error>, errors: &mut Vec<Error>,
) { ) {
if let Some(props) = &schema_arc.obj.properties { if let Some(props) = &schema_arc.obj.properties {
for (k, v) in props.iter() { for (k, v) in props.iter() {

View File

@ -5,7 +5,9 @@ pub mod filter;
pub mod polymorphism; pub mod polymorphism;
use crate::database::schema::Schema; use crate::database::schema::Schema;
use crate::drop::{Error, ErrorDetails};
use indexmap::IndexMap; use indexmap::IndexMap;
use std::collections::HashMap;
impl Schema { impl Schema {
pub fn compile( pub fn compile(
@ -13,7 +15,7 @@ impl Schema {
db: &crate::database::Database, db: &crate::database::Database,
root_id: &str, root_id: &str,
path: String, path: String,
errors: &mut Vec<crate::drop::Error>, errors: &mut Vec<Error>,
) { ) {
if self.obj.compiled_properties.get().is_some() { if self.obj.compiled_properties.get().is_some() {
return; return;
@ -72,13 +74,10 @@ impl Schema {
} }
if custom_type_count > 1 { if custom_type_count > 1 {
errors.push(crate::drop::Error { errors.push(Error {
code: "MULTIPLE_INHERITANCE_PROHIBITED".to_string(), code: "MULTIPLE_INHERITANCE_PROHIBITED".to_string(),
message: format!( values: Some(HashMap::from([("types".to_string(), types.join(", "))])),
"Schema attempts to extend multiple custom object pointers in its type array {:?}. Use 'oneOf' for polymorphism and tagged unions.", details: ErrorDetails {
types
),
details: crate::drop::ErrorDetails {
path: Some(path.clone()), path: Some(path.clone()),
schema: Some(root_id.to_string()), schema: Some(root_id.to_string()),
..Default::default() ..Default::default()

View File

@ -1,4 +1,6 @@
use crate::drop::{Error, ErrorDetails};
use indexmap::IndexSet; use indexmap::IndexSet;
use std::collections::HashMap;
use crate::database::schema::Schema; use crate::database::schema::Schema;
impl Schema { impl Schema {
@ -7,7 +9,7 @@ impl Schema {
db: &crate::database::Database, db: &crate::database::Database,
root_id: &str, root_id: &str,
path: &str, path: &str,
errors: &mut Vec<crate::drop::Error>, errors: &mut Vec<Error>,
) { ) {
let mut options = indexmap::IndexMap::new(); let mut options = indexmap::IndexMap::new();
let strategy: &str; let strategy: &str;
@ -118,16 +120,16 @@ impl Schema {
}; };
if strategy.is_empty() { if strategy.is_empty() {
errors.push(crate::drop::Error { errors.push(Error {
code: "AMBIGUOUS_POLYMORPHISM".to_string(), code: "AMBIGUOUS_POLYMORPHISM".to_string(),
message: format!("oneOf boundaries must map mathematically unique 'type' or 'kind' discriminators, or strictly contain disjoint primitive types."), values: None,
details: crate::drop::ErrorDetails { details: ErrorDetails {
path: Some(path.to_string()), path: Some(path.to_string()),
schema: Some(root_id.to_string()), schema: Some(root_id.to_string()),
..Default::default() ..Default::default()
} }
}); });
return; return;
} }
for (i, c) in one_of.iter().enumerate() { 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 let Some(val) = c.obj.get_discriminator_value(&strategy, &child_id) {
if options.contains_key(&val) { if options.contains_key(&val) {
errors.push(crate::drop::Error { errors.push(Error {
code: "POLYMORPHIC_COLLISION".to_string(), code: "POLYMORPHIC_COLLISION".to_string(),
message: format!("Polymorphic boundary defines multiple candidates mapped to the identical discriminator value '{}'.", val), values: Some(HashMap::from([("value".to_string(), val.to_string())])),
details: crate::drop::ErrorDetails { details: ErrorDetails {
path: Some(path.to_string()), path: Some(path.to_string()),
schema: Some(root_id.to_string()), schema: Some(root_id.to_string()),
..Default::default() ..Default::default()
} }
}); });
continue; continue;
} }

View File

@ -1,7 +1,8 @@
use crate::drop::{Error, ErrorDetails};
use serde_json::Value; use serde_json::Value;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
pub fn compose(val: &mut Value, errors: &mut Vec<crate::drop::Error>) -> Result<(), String> { pub fn compose(val: &mut Value, errors: &mut Vec<Error>) {
let mut traits = HashMap::new(); let mut traits = HashMap::new();
let mut schemas = HashMap::new(); let mut schemas = HashMap::new();
@ -56,15 +57,13 @@ pub fn compose(val: &mut Value, errors: &mut Vec<crate::drop::Error>) -> Result<
} }
} }
} }
Ok(())
} }
fn resolve_in_place( fn resolve_in_place(
current: &mut Value, current: &mut Value,
traits: &HashMap<String, Value>, traits: &HashMap<String, Value>,
schemas: &HashMap<String, Value>, schemas: &HashMap<String, Value>,
errors: &mut Vec<crate::drop::Error>, errors: &mut Vec<Error>,
schema_id: &str, schema_id: &str,
path: &str, path: &str,
visited: &mut HashSet<String>, visited: &mut HashSet<String>,
@ -118,10 +117,10 @@ fn resolve_in_place(
for inc in include_arr { for inc in include_arr {
if let Some(inc_name) = inc.as_str() { if let Some(inc_name) = inc.as_str() {
if visited.contains(inc_name) { if visited.contains(inc_name) {
errors.push(crate::drop::Error { errors.push(Error {
code: "CIRCULAR_INCLUDE_DETECTED".to_string(), code: "CIRCULAR_INCLUDE_DETECTED".to_string(),
message: format!("Circular inclusion detected for '{}'", inc_name), values: Some(HashMap::from([("include".to_string(), inc_name.to_string())])),
details: crate::drop::ErrorDetails { details: ErrorDetails {
schema: Some(schema_id.to_string()), schema: Some(schema_id.to_string()),
path: Some(path.to_string()), path: Some(path.to_string()),
..Default::default() ..Default::default()
@ -232,10 +231,10 @@ fn resolve_in_place(
} }
} }
} else { } else {
errors.push(crate::drop::Error { errors.push(Error {
code: "TRAIT_NOT_FOUND".to_string(), code: "TRAIT_NOT_FOUND".to_string(),
message: format!("Trait or schema '{}' not found for inclusion", inc_name), values: Some(HashMap::from([("include".to_string(), inc_name.to_string())])),
details: crate::drop::ErrorDetails { details: ErrorDetails {
schema: Some(schema_id.to_string()), schema: Some(schema_id.to_string()),
path: Some(path.to_string()), path: Some(path.to_string()),
..Default::default() ..Default::default()

View File

@ -28,6 +28,8 @@ use serde_json::Value;
use indexmap::IndexMap; use indexmap::IndexMap;
use std::sync::Arc; use std::sync::Arc;
use r#type::Type; use r#type::Type;
use std::collections::HashMap;
use crate::drop::{Drop, Error, ErrorDetails};
#[derive(serde::Serialize)] #[derive(serde::Serialize)]
pub struct Database { pub struct Database {
@ -57,13 +59,7 @@ impl Database {
let mut errors = Vec::new(); let mut errors = Vec::new();
if let Err(e) = compose::compose(&mut val, &mut errors) { 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(),
});
}
if let serde_json::Value::Object(mut map) = val { if let serde_json::Value::Object(mut map) = val {
if let Some(serde_json::Value::Array(arr)) = map.remove("enums") { if let Some(serde_json::Value::Array(arr)) = map.remove("enums") {
@ -78,10 +74,13 @@ impl Database {
db.enums.insert(def.name.clone(), def); db.enums.insert(def.name.clone(), def);
} }
Err(e) => { Err(e) => {
errors.push(crate::drop::Error { errors.push(Error {
code: "DATABASE_ENUM_PARSE_FAILED".to_string(), code: "DATABASE_ENUM_PARSE_FAILED".to_string(),
message: format!("Failed to parse database enum '{}': {}", name, e), values: Some(HashMap::from([
details: crate::drop::ErrorDetails { ("enum".to_string(), name.clone()),
("reason".to_string(), e.to_string()),
])),
details: ErrorDetails {
context: Some(serde_json::json!(name)), context: Some(serde_json::json!(name)),
..Default::default() ..Default::default()
}, },
@ -103,10 +102,13 @@ impl Database {
db.types.insert(def.name.clone(), def); db.types.insert(def.name.clone(), def);
} }
Err(e) => { Err(e) => {
errors.push(crate::drop::Error { errors.push(Error {
code: "DATABASE_TYPE_PARSE_FAILED".to_string(), code: "DATABASE_TYPE_PARSE_FAILED".to_string(),
message: format!("Failed to parse database type '{}': {}", name, e), values: Some(HashMap::from([
details: crate::drop::ErrorDetails { ("type".to_string(), name.clone()),
("reason".to_string(), e.to_string()),
])),
details: ErrorDetails {
context: Some(serde_json::json!(name)), context: Some(serde_json::json!(name)),
..Default::default() ..Default::default()
}, },
@ -132,10 +134,13 @@ impl Database {
} }
} }
Err(e) => { Err(e) => {
errors.push(crate::drop::Error { errors.push(Error {
code: "DATABASE_RELATION_PARSE_FAILED".to_string(), code: "DATABASE_RELATION_PARSE_FAILED".to_string(),
message: format!("Failed to parse database relation '{}': {}", constraint, e), values: Some(HashMap::from([
details: crate::drop::ErrorDetails { ("relation".to_string(), constraint.clone()),
("reason".to_string(), e.to_string()),
])),
details: ErrorDetails {
context: Some(serde_json::json!(constraint)), context: Some(serde_json::json!(constraint)),
..Default::default() ..Default::default()
}, },
@ -157,10 +162,13 @@ impl Database {
db.puncs.insert(def.name.clone(), def); db.puncs.insert(def.name.clone(), def);
} }
Err(e) => { Err(e) => {
errors.push(crate::drop::Error { errors.push(Error {
code: "DATABASE_PUNC_PARSE_FAILED".to_string(), code: "DATABASE_PUNC_PARSE_FAILED".to_string(),
message: format!("Failed to parse database punc '{}': {}", name, e), values: Some(HashMap::from([
details: crate::drop::ErrorDetails { ("punc".to_string(), name.clone()),
("reason".to_string(), e.to_string()),
])),
details: ErrorDetails {
context: Some(serde_json::json!(name)), context: Some(serde_json::json!(name)),
..Default::default() ..Default::default()
}, },
@ -173,9 +181,9 @@ impl Database {
db.compile(&mut errors); db.compile(&mut errors);
let drop = if errors.is_empty() { let drop = if errors.is_empty() {
crate::drop::Drop::success() Drop::success()
} else { } else {
crate::drop::Drop::with_errors(errors) Drop::with_errors(errors)
}; };
(db, drop) (db, drop)
} }
@ -443,7 +451,7 @@ impl Database {
// Abort relation discovery early if no hierarchical inheritance match was found // Abort relation discovery early if no hierarchical inheritance match was found
if matching_rels.is_empty() { if matching_rels.is_empty() {
let mut details = crate::drop::ErrorDetails { let mut details = ErrorDetails {
path: Some(path.to_string()), path: Some(path.to_string()),
..Default::default() ..Default::default()
}; };
@ -451,12 +459,13 @@ impl Database {
details.schema = Some(sid.to_string()); details.schema = Some(sid.to_string());
} }
errors.push(crate::drop::Error { errors.push(Error {
code: "EDGE_MISSING".to_string(), code: "EDGE_MISSING".to_string(),
message: format!( values: Some(HashMap::from([
"No database relation exists between '{}' and '{}' for property '{}'", ("parent_type".to_string(), parent_type.to_string()),
parent_type, child_type, prop_name ("child_type".to_string(), child_type.to_string()),
), ("property_name".to_string(), prop_name.to_string()),
])),
details, details,
}); });
return None; return None;
@ -542,7 +551,7 @@ impl Database {
// we must abort rather than silently guessing. Returning None prevents arbitrary SQL generation // we must abort rather than silently guessing. Returning None prevents arbitrary SQL generation
// and forces a clean structural error for the architect. // and forces a clean structural error for the architect.
if !resolved { if !resolved {
let mut details = crate::drop::ErrorDetails { let mut details = ErrorDetails {
path: Some(path.to_string()), path: Some(path.to_string()),
context: serde_json::to_value(&matching_rels).ok(), context: serde_json::to_value(&matching_rels).ok(),
cause: Some("Multiple conflicting constraints found matching prefixes".to_string()), cause: Some("Multiple conflicting constraints found matching prefixes".to_string()),
@ -552,12 +561,13 @@ impl Database {
details.schema = Some(sid.to_string()); details.schema = Some(sid.to_string());
} }
errors.push(crate::drop::Error { errors.push(Error {
code: "AMBIGUOUS_TYPE_RELATIONS".to_string(), code: "AMBIGUOUS_TYPE_RELATIONS".to_string(),
message: format!( values: Some(HashMap::from([
"Ambiguous database relation between '{}' and '{}' for property '{}'", ("parent_type".to_string(), parent_type.to_string()),
parent_type, child_type, prop_name ("child_type".to_string(), child_type.to_string()),
), ("property_name".to_string(), prop_name.to_string()),
])),
details, details,
}); });
return None; return None;

View File

@ -57,10 +57,13 @@ impl Drop {
} }
} }
use std::collections::HashMap;
#[derive(Debug, Serialize, Deserialize, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Error { pub struct Error {
pub code: String, pub code: String,
pub message: String, #[serde(skip_serializing_if = "Option::is_none")]
pub values: Option<HashMap<String, String>>,
pub details: ErrorDetails, pub details: ErrorDetails,
} }

View File

@ -31,7 +31,7 @@ lazy_static::lazy_static! {
fn jspg_failure() -> JsonB { fn jspg_failure() -> JsonB {
let error = crate::drop::Error { let error = crate::drop::Error {
code: "ENGINE_NOT_INITIALIZED".to_string(), code: "ENGINE_NOT_INITIALIZED".to_string(),
message: "JSPG extension has not been initialized via jspg_setup".to_string(), values: None,
details: crate::drop::ErrorDetails { details: crate::drop::ErrorDetails {
path: None, path: None,
cause: None, cause: None,

View File

@ -5,7 +5,9 @@ pub mod cache;
use crate::database::Database; use crate::database::Database;
use crate::database::r#type::Type; use crate::database::r#type::Type;
use crate::drop::{Drop, Error, ErrorDetails};
use serde_json::Value; use serde_json::Value;
use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
pub struct Merger { 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 mut notifications_queue = Vec::new();
let target_schema = match self.db.schemas.get(schema_id) { let target_schema = match self.db.schemas.get(schema_id) {
Some(s) => Arc::clone(&s), Some(s) => Arc::clone(&s),
None => { None => {
return crate::drop::Drop::with_errors(vec![crate::drop::Error { return Drop::with_errors(vec![Error {
code: "MERGE_FAILED".to_string(), code: "SCHEMA_NOT_FOUND".to_string(),
message: format!("Unknown schema_id: {}", schema_id), values: Some(HashMap::from([
details: crate::drop::ErrorDetails { ("schema".to_string(), schema_id.to_string()),
])),
details: ErrorDetails {
path: None, path: None,
cause: None, cause: None,
context: Some(data), 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, code: final_code,
message: final_message, values: Some(HashMap::from([
details: crate::drop::ErrorDetails { ("error".to_string(), final_message),
])),
details: ErrorDetails {
path: None, path: None,
cause: final_cause, cause: final_cause,
context: None, context: None,
@ -88,10 +94,12 @@ impl Merger {
// Execute the globally collected, pre-ordered notifications last! // Execute the globally collected, pre-ordered notifications last!
for notify_sql in notifications_queue { for notify_sql in notifications_queue {
if let Err(e) = self.db.execute(&notify_sql, None) { if let Err(e) = self.db.execute(&notify_sql, None) {
return crate::drop::Drop::with_errors(vec![crate::drop::Error { return Drop::with_errors(vec![Error {
code: "MERGE_FAILED".to_string(), code: "MERGE_FAILED".to_string(),
message: format!("Executor Error in pre-ordered notify: {:?}", e), values: Some(HashMap::from([
details: crate::drop::ErrorDetails { ("error".to_string(), e.to_string()),
])),
details: ErrorDetails {
path: None, path: None,
cause: None, cause: None,
context: None, context: None,

View File

@ -1,4 +1,6 @@
use crate::database::Database; use crate::database::Database;
use crate::drop::{Drop, Error, ErrorDetails};
use std::collections::HashMap;
use std::sync::Arc; use std::sync::Arc;
pub mod compiler; pub mod compiler;
@ -22,17 +24,19 @@ impl Queryer {
&self, &self,
schema_id: &str, schema_id: &str,
filter: Option<&serde_json::Value>, filter: Option<&serde_json::Value>,
) -> crate::drop::Drop { ) -> Drop {
let filters_map = filter.and_then(|f| f.as_object()); let filters_map = filter.and_then(|f| f.as_object());
// 1. Process filters into structured $op keys and linear values // 1. Process filters into structured $op keys and linear values
let (filter_keys, args) = match self.parse_filter_entries(filters_map) { let (filter_keys, args) = match self.parse_filter_entries(filters_map) {
Ok(res) => res, Ok(res) => res,
Err(msg) => { Err(msg) => {
return crate::drop::Drop::with_errors(vec![crate::drop::Error { return Drop::with_errors(vec![Error {
code: "FILTER_PARSE_FAILED".to_string(), code: "FILTER_PARSE_FAILED".to_string(),
message: msg.clone(), values: Some(HashMap::from([
details: crate::drop::ErrorDetails { ("error".to_string(), msg.clone()),
])),
details: ErrorDetails {
path: None, // filters apply to the root query path: None, // filters apply to the root query
cause: Some(msg), cause: Some(msg),
context: filter.cloned(), context: filter.cloned(),
@ -134,10 +138,12 @@ impl Queryer {
.insert(cache_key.to_string(), compiled_sql.clone()); .insert(cache_key.to_string(), compiled_sql.clone());
Ok(compiled_sql) 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(), code: "QUERY_COMPILATION_FAILED".to_string(),
message: e.clone(), values: Some(HashMap::from([
details: crate::drop::ErrorDetails { ("error".to_string(), e.clone()),
])),
details: ErrorDetails {
path: None, path: None,
cause: Some(e), cause: Some(e),
context: None, context: None,
@ -152,29 +158,33 @@ impl Queryer {
schema_id: &str, schema_id: &str,
sql: &str, sql: &str,
args: Vec<serde_json::Value>, args: Vec<serde_json::Value>,
) -> crate::drop::Drop { ) -> Drop {
match self.db.query(sql, Some(args)) { match self.db.query(sql, Some(args)) {
Ok(serde_json::Value::Array(table)) => { Ok(serde_json::Value::Array(table)) => {
if table.is_empty() { if table.is_empty() {
crate::drop::Drop::success_with_val(serde_json::Value::Null) Drop::success_with_val(serde_json::Value::Null)
} else { } 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(), code: "QUERY_FAILED".to_string(),
message: format!("Expected array from generic query, got: {:?}", other), values: Some(HashMap::from([
details: crate::drop::ErrorDetails { ("error".to_string(), format!("Expected array from generic query, got: {:?}", other)),
])),
details: ErrorDetails {
path: None, path: None,
cause: Some(format!("Expected array, got {}", other)), cause: Some(format!("Expected array, got {}", other)),
context: Some(serde_json::json!([sql])), context: Some(serde_json::json!([sql])),
schema: Some(schema_id.to_string()), 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(), code: "QUERY_FAILED".to_string(),
message: format!("SPI error in queryer: {}", e), values: Some(HashMap::from([
details: crate::drop::ErrorDetails { ("error".to_string(), e.to_string()),
])),
details: ErrorDetails {
path: None, path: None,
cause: Some(format!("SPI error in queryer: {}", e)), cause: Some(format!("SPI error in queryer: {}", e)),
context: Some(serde_json::json!([sql])), context: Some(serde_json::json!([sql])),

View File

@ -18,7 +18,6 @@ fn test_library_api() {
"type": "drop", "type": "drop",
"errors": [{ "errors": [{
"code": "ENGINE_NOT_INITIALIZED", "code": "ENGINE_NOT_INITIALIZED",
"message": "JSPG extension has not been initialized via jspg_setup",
"details": {} "details": {}
}] }]
}) })
@ -250,7 +249,9 @@ fn test_library_api() {
"errors": [ "errors": [
{ {
"code": "REQUIRED_FIELD_MISSING", "code": "REQUIRED_FIELD_MISSING",
"message": "Missing name", "values": {
"field_name": "name"
},
"details": { "details": {
"path": "name", "path": "name",
"schema": "source_schema" "schema": "source_schema"
@ -258,7 +259,9 @@ fn test_library_api() {
}, },
{ {
"code": "STRICT_PROPERTY_VIOLATION", "code": "STRICT_PROPERTY_VIOLATION",
"message": "Unexpected property 'wrong'", "values": {
"property_name": "wrong"
},
"details": { "details": {
"path": "wrong", "path": "wrong",
"schema": "source_schema" "schema": "source_schema"

View File

@ -86,7 +86,7 @@ pub fn run_test_case(path: &str, suite_idx: usize, case_idx: usize) -> Result<()
let error_messages: Vec<String> = drop let error_messages: Vec<String> = drop
.errors .errors
.iter() .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(); .collect();
failures.push(format!( failures.push(format!(
"[{}] Cannot run '{}' test '{}': System Setup Compilation structurally failed:\n{}", "[{}] 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" => { "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 { if let Err(e) = result {
println!("TEST VALIDATE ERROR FOR '{}': {}", test.description, e); println!("TEST VALIDATE ERROR FOR '{}': {}", test.description, e);
failures.push(format!( failures.push(format!(
@ -205,6 +205,25 @@ pub fn canonicalize_with_map(s: &str, uuid_map: &HashMap<String, String>, gen_ma
ts_re.replace_all(&s1, "{{timestamp}}").to_string() 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]) { pub fn update_sql_fixture(path: &str, suite_idx: usize, case_idx: usize, queries: &[String]) {
use crate::tests::formatter::SqlFormatter; use crate::tests::formatter::SqlFormatter;
let content = fs::read_to_string(path).unwrap(); let content = fs::read_to_string(path).unwrap();

View File

@ -1,7 +1,10 @@
use super::expect::Expect; use super::expect::Expect;
use crate::database::Database; use crate::database::Database;
use crate::tests::runner::update_validation_fixture;
use crate::validator::Validator;
use serde::Deserialize; use serde::Deserialize;
use serde_json::Value; use serde_json::Value;
use std::env;
use std::sync::Arc; use std::sync::Arc;
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@ -58,16 +61,16 @@ impl Case {
Ok(()) Ok(())
} }
pub fn run_validate(&self, db: Arc<Database>) -> Result<(), String> { pub fn run_validate(&self, db: Arc<Database>, path: &str, suite_idx: usize, case_idx: usize) -> Result<(), String> {
use crate::validator::Validator;
let validator = Validator::new(db); let validator = Validator::new(db);
let schema_id = &self.schema_id; let schema_id = &self.schema_id;
let test_data = self.data.clone().unwrap_or(Value::Null); let test_data = self.data.clone().unwrap_or(Value::Null);
let result = validator.validate(schema_id, &test_data); 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 { if let Some(expect) = &self.expect {
expect.assert_drop(&result)?; expect.assert_drop(&result)?;
} }

View File

@ -1,6 +1,9 @@
use std::collections::HashMap;
#[derive(Debug, Clone, serde::Serialize)] #[derive(Debug, Clone, serde::Serialize)]
pub struct ValidationError { pub struct ValidationError {
pub code: String, pub code: String,
pub message: String, #[serde(skip_serializing_if = "Option::is_none")]
pub values: Option<HashMap<String, String>>,
pub path: String, pub path: String,
} }

View File

@ -1,4 +1,4 @@
use std::collections::HashSet; use std::collections::{HashMap, HashSet};
pub mod context; pub mod context;
pub mod error; pub mod error;
@ -13,6 +13,7 @@ use crate::database::Database;
use crate::validator::rules::util::is_integer; use crate::validator::rules::util::is_integer;
use serde_json::Value; use serde_json::Value;
use std::sync::Arc; use std::sync::Arc;
use crate::drop::{Drop, Error, ErrorDetails};
pub struct Validator { pub struct Validator {
pub db: Arc<Database>, pub db: Arc<Database>,
@ -57,15 +58,15 @@ impl Validator {
match ctx.validate_scoped() { match ctx.validate_scoped() {
Ok(result) => { Ok(result) => {
if result.is_valid() { if result.is_valid() {
crate::drop::Drop::success() Drop::success()
} else { } else {
let errors: Vec<crate::drop::Error> = result let errors: Vec<Error> = result
.errors .errors
.into_iter() .into_iter()
.map(|e| crate::drop::Error { .map(|e| Error {
code: e.code, code: e.code,
message: e.message, values: e.values,
details: crate::drop::ErrorDetails { details: ErrorDetails {
path: Some(e.path), path: Some(e.path),
cause: None, cause: None,
context: None, context: None,
@ -73,13 +74,13 @@ impl Validator {
}, },
}) })
.collect(); .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, code: e.code,
message: e.message, values: e.values,
details: crate::drop::ErrorDetails { details: ErrorDetails {
path: Some(e.path), path: Some(e.path),
cause: None, cause: None,
context: None, context: None,
@ -88,10 +89,12 @@ impl Validator {
}]), }]),
} }
} else { } else {
crate::drop::Drop::with_errors(vec![crate::drop::Error { Drop::with_errors(vec![Error {
code: "SCHEMA_NOT_FOUND".to_string(), code: "SCHEMA_NOT_FOUND".to_string(),
message: format!("Schema {} not found", schema_id), values: Some(HashMap::from([
details: crate::drop::ErrorDetails { ("schema".to_string(), schema_id.to_string()),
])),
details: ErrorDetails {
path: Some("/".to_string()), path: Some("/".to_string()),
cause: None, cause: None,
context: None, context: None,

View File

@ -1,4 +1,4 @@
use std::collections::HashSet; use std::collections::{HashMap, HashSet};
use serde_json::Value; use serde_json::Value;
@ -17,8 +17,11 @@ impl<'a> ValidationContext<'a> {
&& (arr.len() as f64) < min && (arr.len() as f64) < min
{ {
result.errors.push(ValidationError { result.errors.push(ValidationError {
code: "MIN_ITEMS".to_string(), code: "MIN_ITEMS_VIOLATED".to_string(),
message: "Too few items".to_string(), values: Some(HashMap::from([
("count".to_string(), arr.len().to_string()),
("limit".to_string(), min.to_string()),
])),
path: self.path.to_string(), path: self.path.to_string(),
}); });
} }
@ -26,8 +29,11 @@ impl<'a> ValidationContext<'a> {
&& (arr.len() as f64) > max && (arr.len() as f64) > max
{ {
result.errors.push(ValidationError { result.errors.push(ValidationError {
code: "MAX_ITEMS".to_string(), code: "MAX_ITEMS_VIOLATED".to_string(),
message: "Too many items".to_string(), values: Some(HashMap::from([
("count".to_string(), arr.len().to_string()),
("limit".to_string(), max.to_string()),
])),
path: self.path.to_string(), path: self.path.to_string(),
}); });
} }
@ -38,7 +44,7 @@ impl<'a> ValidationContext<'a> {
if seen.contains(&item) { if seen.contains(&item) {
result.errors.push(ValidationError { result.errors.push(ValidationError {
code: "UNIQUE_ITEMS_VIOLATED".to_string(), code: "UNIQUE_ITEMS_VIOLATED".to_string(),
message: "Array has duplicate items".to_string(), values: None,
path: self.path.to_string(), path: self.path.to_string(),
}); });
break; break;
@ -71,7 +77,10 @@ impl<'a> ValidationContext<'a> {
if _match_count < min { if _match_count < min {
result.errors.push(ValidationError { result.errors.push(ValidationError {
code: "CONTAINS_VIOLATED".to_string(), 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(), path: self.path.to_string(),
}); });
} }
@ -80,7 +89,10 @@ impl<'a> ValidationContext<'a> {
{ {
result.errors.push(ValidationError { result.errors.push(ValidationError {
code: "CONTAINS_VIOLATED".to_string(), 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(), path: self.path.to_string(),
}); });
} }

View File

@ -1,3 +1,5 @@
use std::collections::HashMap;
use crate::validator::Validator; use crate::validator::Validator;
use crate::validator::context::ValidationContext; use crate::validator::context::ValidationContext;
use crate::validator::error::ValidationError; use crate::validator::error::ValidationError;
@ -17,7 +19,9 @@ impl<'a> ValidationContext<'a> {
if !Validator::check_type(t, current) { if !Validator::check_type(t, current) {
result.errors.push(ValidationError { result.errors.push(ValidationError {
code: "INVALID_TYPE".to_string(), 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(), path: self.path.to_string(),
}); });
} }
@ -33,7 +37,9 @@ impl<'a> ValidationContext<'a> {
if !valid { if !valid {
result.errors.push(ValidationError { result.errors.push(ValidationError {
code: "INVALID_TYPE".to_string(), 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(), path: self.path.to_string(),
}); });
} }
@ -45,7 +51,9 @@ impl<'a> ValidationContext<'a> {
if !equals(current, const_val) { if !equals(current, const_val) {
result.errors.push(ValidationError { result.errors.push(ValidationError {
code: "CONST_VIOLATED".to_string(), 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(), path: self.path.to_string(),
}); });
} else if let Some(obj) = current.as_object() { } else if let Some(obj) = current.as_object() {
@ -66,7 +74,9 @@ impl<'a> ValidationContext<'a> {
if !found { if !found {
result.errors.push(ValidationError { result.errors.push(ValidationError {
code: "ENUM_MISMATCH".to_string(), 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(), path: self.path.to_string(),
}); });
} else if let Some(obj) = current.as_object() { } else if let Some(obj) = current.as_object() {

View File

@ -1,3 +1,5 @@
use std::collections::HashMap;
use crate::validator::context::ValidationContext; use crate::validator::context::ValidationContext;
use crate::validator::error::ValidationError; use crate::validator::error::ValidationError;
use crate::validator::result::ValidationResult; use crate::validator::result::ValidationResult;
@ -27,7 +29,9 @@ impl<'a> ValidationContext<'a> {
if !result.evaluated_keys.contains(key) && !self.overrides.contains(key) { if !result.evaluated_keys.contains(key) && !self.overrides.contains(key) {
result.errors.push(ValidationError { result.errors.push(ValidationError {
code: "STRICT_PROPERTY_VIOLATION".to_string(), 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), path: self.join_path(key),
}); });
} }
@ -47,7 +51,9 @@ impl<'a> ValidationContext<'a> {
} }
result.errors.push(ValidationError { result.errors.push(ValidationError {
code: "STRICT_ITEM_VIOLATION".to_string(), 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, path: item_path,
}); });
} }

View File

@ -1,3 +1,5 @@
use std::collections::HashMap;
use crate::validator::context::ValidationContext; use crate::validator::context::ValidationContext;
use crate::validator::error::ValidationError; use crate::validator::error::ValidationError;
use crate::validator::result::ValidationResult; use crate::validator::result::ValidationResult;
@ -19,7 +21,10 @@ impl<'a> ValidationContext<'a> {
if should && let Err(e) = f(current) { if should && let Err(e) = f(current) {
result.errors.push(ValidationError { result.errors.push(ValidationError {
code: "FORMAT_MISMATCH".to_string(), 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(), path: self.path.to_string(),
}); });
} }
@ -30,7 +35,9 @@ impl<'a> ValidationContext<'a> {
{ {
result.errors.push(ValidationError { result.errors.push(ValidationError {
code: "FORMAT_MISMATCH".to_string(), 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(), path: self.path.to_string(),
}); });
} }

View File

@ -1,6 +1,7 @@
use crate::validator::context::ValidationContext; use crate::validator::context::ValidationContext;
use crate::validator::error::ValidationError; use crate::validator::error::ValidationError;
use crate::validator::result::ValidationResult; use crate::validator::result::ValidationResult;
use std::collections::HashMap;
pub mod array; pub mod array;
pub mod cases; pub mod cases;
@ -61,7 +62,9 @@ impl<'a> ValidationContext<'a> {
if self.depth > 100 { if self.depth > 100 {
Err(ValidationError { Err(ValidationError {
code: "RECURSION_LIMIT_EXCEEDED".to_string(), 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(), path: self.path.to_string(),
}) })
} else { } else {
@ -73,7 +76,7 @@ impl<'a> ValidationContext<'a> {
if self.schema.always_fail { if self.schema.always_fail {
result.errors.push(ValidationError { result.errors.push(ValidationError {
code: "FALSE_SCHEMA".to_string(), code: "FALSE_SCHEMA".to_string(),
message: "Schema is false".to_string(), values: None,
path: self.path.to_string(), path: self.path.to_string(),
}); });
// Short-circuit // Short-circuit

View File

@ -13,7 +13,7 @@ impl<'a> ValidationContext<'a> {
if sub_res.is_valid() { if sub_res.is_valid() {
result.errors.push(ValidationError { result.errors.push(ValidationError {
code: "NOT_VIOLATED".to_string(), code: "NOT_VIOLATED".to_string(),
message: "Matched 'not' schema".to_string(), values: None,
path: self.path.to_string(), path: self.path.to_string(),
}); });
} }

View File

@ -1,3 +1,5 @@
use std::collections::HashMap;
use crate::validator::context::ValidationContext; use crate::validator::context::ValidationContext;
use crate::validator::error::ValidationError; use crate::validator::error::ValidationError;
use crate::validator::result::ValidationResult; use crate::validator::result::ValidationResult;
@ -14,7 +16,10 @@ impl<'a> ValidationContext<'a> {
{ {
result.errors.push(ValidationError { result.errors.push(ValidationError {
code: "MINIMUM_VIOLATED".to_string(), 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(), path: self.path.to_string(),
}); });
} }
@ -23,7 +28,10 @@ impl<'a> ValidationContext<'a> {
{ {
result.errors.push(ValidationError { result.errors.push(ValidationError {
code: "MAXIMUM_VIOLATED".to_string(), 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(), path: self.path.to_string(),
}); });
} }
@ -32,7 +40,10 @@ impl<'a> ValidationContext<'a> {
{ {
result.errors.push(ValidationError { result.errors.push(ValidationError {
code: "EXCLUSIVE_MINIMUM_VIOLATED".to_string(), 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(), path: self.path.to_string(),
}); });
} }
@ -41,7 +52,10 @@ impl<'a> ValidationContext<'a> {
{ {
result.errors.push(ValidationError { result.errors.push(ValidationError {
code: "EXCLUSIVE_MAXIMUM_VIOLATED".to_string(), 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(), path: self.path.to_string(),
}); });
} }
@ -50,7 +64,10 @@ impl<'a> ValidationContext<'a> {
if (val - val.round()).abs() > f64::EPSILON { if (val - val.round()).abs() > f64::EPSILON {
result.errors.push(ValidationError { result.errors.push(ValidationError {
code: "MULTIPLE_OF_VIOLATED".to_string(), 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(), path: self.path.to_string(),
}); });
} }

View File

@ -1,4 +1,4 @@
use std::collections::HashSet; use std::collections::{HashMap, HashSet};
use serde_json::Value; use serde_json::Value;
@ -38,10 +38,9 @@ impl<'a> ValidationContext<'a> {
} else { } else {
result.errors.push(ValidationError { result.errors.push(ValidationError {
code: "CONST_VIOLATED".to_string(), // Aligning with original const override errors natively code: "CONST_VIOLATED".to_string(), // Aligning with original const override errors natively
message: format!( values: Some(HashMap::from([
"Type '{}' is not a valid descendant for this entity bound schema", ("value".to_string(), type_str.to_string()),
type_str ])),
),
path: self.join_path("type"), 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 // Because it's a global entity target, the payload must structurally provide a discriminator natively
result.errors.push(ValidationError { result.errors.push(ValidationError {
code: "MISSING_TYPE".to_string(), code: "MISSING_TYPE".to_string(),
message: format!( values: Some(HashMap::from([
"Schema mechanically requires type discrimination '{}'", ("expected".to_string(), expected_type.to_string()),
expected_type ])),
),
path: self.path.clone(), // Empty boundary path: self.path.clone(), // Empty boundary
}); });
} }
@ -70,8 +68,7 @@ impl<'a> ValidationContext<'a> {
if obj.get("kind").is_none() { if obj.get("kind").is_none() {
result.errors.push(ValidationError { result.errors.push(ValidationError {
code: "MISSING_KIND".to_string(), code: "MISSING_KIND".to_string(),
message: "Schema mechanically requires horizontal kind discrimination" values: None,
.to_string(),
path: self.path.clone(), path: self.path.clone(),
}); });
} else { } else {
@ -106,8 +103,11 @@ impl<'a> ValidationContext<'a> {
&& (obj.len() as f64) < min && (obj.len() as f64) < min
{ {
result.errors.push(ValidationError { result.errors.push(ValidationError {
code: "MIN_PROPERTIES".to_string(), code: "MIN_PROPERTIES_VIOLATED".to_string(),
message: "Too few properties".to_string(), values: Some(HashMap::from([
("count".to_string(), obj.len().to_string()),
("limit".to_string(), min.to_string()),
])),
path: self.path.to_string(), path: self.path.to_string(),
}); });
} }
@ -116,8 +116,11 @@ impl<'a> ValidationContext<'a> {
&& (obj.len() as f64) > max && (obj.len() as f64) > max
{ {
result.errors.push(ValidationError { result.errors.push(ValidationError {
code: "MAX_PROPERTIES".to_string(), code: "MAX_PROPERTIES_VIOLATED".to_string(),
message: "Too many properties".to_string(), values: Some(HashMap::from([
("count".to_string(), obj.len().to_string()),
("limit".to_string(), max.to_string()),
])),
path: self.path.to_string(), path: self.path.to_string(),
}); });
} }
@ -128,13 +131,17 @@ impl<'a> ValidationContext<'a> {
if field == "type" { if field == "type" {
result.errors.push(ValidationError { result.errors.push(ValidationError {
code: "MISSING_TYPE".to_string(), 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), path: self.join_path(field),
}); });
} else { } else {
result.errors.push(ValidationError { result.errors.push(ValidationError {
code: "REQUIRED_FIELD_MISSING".to_string(), 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), path: self.join_path(field),
}); });
} }
@ -151,7 +158,10 @@ impl<'a> ValidationContext<'a> {
if !obj.contains_key(req_prop) { if !obj.contains_key(req_prop) {
result.errors.push(ValidationError { result.errors.push(ValidationError {
code: "DEPENDENCY_MISSING".to_string(), 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(), path: self.path.to_string(),
}); });
} }

View File

@ -1,3 +1,5 @@
use std::collections::HashMap;
use crate::validator::context::ValidationContext; use crate::validator::context::ValidationContext;
use crate::validator::error::ValidationError; use crate::validator::error::ValidationError;
use crate::validator::result::ValidationResult; use crate::validator::result::ValidationResult;
@ -22,7 +24,7 @@ impl<'a> ValidationContext<'a> {
if conflicts { if conflicts {
result.errors.push(ValidationError { result.errors.push(ValidationError {
code: "INVALID_SCHEMA".to_string(), code: "INVALID_SCHEMA".to_string(),
message: "family must be used exclusively without other constraints".to_string(), values: None,
path: self.path.to_string(), path: self.path.to_string(),
}); });
return Ok(false); return Ok(false);
@ -35,7 +37,7 @@ impl<'a> ValidationContext<'a> {
} else { } else {
result.errors.push(ValidationError { result.errors.push(ValidationError {
code: "UNCOMPILED_FAMILY".to_string(), 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(), path: self.path.to_string(),
}); });
return Ok(false); return Ok(false);
@ -55,7 +57,7 @@ impl<'a> ValidationContext<'a> {
} else { } else {
result.errors.push(ValidationError { result.errors.push(ValidationError {
code: "UNCOMPILED_ONEOF".to_string(), 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(), path: self.path.to_string(),
}); });
return Ok(false); return Ok(false);
@ -109,10 +111,9 @@ impl<'a> ValidationContext<'a> {
} else { } else {
result.errors.push(ValidationError { result.errors.push(ValidationError {
code: "MISSING_COMPILED_SCHEMA".to_string(), code: "MISSING_COMPILED_SCHEMA".to_string(),
message: format!( values: Some(HashMap::from([
"Polymorphic router target '{}' does not exist in the database schemas map", ("target_id".to_string(), target_id.to_string()),
target_id ])),
),
path: self.path.to_string(), path: self.path.to_string(),
}); });
return Ok(false); return Ok(false);
@ -132,10 +133,9 @@ impl<'a> ValidationContext<'a> {
} else { } else {
result.errors.push(ValidationError { result.errors.push(ValidationError {
code: "MISSING_COMPILED_SCHEMA".to_string(), code: "MISSING_COMPILED_SCHEMA".to_string(),
message: format!( values: Some(HashMap::from([
"Polymorphic index target '{}' does not exist in the local oneOf array", ("index".to_string(), idx.to_string()),
idx ])),
),
path: self.path.to_string(), path: self.path.to_string(),
}); });
return Ok(false); return Ok(false);
@ -144,10 +144,15 @@ impl<'a> ValidationContext<'a> {
return Ok(false); return Ok(false);
} }
} else { } else {
let disc_msg = if let Some(d) = self.schema.compiled_discriminator.get() { let values = if let Some(d) = self.schema.compiled_discriminator.get() {
format!("discriminator {}='{}'", d, val) Some(HashMap::from([
("discriminator".to_string(), d.to_string()),
("value".to_string(), val.to_string()),
]))
} else { } else {
format!("structural JSON base primitive '{}'", val) Some(HashMap::from([
("primitive".to_string(), val.to_string()),
]))
}; };
result.errors.push(ValidationError { result.errors.push(ValidationError {
code: if self.schema.family.is_some() { code: if self.schema.family.is_some() {
@ -155,10 +160,7 @@ impl<'a> ValidationContext<'a> {
} else { } else {
"NO_ONEOF_MATCH".to_string() "NO_ONEOF_MATCH".to_string()
}, },
message: format!( values,
"Payload matched no candidate boundaries based on its {}",
disc_msg
),
path: self.path.to_string(), path: self.path.to_string(),
}); });
return Ok(false); return Ok(false);
@ -167,10 +169,9 @@ impl<'a> ValidationContext<'a> {
if let Some(d) = self.schema.compiled_discriminator.get() { if let Some(d) = self.schema.compiled_discriminator.get() {
result.errors.push(ValidationError { result.errors.push(ValidationError {
code: "MISSING_TYPE".to_string(), code: "MISSING_TYPE".to_string(),
message: format!( values: Some(HashMap::from([
"Missing explicit '{}' discriminator. Unable to resolve polymorphic boundaries", ("discriminator".to_string(), d.to_string()),
d ])),
),
path: self.path.to_string(), path: self.path.to_string(),
}); });
} }

View File

@ -1,3 +1,5 @@
use std::collections::HashMap;
use crate::validator::context::ValidationContext; use crate::validator::context::ValidationContext;
use crate::validator::error::ValidationError; use crate::validator::error::ValidationError;
use crate::validator::result::ValidationResult; use crate::validator::result::ValidationResult;
@ -15,7 +17,10 @@ impl<'a> ValidationContext<'a> {
{ {
result.errors.push(ValidationError { result.errors.push(ValidationError {
code: "MIN_LENGTH_VIOLATED".to_string(), 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(), path: self.path.to_string(),
}); });
} }
@ -24,7 +29,10 @@ impl<'a> ValidationContext<'a> {
{ {
result.errors.push(ValidationError { result.errors.push(ValidationError {
code: "MAX_LENGTH_VIOLATED".to_string(), 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(), path: self.path.to_string(),
}); });
} }
@ -32,7 +40,9 @@ impl<'a> ValidationContext<'a> {
if !compiled_re.0.is_match(s) { if !compiled_re.0.is_match(s) {
result.errors.push(ValidationError { result.errors.push(ValidationError {
code: "PATTERN_VIOLATED".to_string(), 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(), path: self.path.to_string(),
}); });
} }
@ -42,7 +52,9 @@ impl<'a> ValidationContext<'a> {
{ {
result.errors.push(ValidationError { result.errors.push(ValidationError {
code: "PATTERN_VIOLATED".to_string(), 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(), path: self.path.to_string(),
}); });
} }

View File

@ -1,6 +1,8 @@
use crate::database::object::{is_primitive_type, SchemaTypeOrArray};
use crate::validator::context::ValidationContext; use crate::validator::context::ValidationContext;
use crate::validator::error::ValidationError; use crate::validator::error::ValidationError;
use crate::validator::result::ValidationResult; use crate::validator::result::ValidationResult;
use std::collections::HashMap;
impl<'a> ValidationContext<'a> { impl<'a> ValidationContext<'a> {
pub(crate) fn validate_type( pub(crate) fn validate_type(
@ -24,19 +26,19 @@ impl<'a> ValidationContext<'a> {
let mut custom_types = Vec::new(); let mut custom_types = Vec::new();
match &self.schema.type_ { match &self.schema.type_ {
Some(crate::database::object::SchemaTypeOrArray::Single(t)) => { Some(SchemaTypeOrArray::Single(t)) => {
if !crate::database::object::is_primitive_type(t) { if !is_primitive_type(t) {
custom_types.push(t.clone()); custom_types.push(t.clone());
} }
} }
Some(crate::database::object::SchemaTypeOrArray::Multiple(arr)) => { Some(SchemaTypeOrArray::Multiple(arr)) => {
if arr.contains(&payload_primitive.to_string()) if arr.contains(&payload_primitive.to_string())
|| (payload_primitive == "integer" && arr.contains(&"number".to_string())) || (payload_primitive == "integer" && arr.contains(&"number".to_string()))
{ {
// It natively matched a primitive in the array options, skip forcing custom proxy fallback // It natively matched a primitive in the array options, skip forcing custom proxy fallback
} else { } else {
for t in arr { for t in arr {
if !crate::database::object::is_primitive_type(t) { if !is_primitive_type(t) {
custom_types.push(t.clone()); custom_types.push(t.clone());
} }
} }
@ -51,7 +53,7 @@ impl<'a> ValidationContext<'a> {
// 1. DYNAMIC TYPE (Composition) // 1. DYNAMIC TYPE (Composition)
if t.starts_with('$') { if t.starts_with('$') {
let parts: Vec<&str> = t.split('.').collect(); 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 { let suffix = if parts.len() > 1 {
format!(".{}", parts[1..].join(".")) format!(".{}", parts[1..].join("."))
} else { } else {
@ -74,10 +76,10 @@ impl<'a> ValidationContext<'a> {
if !resolved { if !resolved {
result.errors.push(ValidationError { result.errors.push(ValidationError {
code: "DYNAMIC_TYPE_RESOLUTION_FAILED".to_string(), code: "DYNAMIC_TYPE_RESOLUTION_FAILED".to_string(),
message: format!( values: Some(HashMap::from([
"Dynamic type pointer '{}' could not resolve discriminator property '{}' on parent instance", ("pointer".to_string(), t.clone()),
t, var_name ("discriminator".to_string(), var_name.to_string()),
), ])),
path: self.path.to_string(), path: self.path.to_string(),
}); });
continue; continue;
@ -107,28 +109,25 @@ impl<'a> ValidationContext<'a> {
if t.starts_with('$') { if t.starts_with('$') {
result.errors.push(ValidationError { result.errors.push(ValidationError {
code: "DYNAMIC_TYPE_RESOLUTION_FAILED".to_string(), code: "DYNAMIC_TYPE_RESOLUTION_FAILED".to_string(),
message: format!( values: Some(HashMap::from([
"Resolved dynamic type pointer '{}' was not found in schema registry", ("pointer".to_string(), target_id.to_string()),
target_id ])),
),
path: self.path.to_string(), path: self.path.to_string(),
}); });
} else if self.schema.is_proxy() { } else if self.schema.is_proxy() {
result.errors.push(ValidationError { result.errors.push(ValidationError {
code: "PROXY_TYPE_RESOLUTION_FAILED".to_string(), code: "PROXY_TYPE_RESOLUTION_FAILED".to_string(),
message: format!( values: Some(HashMap::from([
"Composed proxy entity pointer '{}' was not found in schema registry", ("pointer".to_string(), target_id.to_string()),
target_id ])),
),
path: self.path.to_string(), path: self.path.to_string(),
}); });
} else { } else {
result.errors.push(ValidationError { result.errors.push(ValidationError {
code: "INHERITANCE_RESOLUTION_FAILED".to_string(), code: "INHERITANCE_RESOLUTION_FAILED".to_string(),
message: format!( values: Some(HashMap::from([
"Inherited entity pointer '{}' was not found in schema registry", ("pointer".to_string(), target_id.to_string()),
target_id ])),
),
path: self.path.to_string(), path: self.path.to_string(),
}); });
} }