validator now uses hybrid uuid and numeric index pathing

This commit is contained in:
2026-03-26 14:06:24 -04:00
parent 9fe5a34163
commit 8bf3649465
4 changed files with 231 additions and 5 deletions

178
fixtures/paths.json Normal file
View File

@ -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"
}
]
}
}
]
}
]

View File

@ -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"));

View File

@ -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,

View File

@ -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,
});
}
}