diff --git a/fixtures/paths.json b/fixtures/paths.json new file mode 100644 index 0000000..416e472 --- /dev/null +++ b/fixtures/paths.json @@ -0,0 +1,178 @@ +[ + { + "description": "Hybrid Array Pathing", + "database": { + "schemas": [ + { + "$id": "hybrid_pathing", + "type": "object", + "properties": { + "primitives": { + "type": "array", + "items": { + "type": "string" + } + }, + "ad_hoc_objects": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + } + }, + "required": [ + "name" + ] + } + }, + "entities": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "value": { + "type": "number", + "minimum": 10 + } + } + } + }, + "deep_entities": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "nested": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string" + }, + "flag": { + "type": "boolean" + } + } + } + } + } + } + } + } + } + ] + }, + "tests": [ + { + "description": "happy path passes structural validation", + "data": { + "primitives": ["a", "b"], + "ad_hoc_objects": [{"name": "obj1"}], + "entities": [{"id": "entity-1", "value": 15}], + "deep_entities": [ + { + "id": "parent-1", + "nested": [{"id": "child-1", "flag": true}] + } + ] + }, + "schema_id": "hybrid_pathing", + "action": "validate", + "expect": { + "success": true + } + }, + { + "description": "primitive arrays use numeric indexing", + "data": { + "primitives": ["a", 123] + }, + "schema_id": "hybrid_pathing", + "action": "validate", + "expect": { + "success": false, + "errors": [ + { + "code": "INVALID_TYPE", + "path": "/primitives/1" + } + ] + } + }, + { + "description": "ad-hoc objects without ids use numeric indexing", + "data": { + "ad_hoc_objects": [ + {"name": "valid"}, + {"age": 30} + ] + }, + "schema_id": "hybrid_pathing", + "action": "validate", + "expect": { + "success": false, + "errors": [ + { + "code": "REQUIRED_FIELD_MISSING", + "path": "/ad_hoc_objects/1/name" + } + ] + } + }, + { + "description": "arrays of objects with ids use topological uuid indexing", + "data": { + "entities": [ + {"id": "entity-alpha", "value": 20}, + {"id": "entity-beta", "value": 5} + ] + }, + "schema_id": "hybrid_pathing", + "action": "validate", + "expect": { + "success": false, + "errors": [ + { + "code": "MINIMUM_VIOLATED", + "path": "/entities/entity-beta/value" + } + ] + } + }, + { + "description": "deeply nested entity arrays retain full topological paths", + "data": { + "deep_entities": [ + { + "id": "parent-omega", + "nested": [ + {"id": "child-alpha", "flag": true}, + {"id": "child-beta", "flag": "invalid-string"} + ] + } + ] + }, + "schema_id": "hybrid_pathing", + "action": "validate", + "expect": { + "success": false, + "errors": [ + { + "code": "INVALID_TYPE", + "path": "/deep_entities/parent-omega/nested/child-beta/flag" + } + ] + } + } + ] + } +] diff --git a/src/tests/fixtures.rs b/src/tests/fixtures.rs index 9bacdf3..fa7ff5f 100644 --- a/src/tests/fixtures.rs +++ b/src/tests/fixtures.rs @@ -2927,6 +2927,36 @@ fn test_minimum_1_6() { crate::tests::runner::run_test_case(&path, 1, 6).unwrap(); } +#[test] +fn test_paths_0_0() { + let path = format!("{}/fixtures/paths.json", env!("CARGO_MANIFEST_DIR")); + crate::tests::runner::run_test_case(&path, 0, 0).unwrap(); +} + +#[test] +fn test_paths_0_1() { + let path = format!("{}/fixtures/paths.json", env!("CARGO_MANIFEST_DIR")); + crate::tests::runner::run_test_case(&path, 0, 1).unwrap(); +} + +#[test] +fn test_paths_0_2() { + let path = format!("{}/fixtures/paths.json", env!("CARGO_MANIFEST_DIR")); + crate::tests::runner::run_test_case(&path, 0, 2).unwrap(); +} + +#[test] +fn test_paths_0_3() { + let path = format!("{}/fixtures/paths.json", env!("CARGO_MANIFEST_DIR")); + crate::tests::runner::run_test_case(&path, 0, 3).unwrap(); +} + +#[test] +fn test_paths_0_4() { + let path = format!("{}/fixtures/paths.json", env!("CARGO_MANIFEST_DIR")); + crate::tests::runner::run_test_case(&path, 0, 4).unwrap(); +} + #[test] fn test_one_of_0_0() { let path = format!("{}/fixtures/oneOf.json", env!("CARGO_MANIFEST_DIR")); diff --git a/src/validator/rules/array.rs b/src/validator/rules/array.rs index b2785b1..56acbfb 100644 --- a/src/validator/rules/array.rs +++ b/src/validator/rules/array.rs @@ -91,12 +91,17 @@ impl<'a> ValidationContext<'a> { if let Some(ref prefix) = self.schema.prefix_items { for (i, sub_schema) in prefix.iter().enumerate() { if i < len { - let path = format!("{}/{}", self.path, i); if let Some(child_instance) = arr.get(i) { + let mut item_path = format!("{}/{}", self.path, i); + if let Some(obj) = child_instance.as_object() { + if let Some(id_str) = obj.get("id").and_then(|v| v.as_str()) { + item_path = format!("{}/{}", self.path, id_str); + } + } let derived = self.derive( sub_schema, child_instance, - &path, + &item_path, HashSet::new(), self.extensible, false, @@ -112,12 +117,17 @@ impl<'a> ValidationContext<'a> { if let Some(ref items_schema) = self.schema.items { for i in validation_index..len { - let path = format!("{}/{}", self.path, i); if let Some(child_instance) = arr.get(i) { + let mut item_path = format!("{}/{}", self.path, i); + if let Some(obj) = child_instance.as_object() { + if let Some(id_str) = obj.get("id").and_then(|v| v.as_str()) { + item_path = format!("{}/{}", self.path, id_str); + } + } let derived = self.derive( items_schema, child_instance, - &path, + &item_path, HashSet::new(), self.extensible, false, diff --git a/src/validator/rules/conditionals.rs b/src/validator/rules/conditionals.rs index bd3e0c4..076f7c2 100644 --- a/src/validator/rules/conditionals.rs +++ b/src/validator/rules/conditionals.rs @@ -53,10 +53,18 @@ impl<'a> ValidationContext<'a> { if let Some(arr) = self.instance.as_array() { for i in 0..arr.len() { if !result.evaluated_indices.contains(&i) { + let mut item_path = format!("{}/{}", self.path, i); + if let Some(child_instance) = arr.get(i) { + if let Some(obj) = child_instance.as_object() { + if let Some(id_str) = obj.get("id").and_then(|v| v.as_str()) { + item_path = format!("{}/{}", self.path, id_str); + } + } + } result.errors.push(ValidationError { code: "STRICT_ITEM_VIOLATION".to_string(), message: format!("Unexpected item at index {}", i), - path: format!("{}/{}", self.path, i), + path: item_path, }); } }