mirror of
https://github.com/microsoft/regorus.git
synced 2026-08-05 02:16:11 +00:00
Builds on #57. Swap Value::Object's payload from Rc<BTreeMap<Value, Value>> to Rc<Object> and migrate all call sites to the Object API. as_object / as_object_mut keep their names but return &Object / &mut Object. The mutable accessor handles Rc::make_mut internally, so callers no longer do it themselves. Object grows into_value() and From<Object> for Value. Value's serializer now delegates to Object::serialize, dropping a duplicate non-string-key stringification path. RVM IterationState::Object is rewritten around ObjectCursor: O(log n) steps over a shared Rc<Object>, no eager pair snapshot. Snapshot independence is preserved by Rc copy-on-write; setup_next_iteration advances the cursor inline and advance() becomes a no-op for this variant. A new iteration_state_object_is_snapshot_independent_of_source test covers CoW against a mutated alias. Value::Set still wraps Rc<BTreeSet<Value>>; the matching Set abstraction and its swap ship in follow-up PRs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
735 lines
28 KiB
Rust
735 lines
28 KiB
Rust
// Copyright (c) Microsoft Corporation.
|
|
// Licensed under the MIT License.
|
|
|
|
#![allow(missing_debug_implementations)] // validator is zero-sized marker
|
|
#![allow(dead_code)]
|
|
#![allow(clippy::pattern_type_mismatch, clippy::needless_continue)]
|
|
|
|
use crate::{
|
|
schema::{error::ValidationError, Schema, Type},
|
|
value::Object,
|
|
*,
|
|
};
|
|
use alloc::collections::BTreeMap;
|
|
use regex::Regex;
|
|
|
|
type String = Rc<str>;
|
|
|
|
/// Validator for checking if a Value conforms to a Schema.
|
|
pub struct SchemaValidator;
|
|
|
|
impl SchemaValidator {
|
|
/// Validates a Value against a Schema.
|
|
///
|
|
/// # Arguments
|
|
/// * `value` - The Value to validate
|
|
/// * `schema` - The Schema to validate against
|
|
///
|
|
/// # Returns
|
|
/// * `Ok(())` if the value conforms to the schema
|
|
/// * `Err(ValidationError)` if validation fails
|
|
///
|
|
/// # Example
|
|
/// ```rust
|
|
/// use regorus::schema::{Schema, validate::SchemaValidator};
|
|
/// use regorus::Value;
|
|
/// use serde_json::json;
|
|
///
|
|
/// let schema_json = json!({
|
|
/// "type": "string",
|
|
/// "minLength": 1,
|
|
/// "maxLength": 10
|
|
/// });
|
|
/// let schema = Schema::from_serde_json_value(schema_json).unwrap();
|
|
/// let value = Value::from("hello");
|
|
///
|
|
/// let result = SchemaValidator::validate(&value, &schema);
|
|
/// assert!(result.is_ok());
|
|
/// ```
|
|
pub fn validate(value: &Value, schema: &Schema) -> Result<(), ValidationError> {
|
|
Self::validate_with_path(value, schema, "")
|
|
}
|
|
|
|
/// Internal validation function that tracks the current path for error reporting.
|
|
fn validate_with_path(
|
|
value: &Value,
|
|
schema: &Schema,
|
|
path: &str,
|
|
) -> Result<(), ValidationError> {
|
|
match schema.as_type() {
|
|
Type::Any { .. } => {
|
|
// Any type accepts all values
|
|
Ok(())
|
|
}
|
|
Type::Integer {
|
|
minimum, maximum, ..
|
|
} => Self::validate_integer(value, *minimum, *maximum, path),
|
|
Type::Number {
|
|
minimum, maximum, ..
|
|
} => Self::validate_number(value, *minimum, *maximum, path),
|
|
Type::Boolean { .. } => Self::validate_boolean(value, path),
|
|
Type::Null { .. } => Self::validate_null(value, path),
|
|
Type::String {
|
|
min_length,
|
|
max_length,
|
|
pattern,
|
|
..
|
|
} => Self::validate_string(value, *min_length, *max_length, pattern.as_ref(), path),
|
|
Type::Array {
|
|
items,
|
|
min_items,
|
|
max_items,
|
|
..
|
|
} => Self::validate_array(value, items, *min_items, *max_items, path),
|
|
Type::Object {
|
|
properties,
|
|
required,
|
|
additional_properties,
|
|
discriminated_subobject,
|
|
..
|
|
} => Self::validate_object(
|
|
value,
|
|
properties,
|
|
required.as_ref().map(|r| &**r),
|
|
additional_properties.as_ref(),
|
|
discriminated_subobject.as_ref().map(|d| &**d),
|
|
path,
|
|
),
|
|
Type::AnyOf(schemas) => Self::validate_any_of(value, schemas, path),
|
|
Type::Const {
|
|
value: const_value, ..
|
|
} => Self::validate_const(value, const_value, path),
|
|
Type::Enum { values, .. } => Self::validate_enum(value, values, path),
|
|
Type::Set { items, .. } => Self::validate_set(value, items, path),
|
|
}
|
|
}
|
|
|
|
fn validate_integer(
|
|
value: &Value,
|
|
minimum: Option<i64>,
|
|
maximum: Option<i64>,
|
|
path: &str,
|
|
) -> Result<(), ValidationError> {
|
|
match value {
|
|
Value::Number(num) => {
|
|
if let Some(int_val) = num.as_i64() {
|
|
if let Some(min) = minimum {
|
|
if int_val < min {
|
|
return Err(ValidationError::OutOfRange {
|
|
value: int_val.to_string().into(),
|
|
min: Some(min.to_string().into()),
|
|
max: maximum.map(|m| m.to_string().into()),
|
|
path: path.to_string().into(),
|
|
});
|
|
}
|
|
}
|
|
if let Some(max) = maximum {
|
|
if int_val > max {
|
|
return Err(ValidationError::OutOfRange {
|
|
value: int_val.to_string().into(),
|
|
min: minimum.map(|m| m.to_string().into()),
|
|
max: Some(max.to_string().into()),
|
|
path: path.into(),
|
|
});
|
|
}
|
|
}
|
|
Ok(())
|
|
} else {
|
|
Err(ValidationError::TypeMismatch {
|
|
expected: "integer".into(),
|
|
actual: "non-integer number".into(),
|
|
path: path.into(),
|
|
})
|
|
}
|
|
}
|
|
_ => Err(ValidationError::TypeMismatch {
|
|
expected: "integer".into(),
|
|
actual: Self::value_type_name(value),
|
|
path: path.into(),
|
|
}),
|
|
}
|
|
}
|
|
|
|
fn validate_number(
|
|
value: &Value,
|
|
minimum: Option<f64>,
|
|
maximum: Option<f64>,
|
|
path: &str,
|
|
) -> Result<(), ValidationError> {
|
|
match value {
|
|
Value::Number(num) => {
|
|
if let Some(float_val) = num.as_f64() {
|
|
if let Some(min) = minimum {
|
|
if float_val < min {
|
|
return Err(ValidationError::OutOfRange {
|
|
value: float_val.to_string().into(),
|
|
min: Some(min.to_string().into()),
|
|
max: maximum.map(|m| m.to_string().into()),
|
|
path: path.into(),
|
|
});
|
|
}
|
|
}
|
|
if let Some(max) = maximum {
|
|
if float_val > max {
|
|
return Err(ValidationError::OutOfRange {
|
|
value: float_val.to_string().into(),
|
|
min: minimum.map(|m| m.to_string().into()),
|
|
max: Some(max.to_string().into()),
|
|
path: path.to_string().into(),
|
|
});
|
|
}
|
|
}
|
|
Ok(())
|
|
} else {
|
|
Err(ValidationError::TypeMismatch {
|
|
expected: "number".into(),
|
|
actual: "non-numeric value".into(),
|
|
path: path.into(),
|
|
})
|
|
}
|
|
}
|
|
_ => Err(ValidationError::TypeMismatch {
|
|
expected: "number".into(),
|
|
actual: Self::value_type_name(value),
|
|
path: path.into(),
|
|
}),
|
|
}
|
|
}
|
|
|
|
fn validate_boolean(value: &Value, path: &str) -> Result<(), ValidationError> {
|
|
match value {
|
|
Value::Bool(_) => Ok(()),
|
|
_ => Err(ValidationError::TypeMismatch {
|
|
expected: "boolean".into(),
|
|
actual: Self::value_type_name(value),
|
|
path: path.into(),
|
|
}),
|
|
}
|
|
}
|
|
|
|
fn validate_null(value: &Value, path: &str) -> Result<(), ValidationError> {
|
|
match value {
|
|
Value::Null => Ok(()),
|
|
_ => Err(ValidationError::TypeMismatch {
|
|
expected: "null".into(),
|
|
actual: Self::value_type_name(value),
|
|
path: path.into(),
|
|
}),
|
|
}
|
|
}
|
|
|
|
fn validate_string(
|
|
value: &Value,
|
|
min_length: Option<usize>,
|
|
max_length: Option<usize>,
|
|
pattern: Option<&String>,
|
|
path: &str,
|
|
) -> Result<(), ValidationError> {
|
|
match value {
|
|
Value::String(string_value) => {
|
|
let str_len = string_value.len();
|
|
|
|
// Check length constraints
|
|
if let Some(min) = min_length {
|
|
if str_len < min {
|
|
return Err(ValidationError::LengthConstraint {
|
|
actual_length: str_len,
|
|
min_length: Some(min),
|
|
max_length,
|
|
path: path.into(),
|
|
});
|
|
}
|
|
}
|
|
if let Some(max) = max_length {
|
|
if str_len > max {
|
|
return Err(ValidationError::LengthConstraint {
|
|
actual_length: str_len,
|
|
min_length,
|
|
max_length: Some(max),
|
|
path: path.into(),
|
|
});
|
|
}
|
|
}
|
|
|
|
// Check pattern constraint
|
|
if let Some(pattern_str) = pattern {
|
|
let regex =
|
|
Regex::new(pattern_str).map_err(|e| ValidationError::InvalidPattern {
|
|
pattern: pattern_str.as_ref().into(),
|
|
error: e.to_string().into(),
|
|
})?;
|
|
|
|
if !regex.is_match(string_value) {
|
|
return Err(ValidationError::PatternMismatch {
|
|
value: string_value.to_string().into(),
|
|
pattern: pattern_str.clone(),
|
|
path: path.into(),
|
|
});
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
_ => Err(ValidationError::TypeMismatch {
|
|
expected: "string".into(),
|
|
actual: Self::value_type_name(value),
|
|
path: path.into(),
|
|
}),
|
|
}
|
|
}
|
|
|
|
fn validate_array(
|
|
value: &Value,
|
|
items_schema: &Schema,
|
|
min_items: Option<usize>,
|
|
max_items: Option<usize>,
|
|
path: &str,
|
|
) -> Result<(), ValidationError> {
|
|
match value {
|
|
Value::Array(array_value) => {
|
|
let arr_len = array_value.len();
|
|
|
|
// Check size constraints
|
|
if let Some(min) = min_items {
|
|
if arr_len < min {
|
|
return Err(ValidationError::ArraySizeConstraint {
|
|
actual_size: arr_len,
|
|
min_items: Some(min),
|
|
max_items,
|
|
path: path.into(),
|
|
});
|
|
}
|
|
}
|
|
if let Some(max) = max_items {
|
|
if arr_len > max {
|
|
return Err(ValidationError::ArraySizeConstraint {
|
|
actual_size: arr_len,
|
|
min_items,
|
|
max_items: Some(max),
|
|
path: path.into(),
|
|
});
|
|
}
|
|
}
|
|
|
|
// Validate each item
|
|
for (index, item) in array_value.iter().enumerate() {
|
|
Self::validate_with_path(
|
|
item,
|
|
items_schema,
|
|
&if path.is_empty() {
|
|
format!("[{index}]")
|
|
} else {
|
|
format!("{path}[{index}]")
|
|
},
|
|
)
|
|
.map_err(|e| {
|
|
ValidationError::ArrayItemValidationFailed {
|
|
index,
|
|
path: path.into(),
|
|
error: Box::new(e),
|
|
}
|
|
})?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
_ => Err(ValidationError::TypeMismatch {
|
|
expected: "array".into(),
|
|
actual: Self::value_type_name(value),
|
|
path: path.into(),
|
|
}),
|
|
}
|
|
}
|
|
|
|
fn validate_object(
|
|
value: &Value,
|
|
properties: &BTreeMap<String, Schema>,
|
|
required: Option<&Vec<String>>,
|
|
additional_properties: Option<&Schema>,
|
|
discriminated_subobject: Option<&crate::schema::DiscriminatedSubobject>,
|
|
path: &str,
|
|
) -> Result<(), ValidationError> {
|
|
match value {
|
|
Value::Object(object_value) => {
|
|
// Check required properties
|
|
if let Some(required_props) = required {
|
|
for required_prop in required_props.iter() {
|
|
if !object_value.contains_key(&Value::String(required_prop.clone())) {
|
|
return Err(ValidationError::MissingRequiredProperty {
|
|
property: required_prop.clone(),
|
|
path: path.into(),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Handle discriminated subobjects (allOf with if/then)
|
|
// Validates against the appropriate variant schema based on discriminator field value
|
|
if let Some(discriminated_subobject) = discriminated_subobject {
|
|
Self::validate_discriminated_subobject_with_base(
|
|
object_value,
|
|
discriminated_subobject,
|
|
properties,
|
|
additional_properties,
|
|
path,
|
|
)?;
|
|
} else {
|
|
// Only validate regular object properties if no discriminated subobject exists
|
|
// Validate each property
|
|
for (prop_name, prop_value) in object_value.iter() {
|
|
// First, ensure the property key is a string
|
|
let prop_name_str = match prop_name {
|
|
Value::String(string_key) => string_key,
|
|
_ => {
|
|
return Err(ValidationError::NonStringKey {
|
|
key_type: Self::value_type_name(prop_name),
|
|
path: path.into(),
|
|
});
|
|
}
|
|
};
|
|
|
|
// Create property path lazily using a closure
|
|
let make_prop_path = || {
|
|
if path.is_empty() {
|
|
format!("[{prop_name_str}]")
|
|
} else {
|
|
format!("{path}.{prop_name_str}")
|
|
}
|
|
};
|
|
|
|
if let Some(prop_schema) = properties.get(prop_name_str) {
|
|
// Property is defined in schema, validate against it
|
|
Self::validate_with_path(prop_value, prop_schema, &make_prop_path())
|
|
.map_err(|e| ValidationError::PropertyValidationFailed {
|
|
property: prop_name_str.clone(),
|
|
path: path.into(),
|
|
error: Box::new(e),
|
|
})?;
|
|
} else if let Some(additional_schema) = additional_properties {
|
|
// Property is not defined but additional properties are allowed
|
|
Self::validate_with_path(
|
|
prop_value,
|
|
additional_schema,
|
|
&make_prop_path(),
|
|
)
|
|
.map_err(|e| {
|
|
ValidationError::PropertyValidationFailed {
|
|
property: prop_name_str.clone(),
|
|
path: path.into(),
|
|
error: Box::new(e),
|
|
}
|
|
})?;
|
|
} else {
|
|
// Property is not defined and additional properties are not allowed
|
|
return Err(ValidationError::AdditionalPropertiesNotAllowed {
|
|
property: prop_name_str.clone(),
|
|
path: path.into(),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
_ => Err(ValidationError::TypeMismatch {
|
|
expected: "object".into(),
|
|
actual: Self::value_type_name(value),
|
|
path: path.into(),
|
|
}),
|
|
}
|
|
}
|
|
|
|
fn validate_any_of(
|
|
value: &Value,
|
|
schemas: &Vec<Schema>,
|
|
path: &str,
|
|
) -> Result<(), ValidationError> {
|
|
let mut errors = Vec::new();
|
|
|
|
for schema in schemas {
|
|
match Self::validate_with_path(value, schema, path) {
|
|
Ok(()) => return Ok(()), // If any schema matches, validation succeeds
|
|
Err(e) => errors.push(e),
|
|
}
|
|
}
|
|
|
|
// If no schema matched, return error with all validation attempts
|
|
Err(ValidationError::NoUnionMatch {
|
|
path: path.into(),
|
|
errors,
|
|
})
|
|
}
|
|
|
|
fn validate_const(
|
|
value: &Value,
|
|
const_value: &Value,
|
|
path: &str,
|
|
) -> Result<(), ValidationError> {
|
|
if value == const_value {
|
|
Ok(())
|
|
} else {
|
|
let expected_json =
|
|
serde_json::to_string(const_value).unwrap_or_else(|_| format!("{const_value:?}"));
|
|
let actual_json = serde_json::to_string(value).unwrap_or_else(|_| format!("{value:?}"));
|
|
|
|
Err(ValidationError::ConstMismatch {
|
|
expected: expected_json.into(),
|
|
actual: actual_json.into(),
|
|
path: path.into(),
|
|
})
|
|
}
|
|
}
|
|
|
|
fn validate_enum(
|
|
value: &Value,
|
|
allowed_values: &[Value],
|
|
path: &str,
|
|
) -> Result<(), ValidationError> {
|
|
if allowed_values.contains(value) {
|
|
Ok(())
|
|
} else {
|
|
// Convert Value to JSON string, fallback to debug format if JSON serialization fails
|
|
let value_json = serde_json::to_string(value).unwrap_or_else(|_| format!("{value:?}"));
|
|
|
|
let allowed_json: Vec<String> = allowed_values
|
|
.iter()
|
|
.map(|v| {
|
|
serde_json::to_string(v)
|
|
.unwrap_or_else(|_| format!("{v:?}"))
|
|
.into()
|
|
})
|
|
.collect();
|
|
|
|
Err(ValidationError::NotInEnum {
|
|
value: value_json.into(),
|
|
allowed_values: allowed_json,
|
|
path: path.into(),
|
|
})
|
|
}
|
|
}
|
|
|
|
fn validate_set(
|
|
value: &Value,
|
|
items_schema: &Schema,
|
|
path: &str,
|
|
) -> Result<(), ValidationError> {
|
|
match value {
|
|
Value::Set(set_value) => {
|
|
// Validate each item in the set
|
|
for (index, item) in set_value.iter().enumerate() {
|
|
Self::validate_with_path(
|
|
item,
|
|
items_schema,
|
|
&if path.is_empty() {
|
|
format!("{{{index}}}]")
|
|
} else {
|
|
format!("{path}{{{index}}}]")
|
|
},
|
|
)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
_ => Err(ValidationError::TypeMismatch {
|
|
expected: "set".into(),
|
|
actual: Self::value_type_name(value),
|
|
path: path.into(),
|
|
}),
|
|
}
|
|
}
|
|
|
|
fn validate_discriminated_subobject_with_base(
|
|
object_value: &Object,
|
|
discriminated_subobject: &crate::schema::DiscriminatedSubobject,
|
|
base_properties: &BTreeMap<String, Schema>,
|
|
base_additional_properties: Option<&Schema>,
|
|
path: &str,
|
|
) -> Result<(), ValidationError> {
|
|
let discriminator_field = &discriminated_subobject.discriminator;
|
|
let discriminator_key = Value::String(discriminator_field.clone());
|
|
|
|
// Find the discriminator field value in the object
|
|
let discriminator_value = object_value.get(&discriminator_key).ok_or_else(|| {
|
|
ValidationError::MissingDiscriminator {
|
|
discriminator: discriminator_field.clone(),
|
|
path: path.into(),
|
|
}
|
|
})?;
|
|
|
|
// Extract the string value from the discriminator field
|
|
let discriminator_str = match discriminator_value {
|
|
Value::String(string_value) => string_value.as_ref(),
|
|
_ => {
|
|
return Err(ValidationError::TypeMismatch {
|
|
expected: "string".into(),
|
|
actual: Self::value_type_name(discriminator_value),
|
|
path: format!("{path}.{discriminator_field}").into(),
|
|
});
|
|
}
|
|
};
|
|
|
|
// Find the corresponding variant schema
|
|
let variant_schema = discriminated_subobject
|
|
.variants
|
|
.get(discriminator_str)
|
|
.ok_or_else(|| ValidationError::UnknownDiscriminatorValue {
|
|
discriminator: discriminator_field.clone(),
|
|
value: discriminator_str.into(),
|
|
allowed_values: discriminated_subobject.variants.keys().cloned().collect(),
|
|
path: path.into(),
|
|
})?;
|
|
|
|
// Validate all properties against the appropriate schemas
|
|
for (prop_name, prop_value) in object_value.iter() {
|
|
// First, ensure the property key is a string
|
|
let prop_name_str = match prop_name {
|
|
Value::String(string_key) => string_key,
|
|
_ => {
|
|
return Err(ValidationError::NonStringKey {
|
|
key_type: Self::value_type_name(prop_name),
|
|
path: path.into(),
|
|
});
|
|
}
|
|
};
|
|
|
|
// Create property path lazily using a closure
|
|
let make_prop_path = || {
|
|
if path.is_empty() {
|
|
format!("[{prop_name_str}]")
|
|
} else {
|
|
format!("{path}.{prop_name_str}")
|
|
}
|
|
};
|
|
|
|
// Check if this property is defined in the variant schema first
|
|
if variant_schema.properties.get(prop_name_str).is_some() {
|
|
// Validate later in subobject.
|
|
continue;
|
|
}
|
|
|
|
// Check if this property is defined in the base schema properties
|
|
if let Some(prop_schema) = base_properties.get(prop_name_str) {
|
|
// Property is defined in base schema, validate against it
|
|
Self::validate_with_path(prop_value, prop_schema, &make_prop_path()).map_err(
|
|
|e| ValidationError::PropertyValidationFailed {
|
|
property: prop_name_str.clone(),
|
|
path: path.into(),
|
|
error: Box::new(e),
|
|
},
|
|
)?;
|
|
continue;
|
|
}
|
|
|
|
// Check if additional properties are allowed in the variant
|
|
if variant_schema.additional_properties.is_some() {
|
|
// Property is not defined but additional properties are allowed in variant.
|
|
// Validate later.
|
|
continue;
|
|
} else if let Some(base_additional) = base_additional_properties {
|
|
// Check if additional properties are allowed in the base schema
|
|
Self::validate_with_path(prop_value, base_additional, &make_prop_path()).map_err(
|
|
|e| ValidationError::PropertyValidationFailed {
|
|
property: prop_name_str.clone(),
|
|
path: path.into(),
|
|
error: Box::new(e),
|
|
},
|
|
)?;
|
|
} else {
|
|
// Property is not defined and additional properties are not allowed
|
|
return Err(ValidationError::AdditionalPropertiesNotAllowed {
|
|
property: prop_name_str.clone(),
|
|
path: path.into(),
|
|
});
|
|
}
|
|
}
|
|
|
|
// Validate the object against the variant schema for required properties
|
|
Self::validate_subobject(object_value, variant_schema, path).map_err(|e| {
|
|
ValidationError::DiscriminatedSubobjectValidationFailed {
|
|
discriminator: discriminator_field.clone(),
|
|
value: discriminator_str.into(),
|
|
path: path.into(),
|
|
error: Box::new(e),
|
|
}
|
|
})
|
|
}
|
|
|
|
fn validate_subobject(
|
|
object_value: &Object,
|
|
subobject: &crate::schema::Subobject,
|
|
path: &str,
|
|
) -> Result<(), ValidationError> {
|
|
// Check required properties from the subobject
|
|
if let Some(required_props) = &subobject.required {
|
|
for required_prop in required_props.iter() {
|
|
if !object_value.contains_key(&Value::String(required_prop.clone())) {
|
|
return Err(ValidationError::MissingRequiredProperty {
|
|
property: required_prop.clone(),
|
|
path: path.into(),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Validate each property in the subobject
|
|
for (prop_name, prop_schema) in subobject.properties.iter() {
|
|
let prop_key = Value::String(prop_name.clone());
|
|
if let Some(prop_value) = object_value.get(&prop_key) {
|
|
Self::validate_with_path(
|
|
prop_value,
|
|
prop_schema,
|
|
&if path.is_empty() {
|
|
format!("[{prop_name}]")
|
|
} else {
|
|
format!("{path}.{prop_name}")
|
|
},
|
|
)
|
|
.map_err(|e| ValidationError::PropertyValidationFailed {
|
|
property: prop_name.clone(),
|
|
path: path.into(),
|
|
error: Box::new(e),
|
|
})?;
|
|
}
|
|
}
|
|
|
|
// Handle additional properties if specified
|
|
if let Some(additional_schema) = &subobject.additional_properties {
|
|
for (prop_name, prop_value) in object_value.iter() {
|
|
if let Value::String(prop_name_str) = prop_name {
|
|
if !subobject.properties.contains_key(prop_name_str) {
|
|
Self::validate_with_path(
|
|
prop_value,
|
|
additional_schema,
|
|
&if path.is_empty() {
|
|
format!("[{prop_name_str}]")
|
|
} else {
|
|
format!("{path}.{prop_name_str}")
|
|
},
|
|
)
|
|
.map_err(|e| {
|
|
ValidationError::PropertyValidationFailed {
|
|
property: prop_name_str.clone(),
|
|
path: path.into(),
|
|
error: Box::new(e),
|
|
}
|
|
})?;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
fn value_type_name(value: &Value) -> String {
|
|
match value {
|
|
Value::Null => "null".into(),
|
|
Value::Bool(_) => "boolean".into(),
|
|
Value::Number(_) => "number".into(),
|
|
Value::String(_) => "string".into(),
|
|
Value::Array(_) => "array".into(),
|
|
Value::Set(_) => "set".into(),
|
|
Value::Object(_) => "object".into(),
|
|
Value::Undefined => "undefined".into(),
|
|
}
|
|
}
|
|
}
|