mirror of
https://github.com/microsoft/regorus.git
synced 2026-08-05 02:16:11 +00:00
feat: Add Schema Registry and Validation Framework (#456)
* feat: Add Schema Registry and Validation Framework This commit introduces a comprehensive schema registry and validation framework, providing schema-based validation of resources and policy effects. - Thread-safe, in-memory registry for schema storage and management - Global registry patterns for effects and resources - Concurrent access with proper error handling - Unicode schema names support - JSON Schema-compliant validation for all primitive types - Advanced constraint validation (patterns, ranges, length limits) - Discriminated union support with anyOf schemas - Detailed error reporting with nested validation paths - Discriminated subobject validation for polymorphic schemas - **Registry Tests**: All registry operations - **Effect Tests**: Policy effect validation - **Resource Tests**: Resource validation - **Validation Tests**: Core validation engine - Thread-safety, error handling, integration scenarios, edge cases - **Dependencies**: dashmap, once_cell, regex - **Thread Safety**: Minimal locking with Rc<Schema> sharing - **Error Types**: TypeMismatch, OutOfRange, PatternMismatch, etc. - Complete schema registry and validation subsystem - Comprehensive test coverage - Foundation for policy validation in Regorus Benchmarks: - Criterion benchmarks for basic types, effects and Azure resources - Performance range: 3.22ns (string) to 34.74µs (Azure VM resource schema validation) - String withs patterns validation: 30.2µs. Need to explore whether regex caching helps bring this down. - Azure policy effects: 188ns-1.4µs Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com> * Address PR feedback - move error to a separate file - use meaningful var names Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com> * Refactor - Reusable Registry struct - Split and simplify tests Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com> --------- Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
This commit is contained in:
committed by
GitHub
parent
77f8544868
commit
db718654b5
70
Cargo.lock
generated
70
Cargo.lock
generated
@@ -264,7 +264,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef4f52386a59ca4c860f7393bcf8abd8dfd91ecccc0f774635ff68e92eeef491"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2 1.0.95",
|
||||
"proc-macro2 1.0.96",
|
||||
"quote 1.0.40",
|
||||
"syn 2.0.104",
|
||||
]
|
||||
@@ -351,6 +351,19 @@ version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5"
|
||||
|
||||
[[package]]
|
||||
name = "dashmap"
|
||||
version = "5.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"hashbrown 0.14.5",
|
||||
"lock_api",
|
||||
"once_cell",
|
||||
"parking_lot_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "data-encoding"
|
||||
version = "2.9.0"
|
||||
@@ -363,7 +376,7 @@ version = "0.2.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.95",
|
||||
"proc-macro2 1.0.96",
|
||||
"quote 1.0.40",
|
||||
"syn 2.0.104",
|
||||
]
|
||||
@@ -452,9 +465,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "glob"
|
||||
version = "0.3.2"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
|
||||
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
|
||||
|
||||
[[package]]
|
||||
name = "globset"
|
||||
@@ -478,6 +491,12 @@ dependencies = [
|
||||
"crunchy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.14.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.5"
|
||||
@@ -628,7 +647,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
"hashbrown 0.15.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -699,9 +718,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.174"
|
||||
version = "0.2.175"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
|
||||
checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
|
||||
|
||||
[[package]]
|
||||
name = "litemap"
|
||||
@@ -962,9 +981,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.95"
|
||||
version = "1.0.96"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
|
||||
checksum = "beef09f85ae72cea1ef96ba6870c51e6382ebfa4f0e85b643459331f3daa5be0"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
@@ -984,7 +1003,7 @@ version = "1.0.40"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.95",
|
||||
"proc-macro2 1.0.96",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1066,7 +1085,7 @@ version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1165225c21bff1f3bbce98f5a1f889949bc902d3575308cc7b0de30b4f6d27c7"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.95",
|
||||
"proc-macro2 1.0.96",
|
||||
"quote 1.0.40",
|
||||
"syn 2.0.104",
|
||||
]
|
||||
@@ -1124,6 +1143,7 @@ dependencies = [
|
||||
"chrono-tz",
|
||||
"clap",
|
||||
"criterion",
|
||||
"dashmap",
|
||||
"data-encoding",
|
||||
"globset",
|
||||
"jsonschema",
|
||||
@@ -1179,7 +1199,7 @@ version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2ee4885492bb655bfa05d039cd9163eb8fe9f79ddebf00ca23a1637510c2fd2"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.95",
|
||||
"proc-macro2 1.0.96",
|
||||
"quote 1.0.40",
|
||||
"syn 2.0.104",
|
||||
]
|
||||
@@ -1211,7 +1231,7 @@ version = "1.0.219"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.95",
|
||||
"proc-macro2 1.0.96",
|
||||
"quote 1.0.40",
|
||||
"syn 2.0.104",
|
||||
]
|
||||
@@ -1294,7 +1314,7 @@ version = "2.0.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.95",
|
||||
"proc-macro2 1.0.96",
|
||||
"quote 1.0.40",
|
||||
"unicode-ident",
|
||||
]
|
||||
@@ -1305,7 +1325,7 @@ version = "0.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.95",
|
||||
"proc-macro2 1.0.96",
|
||||
"quote 1.0.40",
|
||||
"syn 2.0.104",
|
||||
]
|
||||
@@ -1385,9 +1405,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.17.0"
|
||||
version = "1.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d"
|
||||
checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"js-sys",
|
||||
@@ -1457,7 +1477,7 @@ checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"log",
|
||||
"proc-macro2 1.0.95",
|
||||
"proc-macro2 1.0.96",
|
||||
"quote 1.0.40",
|
||||
"syn 2.0.104",
|
||||
"wasm-bindgen-shared",
|
||||
@@ -1479,7 +1499,7 @@ version = "0.2.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.95",
|
||||
"proc-macro2 1.0.96",
|
||||
"quote 1.0.40",
|
||||
"syn 2.0.104",
|
||||
"wasm-bindgen-backend",
|
||||
@@ -1533,7 +1553,7 @@ version = "0.60.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.95",
|
||||
"proc-macro2 1.0.96",
|
||||
"quote 1.0.40",
|
||||
"syn 2.0.104",
|
||||
]
|
||||
@@ -1544,7 +1564,7 @@ version = "0.59.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.95",
|
||||
"proc-macro2 1.0.96",
|
||||
"quote 1.0.40",
|
||||
"syn 2.0.104",
|
||||
]
|
||||
@@ -1753,7 +1773,7 @@ version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.95",
|
||||
"proc-macro2 1.0.96",
|
||||
"quote 1.0.40",
|
||||
"syn 2.0.104",
|
||||
"synstructure",
|
||||
@@ -1774,7 +1794,7 @@ version = "0.8.26"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.95",
|
||||
"proc-macro2 1.0.96",
|
||||
"quote 1.0.40",
|
||||
"syn 2.0.104",
|
||||
]
|
||||
@@ -1794,7 +1814,7 @@ version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.95",
|
||||
"proc-macro2 1.0.96",
|
||||
"quote 1.0.40",
|
||||
"syn 2.0.104",
|
||||
"synstructure",
|
||||
@@ -1828,7 +1848,7 @@ version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.95",
|
||||
"proc-macro2 1.0.96",
|
||||
"quote 1.0.40",
|
||||
"syn 2.0.104",
|
||||
]
|
||||
|
||||
@@ -23,7 +23,7 @@ default = ["full-opa", "arc"]
|
||||
|
||||
arc = ["scientific/arc"]
|
||||
ast = []
|
||||
azure_policy = ["dep:jsonschema"]
|
||||
azure_policy = ["dep:jsonschema", "arc", "dashmap"]
|
||||
base64 = ["dep:data-encoding"]
|
||||
base64url = ["dep:data-encoding"]
|
||||
coverage = []
|
||||
@@ -112,6 +112,7 @@ rand = { version = "0.9.0", default-features = false, features = ["thread_rng"],
|
||||
|
||||
# Causes the project to link with the Spectre-mitigated CRT and libs.
|
||||
msvc_spectre_libs = { version = "0.1", features = ["error"], optional = true }
|
||||
dashmap = { version = "5.5", default-features = false, optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
anyhow = "1.0.45"
|
||||
@@ -151,6 +152,11 @@ test=false
|
||||
name = "regorus_benchmark"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "schema_validation_benchmark"
|
||||
harness = false
|
||||
required-features = ["azure_policy"]
|
||||
|
||||
[[example]]
|
||||
name="regorus"
|
||||
harness=false
|
||||
|
||||
886
benches/schema_validation_benchmark.rs
Normal file
886
benches/schema_validation_benchmark.rs
Normal file
@@ -0,0 +1,886 @@
|
||||
use criterion::{criterion_group, criterion_main, Criterion};
|
||||
use regorus::Value;
|
||||
use regorus::{Schema, SchemaValidator};
|
||||
use serde_json::json;
|
||||
|
||||
// Observed: validate_string - 3.19 ns/iter
|
||||
fn bench_string_validation(c: &mut Criterion) {
|
||||
let schema_json = json!({
|
||||
"type": "string",
|
||||
"minLength": 3,
|
||||
"maxLength": 10
|
||||
});
|
||||
let schema = Schema::from_serde_json_value(schema_json).unwrap();
|
||||
let value = Value::from("hello");
|
||||
|
||||
c.bench_function("validate_string", |b| {
|
||||
b.iter(|| {
|
||||
SchemaValidator::validate(&value, &schema).unwrap();
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
// Observed: validate_number - 146.5 ns/iter
|
||||
fn bench_number_validation(c: &mut Criterion) {
|
||||
let schema_json = json!({
|
||||
"type": "number",
|
||||
"minimum": 0.0,
|
||||
"maximum": 100.0
|
||||
});
|
||||
let schema = Schema::from_serde_json_value(schema_json).unwrap();
|
||||
let value = Value::from(42.5);
|
||||
|
||||
c.bench_function("validate_number", |b| {
|
||||
b.iter(|| {
|
||||
SchemaValidator::validate(&value, &schema).unwrap();
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
// Observed: validate_array - 95.0 ns/iter
|
||||
fn bench_array_validation(c: &mut Criterion) {
|
||||
let schema_json = json!({
|
||||
"type": "array",
|
||||
"items": { "type": "integer" },
|
||||
"minItems": 2,
|
||||
"maxItems": 5
|
||||
});
|
||||
let schema = Schema::from_serde_json_value(schema_json).unwrap();
|
||||
let value = Value::from(json!([1, 2, 3]));
|
||||
|
||||
c.bench_function("validate_array", |b| {
|
||||
b.iter(|| {
|
||||
SchemaValidator::validate(&value, &schema).unwrap();
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
// Observed: validate_object - 126.9 ns/iter
|
||||
fn bench_object_validation(c: &mut Criterion) {
|
||||
let schema_json = json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": { "type": "string" },
|
||||
"age": { "type": "integer", "minimum": 0 }
|
||||
},
|
||||
"required": ["name", "age"]
|
||||
});
|
||||
let schema = Schema::from_serde_json_value(schema_json).unwrap();
|
||||
let value = Value::from(json!({"name": "Alice", "age": 30}));
|
||||
|
||||
c.bench_function("validate_object", |b| {
|
||||
b.iter(|| {
|
||||
SchemaValidator::validate(&value, &schema).unwrap();
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
// Observed: validate_complex_nested - 710.5 ns/iter
|
||||
fn bench_complex_nested_validation(c: &mut Criterion) {
|
||||
let schema_json = json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"user": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": { "type": "string" },
|
||||
"profile": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"email": { "type": "string" },
|
||||
"roles": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"required": ["email", "roles"]
|
||||
}
|
||||
},
|
||||
"required": ["id", "profile"]
|
||||
},
|
||||
"active": { "type": "boolean" }
|
||||
},
|
||||
"required": ["user", "active"]
|
||||
});
|
||||
let schema = Schema::from_serde_json_value(schema_json).unwrap();
|
||||
let value = Value::from(json!({
|
||||
"user": {
|
||||
"id": "u123",
|
||||
"profile": {
|
||||
"email": "alice@example.com",
|
||||
"roles": ["admin", "user"]
|
||||
}
|
||||
},
|
||||
"active": true
|
||||
}));
|
||||
|
||||
c.bench_function("validate_complex_nested", |b| {
|
||||
b.iter(|| {
|
||||
SchemaValidator::validate(&value, &schema).unwrap();
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
// Observed: validate_string_pattern - 29.99 µs/iter
|
||||
fn bench_string_pattern_validation(c: &mut Criterion) {
|
||||
let schema_json = json!({
|
||||
"type": "string",
|
||||
"pattern": "^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$"
|
||||
});
|
||||
let schema = Schema::from_serde_json_value(schema_json).unwrap();
|
||||
let value = Value::from("user@example.com");
|
||||
|
||||
c.bench_function("validate_string_pattern", |b| {
|
||||
b.iter(|| {
|
||||
SchemaValidator::validate(&value, &schema).unwrap();
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
// Observed: validate_enum - 7.26 ns/iter
|
||||
fn bench_enum_validation(c: &mut Criterion) {
|
||||
let schema_json = json!({
|
||||
"enum": ["pending", "approved", "rejected", "cancelled"]
|
||||
});
|
||||
let schema = Schema::from_serde_json_value(schema_json).unwrap();
|
||||
let value = Value::from("approved");
|
||||
|
||||
c.bench_function("validate_enum", |b| {
|
||||
b.iter(|| {
|
||||
SchemaValidator::validate(&value, &schema).unwrap();
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
// Observed: validate_boolean - 3.22 ns/iter
|
||||
fn bench_boolean_validation(c: &mut Criterion) {
|
||||
let schema_json = json!({
|
||||
"type": "boolean"
|
||||
});
|
||||
let schema = Schema::from_serde_json_value(schema_json).unwrap();
|
||||
let value = Value::from(true);
|
||||
|
||||
c.bench_function("validate_boolean", |b| {
|
||||
b.iter(|| {
|
||||
SchemaValidator::validate(&value, &schema).unwrap();
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
// Observed: validate_null - 3.22 ns/iter
|
||||
fn bench_null_validation(c: &mut Criterion) {
|
||||
let schema_json = json!({
|
||||
"type": "null"
|
||||
});
|
||||
let schema = Schema::from_serde_json_value(schema_json).unwrap();
|
||||
let value = Value::Null;
|
||||
|
||||
c.bench_function("validate_null", |b| {
|
||||
b.iter(|| {
|
||||
SchemaValidator::validate(&value, &schema).unwrap();
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
// Observed: validate_large_array - 17.30 µs/iter
|
||||
fn bench_large_array_validation(c: &mut Criterion) {
|
||||
let schema_json = json!({
|
||||
"type": "array",
|
||||
"items": { "type": "number" },
|
||||
"minItems": 50,
|
||||
"maxItems": 200
|
||||
});
|
||||
let schema = Schema::from_serde_json_value(schema_json).unwrap();
|
||||
let large_array: Vec<_> = (0..100).map(|i| json!(i as f64)).collect();
|
||||
let value = Value::from(json!(large_array));
|
||||
|
||||
c.bench_function("validate_large_array", |b| {
|
||||
b.iter(|| {
|
||||
SchemaValidator::validate(&value, &schema).unwrap();
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
// Observed: validate_deeply_nested - 468.2 ns/iter
|
||||
fn bench_deeply_nested_object(c: &mut Criterion) {
|
||||
let schema_json = json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"level1": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"level2": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"level3": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"level4": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"level5": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["level5"]
|
||||
}
|
||||
},
|
||||
"required": ["level4"]
|
||||
}
|
||||
},
|
||||
"required": ["level3"]
|
||||
}
|
||||
},
|
||||
"required": ["level2"]
|
||||
}
|
||||
},
|
||||
"required": ["level1"]
|
||||
});
|
||||
let schema = Schema::from_serde_json_value(schema_json).unwrap();
|
||||
let value = Value::from(json!({
|
||||
"level1": {
|
||||
"level2": {
|
||||
"level3": {
|
||||
"level4": {
|
||||
"level5": "deep value"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
c.bench_function("validate_deeply_nested", |b| {
|
||||
b.iter(|| {
|
||||
SchemaValidator::validate(&value, &schema).unwrap();
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
// Observed: validate_mixed_type_array - 1.36 µs/iter
|
||||
fn bench_mixed_type_array(c: &mut Criterion) {
|
||||
let schema_json = json!({
|
||||
"type": "array",
|
||||
"items": {
|
||||
"anyOf": [
|
||||
{ "type": "string" },
|
||||
{ "type": "number" },
|
||||
{ "type": "boolean" }
|
||||
]
|
||||
}
|
||||
});
|
||||
let schema = Schema::from_serde_json_value(schema_json).unwrap();
|
||||
let value = Value::from(json!(["hello", 42, true, "world", 3.14, false]));
|
||||
|
||||
c.bench_function("validate_mixed_type_array", |b| {
|
||||
b.iter(|| {
|
||||
SchemaValidator::validate(&value, &schema).unwrap();
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
// Observed: validate_additional_properties - 366.4 ns/iter
|
||||
fn bench_additional_properties(c: &mut Criterion) {
|
||||
let schema_json = json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": { "type": "string" },
|
||||
"age": { "type": "integer" }
|
||||
},
|
||||
"additionalProperties": { "type": "string" },
|
||||
"required": ["name"]
|
||||
});
|
||||
let schema = Schema::from_serde_json_value(schema_json).unwrap();
|
||||
let value = Value::from(json!({
|
||||
"name": "Alice",
|
||||
"age": 30,
|
||||
"city": "New York",
|
||||
"country": "USA",
|
||||
"occupation": "Engineer"
|
||||
}));
|
||||
|
||||
c.bench_function("validate_additional_properties", |b| {
|
||||
b.iter(|| {
|
||||
SchemaValidator::validate(&value, &schema).unwrap();
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
// Observed: validate_array_constraints - 146.2 ns/iter
|
||||
fn bench_array_constraints(c: &mut Criterion) {
|
||||
let schema_json = json!({
|
||||
"type": "array",
|
||||
"items": { "type": "string" },
|
||||
"minItems": 2,
|
||||
"maxItems": 10
|
||||
});
|
||||
let schema = Schema::from_serde_json_value(schema_json).unwrap();
|
||||
let value = Value::from(json!(["apple", "banana", "cherry", "date", "elderberry"]));
|
||||
|
||||
c.bench_function("validate_array_constraints", |b| {
|
||||
b.iter(|| {
|
||||
SchemaValidator::validate(&value, &schema).unwrap();
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
// Observed: validate_multi_level - 915.8 ns/iter
|
||||
fn bench_multi_level_validation(c: &mut Criterion) {
|
||||
let schema_json = json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"user": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"profile": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"settings": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"theme": {
|
||||
"enum": ["light", "dark", "auto"]
|
||||
},
|
||||
"notifications": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"required": ["theme"],
|
||||
"additionalProperties": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"required": ["settings"],
|
||||
"additionalProperties": { "type": "any" }
|
||||
}
|
||||
},
|
||||
"required": ["profile"],
|
||||
"additionalProperties": { "type": "any" }
|
||||
}
|
||||
},
|
||||
"required": ["user"],
|
||||
"additionalProperties": { "type": "any" }
|
||||
});
|
||||
let schema = Schema::from_serde_json_value(schema_json).unwrap();
|
||||
let value = Value::from(json!({
|
||||
"user": {
|
||||
"profile": {
|
||||
"settings": {
|
||||
"theme": "dark",
|
||||
"notifications": true,
|
||||
"language": "en"
|
||||
},
|
||||
"avatar": "default.png"
|
||||
},
|
||||
"lastLogin": "2024-01-01"
|
||||
},
|
||||
"metadata": "extra info"
|
||||
}));
|
||||
|
||||
c.bench_function("validate_multi_level", |b| {
|
||||
b.iter(|| {
|
||||
SchemaValidator::validate(&value, &schema).unwrap();
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
// Azure Resource Validation Benchmarks
|
||||
|
||||
// Observed: validate_azure_vm_resource - 34.74 µs/iter
|
||||
fn bench_azure_vm_resource_validation(c: &mut Criterion) {
|
||||
let schema_json = json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"const": "Microsoft.Compute/virtualMachines"
|
||||
},
|
||||
"apiVersion": {
|
||||
"enum": ["2021-03-01", "2021-07-01", "2022-03-01"]
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-zA-Z0-9-._]{1,64}$"
|
||||
},
|
||||
"location": {
|
||||
"type": "string",
|
||||
"description": "Azure region where the VM will be deployed"
|
||||
},
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"hardwareProfile": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"vmSize": {
|
||||
"enum": ["Standard_B1s", "Standard_B2s", "Standard_D2s_v3", "Standard_D4s_v3"]
|
||||
}
|
||||
},
|
||||
"required": ["vmSize"]
|
||||
},
|
||||
"osProfile": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"computerName": {
|
||||
"type": "string"
|
||||
},
|
||||
"adminUsername": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["computerName", "adminUsername"]
|
||||
}
|
||||
},
|
||||
"required": ["hardwareProfile", "osProfile"]
|
||||
}
|
||||
},
|
||||
"required": ["type", "apiVersion", "name", "location", "properties"],
|
||||
"additionalProperties": { "type": "any" }
|
||||
});
|
||||
let schema = Schema::from_serde_json_value(schema_json).unwrap();
|
||||
let value = Value::from(json!({
|
||||
"type": "Microsoft.Compute/virtualMachines",
|
||||
"apiVersion": "2021-03-01",
|
||||
"name": "my-vm-01",
|
||||
"location": "eastus",
|
||||
"properties": {
|
||||
"hardwareProfile": {
|
||||
"vmSize": "Standard_B2s"
|
||||
},
|
||||
"osProfile": {
|
||||
"computerName": "my-computer",
|
||||
"adminUsername": "azureuser"
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
c.bench_function("validate_azure_vm_resource", |b| {
|
||||
b.iter(|| {
|
||||
SchemaValidator::validate(&value, &schema).unwrap();
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
// Observed: validate_azure_storage_resource - 22.12 µs/iter
|
||||
fn bench_azure_storage_resource_validation(c: &mut Criterion) {
|
||||
let schema_json = json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"const": "Microsoft.Storage/storageAccounts"
|
||||
},
|
||||
"apiVersion": {
|
||||
"enum": ["2021-04-01", "2021-06-01", "2022-05-01"]
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z0-9]{3,24}$"
|
||||
},
|
||||
"location": {
|
||||
"type": "string"
|
||||
},
|
||||
"sku": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"enum": ["Standard_LRS", "Standard_GRS", "Standard_RAGRS", "Premium_LRS"]
|
||||
}
|
||||
},
|
||||
"required": ["name"]
|
||||
},
|
||||
"kind": {
|
||||
"enum": ["Storage", "StorageV2", "BlobStorage", "FileStorage", "BlockBlobStorage"]
|
||||
},
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"accessTier": {
|
||||
"enum": ["Hot", "Cool", "Archive"]
|
||||
},
|
||||
"encryption": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"services": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": { "type": "any" }
|
||||
}
|
||||
},
|
||||
"required": ["type", "apiVersion", "name", "location", "sku", "kind"],
|
||||
"additionalProperties": { "type": "any" }
|
||||
});
|
||||
let schema = Schema::from_serde_json_value(schema_json).unwrap();
|
||||
let value = Value::from(json!({
|
||||
"type": "Microsoft.Storage/storageAccounts",
|
||||
"apiVersion": "2021-04-01",
|
||||
"name": "mystorageaccount001",
|
||||
"location": "westus2",
|
||||
"sku": {
|
||||
"name": "Standard_LRS"
|
||||
},
|
||||
"kind": "StorageV2",
|
||||
"properties": {
|
||||
"accessTier": "Hot",
|
||||
"encryption": {
|
||||
"services": {}
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
c.bench_function("validate_azure_storage_resource", |b| {
|
||||
b.iter(|| {
|
||||
SchemaValidator::validate(&value, &schema).unwrap();
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
// Observed: validate_azure_arm_template - 1.99 µs/iter
|
||||
fn bench_azure_arm_template_validation(c: &mut Criterion) {
|
||||
let schema_json = json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"$schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"contentVersion": {
|
||||
"type": "string"
|
||||
},
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"additionalProperties": { "type": "any" }
|
||||
},
|
||||
"variables": {
|
||||
"type": "object",
|
||||
"additionalProperties": { "type": "any" }
|
||||
},
|
||||
"resources": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string"
|
||||
},
|
||||
"apiVersion": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"location": {
|
||||
"type": "string"
|
||||
},
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"additionalProperties": { "type": "any" }
|
||||
},
|
||||
"tags": {
|
||||
"type": "object",
|
||||
"additionalProperties": { "type": "string" }
|
||||
}
|
||||
},
|
||||
"required": ["type", "apiVersion", "name"],
|
||||
"additionalProperties": { "type": "any" }
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"type": "object",
|
||||
"additionalProperties": { "type": "any" }
|
||||
}
|
||||
},
|
||||
"required": ["resources"],
|
||||
"additionalProperties": { "type": "any" }
|
||||
});
|
||||
let schema = Schema::from_serde_json_value(schema_json).unwrap();
|
||||
let value = Value::from(json!({
|
||||
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
|
||||
"contentVersion": "1.0.0.0",
|
||||
"parameters": {
|
||||
"vmName": {
|
||||
"type": "string",
|
||||
"defaultValue": "myVM"
|
||||
}
|
||||
},
|
||||
"variables": {
|
||||
"storageAccountName": "[concat('storage', uniqueString(resourceGroup().id))]"
|
||||
},
|
||||
"resources": [
|
||||
{
|
||||
"type": "Microsoft.Compute/virtualMachines",
|
||||
"apiVersion": "2021-03-01",
|
||||
"name": "[parameters('vmName')]",
|
||||
"location": "[resourceGroup().location]",
|
||||
"properties": {
|
||||
"hardwareProfile": {
|
||||
"vmSize": "Standard_B1s"
|
||||
}
|
||||
},
|
||||
"tags": {
|
||||
"environment": "dev",
|
||||
"project": "test"
|
||||
}
|
||||
}
|
||||
],
|
||||
"outputs": {
|
||||
"vmId": {
|
||||
"type": "string",
|
||||
"value": "[resourceId('Microsoft.Compute/virtualMachines', parameters('vmName'))]"
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
c.bench_function("validate_azure_arm_template", |b| {
|
||||
b.iter(|| {
|
||||
SchemaValidator::validate(&value, &schema).unwrap();
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
// Azure Policy Effect Validation Benchmarks
|
||||
|
||||
// Observed: validate_azure_policy_deny_effect - 188.6 ns/iter
|
||||
fn bench_azure_policy_deny_effect_validation(c: &mut Criterion) {
|
||||
let schema_json = json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"effect": {
|
||||
"const": "deny"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["effect"],
|
||||
"additionalProperties": { "type": "any" }
|
||||
});
|
||||
let schema = Schema::from_serde_json_value(schema_json).unwrap();
|
||||
let value = Value::from(json!({
|
||||
"effect": "deny",
|
||||
"description": "Deny resources that don't meet security requirements"
|
||||
}));
|
||||
|
||||
c.bench_function("validate_azure_policy_deny_effect", |b| {
|
||||
b.iter(|| {
|
||||
SchemaValidator::validate(&value, &schema).unwrap();
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
// Observed: validate_azure_policy_audit_effect - 516.7 ns/iter
|
||||
fn bench_azure_policy_audit_effect_validation(c: &mut Criterion) {
|
||||
let schema_json = json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"effect": {
|
||||
"const": "audit"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"auditDetails": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"category": {
|
||||
"enum": ["security", "compliance", "cost", "operational"]
|
||||
},
|
||||
"severity": {
|
||||
"enum": ["low", "medium", "high", "critical"]
|
||||
}
|
||||
},
|
||||
"additionalProperties": { "type": "any" }
|
||||
}
|
||||
},
|
||||
"required": ["effect"],
|
||||
"additionalProperties": { "type": "any" }
|
||||
});
|
||||
let schema = Schema::from_serde_json_value(schema_json).unwrap();
|
||||
let value = Value::from(json!({
|
||||
"effect": "audit",
|
||||
"description": "Audit non-compliant resources",
|
||||
"auditDetails": {
|
||||
"category": "security",
|
||||
"severity": "high"
|
||||
}
|
||||
}));
|
||||
|
||||
c.bench_function("validate_azure_policy_audit_effect", |b| {
|
||||
b.iter(|| {
|
||||
SchemaValidator::validate(&value, &schema).unwrap();
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
// Observed: validate_azure_policy_modify_effect - 1.17 µs/iter
|
||||
fn bench_azure_policy_modify_effect_validation(c: &mut Criterion) {
|
||||
let schema_json = json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"effect": {
|
||||
"const": "modify"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
"modifyDetails": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"roleDefinitionIds": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
},
|
||||
"operations": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"operation": {
|
||||
"enum": ["add", "replace", "remove"]
|
||||
},
|
||||
"field": {
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"type": "any"
|
||||
}
|
||||
},
|
||||
"required": ["operation", "field"],
|
||||
"additionalProperties": { "type": "any" }
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["roleDefinitionIds", "operations"],
|
||||
"additionalProperties": { "type": "any" }
|
||||
}
|
||||
},
|
||||
"required": ["effect", "modifyDetails"],
|
||||
"additionalProperties": { "type": "any" }
|
||||
});
|
||||
let schema = Schema::from_serde_json_value(schema_json).unwrap();
|
||||
let value = Value::from(json!({
|
||||
"effect": "modify",
|
||||
"description": "Modify resources to ensure compliance",
|
||||
"modifyDetails": {
|
||||
"roleDefinitionIds": [
|
||||
"/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c"
|
||||
],
|
||||
"operations": [
|
||||
{
|
||||
"operation": "add",
|
||||
"field": "tags.environment",
|
||||
"value": "production"
|
||||
}
|
||||
]
|
||||
}
|
||||
}));
|
||||
|
||||
c.bench_function("validate_azure_policy_modify_effect", |b| {
|
||||
b.iter(|| {
|
||||
SchemaValidator::validate(&value, &schema).unwrap();
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
// Observed: validate_azure_policy_complex_effect - 1.40 µs/iter
|
||||
fn bench_azure_policy_complex_effect_validation(c: &mut Criterion) {
|
||||
let schema_json = json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"effect": {
|
||||
"enum": ["auditIfNotExists", "deployIfNotExists"]
|
||||
},
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"additionalProperties": { "type": "any" }
|
||||
},
|
||||
"existenceCondition": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"field": { "type": "string" },
|
||||
"equals": { "type": "string" }
|
||||
},
|
||||
"required": ["field"],
|
||||
"additionalProperties": { "type": "any" }
|
||||
},
|
||||
"deployment": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"mode": {
|
||||
"enum": ["incremental", "complete"]
|
||||
},
|
||||
"template": {
|
||||
"type": "object",
|
||||
"additionalProperties": { "type": "any" }
|
||||
},
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"additionalProperties": { "type": "any" }
|
||||
}
|
||||
},
|
||||
"required": ["mode", "template"],
|
||||
"additionalProperties": { "type": "any" }
|
||||
}
|
||||
},
|
||||
"required": ["properties"],
|
||||
"additionalProperties": { "type": "any" }
|
||||
}
|
||||
},
|
||||
"required": ["effect"],
|
||||
"additionalProperties": { "type": "any" }
|
||||
});
|
||||
let schema = Schema::from_serde_json_value(schema_json).unwrap();
|
||||
let value = Value::from(json!({
|
||||
"effect": "deployIfNotExists",
|
||||
"parameters": {},
|
||||
"existenceCondition": {
|
||||
"field": "Microsoft.Security/complianceResults/resourceStatus",
|
||||
"equals": "OffByPolicy"
|
||||
},
|
||||
"deployment": {
|
||||
"properties": {
|
||||
"mode": "incremental",
|
||||
"template": {
|
||||
"$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
|
||||
"contentVersion": "1.0.0.0",
|
||||
"resources": []
|
||||
},
|
||||
"parameters": {}
|
||||
}
|
||||
}
|
||||
}));
|
||||
|
||||
c.bench_function("validate_azure_policy_complex_effect", |b| {
|
||||
b.iter(|| {
|
||||
SchemaValidator::validate(&value, &schema).unwrap();
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
criterion_group!(
|
||||
schema_validation_benches,
|
||||
bench_string_validation,
|
||||
bench_number_validation,
|
||||
bench_array_validation,
|
||||
bench_object_validation,
|
||||
bench_complex_nested_validation,
|
||||
bench_string_pattern_validation,
|
||||
bench_enum_validation,
|
||||
bench_boolean_validation,
|
||||
bench_null_validation,
|
||||
bench_large_array_validation,
|
||||
bench_deeply_nested_object,
|
||||
bench_mixed_type_array,
|
||||
bench_additional_properties,
|
||||
bench_array_constraints,
|
||||
bench_multi_level_validation,
|
||||
bench_azure_vm_resource_validation,
|
||||
bench_azure_storage_resource_validation,
|
||||
bench_azure_arm_template_validation,
|
||||
bench_azure_policy_deny_effect_validation,
|
||||
bench_azure_policy_audit_effect_validation,
|
||||
bench_azure_policy_modify_effect_validation,
|
||||
bench_azure_policy_complex_effect_validation
|
||||
);
|
||||
criterion_main!(schema_validation_benches);
|
||||
74
bindings/ffi/Cargo.lock
generated
74
bindings/ffi/Cargo.lock
generated
@@ -42,9 +42,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.19"
|
||||
version = "0.6.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933"
|
||||
checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
@@ -72,22 +72,22 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.1.3"
|
||||
version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9"
|
||||
checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2"
|
||||
dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.9"
|
||||
version = "3.0.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882"
|
||||
checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"once_cell_polyfill",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.60.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -178,9 +178,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.31"
|
||||
version = "1.2.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3a42d84bb6b69d3a8b3eaacf0d88f179e1929695e1ad012b6cf64d9caaa5fd2"
|
||||
checksum = "2352e5597e9c544d5e6d9c95190d5d27738ade584fa8db0a16e130e5c2b5296e"
|
||||
dependencies = [
|
||||
"shlex",
|
||||
]
|
||||
@@ -217,18 +217,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.42"
|
||||
version = "4.5.43"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed87a9d530bb41a67537289bafcac159cb3ee28460e0a4571123d2a778a6a882"
|
||||
checksum = "50fd97c9dc2399518aa331917ac6f274280ec5eb34e555dd291899745c48ec6f"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.42"
|
||||
version = "4.5.43"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "64f4f3f3c77c94aff3c7e9aac9a2ca1974a5adf392a8bb751e827d6d127ab966"
|
||||
checksum = "c35b5830294e1fa0462034af85cc95225a4cb07092c088c55bda3147cfcd8f65"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@@ -264,6 +264,19 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dashmap"
|
||||
version = "5.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"hashbrown 0.14.5",
|
||||
"lock_api",
|
||||
"once_cell",
|
||||
"parking_lot_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "data-encoding"
|
||||
version = "2.9.0"
|
||||
@@ -379,9 +392,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.4"
|
||||
version = "0.14.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5"
|
||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
@@ -527,7 +546,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown",
|
||||
"hashbrown 0.15.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -586,9 +605,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.174"
|
||||
version = "0.2.175"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
|
||||
checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
@@ -797,9 +816,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.95"
|
||||
version = "1.0.96"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
|
||||
checksum = "beef09f85ae72cea1ef96ba6870c51e6382ebfa4f0e85b643459331f3daa5be0"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
@@ -927,6 +946,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"chrono-tz",
|
||||
"dashmap",
|
||||
"data-encoding",
|
||||
"globset",
|
||||
"jsonschema",
|
||||
@@ -969,9 +989,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.21"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
|
||||
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
@@ -1218,9 +1238,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.17.0"
|
||||
version = "1.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d"
|
||||
checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"js-sys",
|
||||
@@ -1626,9 +1646,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zerovec"
|
||||
version = "0.11.2"
|
||||
version = "0.11.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428"
|
||||
checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b"
|
||||
dependencies = [
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
|
||||
28
bindings/java/Cargo.lock
generated
28
bindings/java/Cargo.lock
generated
@@ -115,9 +115,9 @@ checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.31"
|
||||
version = "1.2.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3a42d84bb6b69d3a8b3eaacf0d88f179e1929695e1ad012b6cf64d9caaa5fd2"
|
||||
checksum = "2352e5597e9c544d5e6d9c95190d5d27738ade584fa8db0a16e130e5c2b5296e"
|
||||
dependencies = [
|
||||
"shlex",
|
||||
]
|
||||
@@ -273,9 +273,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.4"
|
||||
version = "0.15.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5"
|
||||
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
@@ -490,9 +490,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.174"
|
||||
version = "0.2.175"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
|
||||
checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
|
||||
|
||||
[[package]]
|
||||
name = "litemap"
|
||||
@@ -689,9 +689,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.95"
|
||||
version = "1.0.96"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
|
||||
checksum = "beef09f85ae72cea1ef96ba6870c51e6382ebfa4f0e85b643459331f3daa5be0"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
@@ -847,9 +847,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.21"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
|
||||
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
@@ -1050,9 +1050,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.17.0"
|
||||
version = "1.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d"
|
||||
checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"js-sys",
|
||||
@@ -1460,9 +1460,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zerovec"
|
||||
version = "0.11.2"
|
||||
version = "0.11.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428"
|
||||
checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b"
|
||||
dependencies = [
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
|
||||
28
bindings/python/Cargo.lock
generated
28
bindings/python/Cargo.lock
generated
@@ -109,9 +109,9 @@ checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.31"
|
||||
version = "1.2.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3a42d84bb6b69d3a8b3eaacf0d88f179e1929695e1ad012b6cf64d9caaa5fd2"
|
||||
checksum = "2352e5597e9c544d5e6d9c95190d5d27738ade584fa8db0a16e130e5c2b5296e"
|
||||
dependencies = [
|
||||
"shlex",
|
||||
]
|
||||
@@ -251,9 +251,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.4"
|
||||
version = "0.15.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5"
|
||||
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
@@ -458,9 +458,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.174"
|
||||
version = "0.2.175"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
|
||||
checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
|
||||
|
||||
[[package]]
|
||||
name = "litemap"
|
||||
@@ -681,9 +681,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.95"
|
||||
version = "1.0.96"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
|
||||
checksum = "beef09f85ae72cea1ef96ba6870c51e6382ebfa4f0e85b643459331f3daa5be0"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
@@ -904,9 +904,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.21"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
|
||||
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
@@ -1090,9 +1090,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.17.0"
|
||||
version = "1.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d"
|
||||
checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"js-sys",
|
||||
@@ -1406,9 +1406,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zerovec"
|
||||
version = "0.11.2"
|
||||
version = "0.11.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428"
|
||||
checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b"
|
||||
dependencies = [
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
|
||||
32
bindings/ruby/Cargo.lock
generated
32
bindings/ruby/Cargo.lock
generated
@@ -129,9 +129,9 @@ checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.31"
|
||||
version = "1.2.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3a42d84bb6b69d3a8b3eaacf0d88f179e1929695e1ad012b6cf64d9caaa5fd2"
|
||||
checksum = "2352e5597e9c544d5e6d9c95190d5d27738ade584fa8db0a16e130e5c2b5296e"
|
||||
dependencies = [
|
||||
"shlex",
|
||||
]
|
||||
@@ -285,9 +285,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "glob"
|
||||
version = "0.3.2"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
|
||||
checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
|
||||
|
||||
[[package]]
|
||||
name = "globset"
|
||||
@@ -303,9 +303,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.4"
|
||||
version = "0.15.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5"
|
||||
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
@@ -513,9 +513,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.174"
|
||||
version = "0.2.175"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
|
||||
checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
|
||||
|
||||
[[package]]
|
||||
name = "libloading"
|
||||
@@ -761,9 +761,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.95"
|
||||
version = "1.0.96"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
|
||||
checksum = "beef09f85ae72cea1ef96ba6870c51e6382ebfa4f0e85b643459331f3daa5be0"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
@@ -955,9 +955,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.21"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
|
||||
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
@@ -1158,9 +1158,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.17.0"
|
||||
version = "1.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d"
|
||||
checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be"
|
||||
dependencies = [
|
||||
"getrandom",
|
||||
"js-sys",
|
||||
@@ -1539,9 +1539,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zerovec"
|
||||
version = "0.11.2"
|
||||
version = "0.11.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428"
|
||||
checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b"
|
||||
dependencies = [
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
|
||||
28
bindings/wasm/Cargo.lock
generated
28
bindings/wasm/Cargo.lock
generated
@@ -109,9 +109,9 @@ checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.31"
|
||||
version = "1.2.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3a42d84bb6b69d3a8b3eaacf0d88f179e1929695e1ad012b6cf64d9caaa5fd2"
|
||||
checksum = "2352e5597e9c544d5e6d9c95190d5d27738ade584fa8db0a16e130e5c2b5296e"
|
||||
dependencies = [
|
||||
"shlex",
|
||||
]
|
||||
@@ -266,9 +266,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.4"
|
||||
version = "0.15.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5"
|
||||
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
||||
|
||||
[[package]]
|
||||
name = "iana-time-zone"
|
||||
@@ -461,9 +461,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.174"
|
||||
version = "0.2.175"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
|
||||
checksum = "6a82ae493e598baaea5209805c49bbf2ea7de956d50d7da0da1164f9c6d28543"
|
||||
|
||||
[[package]]
|
||||
name = "litemap"
|
||||
@@ -670,9 +670,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.95"
|
||||
version = "1.0.96"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
|
||||
checksum = "beef09f85ae72cea1ef96ba6870c51e6382ebfa4f0e85b643459331f3daa5be0"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
@@ -831,9 +831,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustversion"
|
||||
version = "1.0.21"
|
||||
version = "1.0.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
|
||||
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
@@ -1014,9 +1014,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.17.0"
|
||||
version = "1.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d"
|
||||
checksum = "f33196643e165781c20a5ead5582283a7dacbb87855d867fbc2df3f81eddc1be"
|
||||
dependencies = [
|
||||
"getrandom 0.3.3",
|
||||
"js-sys",
|
||||
@@ -1411,9 +1411,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zerovec"
|
||||
version = "0.11.2"
|
||||
version = "0.11.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428"
|
||||
checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b"
|
||||
dependencies = [
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
|
||||
@@ -27,6 +27,8 @@ mod interpreter;
|
||||
mod lexer;
|
||||
mod number;
|
||||
mod parser;
|
||||
#[cfg(feature = "azure_policy")]
|
||||
mod registry;
|
||||
mod scheduler;
|
||||
#[cfg(feature = "azure_policy")]
|
||||
mod schema;
|
||||
@@ -35,6 +37,8 @@ mod value;
|
||||
|
||||
pub use engine::Engine;
|
||||
pub use lexer::Source;
|
||||
#[cfg(feature = "azure_policy")]
|
||||
pub use schema::{error::ValidationError, validate::SchemaValidator, Schema};
|
||||
pub use value::Value;
|
||||
|
||||
#[cfg(feature = "arc")]
|
||||
|
||||
289
src/registry.rs
Normal file
289
src/registry.rs
Normal file
@@ -0,0 +1,289 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
#![allow(dead_code)]
|
||||
use crate::*;
|
||||
use core::fmt;
|
||||
use dashmap::DashMap;
|
||||
|
||||
type String = Rc<str>;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
mod core;
|
||||
mod effect;
|
||||
mod resource;
|
||||
}
|
||||
|
||||
/// Errors that can occur when interacting with a Registry.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum RegistryError {
|
||||
AlreadyExists { name: String, registry: String },
|
||||
InvalidName { name: String, registry: String },
|
||||
}
|
||||
|
||||
impl fmt::Display for RegistryError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
RegistryError::AlreadyExists { name, registry } => {
|
||||
write!(
|
||||
f,
|
||||
"{} registration failed: An item with the name '{name}' is already registered.",
|
||||
registry
|
||||
)
|
||||
}
|
||||
RegistryError::InvalidName { name, registry } => {
|
||||
write!(f, "{} registration failed: The name '{name}' is invalid (empty or whitespace-only names are not allowed).", registry)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl core::error::Error for RegistryError {}
|
||||
|
||||
/// Validates that a name is not empty or whitespace-only.
|
||||
pub fn validate_name(name: &str, registry_name: &str) -> Result<(), RegistryError> {
|
||||
if name.is_empty() || name.trim().is_empty() {
|
||||
Err(RegistryError::InvalidName {
|
||||
name: String::from(name),
|
||||
registry: String::from(registry_name),
|
||||
})
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Generic thread-safe registry for items of type T using DashMap.
|
||||
///
|
||||
/// This template can be used to create registries for any type T.
|
||||
/// It provides thread-safe storage and retrieval operations with customizable registry names.
|
||||
#[derive(Clone)]
|
||||
pub struct Registry<T> {
|
||||
inner: DashMap<String, Rc<T>>,
|
||||
name: String,
|
||||
}
|
||||
|
||||
impl<T> Registry<T> {
|
||||
/// Create a new, empty registry with a given name.
|
||||
pub fn new(registry_name: impl Into<String>) -> Self {
|
||||
Self {
|
||||
inner: DashMap::new(),
|
||||
name: registry_name.into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the name of this registry.
|
||||
pub fn name(&self) -> &str {
|
||||
&self.name
|
||||
}
|
||||
|
||||
/// Register an item with a given name. Returns Err if name already exists.
|
||||
pub fn register(&self, name: impl Into<String>, item: Rc<T>) -> Result<(), RegistryError> {
|
||||
let name = name.into();
|
||||
|
||||
// Validate the name first
|
||||
validate_name(&name, &self.name)?;
|
||||
|
||||
use dashmap::mapref::entry::Entry;
|
||||
match self.inner.entry(name.clone()) {
|
||||
Entry::Occupied(e) => Err(RegistryError::AlreadyExists {
|
||||
name: e.key().clone(),
|
||||
registry: self.name.clone(),
|
||||
}),
|
||||
Entry::Vacant(e) => {
|
||||
e.insert(item);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Retrieve an item by name, if it exists.
|
||||
pub fn get(&self, name: &str) -> Option<Rc<T>> {
|
||||
self.inner.get(name).map(|entry| Rc::clone(entry.value()))
|
||||
}
|
||||
|
||||
/// Remove an item by name. Returns the removed item if it existed.
|
||||
pub fn remove(&self, name: &str) -> Option<Rc<T>> {
|
||||
self.inner.remove(name).map(|(_, v)| v)
|
||||
}
|
||||
|
||||
/// List all registered item names.
|
||||
pub fn list_names(&self) -> Vec<String> {
|
||||
self.inner.iter().map(|entry| entry.key().clone()).collect()
|
||||
}
|
||||
|
||||
/// Check if an item with the given name exists.
|
||||
pub fn contains(&self, name: &str) -> bool {
|
||||
self.inner.contains_key(name)
|
||||
}
|
||||
|
||||
/// Get the number of registered items.
|
||||
pub fn len(&self) -> usize {
|
||||
self.inner.len()
|
||||
}
|
||||
|
||||
/// Check if the registry is empty.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
self.inner.is_empty()
|
||||
}
|
||||
|
||||
/// Clear all items from the registry.
|
||||
pub fn clear(&self) {
|
||||
self.inner.clear();
|
||||
}
|
||||
|
||||
/// Get an iterator over all entries in the registry.
|
||||
/// Returns an iterator of (name, item) pairs.
|
||||
pub fn iter(&self) -> impl Iterator<Item = (String, Rc<T>)> + '_ {
|
||||
self.inner
|
||||
.iter()
|
||||
.map(|entry| (entry.key().clone(), Rc::clone(entry.value())))
|
||||
}
|
||||
|
||||
/// Get all registered items as a vector.
|
||||
pub fn list_items(&self) -> Vec<Rc<T>> {
|
||||
self.inner
|
||||
.iter()
|
||||
.map(|entry| Rc::clone(entry.value()))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Try to register an item, but don't fail if the name already exists.
|
||||
/// Returns Ok(true) if the item was registered, Ok(false) if the name already exists.
|
||||
pub fn try_register(
|
||||
&self,
|
||||
name: impl Into<String>,
|
||||
item: Rc<T>,
|
||||
) -> Result<bool, RegistryError> {
|
||||
match self.register(name, item) {
|
||||
Ok(()) => Ok(true),
|
||||
Err(RegistryError::AlreadyExists { .. }) => Ok(false),
|
||||
Err(e) => Err(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Type alias for Schema registry
|
||||
pub type SchemaRegistry = Registry<crate::Schema>;
|
||||
|
||||
/// Global registry instances
|
||||
pub mod instances {
|
||||
use super::*;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
/// Global singleton instance of resource schemas registry.
|
||||
pub static ref RESOURCE_SCHEMA_REGISTRY: Registry<crate::Schema> = Registry::new("RESOURCE_SCHEMA_REGISTRY");
|
||||
}
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
/// Global singleton instance of effect schemas registry.
|
||||
pub static ref EFFECT_SCHEMA_REGISTRY: Registry<crate::Schema> = Registry::new("EFFECT_SCHEMA_REGISTRY");
|
||||
}
|
||||
}
|
||||
|
||||
/// Macro to generate helper functions for registry operations.
|
||||
///
|
||||
/// This macro generates helper functions that wrap the registry operations.
|
||||
/// It reduces code duplication and makes it easier to maintain registry interfaces.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `$registry_var` - The static registry variable to wrap
|
||||
/// * `$item_type` - The type of items stored in the registry (e.g., `crate::Schema`)
|
||||
/// * `$item_description` - Human-readable description of the item type (e.g., "resource schema")
|
||||
/// * `$item_description_plural` - Plural form of the item description (e.g., "resource schemas")
|
||||
macro_rules! generate_registry_helpers {
|
||||
($registry_var:ident, $item_type:ty, $item_description:literal, $item_description_plural:literal) => {
|
||||
#[doc = concat!("Register a ", $item_description, " with a given name.")]
|
||||
pub fn register(
|
||||
name: impl Into<String>,
|
||||
item: Rc<$item_type>,
|
||||
) -> Result<(), RegistryError> {
|
||||
$registry_var.register(name, item)
|
||||
}
|
||||
|
||||
#[doc = concat!("Retrieve a ", $item_description, " by name.")]
|
||||
pub fn get(name: &str) -> Option<Rc<$item_type>> {
|
||||
$registry_var.get(name)
|
||||
}
|
||||
|
||||
#[doc = concat!("Remove a ", $item_description, " by name.")]
|
||||
pub fn remove(name: &str) -> Option<Rc<$item_type>> {
|
||||
$registry_var.remove(name)
|
||||
}
|
||||
|
||||
#[doc = concat!("List all registered ", $item_description, " names.")]
|
||||
pub fn list_names() -> Vec<String> {
|
||||
$registry_var.list_names()
|
||||
}
|
||||
|
||||
#[doc = concat!("Check if a ", $item_description, " with the given name exists.")]
|
||||
pub fn contains(name: &str) -> bool {
|
||||
$registry_var.contains(name)
|
||||
}
|
||||
|
||||
#[doc = concat!("Get the number of registered ", $item_description_plural, ".")]
|
||||
pub fn len() -> usize {
|
||||
$registry_var.len()
|
||||
}
|
||||
|
||||
#[doc = concat!("Check if the ", $item_description, " registry is empty.")]
|
||||
pub fn is_empty() -> bool {
|
||||
$registry_var.is_empty()
|
||||
}
|
||||
|
||||
#[doc = concat!("Clear all ", $item_description_plural, " from the registry.")]
|
||||
pub fn clear() {
|
||||
$registry_var.clear();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Macro to generate a module with helper functions for registry operations.
|
||||
///
|
||||
/// This macro generates a complete module with helper functions that wrap the registry operations.
|
||||
/// It reduces code duplication and makes it easier to maintain registry interfaces.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `$mod_name` - The name of the module to generate
|
||||
/// * `$registry_var` - The static registry variable to wrap
|
||||
/// * `$item_type` - The type of items stored in the registry (e.g., `crate::Schema`)
|
||||
/// * `$item_description` - Human-readable description of the item type (e.g., "resource schema")
|
||||
/// * `$item_description_plural` - Plural form of the item description (e.g., "resource schemas")
|
||||
macro_rules! generate_registry_module {
|
||||
($mod_name:ident, $registry_var:ident, $item_type:ty, $item_description:literal, $item_description_plural:literal) => {
|
||||
#[doc = concat!("Helper functions for ", $item_description, " registry operations.")]
|
||||
pub mod $mod_name {
|
||||
use super::*;
|
||||
|
||||
generate_registry_helpers!(
|
||||
$registry_var,
|
||||
$item_type,
|
||||
$item_description,
|
||||
$item_description_plural
|
||||
);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// Helper functions for schema registry operations.
|
||||
pub mod schemas {
|
||||
use super::*;
|
||||
use instances::*;
|
||||
|
||||
// Generate helper modules for schema registries
|
||||
generate_registry_module!(
|
||||
resource,
|
||||
RESOURCE_SCHEMA_REGISTRY,
|
||||
crate::Schema,
|
||||
"resource schema",
|
||||
"resource schemas"
|
||||
);
|
||||
|
||||
generate_registry_module!(
|
||||
effect,
|
||||
EFFECT_SCHEMA_REGISTRY,
|
||||
crate::Schema,
|
||||
"effect schema",
|
||||
"effect schemas"
|
||||
);
|
||||
}
|
||||
509
src/registry/tests/core.rs
Normal file
509
src/registry/tests/core.rs
Normal file
@@ -0,0 +1,509 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
use super::super::registry::*;
|
||||
use crate::{schema::Schema, *};
|
||||
use serde_json::json;
|
||||
|
||||
type String = Rc<str>;
|
||||
type SchemaRegistryError = RegistryError;
|
||||
|
||||
#[test]
|
||||
fn test_schema_registry_new() {
|
||||
let registry = SchemaRegistry::new("test");
|
||||
assert!(registry.is_empty());
|
||||
assert_eq!(registry.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_schema_registry_register_success() {
|
||||
let registry = SchemaRegistry::new("test");
|
||||
let schema = create_test_schema();
|
||||
|
||||
let result = registry.register("test_schema", schema.clone());
|
||||
assert!(result.is_ok());
|
||||
assert_eq!(registry.len(), 1);
|
||||
assert!(registry.contains("test_schema"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_schema_registry_register_duplicate() {
|
||||
let registry = SchemaRegistry::new("test");
|
||||
let schema = create_test_schema();
|
||||
|
||||
// Register first time - should succeed
|
||||
let result1 = registry.register("test_schema", schema.clone());
|
||||
assert!(result1.is_ok());
|
||||
|
||||
// Register again with same name - should fail
|
||||
let result2 = registry.register("test_schema", schema);
|
||||
assert!(result2.is_err());
|
||||
|
||||
if let Err(SchemaRegistryError::AlreadyExists { name, .. }) = result2 {
|
||||
assert_eq!(name, "test_schema".into());
|
||||
} else {
|
||||
panic!("Expected AlreadyExists error");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_schema_registry_get() {
|
||||
let registry = SchemaRegistry::new("test");
|
||||
let schema = create_test_schema();
|
||||
|
||||
// Get non-existent schema
|
||||
assert!(registry.get("non_existent").is_none());
|
||||
|
||||
// Register and get existing schema
|
||||
registry.register("test_schema", schema.clone()).unwrap();
|
||||
let retrieved = registry.get("test_schema");
|
||||
assert!(retrieved.is_some());
|
||||
|
||||
// Verify it's the same schema (Rc comparison)
|
||||
let retrieved_schema = retrieved.unwrap();
|
||||
assert!(Rc::ptr_eq(&schema, &retrieved_schema));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_schema_registry_remove() {
|
||||
let registry = SchemaRegistry::new("test");
|
||||
let schema = create_test_schema();
|
||||
|
||||
// Remove non-existent schema
|
||||
assert!(registry.remove("non_existent").is_none());
|
||||
|
||||
// Register, then remove
|
||||
registry.register("test_schema", schema.clone()).unwrap();
|
||||
assert_eq!(registry.len(), 1);
|
||||
|
||||
let removed = registry.remove("test_schema");
|
||||
assert!(removed.is_some());
|
||||
assert_eq!(registry.len(), 0);
|
||||
assert!(!registry.contains("test_schema"));
|
||||
|
||||
// Verify it's the same schema
|
||||
let removed_schema = removed.unwrap();
|
||||
assert!(Rc::ptr_eq(&schema, &removed_schema));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_schema_registry_list_names() {
|
||||
let registry = SchemaRegistry::new("TestSchemaRegistry");
|
||||
|
||||
// Empty registry
|
||||
assert!(registry.list_names().is_empty());
|
||||
|
||||
// Add multiple schemas
|
||||
let schema1 = create_test_schema();
|
||||
let schema2 = create_test_schema();
|
||||
|
||||
registry.register("schema_a", schema1).unwrap();
|
||||
registry.register("schema_b", schema2).unwrap();
|
||||
|
||||
let names = registry.list_names();
|
||||
assert_eq!(names.len(), 2);
|
||||
assert!(names.contains(&"schema_a".into()));
|
||||
assert!(names.contains(&"schema_b".into()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_schema_registry_clear() {
|
||||
let registry = SchemaRegistry::new("TestSchemaRegistry");
|
||||
let schema = create_test_schema();
|
||||
|
||||
// Add some schemas
|
||||
registry.register("schema1", schema.clone()).unwrap();
|
||||
registry.register("schema2", schema).unwrap();
|
||||
assert_eq!(registry.len(), 2);
|
||||
|
||||
// Clear all
|
||||
registry.clear();
|
||||
assert!(registry.is_empty());
|
||||
assert_eq!(registry.len(), 0);
|
||||
assert!(registry.list_names().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_display() {
|
||||
let error = SchemaRegistryError::AlreadyExists {
|
||||
name: "test_schema".into(),
|
||||
registry: "test".into(),
|
||||
};
|
||||
let error_message = format!("{error}");
|
||||
assert_eq!(
|
||||
error_message,
|
||||
"test registration failed: An item with the name 'test_schema' is already registered."
|
||||
);
|
||||
|
||||
let invalid_error = SchemaRegistryError::InvalidName {
|
||||
name: " ".into(),
|
||||
registry: "test".into(),
|
||||
};
|
||||
let invalid_error_message = format!("{invalid_error}");
|
||||
assert_eq!(invalid_error_message, "test registration failed: The name ' ' is invalid (empty or whitespace-only names are not allowed).");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "std")]
|
||||
fn test_concurrent_access() {
|
||||
use std::sync::Barrier;
|
||||
use std::thread;
|
||||
|
||||
// Create a fresh registry for this test to avoid interference
|
||||
let test_registry = Rc::new(SchemaRegistry::new("TestSchemaRegistry"));
|
||||
|
||||
let barrier = Rc::new(Barrier::new(4));
|
||||
let mut handles = vec![];
|
||||
|
||||
// Spawn multiple threads trying to register schemas
|
||||
for i in 0..4 {
|
||||
let barrier = Rc::clone(&barrier);
|
||||
let registry = Rc::clone(&test_registry);
|
||||
let handle = thread::spawn(move || {
|
||||
let schema = create_test_schema();
|
||||
barrier.wait();
|
||||
|
||||
// Each thread tries to register a schema with unique name
|
||||
let name = format!("schema_{i}");
|
||||
registry.register(name, schema)
|
||||
});
|
||||
handles.push(handle);
|
||||
}
|
||||
|
||||
// Wait for all threads to complete
|
||||
let results: Vec<_> = handles.into_iter().map(|h| h.join().unwrap()).collect();
|
||||
|
||||
// All registrations should succeed
|
||||
for result in results {
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
// Should have exactly 4 schemas registered
|
||||
assert_eq!(test_registry.len(), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "std")]
|
||||
fn test_concurrent_duplicate_registration() {
|
||||
use std::sync::Barrier;
|
||||
use std::thread;
|
||||
|
||||
// Create a fresh registry for this test to avoid interference
|
||||
let test_registry = Rc::new(SchemaRegistry::new("TestSchemaRegistry"));
|
||||
|
||||
let barrier = Rc::new(Barrier::new(3));
|
||||
let mut handles = vec![];
|
||||
|
||||
// Spawn multiple threads trying to register the same schema name
|
||||
for _ in 0..3 {
|
||||
let barrier = Rc::clone(&barrier);
|
||||
let registry = Rc::clone(&test_registry);
|
||||
let handle = thread::spawn(move || {
|
||||
let schema = create_test_schema();
|
||||
barrier.wait();
|
||||
|
||||
// All threads try to register with the same name
|
||||
registry.register("duplicate_name", schema)
|
||||
});
|
||||
handles.push(handle);
|
||||
}
|
||||
|
||||
// Wait for all threads to complete
|
||||
let results: Vec<_> = handles.into_iter().map(|h| h.join().unwrap()).collect();
|
||||
|
||||
// Only one should succeed, others should fail
|
||||
let successes = results.iter().filter(|r| r.is_ok()).count();
|
||||
let failures = results.iter().filter(|r| r.is_err()).count();
|
||||
|
||||
assert_eq!(successes, 1);
|
||||
assert_eq!(failures, 2);
|
||||
assert_eq!(test_registry.len(), 1);
|
||||
}
|
||||
|
||||
// Helper function to create a test schema
|
||||
fn create_test_schema() -> Rc<Schema> {
|
||||
let schema_json = json!({
|
||||
"type": "string",
|
||||
"description": "A test schema"
|
||||
});
|
||||
|
||||
let schema = Schema::from_serde_json_value(schema_json).unwrap();
|
||||
Rc::new(schema)
|
||||
}
|
||||
|
||||
// Corner case tests
|
||||
#[test]
|
||||
fn test_empty_schema_name() {
|
||||
let registry = SchemaRegistry::new("TestSchemaRegistry");
|
||||
let schema = create_test_schema();
|
||||
|
||||
// Empty string as schema name should fail
|
||||
let result = registry.register("", schema);
|
||||
assert!(result.is_err());
|
||||
assert!(matches!(
|
||||
result.unwrap_err(),
|
||||
SchemaRegistryError::InvalidName { .. }
|
||||
));
|
||||
assert!(!registry.contains(""));
|
||||
assert_eq!(registry.len(), 0);
|
||||
assert!(registry.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unicode_schema_names() {
|
||||
let registry = SchemaRegistry::new("TestSchemaRegistry");
|
||||
let schema = create_test_schema();
|
||||
|
||||
// Test various Unicode characters
|
||||
let unicode_names = vec![
|
||||
"схема", // Cyrillic
|
||||
"スキーマ", // Japanese
|
||||
"模式", // Chinese
|
||||
"🚀schema", // Emoji
|
||||
"café-münü", // Accented characters
|
||||
"ñoño", // Spanish characters
|
||||
];
|
||||
|
||||
for name in &unicode_names {
|
||||
let result = registry.register(*name, schema.clone());
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"Failed to register schema with name: {name}"
|
||||
);
|
||||
assert!(registry.contains(name));
|
||||
}
|
||||
|
||||
assert_eq!(registry.len(), unicode_names.len());
|
||||
|
||||
// Verify all names are listed
|
||||
let listed_names = registry.list_names();
|
||||
for name in &unicode_names {
|
||||
let name: String = (*name).into();
|
||||
assert!(listed_names.contains(&name));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_very_long_schema_name() {
|
||||
let registry = SchemaRegistry::new("TestSchemaRegistry");
|
||||
let schema = create_test_schema();
|
||||
|
||||
// Create a very long name (1000 characters)
|
||||
let long_name: String = "a".repeat(1000).into();
|
||||
|
||||
let result = registry.register(long_name.clone(), schema);
|
||||
assert!(result.is_ok());
|
||||
assert!(registry.contains(&long_name));
|
||||
|
||||
let retrieved = registry.get(&long_name);
|
||||
assert!(retrieved.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_special_character_schema_names() {
|
||||
let registry = SchemaRegistry::new("TestSchemaRegistry");
|
||||
let schema = create_test_schema();
|
||||
|
||||
let special_names = vec![
|
||||
"schema-with-dashes",
|
||||
"schema_with_underscores",
|
||||
"schema.with.dots",
|
||||
"schema:with:colons",
|
||||
"schema/with/slashes",
|
||||
"schema with spaces",
|
||||
"schema\twith\ttabs",
|
||||
"schema\nwith\nnewlines",
|
||||
"UPPERCASE_SCHEMA",
|
||||
"MixedCaseSchema",
|
||||
"123numeric456",
|
||||
"!@#$%^&*()",
|
||||
"\"quoted\"",
|
||||
"'single-quoted'",
|
||||
"[bracketed]",
|
||||
"{curly}",
|
||||
"(parentheses)",
|
||||
];
|
||||
|
||||
for name in &special_names {
|
||||
let result = registry.register(*name, schema.clone());
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"Failed to register schema with name: {name}"
|
||||
);
|
||||
assert!(registry.contains(name));
|
||||
}
|
||||
|
||||
assert_eq!(registry.len(), special_names.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_whitespace_only_names() {
|
||||
let registry = SchemaRegistry::new("TestSchemaRegistry");
|
||||
let schema = create_test_schema();
|
||||
|
||||
let whitespace_names = vec![
|
||||
" ", // Single space
|
||||
"\t", // Tab
|
||||
"\n", // Newline
|
||||
"\r", // Carriage return
|
||||
" ", // Multiple spaces
|
||||
"\t\t", // Multiple tabs
|
||||
" \t\n\r ", // Mixed whitespace
|
||||
];
|
||||
|
||||
for name in &whitespace_names {
|
||||
let result = registry.register(*name, schema.clone());
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"Expected error for whitespace name: {name:?}"
|
||||
);
|
||||
assert!(matches!(
|
||||
result.unwrap_err(),
|
||||
SchemaRegistryError::InvalidName { .. }
|
||||
));
|
||||
assert!(!registry.contains(name));
|
||||
}
|
||||
|
||||
assert_eq!(registry.len(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_valid_names_with_whitespace() {
|
||||
let registry = SchemaRegistry::new("TestSchemaRegistry");
|
||||
let schema = create_test_schema();
|
||||
|
||||
let valid_names = vec![
|
||||
"schema name", // Space in the middle
|
||||
" schema", // Leading space but not only whitespace
|
||||
"schema ", // Trailing space but not only whitespace
|
||||
"my\tschema", // Tab in the middle
|
||||
"multi word schema", // Multiple words
|
||||
];
|
||||
|
||||
for name in &valid_names {
|
||||
let result = registry.register(*name, schema.clone());
|
||||
assert!(result.is_ok(), "Expected success for valid name: {name:?}");
|
||||
assert!(registry.contains(name));
|
||||
}
|
||||
|
||||
assert_eq!(registry.len(), valid_names.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_same_schema_different_names() {
|
||||
let registry = SchemaRegistry::new("TestSchemaRegistry");
|
||||
let schema = create_test_schema();
|
||||
|
||||
// Register the same schema instance with different names
|
||||
let names = vec!["name1", "name2", "name3"];
|
||||
|
||||
for name in &names {
|
||||
let result = registry.register(*name, schema.clone());
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
assert_eq!(registry.len(), names.len());
|
||||
|
||||
// All should point to the same schema instance
|
||||
for name in &names {
|
||||
let retrieved = registry.get(name).unwrap();
|
||||
assert!(Rc::ptr_eq(&schema, &retrieved));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_register_after_remove_and_clear() {
|
||||
let registry = SchemaRegistry::new("TestSchemaRegistry");
|
||||
let schema = create_test_schema();
|
||||
|
||||
// Register, remove, then register again with same name
|
||||
registry.register("test", schema.clone()).unwrap();
|
||||
assert!(registry.contains("test"));
|
||||
|
||||
registry.remove("test");
|
||||
assert!(!registry.contains("test"));
|
||||
|
||||
// Should be able to register again with same name
|
||||
let result = registry.register("test", schema.clone());
|
||||
assert!(result.is_ok());
|
||||
assert!(registry.contains("test"));
|
||||
|
||||
// Clear and register again
|
||||
registry.clear();
|
||||
assert!(registry.is_empty());
|
||||
|
||||
let result = registry.register("test", schema);
|
||||
assert!(result.is_ok());
|
||||
assert!(registry.contains("test"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_case_sensitive_names() {
|
||||
let registry = SchemaRegistry::new("TestSchemaRegistry");
|
||||
let schema = create_test_schema();
|
||||
|
||||
// Register schemas with different cases of the same name
|
||||
let case_variants = vec!["test", "Test", "TEST", "tEsT"];
|
||||
|
||||
for name in &case_variants {
|
||||
let result = registry.register(*name, schema.clone());
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"Failed to register schema with name: {name}"
|
||||
);
|
||||
}
|
||||
|
||||
assert_eq!(registry.len(), case_variants.len());
|
||||
|
||||
// All should be treated as different schemas
|
||||
for name in &case_variants {
|
||||
assert!(registry.contains(name));
|
||||
let retrieved = registry.get(name);
|
||||
assert!(retrieved.is_some());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_error_after_schema_removal() {
|
||||
let registry = SchemaRegistry::new("TestSchemaRegistry");
|
||||
let schema = create_test_schema();
|
||||
|
||||
// Register schema
|
||||
registry.register("test", schema.clone()).unwrap();
|
||||
|
||||
// Remove it
|
||||
registry.remove("test");
|
||||
|
||||
// Try to register again - should succeed
|
||||
let result = registry.register("test", schema);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mixed_operations_sequence() {
|
||||
let registry = SchemaRegistry::new("TestSchemaRegistry");
|
||||
let schema1 = create_test_schema();
|
||||
let schema2 = create_test_schema();
|
||||
|
||||
// Complex sequence of operations
|
||||
registry.register("a", schema1.clone()).unwrap();
|
||||
registry.register("b", schema2.clone()).unwrap();
|
||||
assert_eq!(registry.len(), 2);
|
||||
|
||||
// Try duplicate - should fail
|
||||
assert!(registry.register("a", schema1.clone()).is_err());
|
||||
assert_eq!(registry.len(), 2);
|
||||
|
||||
// Remove one
|
||||
registry.remove("a");
|
||||
assert_eq!(registry.len(), 1);
|
||||
|
||||
// Register with removed name - should succeed
|
||||
registry.register("a", schema1).unwrap();
|
||||
assert_eq!(registry.len(), 2);
|
||||
|
||||
// Clear and verify
|
||||
registry.clear();
|
||||
assert!(registry.is_empty());
|
||||
assert!(registry.list_names().is_empty());
|
||||
}
|
||||
434
src/registry/tests/effect.rs
Normal file
434
src/registry/tests/effect.rs
Normal file
@@ -0,0 +1,434 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
use super::super::registry::*;
|
||||
use crate::{
|
||||
registry::{instances::EFFECT_SCHEMA_REGISTRY, schemas::effect},
|
||||
schema::Schema,
|
||||
*,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
type String = Rc<str>;
|
||||
type SchemaRegistryError = RegistryError;
|
||||
|
||||
// Helper function to create a schema for Azure Policy effects
|
||||
fn create_effect_schema() -> Rc<Schema> {
|
||||
let schema_json = json!({
|
||||
"enum": ["audit", "deny", "disabled", "modify"],
|
||||
"description": "Azure Policy effect types"
|
||||
});
|
||||
|
||||
let schema = Schema::from_serde_json_value(schema_json).unwrap();
|
||||
Rc::new(schema)
|
||||
}
|
||||
|
||||
// Helper function to create a deny effect schema
|
||||
fn create_deny_effect_schema() -> Rc<Schema> {
|
||||
let schema_json = json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"effect": {
|
||||
"const": "deny"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Explanation of what is being denied"
|
||||
}
|
||||
},
|
||||
"required": ["effect"],
|
||||
"description": "Schema for deny effect in Azure Policy"
|
||||
});
|
||||
|
||||
let schema = Schema::from_serde_json_value(schema_json).unwrap();
|
||||
Rc::new(schema)
|
||||
}
|
||||
|
||||
// Helper function to create an audit effect schema
|
||||
fn create_audit_effect_schema() -> Rc<Schema> {
|
||||
let schema_json = json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"effect": {
|
||||
"const": "audit"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Explanation of what is being audited"
|
||||
},
|
||||
"auditDetails": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"category": {
|
||||
"enum": ["security", "compliance", "cost", "operational"]
|
||||
},
|
||||
"severity": {
|
||||
"enum": ["low", "medium", "high", "critical"]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["effect"],
|
||||
"description": "Schema for audit effect in Azure Policy"
|
||||
});
|
||||
|
||||
let schema = Schema::from_serde_json_value(schema_json).unwrap();
|
||||
Rc::new(schema)
|
||||
}
|
||||
|
||||
// Helper function to create a modify effect schema
|
||||
fn create_modify_effect_schema() -> Rc<Schema> {
|
||||
let schema_json = json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"effect": {
|
||||
"const": "modify"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Explanation of what is being modified"
|
||||
},
|
||||
"modifyDetails": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"roleDefinitionIds": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "List of role definition IDs required for modification"
|
||||
},
|
||||
"operations": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"operation": {
|
||||
"enum": ["add", "replace", "remove"]
|
||||
},
|
||||
"field": {
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"type": "any",
|
||||
"description": "Value to add or replace"
|
||||
}
|
||||
},
|
||||
"required": ["operation", "field"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["roleDefinitionIds", "operations"]
|
||||
}
|
||||
},
|
||||
"required": ["effect", "modifyDetails"],
|
||||
"description": "Schema for modify effect in Azure Policy"
|
||||
});
|
||||
|
||||
let schema = Schema::from_serde_json_value(schema_json).unwrap();
|
||||
Rc::new(schema)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_basic_effect_enum_schema() {
|
||||
let effect_schema = create_effect_schema();
|
||||
|
||||
// Test registration of basic effect enum schema
|
||||
let result =
|
||||
EFFECT_SCHEMA_REGISTRY.register("azure.policy.effect.basic", effect_schema.clone());
|
||||
assert!(result.is_ok());
|
||||
assert!(EFFECT_SCHEMA_REGISTRY.contains("azure.policy.effect.basic"));
|
||||
|
||||
// Verify schema can be retrieved
|
||||
let retrieved = EFFECT_SCHEMA_REGISTRY.get("azure.policy.effect.basic");
|
||||
assert!(retrieved.is_some());
|
||||
assert!(Rc::ptr_eq(&effect_schema, &retrieved.unwrap()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_deny_effect_schema() {
|
||||
let deny_schema = create_deny_effect_schema();
|
||||
|
||||
// Test registration of deny effect schema
|
||||
let result = EFFECT_SCHEMA_REGISTRY.register("azure.policy.deny.test", deny_schema.clone());
|
||||
assert!(result.is_ok());
|
||||
assert!(EFFECT_SCHEMA_REGISTRY.contains("azure.policy.deny.test"));
|
||||
|
||||
// Verify basic functionality - we can't inspect internal structure due to private as_type()
|
||||
assert!(EFFECT_SCHEMA_REGISTRY.contains("azure.policy.deny.test"));
|
||||
|
||||
// The schema should be retrievable
|
||||
let retrieved = EFFECT_SCHEMA_REGISTRY.get("azure.policy.deny.test");
|
||||
assert!(retrieved.is_some());
|
||||
assert!(Rc::ptr_eq(&deny_schema, &retrieved.unwrap()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_audit_effect_schema() {
|
||||
let audit_schema = create_audit_effect_schema();
|
||||
|
||||
// Test registration of audit effect schema
|
||||
let result = EFFECT_SCHEMA_REGISTRY.register("azure.policy.audit.test", audit_schema.clone());
|
||||
assert!(result.is_ok());
|
||||
assert!(EFFECT_SCHEMA_REGISTRY.contains("azure.policy.audit.test"));
|
||||
|
||||
// Verify basic functionality - we can't inspect internal structure due to private as_type()
|
||||
assert!(EFFECT_SCHEMA_REGISTRY.contains("azure.policy.audit.test"));
|
||||
|
||||
// The schema should be retrievable
|
||||
let retrieved = EFFECT_SCHEMA_REGISTRY.get("azure.policy.audit.test");
|
||||
assert!(retrieved.is_some());
|
||||
assert!(Rc::ptr_eq(&audit_schema, &retrieved.unwrap()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_modify_effect_schema() {
|
||||
let modify_schema = create_modify_effect_schema();
|
||||
|
||||
// Test registration of modify effect schema
|
||||
let result = EFFECT_SCHEMA_REGISTRY.register("azure.policy.modify.test", modify_schema.clone());
|
||||
assert!(result.is_ok());
|
||||
assert!(EFFECT_SCHEMA_REGISTRY.contains("azure.policy.modify.test"));
|
||||
|
||||
// Verify basic functionality - we can't inspect internal structure due to private as_type()
|
||||
assert!(EFFECT_SCHEMA_REGISTRY.contains("azure.policy.modify.test"));
|
||||
|
||||
// The schema should be retrievable
|
||||
let retrieved = EFFECT_SCHEMA_REGISTRY.get("azure.policy.modify.test");
|
||||
assert!(retrieved.is_some());
|
||||
assert!(Rc::ptr_eq(&modify_schema, &retrieved.unwrap()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_effect_schemas() {
|
||||
// Register all effect schemas with unique names for this specific test
|
||||
let deny_schema = create_deny_effect_schema();
|
||||
let audit_schema = create_audit_effect_schema();
|
||||
let modify_schema = create_modify_effect_schema();
|
||||
|
||||
// Use highly unique names to avoid conflicts with other tests
|
||||
let deny_name = "azure.policy.deny.multiple.test";
|
||||
let audit_name = "azure.policy.audit.multiple.test";
|
||||
let modify_name = "azure.policy.modify.multiple.test";
|
||||
|
||||
assert!(EFFECT_SCHEMA_REGISTRY
|
||||
.register(deny_name, deny_schema)
|
||||
.is_ok());
|
||||
assert!(EFFECT_SCHEMA_REGISTRY
|
||||
.register(audit_name, audit_schema)
|
||||
.is_ok());
|
||||
assert!(EFFECT_SCHEMA_REGISTRY
|
||||
.register(modify_name, modify_schema)
|
||||
.is_ok());
|
||||
|
||||
// Verify all are registered
|
||||
assert!(EFFECT_SCHEMA_REGISTRY.contains(deny_name));
|
||||
assert!(EFFECT_SCHEMA_REGISTRY.contains(audit_name));
|
||||
assert!(EFFECT_SCHEMA_REGISTRY.contains(modify_name));
|
||||
|
||||
// Verify they can all be retrieved
|
||||
assert!(EFFECT_SCHEMA_REGISTRY.get(deny_name).is_some());
|
||||
assert!(EFFECT_SCHEMA_REGISTRY.get(audit_name).is_some());
|
||||
assert!(EFFECT_SCHEMA_REGISTRY.get(modify_name).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_global_effect_registry() {
|
||||
// Register Azure Policy effects with unique names
|
||||
let deny_schema = create_deny_effect_schema();
|
||||
let audit_schema = create_audit_effect_schema();
|
||||
let modify_schema = create_modify_effect_schema();
|
||||
|
||||
// Use highly unique names to avoid conflicts
|
||||
let deny_name = "azure.policy.deny.global.test";
|
||||
let audit_name = "azure.policy.audit.global.test";
|
||||
let modify_name = "azure.policy.modify.global.test";
|
||||
|
||||
assert!(effect::register(deny_name, deny_schema).is_ok());
|
||||
assert!(effect::register(audit_name, audit_schema).is_ok());
|
||||
assert!(effect::register(modify_name, modify_schema).is_ok());
|
||||
|
||||
// Verify all are registered in global registry
|
||||
assert!(effect::contains(deny_name));
|
||||
assert!(effect::contains(audit_name));
|
||||
assert!(effect::contains(modify_name));
|
||||
|
||||
// Test retrieval from global registry
|
||||
let retrieved_deny = effect::get(deny_name);
|
||||
let retrieved_audit = effect::get(audit_name);
|
||||
let retrieved_modify = effect::get(modify_name);
|
||||
|
||||
assert!(retrieved_deny.is_some());
|
||||
assert!(retrieved_audit.is_some());
|
||||
assert!(retrieved_modify.is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_effect_schema_validation_patterns() {
|
||||
// Test schema with various Azure Policy patterns
|
||||
let complex_effect_schema = json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"effect": {
|
||||
"enum": ["audit", "deny", "disabled", "modify", "auditIfNotExists", "deployIfNotExists"]
|
||||
},
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"description": "Parameters for the effect"
|
||||
},
|
||||
"existenceCondition": {
|
||||
"type": "object",
|
||||
"description": "Condition for existence-based effects"
|
||||
},
|
||||
"deployment": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"mode": {
|
||||
"enum": ["incremental", "complete"]
|
||||
},
|
||||
"template": {
|
||||
"type": "object"
|
||||
},
|
||||
"parameters": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["effect"],
|
||||
"description": "Comprehensive Azure Policy effect schema"
|
||||
});
|
||||
|
||||
let schema = Schema::from_serde_json_value(complex_effect_schema).unwrap();
|
||||
let schema_rc = Rc::new(schema);
|
||||
|
||||
let result = EFFECT_SCHEMA_REGISTRY.register("azure.policy.complex.patterns", schema_rc);
|
||||
assert!(result.is_ok());
|
||||
assert!(EFFECT_SCHEMA_REGISTRY.contains("azure.policy.complex.patterns"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_effect_schema_with_invalid_names() {
|
||||
let effect_schema = create_effect_schema();
|
||||
|
||||
// Test invalid names
|
||||
assert!(EFFECT_SCHEMA_REGISTRY
|
||||
.register("", effect_schema.clone())
|
||||
.is_err());
|
||||
assert!(EFFECT_SCHEMA_REGISTRY
|
||||
.register(" ", effect_schema.clone())
|
||||
.is_err());
|
||||
assert!(EFFECT_SCHEMA_REGISTRY
|
||||
.register("\t", effect_schema.clone())
|
||||
.is_err());
|
||||
assert!(EFFECT_SCHEMA_REGISTRY
|
||||
.register("\n", effect_schema)
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_effect_schema_duplicate_registration() {
|
||||
let deny_schema = create_deny_effect_schema();
|
||||
|
||||
// First registration should succeed
|
||||
assert!(EFFECT_SCHEMA_REGISTRY
|
||||
.register("azure.policy.deny.duplicate", deny_schema.clone())
|
||||
.is_ok());
|
||||
|
||||
// Duplicate registration should fail
|
||||
let duplicate_result =
|
||||
EFFECT_SCHEMA_REGISTRY.register("azure.policy.deny.duplicate", deny_schema);
|
||||
assert!(duplicate_result.is_err());
|
||||
|
||||
// Verify error type
|
||||
match duplicate_result.unwrap_err() {
|
||||
SchemaRegistryError::AlreadyExists { name, .. } => {
|
||||
assert_eq!(name.as_ref(), "azure.policy.deny.duplicate");
|
||||
}
|
||||
_ => panic!("Expected AlreadyExists error"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_azure_policy_effect_removal() {
|
||||
// Register multiple Azure Policy effects with unique names
|
||||
let effects = vec![
|
||||
("azure.policy.deny.removal", create_deny_effect_schema()),
|
||||
("azure.policy.audit.removal", create_audit_effect_schema()),
|
||||
("azure.policy.modify.removal", create_modify_effect_schema()),
|
||||
];
|
||||
|
||||
for (name, schema) in &effects {
|
||||
assert!(EFFECT_SCHEMA_REGISTRY
|
||||
.register(*name, schema.clone())
|
||||
.is_ok());
|
||||
}
|
||||
|
||||
// Remove one effect
|
||||
let removed = EFFECT_SCHEMA_REGISTRY.remove("azure.policy.audit.removal");
|
||||
assert!(removed.is_some());
|
||||
assert!(!EFFECT_SCHEMA_REGISTRY.contains("azure.policy.audit.removal"));
|
||||
|
||||
// Verify the removed schema is correct
|
||||
let removed_schema = removed.unwrap();
|
||||
assert!(Rc::ptr_eq(&effects[1].1, &removed_schema));
|
||||
|
||||
// Other effects should still be present
|
||||
assert!(EFFECT_SCHEMA_REGISTRY.contains("azure.policy.deny.removal"));
|
||||
assert!(EFFECT_SCHEMA_REGISTRY.contains("azure.policy.modify.removal"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "std")]
|
||||
fn test_concurrent_effect_schema_access() {
|
||||
use std::sync::Barrier;
|
||||
use std::thread;
|
||||
|
||||
let barrier = Rc::new(Barrier::new(3));
|
||||
let mut handles = vec![];
|
||||
|
||||
// Test concurrent registration of different Azure Policy effects
|
||||
let effects = [
|
||||
"concurrent_effect_schema_access.deny",
|
||||
"concurrent_effect_schema_access.audit",
|
||||
"concurrent_effect_schema_access.modify",
|
||||
];
|
||||
|
||||
for (i, effect_name) in effects.iter().enumerate() {
|
||||
let barrier = Rc::clone(&barrier);
|
||||
let name: String = (*effect_name).into();
|
||||
|
||||
let handle: thread::JoinHandle<Result<(), SchemaRegistryError>> =
|
||||
thread::spawn(move || {
|
||||
let schema = match i {
|
||||
0 => create_deny_effect_schema(),
|
||||
1 => create_audit_effect_schema(),
|
||||
2 => create_modify_effect_schema(),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
barrier.wait();
|
||||
EFFECT_SCHEMA_REGISTRY.register(name, schema)
|
||||
});
|
||||
|
||||
handles.push(handle);
|
||||
}
|
||||
|
||||
// Wait for all threads to complete
|
||||
let results: Vec<_> = handles.into_iter().map(|h| h.join().unwrap()).collect();
|
||||
|
||||
// All registrations should succeed
|
||||
for result in results {
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
// Should have effect schemas registered
|
||||
assert!(EFFECT_SCHEMA_REGISTRY.contains("concurrent_effect_schema_access.deny"));
|
||||
assert!(EFFECT_SCHEMA_REGISTRY.contains("concurrent_effect_schema_access.audit"));
|
||||
assert!(EFFECT_SCHEMA_REGISTRY.contains("concurrent_effect_schema_access.modify"));
|
||||
}
|
||||
580
src/registry/tests/resource.rs
Normal file
580
src/registry/tests/resource.rs
Normal file
@@ -0,0 +1,580 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
use super::super::registry::*;
|
||||
use crate::{
|
||||
registry::{instances::RESOURCE_SCHEMA_REGISTRY, schemas::resource},
|
||||
schema::Schema,
|
||||
*,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
type String = Rc<str>;
|
||||
type SchemaRegistryError = RegistryError;
|
||||
|
||||
// Helper function to create a schema for Azure Resource types
|
||||
fn create_resource_schema() -> Rc<Schema> {
|
||||
let schema_json = json!({
|
||||
"enum": ["Microsoft.Compute/virtualMachines", "Microsoft.Storage/storageAccounts", "Microsoft.Network/virtualNetworks"],
|
||||
"description": "Azure Resource types"
|
||||
});
|
||||
|
||||
let schema = Schema::from_serde_json_value(schema_json).unwrap();
|
||||
Rc::new(schema)
|
||||
}
|
||||
|
||||
// Helper function to create a virtual machine resource schema
|
||||
fn create_vm_resource_schema() -> Rc<Schema> {
|
||||
let schema_json = json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"const": "Microsoft.Compute/virtualMachines"
|
||||
},
|
||||
"apiVersion": {
|
||||
"enum": ["2021-03-01", "2021-07-01", "2022-03-01"]
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-zA-Z0-9-._]{1,64}$"
|
||||
},
|
||||
"location": {
|
||||
"type": "string",
|
||||
"description": "Azure region where the VM will be deployed"
|
||||
},
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"hardwareProfile": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"vmSize": {
|
||||
"enum": ["Standard_B1s", "Standard_B2s", "Standard_D2s_v3", "Standard_D4s_v3"]
|
||||
}
|
||||
},
|
||||
"required": ["vmSize"]
|
||||
},
|
||||
"osProfile": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"computerName": {
|
||||
"type": "string"
|
||||
},
|
||||
"adminUsername": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": ["computerName", "adminUsername"]
|
||||
}
|
||||
},
|
||||
"required": ["hardwareProfile", "osProfile"]
|
||||
}
|
||||
},
|
||||
"required": ["type", "apiVersion", "name", "location", "properties"],
|
||||
"description": "Schema for Azure Virtual Machine resources"
|
||||
});
|
||||
|
||||
let schema = Schema::from_serde_json_value(schema_json).unwrap();
|
||||
Rc::new(schema)
|
||||
}
|
||||
|
||||
// Helper function to create a storage account resource schema
|
||||
fn create_storage_resource_schema() -> Rc<Schema> {
|
||||
let schema_json = json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"const": "Microsoft.Storage/storageAccounts"
|
||||
},
|
||||
"apiVersion": {
|
||||
"enum": ["2021-04-01", "2021-06-01", "2022-05-01"]
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-z0-9]{3,24}$"
|
||||
},
|
||||
"location": {
|
||||
"type": "string",
|
||||
"description": "Azure region for the storage account"
|
||||
},
|
||||
"sku": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"enum": ["Standard_LRS", "Standard_GRS", "Standard_RAGRS", "Premium_LRS"]
|
||||
}
|
||||
},
|
||||
"required": ["name"]
|
||||
},
|
||||
"kind": {
|
||||
"enum": ["Storage", "StorageV2", "BlobStorage", "FileStorage", "BlockBlobStorage"]
|
||||
},
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"accessTier": {
|
||||
"enum": ["Hot", "Cool"]
|
||||
},
|
||||
"encryption": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"services": {
|
||||
"type": "object"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["type", "apiVersion", "name", "location", "sku", "kind"],
|
||||
"description": "Schema for Azure Storage Account resources"
|
||||
});
|
||||
|
||||
let schema = Schema::from_serde_json_value(schema_json).unwrap();
|
||||
Rc::new(schema)
|
||||
}
|
||||
|
||||
// Helper function to create a network resource schema
|
||||
fn create_network_resource_schema() -> Rc<Schema> {
|
||||
let schema_json = json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"const": "Microsoft.Network/virtualNetworks"
|
||||
},
|
||||
"apiVersion": {
|
||||
"enum": ["2020-11-01", "2021-02-01", "2021-05-01"]
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-zA-Z0-9-._]{2,64}$"
|
||||
},
|
||||
"location": {
|
||||
"type": "string",
|
||||
"description": "Azure region for the virtual network"
|
||||
},
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"addressSpace": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"addressPrefixes": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"pattern": "^(?:[0-9]{1,3}\\.){3}[0-9]{1,3}/[0-9]{1,2}$"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["addressPrefixes"]
|
||||
},
|
||||
"subnets": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"addressPrefix": {
|
||||
"type": "string",
|
||||
"pattern": "^(?:[0-9]{1,3}\\.){3}[0-9]{1,3}/[0-9]{1,2}$"
|
||||
}
|
||||
},
|
||||
"required": ["addressPrefix"]
|
||||
}
|
||||
},
|
||||
"required": ["name", "properties"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["addressSpace"]
|
||||
}
|
||||
},
|
||||
"required": ["type", "apiVersion", "name", "location", "properties"],
|
||||
"description": "Schema for Azure Virtual Network resources"
|
||||
});
|
||||
|
||||
let schema = Schema::from_serde_json_value(schema_json).unwrap();
|
||||
Rc::new(schema)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_basic_resource_enum_schema() {
|
||||
let resource_schema = create_resource_schema();
|
||||
|
||||
// Test registration of basic resource enum schema
|
||||
let result =
|
||||
RESOURCE_SCHEMA_REGISTRY.register("azure.resource.types.basic", resource_schema.clone());
|
||||
assert!(result.is_ok());
|
||||
assert!(RESOURCE_SCHEMA_REGISTRY.contains("azure.resource.types.basic"));
|
||||
|
||||
// Verify schema can be retrieved
|
||||
let retrieved = RESOURCE_SCHEMA_REGISTRY.get("azure.resource.types.basic");
|
||||
assert!(retrieved.is_some());
|
||||
assert!(Rc::ptr_eq(&resource_schema, &retrieved.unwrap()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_vm_resource_schema() {
|
||||
let vm_schema = create_vm_resource_schema();
|
||||
|
||||
// Test registration of VM resource schema
|
||||
let result = RESOURCE_SCHEMA_REGISTRY.register("azure.resource.vm.test", vm_schema.clone());
|
||||
assert!(result.is_ok());
|
||||
assert!(RESOURCE_SCHEMA_REGISTRY.contains("azure.resource.vm.test"));
|
||||
|
||||
// Verify schema structure
|
||||
// Verify basic functionality - schema retrieval and pointer equality
|
||||
|
||||
// The schema should be retrievable and be the same instance
|
||||
let retrieved = RESOURCE_SCHEMA_REGISTRY.get("azure.resource.vm.test");
|
||||
assert!(retrieved.is_some());
|
||||
assert!(Rc::ptr_eq(&vm_schema, &retrieved.unwrap()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_storage_resource_schema() {
|
||||
let storage_schema = create_storage_resource_schema();
|
||||
|
||||
// Test registration of storage resource schema
|
||||
let result =
|
||||
RESOURCE_SCHEMA_REGISTRY.register("azure.resource.storage.test", storage_schema.clone());
|
||||
assert!(result.is_ok());
|
||||
assert!(RESOURCE_SCHEMA_REGISTRY.contains("azure.resource.storage.test"));
|
||||
|
||||
// Verify basic functionality - schema retrieval and pointer equality
|
||||
|
||||
// The schema should be retrievable and be the same instance
|
||||
let retrieved = RESOURCE_SCHEMA_REGISTRY.get("azure.resource.storage.test");
|
||||
assert!(retrieved.is_some());
|
||||
assert!(Rc::ptr_eq(&storage_schema, &retrieved.unwrap()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_network_resource_schema() {
|
||||
let network_schema = create_network_resource_schema();
|
||||
|
||||
// Test registration of network resource schema
|
||||
let result =
|
||||
RESOURCE_SCHEMA_REGISTRY.register("azure.resource.network.test", network_schema.clone());
|
||||
assert!(result.is_ok());
|
||||
assert!(RESOURCE_SCHEMA_REGISTRY.contains("azure.resource.network.test"));
|
||||
|
||||
// Verify basic functionality - schema retrieval and pointer equality
|
||||
|
||||
// The schema should be retrievable and be the same instance
|
||||
let retrieved = RESOURCE_SCHEMA_REGISTRY.get("azure.resource.network.test");
|
||||
assert!(retrieved.is_some());
|
||||
assert!(Rc::ptr_eq(&network_schema, &retrieved.unwrap()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_multiple_resource_schemas() {
|
||||
// Register all resource schemas with unique names
|
||||
let vm_schema = create_vm_resource_schema();
|
||||
let storage_schema = create_storage_resource_schema();
|
||||
let network_schema = create_network_resource_schema();
|
||||
|
||||
let vm_name = "azure.resource.vm.multiple";
|
||||
let storage_name = "azure.resource.storage.multiple";
|
||||
let network_name = "azure.resource.network.multiple";
|
||||
|
||||
assert!(RESOURCE_SCHEMA_REGISTRY
|
||||
.register(vm_name, vm_schema)
|
||||
.is_ok());
|
||||
assert!(RESOURCE_SCHEMA_REGISTRY
|
||||
.register(storage_name, storage_schema)
|
||||
.is_ok());
|
||||
assert!(RESOURCE_SCHEMA_REGISTRY
|
||||
.register(network_name, network_schema)
|
||||
.is_ok());
|
||||
|
||||
// Verify all are registered
|
||||
assert!(RESOURCE_SCHEMA_REGISTRY.contains(vm_name));
|
||||
assert!(RESOURCE_SCHEMA_REGISTRY.contains(storage_name));
|
||||
assert!(RESOURCE_SCHEMA_REGISTRY.contains(network_name));
|
||||
|
||||
// Verify they can all be retrieved
|
||||
assert!(RESOURCE_SCHEMA_REGISTRY.get(vm_name).is_some());
|
||||
assert!(RESOURCE_SCHEMA_REGISTRY.get(storage_name).is_some());
|
||||
assert!(RESOURCE_SCHEMA_REGISTRY.get(network_name).is_some());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_global_resource_registry() {
|
||||
// Register Azure Resource schemas with unique names
|
||||
let vm_schema = create_vm_resource_schema();
|
||||
let storage_schema = create_storage_resource_schema();
|
||||
let network_schema = create_network_resource_schema();
|
||||
|
||||
let vm_name = "azure.resource.vm.global";
|
||||
let storage_name = "azure.resource.storage.global";
|
||||
let network_name = "azure.resource.network.global";
|
||||
|
||||
assert!(resource::register(vm_name, vm_schema.clone()).is_ok());
|
||||
assert!(resource::register(storage_name, storage_schema.clone()).is_ok());
|
||||
assert!(resource::register(network_name, network_schema.clone()).is_ok());
|
||||
|
||||
// Verify all are registered in global registry
|
||||
assert!(resource::contains(vm_name));
|
||||
assert!(resource::contains(storage_name));
|
||||
assert!(resource::contains(network_name));
|
||||
|
||||
// Test retrieval from global registry
|
||||
let retrieved_vm = resource::get(vm_name);
|
||||
let retrieved_storage = resource::get(storage_name);
|
||||
let retrieved_network = resource::get(network_name);
|
||||
|
||||
assert!(retrieved_vm.is_some());
|
||||
assert!(retrieved_storage.is_some());
|
||||
assert!(retrieved_network.is_some());
|
||||
|
||||
// Verify pointer equality
|
||||
assert!(Rc::ptr_eq(&vm_schema, &retrieved_vm.unwrap()));
|
||||
assert!(Rc::ptr_eq(&storage_schema, &retrieved_storage.unwrap()));
|
||||
assert!(Rc::ptr_eq(&network_schema, &retrieved_network.unwrap()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resource_schema_validation_patterns() {
|
||||
// Test schema with various Azure Resource patterns
|
||||
let complex_resource_schema = json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"resources": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-zA-Z0-9]+\\.[a-zA-Z0-9]+/[a-zA-Z0-9]+$"
|
||||
},
|
||||
"apiVersion": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9]{4}-[0-9]{2}-[0-9]{2}$"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"location": {
|
||||
"type": "string"
|
||||
},
|
||||
"dependsOn": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"tags": {
|
||||
"type": "object",
|
||||
"additionalProperties": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["type", "apiVersion", "name"]
|
||||
}
|
||||
},
|
||||
"parameters": {
|
||||
"type": "object"
|
||||
},
|
||||
"variables": {
|
||||
"type": "object"
|
||||
},
|
||||
"outputs": {
|
||||
"type": "object"
|
||||
}
|
||||
},
|
||||
"required": ["resources"],
|
||||
"description": "Comprehensive Azure Resource Manager template schema"
|
||||
});
|
||||
|
||||
let schema = Schema::from_serde_json_value(complex_resource_schema).unwrap();
|
||||
let schema_rc = Rc::new(schema);
|
||||
|
||||
let result =
|
||||
RESOURCE_SCHEMA_REGISTRY.register("azure.template.arm.patterns", schema_rc.clone());
|
||||
assert!(result.is_ok());
|
||||
assert!(RESOURCE_SCHEMA_REGISTRY.contains("azure.template.arm.patterns"));
|
||||
|
||||
// Verify schema retrieval and pointer equality
|
||||
let retrieved = RESOURCE_SCHEMA_REGISTRY.get("azure.template.arm.patterns");
|
||||
assert!(retrieved.is_some());
|
||||
assert!(Rc::ptr_eq(&schema_rc, &retrieved.unwrap()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resource_schema_with_invalid_names() {
|
||||
let resource_schema = create_resource_schema();
|
||||
|
||||
// Test invalid names
|
||||
assert!(RESOURCE_SCHEMA_REGISTRY
|
||||
.register("", resource_schema.clone())
|
||||
.is_err());
|
||||
assert!(RESOURCE_SCHEMA_REGISTRY
|
||||
.register(" ", resource_schema.clone())
|
||||
.is_err());
|
||||
assert!(RESOURCE_SCHEMA_REGISTRY
|
||||
.register("\t", resource_schema.clone())
|
||||
.is_err());
|
||||
assert!(RESOURCE_SCHEMA_REGISTRY
|
||||
.register("\n", resource_schema)
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resource_schema_duplicate_registration() {
|
||||
let vm_schema = create_vm_resource_schema();
|
||||
|
||||
// First registration should succeed
|
||||
assert!(RESOURCE_SCHEMA_REGISTRY
|
||||
.register("azure.resource.vm.duplicate", vm_schema.clone())
|
||||
.is_ok());
|
||||
|
||||
// Duplicate registration should fail
|
||||
let duplicate_result =
|
||||
RESOURCE_SCHEMA_REGISTRY.register("azure.resource.vm.duplicate", vm_schema);
|
||||
assert!(duplicate_result.is_err());
|
||||
|
||||
// Verify error type
|
||||
match duplicate_result.unwrap_err() {
|
||||
SchemaRegistryError::AlreadyExists { name, .. } => {
|
||||
assert_eq!(name.as_ref(), "azure.resource.vm.duplicate");
|
||||
}
|
||||
_ => panic!("Expected AlreadyExists error"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_azure_resource_removal() {
|
||||
// Register multiple Azure Resource schemas with unique names
|
||||
let resources = vec![
|
||||
("azure.resource.vm.removal", create_vm_resource_schema()),
|
||||
(
|
||||
"azure.resource.storage.removal",
|
||||
create_storage_resource_schema(),
|
||||
),
|
||||
(
|
||||
"azure.resource.network.removal",
|
||||
create_network_resource_schema(),
|
||||
),
|
||||
];
|
||||
|
||||
for (name, schema) in &resources {
|
||||
assert!(RESOURCE_SCHEMA_REGISTRY
|
||||
.register(*name, schema.clone())
|
||||
.is_ok());
|
||||
}
|
||||
|
||||
// Remove one resource
|
||||
let removed = RESOURCE_SCHEMA_REGISTRY.remove("azure.resource.storage.removal");
|
||||
assert!(removed.is_some());
|
||||
assert!(!RESOURCE_SCHEMA_REGISTRY.contains("azure.resource.storage.removal"));
|
||||
|
||||
// Verify the removed schema is correct
|
||||
let removed_schema = removed.unwrap();
|
||||
assert!(Rc::ptr_eq(&resources[1].1, &removed_schema));
|
||||
|
||||
// Other resources should still be present
|
||||
assert!(RESOURCE_SCHEMA_REGISTRY.contains("azure.resource.vm.removal"));
|
||||
assert!(RESOURCE_SCHEMA_REGISTRY.contains("azure.resource.network.removal"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "std")]
|
||||
fn test_concurrent_resource_schema_access() {
|
||||
use std::sync::Barrier;
|
||||
use std::thread;
|
||||
|
||||
let barrier = Rc::new(Barrier::new(3));
|
||||
let mut handles = vec![];
|
||||
|
||||
// Test concurrent registration of different Azure Resource schemas
|
||||
let resources = [
|
||||
"concurrent_resource_schema_access.vm",
|
||||
"concurrent_resource_schema_access.storage",
|
||||
"concurrent_resource_schema_access.network",
|
||||
];
|
||||
|
||||
for (i, resource_name) in resources.iter().enumerate() {
|
||||
let barrier = Rc::clone(&barrier);
|
||||
let name: String = (*resource_name).into();
|
||||
|
||||
let handle: thread::JoinHandle<Result<(), SchemaRegistryError>> =
|
||||
thread::spawn(move || {
|
||||
let schema = match i {
|
||||
0 => create_vm_resource_schema(),
|
||||
1 => create_storage_resource_schema(),
|
||||
2 => create_network_resource_schema(),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
barrier.wait();
|
||||
RESOURCE_SCHEMA_REGISTRY.register(name, schema)
|
||||
});
|
||||
|
||||
handles.push(handle);
|
||||
}
|
||||
|
||||
// Wait for all threads to complete
|
||||
let results: Vec<_> = handles.into_iter().map(|h| h.join().unwrap()).collect();
|
||||
|
||||
// All registrations should succeed
|
||||
for result in results {
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
// Should have resource schemas registered
|
||||
assert!(RESOURCE_SCHEMA_REGISTRY.contains("concurrent_resource_schema_access.vm"));
|
||||
assert!(RESOURCE_SCHEMA_REGISTRY.contains("concurrent_resource_schema_access.storage"));
|
||||
assert!(RESOURCE_SCHEMA_REGISTRY.contains("concurrent_resource_schema_access.network"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resource_type_validation() {
|
||||
// Test different Azure resource types with specific naming patterns
|
||||
let resource_types = vec![
|
||||
"azure.compute.vm.validation",
|
||||
"azure.storage.account.validation",
|
||||
"azure.network.vnet.validation",
|
||||
"azure.keyvault.vault.validation",
|
||||
"azure.sql.database.validation",
|
||||
"azure.webapp.site.validation",
|
||||
];
|
||||
|
||||
let basic_schema = create_resource_schema();
|
||||
|
||||
// Register all resource types
|
||||
for resource_type in &resource_types {
|
||||
let result = RESOURCE_SCHEMA_REGISTRY.register(*resource_type, basic_schema.clone());
|
||||
assert!(result.is_ok(), "Failed to register {resource_type}");
|
||||
}
|
||||
|
||||
// Verify all are registered
|
||||
for resource_type in &resource_types {
|
||||
assert!(
|
||||
RESOURCE_SCHEMA_REGISTRY.contains(resource_type),
|
||||
"Missing {resource_type}"
|
||||
);
|
||||
assert!(
|
||||
RESOURCE_SCHEMA_REGISTRY.get(resource_type).is_some(),
|
||||
"Cannot retrieve {resource_type}"
|
||||
);
|
||||
}
|
||||
|
||||
// Verify list contains all types
|
||||
let names = RESOURCE_SCHEMA_REGISTRY.list_names();
|
||||
|
||||
for resource_type in &resource_types {
|
||||
assert!(
|
||||
names.contains(&(*resource_type).into()),
|
||||
"Name list missing {resource_type}"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -199,7 +199,9 @@ use crate::{format, Box, Rc, Value, Vec};
|
||||
|
||||
type String = Rc<str>;
|
||||
|
||||
pub mod error;
|
||||
mod meta;
|
||||
pub mod validate;
|
||||
|
||||
/// A schema represents a type definition that can be used for validation.
|
||||
///
|
||||
@@ -305,7 +307,7 @@ impl Schema {
|
||||
|
||||
/// Parse a JSON Schema document into a `Schema` instance.
|
||||
/// Provides better error messages than `serde_json::from_value`.
|
||||
fn from_serde_json_value(
|
||||
pub fn from_serde_json_value(
|
||||
schema: serde_json::Value,
|
||||
) -> Result<Self, Box<dyn core::error::Error + Send + Sync>> {
|
||||
let meta_schema_validation_result = meta::validate_schema_detailed(&schema);
|
||||
@@ -319,7 +321,7 @@ impl Schema {
|
||||
|
||||
/// Parse a JSON Schema document from a string into a `Schema` instance.
|
||||
/// Provides better error messages than `serde_json::from_str`.
|
||||
fn from_json_str(s: &str) -> Result<Self, Box<dyn core::error::Error + Send + Sync>> {
|
||||
pub fn from_json_str(s: &str) -> Result<Self, Box<dyn core::error::Error + Send + Sync>> {
|
||||
let value: serde_json::Value =
|
||||
serde_json::from_str(s).map_err(|e| format!("Failed to parse schema: {e}"))?;
|
||||
Self::from_serde_json_value(value)
|
||||
@@ -1030,4 +1032,11 @@ impl<'de> Deserialize<'de> for DiscriminatedSubobject {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
mod tests {
|
||||
mod azure;
|
||||
mod suite;
|
||||
mod validate {
|
||||
mod effect;
|
||||
mod resource;
|
||||
}
|
||||
}
|
||||
|
||||
276
src/schema/error.rs
Normal file
276
src/schema/error.rs
Normal file
@@ -0,0 +1,276 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
use crate::*;
|
||||
|
||||
type String = Rc<str>;
|
||||
|
||||
/// Validation errors that can occur when validating a Value against a Schema.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum ValidationError {
|
||||
/// Value type does not match the expected schema type.
|
||||
TypeMismatch {
|
||||
expected: String,
|
||||
actual: String,
|
||||
path: String,
|
||||
},
|
||||
/// Numeric value is outside the allowed range.
|
||||
OutOfRange {
|
||||
value: String,
|
||||
min: Option<String>,
|
||||
max: Option<String>,
|
||||
path: String,
|
||||
},
|
||||
/// String length constraint violation.
|
||||
LengthConstraint {
|
||||
actual_length: usize,
|
||||
min_length: Option<usize>,
|
||||
max_length: Option<usize>,
|
||||
path: String,
|
||||
},
|
||||
/// String does not match required pattern.
|
||||
PatternMismatch {
|
||||
value: String,
|
||||
pattern: String,
|
||||
path: String,
|
||||
},
|
||||
/// Array size constraint violation.
|
||||
ArraySizeConstraint {
|
||||
actual_size: usize,
|
||||
min_items: Option<usize>,
|
||||
max_items: Option<usize>,
|
||||
path: String,
|
||||
},
|
||||
/// Required object property is missing.
|
||||
MissingRequiredProperty { property: String, path: String },
|
||||
/// Object property failed validation.
|
||||
PropertyValidationFailed {
|
||||
property: String,
|
||||
path: String,
|
||||
error: Box<ValidationError>,
|
||||
},
|
||||
/// Additional properties are not allowed.
|
||||
AdditionalPropertiesNotAllowed { property: String, path: String },
|
||||
/// Value is not in the allowed enum values.
|
||||
NotInEnum {
|
||||
value: String,
|
||||
allowed_values: Vec<String>,
|
||||
path: String,
|
||||
},
|
||||
/// Value does not match the required constant.
|
||||
ConstMismatch {
|
||||
expected: String,
|
||||
actual: String,
|
||||
path: String,
|
||||
},
|
||||
/// Value does not match any schema in a union (anyOf).
|
||||
NoUnionMatch {
|
||||
path: String,
|
||||
errors: Vec<ValidationError>,
|
||||
},
|
||||
/// Invalid regex pattern in schema.
|
||||
InvalidPattern { pattern: String, error: String },
|
||||
/// Array item validation failed.
|
||||
ArrayItemValidationFailed {
|
||||
index: usize,
|
||||
path: String,
|
||||
error: Box<ValidationError>,
|
||||
},
|
||||
/// Object key is not a string.
|
||||
NonStringKey { key_type: String, path: String },
|
||||
/// Missing discriminator field in discriminated subobject.
|
||||
MissingDiscriminator { discriminator: String, path: String },
|
||||
/// Unknown discriminator value in discriminated subobject.
|
||||
UnknownDiscriminatorValue {
|
||||
discriminator: String,
|
||||
value: String,
|
||||
allowed_values: Vec<String>,
|
||||
path: String,
|
||||
},
|
||||
/// Discriminated subobject validation failed.
|
||||
DiscriminatedSubobjectValidationFailed {
|
||||
discriminator: String,
|
||||
value: String,
|
||||
path: String,
|
||||
error: Box<ValidationError>,
|
||||
},
|
||||
}
|
||||
|
||||
impl fmt::Display for ValidationError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
ValidationError::TypeMismatch {
|
||||
expected,
|
||||
actual,
|
||||
path,
|
||||
} => {
|
||||
write!(
|
||||
f,
|
||||
"Type mismatch at '{path}': expected {expected}, got {actual}"
|
||||
)
|
||||
}
|
||||
ValidationError::OutOfRange {
|
||||
value,
|
||||
min,
|
||||
max,
|
||||
path,
|
||||
} => {
|
||||
let range_desc = match (min, max) {
|
||||
(Some(min), Some(max)) => format!("between {min} and {max}"),
|
||||
(Some(min), None) => format!("at least {min}"),
|
||||
(None, Some(max)) => format!("at most {max}"),
|
||||
(None, None) => "within valid range".to_string(),
|
||||
};
|
||||
write!(
|
||||
f,
|
||||
"Value {value} at '{path}' is out of range: must be {range_desc}"
|
||||
)
|
||||
}
|
||||
ValidationError::LengthConstraint {
|
||||
actual_length,
|
||||
min_length,
|
||||
max_length,
|
||||
path,
|
||||
} => {
|
||||
let constraint_desc = match (min_length, max_length) {
|
||||
(Some(min), Some(max)) => format!("between {min} and {max} characters"),
|
||||
(Some(min), None) => format!("at least {min} characters"),
|
||||
(None, Some(max)) => format!("at most {max} characters"),
|
||||
(None, None) => "within valid length".to_string(),
|
||||
};
|
||||
write!(
|
||||
f,
|
||||
"String length {actual_length} at '{path}' violates constraint: must be {constraint_desc}"
|
||||
)
|
||||
}
|
||||
ValidationError::PatternMismatch {
|
||||
value,
|
||||
pattern,
|
||||
path,
|
||||
} => {
|
||||
write!(
|
||||
f,
|
||||
"String '{value}' at '{path}' does not match pattern '{pattern}'"
|
||||
)
|
||||
}
|
||||
ValidationError::ArraySizeConstraint {
|
||||
actual_size,
|
||||
min_items,
|
||||
max_items,
|
||||
path,
|
||||
} => {
|
||||
let constraint_desc = match (min_items, max_items) {
|
||||
(Some(min), Some(max)) => format!("between {min} and {max} items"),
|
||||
(Some(min), None) => format!("at least {min} items"),
|
||||
(None, Some(max)) => format!("at most {max} items"),
|
||||
(None, None) => "within valid size".to_string(),
|
||||
};
|
||||
write!(
|
||||
f,
|
||||
"Array size {actual_size} at '{path}' violates constraint: must have {constraint_desc}"
|
||||
)
|
||||
}
|
||||
ValidationError::MissingRequiredProperty { property, path } => {
|
||||
write!(f, "Missing required property '{property}' at '{path}'")
|
||||
}
|
||||
ValidationError::PropertyValidationFailed {
|
||||
property,
|
||||
path,
|
||||
error,
|
||||
} => {
|
||||
write!(
|
||||
f,
|
||||
"Property '{property}' at '{path}' failed validation: {error}"
|
||||
)
|
||||
}
|
||||
ValidationError::AdditionalPropertiesNotAllowed { property, path } => {
|
||||
write!(
|
||||
f,
|
||||
"Additional property '{property}' not allowed at '{path}'"
|
||||
)
|
||||
}
|
||||
ValidationError::NotInEnum {
|
||||
value,
|
||||
allowed_values,
|
||||
path,
|
||||
} => {
|
||||
let values_json = serde_json::to_string(&allowed_values)
|
||||
.unwrap_or_else(|_| format!("{allowed_values:?}"));
|
||||
|
||||
write!(
|
||||
f,
|
||||
"Value '{value}' at '{path}' is not in allowed enum values: {values_json}",
|
||||
)
|
||||
}
|
||||
ValidationError::ConstMismatch {
|
||||
expected,
|
||||
actual,
|
||||
path,
|
||||
} => {
|
||||
write!(
|
||||
f,
|
||||
"Constant mismatch at '{path}': expected '{expected}', got '{actual}'"
|
||||
)
|
||||
}
|
||||
ValidationError::NoUnionMatch { path, errors } => {
|
||||
write!(
|
||||
f,
|
||||
"Value at '{path}' does not match any schema in union. Errors: {errors:?}"
|
||||
)
|
||||
}
|
||||
ValidationError::InvalidPattern { pattern, error } => {
|
||||
write!(f, "Invalid regex pattern '{pattern}': {error}")
|
||||
}
|
||||
ValidationError::ArrayItemValidationFailed { index, path, error } => {
|
||||
write!(
|
||||
f,
|
||||
"Array item {index} at '{path}' failed validation: {error}"
|
||||
)
|
||||
}
|
||||
ValidationError::NonStringKey { key_type, path } => {
|
||||
write!(
|
||||
f,
|
||||
"Object key at '{path}' must be a string, but found {key_type}"
|
||||
)
|
||||
}
|
||||
ValidationError::MissingDiscriminator {
|
||||
discriminator,
|
||||
path,
|
||||
} => {
|
||||
write!(
|
||||
f,
|
||||
"Missing discriminator field '{discriminator}' at '{path}'"
|
||||
)
|
||||
}
|
||||
ValidationError::UnknownDiscriminatorValue {
|
||||
discriminator,
|
||||
value,
|
||||
allowed_values,
|
||||
path,
|
||||
} => {
|
||||
let values_json: Vec<serde_json::Value> = allowed_values
|
||||
.iter()
|
||||
.map(|v| serde_json::Value::String(v.to_string()))
|
||||
.collect();
|
||||
write!(
|
||||
f,
|
||||
"Unknown discriminator value '{value}' for field '{discriminator}' at '{path}'. Allowed values: {}",
|
||||
serde_json::to_string(&values_json).unwrap_or_else(|_| format!("{values_json:?}"))
|
||||
)
|
||||
}
|
||||
ValidationError::DiscriminatedSubobjectValidationFailed {
|
||||
discriminator,
|
||||
value,
|
||||
path,
|
||||
error,
|
||||
} => {
|
||||
write!(
|
||||
f,
|
||||
"Discriminated subobject validation failed for discriminator '{discriminator}' with value '{value}' at '{path}': {error}"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl core::error::Error for ValidationError {}
|
||||
@@ -3,3 +3,4 @@
|
||||
|
||||
mod azure;
|
||||
mod suite;
|
||||
mod validate;
|
||||
|
||||
1362
src/schema/tests/validate.rs
Normal file
1362
src/schema/tests/validate.rs
Normal file
File diff suppressed because it is too large
Load Diff
511
src/schema/tests/validate/effect.rs
Normal file
511
src/schema/tests/validate/effect.rs
Normal file
@@ -0,0 +1,511 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
use crate::{
|
||||
schema::{error::ValidationError, validate::SchemaValidator, Schema},
|
||||
*,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
// Helper function to create a schema for Azure Policy effects
|
||||
fn create_effect_schema() -> Rc<Schema> {
|
||||
let schema_json = json!({
|
||||
"enum": ["audit", "deny", "disabled", "modify"],
|
||||
"description": "Azure Policy effect types"
|
||||
});
|
||||
|
||||
let schema = Schema::from_serde_json_value(schema_json).unwrap();
|
||||
Rc::new(schema)
|
||||
}
|
||||
|
||||
// Helper function to create a deny effect schema
|
||||
fn create_deny_effect_schema() -> Rc<Schema> {
|
||||
let schema_json = json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"effect": {
|
||||
"const": "deny"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Explanation of what is being denied"
|
||||
}
|
||||
},
|
||||
"required": ["effect"],
|
||||
"description": "Schema for deny effect in Azure Policy"
|
||||
});
|
||||
|
||||
let schema = Schema::from_serde_json_value(schema_json).unwrap();
|
||||
Rc::new(schema)
|
||||
}
|
||||
|
||||
// Helper function to create an audit effect schema
|
||||
fn create_audit_effect_schema() -> Rc<Schema> {
|
||||
let schema_json = json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"effect": {
|
||||
"const": "audit"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Explanation of what is being audited"
|
||||
},
|
||||
"auditDetails": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"category": {
|
||||
"enum": ["security", "compliance", "cost", "operational"]
|
||||
},
|
||||
"severity": {
|
||||
"enum": ["low", "medium", "high", "critical"]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["effect"],
|
||||
"description": "Schema for audit effect in Azure Policy"
|
||||
});
|
||||
|
||||
let schema = Schema::from_serde_json_value(schema_json).unwrap();
|
||||
Rc::new(schema)
|
||||
}
|
||||
|
||||
// Helper function to create a modify effect schema
|
||||
fn create_modify_effect_schema() -> Rc<Schema> {
|
||||
let schema_json = json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"effect": {
|
||||
"const": "modify"
|
||||
},
|
||||
"description": {
|
||||
"type": "string",
|
||||
"description": "Explanation of what is being modified"
|
||||
},
|
||||
"modifyDetails": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"roleDefinitionIds": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "List of role definition IDs required for modification"
|
||||
},
|
||||
"operations": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"operation": {
|
||||
"enum": ["add", "replace", "remove"]
|
||||
},
|
||||
"field": {
|
||||
"type": "string"
|
||||
},
|
||||
"value": {
|
||||
"type": "any",
|
||||
"description": "Value to add or replace"
|
||||
}
|
||||
},
|
||||
"required": ["operation", "field"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": ["roleDefinitionIds", "operations"]
|
||||
}
|
||||
},
|
||||
"required": ["effect", "modifyDetails"],
|
||||
"description": "Schema for modify effect in Azure Policy"
|
||||
});
|
||||
|
||||
let schema = Schema::from_serde_json_value(schema_json).unwrap();
|
||||
Rc::new(schema)
|
||||
}
|
||||
|
||||
// Schema validation tests for Azure Policy effects
|
||||
|
||||
#[test]
|
||||
fn test_validate_deny_effect_valid() {
|
||||
let schema = create_deny_effect_schema();
|
||||
|
||||
let valid_deny_data = json!({
|
||||
"effect": "deny",
|
||||
"description": "Deny resources that don't meet security requirements"
|
||||
});
|
||||
|
||||
let value = Value::from(valid_deny_data);
|
||||
let result = SchemaValidator::validate(&value, &schema);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_deny_effect_missing_required() {
|
||||
let schema = create_deny_effect_schema();
|
||||
|
||||
let invalid_deny_data = json!({
|
||||
"description": "Missing required effect field"
|
||||
});
|
||||
|
||||
let value = Value::from(invalid_deny_data);
|
||||
let result = SchemaValidator::validate(&value, &schema);
|
||||
assert!(result.is_err());
|
||||
|
||||
match result.unwrap_err() {
|
||||
ValidationError::MissingRequiredProperty { property, .. } => {
|
||||
assert_eq!(property, "effect".into());
|
||||
}
|
||||
other => panic!("Expected MissingRequiredProperty error, got: {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_deny_effect_wrong_const() {
|
||||
let schema = create_deny_effect_schema();
|
||||
|
||||
let invalid_deny_data = json!({
|
||||
"effect": "audit",
|
||||
"description": "Wrong effect type"
|
||||
});
|
||||
|
||||
let value = Value::from(invalid_deny_data);
|
||||
let result = SchemaValidator::validate(&value, &schema);
|
||||
assert!(result.is_err());
|
||||
|
||||
match result.unwrap_err() {
|
||||
ValidationError::PropertyValidationFailed {
|
||||
property, error, ..
|
||||
} => {
|
||||
assert_eq!(property, "effect".into());
|
||||
match error.as_ref() {
|
||||
ValidationError::ConstMismatch {
|
||||
expected, actual, ..
|
||||
} => {
|
||||
assert_eq!(*expected, "\"deny\"".into());
|
||||
assert_eq!(*actual, "\"audit\"".into());
|
||||
}
|
||||
other => panic!(
|
||||
"Expected ConstMismatch error in nested structure, got: {:?}",
|
||||
other
|
||||
),
|
||||
}
|
||||
}
|
||||
other => panic!("Expected PropertyValidationFailed error, got: {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_audit_effect_valid() {
|
||||
let schema = create_audit_effect_schema();
|
||||
|
||||
let valid_audit_data = json!({
|
||||
"effect": "audit",
|
||||
"description": "Audit non-compliant resources",
|
||||
"auditDetails": {
|
||||
"category": "security",
|
||||
"severity": "high"
|
||||
}
|
||||
});
|
||||
|
||||
let value = Value::from(valid_audit_data);
|
||||
let result = SchemaValidator::validate(&value, &schema);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_audit_effect_invalid_enum() {
|
||||
let schema = create_audit_effect_schema();
|
||||
|
||||
let invalid_audit_data = json!({
|
||||
"effect": "audit",
|
||||
"description": "Audit with invalid category",
|
||||
"auditDetails": {
|
||||
"category": "invalid_category",
|
||||
"severity": "medium"
|
||||
}
|
||||
});
|
||||
|
||||
let value = Value::from(invalid_audit_data);
|
||||
let result = SchemaValidator::validate(&value, &schema);
|
||||
assert!(result.is_err());
|
||||
|
||||
match result.unwrap_err() {
|
||||
ValidationError::PropertyValidationFailed {
|
||||
property, error, ..
|
||||
} => {
|
||||
assert_eq!(property, "auditDetails".into());
|
||||
match error.as_ref() {
|
||||
ValidationError::PropertyValidationFailed {
|
||||
property: inner_prop,
|
||||
error: inner_error,
|
||||
..
|
||||
} => {
|
||||
assert_eq!(*inner_prop, "category".into());
|
||||
match inner_error.as_ref() {
|
||||
ValidationError::NotInEnum { .. } => {
|
||||
// Expected nested error structure
|
||||
}
|
||||
other => panic!(
|
||||
"Expected NotInEnum error in nested structure, got: {:?}",
|
||||
other
|
||||
),
|
||||
}
|
||||
}
|
||||
other => panic!(
|
||||
"Expected nested PropertyValidationFailed error, got: {:?}",
|
||||
other
|
||||
),
|
||||
}
|
||||
}
|
||||
other => panic!("Expected PropertyValidationFailed error, got: {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_modify_effect_valid() {
|
||||
let schema = create_modify_effect_schema();
|
||||
|
||||
let valid_modify_data = json!({
|
||||
"effect": "modify",
|
||||
"description": "Modify resources to add required tags",
|
||||
"modifyDetails": {
|
||||
"roleDefinitionIds": [
|
||||
"/subscriptions/{subscriptionId}/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c"
|
||||
],
|
||||
"operations": [
|
||||
{
|
||||
"operation": "add",
|
||||
"field": "tags.Environment",
|
||||
"value": "Production"
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
let value = Value::from(valid_modify_data);
|
||||
let result = SchemaValidator::validate(&value, &schema);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_modify_effect_missing_required_details() {
|
||||
let schema = create_modify_effect_schema();
|
||||
|
||||
let invalid_modify_data = json!({
|
||||
"effect": "modify",
|
||||
"description": "Missing modifyDetails"
|
||||
});
|
||||
|
||||
let value = Value::from(invalid_modify_data);
|
||||
let result = SchemaValidator::validate(&value, &schema);
|
||||
assert!(result.is_err());
|
||||
|
||||
match result.unwrap_err() {
|
||||
ValidationError::MissingRequiredProperty { property, .. } => {
|
||||
assert_eq!(property, "modifyDetails".into());
|
||||
}
|
||||
other => panic!("Expected MissingRequiredProperty error, got: {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_modify_effect_invalid_operation() {
|
||||
let schema = create_modify_effect_schema();
|
||||
|
||||
let invalid_modify_data = json!({
|
||||
"effect": "modify",
|
||||
"description": "Invalid operation type",
|
||||
"modifyDetails": {
|
||||
"roleDefinitionIds": ["role-id-1"],
|
||||
"operations": [
|
||||
{
|
||||
"operation": "invalid_op",
|
||||
"field": "tags.Environment",
|
||||
"value": "Production"
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
let value = Value::from(invalid_modify_data);
|
||||
let result = SchemaValidator::validate(&value, &schema);
|
||||
assert!(result.is_err());
|
||||
|
||||
match result.unwrap_err() {
|
||||
ValidationError::PropertyValidationFailed {
|
||||
property, error, ..
|
||||
} => {
|
||||
assert_eq!(property, "modifyDetails".into());
|
||||
match error.as_ref() {
|
||||
ValidationError::PropertyValidationFailed {
|
||||
property: inner_prop,
|
||||
error: inner_error,
|
||||
..
|
||||
} => {
|
||||
assert_eq!(*inner_prop, "operations".into());
|
||||
match inner_error.as_ref() {
|
||||
ValidationError::ArrayItemValidationFailed {
|
||||
index,
|
||||
error: array_error,
|
||||
..
|
||||
} => {
|
||||
assert_eq!(*index, 0);
|
||||
match array_error.as_ref() {
|
||||
ValidationError::PropertyValidationFailed {
|
||||
property: op_prop,
|
||||
error: op_error,
|
||||
..
|
||||
} => {
|
||||
assert_eq!(*op_prop, "operation".into());
|
||||
match op_error.as_ref() {
|
||||
ValidationError::NotInEnum { .. } => {
|
||||
// Expected deeply nested error structure
|
||||
}
|
||||
other => panic!("Expected NotInEnum error in operation validation, got: {:?}", other),
|
||||
}
|
||||
}
|
||||
other => panic!(
|
||||
"Expected PropertyValidationFailed for operation, got: {:?}",
|
||||
other
|
||||
),
|
||||
}
|
||||
}
|
||||
other => {
|
||||
panic!("Expected ArrayItemValidationFailed error, got: {:?}", other)
|
||||
}
|
||||
}
|
||||
}
|
||||
other => panic!(
|
||||
"Expected nested PropertyValidationFailed error, got: {:?}",
|
||||
other
|
||||
),
|
||||
}
|
||||
}
|
||||
other => panic!("Expected PropertyValidationFailed error, got: {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_basic_effect_enum() {
|
||||
let schema = create_effect_schema();
|
||||
|
||||
// Test all valid enum values
|
||||
let valid_effects = ["audit", "deny", "disabled", "modify"];
|
||||
|
||||
for effect in valid_effects {
|
||||
let value = Value::from(effect);
|
||||
let result = SchemaValidator::validate(&value, &schema);
|
||||
assert!(result.is_ok(), "Effect '{effect}' should be valid");
|
||||
}
|
||||
|
||||
// Test invalid enum value
|
||||
let invalid_value = Value::from("invalid_effect");
|
||||
let result = SchemaValidator::validate(&invalid_value, &schema);
|
||||
assert!(result.is_err());
|
||||
|
||||
match result.unwrap_err() {
|
||||
ValidationError::NotInEnum { .. } => {
|
||||
// Expected error type
|
||||
}
|
||||
other => panic!("Expected NotInEnum error, got: {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_complex_azure_policy_effect() {
|
||||
let complex_schema_json = json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"effect": {
|
||||
"enum": ["auditIfNotExists", "deployIfNotExists"]
|
||||
},
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"additionalProperties": { "type": "any" }
|
||||
},
|
||||
"existenceCondition": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"field": { "type": "string" },
|
||||
"equals": { "type": "string" }
|
||||
},
|
||||
"required": ["field"],
|
||||
"additionalProperties": { "type": "any" }
|
||||
},
|
||||
"deployment": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"properties": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"mode": {
|
||||
"enum": ["incremental", "complete"]
|
||||
},
|
||||
"template": {
|
||||
"type": "object",
|
||||
"additionalProperties": { "type": "any" }
|
||||
},
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"additionalProperties": { "type": "any" }
|
||||
}
|
||||
},
|
||||
"required": ["mode", "template"],
|
||||
"additionalProperties": { "type": "any" }
|
||||
}
|
||||
},
|
||||
"required": ["properties"],
|
||||
"additionalProperties": { "type": "any" }
|
||||
}
|
||||
},
|
||||
"required": ["effect"],
|
||||
"additionalProperties": { "type": "any" }
|
||||
});
|
||||
|
||||
let schema = Schema::from_serde_json_value(complex_schema_json).unwrap();
|
||||
|
||||
let valid_complex_data = json!({
|
||||
"effect": "deployIfNotExists",
|
||||
"parameters": {},
|
||||
"existenceCondition": {
|
||||
"field": "Microsoft.Security/complianceResults/resourceStatus",
|
||||
"equals": "OffByPolicy"
|
||||
},
|
||||
"deployment": {
|
||||
"properties": {
|
||||
"mode": "incremental",
|
||||
"template": {
|
||||
"$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
|
||||
"contentVersion": "1.0.0.0",
|
||||
"resources": []
|
||||
},
|
||||
"parameters": {}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let value = Value::from(valid_complex_data);
|
||||
let result = SchemaValidator::validate(&value, &schema);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_effect_type_mismatch() {
|
||||
let schema = create_deny_effect_schema();
|
||||
|
||||
// Pass a non-object value to object schema
|
||||
let invalid_data = Value::from("not an object");
|
||||
let result = SchemaValidator::validate(&invalid_data, &schema);
|
||||
assert!(result.is_err());
|
||||
|
||||
match result.unwrap_err() {
|
||||
ValidationError::TypeMismatch {
|
||||
expected, actual, ..
|
||||
} => {
|
||||
assert_eq!(expected, "object".into());
|
||||
assert_eq!(actual, "string".into());
|
||||
}
|
||||
other => panic!("Expected TypeMismatch error, got: {:?}", other),
|
||||
}
|
||||
}
|
||||
1328
src/schema/tests/validate/resource.rs
Normal file
1328
src/schema/tests/validate/resource.rs
Normal file
File diff suppressed because it is too large
Load Diff
731
src/schema/validate.rs
Normal file
731
src/schema/validate.rs
Normal file
@@ -0,0 +1,731 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
#![allow(dead_code)]
|
||||
|
||||
use crate::{
|
||||
schema::{error::ValidationError, Schema, Type},
|
||||
*,
|
||||
};
|
||||
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: &BTreeMap<Value, Value>,
|
||||
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: &BTreeMap<Value, Value>,
|
||||
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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user