use crate::{
error::{error, no_error, ErrorIterator},
evaluation::{Annotations, ErrorDescription, Evaluation, EvaluationNode},
node::SchemaNode,
paths::{LazyLocation, Location, RefTracker},
Draft, ValidationError, ValidationOptions,
};
use ahash::AHashMap;
use serde_json::Value;
pub(crate) use crate::paths::LazyEvaluationPath;
#[derive(Default)]
pub struct ValidationContext {
validating: Vec<(usize, usize)>,
is_valid_cache: Option<AHashMap<(usize, usize), bool>>,
ecma_regex_cache: Option<AHashMap<String, bool>>,
}
impl ValidationContext {
pub(crate) fn new() -> Self {
Self::default()
}
#[inline]
pub(crate) fn enter(&mut self, node_id: usize, instance: &Value) -> bool {
let key = (node_id, std::ptr::from_ref::<Value>(instance) as usize);
if self.validating.contains(&key) {
return true;
}
self.validating.push(key);
false
}
#[inline]
pub(crate) fn exit(&mut self, node_id: usize, instance: &Value) {
let key = (node_id, std::ptr::from_ref::<Value>(instance) as usize);
let popped = self.validating.pop();
debug_assert_eq!(
popped,
Some(key),
"ValidationContext::exit called out of order"
);
}
#[inline]
pub(crate) fn get_cached_result(&self, node_id: usize, instance: &Value) -> Option<bool> {
if !matches!(instance, Value::Array(_) | Value::Object(_)) {
return None;
}
let cache = self.is_valid_cache.as_ref()?;
let key = (node_id, std::ptr::from_ref::<Value>(instance) as usize);
cache.get(&key).copied()
}
#[inline]
pub(crate) fn cache_result(&mut self, node_id: usize, instance: &Value, result: bool) {
if !matches!(instance, Value::Array(_) | Value::Object(_)) {
return;
}
let key = (node_id, std::ptr::from_ref::<Value>(instance) as usize);
self.is_valid_cache
.get_or_insert_with(AHashMap::new)
.insert(key, result);
}
pub(crate) fn is_valid_ecma_regex(&mut self, pattern: &str) -> bool {
if let Some(cache) = &self.ecma_regex_cache {
if let Some(&result) = cache.get(pattern) {
return result;
}
}
let result = crate::ecma::to_rust_regex(pattern).is_ok();
self.ecma_regex_cache
.get_or_insert_with(AHashMap::new)
.insert(pattern.to_owned(), result);
result
}
}
pub(crate) trait Validate: Send + Sync {
fn iter_errors<'i>(
&self,
instance: &'i Value,
location: &LazyLocation,
tracker: Option<&RefTracker>,
ctx: &mut ValidationContext,
) -> ErrorIterator<'i> {
match self.validate(instance, location, tracker, ctx) {
Ok(()) => no_error(),
Err(err) => error(err),
}
}
fn is_valid(&self, instance: &Value, ctx: &mut ValidationContext) -> bool;
fn validate<'i>(
&self,
instance: &'i Value,
location: &LazyLocation,
tracker: Option<&RefTracker>,
ctx: &mut ValidationContext,
) -> Result<(), ValidationError<'i>>;
fn evaluate(
&self,
instance: &Value,
location: &LazyLocation,
tracker: Option<&RefTracker>,
ctx: &mut ValidationContext,
) -> EvaluationResult {
let errors: Vec<ErrorDescription> = self
.iter_errors(instance, location, tracker, ctx)
.map(|e| ErrorDescription::from_validation_error(&e))
.collect();
if errors.is_empty() {
EvaluationResult::valid_empty()
} else {
EvaluationResult::invalid_empty(errors)
}
}
fn canonical_location(&self) -> Option<&Location> {
None
}
}
#[derive(PartialEq)]
pub(crate) enum EvaluationResult {
Valid {
annotations: Option<Annotations>,
children: Vec<EvaluationNode>,
},
Invalid {
errors: Vec<ErrorDescription>,
children: Vec<EvaluationNode>,
annotations: Option<Annotations>,
},
}
impl EvaluationResult {
pub(crate) fn valid_empty() -> EvaluationResult {
EvaluationResult::Valid {
annotations: None,
children: Vec::new(),
}
}
pub(crate) fn invalid_empty(errors: Vec<ErrorDescription>) -> EvaluationResult {
EvaluationResult::Invalid {
errors,
children: Vec::new(),
annotations: None,
}
}
pub(crate) fn annotate(&mut self, new_annotations: Annotations) {
match self {
Self::Valid { annotations, .. } | Self::Invalid { annotations, .. } => {
*annotations = Some(new_annotations);
}
}
}
pub(crate) fn mark_errored(&mut self, error: ErrorDescription) {
match self {
Self::Invalid { errors, .. } => errors.push(error),
Self::Valid {
annotations,
children,
} => {
*self = Self::Invalid {
errors: vec![error],
children: std::mem::take(children),
annotations: annotations.take(),
}
}
}
}
pub(crate) fn from_children(children: Vec<EvaluationNode>) -> EvaluationResult {
if children.iter().any(|node| !node.valid) {
EvaluationResult::Invalid {
errors: Vec::new(),
children,
annotations: None,
}
} else {
EvaluationResult::Valid {
annotations: None,
children,
}
}
}
}
impl From<EvaluationNode> for EvaluationResult {
fn from(node: EvaluationNode) -> Self {
if node.valid {
EvaluationResult::Valid {
annotations: None,
children: vec![node],
}
} else {
EvaluationResult::Invalid {
errors: Vec::new(),
children: vec![node],
annotations: None,
}
}
}
}
#[derive(Clone, Debug)]
pub struct Validator {
pub(crate) root: SchemaNode,
pub(crate) draft: Draft,
}
impl Validator {
#[must_use]
pub fn options<'i>() -> ValidationOptions<'i> {
ValidationOptions::default()
}
#[cfg(feature = "resolve-async")]
#[must_use]
pub fn async_options<'i>(
) -> ValidationOptions<'i, std::sync::Arc<dyn referencing::AsyncRetrieve>> {
ValidationOptions::default()
}
pub fn new(schema: &Value) -> Result<Validator, ValidationError<'static>> {
Self::options().build(schema)
}
#[cfg(feature = "resolve-async")]
pub async fn async_new(schema: &Value) -> Result<Validator, ValidationError<'static>> {
Self::async_options().build(schema).await
}
#[inline]
pub fn validate<'i>(&self, instance: &'i Value) -> Result<(), ValidationError<'i>> {
let mut ctx = ValidationContext::new();
self.root
.validate(instance, &LazyLocation::new(), None, &mut ctx)
}
#[inline]
#[must_use]
pub fn iter_errors<'i>(&'i self, instance: &'i Value) -> ErrorIterator<'i> {
let mut ctx = ValidationContext::new();
self.root
.iter_errors(instance, &LazyLocation::new(), None, &mut ctx)
}
#[must_use]
#[inline]
pub fn is_valid(&self, instance: &Value) -> bool {
let mut ctx = ValidationContext::new();
self.root.is_valid(instance, &mut ctx)
}
#[must_use]
#[inline]
pub fn evaluate(&self, instance: &Value) -> Evaluation {
let mut ctx = ValidationContext::new();
let root = self
.root
.evaluate_instance(instance, &LazyLocation::new(), None, &mut ctx);
Evaluation::new(root)
}
#[must_use]
pub fn draft(&self) -> Draft {
self.draft
}
}
#[derive(Debug)]
pub struct ValidatorMap {
pub(crate) validators: AHashMap<String, Validator>,
}
impl ValidatorMap {
#[must_use]
pub fn get(&self, pointer: &str) -> Option<&Validator> {
self.validators.get(pointer)
}
#[must_use]
pub fn contains_key(&self, pointer: &str) -> bool {
self.validators.contains_key(pointer)
}
pub fn keys(&self) -> impl Iterator<Item = &str> {
self.validators.keys().map(String::as_str)
}
#[must_use]
pub fn len(&self) -> usize {
self.validators.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.validators.is_empty()
}
}
impl std::ops::Index<&str> for ValidatorMap {
type Output = Validator;
fn index(&self, pointer: &str) -> &Validator {
self.validators
.get(pointer)
.unwrap_or_else(|| panic!("JSON pointer '{pointer}' not found in ValidatorMap"))
}
}
#[cfg(test)]
mod tests {
use crate::{
error::ValidationError, keywords::custom::Keyword, paths::Location, Validator, ValidatorMap,
};
use fancy_regex::Regex;
use num_cmp::NumCmp;
use serde_json::{json, Map, Value};
use std::sync::LazyLock;
#[cfg(not(target_arch = "wasm32"))]
fn load(path: &str, idx: usize) -> Value {
use std::{fs::File, io::Read, path::Path};
let path = Path::new(path);
let mut file = File::open(path).unwrap();
let mut content = String::new();
file.read_to_string(&mut content).ok().unwrap();
let data: Value = serde_json::from_str(&content).unwrap();
let case = &data.as_array().unwrap()[idx];
case.get("schema").unwrap().clone()
}
#[test]
fn only_keyword() {
let schema = json!({"type": "string"});
let validator = crate::validator_for(&schema).unwrap();
let value1 = json!("AB");
let value2 = json!(1);
assert_eq!(validator.root.validators().len(), 1);
assert!(validator.validate(&value1).is_ok());
assert!(validator.validate(&value2).is_err());
}
#[test]
#[cfg(not(target_arch = "wasm32"))]
fn validate_ref() {
let schema = load("tests/suite/tests/draft7/ref.json", 1);
let value = json!({"bar": 3});
let validator = crate::validator_for(&schema).unwrap();
assert!(validator.validate(&value).is_ok());
let value = json!({"bar": true});
assert!(validator.validate(&value).is_err());
}
#[test]
fn wrong_schema_type() {
let schema = json!([1]);
let validator = crate::validator_for(&schema);
assert!(validator.is_err());
}
#[test]
fn multiple_errors() {
let schema = json!({"minProperties": 2, "propertyNames": {"minLength": 3}});
let value = json!({"a": 3});
let validator = crate::validator_for(&schema).unwrap();
let errors: Vec<_> = validator.iter_errors(&value).collect();
assert_eq!(errors.len(), 2);
assert_eq!(
errors[0].to_string(),
r#"{"a":3} has less than 2 properties"#
);
assert_eq!(errors[1].to_string(), r#""a" is shorter than 3 characters"#);
}
#[test]
fn custom_keyword_definition() {
struct CustomObjectValidator;
impl Keyword for CustomObjectValidator {
fn validate<'i>(&self, instance: &'i Value) -> Result<(), ValidationError<'i>> {
for key in instance.as_object().unwrap().keys() {
if !key.is_ascii() {
return Err(ValidationError::custom("Key is not ASCII"));
}
}
Ok(())
}
fn is_valid(&self, instance: &Value) -> bool {
for (key, _value) in instance.as_object().unwrap() {
if !key.is_ascii() {
return false;
}
}
true
}
}
fn custom_object_type_factory<'a>(
_: &'a Map<String, Value>,
schema: &'a Value,
_path: Location,
) -> Result<Box<dyn Keyword>, ValidationError<'a>> {
const EXPECTED: &str = "ascii-keys";
if schema.as_str() == Some(EXPECTED) {
Ok(Box::new(CustomObjectValidator))
} else {
Err(ValidationError::schema(format!(
"Expected '{EXPECTED}', got {schema}"
)))
}
}
let schema =
json!({ "custom-object-type": "ascii-keys", "type": "object", "minProperties": 1 });
let validator = crate::options()
.with_keyword("custom-object-type", custom_object_type_factory)
.build(&schema)
.unwrap();
let instance = json!({});
assert!(validator.validate(&instance).is_err());
assert!(!validator.is_valid(&instance));
let instance = json!({ "a" : 1 });
assert!(validator.validate(&instance).is_ok());
assert!(validator.is_valid(&instance));
let instance = json!({ "Ã¥" : 1 });
let error = validator.validate(&instance).expect_err("Should fail");
assert_eq!(error.to_string(), "Key is not ASCII");
assert!(!validator.is_valid(&instance));
}
#[test]
fn custom_format_and_override_keyword() {
fn currency_format_checker(s: &str) -> bool {
static CURRENCY_RE: LazyLock<Regex> = LazyLock::new(|| {
Regex::new("^(0|([1-9]+[0-9]*))(\\.[0-9]{2})$").expect("Invalid regex")
});
CURRENCY_RE.is_match(s).expect("Invalid regex")
}
struct CustomMinimumValidator {
limit: f64,
with_currency_format: bool,
}
impl Keyword for CustomMinimumValidator {
fn validate<'i>(&self, instance: &'i Value) -> Result<(), ValidationError<'i>> {
if self.is_valid(instance) {
Ok(())
} else {
Err(ValidationError::custom(format!(
"value is less than the minimum of {}",
self.limit
)))
}
}
fn is_valid(&self, instance: &Value) -> bool {
match instance {
Value::Number(instance) => {
if let Some(item) = instance.as_u64() {
!NumCmp::num_lt(item, self.limit)
} else if let Some(item) = instance.as_i64() {
!NumCmp::num_lt(item, self.limit)
} else {
let item = instance.as_f64().expect("Always valid");
!NumCmp::num_lt(item, self.limit)
}
}
Value::String(instance) => {
if self.with_currency_format && currency_format_checker(instance) {
let value = instance
.parse::<f64>()
.expect("format validated by regex checker");
!NumCmp::num_lt(value, self.limit)
} else {
true
}
}
_ => true,
}
}
}
fn custom_minimum_factory<'a>(
parent: &'a Map<String, Value>,
schema: &'a Value,
_path: Location,
) -> Result<Box<dyn Keyword>, ValidationError<'a>> {
let limit = if let Value::Number(limit) = schema {
limit.as_f64().expect("Always valid")
} else {
return Err(ValidationError::schema("minimum must be a number"));
};
let with_currency_format = parent
.get("format")
.is_some_and(|format| format == "currency");
Ok(Box::new(CustomMinimumValidator {
limit,
with_currency_format,
}))
}
let schema = json!({ "minimum": 2, "type": "string", "format": "currency" });
let validator = crate::options()
.with_format("currency", currency_format_checker)
.with_keyword("minimum", custom_minimum_factory)
.with_keyword("minimum-2", custom_minimum_factory)
.should_validate_formats(true)
.build(&schema)
.expect("Invalid schema");
let instance = json!(15);
assert!(validator.validate(&instance).is_err());
assert!(!validator.is_valid(&instance));
let instance = json!("not a currency");
assert!(validator.validate(&instance).is_err());
assert!(!validator.is_valid(&instance));
let instance = json!("3.00");
assert!(validator.validate(&instance).is_ok());
assert!(validator.is_valid(&instance));
let instance = json!("1.99");
assert!(validator.validate(&instance).is_err());
assert!(!validator.is_valid(&instance));
let schema = json!({ "minimum": 2, "type": "integer" });
let validator = crate::options()
.with_format("currency", currency_format_checker)
.with_keyword("minimum", custom_minimum_factory)
.build(&schema)
.expect("Invalid schema");
let instance = json!(3);
assert!(validator.validate(&instance).is_ok());
assert!(validator.is_valid(&instance));
let instance = json!(1);
assert!(validator.validate(&instance).is_err());
assert!(!validator.is_valid(&instance));
let schema = json!({ "minimum": "foo" });
let error = crate::options()
.with_keyword("minimum", custom_minimum_factory)
.build(&schema)
.expect_err("Should fail");
assert_eq!(error.to_string(), "\"foo\" is not of type \"number\"");
}
#[test]
fn custom_keyword_validation_error_paths() {
struct AlwaysFailValidator;
impl Keyword for AlwaysFailValidator {
fn validate<'i>(&self, _instance: &'i Value) -> Result<(), ValidationError<'i>> {
Err(ValidationError::custom("always fails"))
}
fn is_valid(&self, _instance: &Value) -> bool {
false
}
}
fn always_fail_factory<'a>(
_: &'a Map<String, Value>,
_: &'a Value,
_: Location,
) -> Result<Box<dyn Keyword>, ValidationError<'a>> {
Ok(Box::new(AlwaysFailValidator))
}
let schema = json!({
"properties": {
"name": { "alwaysFail": true }
}
});
let validator = crate::options()
.with_keyword("alwaysFail", always_fail_factory)
.build(&schema)
.expect("Valid schema");
let instance = json!({"name": "test"});
let error = validator.validate(&instance).expect_err("Should fail");
assert_eq!(error.instance_path().as_str(), "/name");
assert_eq!(error.schema_path().as_str(), "/properties/name/alwaysFail");
assert_eq!(
error.evaluation_path().as_str(),
"/properties/name/alwaysFail"
);
assert_eq!(error.kind().keyword(), "alwaysFail");
}
#[test]
fn custom_keyword_factory_error_schema_path() {
fn failing_factory<'a>(
_: &'a Map<String, Value>,
_: &'a Value,
_: Location,
) -> Result<Box<dyn Keyword>, ValidationError<'a>> {
Err(ValidationError::schema("invalid schema value"))
}
let schema = json!({
"properties": {
"field": { "myKeyword": "bad-value" }
}
});
let error = crate::options()
.with_keyword("myKeyword", failing_factory)
.build(&schema)
.expect_err("Should fail");
assert_eq!(error.to_string(), "invalid schema value");
assert_eq!(error.schema_path().as_str(), "/properties/field/myKeyword");
}
#[test]
fn test_validator_is_send_and_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<Validator>();
}
#[test]
fn test_validator_map_is_send_and_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<ValidatorMap>();
}
#[test]
fn test_validator_clone() {
let schema = json!({"type": "string", "minLength": 3});
let validator = crate::validator_for(&schema).expect("Valid schema");
let cloned = validator.clone();
assert!(validator.is_valid(&json!("hello")));
assert!(!validator.is_valid(&json!("hi")));
assert!(cloned.is_valid(&json!("hello")));
assert!(!cloned.is_valid(&json!("hi")));
assert_eq!(
validator.is_valid(&json!("test")),
cloned.is_valid(&json!("test"))
);
}
#[test]
fn ref_with_required_multiple_missing_clones_deferred_eval_path() {
let schema = json!({
"$defs": {
"Person": {
"type": "object",
"required": ["name", "age", "email"]
}
},
"$ref": "#/$defs/Person"
});
let validator = crate::validator_for(&schema).expect("Valid schema");
let instance = json!({});
let errors: Vec<_> = validator.iter_errors(&instance).collect();
assert_eq!(errors.len(), 3);
for error in &errors {
assert_eq!(error.schema_path().as_str(), "/$defs/Person/required");
assert_eq!(error.evaluation_path().as_str(), "/$ref/required");
}
}
#[test]
fn test_validator_map_get_existing() {
let schema = json!({
"$defs": {
"User": {"type": "object", "required": ["name"]}
}
});
let map = crate::validator_map_for(&schema).unwrap();
let v = map.get("#/$defs/User").unwrap();
assert!(v.is_valid(&json!({"name": "Alice"})));
assert!(!v.is_valid(&json!({})));
}
#[test]
fn test_validator_map_root_always_present() {
let schema = json!({"type": "string"});
let map = crate::validator_map_for(&schema).unwrap();
assert!(map.get("#").unwrap().is_valid(&json!("hello")));
assert!(!map.get("#").unwrap().is_valid(&json!(42)));
}
#[test]
fn test_validator_map_get_missing_returns_none() {
let schema = json!({"type": "object"});
let map = crate::validator_map_for(&schema).unwrap();
assert!(map.get("#/nonexistent").is_none());
}
#[test]
fn test_validator_map_contains_key() {
let schema = json!({"$defs": {"Foo": {"type": "string"}}});
let map = crate::validator_map_for(&schema).unwrap();
assert!(map.contains_key("#/$defs/Foo"));
assert!(!map.contains_key("#/missing"));
}
#[test]
fn test_validator_map_keys_includes_root_and_defs() {
let schema = json!({
"type": "object",
"$defs": {"Foo": {"type": "string"}}
});
let map = crate::validator_map_for(&schema).unwrap();
let keys: std::collections::HashSet<&str> = map.keys().collect();
assert!(keys.contains("#"));
assert!(keys.contains("#/$defs/Foo"));
}
#[test]
fn test_validator_map_index() {
let schema = json!({"$defs": {"Bar": {"type": "integer"}}});
let map = crate::validator_map_for(&schema).unwrap();
assert!(map["#/$defs/Bar"].is_valid(&json!(42)));
assert!(!map["#/$defs/Bar"].is_valid(&json!("nope")));
}
#[test]
#[should_panic(expected = "JSON pointer '#/nonexistent' not found in ValidatorMap")]
fn test_validator_map_index_panics_on_missing() {
let schema = json!({"type": "object"});
let map = crate::validator_map_for(&schema).unwrap();
let _ = &map["#/nonexistent"];
}
#[test]
fn test_validator_map_via_options() {
let schema = json!({"$defs": {"S": {"type": "string"}}});
let map = crate::options().build_map(&schema).unwrap();
assert!(map.get("#/$defs/S").unwrap().is_valid(&json!("hello")));
}
#[cfg(all(feature = "resolve-async", not(target_family = "wasm")))]
#[tokio::test]
async fn test_async_validator_map_for() {
let schema = json!({"$defs": {"T": {"type": "string"}}});
let map = crate::async_validator_map_for(&schema).await.unwrap();
assert!(map["#/$defs/T"].is_valid(&json!("hello")));
assert!(!map["#/$defs/T"].is_valid(&json!(42)));
}
#[cfg(all(feature = "resolve-async", not(target_family = "wasm")))]
#[tokio::test]
async fn test_async_build_map_via_options() {
let schema = json!({"$defs": {"N": {"type": "integer"}}});
let map = crate::async_options().build_map(&schema).await.unwrap();
assert!(map["#/$defs/N"].is_valid(&json!(7)));
assert!(!map["#/$defs/N"].is_valid(&json!("not an int")));
}
#[test]
fn test_validator_map_len() {
let schema = json!({
"$defs": {
"A": {"type": "string"},
"B": {"type": "integer"}
}
});
let map = crate::validator_map_for(&schema).unwrap();
assert!(map.len() >= 3); }
#[test]
fn test_validator_map_is_empty_false() {
let schema = json!({"type": "string"});
let map = crate::validator_map_for(&schema).unwrap();
assert!(!map.is_empty());
}
#[test]
fn test_validator_map_nested_pointer() {
let schema = json!({
"properties": {
"name": {"type": "string", "minLength": 2}
}
});
let map = crate::validator_map_for(&schema).unwrap();
assert!(map["#/properties/name"].is_valid(&json!("Al")));
assert!(!map["#/properties/name"].is_valid(&json!("A")));
}
#[test]
fn test_validator_map_with_registry() {
let address_schema = json!({
"type": "object",
"properties": {
"city": {"type": "string"},
"zip": {"type": "string"}
},
"required": ["city"]
});
let registry = crate::Registry::new()
.add("https://example.com/address.json", &address_schema)
.expect("valid resource")
.prepare()
.expect("registry build failed");
let schema = json!({
"$defs": {
"Address": {"$ref": "https://example.com/address.json"}
}
});
let map = crate::options()
.with_registry(®istry)
.build_map(&schema)
.unwrap();
let v = map.get("#/$defs/Address").unwrap();
assert!(v.is_valid(&json!({"city": "NYC", "zip": "10001"})));
assert!(!v.is_valid(&json!({"zip": "10001"}))); }
#[test]
fn test_validator_map_with_registry_multiple_refs() {
let user_schema = json!({
"type": "object",
"properties": {"name": {"type": "string"}},
"required": ["name"]
});
let role_schema = json!({
"type": "string",
"enum": ["admin", "user", "guest"]
});
let registry = crate::Registry::new()
.add("https://example.com/user.json", &user_schema)
.expect("valid resource")
.add("https://example.com/role.json", &role_schema)
.expect("valid resource")
.prepare()
.expect("registry build failed");
let schema = json!({
"$defs": {
"User": {"$ref": "https://example.com/user.json"},
"Role": {"$ref": "https://example.com/role.json"}
}
});
let map = crate::options()
.with_registry(®istry)
.build_map(&schema)
.unwrap();
assert!(map["#/$defs/User"].is_valid(&json!({"name": "Alice"})));
assert!(!map["#/$defs/User"].is_valid(&json!({})));
assert!(map["#/$defs/Role"].is_valid(&json!("admin")));
assert!(!map["#/$defs/Role"].is_valid(&json!("superuser")));
}
#[cfg(all(feature = "resolve-async", not(target_family = "wasm")))]
#[tokio::test]
async fn test_async_validator_map_with_registry() {
let address_schema = json!({
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"]
});
let registry = crate::Registry::new()
.add("https://example.com/address.json", &address_schema)
.expect("valid resource")
.prepare()
.expect("registry build failed");
let schema = json!({
"$defs": {
"Address": {"$ref": "https://example.com/address.json"}
}
});
let map = crate::async_options()
.with_registry(®istry)
.build_map(&schema)
.await
.unwrap();
let v = map.get("#/$defs/Address").unwrap();
assert!(v.is_valid(&json!({"city": "NYC"})));
assert!(!v.is_valid(&json!({})));
}
#[cfg(all(feature = "resolve-async", not(target_family = "wasm")))]
#[tokio::test]
async fn test_async_validator_map_with_registry_multiple_refs() {
let user_schema = json!({
"type": "object",
"properties": {"name": {"type": "string"}},
"required": ["name"]
});
let role_schema = json!({
"type": "string",
"enum": ["admin", "user", "guest"]
});
let registry = crate::Registry::new()
.add("https://example.com/user.json", &user_schema)
.expect("valid resource")
.add("https://example.com/role.json", &role_schema)
.expect("valid resource")
.prepare()
.expect("registry build failed");
let schema = json!({
"$defs": {
"User": {"$ref": "https://example.com/user.json"},
"Role": {"$ref": "https://example.com/role.json"}
}
});
let map = crate::async_options()
.with_registry(®istry)
.build_map(&schema)
.await
.unwrap();
assert!(map["#/$defs/User"].is_valid(&json!({"name": "Alice"})));
assert!(!map["#/$defs/User"].is_valid(&json!({})));
assert!(map["#/$defs/Role"].is_valid(&json!("admin")));
assert!(!map["#/$defs/Role"].is_valid(&json!("superuser")));
}
}