diff --git a/Cargo.lock b/Cargo.lock index 21abc3b..0156885 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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", ] diff --git a/Cargo.toml b/Cargo.toml index 6cf5588..a239c12 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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 diff --git a/benches/schema_validation_benchmark.rs b/benches/schema_validation_benchmark.rs new file mode 100644 index 0000000..f13e199 --- /dev/null +++ b/benches/schema_validation_benchmark.rs @@ -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); diff --git a/bindings/ffi/Cargo.lock b/bindings/ffi/Cargo.lock index 054ed4d..b53a499 100644 --- a/bindings/ffi/Cargo.lock +++ b/bindings/ffi/Cargo.lock @@ -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", diff --git a/bindings/java/Cargo.lock b/bindings/java/Cargo.lock index 85172a5..a19b869 100644 --- a/bindings/java/Cargo.lock +++ b/bindings/java/Cargo.lock @@ -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", diff --git a/bindings/python/Cargo.lock b/bindings/python/Cargo.lock index ba87ec7..38275d6 100644 --- a/bindings/python/Cargo.lock +++ b/bindings/python/Cargo.lock @@ -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", diff --git a/bindings/ruby/Cargo.lock b/bindings/ruby/Cargo.lock index a8f5c28..55b7c9e 100644 --- a/bindings/ruby/Cargo.lock +++ b/bindings/ruby/Cargo.lock @@ -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", diff --git a/bindings/wasm/Cargo.lock b/bindings/wasm/Cargo.lock index 1407b66..440716d 100644 --- a/bindings/wasm/Cargo.lock +++ b/bindings/wasm/Cargo.lock @@ -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", diff --git a/src/lib.rs b/src/lib.rs index 8c40dff..996a493 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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")] diff --git a/src/registry.rs b/src/registry.rs new file mode 100644 index 0000000..3bb7aaf --- /dev/null +++ b/src/registry.rs @@ -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; + +#[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 { + inner: DashMap>, + name: String, +} + +impl Registry { + /// Create a new, empty registry with a given name. + pub fn new(registry_name: impl Into) -> 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, item: Rc) -> 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> { + 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> { + self.inner.remove(name).map(|(_, v)| v) + } + + /// List all registered item names. + pub fn list_names(&self) -> Vec { + 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)> + '_ { + 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> { + 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, + item: Rc, + ) -> Result { + 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; + +/// 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 = Registry::new("RESOURCE_SCHEMA_REGISTRY"); + } + + lazy_static::lazy_static! { + /// Global singleton instance of effect schemas registry. + pub static ref EFFECT_SCHEMA_REGISTRY: Registry = 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, + 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> { + $registry_var.get(name) + } + + #[doc = concat!("Remove a ", $item_description, " by name.")] + pub fn remove(name: &str) -> Option> { + $registry_var.remove(name) + } + + #[doc = concat!("List all registered ", $item_description, " names.")] + pub fn list_names() -> Vec { + $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" + ); +} diff --git a/src/registry/tests/core.rs b/src/registry/tests/core.rs new file mode 100644 index 0000000..9590019 --- /dev/null +++ b/src/registry/tests/core.rs @@ -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; +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 { + 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()); +} diff --git a/src/registry/tests/effect.rs b/src/registry/tests/effect.rs new file mode 100644 index 0000000..a7da06f --- /dev/null +++ b/src/registry/tests/effect.rs @@ -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; +type SchemaRegistryError = RegistryError; + +// Helper function to create a schema for Azure Policy effects +fn create_effect_schema() -> Rc { + 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 { + 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 { + 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 { + 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> = + 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")); +} diff --git a/src/registry/tests/resource.rs b/src/registry/tests/resource.rs new file mode 100644 index 0000000..2c42829 --- /dev/null +++ b/src/registry/tests/resource.rs @@ -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; +type SchemaRegistryError = RegistryError; + +// Helper function to create a schema for Azure Resource types +fn create_resource_schema() -> Rc { + 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 { + 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 { + 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 { + 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> = + 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}" + ); + } +} diff --git a/src/schema.rs b/src/schema.rs index 0304829..01d54a7 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -199,7 +199,9 @@ use crate::{format, Box, Rc, Value, Vec}; type String = Rc; +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> { 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> { + pub fn from_json_str(s: &str) -> Result> { 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; + } +} diff --git a/src/schema/error.rs b/src/schema/error.rs new file mode 100644 index 0000000..b092db9 --- /dev/null +++ b/src/schema/error.rs @@ -0,0 +1,276 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::*; + +type String = Rc; + +/// 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, + max: Option, + path: String, + }, + /// String length constraint violation. + LengthConstraint { + actual_length: usize, + min_length: Option, + max_length: Option, + 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, + max_items: Option, + path: String, + }, + /// Required object property is missing. + MissingRequiredProperty { property: String, path: String }, + /// Object property failed validation. + PropertyValidationFailed { + property: String, + path: String, + error: Box, + }, + /// Additional properties are not allowed. + AdditionalPropertiesNotAllowed { property: String, path: String }, + /// Value is not in the allowed enum values. + NotInEnum { + value: String, + allowed_values: Vec, + 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, + }, + /// Invalid regex pattern in schema. + InvalidPattern { pattern: String, error: String }, + /// Array item validation failed. + ArrayItemValidationFailed { + index: usize, + path: String, + error: Box, + }, + /// 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, + path: String, + }, + /// Discriminated subobject validation failed. + DiscriminatedSubobjectValidationFailed { + discriminator: String, + value: String, + path: String, + error: Box, + }, +} + +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 = 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 {} diff --git a/src/schema/tests.rs b/src/schema/tests.rs index 3f4085c..2093adb 100644 --- a/src/schema/tests.rs +++ b/src/schema/tests.rs @@ -3,3 +3,4 @@ mod azure; mod suite; +mod validate; diff --git a/src/schema/tests/validate.rs b/src/schema/tests/validate.rs new file mode 100644 index 0000000..fce2108 --- /dev/null +++ b/src/schema/tests/validate.rs @@ -0,0 +1,1362 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::{ + schema::{error::ValidationError, validate::SchemaValidator, Schema}, + *, +}; +use alloc::collections::BTreeMap; +use serde_json::json; + +#[test] +fn test_validate_integer() { + let schema_json = json!({ + "type": "integer", + "minimum": 0, + "maximum": 100 + }); + let schema = Schema::from_serde_json_value(schema_json).unwrap(); + + // Valid integer + assert!(SchemaValidator::validate(&Value::from(50), &schema).is_ok()); + + // Invalid - below minimum + let result = SchemaValidator::validate(&Value::from(-1), &schema); + assert!(result.is_err()); + if let Err(ValidationError::OutOfRange { + value, + min, + max, + path, + }) = result + { + assert_eq!(value.as_ref(), "-1"); + assert_eq!(min.as_ref().map(|s| s.as_ref()), Some("0")); + assert_eq!(max.as_ref().map(|s| s.as_ref()), Some("100")); + assert_eq!(path.as_ref(), ""); + } else { + panic!("Expected OutOfRange error"); + } + + // Invalid - above maximum + let result = SchemaValidator::validate(&Value::from(101), &schema); + assert!(result.is_err()); + if let Err(ValidationError::OutOfRange { + value, + min, + max, + path, + }) = result + { + assert_eq!(value.as_ref(), "101"); + assert_eq!(min.as_ref().map(|s| s.as_ref()), Some("0")); + assert_eq!(max.as_ref().map(|s| s.as_ref()), Some("100")); + assert_eq!(path.as_ref(), ""); + } else { + panic!("Expected OutOfRange error"); + } + + // Invalid - not an integer + let result = SchemaValidator::validate(&Value::from("string"), &schema); + assert!(result.is_err()); + if let Err(ValidationError::TypeMismatch { + expected, + actual, + path, + }) = result + { + assert_eq!(expected.as_ref(), "integer"); + assert_eq!(actual.as_ref(), "string"); + assert_eq!(path.as_ref(), ""); + } else { + panic!("Expected TypeMismatch error"); + } +} + +#[test] +fn test_validate_string() { + let schema_json = json!({ + "type": "string", + "minLength": 2, + "maxLength": 10, + "pattern": "^[a-zA-Z]+$" + }); + let schema = Schema::from_serde_json_value(schema_json).unwrap(); + + // Valid string + assert!(SchemaValidator::validate(&Value::from("hello"), &schema).is_ok()); + + // Invalid - too short + let result = SchemaValidator::validate(&Value::from("a"), &schema); + assert!(result.is_err()); + if let Err(ValidationError::LengthConstraint { + actual_length, + min_length, + max_length, + path, + }) = result + { + assert_eq!(actual_length, 1); + assert_eq!(min_length, Some(2)); + assert_eq!(max_length, Some(10)); + assert_eq!(path.as_ref(), ""); + } else { + panic!("Expected LengthConstraint error"); + } + + // Invalid - too long + let result = SchemaValidator::validate(&Value::from("verylongstring"), &schema); + assert!(result.is_err()); + if let Err(ValidationError::LengthConstraint { + actual_length, + min_length, + max_length, + path, + }) = result + { + assert_eq!(actual_length, 14); + assert_eq!(min_length, Some(2)); + assert_eq!(max_length, Some(10)); + assert_eq!(path.as_ref(), ""); + } else { + panic!("Expected LengthConstraint error"); + } + + // Invalid - pattern mismatch + let result = SchemaValidator::validate(&Value::from("hello123"), &schema); + assert!(result.is_err()); + if let Err(ValidationError::PatternMismatch { + value, + pattern, + path, + }) = result + { + assert_eq!(value.as_ref(), "hello123"); + assert_eq!(pattern.as_ref(), "^[a-zA-Z]+$"); + assert_eq!(path.as_ref(), ""); + } else { + panic!("Expected PatternMismatch error"); + } + + // Invalid - not a string + let result = SchemaValidator::validate(&Value::from(42), &schema); + assert!(result.is_err()); + if let Err(ValidationError::TypeMismatch { + expected, + actual, + path, + }) = result + { + assert_eq!(expected.as_ref(), "string"); + assert_eq!(actual.as_ref(), "number"); + assert_eq!(path.as_ref(), ""); + } else { + panic!("Expected TypeMismatch error"); + } +} + +#[test] +fn test_validate_array() { + let schema_json = json!({ + "type": "array", + "items": { "type": "string" }, + "minItems": 1, + "maxItems": 3 + }); + let schema = Schema::from_serde_json_value(schema_json).unwrap(); + + // Valid array + let valid_json = json!(["item1", "item2"]); + let valid_array = Value::from(valid_json); + assert!(SchemaValidator::validate(&valid_array, &schema).is_ok()); + + // Invalid - empty array (below minItems) + let empty_json = json!([]); + let empty_array = Value::from(empty_json); + let result = SchemaValidator::validate(&empty_array, &schema); + assert!(result.is_err()); + if let Err(ValidationError::ArraySizeConstraint { + actual_size, + min_items, + max_items, + path, + }) = result + { + assert_eq!(actual_size, 0); + assert_eq!(min_items, Some(1)); + assert_eq!(max_items, Some(3)); + assert_eq!(path.as_ref(), ""); + } else { + panic!("Expected ArraySizeConstraint error"); + } + + // Invalid - too many items + let large_json = json!(["item1", "item2", "item3", "item4"]); + let large_array = Value::from(large_json); + let result = SchemaValidator::validate(&large_array, &schema); + assert!(result.is_err()); + if let Err(ValidationError::ArraySizeConstraint { + actual_size, + min_items, + max_items, + path, + }) = result + { + assert_eq!(actual_size, 4); + assert_eq!(min_items, Some(1)); + assert_eq!(max_items, Some(3)); + assert_eq!(path.as_ref(), ""); + } else { + panic!("Expected ArraySizeConstraint error"); + } + + // Invalid - wrong item type + let wrong_type_json = json!(["item1", 42]); + let wrong_type_array = Value::from(wrong_type_json); + let result = SchemaValidator::validate(&wrong_type_array, &schema); + assert!(result.is_err()); + if let Err(ValidationError::ArrayItemValidationFailed { index, path, error }) = result { + assert_eq!(index, 1); + assert_eq!(path.as_ref(), ""); + + // Assert the exact nested error + if let ValidationError::TypeMismatch { + expected, actual, .. + } = error.as_ref() + { + assert_eq!(expected.as_ref(), "string"); + assert_eq!(actual.as_ref(), "number"); + } else { + panic!("Expected nested TypeMismatch error in ArrayItemValidationFailed"); + } + } else { + panic!("Expected ArrayItemValidationFailed error"); + } +} + +#[test] +fn test_validate_object() { + let schema_json = json!({ + "type": "object", + "properties": { + "name": { "type": "string" }, + "age": { "type": "integer", "minimum": 0 } + }, + "required": ["name"] + }); + let schema = Schema::from_serde_json_value(schema_json).unwrap(); + + // Valid object + let valid_json = json!({ + "name": "John", + "age": 30 + }); + let valid_object = Value::from(valid_json); + assert!(SchemaValidator::validate(&valid_object, &schema).is_ok()); + + // Invalid - missing required property + let missing_json = json!({ + "age": 30 + }); + let missing_required = Value::from(missing_json); + let result = SchemaValidator::validate(&missing_required, &schema); + assert!(result.is_err()); + if let Err(ValidationError::MissingRequiredProperty { property, path }) = result { + assert_eq!(property.as_ref(), "name"); + assert_eq!(path.as_ref(), ""); + } else { + panic!("Expected MissingRequiredProperty error"); + } + + // Invalid - wrong property type + let wrong_type_json = json!({ + "name": "John", + "age": "thirty" + }); + let wrong_type_object = Value::from(wrong_type_json); + let result = SchemaValidator::validate(&wrong_type_object, &schema); + assert!(result.is_err()); + if let Err(ValidationError::PropertyValidationFailed { + property, + path, + error: _, + }) = result + { + assert_eq!(property.as_ref(), "age"); + assert_eq!(path.as_ref(), ""); + } else { + panic!("Expected PropertyValidationFailed error"); + } +} + +#[test] +fn test_validate_enum() { + let schema_json = json!({ + "enum": ["red", "green", "blue"] + }); + let schema = Schema::from_serde_json_value(schema_json).unwrap(); + + // Valid enum value + assert!(SchemaValidator::validate(&Value::from("red"), &schema).is_ok()); + + // Invalid enum value + let result = SchemaValidator::validate(&Value::from("yellow"), &schema); + assert!(result.is_err()); + if let Err(ValidationError::NotInEnum { + value, + allowed_values, + path, + }) = result + { + assert_eq!(value.as_ref(), "\"yellow\""); + assert_eq!(allowed_values.len(), 3); + assert!(allowed_values.contains(&"\"red\"".into())); + assert!(allowed_values.contains(&"\"green\"".into())); + assert!(allowed_values.contains(&"\"blue\"".into())); + assert_eq!(path.as_ref(), ""); + } else { + panic!("Expected NotInEnum error"); + } +} + +#[test] +fn test_validate_const() { + let schema_json = json!({ + "const": "specific_value" + }); + let schema = Schema::from_serde_json_value(schema_json).unwrap(); + + // Valid const value + assert!(SchemaValidator::validate(&Value::from("specific_value"), &schema).is_ok()); + + // Invalid const value + let result = SchemaValidator::validate(&Value::from("other_value"), &schema); + assert!(result.is_err()); + if let Err(ValidationError::ConstMismatch { + expected, + actual, + path, + }) = result + { + assert_eq!(expected.as_ref(), "\"specific_value\""); + assert_eq!(actual.as_ref(), "\"other_value\""); + assert_eq!(path.as_ref(), ""); + } else { + panic!("Expected ConstMismatch error"); + } +} + +#[test] +fn test_validate_any_of() { + let schema_json = json!({ + "anyOf": [ + { "type": "string" }, + { "type": "integer", "minimum": 0 } + ] + }); + let schema = Schema::from_serde_json_value(schema_json).unwrap(); + + // Valid - matches string schema + assert!(SchemaValidator::validate(&Value::from("hello"), &schema).is_ok()); + + // Valid - matches integer schema + assert!(SchemaValidator::validate(&Value::from(42), &schema).is_ok()); + + // Invalid - matches neither schema + let result = SchemaValidator::validate(&Value::from(-1), &schema); + assert!(result.is_err()); + if let Err(ValidationError::NoUnionMatch { path, errors }) = result { + assert_eq!(path.as_ref(), ""); + assert_eq!(errors.len(), 2); // Two schemas in anyOf, both should fail + + // First error should be TypeMismatch (string schema failure) + if let ValidationError::TypeMismatch { + expected, actual, .. + } = &errors[0] + { + assert_eq!(expected.as_ref(), "string"); + assert_eq!(actual.as_ref(), "number"); + } else { + panic!("Expected first error to be TypeMismatch for string schema"); + } + + // Second error should be OutOfRange (integer schema failure - value below minimum) + if let ValidationError::OutOfRange { + value, min, max, .. + } = &errors[1] + { + assert_eq!(value.as_ref(), "-1"); + assert_eq!(min.as_ref().map(|s| s.as_ref()), Some("0")); + assert_eq!(max, &None); + } else { + panic!("Expected second error to be OutOfRange for integer schema"); + } + } else { + panic!("Expected NoUnionMatch error"); + } +} + +#[test] +fn test_validate_non_string_key() { + let schema_json = json!({ + "type": "object", + "properties": { + "name": { "type": "string" } + } + }); + let schema = Schema::from_serde_json_value(schema_json).unwrap(); + + // Create an object with a non-string key (number) + let mut invalid_obj = BTreeMap::new(); + invalid_obj.insert(Value::from(42), Value::from("value")); + let invalid_object = Value::Object(Rc::new(invalid_obj)); + + // This should fail because the key is not a string + let result = SchemaValidator::validate(&invalid_object, &schema); + assert!(result.is_err()); + + if let Err(ValidationError::NonStringKey { key_type, path }) = result { + assert_eq!(key_type.as_ref(), "number"); + assert_eq!(path.as_ref(), ""); + } else { + panic!("Expected NonStringKey error, got: {:?}", result); + } +} + +#[test] +fn test_validate_boolean() { + let schema_json = json!({ + "type": "boolean" + }); + let schema = Schema::from_serde_json_value(schema_json).unwrap(); + + // Valid boolean values + assert!(SchemaValidator::validate(&Value::from(true), &schema).is_ok()); + assert!(SchemaValidator::validate(&Value::from(false), &schema).is_ok()); + + // Invalid - not a boolean + let result = SchemaValidator::validate(&Value::from("true"), &schema); + assert!(result.is_err()); + if let Err(ValidationError::TypeMismatch { + expected, + actual, + path, + }) = result + { + assert_eq!(expected.as_ref(), "boolean"); + assert_eq!(actual.as_ref(), "string"); + assert_eq!(path.as_ref(), ""); + } else { + panic!("Expected TypeMismatch error"); + } + + let result = SchemaValidator::validate(&Value::from(1), &schema); + assert!(result.is_err()); + if let Err(ValidationError::TypeMismatch { + expected, + actual, + path, + }) = result + { + assert_eq!(expected.as_ref(), "boolean"); + assert_eq!(actual.as_ref(), "number"); + assert_eq!(path.as_ref(), ""); + } else { + panic!("Expected TypeMismatch error"); + } +} + +#[test] +fn test_validate_null() { + let schema_json = json!({ + "type": "null" + }); + let schema = Schema::from_serde_json_value(schema_json).unwrap(); + + // Valid null value + assert!(SchemaValidator::validate(&Value::Null, &schema).is_ok()); + + // Invalid - not null + let result = SchemaValidator::validate(&Value::from("null"), &schema); + assert!(result.is_err()); + if let Err(ValidationError::TypeMismatch { + expected, + actual, + path, + }) = result + { + assert_eq!(expected.as_ref(), "null"); + assert_eq!(actual.as_ref(), "string"); + assert_eq!(path.as_ref(), ""); + } else { + panic!("Expected TypeMismatch error"); + } + + let result = SchemaValidator::validate(&Value::from(0), &schema); + assert!(result.is_err()); + if let Err(ValidationError::TypeMismatch { + expected, + actual, + path, + }) = result + { + assert_eq!(expected.as_ref(), "null"); + assert_eq!(actual.as_ref(), "number"); + assert_eq!(path.as_ref(), ""); + } else { + panic!("Expected TypeMismatch error"); + } +} + +#[test] +fn test_validate_number() { + let schema_json = json!({ + "type": "number", + "minimum": 0.5, + "maximum": 99.7 + }); + let schema = Schema::from_serde_json_value(schema_json).unwrap(); + + // Valid numbers + assert!(SchemaValidator::validate(&Value::from(50.5), &schema).is_ok()); + assert!(SchemaValidator::validate(&Value::from(1), &schema).is_ok()); + assert!(SchemaValidator::validate(&Value::from(99), &schema).is_ok()); + + // Invalid - below minimum + let result = SchemaValidator::validate(&Value::from(0.3), &schema); + assert!(result.is_err()); + if let Err(ValidationError::OutOfRange { + value, + min, + max, + path, + }) = result + { + assert_eq!(value.as_ref(), "0.3"); + assert_eq!(min.as_ref().map(|s| s.as_ref()), Some("0.5")); + assert_eq!(max.as_ref().map(|s| s.as_ref()), Some("99.7")); + assert_eq!(path.as_ref(), ""); + } else { + panic!("Expected OutOfRange error"); + } + + // Invalid - above maximum + let result = SchemaValidator::validate(&Value::from(100.2), &schema); + assert!(result.is_err()); + if let Err(ValidationError::OutOfRange { + value, + min, + max, + path, + }) = result + { + assert_eq!(value.as_ref(), "100.2"); + assert_eq!(min.as_ref().map(|s| s.as_ref()), Some("0.5")); + assert_eq!(max.as_ref().map(|s| s.as_ref()), Some("99.7")); + assert_eq!(path.as_ref(), ""); + } else { + panic!("Expected OutOfRange error"); + } + + // Invalid - not a number + let result = SchemaValidator::validate(&Value::from("50.5"), &schema); + assert!(result.is_err()); + if let Err(ValidationError::TypeMismatch { + expected, + actual, + path, + }) = result + { + assert_eq!(expected.as_ref(), "number"); + assert_eq!(actual.as_ref(), "string"); + assert_eq!(path.as_ref(), ""); + } else { + panic!("Expected TypeMismatch error"); + } +} + +#[test] +fn test_validate_discriminated_subobject() { + // Test schema with discriminated subobjects (polymorphic objects) + let schema_json = json!({ + "type": "object", + "properties": { + "name": {"type": "string"}, + "kind": {"type": "string"} + }, + "required": ["name", "kind"], + "allOf": [ + { + "if": { "properties": { "kind": { "const": "user" } } }, + "then": { + "properties": { + "email": { "type": "string" }, + "age": { "type": "integer" } + }, + "required": ["email"] + } + }, + { + "if": { "properties": { "kind": { "const": "admin" } } }, + "then": { + "properties": { + "permissions": { "type": "array", "items": { "type": "string" } }, + "level": { "type": "integer" } + }, + "required": ["permissions"] + } + } + ] + }); + + let schema = Schema::from_serde_json_value(schema_json).unwrap(); + + // Test valid user object + let user_json = json!({ + "name": "John", + "kind": "user", + "email": "john@example.com", + "age": 30 + }); + let user_value = Value::from(user_json); + + let result = SchemaValidator::validate(&user_value, &schema); + match &result { + Ok(_) => {} + Err(e) => panic!("User validation failed: {:?}", e), + } + + // Test valid admin object + let admin_json = json!({ + "name": "Alice", + "kind": "admin", + "permissions": ["read", "write"], + "level": 5 + }); + let admin_value = Value::from(admin_json); + + let result = SchemaValidator::validate(&admin_value, &schema); + assert!(result.is_ok(), "Valid admin object should pass validation"); + + // Test object missing discriminator + let invalid_json = json!({ + "name": "Bob" + }); + let invalid_value = Value::from(invalid_json); + + let result = SchemaValidator::validate(&invalid_value, &schema); + assert!(result.is_err(), "Object missing discriminator should fail"); + if let Err(ValidationError::MissingRequiredProperty { property, .. }) = result { + assert_eq!(property.as_ref(), "kind"); + } else { + panic!("Expected MissingRequiredProperty error for discriminator"); + } + + // Test unknown discriminator value + let unknown_json = json!({ + "name": "Charlie", + "kind": "guest" + }); + let unknown_value = Value::from(unknown_json); + + let result = SchemaValidator::validate(&unknown_value, &schema); + assert!(result.is_err(), "Unknown discriminator value should fail"); + if let Err(ValidationError::UnknownDiscriminatorValue { + discriminator, + value, + allowed_values, + .. + }) = result + { + assert_eq!(discriminator.as_ref(), "kind"); + assert_eq!(value.as_ref(), "guest"); + assert_eq!(allowed_values.len(), 2); + assert!(allowed_values.contains(&"user".into())); + assert!(allowed_values.contains(&"admin".into())); + } else { + panic!("Expected UnknownDiscriminatorValue error"); + } + + // Test user object missing required field from variant + let incomplete_json = json!({ + "name": "Dave", + "kind": "user" + // Missing required email field + }); + let incomplete_value = Value::from(incomplete_json); + + let result = SchemaValidator::validate(&incomplete_value, &schema); + assert!(result.is_err(), "User object missing email should fail"); + if let Err(ValidationError::DiscriminatedSubobjectValidationFailed { + discriminator, + value, + error, + .. + }) = result + { + assert_eq!(discriminator.as_ref(), "kind"); + assert_eq!(value.as_ref(), "user"); + if let ValidationError::MissingRequiredProperty { property, .. } = error.as_ref() { + assert_eq!(property.as_ref(), "email"); + } else { + panic!("Expected MissingRequiredProperty error in discriminated subobject validation"); + } + } else { + panic!("Expected DiscriminatedSubobjectValidationFailed error"); + } + + // Test admin object with invalid permission type + let invalid_admin_json = json!({ + "name": "Eve", + "kind": "admin", + "permissions": "not_an_array" // Wrong type - should be array + }); + let invalid_admin_value = Value::from(invalid_admin_json); + + let result = SchemaValidator::validate(&invalid_admin_value, &schema); + assert!( + result.is_err(), + "Admin object with invalid permissions type should fail" + ); + + if let Err(ValidationError::DiscriminatedSubobjectValidationFailed { + discriminator, + value, + error, + .. + }) = result + { + assert_eq!(discriminator.as_ref(), "kind"); + assert_eq!(value.as_ref(), "admin"); + if let ValidationError::PropertyValidationFailed { + property, + error: nested_error, + .. + } = error.as_ref() + { + assert_eq!(property.as_ref(), "permissions"); + if let ValidationError::TypeMismatch { + expected, actual, .. + } = nested_error.as_ref() + { + assert_eq!(expected.as_ref(), "array"); + assert_eq!(actual.as_ref(), "string"); + } else { + panic!("Expected TypeMismatch error for permissions field"); + } + } else { + panic!("Expected PropertyValidationFailed error in discriminated subobject validation"); + } + } else { + panic!("Expected DiscriminatedSubobjectValidationFailed error"); + } +} + +#[test] +fn test_discriminated_subobject_complex_nested() { + // Test deeply nested discriminated subobjects with multiple levels + let schema_json = json!({ + "type": "object", + "properties": { + "type": {"type": "string"} + }, + "required": ["type"], + "allOf": [ + { + "if": { "properties": { "type": { "const": "container" } } }, + "then": { + "properties": { + "image": { "type": "string", "pattern": "^[a-zA-Z0-9/_-]+:[a-zA-Z0-9._-]+$" }, + "ports": { + "type": "array", + "items": { + "type": "object", + "properties": { + "containerPort": { "type": "integer", "minimum": 1, "maximum": 65535 }, + "protocol": { "enum": ["TCP", "UDP"] } + }, + "required": ["containerPort"] + } + }, + "env": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { "type": "string", "pattern": "^[A-Z_][A-Z0-9_]*$" }, + "value": { "type": "string" } + }, + "required": ["name", "value"] + } + } + }, + "required": ["image"] + } + }, + { + "if": { "properties": { "type": { "const": "volume" } } }, + "then": { + "properties": { + "mountPath": { "type": "string", "pattern": "^/[a-zA-Z0-9/_-]*$" }, + "size": { "type": "string", "pattern": "^[0-9]+[GMK]i?$" }, + "accessModes": { + "type": "array", + "items": { "enum": ["ReadWriteOnce", "ReadOnlyMany", "ReadWriteMany"] }, + "minItems": 1 + } + }, + "required": ["mountPath", "size", "accessModes"] + } + } + ] + }); + + let schema = Schema::from_serde_json_value(schema_json).unwrap(); + + // Test valid container with complex nested structure + let valid_container = json!({ + "type": "container", + "image": "nginx:1.21.6", + "ports": [ + { + "containerPort": 80, + "protocol": "TCP" + }, + { + "containerPort": 443, + "protocol": "TCP" + } + ], + "env": [ + { + "name": "NGINX_HOST", + "value": "example.com" + }, + { + "name": "NGINX_PORT", + "value": "80" + } + ] + }); + + let result = SchemaValidator::validate(&Value::from(valid_container), &schema); + match &result { + Ok(_) => {} + Err(e) => panic!("Valid container should pass validation. Error: {:?}", e), + } + + // Test invalid container with malformed image + let invalid_container_image = json!({ + "type": "container", + "image": "invalid image name!", + "ports": [] + }); + + let result = SchemaValidator::validate(&Value::from(invalid_container_image), &schema); + assert!(result.is_err(), "Container with invalid image should fail"); + + // Test invalid container with out-of-range port + let invalid_container_port = json!({ + "type": "container", + "image": "nginx:latest", + "ports": [ + { + "containerPort": 70000, + "protocol": "TCP" + } + ] + }); + + let result = SchemaValidator::validate(&Value::from(invalid_container_port), &schema); + assert!(result.is_err(), "Container with invalid port should fail"); + + // Test valid volume + let valid_volume = json!({ + "type": "volume", + "mountPath": "/data/storage", + "size": "10Gi", + "accessModes": ["ReadWriteOnce"] + }); + + let result = SchemaValidator::validate(&Value::from(valid_volume), &schema); + assert!(result.is_ok(), "Valid volume should pass validation"); + + // Test invalid volume with malformed mount path + let invalid_volume_path = json!({ + "type": "volume", + "mountPath": "invalid-path", + "size": "10Gi", + "accessModes": ["ReadWriteOnce"] + }); + + let result = SchemaValidator::validate(&Value::from(invalid_volume_path), &schema); + assert!( + result.is_err(), + "Volume with invalid mount path should fail" + ); +} + +#[test] +fn test_discriminated_subobject_additional_properties() { + // Test discriminated subobjects with additional properties handling + let schema_json = json!({ + "type": "object", + "properties": { + "kind": {"type": "string"}, + "name": {"type": "string"} + }, + "required": ["kind", "name"], + "allOf": [ + { + "if": { "properties": { "kind": { "const": "service" } } }, + "then": { + "properties": { + "port": { "type": "integer", "minimum": 1, "maximum": 65535 }, + "protocol": { "enum": ["HTTP", "HTTPS", "TCP", "UDP"] } + }, + "required": ["port"] + } + }, + { + "if": { "properties": { "kind": { "const": "database" } } }, + "then": { + "properties": { + "connectionString": { "type": "string" }, + "maxConnections": { "type": "integer", "minimum": 1 } + }, + "required": ["connectionString"] + } + } + ] + }); + + let schema = Schema::from_serde_json_value(schema_json).unwrap(); + + // Test service with required properties + let service_basic = json!({ + "kind": "service", + "name": "web-service", + "port": 8080, + "protocol": "HTTP" + }); + + let result = SchemaValidator::validate(&Value::from(service_basic), &schema); + match &result { + Ok(_) => {} + Err(e) => panic!("Service with valid properties should pass. Error: {:?}", e), + } + + // Test service with invalid port + let service_invalid_port = json!({ + "kind": "service", + "name": "web-service", + "port": 70000 // Out of range + }); + + let result = SchemaValidator::validate(&Value::from(service_invalid_port), &schema); + assert!(result.is_err(), "Service with invalid port should fail"); + + // Test database with valid properties + let database_basic = json!({ + "kind": "database", + "name": "main-db", + "connectionString": "postgresql://localhost:5432/mydb", + "maxConnections": 100 + }); + + let result = SchemaValidator::validate(&Value::from(database_basic), &schema); + assert!(result.is_ok(), "Database with valid properties should pass"); + + // Test unknown discriminator should fail + let unknown_kind = json!({ + "kind": "unknown", + "name": "test" + }); + + let result = SchemaValidator::validate(&Value::from(unknown_kind), &schema); + assert!(result.is_err(), "Unknown discriminator should fail"); +} + +#[test] +fn test_discriminated_subobject_edge_cases() { + // Test various edge cases and corner cases + let schema_json = json!({ + "type": "object", + "properties": { + "discriminator": {"type": "string"}, + "id": {"type": "integer"} + }, + "required": ["discriminator"], + "allOf": [ + { + "if": { "properties": { "discriminator": { "const": "empty" } } }, + "then": { + "properties": {} + } + }, + { + "if": { "properties": { "discriminator": { "const": "minimal" } } }, + "then": { + "properties": { + "value": { "type": "null" } + }, + "required": ["value"] + } + }, + { + "if": { "properties": { "discriminator": { "const": "recursive" } } }, + "then": { + "properties": { + "child": { + "type": "object", + "properties": { + "discriminator": {"type": "string"} + }, + "required": ["discriminator"] + } + }, + "required": ["child"] + } + } + ] + }); + + let schema = Schema::from_serde_json_value(schema_json).unwrap(); + + // Test empty variant - minimal properties + let empty_valid = json!({ + "discriminator": "empty", + "id": 123 + }); + + let result = SchemaValidator::validate(&Value::from(empty_valid), &schema); + assert!( + result.is_ok(), + "Empty variant with base properties should pass" + ); + + // Test minimal variant with null value + let minimal_valid = json!({ + "discriminator": "minimal", + "id": 456, + "value": null + }); + + let result = SchemaValidator::validate(&Value::from(minimal_valid), &schema); + assert!( + result.is_ok(), + "Minimal variant with null value should pass" + ); + + let minimal_invalid = json!({ + "discriminator": "minimal", + "id": 456, + "value": "not null" + }); + + let result = SchemaValidator::validate(&Value::from(minimal_invalid), &schema); + assert!( + result.is_err(), + "Minimal variant with non-null value should fail" + ); + + // Test recursive/nested structure + let recursive_valid = json!({ + "discriminator": "recursive", + "id": 789, + "child": { + "discriminator": "nested" + } + }); + + let result = SchemaValidator::validate(&Value::from(recursive_valid), &schema); + match &result { + Ok(_) => {} + Err(e) => panic!( + "Recursive variant with valid nested object should pass. Error: {:?}", + e + ), + } + + let recursive_invalid = json!({ + "discriminator": "recursive", + "id": 789, + "child": { + "missingDiscriminator": true + } + }); + + let result = SchemaValidator::validate(&Value::from(recursive_invalid), &schema); + assert!( + result.is_err(), + "Recursive variant with invalid nested object should fail" + ); +} + +#[test] +fn test_discriminated_subobject_unicode_and_special_chars() { + // Test discriminated subobjects with Unicode and special characters + let schema_json = json!({ + "type": "object", + "properties": { + "类型": {"type": "string"}, // Chinese characters + "🎯": {"type": "string"} // Emoji + }, + "required": ["类型"], + "allOf": [ + { + "if": { "properties": { "类型": { "const": "用户" } } }, // Chinese "user" + "then": { + "properties": { + "姓名": { "type": "string" }, // Chinese "name" + "年龄": { "type": "integer", "minimum": 0 } // Chinese "age" + }, + "required": ["姓名"] + } + }, + { + "if": { "properties": { "类型": { "const": "管理员" } } }, // Chinese "admin" + "then": { + "properties": { + "权限": { // Chinese "permissions" + "type": "array", + "items": { "type": "string" } + }, + "级别": { "type": "integer" } // Chinese "level" + }, + "required": ["权限"] + } + } + ] + }); + + let schema = Schema::from_serde_json_value(schema_json).unwrap(); + + // Test valid user with Chinese properties + let valid_user = json!({ + "类型": "用户", + "姓名": "张三", + "年龄": 25, + "🎯": "target" + }); + + let result = SchemaValidator::validate(&Value::from(valid_user), &schema); + assert!( + result.is_ok(), + "Valid user with Unicode properties should pass" + ); + + // Test valid admin with Chinese properties + let valid_admin = json!({ + "类型": "管理员", + "权限": ["读取", "写入", "删除"], + "级别": 5 + }); + + let result = SchemaValidator::validate(&Value::from(valid_admin), &schema); + assert!( + result.is_ok(), + "Valid admin with Unicode properties should pass" + ); + + // Test invalid discriminator value + let invalid_discriminator = json!({ + "类型": "访客", // Chinese "guest" - not in allowed values + "姓名": "李四" + }); + + let result = SchemaValidator::validate(&Value::from(invalid_discriminator), &schema); + assert!(result.is_err(), "Invalid Unicode discriminator should fail"); +} + +#[test] +fn test_discriminated_subobject_performance_stress() { + // Test with many variants and deep nesting for performance + let mut variants = Vec::new(); + + // Create 50 variants to stress test performance + for i in 0..50 { + variants.push(json!({ + "if": { "properties": { "type": { "const": format!("variant_{}", i) } } }, + "then": { + "properties": { + "data": { + "type": "object", + "properties": { + "level1": { + "type": "object", + "properties": { + "level2": { + "type": "object", + "properties": { + "level3": { + "type": "object", + "properties": { + "value": { "type": "integer" } + }, + "required": ["value"] + } + }, + "required": ["level3"] + } + }, + "required": ["level2"] + } + }, + "required": ["level1"] + } + }, + "required": ["data"] + } + })); + } + + let schema_json = json!({ + "type": "object", + "properties": { + "type": {"type": "string"} + }, + "required": ["type"], + "allOf": variants + }); + + let schema = Schema::from_serde_json_value(schema_json).unwrap(); + + // Test validation with various variants + for i in [0, 25, 49] { + // Test first, middle, and last variants + let test_data = json!({ + "type": format!("variant_{}", i), + "data": { + "level1": { + "level2": { + "level3": { + "value": i * 10 + } + } + } + } + }); + + let result = SchemaValidator::validate(&Value::from(test_data), &schema); + assert!(result.is_ok(), "Variant {} should pass validation", i); + } + + // Test invalid variant + let invalid_data = json!({ + "type": "variant_100", // Non-existent variant + "data": {} + }); + + let result = SchemaValidator::validate(&Value::from(invalid_data), &schema); + assert!(result.is_err(), "Non-existent variant should fail"); + + // Test valid variant with invalid nested structure + let invalid_nested = json!({ + "type": "variant_10", + "data": { + "level1": { + "level2": { + "level3": { + "value": "not an integer" // Should be integer + } + } + } + } + }); + + let result = SchemaValidator::validate(&Value::from(invalid_nested), &schema); + assert!( + result.is_err(), + "Valid variant with invalid nested data should fail" + ); +} + +#[test] +fn test_discriminated_subobject_conflict_resolution() { + // Test property conflicts between base and variant schemas + let schema_json = json!({ + "type": "object", + "properties": { + "type": {"type": "string"}, + "name": {"type": "string", "maxLength": 50}, // Base constraint + "description": {"type": "string"} + }, + "required": ["type", "name"], + "allOf": [ + { + "if": { "properties": { "type": { "const": "product" } } }, + "then": { + "properties": { + "name": {"type": "string", "maxLength": 20}, // Stricter constraint in variant + "price": {"type": "number", "minimum": 0}, + "category": {"type": "string"} + }, + "required": ["price"] + } + }, + { + "if": { "properties": { "type": { "const": "person" } } }, + "then": { + "properties": { + "name": {"type": "string", "pattern": "^[A-Za-z ]+$"}, // Different constraint in variant + "age": {"type": "integer", "minimum": 0, "maximum": 150}, + "email": {"type": "string"} + }, + "required": ["age"] + } + } + ] + }); + + let schema = Schema::from_serde_json_value(schema_json).unwrap(); + + // Test product with name that satisfies variant constraint (stricter) + let valid_product = json!({ + "type": "product", + "name": "Short Name", // ≤20 chars, satisfies variant constraint + "description": "A great product", + "price": 29.99, + "category": "electronics" + }); + + let result = SchemaValidator::validate(&Value::from(valid_product), &schema); + assert!(result.is_ok(), "Product with short name should pass"); + + // Test product with name that violates variant constraint but satisfies base + let invalid_product_name = json!({ + "type": "product", + "name": "This is a very long product name that exceeds 20 characters", // >20 chars + "price": 29.99 + }); + + let result = SchemaValidator::validate(&Value::from(invalid_product_name), &schema); + assert!( + result.is_err(), + "Product with long name should fail variant validation" + ); + + // Test person with valid name pattern + let valid_person = json!({ + "type": "person", + "name": "John Doe", // Matches pattern ^[A-Za-z ]+$ + "age": 30, + "email": "john@example.com" + }); + + let result = SchemaValidator::validate(&Value::from(valid_person), &schema); + assert!(result.is_ok(), "Person with valid name pattern should pass"); + + // Test person with invalid name pattern + let invalid_person_name = json!({ + "type": "person", + "name": "John123", // Contains numbers, violates pattern + "age": 30 + }); + + let result = SchemaValidator::validate(&Value::from(invalid_person_name), &schema); + assert!( + result.is_err(), + "Person with invalid name pattern should fail" + ); +} diff --git a/src/schema/tests/validate/effect.rs b/src/schema/tests/validate/effect.rs new file mode 100644 index 0000000..c6bb4b8 --- /dev/null +++ b/src/schema/tests/validate/effect.rs @@ -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 { + 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 { + 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 { + 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 { + 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), + } +} diff --git a/src/schema/tests/validate/resource.rs b/src/schema/tests/validate/resource.rs new file mode 100644 index 0000000..07bc8a6 --- /dev/null +++ b/src/schema/tests/validate/resource.rs @@ -0,0 +1,1328 @@ +// 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 Resource types +fn create_resource_schema() -> Rc { + 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 { + 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 { + 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 { + 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) +} + +// Schema validation tests for Azure Resource schemas + +#[test] +fn test_validate_vm_resource_valid() { + let schema = create_vm_resource_schema(); + + let valid_vm_data = 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" + } + } + }); + + let value = Value::from(valid_vm_data); + let result = SchemaValidator::validate(&value, &schema); + assert!(result.is_ok()); +} + +#[test] +fn test_validate_vm_resource_missing_required() { + let schema = create_vm_resource_schema(); + + let invalid_vm_data = json!({ + "type": "Microsoft.Compute/virtualMachines", + "apiVersion": "2021-03-01", + "name": "my-vm-01" + // Missing location and properties + }); + + let value = Value::from(invalid_vm_data); + let result = SchemaValidator::validate(&value, &schema); + assert!(result.is_err()); + + match result.unwrap_err() { + ValidationError::MissingRequiredProperty { property, .. } => { + // Should be missing either location or properties + assert!(property == "location".into() || property == "properties".into()); + } + other => panic!("Expected MissingRequiredProperty error, got: {:?}", other), + } +} + +#[test] +fn test_validate_vm_resource_invalid_vm_size() { + let schema = create_vm_resource_schema(); + + let invalid_vm_data = json!({ + "type": "Microsoft.Compute/virtualMachines", + "apiVersion": "2021-03-01", + "name": "my-vm-01", + "location": "eastus", + "properties": { + "hardwareProfile": { + "vmSize": "InvalidSize" + }, + "osProfile": { + "computerName": "my-computer", + "adminUsername": "azureuser" + } + } + }); + + let value = Value::from(invalid_vm_data); + let result = SchemaValidator::validate(&value, &schema); + assert!(result.is_err()); + + match result.unwrap_err() { + ValidationError::PropertyValidationFailed { + property, error, .. + } => { + assert_eq!(property, "properties".into()); + match error.as_ref() { + ValidationError::PropertyValidationFailed { + property: inner_prop, + error: inner_error, + .. + } => { + assert_eq!(*inner_prop, "hardwareProfile".into()); + match inner_error.as_ref() { + ValidationError::PropertyValidationFailed { + property: vm_size_prop, + error: vm_size_error, + .. + } => { + assert_eq!(*vm_size_prop, "vmSize".into()); + match vm_size_error.as_ref() { + ValidationError::NotInEnum { .. } => { + // Expected deeply nested error structure + } + other => { + panic!("Expected NotInEnum error for vmSize, got: {:?}", other) + } + } + } + other => panic!( + "Expected PropertyValidationFailed for vmSize, got: {:?}", + other + ), + } + } + other => panic!( + "Expected PropertyValidationFailed for hardwareProfile, got: {:?}", + other + ), + } + } + other => panic!("Expected PropertyValidationFailed error, got: {:?}", other), + } +} + +#[test] +fn test_validate_storage_resource_valid() { + let schema = create_storage_resource_schema(); + + let valid_storage_data = json!({ + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2021-04-01", + "name": "mystorageaccount001", + "location": "westus2", + "sku": { + "name": "Standard_LRS" + }, + "kind": "StorageV2", + "properties": { + "accessTier": "Hot", + "encryption": { + "services": {} + } + } + }); + + let value = Value::from(valid_storage_data); + let result = SchemaValidator::validate(&value, &schema); + assert!(result.is_ok()); +} + +#[test] +fn test_validate_storage_resource_invalid_name() { + let schema = create_storage_resource_schema(); + + let invalid_storage_data = json!({ + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2021-04-01", + "name": "Invalid-Storage-Name-With-Caps-And-Dashes", + "location": "westus2", + "sku": { + "name": "Standard_LRS" + }, + "kind": "StorageV2" + }); + + let value = Value::from(invalid_storage_data); + let result = SchemaValidator::validate(&value, &schema); + assert!(result.is_err()); + + match result.unwrap_err() { + ValidationError::PropertyValidationFailed { + property, error, .. + } => { + assert_eq!(property, "name".into()); + match error.as_ref() { + ValidationError::PatternMismatch { .. } => { + // Expected pattern mismatch for storage account name + } + other => panic!("Expected PatternMismatch error for name, got: {:?}", other), + } + } + other => panic!("Expected PropertyValidationFailed error, got: {:?}", other), + } +} + +#[test] +fn test_validate_storage_resource_invalid_sku() { + let schema = create_storage_resource_schema(); + + let invalid_storage_data = json!({ + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2021-04-01", + "name": "mystorageaccount001", + "location": "westus2", + "sku": { + "name": "Invalid_SKU" + }, + "kind": "StorageV2" + }); + + let value = Value::from(invalid_storage_data); + let result = SchemaValidator::validate(&value, &schema); + assert!(result.is_err()); + + match result.unwrap_err() { + ValidationError::PropertyValidationFailed { + property, error, .. + } => { + assert_eq!(property, "sku".into()); + match error.as_ref() { + ValidationError::PropertyValidationFailed { + property: sku_prop, + error: sku_error, + .. + } => { + assert_eq!(*sku_prop, "name".into()); + match sku_error.as_ref() { + ValidationError::NotInEnum { .. } => { + // Expected enum validation error + } + other => panic!("Expected NotInEnum error for sku name, got: {:?}", other), + } + } + other => panic!( + "Expected PropertyValidationFailed for sku name, got: {:?}", + other + ), + } + } + other => panic!("Expected PropertyValidationFailed error, got: {:?}", other), + } +} + +#[test] +fn test_validate_network_resource_valid() { + let schema = create_network_resource_schema(); + + let valid_network_data = json!({ + "type": "Microsoft.Network/virtualNetworks", + "apiVersion": "2021-02-01", + "name": "my-vnet", + "location": "eastus", + "properties": { + "addressSpace": { + "addressPrefixes": ["10.0.0.0/16"] + }, + "subnets": [ + { + "name": "default", + "properties": { + "addressPrefix": "10.0.1.0/24" + } + } + ] + } + }); + + let value = Value::from(valid_network_data); + let result = SchemaValidator::validate(&value, &schema); + assert!(result.is_ok()); +} + +#[test] +fn test_validate_network_resource_invalid_address_prefix() { + let schema = create_network_resource_schema(); + + let invalid_network_data = json!({ + "type": "Microsoft.Network/virtualNetworks", + "apiVersion": "2021-02-01", + "name": "my-vnet", + "location": "eastus", + "properties": { + "addressSpace": { + "addressPrefixes": ["invalid-cidr"] + } + } + }); + + let value = Value::from(invalid_network_data); + let result = SchemaValidator::validate(&value, &schema); + assert!(result.is_err()); + + match result.unwrap_err() { + ValidationError::PropertyValidationFailed { + property, error, .. + } => { + assert_eq!(property, "properties".into()); + match error.as_ref() { + ValidationError::PropertyValidationFailed { + property: addr_prop, + .. + } => { + assert_eq!(*addr_prop, "addressSpace".into()); + // Continue checking nested structure for array validation + } + other => panic!( + "Expected PropertyValidationFailed for addressSpace, got: {:?}", + other + ), + } + } + other => panic!("Expected PropertyValidationFailed error, got: {:?}", other), + } +} + +#[test] +fn test_validate_basic_resource_enum() { + let schema = create_resource_schema(); + + // Test all valid resource types + let valid_types = [ + "Microsoft.Compute/virtualMachines", + "Microsoft.Storage/storageAccounts", + "Microsoft.Network/virtualNetworks", + ]; + + for resource_type in valid_types { + let value = Value::from(resource_type); + let result = SchemaValidator::validate(&value, &schema); + assert!( + result.is_ok(), + "Resource type '{resource_type}' should be valid" + ); + } + + // Test invalid resource type + let invalid_value = Value::from("Microsoft.Invalid/resourceType"); + 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_arm_template() { + let complex_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(complex_schema_json).unwrap(); + + let valid_template_data = 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'))]" + } + } + }); + + let value = Value::from(valid_template_data); + let result = SchemaValidator::validate(&value, &schema); + assert!(result.is_ok()); +} + +#[test] +fn test_validate_resource_type_mismatch() { + let schema = create_vm_resource_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), + } +} + +#[test] +fn test_complex_nested_azure_template_validation() { + // Create a complex ARM template schema with deeply nested properties + let complex_schema = json!({ + "type": "object", + "properties": { + "$schema": { "type": "string" }, + "contentVersion": { "type": "string", "pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+\\.[0-9]+$" }, + "metadata": { + "type": "object", + "properties": { + "description": { "type": "string" }, + "author": { "type": "string" }, + "tags": { + "type": "object", + "additionalProperties": { "type": "string" } + } + } + }, + "parameters": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "type": { "enum": ["string", "int", "bool", "array", "object"] }, + "defaultValue": { "type": "any" }, + "allowedValues": { "type": "array", "items": { "type": "any" } }, + "minValue": { "type": "number" }, + "maxValue": { "type": "number" }, + "minLength": { "type": "integer" }, + "maxLength": { "type": "integer" }, + "metadata": { + "type": "object", + "properties": { + "description": { "type": "string" }, + "strongType": { "type": "string" } + } + } + }, + "required": ["type"] + } + }, + "variables": { "type": "object" }, + "resources": { + "type": "array", + "items": { + "type": "object", + "properties": { + "type": { "type": "string" }, + "apiVersion": { "type": "string" }, + "name": { "type": "string" }, + "location": { "type": "string" }, + "dependsOn": { + "type": "array", + "items": { "type": "string" } + }, + "condition": { "type": "boolean" }, + "copy": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "count": { "type": "integer", "minimum": 1, "maximum": 800 }, + "mode": { "enum": ["Parallel", "Serial"] }, + "batchSize": { "type": "integer", "minimum": 1 } + }, + "required": ["name", "count"] + }, + "properties": { "type": "object" }, + "tags": { + "type": "object", + "additionalProperties": { "type": "string" } + } + }, + "required": ["type", "apiVersion", "name"] + } + }, + "outputs": { + "type": "object", + "additionalProperties": { + "type": "object", + "properties": { + "type": { "enum": ["string", "int", "bool", "array", "object"] }, + "value": { "type": "any" }, + "condition": { "type": "boolean" }, + "copy": { + "type": "object", + "properties": { + "count": { "type": "integer", "minimum": 1 }, + "input": { "type": "any" } + }, + "required": ["count", "input"] + } + }, + "required": ["type", "value"] + } + } + }, + "required": ["$schema", "contentVersion", "resources"] + }); + + let schema = Schema::from_serde_json_value(complex_schema).unwrap(); + + // Valid complex template with nested copy loops and conditions + let valid_template = json!({ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.2.3.4", + "metadata": { + "description": "Complex deployment with copy loops and conditions", + "author": "Azure DevOps Team", + "tags": { + "environment": "production", + "cost-center": "engineering", + "department": "cloud-infrastructure" + } + }, + "parameters": { + "vmCount": { + "type": "int", + "defaultValue": 3, + "minValue": 1, + "maxValue": 10, + "metadata": { + "description": "Number of VMs to deploy", + "strongType": "Microsoft.Compute/SKUs" + } + }, + "environment": { + "type": "string", + "defaultValue": "dev", + "allowedValues": ["dev", "test", "staging", "prod"], + "metadata": { + "description": "Environment name for resource naming" + } + }, + "enableMonitoring": { + "type": "bool", + "defaultValue": true, + "metadata": { + "description": "Whether to enable monitoring extensions" + } + } + }, + "variables": { + "vmPrefix": "[concat(parameters('environment'), '-vm-')]", + "storageAccountName": "[concat('storage', uniqueString(resourceGroup().id))]" + }, + "resources": [ + { + "type": "Microsoft.Storage/storageAccounts", + "apiVersion": "2021-04-01", + "name": "[variables('storageAccountName')]", + "location": "eastus", + "properties": { + "accountType": "Standard_LRS" + }, + "tags": { + "purpose": "vm-diagnostics", + "environment": "[parameters('environment')]" + } + }, + { + "type": "Microsoft.Compute/virtualMachines", + "apiVersion": "2021-03-01", + "name": "[concat(variables('vmPrefix'), copyIndex(1))]", + "location": "eastus", + "condition": true, + "dependsOn": [ + "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]" + ], + "copy": { + "name": "vmLoop", + "count": 5, + "mode": "Parallel", + "batchSize": 2 + }, + "properties": { + "hardwareProfile": { + "vmSize": "Standard_B2s" + } + }, + "tags": { + "environment": "[parameters('environment')]", + "vm-index": "[string(copyIndex())]" + } + } + ], + "outputs": { + "storageAccountId": { + "type": "string", + "value": "[resourceId('Microsoft.Storage/storageAccounts', variables('storageAccountName'))]" + }, + "vmIds": { + "type": "array", + "copy": { + "count": 5, + "input": "[resourceId('Microsoft.Compute/virtualMachines', concat(variables('vmPrefix'), copyIndex(1)))]" + }, + "value" : "[resourceId('Microsoft.Compute/virtualMachines', concat(variables('vmPrefix'), copyIndex(1)))]" + } + } + }); + + let value = Value::from(valid_template); + let result = SchemaValidator::validate(&value, &schema); + std::dbg!(&result); + assert!( + result.is_ok(), + "Complex valid template should pass validation" + ); + + // Test invalid template with constraint violations + let invalid_template = json!({ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "invalid-version", // Should match pattern + "resources": [ + { + "type": "Microsoft.Compute/virtualMachines", + "apiVersion": "2021-03-01", + "name": "test-vm", + "copy": { + "name": "vmLoop", + "count": 1000 // Exceeds maximum of 800 + } + } + ] + }); + + let value = Value::from(invalid_template); + let result = SchemaValidator::validate(&value, &schema); + assert!( + result.is_err(), + "Template with constraint violations should fail" + ); +} + +#[test] +fn test_deep_nesting_and_recursive_structures() { + // Schema with moderate nesting (5 levels) to avoid macro recursion limits + let deep_schema = json!({ + "type": "object", + "properties": { + "level1": { + "type": "object", + "properties": { + "level2": { + "type": "object", + "properties": { + "level3": { + "type": "object", + "properties": { + "level4": { + "type": "object", + "properties": { + "level5": { + "type": "object", + "properties": { + "deepValue": { + "type": "string", + "pattern": "^deep-[0-9]+$" + }, + "recursiveArray": { + "type": "array", + "items": { + "type": "object", + "properties": { + "nested": { + "type": "object", + "properties": { + "value": { "type": "number" }, + "metadata": { + "type": "object", + "additionalProperties": { "type": "string" } + } + } + } + } + } + } + }, + "required": ["deepValue"] + } + } + } + } + } + } + } + } + } + }, + "required": ["level1"] + }); + + let schema = Schema::from_serde_json_value(deep_schema).unwrap(); + + // Valid deeply nested structure + let valid_deep_data = json!({ + "level1": { + "level2": { + "level3": { + "level4": { + "level5": { + "deepValue": "deep-12345", + "recursiveArray": [ + { + "nested": { + "value": 42.5, + "metadata": { + "type": "numeric", + "unit": "percentage", + "source": "sensor-1" + } + } + }, + { + "nested": { + "value": 15.3, + "metadata": { + "type": "numeric", + "unit": "temperature", + "source": "sensor-2" + } + } + } + ] + } + } + } + } + } + }); + + let value = Value::from(valid_deep_data); + let result = SchemaValidator::validate(&value, &schema); + assert!(result.is_ok(), "Valid deeply nested structure should pass"); + + // Invalid - missing required deepValue + let invalid_deep_data = json!({ + "level1": { + "level2": { + "level3": { + "level4": { + "level5": { + // Missing required "deepValue" + "recursiveArray": [] + } + } + } + } + } + }); + + let value = Value::from(invalid_deep_data); + let result = SchemaValidator::validate(&value, &schema); + assert!( + result.is_err(), + "Structure missing required deep field should fail" + ); +} + +#[test] +fn test_unicode_and_internationalization_validation() { + // Schema supporting international characters and Unicode + let unicode_schema = json!({ + "type": "object", + "properties": { + "names": { + "type": "object", + "properties": { + "chinese": { "type": "string", "pattern": "^[\\u4e00-\\u9fff]+$" }, + "russian": { "type": "string", "pattern": "^[\\u0400-\\u04ff]+$" }, + "japanese": { "type": "string", "pattern": "^[\\u3040-\\u309f\\u30a0-\\u30ff\\u4e00-\\u9fff]+$" }, + "korean": { "type": "string", "pattern": "^[\\uac00-\\ud7af]+$" }, + "hindi": { "type": "string", "pattern": "^[\\u0900-\\u097f]+$" }, + "french": { "type": "string", "pattern": "^[a-zA-ZàâäéèêëïîôöùûüÿñæœÀÂÄÉÈÊËÏÎÔÖÙÛÜŸÑÆŒ\\s-]+$" } + } + }, + "descriptions": { + "type": "object", + "additionalProperties": { + "type": "string", + "minLength": 1, + "maxLength": 1000 + } + }, + "metadata": { + "type": "object", + "properties": { + "encoding": { "enum": ["UTF-8", "UTF-16", "UTF-32"] }, + "locale": { "type": "string", "pattern": "^[a-z]{2}-[A-Z]{2}$" }, + "timezone": { "type": "string" } + } + } + }, + "required": ["names", "metadata"] + }); + + let schema = Schema::from_serde_json_value(unicode_schema).unwrap(); + + // Valid international data + let valid_unicode_data = json!({ + "names": { + "chinese": "你好世界", + "russian": "привет", + "japanese": "こんにちは世界", + "korean": "안녕하세요", + "hindi": "नमस्ते", + "french": "Bonjour le Monde" + }, + "descriptions": { + "en-US": "Hello World application for international users", + "zh-CN": "面向国际用户的你好世界应用程序", + "ru-RU": "Приложение Hello World для международных пользователей", + "ja-JP": "国際ユーザー向けのHello Worldアプリケーション", + "ko-KR": "국제 사용자를 위한 Hello World 애플리케이션", + "hi-IN": "अंतर्राष्ट्रीय उपयोगकर्ताओं के लिए हैलो वर्ल्ड एप्लिकेशन", + "fr-FR": "Application Hello World pour les utilisateurs internationaux" + }, + "metadata": { + "encoding": "UTF-8", + "locale": "en-US", + "timezone": "UTC" + } + }); + + let value = Value::from(valid_unicode_data); + let result = SchemaValidator::validate(&value, &schema); + assert!(result.is_ok(), "Valid Unicode data should pass validation"); + + // Invalid - non-matching Unicode patterns + let invalid_unicode_data = json!({ + "names": { + "chinese": "Hello", // Should be Chinese characters + "russian": "Goodbye", // Should be Russian characters + "japanese": "Test", // Should be Japanese characters + "korean": "Invalid", // Should be Korean characters + "hindi": "Wrong", // Should be Hindi characters + "french": "123456" // Should be French text + }, + "metadata": { + "encoding": "UTF-8", + "locale": "invalid-locale", // Should match pattern + "timezone": "UTC" + } + }); + + let value = Value::from(invalid_unicode_data); + let result = SchemaValidator::validate(&value, &schema); + assert!( + result.is_err(), + "Invalid Unicode patterns should fail validation" + ); +} + +#[test] +fn test_edge_cases_and_boundary_conditions() { + // Schema with strict boundary conditions + let boundary_schema = json!({ + "type": "object", + "properties": { + "strings": { + "type": "object", + "properties": { + "empty": { "type": "string", "minLength": 0, "maxLength": 0 }, + "single": { "type": "string", "minLength": 1, "maxLength": 1 }, + "exact_length": { "type": "string", "minLength": 10, "maxLength": 10 }, + "very_long": { "type": "string", "maxLength": 10000 } + } + }, + "numbers": { + "type": "object", + "properties": { + "zero": { "type": "number", "minimum": 0, "maximum": 0 }, + "negative": { "type": "number", "minimum": -1000, "maximum": -1 }, + "positive": { "type": "number", "minimum": 1, "maximum": 1000 }, + "float_precision": { "type": "number" } + } + }, + "arrays": { + "type": "object", + "properties": { + "empty": { "type": "array", "minItems": 0, "maxItems": 0, "items": { "type": "string" } }, + "single_item": { "type": "array", "minItems": 1, "maxItems": 1, "items": { "type": "string" } }, + "exact_size": { "type": "array", "minItems": 5, "maxItems": 5, "items": { "type": "integer" } }, + "large_array": { "type": "array", "maxItems": 1000, "items": { "type": "boolean" } } + } + }, + "objects": { + "type": "object", + "properties": { + "empty": { "type": "object", "additionalProperties": false }, + "single_prop": { + "type": "object", + "properties": { "only": { "type": "string" } }, + "additionalProperties": false, + "required": ["only"] + } + } + }, + "nulls_and_optionals": { + "type": "object", + "properties": { + "nullable": { "type": "string" }, + "optional": { "type": "string" }, + "required_null": { "type": "null" } + }, + "required": ["required_null"] + } + }, + "required": ["strings", "numbers", "arrays", "objects", "nulls_and_optionals"] + }); + + let schema = Schema::from_serde_json_value(boundary_schema).unwrap(); + + // Valid boundary condition data + let valid_boundary_data = json!({ + "strings": { + "empty": "", + "single": "a", + "exact_length": "exactly_10", + "very_long": "a".repeat(9999) + }, + "numbers": { + "zero": 0, + "negative": -500, + "positive": 250, + "float_precision": 123.45 + }, + "arrays": { + "empty": [], + "single_item": ["test"], + "exact_size": [1, 2, 3, 4, 5], + "large_array": vec![true; 500] + }, + "objects": { + "empty": {}, + "single_prop": { + "only": "value" + } + }, + "nulls_and_optionals": { + "nullable": "string_value", + "optional": "present", + "required_null": null + } + }); + + let value = Value::from(valid_boundary_data); + let result = SchemaValidator::validate(&value, &schema); + assert!( + result.is_ok(), + "Valid boundary conditions should pass validation" + ); + + // Invalid boundary violations + let invalid_boundary_data = json!({ + "strings": { + "empty": "not empty", // Should be empty + "single": "too long", // Should be exactly 1 character + "exact_length": "wrong", // Should be exactly 10 characters + "very_long": "a".repeat(10001) // Exceeds maximum length + }, + "numbers": { + "zero": 0.1, // Should be exactly 0 + "negative": 1, // Should be negative + "positive": -1, // Should be positive + "float_precision": 123.456 // Wrong precision + }, + "arrays": { + "empty": ["not empty"], // Should be empty + "single_item": [], // Should have exactly 1 item + "exact_size": [1, 2, 3], // Should have exactly 5 items + "large_array": vec![true; 1001] // Exceeds maximum items + }, + "objects": { + "empty": { "should_be_empty": true }, // Should have no properties + "single_prop": {} // Missing required property + }, + "nulls_and_optionals": { + "nullable": "should allow null or string", + "required_null": "should be null" // Should be null + } + }); + + let value = Value::from(invalid_boundary_data); + let result = SchemaValidator::validate(&value, &schema); + assert!( + result.is_err(), + "Boundary violations should fail validation" + ); +} + +#[test] +fn test_concurrent_schema_validation_stress() { + use std::sync::Arc; + use std::thread; + + // Create a complex schema for concurrent testing + let concurrent_schema = json!({ + "type": "object", + "properties": { + "id": { "type": "string", "pattern": "^[a-zA-Z0-9-]{8,64}$" }, + "timestamp": { "type": "string" }, + "data": { + "type": "object", + "properties": { + "values": { + "type": "array", + "items": { "type": "number" }, + "minItems": 1, + "maxItems": 100 + }, + "metadata": { + "type": "object", + "additionalProperties": { "type": "string" } + } + }, + "required": ["values"] + } + }, + "required": ["id", "timestamp", "data"] + }); + + let schema = Arc::new(Schema::from_serde_json_value(concurrent_schema).unwrap()); + + // Spawn multiple threads to validate concurrently + let mut handles = vec![]; + + for thread_id in 0..10 { + let schema_clone = Arc::clone(&schema); + + let handle = thread::spawn(move || { + let mut results = Vec::new(); + + for i in 0..100 { + let test_data = json!({ + "id": format!("thread-{}-item-{}", thread_id, i), + "timestamp": "2023-12-31T23:59:59Z", + "data": { + "values": vec![1.0, 2.0, 3.0, (i as f64)], + "metadata": { + "thread": thread_id.to_string(), + "iteration": i.to_string(), + "test_type": "concurrent" + } + } + }); + + let value = Value::from(test_data); + let result = SchemaValidator::validate(&value, &schema_clone); + results.push(result.is_ok()); + } + + results + }); + + handles.push(handle); + } + + // Wait for all threads and collect results + let mut all_results = Vec::new(); + for handle in handles { + let thread_results = handle.join().expect("Thread should complete successfully"); + all_results.extend(thread_results); + } + + // All validations should pass + let successful_validations = all_results.iter().filter(|&&result| result).count(); + assert_eq!( + successful_validations, 1000, + "All 1000 concurrent validations should pass" + ); +} diff --git a/src/schema/validate.rs b/src/schema/validate.rs new file mode 100644 index 0000000..1e56e8e --- /dev/null +++ b/src/schema/validate.rs @@ -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; + +/// 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, + maximum: Option, + 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, + maximum: Option, + 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, + max_length: Option, + 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, + max_items: Option, + 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, + required: Option<&Vec>, + 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, + 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 = 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, + discriminated_subobject: &crate::schema::DiscriminatedSubobject, + base_properties: &BTreeMap, + 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, + 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(), + } + } +}