Compare commits

..

4 Commits

11 changed files with 293 additions and 23 deletions

View File

@ -142,7 +142,7 @@
"errors": [ "errors": [
{ {
"code": "CONST_VIOLATED", "code": "CONST_VIOLATED",
"path": "/con" "path": "con"
} }
] ]
} }

View File

@ -48,7 +48,7 @@
"errors": [ "errors": [
{ {
"code": "TYPE_MISMATCH", "code": "TYPE_MISMATCH",
"path": "/base_prop" "path": "base_prop"
} }
] ]
} }
@ -109,7 +109,7 @@
"errors": [ "errors": [
{ {
"code": "REQUIRED_FIELD_MISSING", "code": "REQUIRED_FIELD_MISSING",
"path": "/a" "path": "a"
} }
] ]
} }
@ -126,7 +126,7 @@
"errors": [ "errors": [
{ {
"code": "REQUIRED_FIELD_MISSING", "code": "REQUIRED_FIELD_MISSING",
"path": "/b" "path": "b"
} }
] ]
} }
@ -196,7 +196,7 @@
"errors": [ "errors": [
{ {
"code": "DEPENDENCY_FAILED", "code": "DEPENDENCY_FAILED",
"path": "/base_dep" "path": "base_dep"
} }
] ]
} }
@ -214,7 +214,7 @@
"errors": [ "errors": [
{ {
"code": "DEPENDENCY_FAILED", "code": "DEPENDENCY_FAILED",
"path": "/child_dep" "path": "child_dep"
} }
] ]
} }

214
fixtures/paths.json Normal file
View File

@ -0,0 +1,214 @@
[
{
"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

@ -677,7 +677,7 @@
"errors": [ "errors": [
{ {
"code": "TYPE_MISMATCH", "code": "TYPE_MISMATCH",
"path": "/type" "path": "type"
} }
] ]
} }
@ -782,7 +782,7 @@
"errors": [ "errors": [
{ {
"code": "TYPE_MISMATCH", "code": "TYPE_MISMATCH",
"path": "/type" "path": "type"
} }
] ]
} }

View File

@ -2927,6 +2927,36 @@ fn test_minimum_1_6() {
crate::tests::runner::run_test_case(&path, 1, 6).unwrap(); 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] #[test]
fn test_one_of_0_0() { fn test_one_of_0_0() {
let path = format!("{}/fixtures/oneOf.json", env!("CARGO_MANIFEST_DIR")); let path = format!("{}/fixtures/oneOf.json", env!("CARGO_MANIFEST_DIR"));

View File

@ -134,12 +134,12 @@ fn test_library_api() {
{ {
"code": "REQUIRED_FIELD_MISSING", "code": "REQUIRED_FIELD_MISSING",
"message": "Missing name", "message": "Missing name",
"details": { "path": "/name" } "details": { "path": "name" }
}, },
{ {
"code": "STRICT_PROPERTY_VIOLATION", "code": "STRICT_PROPERTY_VIOLATION",
"message": "Unexpected property 'wrong'", "message": "Unexpected property 'wrong'",
"details": { "path": "/wrong" } "details": { "path": "wrong" }
} }
] ]
}) })

View File

@ -41,6 +41,14 @@ impl<'a> ValidationContext<'a> {
} }
} }
pub fn join_path(&self, key: &str) -> String {
if self.path.is_empty() {
key.to_string()
} else {
format!("{}/{}", self.path, key)
}
}
pub fn derive( pub fn derive(
&self, &self,
schema: &'a Schema, schema: &'a Schema,

View File

@ -91,12 +91,17 @@ impl<'a> ValidationContext<'a> {
if let Some(ref prefix) = self.schema.prefix_items { if let Some(ref prefix) = self.schema.prefix_items {
for (i, sub_schema) in prefix.iter().enumerate() { for (i, sub_schema) in prefix.iter().enumerate() {
if i < len { if i < len {
let path = format!("{}/{}", self.path, i);
if let Some(child_instance) = arr.get(i) { if let Some(child_instance) = arr.get(i) {
let mut item_path = self.join_path(&i.to_string());
if let Some(obj) = child_instance.as_object() {
if let Some(id_str) = obj.get("id").and_then(|v| v.as_str()) {
item_path = self.join_path(id_str);
}
}
let derived = self.derive( let derived = self.derive(
sub_schema, sub_schema,
child_instance, child_instance,
&path, &item_path,
HashSet::new(), HashSet::new(),
self.extensible, self.extensible,
false, false,
@ -112,12 +117,17 @@ impl<'a> ValidationContext<'a> {
if let Some(ref items_schema) = self.schema.items { if let Some(ref items_schema) = self.schema.items {
for i in validation_index..len { for i in validation_index..len {
let path = format!("{}/{}", self.path, i);
if let Some(child_instance) = arr.get(i) { if let Some(child_instance) = arr.get(i) {
let mut item_path = self.join_path(&i.to_string());
if let Some(obj) = child_instance.as_object() {
if let Some(id_str) = obj.get("id").and_then(|v| v.as_str()) {
item_path = self.join_path(id_str);
}
}
let derived = self.derive( let derived = self.derive(
items_schema, items_schema,
child_instance, child_instance,
&path, &item_path,
HashSet::new(), HashSet::new(),
self.extensible, self.extensible,
false, false,

View File

@ -44,7 +44,7 @@ impl<'a> ValidationContext<'a> {
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), message: format!("Unexpected property '{}'", key),
path: format!("{}/{}", self.path, key), path: self.join_path(key),
}); });
} }
} }
@ -53,10 +53,18 @@ impl<'a> ValidationContext<'a> {
if let Some(arr) = self.instance.as_array() { if let Some(arr) = self.instance.as_array() {
for i in 0..arr.len() { for i in 0..arr.len() {
if !result.evaluated_indices.contains(&i) { if !result.evaluated_indices.contains(&i) {
let mut item_path = self.join_path(&i.to_string());
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 = self.join_path(id_str);
}
}
}
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), message: format!("Unexpected item at index {}", i),
path: format!("{}/{}", self.path, i), path: item_path,
}); });
} }
} }

View File

@ -32,7 +32,7 @@ impl<'a> ValidationContext<'a> {
"Type '{}' is not a valid descendant for this entity bound schema", "Type '{}' is not a valid descendant for this entity bound schema",
type_str type_str
), ),
path: format!("{}/type", self.path), path: self.join_path("type"),
}); });
} }
} else { } else {
@ -70,7 +70,7 @@ impl<'a> ValidationContext<'a> {
result.errors.push(ValidationError { result.errors.push(ValidationError {
code: "REQUIRED_FIELD_MISSING".to_string(), code: "REQUIRED_FIELD_MISSING".to_string(),
message: format!("Missing {}", field), message: format!("Missing {}", field),
path: format!("{}/{}", self.path, field), path: self.join_path(field),
}); });
} }
} }
@ -109,7 +109,7 @@ impl<'a> ValidationContext<'a> {
} }
if let Some(child_instance) = obj.get(key) { if let Some(child_instance) = obj.get(key) {
let new_path = format!("{}/{}", self.path, key); let new_path = self.join_path(key);
let is_ref = sub_schema.r#ref.is_some(); let is_ref = sub_schema.r#ref.is_some();
let next_extensible = if is_ref { false } else { self.extensible }; let next_extensible = if is_ref { false } else { self.extensible };
@ -147,7 +147,7 @@ impl<'a> ValidationContext<'a> {
for (compiled_re, sub_schema) in compiled_pp { for (compiled_re, sub_schema) in compiled_pp {
for (key, child_instance) in obj { for (key, child_instance) in obj {
if compiled_re.0.is_match(key) { if compiled_re.0.is_match(key) {
let new_path = format!("{}/{}", self.path, key); let new_path = self.join_path(key);
let is_ref = sub_schema.r#ref.is_some(); let is_ref = sub_schema.r#ref.is_some();
let next_extensible = if is_ref { false } else { self.extensible }; let next_extensible = if is_ref { false } else { self.extensible };
@ -186,7 +186,7 @@ impl<'a> ValidationContext<'a> {
} }
if !locally_matched { if !locally_matched {
let new_path = format!("{}/{}", self.path, key); let new_path = self.join_path(key);
let is_ref = additional_schema.r#ref.is_some(); let is_ref = additional_schema.r#ref.is_some();
let next_extensible = if is_ref { false } else { self.extensible }; let next_extensible = if is_ref { false } else { self.extensible };
@ -207,7 +207,7 @@ impl<'a> ValidationContext<'a> {
if let Some(ref property_names) = self.schema.property_names { if let Some(ref property_names) = self.schema.property_names {
for key in obj.keys() { for key in obj.keys() {
let _new_path = format!("{}/propertyNames/{}", self.path, key); let _new_path = self.join_path(&format!("propertyNames/{}", key));
let val_str = Value::String(key.to_string()); let val_str = Value::String(key.to_string());
let ctx = ValidationContext::new( let ctx = ValidationContext::new(

View File

@ -1 +1 @@
1.0.91 1.0.93