mirror of
https://github.com/microsoft/regorus.git
synced 2026-08-05 02:16:11 +00:00
feat(azure-policy): add alias normalization and denormalization (#635)
* feat: add Azure Policy alias normalization/denormalization Add normalizer and denormalizer for ARM JSON resources, enabling Azure Policy alias short names to become direct paths into a flat structure. - Normalizer: flattens properties wrappers, lowercases keys, resolves per-alias versioned ARM paths, handles sub-resource array flattening, element-level field remaps, and array base renames - Denormalizer: reverses all transformations with casing restoration - AliasRegistry: loads production alias catalogs and data policy manifests - Types: serde deserialization for ARM provider alias formats - YAML test suite: 13 test files covering normalize, denormalize, round-trip, data-plane, edge cases, malformed input, sub-resources, and registry API - Benchmark suite for normalization performance * feat: add FFI and C# bindings for alias normalization - FFI: alias_registry.rs with C-compatible API for loading catalogs, normalizing resources, and denormalizing back to ARM JSON - C#: AliasRegistry wrapper class with NativeMethods P/Invoke bindings and integration tests - Updated Cargo.lock files for new serde_json dependency
This commit is contained in:
committed by
GitHub
parent
35fb5d5953
commit
d36f952133
197
Cargo.lock
generated
197
Cargo.lock
generated
@@ -63,9 +63,9 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299"
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "0.6.21"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a"
|
||||
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
@@ -78,15 +78,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.13"
|
||||
version = "1.0.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
|
||||
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "0.2.7"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
|
||||
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
@@ -140,9 +140,9 @@ checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.10.0"
|
||||
version = "2.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
|
||||
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
|
||||
|
||||
[[package]]
|
||||
name = "borrow-or-share"
|
||||
@@ -162,9 +162,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.19.1"
|
||||
version = "3.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
|
||||
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
|
||||
|
||||
[[package]]
|
||||
name = "bytecount"
|
||||
@@ -186,9 +186,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.55"
|
||||
version = "1.2.58"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29"
|
||||
checksum = "e1e928d4b69e3077709075a938a05ffbedfa53a84c8f766efbf8220bb1ff60e1"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"shlex",
|
||||
@@ -263,9 +263,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.60"
|
||||
version = "4.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a"
|
||||
checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@@ -273,9 +273,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.60"
|
||||
version = "4.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876"
|
||||
checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@@ -285,14 +285,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.5.55"
|
||||
version = "4.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5"
|
||||
checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2 1.0.106",
|
||||
"quote 1.0.44",
|
||||
"syn 2.0.114",
|
||||
"quote 1.0.45",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -312,9 +312,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.4"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
|
||||
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
@@ -433,8 +433,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.106",
|
||||
"quote 1.0.44",
|
||||
"syn 2.0.114",
|
||||
"quote 1.0.45",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -497,9 +497,9 @@ checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||
|
||||
[[package]]
|
||||
name = "flate2"
|
||||
version = "1.1.8"
|
||||
version = "1.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b375d6465b98090a5f25b1c7703f3859783755aa9a80433b36e0379a3ec2f369"
|
||||
checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
|
||||
dependencies = [
|
||||
"crc32fast",
|
||||
"miniz_oxide",
|
||||
@@ -835,15 +835,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.17"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.85"
|
||||
version = "0.3.91"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3"
|
||||
checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"wasm-bindgen",
|
||||
@@ -893,9 +893,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.180"
|
||||
version = "0.2.183"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
|
||||
checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
|
||||
|
||||
[[package]]
|
||||
name = "litemap"
|
||||
@@ -926,9 +926,9 @@ checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.6"
|
||||
version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
|
||||
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||
|
||||
[[package]]
|
||||
name = "miniz_oxide"
|
||||
@@ -1040,9 +1040,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell_polyfill"
|
||||
@@ -1064,9 +1064,9 @@ checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e"
|
||||
|
||||
[[package]]
|
||||
name = "owo-colors"
|
||||
version = "4.2.3"
|
||||
version = "4.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52"
|
||||
checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d"
|
||||
|
||||
[[package]]
|
||||
name = "page_size"
|
||||
@@ -1192,7 +1192,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.106",
|
||||
"syn 2.0.114",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1224,9 +1224,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.44"
|
||||
version = "1.0.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
|
||||
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.106",
|
||||
]
|
||||
@@ -1305,8 +1305,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.106",
|
||||
"quote 1.0.44",
|
||||
"syn 2.0.114",
|
||||
"quote 1.0.45",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1338,9 +1338,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.13"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c"
|
||||
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
@@ -1349,9 +1349,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.8"
|
||||
version = "0.8.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
|
||||
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
||||
|
||||
[[package]]
|
||||
name = "regorus"
|
||||
@@ -1366,6 +1366,7 @@ dependencies = [
|
||||
"dashmap",
|
||||
"data-encoding",
|
||||
"globset",
|
||||
"hashbrown 0.16.1",
|
||||
"icu_casemap",
|
||||
"indexmap",
|
||||
"ipnet",
|
||||
@@ -1416,9 +1417,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.22"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984"
|
||||
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
@@ -1467,8 +1468,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.106",
|
||||
"quote 1.0.44",
|
||||
"syn 2.0.114",
|
||||
"quote 1.0.45",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1505,9 +1506,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
|
||||
|
||||
[[package]]
|
||||
name = "simd-adler32"
|
||||
version = "0.3.8"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"
|
||||
checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
|
||||
|
||||
[[package]]
|
||||
name = "siphasher"
|
||||
@@ -1558,12 +1559,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.114"
|
||||
version = "2.0.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a"
|
||||
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.106",
|
||||
"quote 1.0.44",
|
||||
"quote 1.0.45",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
@@ -1574,8 +1575,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.106",
|
||||
"quote 1.0.44",
|
||||
"syn 2.0.114",
|
||||
"quote 1.0.45",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1606,8 +1607,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.106",
|
||||
"quote 1.0.44",
|
||||
"syn 2.0.114",
|
||||
"quote 1.0.45",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1663,9 +1664,9 @@ checksum = "0b993bddc193ae5bd0d623b49ec06ac3e9312875fdae725a975c51db1cc1677f"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.22"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
@@ -1711,9 +1712,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.22.0"
|
||||
version = "1.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37"
|
||||
checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9"
|
||||
dependencies = [
|
||||
"getrandom 0.4.2",
|
||||
"rand",
|
||||
@@ -1771,9 +1772,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.108"
|
||||
version = "0.2.114"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566"
|
||||
checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
@@ -1784,32 +1785,32 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.108"
|
||||
version = "0.2.114"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608"
|
||||
checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6"
|
||||
dependencies = [
|
||||
"quote 1.0.44",
|
||||
"quote 1.0.45",
|
||||
"wasm-bindgen-macro-support",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.108"
|
||||
version = "0.2.114"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55"
|
||||
checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2 1.0.106",
|
||||
"quote 1.0.44",
|
||||
"syn 2.0.114",
|
||||
"quote 1.0.45",
|
||||
"syn 2.0.117",
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.108"
|
||||
version = "0.2.114"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12"
|
||||
checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
@@ -1850,9 +1851,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "web-sys"
|
||||
version = "0.3.85"
|
||||
version = "0.3.91"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598"
|
||||
checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
@@ -1909,8 +1910,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.106",
|
||||
"quote 1.0.44",
|
||||
"syn 2.0.114",
|
||||
"quote 1.0.45",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1920,8 +1921,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.106",
|
||||
"quote 1.0.44",
|
||||
"syn 2.0.114",
|
||||
"quote 1.0.45",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1996,7 +1997,7 @@ dependencies = [
|
||||
"heck",
|
||||
"indexmap",
|
||||
"prettyplease",
|
||||
"syn 2.0.114",
|
||||
"syn 2.0.117",
|
||||
"wasm-metadata",
|
||||
"wit-bindgen-core",
|
||||
"wit-component",
|
||||
@@ -2011,8 +2012,8 @@ dependencies = [
|
||||
"anyhow",
|
||||
"prettyplease",
|
||||
"proc-macro2 1.0.106",
|
||||
"quote 1.0.44",
|
||||
"syn 2.0.114",
|
||||
"quote 1.0.45",
|
||||
"syn 2.0.117",
|
||||
"wit-bindgen-core",
|
||||
"wit-bindgen-rust",
|
||||
]
|
||||
@@ -2090,29 +2091,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.106",
|
||||
"quote 1.0.44",
|
||||
"syn 2.0.114",
|
||||
"quote 1.0.45",
|
||||
"syn 2.0.117",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.36"
|
||||
version = "0.8.47"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dafd85c832c1b68bbb4ec0c72c7f6f4fc5179627d2bc7c26b30e4c0cc11e76cc"
|
||||
checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.8.36"
|
||||
version = "0.8.47"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7cb7e4e8436d9db52fbd6625dbf2f45243ab84994a72882ec8227b99e72b439a"
|
||||
checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.106",
|
||||
"quote 1.0.44",
|
||||
"syn 2.0.114",
|
||||
"quote 1.0.45",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2131,8 +2132,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.106",
|
||||
"quote 1.0.44",
|
||||
"syn 2.0.114",
|
||||
"quote 1.0.45",
|
||||
"syn 2.0.117",
|
||||
"synstructure",
|
||||
]
|
||||
|
||||
@@ -2166,8 +2167,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3"
|
||||
dependencies = [
|
||||
"proc-macro2 1.0.106",
|
||||
"quote 1.0.44",
|
||||
"syn 2.0.114",
|
||||
"quote 1.0.45",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2184,6 +2185,6 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.17"
|
||||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02aae0f83f69aafc94776e879363e9771d7ecbffe2c7fbb6c14c5e00dfe88439"
|
||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||
|
||||
@@ -24,7 +24,7 @@ default = ["full-opa", "arc", "rvm"]
|
||||
|
||||
arc = []
|
||||
ast = []
|
||||
azure_policy = ["dep:jsonschema", "dep:chrono", "dep:ipnet", "dep:icu_casemap", "arc", "dashmap"]
|
||||
azure_policy = ["dep:jsonschema", "dep:chrono", "dep:ipnet", "dep:icu_casemap", "dep:hashbrown", "arc", "dashmap"]
|
||||
azure-rbac = ["regex", "time", "net"]
|
||||
base64 = ["dep:data-encoding"]
|
||||
base64url = ["dep:data-encoding"]
|
||||
@@ -99,6 +99,7 @@ rand = ["dep:rand"]
|
||||
anyhow = { version = "1.0.102", default-features = false }
|
||||
serde = {version = "1.0.150", default-features = false, features = ["derive", "rc", "alloc"] }
|
||||
serde_json = { version = "1.0.89", default-features = false, features = ["alloc"] }
|
||||
hashbrown = { version = "0.16", default-features = false, features = ["default-hasher"], optional = true }
|
||||
lazy_static = { version = "1.4.0", default-features = false }
|
||||
thiserror = { version = "2.0", default-features = false }
|
||||
|
||||
@@ -197,6 +198,11 @@ name = "rvm_benchmark"
|
||||
harness = false
|
||||
required-features = ["rvm"]
|
||||
|
||||
[[bench]]
|
||||
name = "normalization_benchmark"
|
||||
harness = false
|
||||
required-features = ["azure_policy"]
|
||||
|
||||
[[example]]
|
||||
name="regorus"
|
||||
harness=false
|
||||
|
||||
560
benches/normalization_benchmark.rs
Normal file
560
benches/normalization_benchmark.rs
Normal file
@@ -0,0 +1,560 @@
|
||||
use std::hint::black_box;
|
||||
|
||||
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
|
||||
use regorus::languages::azure_policy::aliases::{denormalizer, normalizer, AliasRegistry};
|
||||
use regorus::Value;
|
||||
use serde_json::json;
|
||||
|
||||
// ─── Alias catalog (reused across benchmarks) ───────────────────────────────
|
||||
|
||||
const ALIASES_JSON: &str = r#"[
|
||||
{
|
||||
"namespace": "Microsoft.Network",
|
||||
"resourceTypes": [
|
||||
{
|
||||
"resourceType": "networkSecurityGroups",
|
||||
"aliases": [
|
||||
{
|
||||
"name": "Microsoft.Network/networkSecurityGroups/securityRules[*].protocol",
|
||||
"defaultPath": "properties.securityRules[*].properties.protocol",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Network/networkSecurityGroups/securityRules[*].access",
|
||||
"defaultPath": "properties.securityRules[*].properties.access",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Network/networkSecurityGroups/securityRules[*].priority",
|
||||
"defaultPath": "properties.securityRules[*].properties.priority",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Network/networkSecurityGroups/securityRules[*].direction",
|
||||
"defaultPath": "properties.securityRules[*].properties.direction",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Network/networkSecurityGroups/securityRules[*].sourceAddressPrefix",
|
||||
"defaultPath": "properties.securityRules[*].properties.sourceAddressPrefix",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Network/networkSecurityGroups/securityRules[*].destinationPortRange",
|
||||
"defaultPath": "properties.securityRules[*].properties.destinationPortRange",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Network/networkSecurityGroups/securityRules[*].name",
|
||||
"defaultPath": "properties.securityRules[*].name",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Network/networkSecurityGroups/defaultSecurityRules[*].protocol",
|
||||
"defaultPath": "properties.defaultSecurityRules[*].properties.protocol",
|
||||
"paths": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"namespace": "Microsoft.Storage",
|
||||
"resourceTypes": [
|
||||
{
|
||||
"resourceType": "storageAccounts",
|
||||
"aliases": [
|
||||
{
|
||||
"name": "Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly",
|
||||
"defaultPath": "properties.supportsHttpsTrafficOnly",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Storage/storageAccounts/accessTier",
|
||||
"defaultPath": "properties.accessTier",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Storage/storageAccounts/isHnsEnabled",
|
||||
"defaultPath": "properties.isHnsEnabled",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Storage/storageAccounts/minimumTlsVersion",
|
||||
"defaultPath": "properties.minimumTlsVersion",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Storage/storageAccounts/allowBlobPublicAccess",
|
||||
"defaultPath": "properties.allowBlobPublicAccess",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Storage/storageAccounts/sku.name",
|
||||
"defaultPath": "sku.name",
|
||||
"paths": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]"#;
|
||||
|
||||
fn build_registry() -> AliasRegistry {
|
||||
let mut reg = AliasRegistry::new();
|
||||
reg.load_from_json(ALIASES_JSON).unwrap();
|
||||
reg
|
||||
}
|
||||
|
||||
/// Convert a serde_json::Value to regorus::Value.
|
||||
fn to_regorus(v: serde_json::Value) -> Value {
|
||||
Value::from(v)
|
||||
}
|
||||
|
||||
// ─── Input resources ────────────────────────────────────────────────────────
|
||||
|
||||
fn simple_storage_resource() -> Value {
|
||||
to_regorus(json!({
|
||||
"name": "myStorageAccount",
|
||||
"type": "Microsoft.Storage/storageAccounts",
|
||||
"location": "westus2",
|
||||
"kind": "StorageV2",
|
||||
"sku": { "name": "Standard_LRS", "tier": "Standard" },
|
||||
"tags": { "environment": "production", "team": "platform" },
|
||||
"properties": {
|
||||
"supportsHttpsTrafficOnly": true,
|
||||
"accessTier": "Hot",
|
||||
"isHnsEnabled": false,
|
||||
"minimumTlsVersion": "TLS1_2",
|
||||
"allowBlobPublicAccess": false
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
fn nsg_resource(rule_count: usize) -> Value {
|
||||
let rules: Vec<serde_json::Value> = (0..rule_count)
|
||||
.map(|i| {
|
||||
json!({
|
||||
"name": format!("rule-{}", i),
|
||||
"properties": {
|
||||
"protocol": "Tcp",
|
||||
"access": if i % 2 == 0 { "Allow" } else { "Deny" },
|
||||
"priority": 100 + i,
|
||||
"direction": "Inbound",
|
||||
"sourceAddressPrefix": format!("10.0.{}.0/24", i % 256),
|
||||
"destinationPortRange": format!("{}", 80 + i)
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
to_regorus(json!({
|
||||
"name": "myNsg",
|
||||
"type": "Microsoft.Network/networkSecurityGroups",
|
||||
"location": "eastus",
|
||||
"properties": {
|
||||
"securityRules": rules
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
// ─── Benchmarks ─────────────────────────────────────────────────────────────
|
||||
|
||||
fn bench_normalize_simple(c: &mut Criterion) {
|
||||
let registry = build_registry();
|
||||
let resource = simple_storage_resource();
|
||||
|
||||
c.bench_function("normalize/simple_storage", |b| {
|
||||
b.iter(|| normalizer::normalize(black_box(&resource), Some(®istry), None))
|
||||
});
|
||||
}
|
||||
|
||||
fn bench_normalize_no_aliases(c: &mut Criterion) {
|
||||
let resource = simple_storage_resource();
|
||||
|
||||
c.bench_function("normalize/simple_no_aliases", |b| {
|
||||
b.iter(|| normalizer::normalize(black_box(&resource), None, None))
|
||||
});
|
||||
}
|
||||
|
||||
fn bench_normalize_nsg_scaling(c: &mut Criterion) {
|
||||
let registry = build_registry();
|
||||
let mut group = c.benchmark_group("normalize/nsg_rules");
|
||||
|
||||
for rule_count in [5, 20, 100] {
|
||||
let resource = nsg_resource(rule_count);
|
||||
group.bench_with_input(
|
||||
BenchmarkId::from_parameter(rule_count),
|
||||
&resource,
|
||||
|b, res| b.iter(|| normalizer::normalize(black_box(res), Some(®istry), None)),
|
||||
);
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn bench_denormalize_simple(c: &mut Criterion) {
|
||||
let registry = build_registry();
|
||||
let resource = simple_storage_resource();
|
||||
let normalized = normalizer::normalize(&resource, Some(®istry), None);
|
||||
|
||||
c.bench_function("denormalize/simple_storage", |b| {
|
||||
b.iter(|| denormalizer::denormalize(black_box(&normalized), Some(®istry), None))
|
||||
});
|
||||
}
|
||||
|
||||
fn bench_denormalize_nsg_scaling(c: &mut Criterion) {
|
||||
let registry = build_registry();
|
||||
let mut group = c.benchmark_group("denormalize/nsg_rules");
|
||||
|
||||
for rule_count in [5, 20, 100] {
|
||||
let resource = nsg_resource(rule_count);
|
||||
let normalized = normalizer::normalize(&resource, Some(®istry), None);
|
||||
group.bench_with_input(
|
||||
BenchmarkId::from_parameter(rule_count),
|
||||
&normalized,
|
||||
|b, norm| b.iter(|| denormalizer::denormalize(black_box(norm), Some(®istry), None)),
|
||||
);
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn bench_round_trip(c: &mut Criterion) {
|
||||
let registry = build_registry();
|
||||
let resource = nsg_resource(20);
|
||||
|
||||
c.bench_function("round_trip/nsg_20_rules", |b| {
|
||||
b.iter(|| {
|
||||
let n = normalizer::normalize(black_box(&resource), Some(®istry), None);
|
||||
denormalizer::denormalize(&n, Some(®istry), None)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
fn bench_normalize_and_wrap(c: &mut Criterion) {
|
||||
let registry = build_registry();
|
||||
let resource = nsg_resource(20);
|
||||
let context = to_regorus(json!({"resourceGroup": {"name": "rg1"}}));
|
||||
let parameters = to_regorus(json!({"env": "prod"}));
|
||||
|
||||
c.bench_function("normalize_and_wrap/nsg_20_rules", |b| {
|
||||
b.iter(|| {
|
||||
registry.normalize_and_wrap(
|
||||
black_box(&resource),
|
||||
None,
|
||||
Some(context.clone()),
|
||||
Some(parameters.clone()),
|
||||
)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
fn bench_registry_load(c: &mut Criterion) {
|
||||
c.bench_function("registry/load_from_json", |b| {
|
||||
b.iter(|| {
|
||||
let mut reg = AliasRegistry::new();
|
||||
reg.load_from_json(black_box(ALIASES_JSON)).unwrap();
|
||||
reg
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Large-payload benchmarks ───────────────────────────────────────────────
|
||||
//
|
||||
// These stress the hot paths identified in the performance analysis:
|
||||
// - Nested set helpers (alias-heavy catalog with deep properties)
|
||||
// - Array element remap/cleanup/rewrap (large sub-resource arrays)
|
||||
// - Scalar denormalization lookups (many aliases × many fields)
|
||||
|
||||
/// Build a large alias catalog with `n` scalar aliases for storage accounts.
|
||||
/// Each alias maps to a nested `properties.section_i.field_j` path, creating
|
||||
/// deep nested-set workloads.
|
||||
fn large_alias_catalog(n: usize) -> String {
|
||||
let mut aliases = Vec::new();
|
||||
for i in 0..n {
|
||||
let section = i / 10;
|
||||
let field = i % 10;
|
||||
aliases.push(format!(
|
||||
r#"{{
|
||||
"name": "Microsoft.Storage/storageAccounts/section{section}Field{field}",
|
||||
"defaultPath": "properties.section{section}.field{field}",
|
||||
"paths": []
|
||||
}}"#,
|
||||
));
|
||||
}
|
||||
format!(
|
||||
r#"[{{
|
||||
"namespace": "Microsoft.Storage",
|
||||
"resourceTypes": [{{
|
||||
"resourceType": "storageAccounts",
|
||||
"aliases": [{aliases}]
|
||||
}}]
|
||||
}}]"#,
|
||||
aliases = aliases.join(",")
|
||||
)
|
||||
}
|
||||
|
||||
/// Build a storage account resource whose `properties` contain nested sections
|
||||
/// matching the large alias catalog.
|
||||
fn large_storage_resource(alias_count: usize) -> Value {
|
||||
let mut sections = serde_json::Map::new();
|
||||
for i in 0..alias_count {
|
||||
let section = i / 10;
|
||||
let field = i % 10;
|
||||
let section_key = format!("section{section}");
|
||||
let section_obj = sections
|
||||
.entry(section_key)
|
||||
.or_insert_with(|| serde_json::Value::Object(serde_json::Map::new()));
|
||||
if let serde_json::Value::Object(m) = section_obj {
|
||||
m.insert(format!("field{field}"), serde_json::Value::from(i));
|
||||
}
|
||||
}
|
||||
Value::from(json!({
|
||||
"name": "bigStorage",
|
||||
"type": "Microsoft.Storage/storageAccounts",
|
||||
"location": "westus2",
|
||||
"properties": sections
|
||||
}))
|
||||
}
|
||||
|
||||
fn bench_normalize_large_catalog(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("normalize/large_catalog");
|
||||
for alias_count in [50, 200] {
|
||||
let catalog_json = large_alias_catalog(alias_count);
|
||||
let mut reg = AliasRegistry::new();
|
||||
reg.load_from_json(&catalog_json).unwrap();
|
||||
let resource = large_storage_resource(alias_count);
|
||||
group.bench_with_input(
|
||||
BenchmarkId::from_parameter(alias_count),
|
||||
&(reg, resource),
|
||||
|b, (reg, res)| b.iter(|| normalizer::normalize(black_box(res), Some(reg), None)),
|
||||
);
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn bench_denormalize_large_catalog(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("denormalize/large_catalog");
|
||||
for alias_count in [50, 200] {
|
||||
let catalog_json = large_alias_catalog(alias_count);
|
||||
let mut reg = AliasRegistry::new();
|
||||
reg.load_from_json(&catalog_json).unwrap();
|
||||
let resource = large_storage_resource(alias_count);
|
||||
let normalized = normalizer::normalize(&resource, Some(®), None);
|
||||
group.bench_with_input(
|
||||
BenchmarkId::from_parameter(alias_count),
|
||||
&(reg, normalized),
|
||||
|b, (reg, norm)| b.iter(|| denormalizer::denormalize(black_box(norm), Some(reg), None)),
|
||||
);
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn bench_nsg_large_subarrays(c: &mut Criterion) {
|
||||
let registry = build_registry();
|
||||
let mut group = c.benchmark_group("round_trip/nsg_sub_resource");
|
||||
for rule_count in [50, 200, 500] {
|
||||
let resource = nsg_resource(rule_count);
|
||||
group.bench_with_input(
|
||||
BenchmarkId::from_parameter(rule_count),
|
||||
&resource,
|
||||
|b, res| {
|
||||
b.iter(|| {
|
||||
let n = normalizer::normalize(black_box(res), Some(®istry), None);
|
||||
denormalizer::denormalize(&n, Some(®istry), None)
|
||||
})
|
||||
},
|
||||
);
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
|
||||
// ─── Versioned-path benchmarks ──────────────────────────────────────────────
|
||||
//
|
||||
// Exercise the precomputed versioned-path aggregates by building a catalog
|
||||
// where wildcard (array) aliases have version-specific paths that differ from
|
||||
// the default, then running normalize/denormalize with an explicit api_version.
|
||||
|
||||
/// NSG-like alias catalog where wildcard aliases have versioned paths that
|
||||
/// differ from the default. This forces the normalize/denormalize path through
|
||||
/// the versioned aggregate lookup rather than the default-aggregate fast path.
|
||||
const VERSIONED_ALIASES_JSON: &str = r#"[
|
||||
{
|
||||
"namespace": "Microsoft.Network",
|
||||
"resourceTypes": [
|
||||
{
|
||||
"resourceType": "networkSecurityGroups",
|
||||
"aliases": [
|
||||
{
|
||||
"name": "Microsoft.Network/networkSecurityGroups/securityRules[*].protocol",
|
||||
"defaultPath": "properties.securityRules[*].properties.protocol",
|
||||
"paths": [
|
||||
{ "path": "properties.securityRules[*].properties.transportProtocol", "apiVersions": ["2020-01-01"] },
|
||||
{ "path": "properties.securityRules[*].properties.protocol", "apiVersions": ["2022-01-01"] }
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Network/networkSecurityGroups/securityRules[*].access",
|
||||
"defaultPath": "properties.securityRules[*].properties.access",
|
||||
"paths": [
|
||||
{ "path": "properties.securityRules[*].properties.accessLevel", "apiVersions": ["2020-01-01"] },
|
||||
{ "path": "properties.securityRules[*].properties.access", "apiVersions": ["2022-01-01"] }
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Network/networkSecurityGroups/securityRules[*].priority",
|
||||
"defaultPath": "properties.securityRules[*].properties.priority",
|
||||
"paths": [
|
||||
{ "path": "properties.securityRules[*].properties.rulePriority", "apiVersions": ["2020-01-01"] },
|
||||
{ "path": "properties.securityRules[*].properties.priority", "apiVersions": ["2022-01-01"] }
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Network/networkSecurityGroups/securityRules[*].direction",
|
||||
"defaultPath": "properties.securityRules[*].properties.direction",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Network/networkSecurityGroups/securityRules[*].sourceAddressPrefix",
|
||||
"defaultPath": "properties.securityRules[*].properties.sourceAddressPrefix",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Network/networkSecurityGroups/securityRules[*].destinationPortRange",
|
||||
"defaultPath": "properties.securityRules[*].properties.destinationPortRange",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Network/networkSecurityGroups/securityRules[*].name",
|
||||
"defaultPath": "properties.securityRules[*].name",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Network/networkSecurityGroups/provisioningState",
|
||||
"defaultPath": "properties.provisioningState",
|
||||
"paths": [
|
||||
{ "path": "properties.state", "apiVersions": ["2020-01-01"] },
|
||||
{ "path": "properties.provisioningState", "apiVersions": ["2022-01-01"] }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]"#;
|
||||
|
||||
fn build_versioned_registry() -> AliasRegistry {
|
||||
let mut reg = AliasRegistry::new();
|
||||
reg.load_from_json(VERSIONED_ALIASES_JSON).unwrap();
|
||||
reg
|
||||
}
|
||||
|
||||
/// Build an NSG resource for versioned-path benchmarks.
|
||||
/// Uses the 2020-01-01 field names (`transportProtocol`, `accessLevel`,
|
||||
/// `rulePriority`) so that versioned path resolution actually differs from
|
||||
/// the default.
|
||||
fn nsg_versioned_resource(rule_count: usize) -> Value {
|
||||
let rules: Vec<serde_json::Value> = (0..rule_count)
|
||||
.map(|i| {
|
||||
json!({
|
||||
"name": format!("rule-{}", i),
|
||||
"properties": {
|
||||
"transportProtocol": "Tcp",
|
||||
"accessLevel": if i % 2 == 0 { "Allow" } else { "Deny" },
|
||||
"rulePriority": 100 + i,
|
||||
"direction": "Inbound",
|
||||
"sourceAddressPrefix": format!("10.0.{}.0/24", i % 256),
|
||||
"destinationPortRange": format!("{}", 80 + i)
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
to_regorus(json!({
|
||||
"name": "myNsg",
|
||||
"type": "Microsoft.Network/networkSecurityGroups",
|
||||
"location": "eastus",
|
||||
"properties": {
|
||||
"state": "Succeeded",
|
||||
"securityRules": rules
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
fn bench_normalize_versioned(c: &mut Criterion) {
|
||||
let registry = build_versioned_registry();
|
||||
let mut group = c.benchmark_group("normalize_versioned/nsg_rules");
|
||||
|
||||
for rule_count in [5, 20, 100] {
|
||||
let resource = nsg_versioned_resource(rule_count);
|
||||
group.bench_with_input(
|
||||
BenchmarkId::from_parameter(rule_count),
|
||||
&resource,
|
||||
|b, res| {
|
||||
b.iter(|| {
|
||||
normalizer::normalize(black_box(res), Some(®istry), Some("2020-01-01"))
|
||||
})
|
||||
},
|
||||
);
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn bench_denormalize_versioned(c: &mut Criterion) {
|
||||
let registry = build_versioned_registry();
|
||||
let mut group = c.benchmark_group("denormalize_versioned/nsg_rules");
|
||||
|
||||
for rule_count in [5, 20, 100] {
|
||||
let resource = nsg_versioned_resource(rule_count);
|
||||
let normalized = normalizer::normalize(&resource, Some(®istry), Some("2020-01-01"));
|
||||
group.bench_with_input(
|
||||
BenchmarkId::from_parameter(rule_count),
|
||||
&normalized,
|
||||
|b, norm| {
|
||||
b.iter(|| {
|
||||
denormalizer::denormalize(black_box(norm), Some(®istry), Some("2020-01-01"))
|
||||
})
|
||||
},
|
||||
);
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
|
||||
fn bench_round_trip_versioned(c: &mut Criterion) {
|
||||
let registry = build_versioned_registry();
|
||||
let mut group = c.benchmark_group("round_trip_versioned/nsg_rules");
|
||||
|
||||
for rule_count in [20, 100] {
|
||||
let resource = nsg_versioned_resource(rule_count);
|
||||
group.bench_with_input(
|
||||
BenchmarkId::from_parameter(rule_count),
|
||||
&resource,
|
||||
|b, res| {
|
||||
b.iter(|| {
|
||||
let n =
|
||||
normalizer::normalize(black_box(res), Some(®istry), Some("2020-01-01"));
|
||||
denormalizer::denormalize(&n, Some(®istry), Some("2020-01-01"))
|
||||
})
|
||||
},
|
||||
);
|
||||
}
|
||||
group.finish();
|
||||
}
|
||||
|
||||
criterion_group!(
|
||||
normalization_benches,
|
||||
bench_normalize_simple,
|
||||
bench_normalize_no_aliases,
|
||||
bench_normalize_nsg_scaling,
|
||||
bench_denormalize_simple,
|
||||
bench_denormalize_nsg_scaling,
|
||||
bench_round_trip,
|
||||
bench_normalize_and_wrap,
|
||||
bench_registry_load,
|
||||
bench_normalize_large_catalog,
|
||||
bench_denormalize_large_catalog,
|
||||
bench_nsg_large_subarrays,
|
||||
bench_normalize_versioned,
|
||||
bench_denormalize_versioned,
|
||||
bench_round_trip_versioned,
|
||||
);
|
||||
criterion_main!(normalization_benches);
|
||||
191
bindings/csharp/Regorus.Tests/AliasRegistryTests.cs
Normal file
191
bindings/csharp/Regorus.Tests/AliasRegistryTests.cs
Normal file
@@ -0,0 +1,191 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Nodes;
|
||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||
using Regorus;
|
||||
|
||||
namespace Regorus.Tests;
|
||||
|
||||
[TestClass]
|
||||
public class AliasRegistryTests
|
||||
{
|
||||
private const string AliasesJson = @"[{
|
||||
""namespace"": ""Microsoft.Storage"",
|
||||
""resourceTypes"": [{
|
||||
""resourceType"": ""storageAccounts"",
|
||||
""aliases"": [{
|
||||
""name"": ""Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly"",
|
||||
""defaultPath"": ""properties.supportsHttpsTrafficOnly"",
|
||||
""paths"": []
|
||||
}, {
|
||||
""name"": ""Microsoft.Storage/storageAccounts/accessTier"",
|
||||
""defaultPath"": ""properties.accessTier"",
|
||||
""paths"": []
|
||||
}]
|
||||
}]
|
||||
}]";
|
||||
|
||||
private const string ManifestJson = @"{
|
||||
""dataNamespace"": ""Microsoft.KeyVault.Data"",
|
||||
""aliases"": [],
|
||||
""resourceTypeAliases"": [{
|
||||
""resourceType"": ""vaults/certificates"",
|
||||
""aliases"": [{
|
||||
""name"": ""Microsoft.KeyVault.Data/vaults/certificates/keySize"",
|
||||
""paths"": [{ ""path"": ""keySize"", ""apiVersions"": [""7.0""] }]
|
||||
}]
|
||||
}]
|
||||
}";
|
||||
|
||||
[TestMethod]
|
||||
public void Create_and_dispose_succeeds()
|
||||
{
|
||||
using var registry = new AliasRegistry();
|
||||
Assert.AreEqual(0, registry.Length);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void LoadJson_populates_registry()
|
||||
{
|
||||
using var registry = new AliasRegistry();
|
||||
registry.LoadJson(AliasesJson);
|
||||
Assert.AreEqual(1, registry.Length);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void LoadManifest_populates_registry()
|
||||
{
|
||||
using var registry = new AliasRegistry();
|
||||
registry.LoadManifest(ManifestJson);
|
||||
Assert.AreEqual(1, registry.Length);
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void NormalizeAndWrap_produces_envelope()
|
||||
{
|
||||
using var registry = new AliasRegistry();
|
||||
registry.LoadJson(AliasesJson);
|
||||
|
||||
var resource = @"{
|
||||
""name"": ""acct1"",
|
||||
""type"": ""Microsoft.Storage/storageAccounts"",
|
||||
""properties"": { ""supportsHttpsTrafficOnly"": true, ""accessTier"": ""Hot"" }
|
||||
}";
|
||||
|
||||
var result = registry.NormalizeAndWrap(resource, "2023-01-01", "{}", "{}");
|
||||
Assert.IsNotNull(result);
|
||||
|
||||
var envelope = JsonNode.Parse(result!)!;
|
||||
Assert.IsNotNull(envelope["resource"]);
|
||||
Assert.IsNotNull(envelope["parameters"]);
|
||||
Assert.IsNotNull(envelope["context"]);
|
||||
|
||||
// Normalized resource should have lowercased alias field names
|
||||
var res = envelope["resource"]!;
|
||||
Assert.AreEqual(true, res["supportshttpstrafficonly"]?.GetValue<bool>());
|
||||
Assert.AreEqual("Hot", res["accesstier"]?.GetValue<string>());
|
||||
Assert.AreEqual("acct1", res["name"]?.GetValue<string>());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void NormalizeAndWrap_with_context_and_parameters()
|
||||
{
|
||||
using var registry = new AliasRegistry();
|
||||
registry.LoadJson(AliasesJson);
|
||||
|
||||
var resource = @"{
|
||||
""name"": ""acct1"",
|
||||
""type"": ""Microsoft.Storage/storageAccounts"",
|
||||
""properties"": { ""supportsHttpsTrafficOnly"": true }
|
||||
}";
|
||||
var context = @"{""resourceGroup"": {""name"": ""rg1""}}";
|
||||
var parameters = @"{""env"": ""prod""}";
|
||||
|
||||
var result = registry.NormalizeAndWrap(resource, "2023-01-01", context, parameters);
|
||||
Assert.IsNotNull(result);
|
||||
|
||||
var envelope = JsonNode.Parse(result!)!;
|
||||
Assert.AreEqual("rg1", envelope["context"]!["resourceGroup"]!["name"]?.GetValue<string>());
|
||||
Assert.AreEqual("prod", envelope["parameters"]!["env"]?.GetValue<string>());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Denormalize_restores_properties()
|
||||
{
|
||||
using var registry = new AliasRegistry();
|
||||
registry.LoadJson(AliasesJson);
|
||||
|
||||
var normalized = @"{
|
||||
""name"": ""acct1"",
|
||||
""type"": ""Microsoft.Storage/storageAccounts"",
|
||||
""supportshttpstrafficonly"": true,
|
||||
""accesstier"": ""Hot""
|
||||
}";
|
||||
|
||||
var result = registry.Denormalize(normalized, "2023-01-01");
|
||||
Assert.IsNotNull(result);
|
||||
|
||||
var arm = JsonNode.Parse(result!)!;
|
||||
Assert.AreEqual("acct1", arm["name"]?.GetValue<string>());
|
||||
Assert.AreEqual(true, arm["properties"]!["supportsHttpsTrafficOnly"]?.GetValue<bool>());
|
||||
Assert.AreEqual("Hot", arm["properties"]!["accessTier"]?.GetValue<string>());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void Round_trip_normalize_then_denormalize()
|
||||
{
|
||||
using var registry = new AliasRegistry();
|
||||
registry.LoadJson(AliasesJson);
|
||||
|
||||
var resource = @"{
|
||||
""name"": ""acct1"",
|
||||
""type"": ""Microsoft.Storage/storageAccounts"",
|
||||
""properties"": { ""supportsHttpsTrafficOnly"": true, ""accessTier"": ""Hot"" }
|
||||
}";
|
||||
|
||||
// Normalize
|
||||
var envelopeJson = registry.NormalizeAndWrap(resource, "2023-01-01", "{}", "{}");
|
||||
Assert.IsNotNull(envelopeJson);
|
||||
|
||||
var envelope = JsonNode.Parse(envelopeJson!)!;
|
||||
var normalizedResource = envelope["resource"]!.ToJsonString();
|
||||
|
||||
// Denormalize
|
||||
var armJson = registry.Denormalize(normalizedResource, "2023-01-01");
|
||||
Assert.IsNotNull(armJson);
|
||||
|
||||
var arm = JsonNode.Parse(armJson!)!;
|
||||
Assert.AreEqual(true, arm["properties"]!["supportsHttpsTrafficOnly"]?.GetValue<bool>());
|
||||
Assert.AreEqual("Hot", arm["properties"]!["accessTier"]?.GetValue<string>());
|
||||
Assert.AreEqual("acct1", arm["name"]?.GetValue<string>());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
public void DataPlane_manifest_normalize()
|
||||
{
|
||||
using var registry = new AliasRegistry();
|
||||
registry.LoadManifest(ManifestJson);
|
||||
|
||||
var resource = @"{
|
||||
""type"": ""Microsoft.KeyVault.Data/vaults/certificates"",
|
||||
""keySize"": 2048
|
||||
}";
|
||||
|
||||
var result = registry.NormalizeAndWrap(resource, "7.0", "{}", "{}");
|
||||
Assert.IsNotNull(result);
|
||||
|
||||
var envelope = JsonNode.Parse(result!)!;
|
||||
Assert.AreEqual(2048, envelope["resource"]!["keysize"]?.GetValue<int>());
|
||||
}
|
||||
|
||||
[TestMethod]
|
||||
[ExpectedException(typeof(InvalidOperationException))]
|
||||
public void LoadJson_invalid_throws()
|
||||
{
|
||||
using var registry = new AliasRegistry();
|
||||
registry.LoadJson("not valid json");
|
||||
}
|
||||
}
|
||||
153
bindings/csharp/Regorus/AliasRegistry.cs
Normal file
153
bindings/csharp/Regorus/AliasRegistry.cs
Normal file
@@ -0,0 +1,153 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
using System;
|
||||
using Regorus.Internal;
|
||||
|
||||
#nullable enable
|
||||
namespace Regorus
|
||||
{
|
||||
/// <summary>
|
||||
/// Manages Azure Policy alias definitions used for resource normalization
|
||||
/// and policy compilation.
|
||||
/// </summary>
|
||||
public unsafe sealed class AliasRegistry : SafeHandleWrapper
|
||||
{
|
||||
/// <summary>
|
||||
/// Create an empty alias registry.
|
||||
/// </summary>
|
||||
public AliasRegistry()
|
||||
: base(RegorusAliasRegistryHandle.Create(), nameof(AliasRegistry))
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Load control-plane alias data (array of ProviderAliases) from a JSON string.
|
||||
/// </summary>
|
||||
/// <param name="json">JSON array of ProviderAliases (e.g. from Get-AzPolicyAlias or ResourceTypesAndAliases.json)</param>
|
||||
public void LoadJson(string json)
|
||||
{
|
||||
Utf8Marshaller.WithUtf8(json, jsonPtr =>
|
||||
{
|
||||
UseHandle(regPtr =>
|
||||
{
|
||||
CheckAndDropResult(API.regorus_alias_registry_load_json(
|
||||
(RegorusAliasRegistry*)regPtr, (byte*)jsonPtr));
|
||||
return 0;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Load a data-plane policy manifest from a JSON string.
|
||||
/// </summary>
|
||||
/// <param name="json">JSON object containing a DataPolicyManifest</param>
|
||||
public void LoadManifest(string json)
|
||||
{
|
||||
Utf8Marshaller.WithUtf8(json, jsonPtr =>
|
||||
{
|
||||
UseHandle(regPtr =>
|
||||
{
|
||||
CheckAndDropResult(API.regorus_alias_registry_load_manifest(
|
||||
(RegorusAliasRegistry*)regPtr, (byte*)jsonPtr));
|
||||
return 0;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the number of resource types loaded in the registry.
|
||||
/// </summary>
|
||||
public long Length
|
||||
{
|
||||
get
|
||||
{
|
||||
return UseHandle(regPtr =>
|
||||
{
|
||||
return ResultHelpers.GetIntResult(
|
||||
API.regorus_alias_registry_len((RegorusAliasRegistry*)regPtr));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Normalize an ARM resource JSON and wrap it into the standard input envelope
|
||||
/// expected by a compiled Azure Policy program.
|
||||
/// </summary>
|
||||
/// <param name="resourceJson">Raw ARM resource JSON</param>
|
||||
/// <param name="apiVersion">API version string (e.g. "2023-01-01"), or null to use default alias paths</param>
|
||||
/// <param name="contextJson">Additional context JSON object (pass "{}" if none)</param>
|
||||
/// <param name="parametersJson">Policy parameter values JSON (pass "{}" if none)</param>
|
||||
/// <returns>JSON string: { "resource": <normalized>, "context": <context>, "parameters": <params> }</returns>
|
||||
public string? NormalizeAndWrap(string resourceJson, string? apiVersion = null, string contextJson = "{}", string parametersJson = "{}")
|
||||
{
|
||||
return Utf8Marshaller.WithUtf8(resourceJson, resPtr =>
|
||||
Utf8Marshaller.WithUtf8(contextJson, ctxPtr =>
|
||||
Utf8Marshaller.WithUtf8(parametersJson, paramsPtr =>
|
||||
{
|
||||
if (apiVersion is null)
|
||||
{
|
||||
return UseHandle(regPtr =>
|
||||
{
|
||||
return ResultHelpers.GetStringResult(
|
||||
API.regorus_alias_registry_normalize_and_wrap(
|
||||
(RegorusAliasRegistry*)regPtr,
|
||||
(byte*)resPtr, null,
|
||||
(byte*)ctxPtr, (byte*)paramsPtr));
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
return Utf8Marshaller.WithUtf8(apiVersion, apiPtr =>
|
||||
UseHandle(regPtr =>
|
||||
{
|
||||
return ResultHelpers.GetStringResult(
|
||||
API.regorus_alias_registry_normalize_and_wrap(
|
||||
(RegorusAliasRegistry*)regPtr,
|
||||
(byte*)resPtr, (byte*)apiPtr,
|
||||
(byte*)ctxPtr, (byte*)paramsPtr));
|
||||
}));
|
||||
}
|
||||
})));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Denormalize a previously-normalized resource JSON back to ARM format.
|
||||
/// </summary>
|
||||
/// <param name="normalizedJson">The normalized resource JSON</param>
|
||||
/// <param name="apiVersion">API version string, or null to use default alias paths</param>
|
||||
/// <returns>Denormalized ARM JSON string</returns>
|
||||
public string? Denormalize(string normalizedJson, string? apiVersion = null)
|
||||
{
|
||||
return Utf8Marshaller.WithUtf8(normalizedJson, normPtr =>
|
||||
{
|
||||
if (apiVersion is null)
|
||||
{
|
||||
return UseHandle(regPtr =>
|
||||
{
|
||||
return ResultHelpers.GetStringResult(
|
||||
API.regorus_alias_registry_denormalize(
|
||||
(RegorusAliasRegistry*)regPtr,
|
||||
(byte*)normPtr, null));
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
return Utf8Marshaller.WithUtf8(apiVersion, apiPtr =>
|
||||
UseHandle(regPtr =>
|
||||
{
|
||||
return ResultHelpers.GetStringResult(
|
||||
API.regorus_alias_registry_denormalize(
|
||||
(RegorusAliasRegistry*)regPtr,
|
||||
(byte*)normPtr, (byte*)apiPtr));
|
||||
}));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private static string? CheckAndDropResult(RegorusResult result)
|
||||
{
|
||||
return ResultHelpers.GetStringResult(result);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -669,6 +669,55 @@ namespace Regorus.Internal
|
||||
internal static extern RegorusResult regorus_effect_schema_clear();
|
||||
|
||||
#endregion
|
||||
|
||||
#region Alias Registry Methods
|
||||
|
||||
/// <summary>
|
||||
/// Create a new, empty AliasRegistry.
|
||||
/// </summary>
|
||||
[DllImport(LibraryName, EntryPoint = "regorus_alias_registry_new", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
|
||||
internal static extern RegorusAliasRegistry* regorus_alias_registry_new();
|
||||
|
||||
/// <summary>
|
||||
/// Drop an AliasRegistry.
|
||||
/// </summary>
|
||||
[DllImport(LibraryName, EntryPoint = "regorus_alias_registry_drop", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
|
||||
internal static extern void regorus_alias_registry_drop(RegorusAliasRegistry* registry);
|
||||
|
||||
/// <summary>
|
||||
/// Load control-plane alias data (array of ProviderAliases) into the registry.
|
||||
/// </summary>
|
||||
[DllImport(LibraryName, EntryPoint = "regorus_alias_registry_load_json", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
|
||||
internal static extern RegorusResult regorus_alias_registry_load_json(RegorusAliasRegistry* registry, byte* json);
|
||||
|
||||
/// <summary>
|
||||
/// Load a data-plane policy manifest into the registry.
|
||||
/// </summary>
|
||||
[DllImport(LibraryName, EntryPoint = "regorus_alias_registry_load_manifest", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
|
||||
internal static extern RegorusResult regorus_alias_registry_load_manifest(RegorusAliasRegistry* registry, byte* json);
|
||||
|
||||
/// <summary>
|
||||
/// Return the number of resource types loaded in the alias registry.
|
||||
/// </summary>
|
||||
[DllImport(LibraryName, EntryPoint = "regorus_alias_registry_len", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
|
||||
internal static extern RegorusResult regorus_alias_registry_len(RegorusAliasRegistry* registry);
|
||||
|
||||
/// <summary>
|
||||
/// Normalize an ARM resource JSON and wrap it into the standard input envelope.
|
||||
/// Returns a JSON string.
|
||||
/// </summary>
|
||||
[DllImport(LibraryName, EntryPoint = "regorus_alias_registry_normalize_and_wrap", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
|
||||
internal static extern RegorusResult regorus_alias_registry_normalize_and_wrap(
|
||||
RegorusAliasRegistry* registry, byte* resource_json, byte* api_version, byte* context_json, byte* parameters_json);
|
||||
|
||||
/// <summary>
|
||||
/// Denormalize a previously-normalized resource JSON back to ARM format.
|
||||
/// </summary>
|
||||
[DllImport(LibraryName, EntryPoint = "regorus_alias_registry_denormalize", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)]
|
||||
internal static extern RegorusResult regorus_alias_registry_denormalize(
|
||||
RegorusAliasRegistry* registry, byte* normalized_json, byte* api_version);
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
#region Native Structures
|
||||
@@ -874,5 +923,13 @@ namespace Regorus.Internal
|
||||
public byte* content;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wrapper for AliasRegistry.
|
||||
/// </summary>
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal unsafe partial struct RegorusAliasRegistry
|
||||
{
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -183,4 +183,52 @@ namespace Regorus
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class RegorusAliasRegistryHandle : SafeHandleZeroOrMinusOneIsInvalid
|
||||
{
|
||||
private RegorusAliasRegistryHandle() : base(ownsHandle: true)
|
||||
{
|
||||
}
|
||||
|
||||
internal static RegorusAliasRegistryHandle Create()
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
var raw = Internal.API.regorus_alias_registry_new();
|
||||
if (raw is null)
|
||||
{
|
||||
throw new InvalidOperationException("Failed to create Regorus alias registry.");
|
||||
}
|
||||
|
||||
var handle = new RegorusAliasRegistryHandle();
|
||||
handle.SetHandle((IntPtr)raw);
|
||||
return handle;
|
||||
}
|
||||
}
|
||||
|
||||
internal static RegorusAliasRegistryHandle FromPointer(IntPtr pointer)
|
||||
{
|
||||
if (pointer == IntPtr.Zero)
|
||||
{
|
||||
throw new ArgumentException("Pointer cannot be zero.", nameof(pointer));
|
||||
}
|
||||
|
||||
var handle = new RegorusAliasRegistryHandle();
|
||||
handle.SetHandle(pointer);
|
||||
return handle;
|
||||
}
|
||||
|
||||
protected override bool ReleaseHandle()
|
||||
{
|
||||
if (!IsInvalid)
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
Internal.API.regorus_alias_registry_drop((Internal.RegorusAliasRegistry*)handle);
|
||||
}
|
||||
SetHandle(IntPtr.Zero);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
141
bindings/ffi/Cargo.lock
generated
141
bindings/ffi/Cargo.lock
generated
@@ -57,9 +57,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.13"
|
||||
version = "1.0.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
|
||||
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
@@ -119,9 +119,9 @@ checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.10.0"
|
||||
version = "2.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
|
||||
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
|
||||
|
||||
[[package]]
|
||||
name = "borrow-or-share"
|
||||
@@ -141,9 +141,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.19.1"
|
||||
version = "3.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
|
||||
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
|
||||
|
||||
[[package]]
|
||||
name = "bytecount"
|
||||
@@ -172,9 +172,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.55"
|
||||
version = "1.2.58"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29"
|
||||
checksum = "e1e928d4b69e3077709075a938a05ffbedfa53a84c8f766efbf8220bb1ff60e1"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"shlex",
|
||||
@@ -258,9 +258,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.4"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
|
||||
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation-sys"
|
||||
@@ -672,15 +672,15 @@ checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.17"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.85"
|
||||
version = "0.3.91"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3"
|
||||
checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"wasm-bindgen",
|
||||
@@ -727,15 +727,15 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.180"
|
||||
version = "0.2.183"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
|
||||
checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.11.0"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
|
||||
checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
|
||||
|
||||
[[package]]
|
||||
name = "litemap"
|
||||
@@ -766,9 +766,9 @@ checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.6"
|
||||
version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
|
||||
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||
|
||||
[[package]]
|
||||
name = "msvc_spectre_libs"
|
||||
@@ -860,9 +860,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell_polyfill"
|
||||
@@ -967,9 +967,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.44"
|
||||
version = "1.0.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
|
||||
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
@@ -1061,9 +1061,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.13"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c"
|
||||
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
@@ -1072,9 +1072,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.8"
|
||||
version = "0.8.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
|
||||
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
||||
|
||||
[[package]]
|
||||
name = "regorus"
|
||||
@@ -1086,6 +1086,7 @@ dependencies = [
|
||||
"dashmap",
|
||||
"data-encoding",
|
||||
"globset",
|
||||
"hashbrown 0.16.1",
|
||||
"icu_casemap",
|
||||
"indexmap",
|
||||
"ipnet",
|
||||
@@ -1137,9 +1138,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "rustix"
|
||||
version = "1.1.3"
|
||||
version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34"
|
||||
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"errno",
|
||||
@@ -1156,9 +1157,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.22"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984"
|
||||
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
@@ -1217,9 +1218,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde_spanned"
|
||||
version = "1.0.4"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776"
|
||||
checksum = "876ac351060d4f882bb1032b6369eb0aef79ad9df1ea8bc404874d8cc3d0cd98"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
]
|
||||
@@ -1275,9 +1276,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.114"
|
||||
version = "2.0.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a"
|
||||
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -1297,12 +1298,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.24.0"
|
||||
version = "3.27.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c"
|
||||
checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"getrandom 0.3.4",
|
||||
"getrandom 0.4.2",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys",
|
||||
@@ -1341,9 +1342,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.9.11+spec-1.1.0"
|
||||
version = "0.9.12+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f3afc9a848309fe1aaffaed6e1546a7a14de1f935dc9d89d32afd9a44bab7c46"
|
||||
checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"serde_core",
|
||||
@@ -1351,7 +1352,7 @@ dependencies = [
|
||||
"toml_datetime",
|
||||
"toml_parser",
|
||||
"toml_writer",
|
||||
"winnow",
|
||||
"winnow 0.7.15",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1365,18 +1366,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "toml_parser"
|
||||
version = "1.0.6+spec-1.1.0"
|
||||
version = "1.1.0+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44"
|
||||
checksum = "2334f11ee363607eb04df9b8fc8a13ca1715a72ba8662a26ac285c98aabb4011"
|
||||
dependencies = [
|
||||
"winnow",
|
||||
"winnow 1.0.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "toml_writer"
|
||||
version = "1.0.6+spec-1.1.0"
|
||||
version = "1.1.0+spec-1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607"
|
||||
checksum = "d282ade6016312faf3e41e57ebbba0c073e4056dab1232ab1cb624199648f8ed"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-general-category"
|
||||
@@ -1386,9 +1387,9 @@ checksum = "0b993bddc193ae5bd0d623b49ec06ac3e9312875fdae725a975c51db1cc1677f"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.22"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
@@ -1428,9 +1429,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.22.0"
|
||||
version = "1.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37"
|
||||
checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9"
|
||||
dependencies = [
|
||||
"getrandom 0.4.2",
|
||||
"rand",
|
||||
@@ -1478,9 +1479,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.108"
|
||||
version = "0.2.114"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566"
|
||||
checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
@@ -1491,9 +1492,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.108"
|
||||
version = "0.2.114"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608"
|
||||
checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
@@ -1501,9 +1502,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.108"
|
||||
version = "0.2.114"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55"
|
||||
checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
@@ -1514,9 +1515,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.108"
|
||||
version = "0.2.114"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12"
|
||||
checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
@@ -1625,9 +1626,15 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "0.7.14"
|
||||
version = "0.7.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829"
|
||||
checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8"
|
||||
|
||||
[[package]]
|
||||
name = "wit-bindgen"
|
||||
@@ -1748,18 +1755,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.36"
|
||||
version = "0.8.47"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dafd85c832c1b68bbb4ec0c72c7f6f4fc5179627d2bc7c26b30e4c0cc11e76cc"
|
||||
checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.8.36"
|
||||
version = "0.8.47"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7cb7e4e8436d9db52fbd6625dbf2f45243ab84994a72882ec8227b99e72b439a"
|
||||
checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -1823,6 +1830,6 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.17"
|
||||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02aae0f83f69aafc94776e879363e9771d7ecbffe2c7fbb6c14c5e00dfe88439"
|
||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||
|
||||
479
bindings/ffi/src/alias_registry.rs
Normal file
479
bindings/ffi/src/alias_registry.rs
Normal file
@@ -0,0 +1,479 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
//! FFI bindings for `AliasRegistry` – Azure Policy alias catalog management.
|
||||
|
||||
#![cfg(feature = "azure_policy")]
|
||||
|
||||
use crate::common::{from_c_str, to_ref, RegorusResult, RegorusStatus};
|
||||
use crate::panic_guard::with_unwind_guard;
|
||||
|
||||
use alloc::boxed::Box;
|
||||
use alloc::format;
|
||||
use alloc::string::String;
|
||||
use anyhow::Result;
|
||||
use core::ffi::c_char;
|
||||
use core::ptr;
|
||||
|
||||
use regorus::languages::azure_policy::aliases::AliasRegistry;
|
||||
|
||||
/// Opaque wrapper for `AliasRegistry`.
|
||||
pub struct RegorusAliasRegistry {
|
||||
registry: AliasRegistry,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Lifecycle
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Create a new, empty `AliasRegistry`.
|
||||
///
|
||||
/// The caller must eventually call `regorus_alias_registry_drop` to free the handle.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn regorus_alias_registry_new() -> *mut RegorusAliasRegistry {
|
||||
let wrapper = RegorusAliasRegistry {
|
||||
registry: AliasRegistry::new(),
|
||||
};
|
||||
Box::into_raw(Box::new(wrapper))
|
||||
}
|
||||
|
||||
/// Drop a `RegorusAliasRegistry`.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn regorus_alias_registry_drop(registry: *mut RegorusAliasRegistry) {
|
||||
if let Ok(r) = to_ref(registry) {
|
||||
unsafe {
|
||||
let _ = Box::from_raw(ptr::from_mut(r));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Loading
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Load control-plane alias data (array of `ProviderAliases`) into the registry.
|
||||
///
|
||||
/// `json` must be a valid null-terminated UTF-8 string containing the JSON
|
||||
/// array returned by `Get-AzPolicyAlias` or the static
|
||||
/// `ResourceTypesAndAliases.json` file.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn regorus_alias_registry_load_json(
|
||||
registry: *mut RegorusAliasRegistry,
|
||||
json: *const c_char,
|
||||
) -> RegorusResult {
|
||||
with_unwind_guard(|| {
|
||||
let output = || -> Result<()> {
|
||||
let json_str = from_c_str(json)?;
|
||||
to_ref(registry)?.registry.load_from_json(&json_str)?;
|
||||
Ok(())
|
||||
}();
|
||||
|
||||
match output {
|
||||
Ok(()) => RegorusResult::ok_void(),
|
||||
Err(e) => RegorusResult::err_with_message(
|
||||
RegorusStatus::InvalidDataFormat,
|
||||
format!("Failed to load alias catalog: {e}"),
|
||||
),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Load a data-plane policy manifest into the registry.
|
||||
///
|
||||
/// `json` must be a valid null-terminated UTF-8 string containing a single
|
||||
/// `DataPolicyManifest` JSON object.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn regorus_alias_registry_load_manifest(
|
||||
registry: *mut RegorusAliasRegistry,
|
||||
json: *const c_char,
|
||||
) -> RegorusResult {
|
||||
with_unwind_guard(|| {
|
||||
let output = || -> Result<()> {
|
||||
let json_str = from_c_str(json)?;
|
||||
to_ref(registry)?
|
||||
.registry
|
||||
.load_data_policy_manifest_json(&json_str)?;
|
||||
Ok(())
|
||||
}();
|
||||
|
||||
match output {
|
||||
Ok(()) => RegorusResult::ok_void(),
|
||||
Err(e) => RegorusResult::err_with_message(
|
||||
RegorusStatus::InvalidDataFormat,
|
||||
format!("Failed to load data-plane manifest: {e}"),
|
||||
),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Queries
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Return the number of resource types loaded in the alias registry.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn regorus_alias_registry_len(registry: *mut RegorusAliasRegistry) -> RegorusResult {
|
||||
with_unwind_guard(|| {
|
||||
let output = || -> Result<i64> {
|
||||
let len = to_ref(registry)?.registry.len();
|
||||
Ok(len as i64)
|
||||
}();
|
||||
|
||||
match output {
|
||||
Ok(n) => RegorusResult::ok_int(n),
|
||||
Err(e) => RegorusResult::err_with_message(RegorusStatus::Error, format!("{e}")),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Normalize / Denormalize
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Normalize an ARM resource JSON and wrap it into the standard input envelope.
|
||||
///
|
||||
/// Returns a JSON string:
|
||||
/// `{ "resource": <normalized>, "context": <context>, "parameters": <params> }`.
|
||||
///
|
||||
/// * `resource_json` – raw ARM resource JSON
|
||||
/// * `api_version` – API version string (e.g. `"2023-01-01"`), or null to use
|
||||
/// the default alias paths
|
||||
/// * `context_json` – JSON object for additional context (pass `"{}"` if none)
|
||||
/// * `parameters_json` – JSON object of policy parameter values (pass `"{}"` if none)
|
||||
#[no_mangle]
|
||||
pub extern "C" fn regorus_alias_registry_normalize_and_wrap(
|
||||
registry: *mut RegorusAliasRegistry,
|
||||
resource_json: *const c_char,
|
||||
api_version: *const c_char,
|
||||
context_json: *const c_char,
|
||||
parameters_json: *const c_char,
|
||||
) -> RegorusResult {
|
||||
with_unwind_guard(|| {
|
||||
let output = || -> Result<String> {
|
||||
let resource_str = from_c_str(resource_json)?;
|
||||
let api_ver = if api_version.is_null() {
|
||||
None
|
||||
} else {
|
||||
let s = from_c_str(api_version)?;
|
||||
if s.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(s)
|
||||
}
|
||||
};
|
||||
let context_str = from_c_str(context_json)?;
|
||||
let params_str = from_c_str(parameters_json)?;
|
||||
|
||||
let resource = regorus::Value::from_json_str(&resource_str)?;
|
||||
let context = regorus::Value::from_json_str(&context_str)?;
|
||||
let params = regorus::Value::from_json_str(¶ms_str)?;
|
||||
|
||||
let wrapped = to_ref(registry)?.registry.normalize_and_wrap(
|
||||
&resource,
|
||||
api_ver.as_deref(),
|
||||
Some(context),
|
||||
Some(params),
|
||||
);
|
||||
wrapped.to_json_str()
|
||||
}();
|
||||
|
||||
match output {
|
||||
Ok(s) => RegorusResult::ok_string(s),
|
||||
Err(e) => RegorusResult::err_with_message(RegorusStatus::Error, format!("{e}")),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Denormalize a previously-normalized resource JSON back to ARM format.
|
||||
///
|
||||
/// * `normalized_json` – the normalized resource JSON
|
||||
/// * `api_version` – API version string, or null to use the default alias paths
|
||||
///
|
||||
/// Returns the denormalized ARM JSON string.
|
||||
#[no_mangle]
|
||||
pub extern "C" fn regorus_alias_registry_denormalize(
|
||||
registry: *mut RegorusAliasRegistry,
|
||||
normalized_json: *const c_char,
|
||||
api_version: *const c_char,
|
||||
) -> RegorusResult {
|
||||
with_unwind_guard(|| {
|
||||
let output = || -> Result<String> {
|
||||
let normalized_str = from_c_str(normalized_json)?;
|
||||
let api_ver = if api_version.is_null() {
|
||||
None
|
||||
} else {
|
||||
let s = from_c_str(api_version)?;
|
||||
if s.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(s)
|
||||
}
|
||||
};
|
||||
|
||||
let normalized = regorus::Value::from_json_str(&normalized_str)?;
|
||||
|
||||
let result = to_ref(registry)?
|
||||
.registry
|
||||
.denormalize(&normalized, api_ver.as_deref());
|
||||
result.to_json_str()
|
||||
}();
|
||||
|
||||
match output {
|
||||
Ok(s) => RegorusResult::ok_string(s),
|
||||
Err(e) => RegorusResult::err_with_message(RegorusStatus::Error, format!("{e}")),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::common::regorus_result_drop;
|
||||
use core::ffi::CStr;
|
||||
use std::ffi::CString;
|
||||
|
||||
/// Helper: create a C string from a Rust &str.
|
||||
fn c(s: &str) -> CString {
|
||||
CString::new(s).expect("CString::new failed")
|
||||
}
|
||||
|
||||
/// Helper: assert a RegorusResult has Ok status and extract string output.
|
||||
fn assert_ok_string(r: &RegorusResult) -> String {
|
||||
assert_eq!(r.status, RegorusStatus::Ok, "expected Ok status");
|
||||
assert!(!r.output.is_null(), "expected non-null output");
|
||||
let s = unsafe { CStr::from_ptr(r.output) }
|
||||
.to_str()
|
||||
.expect("invalid UTF-8 in output")
|
||||
.to_string();
|
||||
s
|
||||
}
|
||||
|
||||
/// Helper: assert a RegorusResult has Ok status with integer output.
|
||||
fn assert_ok_int(r: &RegorusResult) -> i64 {
|
||||
assert_eq!(r.status, RegorusStatus::Ok, "expected Ok status");
|
||||
r.int_value
|
||||
}
|
||||
|
||||
const ALIASES: &str = r#"[{
|
||||
"namespace": "Microsoft.Storage",
|
||||
"resourceTypes": [{
|
||||
"resourceType": "storageAccounts",
|
||||
"aliases": [{
|
||||
"name": "Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly",
|
||||
"defaultPath": "properties.supportsHttpsTrafficOnly",
|
||||
"paths": []
|
||||
}]
|
||||
}]
|
||||
}]"#;
|
||||
|
||||
const MANIFEST: &str = r#"{
|
||||
"dataNamespace": "Microsoft.KeyVault.Data",
|
||||
"aliases": [],
|
||||
"resourceTypeAliases": [{
|
||||
"resourceType": "vaults/certificates",
|
||||
"aliases": [{
|
||||
"name": "Microsoft.KeyVault.Data/vaults/certificates/keySize",
|
||||
"paths": [{ "path": "keySize", "apiVersions": ["7.0"] }]
|
||||
}]
|
||||
}]
|
||||
}"#;
|
||||
|
||||
#[test]
|
||||
fn lifecycle_new_and_drop() {
|
||||
let reg = regorus_alias_registry_new();
|
||||
assert!(!reg.is_null());
|
||||
regorus_alias_registry_drop(reg);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_json_and_check_len() {
|
||||
let reg = regorus_alias_registry_new();
|
||||
let json = c(ALIASES);
|
||||
|
||||
let r = regorus_alias_registry_load_json(reg, json.as_ptr());
|
||||
assert_eq!(r.status, RegorusStatus::Ok);
|
||||
regorus_result_drop(r);
|
||||
|
||||
let r = regorus_alias_registry_len(reg);
|
||||
assert_eq!(assert_ok_int(&r), 1);
|
||||
regorus_result_drop(r);
|
||||
|
||||
regorus_alias_registry_drop(reg);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_manifest_and_check_len() {
|
||||
let reg = regorus_alias_registry_new();
|
||||
let json = c(MANIFEST);
|
||||
|
||||
let r = regorus_alias_registry_load_manifest(reg, json.as_ptr());
|
||||
assert_eq!(r.status, RegorusStatus::Ok);
|
||||
regorus_result_drop(r);
|
||||
|
||||
let r = regorus_alias_registry_len(reg);
|
||||
assert_eq!(assert_ok_int(&r), 1);
|
||||
regorus_result_drop(r);
|
||||
|
||||
regorus_alias_registry_drop(reg);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_invalid_json_returns_error() {
|
||||
let reg = regorus_alias_registry_new();
|
||||
let bad = c("not valid json");
|
||||
|
||||
let r = regorus_alias_registry_load_json(reg, bad.as_ptr());
|
||||
assert_ne!(r.status, RegorusStatus::Ok);
|
||||
regorus_result_drop(r);
|
||||
|
||||
regorus_alias_registry_drop(reg);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_and_wrap_round_trip() {
|
||||
let reg = regorus_alias_registry_new();
|
||||
let aliases = c(ALIASES);
|
||||
let r = regorus_alias_registry_load_json(reg, aliases.as_ptr());
|
||||
assert_eq!(r.status, RegorusStatus::Ok);
|
||||
regorus_result_drop(r);
|
||||
|
||||
let resource = c(r#"{
|
||||
"name": "acct1",
|
||||
"type": "Microsoft.Storage/storageAccounts",
|
||||
"properties": { "supportsHttpsTrafficOnly": true }
|
||||
}"#);
|
||||
let api = c("2023-01-01");
|
||||
let ctx = c(r#"{"resourceGroup": {"name": "rg1"}}"#);
|
||||
let params = c(r#"{"env": "prod"}"#);
|
||||
|
||||
// Normalize
|
||||
let r = regorus_alias_registry_normalize_and_wrap(
|
||||
reg,
|
||||
resource.as_ptr(),
|
||||
api.as_ptr(),
|
||||
ctx.as_ptr(),
|
||||
params.as_ptr(),
|
||||
);
|
||||
let envelope_json = assert_ok_string(&r);
|
||||
regorus_result_drop(r);
|
||||
|
||||
// Parse and verify structure
|
||||
let envelope: serde_json::Value =
|
||||
serde_json::from_str(&envelope_json).expect("invalid JSON output");
|
||||
assert!(
|
||||
envelope.get("resource").is_some(),
|
||||
"envelope missing 'resource'"
|
||||
);
|
||||
assert!(
|
||||
envelope.get("parameters").is_some(),
|
||||
"envelope missing 'parameters'"
|
||||
);
|
||||
assert!(
|
||||
envelope.get("context").is_some(),
|
||||
"envelope missing 'context'"
|
||||
);
|
||||
|
||||
// The normalized resource should have lowercased alias fields
|
||||
let res = &envelope["resource"];
|
||||
assert_eq!(res["supportshttpstrafficonly"], true);
|
||||
assert_eq!(res["name"], "acct1");
|
||||
|
||||
// Context and parameters should be passed through
|
||||
assert_eq!(envelope["context"]["resourceGroup"]["name"], "rg1");
|
||||
assert_eq!(envelope["parameters"]["env"], "prod");
|
||||
|
||||
// Denormalize the resource portion
|
||||
let resource_json = serde_json::to_string(&res).expect("serialize resource");
|
||||
let norm_cstr = c(&resource_json);
|
||||
|
||||
let r = regorus_alias_registry_denormalize(reg, norm_cstr.as_ptr(), api.as_ptr());
|
||||
let denorm_json = assert_ok_string(&r);
|
||||
regorus_result_drop(r);
|
||||
|
||||
let denorm: serde_json::Value =
|
||||
serde_json::from_str(&denorm_json).expect("invalid denorm JSON");
|
||||
// Should be back under properties with restored casing
|
||||
assert_eq!(
|
||||
denorm["properties"]["supportsHttpsTrafficOnly"], true,
|
||||
"expected restored casing under properties"
|
||||
);
|
||||
|
||||
regorus_alias_registry_drop(reg);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn denormalize_invalid_json_returns_error() {
|
||||
let reg = regorus_alias_registry_new();
|
||||
let aliases = c(ALIASES);
|
||||
let r = regorus_alias_registry_load_json(reg, aliases.as_ptr());
|
||||
assert_eq!(r.status, RegorusStatus::Ok);
|
||||
regorus_result_drop(r);
|
||||
|
||||
let bad = c("not json");
|
||||
let api = c("2023-01-01");
|
||||
let r = regorus_alias_registry_denormalize(reg, bad.as_ptr(), api.as_ptr());
|
||||
assert_ne!(r.status, RegorusStatus::Ok);
|
||||
regorus_result_drop(r);
|
||||
|
||||
regorus_alias_registry_drop(reg);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn normalize_data_plane_manifest() {
|
||||
let reg = regorus_alias_registry_new();
|
||||
let manifest = c(MANIFEST);
|
||||
let r = regorus_alias_registry_load_manifest(reg, manifest.as_ptr());
|
||||
assert_eq!(r.status, RegorusStatus::Ok);
|
||||
regorus_result_drop(r);
|
||||
|
||||
let resource = c(r#"{
|
||||
"type": "Microsoft.KeyVault.Data/vaults/certificates",
|
||||
"keySize": 2048
|
||||
}"#);
|
||||
let api = c("7.0");
|
||||
let ctx = c("{}");
|
||||
let params = c("{}");
|
||||
|
||||
let r = regorus_alias_registry_normalize_and_wrap(
|
||||
reg,
|
||||
resource.as_ptr(),
|
||||
api.as_ptr(),
|
||||
ctx.as_ptr(),
|
||||
params.as_ptr(),
|
||||
);
|
||||
let envelope_json = assert_ok_string(&r);
|
||||
regorus_result_drop(r);
|
||||
|
||||
let envelope: serde_json::Value =
|
||||
serde_json::from_str(&envelope_json).expect("invalid JSON output");
|
||||
assert_eq!(envelope["resource"]["keysize"], 2048);
|
||||
|
||||
regorus_alias_registry_drop(reg);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_registry_normalize() {
|
||||
let reg = regorus_alias_registry_new();
|
||||
let resource = c(r#"{"name": "test", "type": "Unknown/type", "properties": {"foo": 1}}"#);
|
||||
let api = c("");
|
||||
let ctx = c("{}");
|
||||
let params = c("{}");
|
||||
|
||||
let r = regorus_alias_registry_normalize_and_wrap(
|
||||
reg,
|
||||
resource.as_ptr(),
|
||||
api.as_ptr(),
|
||||
ctx.as_ptr(),
|
||||
params.as_ptr(),
|
||||
);
|
||||
let json = assert_ok_string(&r);
|
||||
regorus_result_drop(r);
|
||||
|
||||
let envelope: serde_json::Value = serde_json::from_str(&json).expect("invalid JSON");
|
||||
// Without aliases, properties should still be flattened
|
||||
assert_eq!(envelope["resource"]["foo"], 1);
|
||||
assert_eq!(envelope["resource"]["name"], "test");
|
||||
|
||||
regorus_alias_registry_drop(reg);
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ use core::ffi::{c_char, c_longlong, c_void, CStr};
|
||||
use core::{mem, ptr};
|
||||
|
||||
/// Status of a call on `RegorusEngine`.
|
||||
#[derive(Debug, PartialEq)]
|
||||
#[repr(C)]
|
||||
pub enum RegorusStatus {
|
||||
/// The operation was successful.
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
extern crate alloc;
|
||||
|
||||
mod alias_registry;
|
||||
mod allocator;
|
||||
mod common;
|
||||
mod compile;
|
||||
|
||||
88
bindings/java/Cargo.lock
generated
88
bindings/java/Cargo.lock
generated
@@ -69,9 +69,9 @@ checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.10.0"
|
||||
version = "2.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
|
||||
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
|
||||
|
||||
[[package]]
|
||||
name = "borrow-or-share"
|
||||
@@ -91,9 +91,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.19.1"
|
||||
version = "3.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
|
||||
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
|
||||
|
||||
[[package]]
|
||||
name = "bytecount"
|
||||
@@ -109,9 +109,9 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.55"
|
||||
version = "1.2.58"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29"
|
||||
checksum = "e1e928d4b69e3077709075a938a05ffbedfa53a84c8f766efbf8220bb1ff60e1"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"shlex",
|
||||
@@ -512,9 +512,9 @@ checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.17"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "jni"
|
||||
@@ -567,9 +567,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.85"
|
||||
version = "0.3.91"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3"
|
||||
checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"wasm-bindgen",
|
||||
@@ -616,9 +616,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.180"
|
||||
version = "0.2.183"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
|
||||
checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
|
||||
|
||||
[[package]]
|
||||
name = "litemap"
|
||||
@@ -649,9 +649,9 @@ checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.6"
|
||||
version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
|
||||
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||
|
||||
[[package]]
|
||||
name = "msvc_spectre_libs"
|
||||
@@ -743,9 +743,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "outref"
|
||||
@@ -842,9 +842,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.44"
|
||||
version = "1.0.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
|
||||
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
@@ -936,9 +936,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.13"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c"
|
||||
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
@@ -947,9 +947,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.8"
|
||||
version = "0.8.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
|
||||
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
||||
|
||||
[[package]]
|
||||
name = "regorus"
|
||||
@@ -1024,9 +1024,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.22"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984"
|
||||
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
@@ -1153,9 +1153,9 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.114"
|
||||
version = "2.0.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a"
|
||||
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -1211,9 +1211,9 @@ checksum = "0b993bddc193ae5bd0d623b49ec06ac3e9312875fdae725a975c51db1cc1677f"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.22"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
@@ -1247,9 +1247,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.22.0"
|
||||
version = "1.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37"
|
||||
checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9"
|
||||
dependencies = [
|
||||
"getrandom 0.4.2",
|
||||
"rand",
|
||||
@@ -1307,9 +1307,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.108"
|
||||
version = "0.2.114"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566"
|
||||
checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
@@ -1320,9 +1320,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.108"
|
||||
version = "0.2.114"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608"
|
||||
checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
@@ -1330,9 +1330,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.108"
|
||||
version = "0.2.114"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55"
|
||||
checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
@@ -1343,9 +1343,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.108"
|
||||
version = "0.2.114"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12"
|
||||
checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
@@ -1580,18 +1580,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.36"
|
||||
version = "0.8.47"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dafd85c832c1b68bbb4ec0c72c7f6f4fc5179627d2bc7c26b30e4c0cc11e76cc"
|
||||
checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.8.36"
|
||||
version = "0.8.47"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7cb7e4e8436d9db52fbd6625dbf2f45243ab84994a72882ec8227b99e72b439a"
|
||||
checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -1654,6 +1654,6 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.17"
|
||||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02aae0f83f69aafc94776e879363e9771d7ecbffe2c7fbb6c14c5e00dfe88439"
|
||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||
|
||||
96
bindings/python/Cargo.lock
generated
96
bindings/python/Cargo.lock
generated
@@ -69,9 +69,9 @@ checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.10.0"
|
||||
version = "2.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
|
||||
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
|
||||
|
||||
[[package]]
|
||||
name = "borrow-or-share"
|
||||
@@ -91,9 +91,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.19.1"
|
||||
version = "3.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
|
||||
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
|
||||
|
||||
[[package]]
|
||||
name = "bytecount"
|
||||
@@ -103,9 +103,9 @@ checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.55"
|
||||
version = "1.2.58"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29"
|
||||
checksum = "e1e928d4b69e3077709075a938a05ffbedfa53a84c8f766efbf8220bb1ff60e1"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"shlex",
|
||||
@@ -496,15 +496,15 @@ checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.17"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
version = "0.3.85"
|
||||
version = "0.3.91"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3"
|
||||
checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c"
|
||||
dependencies = [
|
||||
"once_cell",
|
||||
"wasm-bindgen",
|
||||
@@ -551,9 +551,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.180"
|
||||
version = "0.2.183"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
|
||||
checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
|
||||
|
||||
[[package]]
|
||||
name = "litemap"
|
||||
@@ -584,9 +584,9 @@ checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.6"
|
||||
version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
|
||||
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||
|
||||
[[package]]
|
||||
name = "msvc_spectre_libs"
|
||||
@@ -678,9 +678,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "ordered-float"
|
||||
@@ -746,9 +746,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "portable-atomic"
|
||||
version = "1.13.0"
|
||||
version = "1.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950"
|
||||
checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
|
||||
|
||||
[[package]]
|
||||
name = "postcard"
|
||||
@@ -851,9 +851,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.44"
|
||||
version = "1.0.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
|
||||
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
@@ -945,9 +945,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.13"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c"
|
||||
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
@@ -956,9 +956,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.8"
|
||||
version = "0.8.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
|
||||
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
||||
|
||||
[[package]]
|
||||
name = "regorus"
|
||||
@@ -1025,9 +1025,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.22"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984"
|
||||
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
||||
|
||||
[[package]]
|
||||
name = "scopeguard"
|
||||
@@ -1129,9 +1129,9 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.114"
|
||||
version = "2.0.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a"
|
||||
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -1151,9 +1151,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "target-lexicon"
|
||||
version = "0.13.4"
|
||||
version = "0.13.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1dd07eb858a2067e2f3c7155d54e929265c264e6f37efe3ee7a8d1b5a1dd0ba"
|
||||
checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
@@ -1193,9 +1193,9 @@ checksum = "0b993bddc193ae5bd0d623b49ec06ac3e9312875fdae725a975c51db1cc1677f"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.22"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
@@ -1229,9 +1229,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.22.0"
|
||||
version = "1.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37"
|
||||
checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9"
|
||||
dependencies = [
|
||||
"getrandom 0.4.2",
|
||||
"rand",
|
||||
@@ -1279,9 +1279,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen"
|
||||
version = "0.2.108"
|
||||
version = "0.2.114"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566"
|
||||
checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"once_cell",
|
||||
@@ -1292,9 +1292,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.108"
|
||||
version = "0.2.114"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608"
|
||||
checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6"
|
||||
dependencies = [
|
||||
"quote",
|
||||
"wasm-bindgen-macro-support",
|
||||
@@ -1302,9 +1302,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro-support"
|
||||
version = "0.2.108"
|
||||
version = "0.2.114"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55"
|
||||
checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3"
|
||||
dependencies = [
|
||||
"bumpalo",
|
||||
"proc-macro2",
|
||||
@@ -1315,9 +1315,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-shared"
|
||||
version = "0.2.108"
|
||||
version = "0.2.114"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12"
|
||||
checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
@@ -1534,18 +1534,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.36"
|
||||
version = "0.8.47"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dafd85c832c1b68bbb4ec0c72c7f6f4fc5179627d2bc7c26b30e4c0cc11e76cc"
|
||||
checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.8.36"
|
||||
version = "0.8.47"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7cb7e4e8436d9db52fbd6625dbf2f45243ab84994a72882ec8227b99e72b439a"
|
||||
checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -1608,6 +1608,6 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.17"
|
||||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02aae0f83f69aafc94776e879363e9771d7ecbffe2c7fbb6c14c5e00dfe88439"
|
||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||
|
||||
95
bindings/wasm/Cargo.lock
generated
95
bindings/wasm/Cargo.lock
generated
@@ -80,9 +80,9 @@ checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.10.0"
|
||||
version = "2.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
|
||||
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
|
||||
|
||||
[[package]]
|
||||
name = "borrow-or-share"
|
||||
@@ -102,9 +102,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "bumpalo"
|
||||
version = "3.19.1"
|
||||
version = "3.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
|
||||
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
|
||||
|
||||
[[package]]
|
||||
name = "bytecount"
|
||||
@@ -120,9 +120,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5"
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.55"
|
||||
version = "1.2.58"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29"
|
||||
checksum = "e1e928d4b69e3077709075a938a05ffbedfa53a84c8f766efbf8220bb1ff60e1"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"shlex",
|
||||
@@ -297,26 +297,25 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.31"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
|
||||
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.31"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
|
||||
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.31"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
|
||||
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
"pin-project-lite",
|
||||
"pin-utils",
|
||||
"slab",
|
||||
]
|
||||
|
||||
@@ -553,9 +552,9 @@ checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.17"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
||||
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
|
||||
|
||||
[[package]]
|
||||
name = "js-sys"
|
||||
@@ -608,9 +607,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.180"
|
||||
version = "0.2.183"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
|
||||
checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d"
|
||||
|
||||
[[package]]
|
||||
name = "libm"
|
||||
@@ -647,9 +646,9 @@ checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.7.6"
|
||||
version = "2.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
|
||||
checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
|
||||
|
||||
[[package]]
|
||||
name = "minicov"
|
||||
@@ -761,9 +760,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.21.3"
|
||||
version = "1.21.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
|
||||
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
|
||||
|
||||
[[package]]
|
||||
name = "oorandom"
|
||||
@@ -826,15 +825,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.16"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
|
||||
|
||||
[[package]]
|
||||
name = "pin-utils"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||
|
||||
[[package]]
|
||||
name = "postcard"
|
||||
@@ -878,9 +871,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.44"
|
||||
version = "1.0.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4"
|
||||
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
@@ -972,9 +965,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.13"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c"
|
||||
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
@@ -983,9 +976,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.8"
|
||||
version = "0.8.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58"
|
||||
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
||||
|
||||
[[package]]
|
||||
name = "regorus"
|
||||
@@ -1042,9 +1035,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.22"
|
||||
version = "1.0.23"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984"
|
||||
checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
@@ -1148,9 +1141,9 @@ checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e"
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.11"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589"
|
||||
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
@@ -1172,9 +1165,9 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.114"
|
||||
version = "2.0.117"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a"
|
||||
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -1230,9 +1223,9 @@ checksum = "0b993bddc193ae5bd0d623b49ec06ac3e9312875fdae725a975c51db1cc1677f"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.22"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
@@ -1266,9 +1259,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
|
||||
|
||||
[[package]]
|
||||
name = "uuid"
|
||||
version = "1.22.0"
|
||||
version = "1.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37"
|
||||
checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9"
|
||||
dependencies = [
|
||||
"getrandom 0.4.2",
|
||||
"js-sys",
|
||||
@@ -1670,18 +1663,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.36"
|
||||
version = "0.8.47"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dafd85c832c1b68bbb4ec0c72c7f6f4fc5179627d2bc7c26b30e4c0cc11e76cc"
|
||||
checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.8.36"
|
||||
version = "0.8.47"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7cb7e4e8436d9db52fbd6625dbf2f45243ab84994a72882ec8227b99e72b439a"
|
||||
checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -1744,6 +1737,6 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zmij"
|
||||
version = "1.0.17"
|
||||
version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "02aae0f83f69aafc94776e879363e9771d7ecbffe2c7fbb6c14c5e00dfe88439"
|
||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||
|
||||
114
src/languages/azure_policy/aliases/denormalizer/casing.rs
Normal file
114
src/languages/azure_policy/aliases/denormalizer/casing.rs
Normal file
@@ -0,0 +1,114 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
//! Key casing restoration from alias metadata.
|
||||
|
||||
use alloc::collections::BTreeMap;
|
||||
use alloc::string::{String, ToString as _};
|
||||
use alloc::vec::Vec;
|
||||
|
||||
use crate::Value;
|
||||
|
||||
use super::super::obj_map::{make_array, make_value, new_map, obj_insert, val_str, ROOT_FIELDS};
|
||||
use super::super::types::ResolvedEntry;
|
||||
|
||||
fn insert_default_casing(map: &mut BTreeMap<String, String>) {
|
||||
for &field in ROOT_FIELDS {
|
||||
map.insert(field.to_ascii_lowercase(), field.to_string());
|
||||
}
|
||||
|
||||
// Canonical casing for standard nested root-field object members that are
|
||||
// not described by alias metadata but still need round-trip restoration.
|
||||
for canonical in [
|
||||
"principalId",
|
||||
"tenantId",
|
||||
"userAssignedIdentities",
|
||||
"promotionCode",
|
||||
"createdBy",
|
||||
"createdByType",
|
||||
"createdAt",
|
||||
"lastModifiedBy",
|
||||
"lastModifiedByType",
|
||||
"lastModifiedAt",
|
||||
] {
|
||||
map.entry(canonical.to_ascii_lowercase())
|
||||
.or_insert_with(|| canonical.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
/// Build the default casing map used when alias metadata is unavailable.
|
||||
pub fn default_casing_map() -> BTreeMap<String, String> {
|
||||
let mut map = BTreeMap::new();
|
||||
insert_default_casing(&mut map);
|
||||
map
|
||||
}
|
||||
|
||||
/// Build a mapping from lowercase key → original-cased key from alias entries.
|
||||
pub fn build_casing_map(entries: &BTreeMap<String, ResolvedEntry>) -> BTreeMap<String, String> {
|
||||
let mut map = BTreeMap::new();
|
||||
insert_default_casing(&mut map);
|
||||
|
||||
for entry in entries.values() {
|
||||
for segment in entry.short_name.split('.') {
|
||||
let clean = segment.replace("[*]", "");
|
||||
if !clean.is_empty() {
|
||||
map.entry(clean.to_ascii_lowercase())
|
||||
.or_insert_with(|| clean.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
for segment in entry.default_path.split('.') {
|
||||
let clean = segment.replace("[*]", "");
|
||||
if !clean.is_empty() && !clean.eq_ignore_ascii_case("properties") {
|
||||
map.entry(clean.to_ascii_lowercase())
|
||||
.or_insert_with(|| clean.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// Also include segments from all version-specific ARM paths so
|
||||
// casing can be restored correctly for versioned aliases.
|
||||
for (_ver, path) in &entry.versioned_paths {
|
||||
for segment in path.split('.') {
|
||||
let clean = segment.replace("[*]", "");
|
||||
if !clean.is_empty() && !clean.eq_ignore_ascii_case("properties") {
|
||||
map.entry(clean.to_ascii_lowercase())
|
||||
.or_insert_with(|| clean.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
map
|
||||
}
|
||||
|
||||
/// Restore the original casing of a key using the casing map.
|
||||
pub fn restore_casing(key: &str, casing_map: &BTreeMap<String, String>) -> String {
|
||||
casing_map
|
||||
.get(&key.to_ascii_lowercase())
|
||||
.cloned()
|
||||
.unwrap_or_else(|| key.to_string())
|
||||
}
|
||||
|
||||
/// Recursively restore key casing in a JSON value.
|
||||
pub fn denormalize_value(value: &Value, casing_map: &BTreeMap<String, String>) -> Value {
|
||||
match value {
|
||||
Value::Object(obj) => {
|
||||
let mut result = new_map();
|
||||
for (k, v) in obj.iter() {
|
||||
if let Some(key_s) = val_str(k) {
|
||||
let restored_key = restore_casing(key_s, casing_map);
|
||||
obj_insert(&mut result, &restored_key, denormalize_value(v, casing_map));
|
||||
}
|
||||
}
|
||||
make_value(result)
|
||||
}
|
||||
Value::Array(arr) => {
|
||||
let items: Vec<Value> = arr
|
||||
.iter()
|
||||
.map(|v| denormalize_value(v, casing_map))
|
||||
.collect();
|
||||
make_array(items)
|
||||
}
|
||||
_ => value.clone(),
|
||||
}
|
||||
}
|
||||
13
src/languages/azure_policy/aliases/denormalizer/helpers.rs
Normal file
13
src/languages/azure_policy/aliases/denormalizer/helpers.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
//! Small helper functions used by the denormalizer.
|
||||
|
||||
use crate::Rc;
|
||||
|
||||
use super::super::obj_map::ObjMap;
|
||||
|
||||
/// Find a key in an ObjMap using case-insensitive comparison.
|
||||
pub fn find_key_ci(obj: &ObjMap, key: &str) -> Option<Rc<str>> {
|
||||
obj.keys().find(|k| k.eq_ignore_ascii_case(key)).cloned()
|
||||
}
|
||||
227
src/languages/azure_policy/aliases/denormalizer/mod.rs
Normal file
227
src/languages/azure_policy/aliases/denormalizer/mod.rs
Normal file
@@ -0,0 +1,227 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
//! Normalized `input.resource` → ARM JSON reverse transformation.
|
||||
|
||||
mod casing;
|
||||
pub(crate) mod helpers;
|
||||
mod sub_resource;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
use alloc::collections::BTreeSet;
|
||||
|
||||
use crate::Value;
|
||||
|
||||
use crate::Rc;
|
||||
|
||||
use super::obj_map::{
|
||||
extract_type_field, is_root_field_collision, make_value, new_map, obj_insert,
|
||||
set_nested_verbatim, val_str, ROOT_FIELDS,
|
||||
};
|
||||
use super::types::ResolvedAliases;
|
||||
use super::AliasRegistry;
|
||||
|
||||
use super::normalizer::{apply_element_remap, ElementRemap};
|
||||
|
||||
use casing::{build_casing_map, default_casing_map, denormalize_value, restore_casing};
|
||||
use helpers::find_key_ci;
|
||||
|
||||
use super::obj_map::remove_element_field;
|
||||
|
||||
/// Denormalize a normalized resource back to ARM JSON structure.
|
||||
pub fn denormalize(
|
||||
normalized: &Value,
|
||||
registry: Option<&AliasRegistry>,
|
||||
api_version: Option<&str>,
|
||||
) -> Value {
|
||||
let aliases = registry.and_then(|r| extract_type_field(normalized).and_then(|rt| r.get(rt)));
|
||||
denormalize_with_aliases(normalized, aliases, api_version)
|
||||
}
|
||||
|
||||
/// Internal denormalization with pre-resolved alias data.
|
||||
pub fn denormalize_with_aliases(
|
||||
normalized: &Value,
|
||||
aliases: Option<&ResolvedAliases>,
|
||||
api_version: Option<&str>,
|
||||
) -> Value {
|
||||
let obj = match normalized.as_object() {
|
||||
Ok(o) => o,
|
||||
Err(_) => return normalized.clone(),
|
||||
};
|
||||
|
||||
let entries = aliases.map(|a| &a.entries);
|
||||
let casing_map = entries
|
||||
.map(build_casing_map)
|
||||
.unwrap_or_else(default_casing_map);
|
||||
let empty_set = BTreeSet::new();
|
||||
let sub_resource_set = aliases.map_or(&empty_set, |a| &a.sub_resource_arrays);
|
||||
|
||||
let is_data_plane =
|
||||
extract_type_field(normalized).is_some_and(|t| t.to_ascii_lowercase().contains(".data/"));
|
||||
|
||||
let mut result = new_map();
|
||||
let mut properties = new_map();
|
||||
|
||||
// Phase 1: Root fields → ARM root with original casing.
|
||||
for &field in ROOT_FIELDS {
|
||||
let lc = field.to_ascii_lowercase();
|
||||
// Fast-path: direct BTreeMap lookup (O(log N)) for the common case
|
||||
// where normalized input was produced by our normalizer with lowercase keys.
|
||||
// Falls back to linear case-insensitive scan for externally-supplied mixed-case input.
|
||||
let lc_key = Value::String(Rc::from(lc.as_str()));
|
||||
let found = obj.get(&lc_key).or_else(|| {
|
||||
obj.iter()
|
||||
.find(|(k, _)| val_str(k).is_some_and(|s| s.eq_ignore_ascii_case(&lc)))
|
||||
.map(|(_, v)| v)
|
||||
});
|
||||
if let Some(val) = found {
|
||||
let restored = denormalize_value(val, &casing_map);
|
||||
obj_insert(&mut result, field, restored);
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 2a: Non-aliased, non-root fields.
|
||||
for (key, val) in obj.iter() {
|
||||
let key_s = match val_str(key) {
|
||||
Some(s) => s,
|
||||
None => continue,
|
||||
};
|
||||
if ROOT_FIELDS.iter().any(|f| f.eq_ignore_ascii_case(key_s)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let lookup_key = key_s.strip_prefix("_p_").unwrap_or(key_s);
|
||||
let lookup_key_lc = lookup_key.to_ascii_lowercase();
|
||||
let has_alias = entries.is_some_and(|e| e.contains_key(lookup_key_lc.as_str()));
|
||||
if has_alias {
|
||||
continue;
|
||||
}
|
||||
|
||||
let denorm_val = denormalize_value(val, &casing_map);
|
||||
|
||||
if key_s.starts_with("_p_") {
|
||||
let restored = restore_casing(lookup_key, &casing_map);
|
||||
obj_insert(&mut properties, &restored, denorm_val);
|
||||
} else if is_data_plane {
|
||||
let restored = restore_casing(key_s, &casing_map);
|
||||
obj_insert(&mut result, &restored, denorm_val);
|
||||
} else {
|
||||
let restored = restore_casing(key_s, &casing_map);
|
||||
obj_insert(&mut properties, &restored, denorm_val);
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 2b: Aliased scalar fields → versioned ARM paths.
|
||||
if let Some(entries) = entries {
|
||||
for (lc_key, entry) in entries {
|
||||
if entry.is_wildcard {
|
||||
continue;
|
||||
}
|
||||
|
||||
if sub_resource_set.contains(lc_key.as_str()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let normalized_key = if is_root_field_collision(&entry.short_name, &entry.default_path)
|
||||
{
|
||||
alloc::format!("_p_{}", entry.short_name.to_ascii_lowercase())
|
||||
} else {
|
||||
lc_key.clone()
|
||||
};
|
||||
|
||||
// Fast-path: direct BTreeMap lookup for lowercase keys,
|
||||
// with case-insensitive fallback for mixed-case external input.
|
||||
let nk_val = Value::String(Rc::from(normalized_key.as_str()));
|
||||
let val = obj.get(&nk_val).or_else(|| {
|
||||
obj.iter()
|
||||
.find(|(k, _)| {
|
||||
val_str(k).is_some_and(|s| s.eq_ignore_ascii_case(&normalized_key))
|
||||
})
|
||||
.map(|(_, v)| v)
|
||||
});
|
||||
let val = match val {
|
||||
Some(v) => v,
|
||||
None => continue,
|
||||
};
|
||||
|
||||
let arm_path = entry.select_path(api_version);
|
||||
let denorm_val = denormalize_value(val, &casing_map);
|
||||
|
||||
if let Some(props_path) = arm_path.strip_prefix("properties.") {
|
||||
set_nested_verbatim(&mut properties, props_path, denorm_val);
|
||||
} else {
|
||||
set_nested_verbatim(&mut result, arm_path, denorm_val);
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 2c + 2d: Use precomputed renames/remaps.
|
||||
// Look up versioned aggregates when api_version is provided,
|
||||
// falling back to default aggregates.
|
||||
if let Some(aliases) = aliases {
|
||||
let agg = api_version.map_or(&aliases.default_aggregates, |ver| {
|
||||
let ver_lc = ver.to_ascii_lowercase();
|
||||
aliases
|
||||
.versioned_aggregates
|
||||
.get(&ver_lc)
|
||||
.unwrap_or(&aliases.default_aggregates)
|
||||
});
|
||||
|
||||
// Phase 2c: Precomputed array base renames.
|
||||
for (alias_base_lc, arm_base) in &agg.array_renames_denormalize {
|
||||
if let Some(key) = find_key_ci(&properties, alias_base_lc) {
|
||||
if let Some(val) = properties.remove(key.as_ref()) {
|
||||
obj_insert(&mut properties, arm_base, val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 2d: Precomputed reverse element-level field remaps.
|
||||
for rev in &agg.reverse_element_remaps {
|
||||
let remap = ElementRemap {
|
||||
array_chain: rev.array_chain.clone(),
|
||||
source_field: rev.source_field.clone(),
|
||||
target_field: if rev.target_field.contains('.') {
|
||||
rev.target_field
|
||||
.split('.')
|
||||
.map(|segment| restore_casing(segment, &casing_map))
|
||||
.collect::<alloc::vec::Vec<_>>()
|
||||
.join(".")
|
||||
} else {
|
||||
restore_casing(&rev.target_field, &casing_map)
|
||||
},
|
||||
};
|
||||
apply_element_remap(&mut properties, &remap, false);
|
||||
remove_element_field(&mut properties, &rev.array_chain, &rev.cleanup_field);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 3: Re-wrap sub-resource array elements.
|
||||
if let Some(aliases) = aliases {
|
||||
if !aliases.sub_resource_arrays.is_empty() {
|
||||
sub_resource::rewrap_sub_resource_arrays(
|
||||
&mut properties,
|
||||
&aliases.sub_resource_arrays,
|
||||
&aliases.entries,
|
||||
api_version,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 4: Attach properties to result.
|
||||
if !properties.is_empty() {
|
||||
if let Some(Value::Object(existing_rc)) = result.get_mut("properties") {
|
||||
// Merge directly into the BTreeMap, avoiding full ObjMap round-trip.
|
||||
let existing = Rc::make_mut(existing_rc);
|
||||
for (k, v) in properties {
|
||||
existing.entry(Value::String(k)).or_insert(v);
|
||||
}
|
||||
} else {
|
||||
obj_insert(&mut result, "properties", make_value(properties));
|
||||
}
|
||||
}
|
||||
|
||||
make_value(result)
|
||||
}
|
||||
228
src/languages/azure_policy/aliases/denormalizer/sub_resource.rs
Normal file
228
src/languages/azure_policy/aliases/denormalizer/sub_resource.rs
Normal file
@@ -0,0 +1,228 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
//! Sub-resource array re-wrapping during denormalization.
|
||||
|
||||
use alloc::collections::{BTreeMap, BTreeSet};
|
||||
use alloc::string::String;
|
||||
use alloc::vec::Vec;
|
||||
|
||||
use crate::Value;
|
||||
|
||||
use super::super::obj_map::{make_value, new_map, obj_insert, val_str, ObjMap};
|
||||
use super::super::types::ResolvedEntry;
|
||||
use super::helpers::find_key_ci;
|
||||
|
||||
/// Sub-resource array element envelope fields that remain at the element root.
|
||||
const ELEMENT_ENVELOPE_FIELDS: &[&str] = &["name", "type", "id", "etag"];
|
||||
|
||||
/// Re-wrap sub-resource array elements by moving non-envelope fields back
|
||||
/// under each element's `properties` object.
|
||||
pub fn rewrap_sub_resource_arrays(
|
||||
properties: &mut ObjMap,
|
||||
sub_arrays: &BTreeSet<String>,
|
||||
entries: &BTreeMap<String, ResolvedEntry>,
|
||||
api_version: Option<&str>,
|
||||
) {
|
||||
let mut sorted: Vec<&String> = sub_arrays.iter().collect();
|
||||
sorted.sort_by(|a, b| {
|
||||
let depth_a = a.chars().filter(|&c| c == '.').count();
|
||||
let depth_b = b.chars().filter(|&c| c == '.').count();
|
||||
depth_b.cmp(&depth_a)
|
||||
});
|
||||
|
||||
for sub_array_path in sorted {
|
||||
let envelope_fields = classify_envelope_fields(sub_array_path, entries, api_version);
|
||||
let parts: Vec<&str> = sub_array_path.split('.').collect();
|
||||
|
||||
if parts.len() == 1 {
|
||||
if let Some(key) = parts.first().and_then(|p| find_key_ci(properties, p)) {
|
||||
if let Some(Value::Array(arr)) = properties.get_mut(key.as_ref()) {
|
||||
let inner = crate::Rc::make_mut(arr);
|
||||
for elem in inner.iter_mut() {
|
||||
*elem = rewrap_element(elem, &envelope_fields);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if let Some((&array_name, parent_parts)) = parts.split_last() {
|
||||
rewrap_nested_array(properties, parent_parts, array_name, &envelope_fields);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Determine which element-level fields are envelope fields for a given
|
||||
/// sub-resource array.
|
||||
fn classify_envelope_fields(
|
||||
sub_array_path: &str,
|
||||
entries: &BTreeMap<String, ResolvedEntry>,
|
||||
api_version: Option<&str>,
|
||||
) -> BTreeSet<String> {
|
||||
let mut envelope = BTreeSet::new();
|
||||
for &f in ELEMENT_ENVELOPE_FIELDS {
|
||||
envelope.insert(f.to_ascii_lowercase());
|
||||
}
|
||||
|
||||
let wildcard_prefix: String = {
|
||||
let parts: Vec<&str> = sub_array_path.split('.').collect();
|
||||
let mut prefix = String::new();
|
||||
for (i, part) in parts.iter().enumerate() {
|
||||
if i > 0 {
|
||||
prefix.push_str("[*].");
|
||||
}
|
||||
prefix.push_str(&part.to_ascii_lowercase());
|
||||
}
|
||||
prefix.push_str("[*].");
|
||||
prefix
|
||||
};
|
||||
|
||||
for (lc_key, entry) in entries {
|
||||
if !lc_key.starts_with(&wildcard_prefix) {
|
||||
continue;
|
||||
}
|
||||
let field = &lc_key[wildcard_prefix.len()..];
|
||||
let first_segment = field.split('.').next().unwrap_or(field);
|
||||
|
||||
let arm_path = entry.select_path(api_version);
|
||||
let arm_after_last_wildcard = arm_path.rsplit("[*].").next().unwrap_or("");
|
||||
|
||||
if !arm_after_last_wildcard.starts_with("properties.") {
|
||||
envelope.insert(first_segment.to_ascii_lowercase());
|
||||
}
|
||||
}
|
||||
|
||||
envelope
|
||||
}
|
||||
|
||||
/// Recursively navigate nested arrays and re-wrap elements of the innermost
|
||||
/// sub-resource array.
|
||||
fn rewrap_nested_array(
|
||||
obj: &mut ObjMap,
|
||||
parent_parts: &[&str],
|
||||
array_name: &str,
|
||||
envelope_fields: &BTreeSet<String>,
|
||||
) {
|
||||
if parent_parts.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let parent_key = match parent_parts.first().and_then(|&p| find_key_ci(obj, p)) {
|
||||
Some(k) => k,
|
||||
None => return,
|
||||
};
|
||||
|
||||
let parent_arr = match obj.get_mut(parent_key.as_ref()) {
|
||||
Some(Value::Array(arr)) => crate::Rc::make_mut(arr),
|
||||
_ => return,
|
||||
};
|
||||
|
||||
for element in parent_arr.iter_mut() {
|
||||
if let Value::Object(obj_rc) = element {
|
||||
let inner_btree = crate::Rc::make_mut(obj_rc);
|
||||
|
||||
if parent_parts.len() > 1 {
|
||||
rewrap_nested_array_in_btree(
|
||||
inner_btree,
|
||||
parent_parts.get(1..).unwrap_or_default(),
|
||||
array_name,
|
||||
envelope_fields,
|
||||
);
|
||||
} else if let Some(arr_key) = find_key_ci_btree(inner_btree, array_name) {
|
||||
if let Some(Value::Array(arr)) = inner_btree.get_mut(&arr_key) {
|
||||
let inner = crate::Rc::make_mut(arr);
|
||||
for inner_elem in inner.iter_mut() {
|
||||
*inner_elem = rewrap_element(inner_elem, envelope_fields);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// BTreeMap-native recursion for nested sub-resource array re-wrapping,
|
||||
/// avoiding ObjMap round-trips on each array element.
|
||||
fn rewrap_nested_array_in_btree(
|
||||
btree: &mut alloc::collections::BTreeMap<Value, Value>,
|
||||
parent_parts: &[&str],
|
||||
array_name: &str,
|
||||
envelope_fields: &BTreeSet<String>,
|
||||
) {
|
||||
if parent_parts.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let parent_key = match parent_parts
|
||||
.first()
|
||||
.and_then(|&p| find_key_ci_btree(btree, p))
|
||||
{
|
||||
Some(k) => k,
|
||||
None => return,
|
||||
};
|
||||
|
||||
let parent_arr = match btree.get_mut(&parent_key) {
|
||||
Some(Value::Array(arr)) => crate::Rc::make_mut(arr),
|
||||
_ => return,
|
||||
};
|
||||
|
||||
for element in parent_arr.iter_mut() {
|
||||
if let Value::Object(obj_rc) = element {
|
||||
let inner_btree = crate::Rc::make_mut(obj_rc);
|
||||
|
||||
if parent_parts.len() > 1 {
|
||||
rewrap_nested_array_in_btree(
|
||||
inner_btree,
|
||||
parent_parts.get(1..).unwrap_or_default(),
|
||||
array_name,
|
||||
envelope_fields,
|
||||
);
|
||||
} else if let Some(arr_key) = find_key_ci_btree(inner_btree, array_name) {
|
||||
if let Some(Value::Array(arr)) = inner_btree.get_mut(&arr_key) {
|
||||
let inner = crate::Rc::make_mut(arr);
|
||||
for inner_elem in inner.iter_mut() {
|
||||
*inner_elem = rewrap_element(inner_elem, envelope_fields);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Find a key in a BTreeMap using case-insensitive comparison.
|
||||
fn find_key_ci_btree(
|
||||
btree: &alloc::collections::BTreeMap<Value, Value>,
|
||||
key: &str,
|
||||
) -> Option<Value> {
|
||||
btree
|
||||
.keys()
|
||||
.find(|k| val_str(k).is_some_and(|s| s.eq_ignore_ascii_case(key)))
|
||||
.cloned()
|
||||
}
|
||||
|
||||
/// Re-wrap a single sub-resource array element by moving non-envelope
|
||||
/// fields back under a `properties` object.
|
||||
fn rewrap_element(element: &Value, envelope_fields: &BTreeSet<String>) -> Value {
|
||||
let obj = match element.as_object() {
|
||||
Ok(o) => o,
|
||||
Err(_) => return element.clone(),
|
||||
};
|
||||
|
||||
let mut envelope = new_map();
|
||||
let mut props = new_map();
|
||||
|
||||
for (key, val) in obj.iter() {
|
||||
let key_s = match val_str(key) {
|
||||
Some(s) => s,
|
||||
None => continue,
|
||||
};
|
||||
if envelope_fields.contains(&key_s.to_ascii_lowercase()) {
|
||||
obj_insert(&mut envelope, key_s, val.clone());
|
||||
} else {
|
||||
obj_insert(&mut props, key_s, val.clone());
|
||||
}
|
||||
}
|
||||
|
||||
if !props.is_empty() {
|
||||
obj_insert(&mut envelope, "properties", make_value(props));
|
||||
}
|
||||
|
||||
make_value(envelope)
|
||||
}
|
||||
55
src/languages/azure_policy/aliases/denormalizer/tests.rs
Normal file
55
src/languages/azure_policy/aliases/denormalizer/tests.rs
Normal file
@@ -0,0 +1,55 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
//! Inline denormalizer unit tests.
|
||||
|
||||
use alloc::collections::BTreeMap;
|
||||
use alloc::string::ToString as _;
|
||||
use alloc::vec;
|
||||
use alloc::vec::Vec;
|
||||
|
||||
use super::super::types::ResolvedEntry;
|
||||
use super::casing::build_casing_map;
|
||||
|
||||
fn make_entry(short: &str, default: &str, versioned: Vec<(&str, &str)>) -> ResolvedEntry {
|
||||
ResolvedEntry::new(
|
||||
short.to_string(),
|
||||
default.to_string(),
|
||||
versioned
|
||||
.into_iter()
|
||||
.map(|(v, p)| (v.to_string(), p.to_string()))
|
||||
.collect(),
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn build_casing_map_extracts_from_aliases() {
|
||||
let mut entries = BTreeMap::new();
|
||||
entries.insert(
|
||||
"supportshttpstrafficonly".to_string(),
|
||||
make_entry(
|
||||
"supportsHttpsTrafficOnly",
|
||||
"properties.supportsHttpsTrafficOnly",
|
||||
vec![],
|
||||
),
|
||||
);
|
||||
entries.insert(
|
||||
"networkacls.defaultaction".to_string(),
|
||||
make_entry(
|
||||
"networkAcls.defaultAction",
|
||||
"properties.networkAcls.defaultAction",
|
||||
vec![],
|
||||
),
|
||||
);
|
||||
|
||||
let map = build_casing_map(&entries);
|
||||
assert_eq!(
|
||||
map.get("supportshttpstrafficonly"),
|
||||
Some(&"supportsHttpsTrafficOnly".to_string())
|
||||
);
|
||||
assert_eq!(map.get("networkacls"), Some(&"networkAcls".to_string()));
|
||||
assert_eq!(map.get("defaultaction"), Some(&"defaultAction".to_string()));
|
||||
assert_eq!(map.get("managedby"), Some(&"managedBy".to_string()));
|
||||
assert_eq!(map.get("apiversion"), Some(&"apiVersion".to_string()));
|
||||
}
|
||||
1310
src/languages/azure_policy/aliases/mod.rs
Normal file
1310
src/languages/azure_policy/aliases/mod.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,96 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
//! Per-alias path resolution: reads values from versioned ARM paths and places
|
||||
//! them at alias short name paths in the normalized output.
|
||||
|
||||
use alloc::string::String;
|
||||
|
||||
use crate::Value;
|
||||
|
||||
use super::super::obj_map::remove_element_field;
|
||||
use super::super::obj_map::{
|
||||
collision_safe_key, is_root_field_collision, obj_contains, obj_insert, obj_remove,
|
||||
set_nested_lowercased, ObjMap,
|
||||
};
|
||||
use super::super::types::ResolvedAliases;
|
||||
use super::element_remap::apply_element_remap_precomputed;
|
||||
use super::flatten::normalize_value;
|
||||
|
||||
/// Apply per-alias path resolution to the normalized result.
|
||||
///
|
||||
/// Uses precomputed element remaps and array renames from [`ResolvedAliases`]
|
||||
/// when `api_version` is `None` (the common case). Falls back to dynamic
|
||||
/// computation when a specific `api_version` is provided.
|
||||
pub fn apply_alias_entries(
|
||||
result: &mut ObjMap,
|
||||
raw: &Value,
|
||||
aliases: &ResolvedAliases,
|
||||
api_version: Option<&str>,
|
||||
) {
|
||||
let entries = &aliases.entries;
|
||||
let sub_resource_set = &aliases.sub_resource_arrays;
|
||||
|
||||
for (lc_key, entry) in entries {
|
||||
if entry.is_wildcard {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip sub-resource array root entries.
|
||||
if sub_resource_set.contains(lc_key.as_str()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Use precomputed segments for all paths (default and versioned).
|
||||
let segments = entry.select_path_segments(api_version);
|
||||
let value = navigate_arm_path_segments(raw, segments);
|
||||
|
||||
if let Some(value) = value {
|
||||
let value = normalize_value(&value, &entry.short_name, None);
|
||||
|
||||
let target = if is_root_field_collision(&entry.short_name, &entry.default_path) {
|
||||
collision_safe_key(&entry.short_name)
|
||||
} else {
|
||||
entry.short_name.clone()
|
||||
};
|
||||
set_nested_lowercased(result, &target, value);
|
||||
}
|
||||
}
|
||||
|
||||
// Look up precomputed aggregates: default or per-version.
|
||||
let agg = api_version.map_or(&aliases.default_aggregates, |ver| {
|
||||
let ver_lc = ver.to_ascii_lowercase();
|
||||
aliases
|
||||
.versioned_aggregates
|
||||
.get(&ver_lc)
|
||||
.unwrap_or(&aliases.default_aggregates)
|
||||
});
|
||||
|
||||
for remap in &agg.element_remaps {
|
||||
apply_element_remap_precomputed(result, remap);
|
||||
// Remove the original ARM field so the normalized output only has the
|
||||
// alias short name. Without this, the stale source key survives and
|
||||
// casing restoration during denormalization can produce a duplicate.
|
||||
remove_element_field(result, &remap.array_chain, &remap.source_field);
|
||||
}
|
||||
|
||||
for (source_lc, target_lc) in &agg.array_renames_normalize {
|
||||
if !obj_contains(result, target_lc.as_str()) {
|
||||
if let Some(val) = obj_remove(result, source_lc.as_str()) {
|
||||
obj_insert(result, target_lc, val);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Navigate an ARM path using precomputed segments (avoids per-call split).
|
||||
fn navigate_arm_path_segments(value: &Value, segments: &[String]) -> Option<Value> {
|
||||
let mut current = value;
|
||||
for segment in segments {
|
||||
current = current
|
||||
.as_object()
|
||||
.ok()?
|
||||
.get(&Value::from(segment.as_str()))?;
|
||||
}
|
||||
Some(current.clone())
|
||||
}
|
||||
252
src/languages/azure_policy/aliases/normalizer/element_remap.rs
Normal file
252
src/languages/azure_policy/aliases/normalizer/element_remap.rs
Normal file
@@ -0,0 +1,252 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
//! Element-level field remapping for array aliases with versioned paths.
|
||||
|
||||
use alloc::string::String;
|
||||
use alloc::vec::Vec;
|
||||
|
||||
use crate::Value;
|
||||
|
||||
use super::super::obj_map::{
|
||||
obj_get, obj_get_mut, obj_insert, set_nested_in_btree, set_nested_lowercased,
|
||||
set_nested_verbatim, ObjMap,
|
||||
};
|
||||
use super::super::types::PrecomputedRemap;
|
||||
|
||||
/// Describes a field remapping inside each element of a (possibly nested) array.
|
||||
pub struct ElementRemap {
|
||||
/// Chain of array navigations for nested `[*]` levels.
|
||||
pub(crate) array_chain: Vec<Vec<String>>,
|
||||
/// Dot-separated path to read within the innermost array element.
|
||||
pub(crate) source_field: String,
|
||||
/// Dot-separated path to write within the innermost array element.
|
||||
pub(crate) target_field: String,
|
||||
}
|
||||
|
||||
/// Apply an element-level field remap to each element of an array (or nested
|
||||
/// array chain).
|
||||
///
|
||||
/// When `lowercase` is `true` (normalizer), target path segments are
|
||||
/// lowercased. When `false` (denormalizer), they are written verbatim
|
||||
/// so that restored casing is preserved.
|
||||
pub fn apply_element_remap(result: &mut ObjMap, remap: &ElementRemap, lowercase: bool) {
|
||||
apply_remap_at_depth(
|
||||
result,
|
||||
&remap.array_chain,
|
||||
0,
|
||||
&remap.source_field,
|
||||
&remap.target_field,
|
||||
lowercase,
|
||||
);
|
||||
}
|
||||
|
||||
/// Apply a precomputed element remap (from [`PrecomputedRemap`]) without
|
||||
/// any per-call string splitting or allocation.
|
||||
pub fn apply_element_remap_precomputed(result: &mut ObjMap, remap: &PrecomputedRemap) {
|
||||
apply_remap_at_depth(
|
||||
result,
|
||||
&remap.array_chain,
|
||||
0,
|
||||
&remap.source_field,
|
||||
&remap.target_field,
|
||||
true,
|
||||
);
|
||||
}
|
||||
|
||||
/// Recursively navigate nested arrays via `array_chain` and apply a field
|
||||
/// remap in each innermost element.
|
||||
fn apply_remap_at_depth(
|
||||
obj: &mut ObjMap,
|
||||
array_chain: &[Vec<String>],
|
||||
depth: usize,
|
||||
source_field: &str,
|
||||
target_field: &str,
|
||||
lowercase: bool,
|
||||
) {
|
||||
let Some(nav) = array_chain.get(depth) else {
|
||||
remap_deep_field(obj, source_field, target_field, lowercase);
|
||||
return;
|
||||
};
|
||||
|
||||
let first = match nav.first() {
|
||||
Some(f) => f.as_str(),
|
||||
None => return,
|
||||
};
|
||||
|
||||
// Navigate through intermediate segments to reach the array value.
|
||||
let arr_val = if nav.len() == 1 {
|
||||
match obj_get_mut(obj, first) {
|
||||
Some(v) => v,
|
||||
None => return,
|
||||
}
|
||||
} else {
|
||||
let mut cur: &mut Value = match obj_get_mut(obj, first) {
|
||||
Some(v) => v,
|
||||
None => return,
|
||||
};
|
||||
for segment in nav.iter().skip(1) {
|
||||
cur = match cur.as_object_mut() {
|
||||
Ok(inner) => match inner.get_mut(&Value::from(segment.as_str())) {
|
||||
Some(v) => v,
|
||||
None => return,
|
||||
},
|
||||
Err(_) => return,
|
||||
};
|
||||
}
|
||||
cur
|
||||
};
|
||||
|
||||
if let Value::Array(elements) = arr_val {
|
||||
let inner = crate::Rc::make_mut(elements);
|
||||
for elem in inner.iter_mut() {
|
||||
if let Value::Object(obj_rc) = elem {
|
||||
let inner_btree = crate::Rc::make_mut(obj_rc);
|
||||
remap_at_depth_in_btree(
|
||||
inner_btree,
|
||||
array_chain,
|
||||
depth.saturating_add(1),
|
||||
source_field,
|
||||
target_field,
|
||||
lowercase,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// BTreeMap-native recursion for element-level remap, avoiding ObjMap
|
||||
/// round-trips on each array element.
|
||||
fn remap_at_depth_in_btree(
|
||||
btree: &mut alloc::collections::BTreeMap<Value, Value>,
|
||||
array_chain: &[Vec<String>],
|
||||
depth: usize,
|
||||
source_field: &str,
|
||||
target_field: &str,
|
||||
lowercase: bool,
|
||||
) {
|
||||
let Some(nav) = array_chain.get(depth) else {
|
||||
remap_deep_field_in_btree(btree, source_field, target_field, lowercase);
|
||||
return;
|
||||
};
|
||||
|
||||
let first = match nav.first() {
|
||||
Some(f) => f.as_str(),
|
||||
None => return,
|
||||
};
|
||||
|
||||
let key_val = Value::from(first);
|
||||
let arr_val = if nav.len() == 1 {
|
||||
match btree.get_mut(&key_val) {
|
||||
Some(v) => v,
|
||||
None => return,
|
||||
}
|
||||
} else {
|
||||
let mut cur: &mut Value = match btree.get_mut(&key_val) {
|
||||
Some(v) => v,
|
||||
None => return,
|
||||
};
|
||||
for segment in nav.iter().skip(1) {
|
||||
cur = match cur.as_object_mut() {
|
||||
Ok(inner) => match inner.get_mut(&Value::from(segment.as_str())) {
|
||||
Some(v) => v,
|
||||
None => return,
|
||||
},
|
||||
Err(_) => return,
|
||||
};
|
||||
}
|
||||
cur
|
||||
};
|
||||
|
||||
if let Value::Array(elements) = arr_val {
|
||||
let inner = crate::Rc::make_mut(elements);
|
||||
for elem in inner.iter_mut() {
|
||||
if let Value::Object(obj_rc) = elem {
|
||||
let inner_btree = crate::Rc::make_mut(obj_rc);
|
||||
remap_at_depth_in_btree(
|
||||
inner_btree,
|
||||
array_chain,
|
||||
depth.saturating_add(1),
|
||||
source_field,
|
||||
target_field,
|
||||
lowercase,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Remap a value between dotted paths directly in a BTreeMap.
|
||||
fn remap_deep_field_in_btree(
|
||||
btree: &mut alloc::collections::BTreeMap<Value, Value>,
|
||||
source: &str,
|
||||
target: &str,
|
||||
lowercase: bool,
|
||||
) {
|
||||
let val = match read_dotted_path_btree(btree, source) {
|
||||
Some(v) => v,
|
||||
None => return,
|
||||
};
|
||||
|
||||
let segments: Vec<&str> = target.split('.').collect();
|
||||
if segments.is_empty() {
|
||||
return;
|
||||
}
|
||||
if segments.len() == 1 {
|
||||
if let Some(seg) = segments.first() {
|
||||
btree.insert(Value::String(crate::Rc::from(*seg)), val);
|
||||
}
|
||||
return;
|
||||
}
|
||||
set_nested_in_btree(btree, &segments, val, lowercase);
|
||||
}
|
||||
|
||||
/// Read a value at a dotted path from a BTreeMap.
|
||||
fn read_dotted_path_btree(
|
||||
btree: &alloc::collections::BTreeMap<Value, Value>,
|
||||
path: &str,
|
||||
) -> Option<Value> {
|
||||
let segments: Vec<&str> = path.split('.').collect();
|
||||
let first = segments.first()?;
|
||||
let mut cur: &Value = btree.get(&Value::from(*first))?;
|
||||
for &seg in segments.iter().skip(1) {
|
||||
cur = cur.as_object().ok()?.get(&Value::from(seg))?;
|
||||
}
|
||||
Some(cur.clone())
|
||||
}
|
||||
|
||||
/// Remap a value from one (possibly nested) dot-separated path to another
|
||||
/// in an ObjMap. Used only at the top level when `depth >= array_chain.len()`.
|
||||
fn remap_deep_field(obj: &mut ObjMap, source: &str, target: &str, lowercase: bool) {
|
||||
let val = match read_dotted_path(obj, source) {
|
||||
Some(v) => v,
|
||||
None => return,
|
||||
};
|
||||
|
||||
let segments: Vec<&str> = target.split('.').collect();
|
||||
if segments.is_empty() {
|
||||
return;
|
||||
}
|
||||
if segments.len() == 1 {
|
||||
if let Some(seg) = segments.first() {
|
||||
obj_insert(obj, seg, val);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if lowercase {
|
||||
set_nested_lowercased(obj, target, val);
|
||||
} else {
|
||||
set_nested_verbatim(obj, target, val);
|
||||
}
|
||||
}
|
||||
|
||||
/// Read a value at a dot-separated path from an ObjMap.
|
||||
fn read_dotted_path(obj: &ObjMap, path: &str) -> Option<Value> {
|
||||
let segments: Vec<&str> = path.split('.').collect();
|
||||
let first = segments.first()?;
|
||||
let mut cur: &Value = obj_get(obj, first)?;
|
||||
for &seg in segments.iter().skip(1) {
|
||||
cur = cur.as_object().ok()?.get(&Value::from(seg))?;
|
||||
}
|
||||
Some(cur.clone())
|
||||
}
|
||||
132
src/languages/azure_policy/aliases/normalizer/flatten.rs
Normal file
132
src/languages/azure_policy/aliases/normalizer/flatten.rs
Normal file
@@ -0,0 +1,132 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
//! Value normalization helpers: recursive key lowercasing, sub-resource array
|
||||
//! flattening, and element merging.
|
||||
|
||||
use alloc::collections::BTreeSet;
|
||||
use alloc::string::String;
|
||||
use alloc::vec::Vec;
|
||||
|
||||
use crate::Value;
|
||||
|
||||
use super::super::obj_map::{make_array, make_value, new_map, obj_contains, obj_insert, val_str};
|
||||
|
||||
/// Lowercase all keys of a JSON object (shallow — values are untouched).
|
||||
/// Non-object values are returned as-is.
|
||||
pub fn lowercase_object_keys(value: &Value) -> Value {
|
||||
match value {
|
||||
Value::Object(obj) => {
|
||||
let mut result = new_map();
|
||||
for (k, v) in obj.iter() {
|
||||
if let Some(s) = val_str(k) {
|
||||
obj_insert(&mut result, &s.to_ascii_lowercase(), v.clone());
|
||||
}
|
||||
}
|
||||
make_value(result)
|
||||
}
|
||||
_ => value.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Recursively normalize a value, flattening sub-resource array elements.
|
||||
pub fn normalize_value(
|
||||
value: &Value,
|
||||
field_path: &str,
|
||||
sub_arrays: Option<&BTreeSet<String>>,
|
||||
) -> Value {
|
||||
match value {
|
||||
Value::Array(arr) => {
|
||||
let items: Vec<Value> = if is_sub_resource_array(field_path, sub_arrays) {
|
||||
arr.iter()
|
||||
.map(|elem| flatten_element(elem, field_path, sub_arrays))
|
||||
.collect()
|
||||
} else {
|
||||
arr.iter()
|
||||
.map(|elem| normalize_value(elem, field_path, sub_arrays))
|
||||
.collect()
|
||||
};
|
||||
make_array(items)
|
||||
}
|
||||
Value::Object(obj) => {
|
||||
let mut result = new_map();
|
||||
for (k, v) in obj.iter() {
|
||||
let key_s = match val_str(k) {
|
||||
Some(s) => s,
|
||||
None => continue,
|
||||
};
|
||||
let child_path = alloc::format!("{}.{}", field_path, key_s);
|
||||
obj_insert(
|
||||
&mut result,
|
||||
&key_s.to_ascii_lowercase(),
|
||||
normalize_value(v, &child_path, sub_arrays),
|
||||
);
|
||||
}
|
||||
make_value(result)
|
||||
}
|
||||
_ => value.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Flatten a sub-resource array element by merging its `properties` into
|
||||
/// the element root.
|
||||
pub fn flatten_element(
|
||||
element: &Value,
|
||||
array_path: &str,
|
||||
sub_arrays: Option<&BTreeSet<String>>,
|
||||
) -> Value {
|
||||
let obj = match element.as_object() {
|
||||
Ok(o) => o,
|
||||
Err(_) => return element.clone(),
|
||||
};
|
||||
|
||||
let mut result = new_map();
|
||||
|
||||
// Copy non-`properties` fields from the element envelope (keys lowercased).
|
||||
for (key, val) in obj.iter() {
|
||||
let key_s = match val_str(key) {
|
||||
Some(s) => s,
|
||||
None => continue,
|
||||
};
|
||||
if key_s.eq_ignore_ascii_case("properties") {
|
||||
continue;
|
||||
}
|
||||
let child_path = alloc::format!("{}.{}", array_path, key_s);
|
||||
obj_insert(
|
||||
&mut result,
|
||||
&key_s.to_ascii_lowercase(),
|
||||
normalize_value(val, &child_path, sub_arrays),
|
||||
);
|
||||
}
|
||||
|
||||
// Merge `properties` into the element (keys lowercased).
|
||||
let props_val = obj
|
||||
.iter()
|
||||
.find(|(k, _)| val_str(k).is_some_and(|s| s.eq_ignore_ascii_case("properties")))
|
||||
.map(|(_, v)| v);
|
||||
if let Some(Value::Object(props)) = props_val {
|
||||
for (key, val) in props.iter() {
|
||||
let key_s = match val_str(key) {
|
||||
Some(s) => s,
|
||||
None => continue,
|
||||
};
|
||||
let lc_key = key_s.to_ascii_lowercase();
|
||||
if obj_contains(&result, &lc_key) {
|
||||
continue;
|
||||
}
|
||||
let child_path = alloc::format!("{}.{}", array_path, key_s);
|
||||
obj_insert(
|
||||
&mut result,
|
||||
&lc_key,
|
||||
normalize_value(val, &child_path, sub_arrays),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
make_value(result)
|
||||
}
|
||||
|
||||
/// Check if a field path corresponds to a sub-resource array.
|
||||
fn is_sub_resource_array(field_path: &str, sub_arrays: Option<&BTreeSet<String>>) -> bool {
|
||||
sub_arrays.is_some_and(|set| set.contains(&field_path.to_ascii_lowercase()))
|
||||
}
|
||||
157
src/languages/azure_policy/aliases/normalizer/mod.rs
Normal file
157
src/languages/azure_policy/aliases/normalizer/mod.rs
Normal file
@@ -0,0 +1,157 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
//! ARM JSON → normalized `input.resource` transformation.
|
||||
//!
|
||||
//! The normalizer flattens `properties` wrappers from raw ARM resource JSON so
|
||||
//! that alias short names become direct paths into the normalized structure.
|
||||
|
||||
mod alias_resolution;
|
||||
mod element_remap;
|
||||
mod flatten;
|
||||
|
||||
// Re-export items used by the denormalizer.
|
||||
pub(crate) use element_remap::{apply_element_remap, ElementRemap};
|
||||
|
||||
use crate::Value;
|
||||
|
||||
use super::obj_map::{
|
||||
extract_type_field, make_value, new_map, obj_contains, obj_insert, val_str, ObjMap, ROOT_FIELDS,
|
||||
};
|
||||
use super::types::ResolvedAliases;
|
||||
use super::AliasRegistry;
|
||||
|
||||
use flatten::{lowercase_object_keys, normalize_value};
|
||||
|
||||
/// Normalize a raw ARM resource JSON value into the `input.resource` structure.
|
||||
///
|
||||
/// The resource type is extracted from the `type` field of `arm_resource` and
|
||||
/// used to look up alias entries in the registry.
|
||||
pub fn normalize(
|
||||
arm_resource: &Value,
|
||||
registry: Option<&AliasRegistry>,
|
||||
api_version: Option<&str>,
|
||||
) -> Value {
|
||||
let aliases = registry.and_then(|r| extract_type_field(arm_resource).and_then(|rt| r.get(rt)));
|
||||
normalize_with_aliases(arm_resource, aliases, api_version)
|
||||
}
|
||||
|
||||
/// Internal normalization with pre-resolved alias data.
|
||||
///
|
||||
/// Core implementation used by [`normalize`] after looking up the alias
|
||||
/// entries from the registry. Also used directly in unit tests.
|
||||
pub fn normalize_with_aliases(
|
||||
arm_resource: &Value,
|
||||
aliases: Option<&ResolvedAliases>,
|
||||
api_version: Option<&str>,
|
||||
) -> Value {
|
||||
let obj = match arm_resource.as_object() {
|
||||
Ok(o) => o,
|
||||
Err(_) => return arm_resource.clone(),
|
||||
};
|
||||
|
||||
let sub_arrays_ref = aliases.map(|a| &a.sub_resource_arrays);
|
||||
let mut result = new_map();
|
||||
|
||||
let is_data_plane =
|
||||
extract_type_field(arm_resource).is_some_and(|t| t.to_ascii_lowercase().contains(".data/"));
|
||||
|
||||
if is_data_plane {
|
||||
for (key, val) in obj {
|
||||
let key_s = match val_str(key) {
|
||||
Some(s) => s,
|
||||
None => continue,
|
||||
};
|
||||
if key_s.eq_ignore_ascii_case("properties") {
|
||||
continue;
|
||||
}
|
||||
let lc_key = key_s.to_ascii_lowercase();
|
||||
let val =
|
||||
if key_s.eq_ignore_ascii_case("tags") || key_s.eq_ignore_ascii_case("identity") {
|
||||
lowercase_object_keys(val)
|
||||
} else {
|
||||
normalize_value(val, key_s, sub_arrays_ref)
|
||||
};
|
||||
obj_insert(&mut result, &lc_key, val);
|
||||
}
|
||||
merge_properties(obj, &mut result, sub_arrays_ref);
|
||||
} else {
|
||||
// Copy root-level fields (keys lowercased).
|
||||
for &field in ROOT_FIELDS {
|
||||
let found = obj
|
||||
.iter()
|
||||
.find(|(k, _)| val_str(k).is_some_and(|s| s.eq_ignore_ascii_case(field)))
|
||||
.map(|(_, v)| v);
|
||||
if let Some(val) = found {
|
||||
let val = if field.eq_ignore_ascii_case("tags")
|
||||
|| field.eq_ignore_ascii_case("identity")
|
||||
{
|
||||
// Keep these shallow to avoid lowercasing dynamic nested-map
|
||||
// keys such as userAssignedIdentities member names.
|
||||
lowercase_object_keys(val)
|
||||
} else {
|
||||
normalize_value(val, field, sub_arrays_ref)
|
||||
};
|
||||
obj_insert(&mut result, &field.to_ascii_lowercase(), val);
|
||||
}
|
||||
}
|
||||
merge_properties(obj, &mut result, sub_arrays_ref);
|
||||
}
|
||||
|
||||
// Per-alias path resolution.
|
||||
if let Some(aliases) = aliases {
|
||||
alias_resolution::apply_alias_entries(&mut result, arm_resource, aliases, api_version);
|
||||
}
|
||||
|
||||
make_value(result)
|
||||
}
|
||||
|
||||
/// Merge `properties` fields into the result map, skipping keys that already
|
||||
/// exist.
|
||||
fn merge_properties(
|
||||
obj: &alloc::collections::BTreeMap<Value, Value>,
|
||||
result: &mut ObjMap,
|
||||
sub_arrays: Option<&alloc::collections::BTreeSet<alloc::string::String>>,
|
||||
) {
|
||||
let props_val = obj
|
||||
.iter()
|
||||
.find(|(k, _)| val_str(k).is_some_and(|s| s.eq_ignore_ascii_case("properties")))
|
||||
.map(|(_, v)| v);
|
||||
if let Some(Value::Object(props)) = props_val {
|
||||
for (key, val) in props.iter() {
|
||||
let key_s = match val_str(key) {
|
||||
Some(s) => s,
|
||||
None => continue,
|
||||
};
|
||||
let lc_key = key_s.to_ascii_lowercase();
|
||||
if obj_contains(result, &lc_key) {
|
||||
continue;
|
||||
}
|
||||
let normalized = normalize_value(val, key_s, sub_arrays);
|
||||
obj_insert(result, &lc_key, normalized);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Wrap a normalized resource into the full `input` envelope.
|
||||
///
|
||||
/// Produces: `{ "resource": <normalized>, "context": <context>, "parameters": <params> }`
|
||||
pub fn build_input_envelope(
|
||||
normalized_resource: Value,
|
||||
context: Option<Value>,
|
||||
parameters: Option<Value>,
|
||||
) -> Value {
|
||||
let mut envelope = new_map();
|
||||
obj_insert(&mut envelope, "resource", normalized_resource);
|
||||
obj_insert(
|
||||
&mut envelope,
|
||||
"context",
|
||||
context.unwrap_or_else(|| make_value(new_map())),
|
||||
);
|
||||
obj_insert(
|
||||
&mut envelope,
|
||||
"parameters",
|
||||
parameters.unwrap_or_else(|| make_value(new_map())),
|
||||
);
|
||||
make_value(envelope)
|
||||
}
|
||||
475
src/languages/azure_policy/aliases/obj_map.rs
Normal file
475
src/languages/azure_policy/aliases/obj_map.rs
Normal file
@@ -0,0 +1,475 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
//! Lightweight string-keyed map used during normalization/denormalization.
|
||||
//!
|
||||
//! Internally uses `hashbrown::HashMap<Rc<str>, Value>` for O(1) lookups,
|
||||
//! then converts to `Value::Object` (a `BTreeMap<Value, Value>`) only at
|
||||
//! the output boundary via [`make_value`].
|
||||
|
||||
use alloc::string::{String, ToString as _};
|
||||
use alloc::vec::Vec;
|
||||
|
||||
use hashbrown::HashMap;
|
||||
|
||||
use crate::Rc;
|
||||
use crate::Value;
|
||||
|
||||
/// A string-keyed map of JSON values.
|
||||
///
|
||||
/// All normalizer / denormalizer code works with this type internally.
|
||||
/// Convert to [`Value::Object`] via [`make_value`] when producing output.
|
||||
pub type ObjMap = HashMap<Rc<str>, Value>;
|
||||
|
||||
/// Create an empty [`ObjMap`].
|
||||
pub fn new_map() -> ObjMap {
|
||||
ObjMap::new()
|
||||
}
|
||||
|
||||
/// Look up a value by string key.
|
||||
pub fn obj_get<'a>(map: &'a ObjMap, key: &str) -> Option<&'a Value> {
|
||||
map.get(key)
|
||||
}
|
||||
|
||||
/// Look up a mutable value reference by string key.
|
||||
pub fn obj_get_mut<'a>(map: &'a mut ObjMap, key: &str) -> Option<&'a mut Value> {
|
||||
map.get_mut(key)
|
||||
}
|
||||
|
||||
/// Insert a key-value pair.
|
||||
pub fn obj_insert(map: &mut ObjMap, key: &str, val: Value) {
|
||||
map.insert(Rc::from(key), val);
|
||||
}
|
||||
|
||||
/// Check whether a key exists.
|
||||
pub fn obj_contains(map: &ObjMap, key: &str) -> bool {
|
||||
map.contains_key(key)
|
||||
}
|
||||
|
||||
/// Remove a key, returning its value if present.
|
||||
pub fn obj_remove(map: &mut ObjMap, key: &str) -> Option<Value> {
|
||||
map.remove(key)
|
||||
}
|
||||
|
||||
/// Convert an [`ObjMap`] into a [`Value::Object`].
|
||||
///
|
||||
/// Keys are converted from `Rc<str>` to `Value::String` and inserted into
|
||||
/// a `BTreeMap` to match the `Value::Object` representation.
|
||||
pub fn make_value(map: ObjMap) -> Value {
|
||||
use alloc::collections::BTreeMap;
|
||||
let mut btree = BTreeMap::new();
|
||||
for (k, v) in map {
|
||||
btree.insert(Value::String(k), v);
|
||||
}
|
||||
Value::Object(Rc::new(btree))
|
||||
}
|
||||
|
||||
/// Convert a `Vec<Value>` into a `Value::Array`.
|
||||
pub fn make_array(items: Vec<Value>) -> Value {
|
||||
Value::Array(Rc::new(items))
|
||||
}
|
||||
|
||||
/// Extract a `&str` from a `Value::String`.
|
||||
pub fn val_str(v: &Value) -> Option<&str> {
|
||||
match v {
|
||||
Value::String(s) => Some(s.as_ref()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract the `type` field value from a resource JSON object.
|
||||
///
|
||||
/// Performs a case-insensitive key lookup so both `"type"` and `"Type"` work.
|
||||
pub fn extract_type_field(resource: &Value) -> Option<&str> {
|
||||
resource.as_object().ok().and_then(|obj| {
|
||||
obj.iter()
|
||||
.find(|(k, _)| val_str(k).is_some_and(|s| s.eq_ignore_ascii_case("type")))
|
||||
.and_then(|(_, v)| val_str(v))
|
||||
})
|
||||
}
|
||||
|
||||
/// Convert a `Value::Object` (BTreeMap<Value, Value>) into an [`ObjMap`].
|
||||
///
|
||||
/// Non-string keys are silently skipped.
|
||||
#[allow(dead_code)]
|
||||
pub fn value_to_obj_map(value: &Value) -> Option<ObjMap> {
|
||||
let btree = value.as_object().ok()?;
|
||||
let mut map = ObjMap::with_capacity(btree.len());
|
||||
for (k, v) in btree.iter() {
|
||||
if let Value::String(s) = k {
|
||||
map.insert(Rc::clone(s), v.clone());
|
||||
}
|
||||
}
|
||||
Some(map)
|
||||
}
|
||||
|
||||
/// Set a value at a dot-separated path in an [`ObjMap`], creating
|
||||
/// intermediate `Value::Object` nodes as needed. All keys are lowercased.
|
||||
pub fn set_nested_lowercased(result: &mut ObjMap, path: &str, value: Value) {
|
||||
let segments: Vec<&str> = path.split('.').collect();
|
||||
if segments.is_empty() {
|
||||
return;
|
||||
}
|
||||
if segments.len() == 1 {
|
||||
if let Some(&seg) = segments.first() {
|
||||
obj_insert(result, &seg.to_ascii_lowercase(), value);
|
||||
}
|
||||
return;
|
||||
}
|
||||
// Build the nested structure from inside-out.
|
||||
set_nested_inner(result, &segments, value, true);
|
||||
}
|
||||
|
||||
/// Set a value at a dot-separated path in an [`ObjMap`], creating
|
||||
/// intermediate `Value::Object` nodes as needed. Keys preserve their casing.
|
||||
pub fn set_nested_verbatim(result: &mut ObjMap, path: &str, value: Value) {
|
||||
let segments: Vec<&str> = path.split('.').collect();
|
||||
if segments.is_empty() {
|
||||
return;
|
||||
}
|
||||
if segments.len() == 1 {
|
||||
if let Some(&seg) = segments.first() {
|
||||
obj_insert(result, seg, value);
|
||||
}
|
||||
return;
|
||||
}
|
||||
set_nested_inner(result, &segments, value, false);
|
||||
}
|
||||
|
||||
/// Core implementation of nested-set. Navigates the first N-1 segments,
|
||||
/// creating intermediate objects, then inserts the value at the last segment.
|
||||
fn set_nested_inner(obj: &mut ObjMap, segments: &[&str], value: Value, lowercase: bool) {
|
||||
let Some(&first) = segments.first() else {
|
||||
return;
|
||||
};
|
||||
|
||||
if segments.len() == 1 {
|
||||
let key = if lowercase {
|
||||
first.to_ascii_lowercase()
|
||||
} else {
|
||||
first.to_string()
|
||||
};
|
||||
obj_insert(obj, &key, value);
|
||||
return;
|
||||
}
|
||||
|
||||
let seg = if lowercase {
|
||||
first.to_ascii_lowercase()
|
||||
} else {
|
||||
first.to_string()
|
||||
};
|
||||
|
||||
// Ensure an intermediate object exists at `seg`.
|
||||
if !obj_contains(obj, &seg) {
|
||||
obj_insert(obj, &seg, make_value(new_map()));
|
||||
}
|
||||
|
||||
// Descend directly into the BTreeMap, avoiding ObjMap round-trip.
|
||||
if let Some(Value::Object(inner_rc)) = obj_get_mut(obj, &seg) {
|
||||
let inner_btree = Rc::make_mut(inner_rc);
|
||||
set_nested_in_btree(
|
||||
inner_btree,
|
||||
segments.get(1..).unwrap_or_default(),
|
||||
value,
|
||||
lowercase,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Set a value at a path directly in a `BTreeMap<Value, Value>`, creating
|
||||
/// intermediate `Value::Object` nodes as needed.
|
||||
///
|
||||
/// This avoids the `btree_to_obj_map` / `obj_map_to_btree` round-trip that
|
||||
/// would clone every sibling entry at each nesting level.
|
||||
pub fn set_nested_in_btree(
|
||||
btree: &mut alloc::collections::BTreeMap<Value, Value>,
|
||||
segments: &[&str],
|
||||
value: Value,
|
||||
lowercase: bool,
|
||||
) {
|
||||
let Some(&first) = segments.first() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let key_str: String = if lowercase {
|
||||
first.to_ascii_lowercase()
|
||||
} else {
|
||||
first.to_string()
|
||||
};
|
||||
let key_val = Value::String(Rc::from(key_str.as_str()));
|
||||
|
||||
if segments.len() == 1 {
|
||||
btree.insert(key_val, value);
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure an intermediate object exists.
|
||||
if !btree.contains_key(&key_val) {
|
||||
btree.insert(key_val.clone(), make_value(new_map()));
|
||||
}
|
||||
|
||||
if let Some(Value::Object(inner_rc)) = btree.get_mut(&key_val) {
|
||||
let inner = Rc::make_mut(inner_rc);
|
||||
set_nested_in_btree(
|
||||
inner,
|
||||
segments.get(1..).unwrap_or_default(),
|
||||
value,
|
||||
lowercase,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Fields that exist at the ARM resource root (not under `properties`).
|
||||
///
|
||||
/// These are the standard ARM resource envelope fields as defined by the
|
||||
/// Azure Resource Manager resource model. They are preserved at the
|
||||
/// resource root during normalization and denormalization.
|
||||
pub const ROOT_FIELDS: &[&str] = &[
|
||||
"name",
|
||||
"type",
|
||||
"location",
|
||||
"kind",
|
||||
"id",
|
||||
"tags",
|
||||
"identity",
|
||||
"sku",
|
||||
"plan",
|
||||
"zones",
|
||||
"managedBy",
|
||||
"etag",
|
||||
"apiVersion",
|
||||
"fullName",
|
||||
"systemData",
|
||||
"extendedLocation",
|
||||
];
|
||||
|
||||
/// Check whether an alias short name collides with a reserved ARM root field
|
||||
/// and needs a collision-safe key.
|
||||
pub fn is_root_field_collision(short_name: &str, default_path: &str) -> bool {
|
||||
ROOT_FIELDS
|
||||
.iter()
|
||||
.any(|f| f.eq_ignore_ascii_case(short_name))
|
||||
&& default_path.to_ascii_lowercase().starts_with("properties.")
|
||||
}
|
||||
|
||||
/// Return a collision-safe key for an alias whose short name collides with a
|
||||
/// root ARM field. The key is `_p_` + the lowercased short name.
|
||||
pub fn collision_safe_key(short_name: &str) -> String {
|
||||
alloc::format!("_p_{}", short_name.to_ascii_lowercase())
|
||||
}
|
||||
|
||||
// ─── Element-level field removal ────────────────────────────────────────────
|
||||
//
|
||||
// Shared by both normalizer (stale source cleanup after remap) and
|
||||
// denormalizer (cleanup after reverse remap).
|
||||
|
||||
/// Remove a (possibly dot-separated) field from each element of a (possibly
|
||||
/// nested) array, navigating via the given `array_chain`.
|
||||
pub fn remove_element_field(obj: &mut ObjMap, array_chain: &[Vec<String>], field: &str) {
|
||||
remove_field_at_depth(obj, array_chain, 0, field);
|
||||
}
|
||||
|
||||
fn remove_field_at_depth(obj: &mut ObjMap, array_chain: &[Vec<String>], depth: usize, field: &str) {
|
||||
let Some(nav) = array_chain.get(depth) else {
|
||||
let segments: Vec<&str> = field.split('.').collect();
|
||||
if segments.len() == 1 {
|
||||
if let Some(&seg) = segments.first() {
|
||||
obj_remove(obj, seg);
|
||||
}
|
||||
} else if segments.len() > 1 {
|
||||
remove_at_dotted_path(obj, &segments);
|
||||
}
|
||||
return;
|
||||
};
|
||||
|
||||
let first = match nav.first() {
|
||||
Some(f) => f.as_str(),
|
||||
None => return,
|
||||
};
|
||||
|
||||
let arr_val = if nav.len() == 1 {
|
||||
match obj_get_mut(obj, first) {
|
||||
Some(v) => v,
|
||||
None => return,
|
||||
}
|
||||
} else {
|
||||
let mut cur: &mut Value = match obj_get_mut(obj, first) {
|
||||
Some(v) => v,
|
||||
None => return,
|
||||
};
|
||||
for segment in nav.iter().skip(1) {
|
||||
cur = match cur.as_object_mut() {
|
||||
Ok(inner) => match inner.get_mut(&Value::from(segment.as_str())) {
|
||||
Some(v) => v,
|
||||
None => return,
|
||||
},
|
||||
Err(_) => return,
|
||||
};
|
||||
}
|
||||
cur
|
||||
};
|
||||
|
||||
if let Value::Array(elements) = arr_val {
|
||||
let inner = Rc::make_mut(elements);
|
||||
for elem in inner.iter_mut() {
|
||||
if let Value::Object(obj_rc) = elem {
|
||||
let inner_btree = Rc::make_mut(obj_rc);
|
||||
remove_field_at_depth_in_btree(
|
||||
inner_btree,
|
||||
array_chain,
|
||||
depth.saturating_add(1),
|
||||
field,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// BTreeMap-native recursion for element-level field removal.
|
||||
fn remove_field_at_depth_in_btree(
|
||||
btree: &mut alloc::collections::BTreeMap<Value, Value>,
|
||||
array_chain: &[Vec<String>],
|
||||
depth: usize,
|
||||
field: &str,
|
||||
) {
|
||||
let Some(nav) = array_chain.get(depth) else {
|
||||
let segments: Vec<&str> = field.split('.').collect();
|
||||
if segments.len() == 1 {
|
||||
if let Some(&seg) = segments.first() {
|
||||
btree.remove(&Value::from(seg));
|
||||
}
|
||||
} else if segments.len() > 1 {
|
||||
remove_at_dotted_path_in_btree(btree, &segments);
|
||||
}
|
||||
return;
|
||||
};
|
||||
|
||||
let first = match nav.first() {
|
||||
Some(f) => f.as_str(),
|
||||
None => return,
|
||||
};
|
||||
|
||||
let key_val = Value::from(first);
|
||||
let arr_val = if nav.len() == 1 {
|
||||
match btree.get_mut(&key_val) {
|
||||
Some(v) => v,
|
||||
None => return,
|
||||
}
|
||||
} else {
|
||||
let mut cur: &mut Value = match btree.get_mut(&key_val) {
|
||||
Some(v) => v,
|
||||
None => return,
|
||||
};
|
||||
for segment in nav.iter().skip(1) {
|
||||
cur = match cur.as_object_mut() {
|
||||
Ok(inner) => match inner.get_mut(&Value::from(segment.as_str())) {
|
||||
Some(v) => v,
|
||||
None => return,
|
||||
},
|
||||
Err(_) => return,
|
||||
};
|
||||
}
|
||||
cur
|
||||
};
|
||||
|
||||
if let Value::Array(elements) = arr_val {
|
||||
let inner = Rc::make_mut(elements);
|
||||
for elem in inner.iter_mut() {
|
||||
if let Value::Object(obj_rc) = elem {
|
||||
let inner_btree = Rc::make_mut(obj_rc);
|
||||
remove_field_at_depth_in_btree(
|
||||
inner_btree,
|
||||
array_chain,
|
||||
depth.saturating_add(1),
|
||||
field,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove the leaf segment at a dotted path directly in a BTreeMap.
|
||||
fn remove_at_dotted_path_in_btree(
|
||||
btree: &mut alloc::collections::BTreeMap<Value, Value>,
|
||||
segments: &[&str],
|
||||
) {
|
||||
let Some((&leaf, parent_segs)) = segments.split_last() else {
|
||||
return;
|
||||
};
|
||||
if parent_segs.is_empty() {
|
||||
btree.remove(&Value::from(leaf));
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(&first) = parent_segs.first() else {
|
||||
return;
|
||||
};
|
||||
let first_key = Value::from(first);
|
||||
let parent_val = match btree.get_mut(&first_key) {
|
||||
Some(v) => v,
|
||||
None => return,
|
||||
};
|
||||
|
||||
if parent_segs.len() == 1 {
|
||||
if let Value::Object(inner_rc) = parent_val {
|
||||
let inner_btree = Rc::make_mut(inner_rc);
|
||||
inner_btree.remove(&Value::from(leaf));
|
||||
}
|
||||
} else {
|
||||
let mut cur = parent_val;
|
||||
for &seg in parent_segs.iter().skip(1) {
|
||||
cur = match cur.as_object_mut() {
|
||||
Ok(inner) => match inner.get_mut(&Value::from(seg)) {
|
||||
Some(v) => v,
|
||||
None => return,
|
||||
},
|
||||
Err(_) => return,
|
||||
};
|
||||
}
|
||||
if let Value::Object(inner_rc) = cur {
|
||||
let inner_btree = Rc::make_mut(inner_rc);
|
||||
inner_btree.remove(&Value::from(leaf));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Remove the leaf segment at a dot-separated path from an ObjMap.
|
||||
fn remove_at_dotted_path(obj: &mut ObjMap, segments: &[&str]) {
|
||||
let Some((&leaf, parent_segs)) = segments.split_last() else {
|
||||
return;
|
||||
};
|
||||
if parent_segs.is_empty() {
|
||||
obj_remove(obj, leaf);
|
||||
return;
|
||||
}
|
||||
|
||||
let Some(&first) = parent_segs.first() else {
|
||||
return;
|
||||
};
|
||||
let parent_val = match obj_get_mut(obj, first) {
|
||||
Some(v) => v,
|
||||
None => return,
|
||||
};
|
||||
|
||||
if parent_segs.len() == 1 {
|
||||
if let Value::Object(inner_rc) = parent_val {
|
||||
let inner_btree = Rc::make_mut(inner_rc);
|
||||
inner_btree.remove(&Value::from(leaf));
|
||||
}
|
||||
} else {
|
||||
let mut cur = parent_val;
|
||||
for &seg in parent_segs.iter().skip(1) {
|
||||
cur = match cur.as_object_mut() {
|
||||
Ok(inner) => match inner.get_mut(&Value::from(seg)) {
|
||||
Some(v) => v,
|
||||
None => return,
|
||||
},
|
||||
Err(_) => return,
|
||||
};
|
||||
}
|
||||
if let Value::Object(inner_rc) = cur {
|
||||
let inner_btree = Rc::make_mut(inner_rc);
|
||||
inner_btree.remove(&Value::from(leaf));
|
||||
}
|
||||
}
|
||||
}
|
||||
686
src/languages/azure_policy/aliases/types.rs
Normal file
686
src/languages/azure_policy/aliases/types.rs
Normal file
@@ -0,0 +1,686 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
//! Data types for Azure Policy alias definitions.
|
||||
//!
|
||||
//! These types deserialize production alias catalog data from multiple sources:
|
||||
//! 1. ARM API response: `GET /providers?$expand=resourceTypes/aliases`
|
||||
//! 2. Static `ResourceTypesAndAliases.json` (used by PolicyTester)
|
||||
//! 3. `az provider list --expand resourceTypes/aliases` CLI output
|
||||
//! (where `defaultPath` may be serialized as `{ path, apiVersions }`)
|
||||
//!
|
||||
//! All data is captured for completeness; fields not yet used by the compiler
|
||||
//! are retained so the types stay in sync with the production schema.
|
||||
|
||||
use alloc::collections::{BTreeMap, BTreeSet};
|
||||
use alloc::string::String;
|
||||
use alloc::vec::Vec;
|
||||
|
||||
use serde::{Deserialize, Deserializer};
|
||||
|
||||
// ─── Top-level response wrappers ────────────────────────────────────────────
|
||||
|
||||
/// ARM API response envelope: `{ "value": [...] }`
|
||||
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
|
||||
pub struct ArmProvidersResponse {
|
||||
pub value: Vec<ProviderAliases>,
|
||||
/// Pagination link (ARM may paginate large responses).
|
||||
#[serde(rename = "nextLink", default)]
|
||||
pub next_link: Option<String>,
|
||||
}
|
||||
|
||||
// ─── Provider / resource type ───────────────────────────────────────────────
|
||||
|
||||
/// A resource provider's alias definitions.
|
||||
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
|
||||
pub struct ProviderAliases {
|
||||
/// The provider namespace (e.g., `"Microsoft.Storage"`).
|
||||
pub namespace: String,
|
||||
|
||||
/// Resource types with their alias entries.
|
||||
#[serde(default, rename = "resourceTypes")]
|
||||
pub resource_types: Vec<ResourceTypeAliases>,
|
||||
}
|
||||
|
||||
/// Aliases for a single resource type within a provider.
|
||||
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ResourceTypeAliases {
|
||||
/// The resource type name (e.g., `"storageAccounts"`).
|
||||
pub resource_type: String,
|
||||
|
||||
/// All alias entries for this resource type.
|
||||
#[serde(default)]
|
||||
pub aliases: Vec<AliasEntry>,
|
||||
|
||||
/// Resource capabilities as a comma-separated string
|
||||
/// (e.g., `"SupportsTags, SupportsLocation"`).
|
||||
#[serde(default)]
|
||||
pub capabilities: Option<String>,
|
||||
|
||||
/// Default API version for the resource type.
|
||||
#[serde(default)]
|
||||
pub default_api_version: Option<String>,
|
||||
}
|
||||
|
||||
// ─── Alias ──────────────────────────────────────────────────────────────────
|
||||
|
||||
/// A single alias entry within a resource type.
|
||||
#[derive(Debug, Clone, Default, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AliasEntry {
|
||||
/// Fully qualified alias name
|
||||
/// (e.g., `"Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly"`).
|
||||
pub name: String,
|
||||
|
||||
/// The default ARM JSON path used when no versioned path matches.
|
||||
///
|
||||
/// In most formats this is a plain string. The `az CLI` format serializes
|
||||
/// it as `{ "path": "...", "apiVersions": [...] }`. The custom
|
||||
/// deserializer accepts both, extracting just the path string.
|
||||
#[serde(default, deserialize_with = "deserialize_default_path")]
|
||||
pub default_path: Option<String>,
|
||||
|
||||
/// Optional metadata for the default path (type, modifiability).
|
||||
#[serde(default)]
|
||||
pub default_metadata: Option<AliasPathMetadata>,
|
||||
|
||||
/// Extraction pattern for the default path (present in ARM responses for
|
||||
/// some aliases; absent from the static file).
|
||||
#[serde(default)]
|
||||
pub default_pattern: Option<AliasPattern>,
|
||||
|
||||
/// Alias-level type as a comma-separated string of flags:
|
||||
/// `"PlainText"`, `"Mask"`, `"Deprecated"`, `"Preview"`, or combinations
|
||||
/// like `"Mask, Deprecated"`. `None` when absent.
|
||||
#[serde(default, rename = "type")]
|
||||
pub alias_type: Option<String>,
|
||||
|
||||
/// Versioned path entries. Empty for the vast majority of aliases that
|
||||
/// have only a `defaultPath`.
|
||||
#[serde(default)]
|
||||
pub paths: Vec<AliasPath>,
|
||||
}
|
||||
|
||||
// ─── Alias path ─────────────────────────────────────────────────────────────
|
||||
|
||||
/// A versioned path mapping for an alias.
|
||||
///
|
||||
/// When an alias maps to different ARM JSON paths across API versions, each
|
||||
/// distinct path is recorded as an `AliasPath` entry.
|
||||
#[derive(Debug, Clone, Default, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AliasPath {
|
||||
/// The ARM JSON path (e.g., `"properties.encryption.services.blob.enabled"`).
|
||||
pub path: String,
|
||||
|
||||
/// API versions for which this path is valid. Empty means all versions.
|
||||
#[serde(default)]
|
||||
pub api_versions: Vec<String>,
|
||||
|
||||
/// Optional per-version metadata.
|
||||
#[serde(default)]
|
||||
pub metadata: Option<AliasPathMetadata>,
|
||||
|
||||
/// Extraction pattern for this specific path.
|
||||
#[serde(default)]
|
||||
pub pattern: Option<AliasPattern>,
|
||||
}
|
||||
|
||||
// ─── Alias path metadata ───────────────────────────────────────────────────
|
||||
|
||||
/// Metadata associated with an alias path (default or versioned).
|
||||
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
|
||||
pub struct AliasPathMetadata {
|
||||
/// The data type of the alias value as a string token
|
||||
/// (e.g., `"String"`, `"Integer"`, `"Boolean"`, `"Array"`, `"Object"`).
|
||||
#[serde(rename = "type")]
|
||||
pub kind: Option<String>,
|
||||
|
||||
/// Attribute flags as a comma-separated string
|
||||
/// (e.g., `"Modifiable"`, `"Modifiable, SupportsCreate, SupportsRead"`).
|
||||
pub attributes: Option<String>,
|
||||
}
|
||||
|
||||
// ─── Alias pattern ──────────────────────────────────────────────────────────
|
||||
|
||||
/// Extraction pattern for an alias path (URI template or regex).
|
||||
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct AliasPattern {
|
||||
/// The pattern phrase (URI template or regex string).
|
||||
pub phrase: String,
|
||||
|
||||
/// The variable to extract from the pattern.
|
||||
#[serde(default)]
|
||||
pub variable: Option<String>,
|
||||
|
||||
/// Pattern type (e.g., `"Extract"`).
|
||||
#[serde(default, rename = "type")]
|
||||
pub pattern_type: Option<String>,
|
||||
}
|
||||
|
||||
// ─── Custom deserializer: defaultPath (string or object) ────────────────────
|
||||
|
||||
/// Accepts either a plain string `"properties.foo"` or an az CLI object
|
||||
/// `{ "path": "properties.foo", "apiVersions": [...] }`, extracting just the
|
||||
/// path string. Handles `null` gracefully.
|
||||
fn deserialize_default_path<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
|
||||
where
|
||||
D: Deserializer<'de>,
|
||||
{
|
||||
#[derive(Deserialize)]
|
||||
#[serde(untagged)]
|
||||
enum RawDefaultPath {
|
||||
Str(String),
|
||||
Obj {
|
||||
path: String,
|
||||
#[serde(default, rename = "apiVersions")]
|
||||
_api_versions: Vec<String>,
|
||||
},
|
||||
Null,
|
||||
}
|
||||
|
||||
match Option::<RawDefaultPath>::deserialize(deserializer)? {
|
||||
None | Some(RawDefaultPath::Null) => Ok(None),
|
||||
Some(RawDefaultPath::Str(s)) => Ok(Some(s)),
|
||||
Some(RawDefaultPath::Obj { path, .. }) => Ok(Some(path)),
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Data Policy Manifest types (data-plane aliases) ────────────────────────
|
||||
|
||||
/// A single alias entry in a data policy manifest.
|
||||
///
|
||||
/// Unlike control-plane [`AliasEntry`], data-plane aliases have no
|
||||
/// `defaultPath` — the path is always taken from `paths[0].path`.
|
||||
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
|
||||
pub struct DataManifestAlias {
|
||||
/// Fully qualified alias name
|
||||
/// (e.g., `"Microsoft.KeyVault.Data/vaults/certificates/attributes.expiresOn"`).
|
||||
pub name: String,
|
||||
|
||||
/// Versioned path entries. `paths[0].path` serves as the default path.
|
||||
#[serde(default)]
|
||||
pub paths: Vec<DataManifestAliasPath>,
|
||||
}
|
||||
|
||||
/// A path entry in a data policy manifest alias.
|
||||
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DataManifestAliasPath {
|
||||
/// The ARM JSON path (e.g., `"attributes.expiresOn"`).
|
||||
pub path: String,
|
||||
|
||||
/// API versions for which this path is valid.
|
||||
#[serde(default)]
|
||||
pub api_versions: Vec<String>,
|
||||
|
||||
/// Schema versions for which this path is valid (used by some data-plane
|
||||
/// providers instead of `apiVersions`).
|
||||
#[serde(default)]
|
||||
pub schema_versions: Vec<String>,
|
||||
}
|
||||
|
||||
/// Per-resource-type alias group in a data policy manifest.
|
||||
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DataManifestResourceTypeAliases {
|
||||
/// Resource type suffix (e.g., `"vaults/certificates"`).
|
||||
pub resource_type: String,
|
||||
|
||||
/// Aliases for this resource type.
|
||||
#[serde(default)]
|
||||
pub aliases: Vec<DataManifestAlias>,
|
||||
}
|
||||
|
||||
/// A data policy manifest describing data-plane aliases for a namespace.
|
||||
///
|
||||
/// This format is used by `dataPolicyManifests/` files, as opposed to the
|
||||
/// control-plane `ProviderAliases` format used by `Get-AzPolicyAlias`.
|
||||
#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct DataPolicyManifest {
|
||||
/// The data namespace (e.g., `"Microsoft.KeyVault.Data"`).
|
||||
pub data_namespace: String,
|
||||
|
||||
/// Top-level aliases not scoped to a specific resource type.
|
||||
#[serde(default)]
|
||||
pub aliases: Vec<DataManifestAlias>,
|
||||
|
||||
/// Per-resource-type alias groups.
|
||||
#[serde(default)]
|
||||
pub resource_type_aliases: Vec<DataManifestResourceTypeAliases>,
|
||||
}
|
||||
|
||||
// ─── Convenience loading functions ──────────────────────────────────────────
|
||||
|
||||
/// Load from the static `ResourceTypesAndAliases.json` file (bare array).
|
||||
pub fn load_from_static_file(json: &str) -> Result<Vec<ProviderAliases>, serde_json::Error> {
|
||||
serde_json::from_str(json)
|
||||
}
|
||||
|
||||
/// Load from an ARM API `GET /providers` response (`{ "value": [...] }`).
|
||||
pub fn load_from_arm_response(json: &str) -> Result<Vec<ProviderAliases>, serde_json::Error> {
|
||||
let resp: ArmProvidersResponse = serde_json::from_str(json)?;
|
||||
Ok(resp.value)
|
||||
}
|
||||
|
||||
/// Load from either format: tries ARM envelope first, then bare array.
|
||||
pub fn load_auto(json: &str) -> Result<Vec<ProviderAliases>, serde_json::Error> {
|
||||
let trimmed = json.trim_start();
|
||||
if trimmed.starts_with('[') {
|
||||
load_from_static_file(json)
|
||||
} else {
|
||||
load_from_arm_response(json)
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Utility methods ────────────────────────────────────────────────────────
|
||||
|
||||
impl AliasEntry {
|
||||
/// Returns `true` if this alias is marked as deprecated (case-insensitive).
|
||||
pub fn is_deprecated(&self) -> bool {
|
||||
has_flag(self.alias_type.as_deref(), "Deprecated")
|
||||
}
|
||||
|
||||
/// Returns `true` if this alias is marked as preview.
|
||||
pub fn is_preview(&self) -> bool {
|
||||
has_flag(self.alias_type.as_deref(), "Preview")
|
||||
}
|
||||
|
||||
/// Returns `true` if this alias's value should be masked (secret).
|
||||
pub fn is_secret(&self) -> bool {
|
||||
has_flag(self.alias_type.as_deref(), "Mask")
|
||||
}
|
||||
|
||||
/// Returns the effective path string (defaultPath or first versioned path).
|
||||
pub fn effective_path(&self) -> Option<&str> {
|
||||
self.default_path
|
||||
.as_deref()
|
||||
.or_else(|| self.paths.first().map(|p| p.path.as_str()))
|
||||
}
|
||||
}
|
||||
|
||||
impl AliasPathMetadata {
|
||||
/// Returns `true` if this path supports modification (Modifiable flag).
|
||||
pub fn is_modifiable(&self) -> bool {
|
||||
has_flag(self.attributes.as_deref(), "Modifiable")
|
||||
}
|
||||
}
|
||||
|
||||
/// Check whether a comma-separated flags string contains a specific flag
|
||||
/// (case-insensitive).
|
||||
pub(crate) fn has_flag(flags: Option<&str>, flag: &str) -> bool {
|
||||
flags.is_some_and(|s| {
|
||||
s.split(',')
|
||||
.any(|part| part.trim().eq_ignore_ascii_case(flag))
|
||||
})
|
||||
}
|
||||
|
||||
/// Parsed alias data for a single resource type, keyed by short name.
|
||||
///
|
||||
/// The short name is derived by stripping the resource type prefix from the
|
||||
/// fully qualified alias name:
|
||||
/// `Microsoft.Storage/storageAccounts/sku.name` → `sku.name`
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ResolvedAliases {
|
||||
/// Fully qualified resource type (e.g., `"Microsoft.Storage/storageAccounts"`).
|
||||
pub resource_type: String,
|
||||
/// Map from alias short name (case-insensitive key, stored lowercase) to
|
||||
/// the resolved ARM path.
|
||||
pub entries: BTreeMap<String, ResolvedEntry>,
|
||||
/// Array field names whose elements are sub-resources (have their own
|
||||
/// `properties` wrapper to flatten). Stored pre-lowercased so consumers
|
||||
/// can look up directly without per-call allocation.
|
||||
pub sub_resource_arrays: BTreeSet<String>,
|
||||
|
||||
// ── Precomputed aggregate fields ────────────────────────────────────
|
||||
/// Precomputed aggregates for the default path (api_version = None).
|
||||
pub default_aggregates: VersionedAggregates,
|
||||
/// Precomputed aggregates keyed by lowercase api_version string.
|
||||
/// Computed at registry-load time for every distinct version found in
|
||||
/// any entry's `versioned_paths`.
|
||||
pub versioned_aggregates: BTreeMap<String, VersionedAggregates>,
|
||||
}
|
||||
|
||||
/// Precomputed aggregate data for a specific API version, or for the default path.
|
||||
///
|
||||
/// Stored at [`ResolvedAliases`] level so it is computed once at registry-load
|
||||
/// time rather than reconstructed on every normalize/denormalize call.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Eq)]
|
||||
pub struct VersionedAggregates {
|
||||
/// Precomputed element-level field remaps for wildcard aliases.
|
||||
pub element_remaps: Vec<PrecomputedRemap>,
|
||||
/// Precomputed reverse element remaps for denormalization.
|
||||
pub reverse_element_remaps: Vec<PrecomputedReverseRemap>,
|
||||
/// Deduplicated array base renames for normalization: `(arm_base_lc, short_base_lc)`.
|
||||
pub array_renames_normalize: Vec<(String, String)>,
|
||||
/// Deduplicated array base renames for denormalization: `(short_base_lc, arm_base)`.
|
||||
pub array_renames_denormalize: Vec<(String, String)>,
|
||||
}
|
||||
|
||||
/// A precomputed element-level field remap, stored at `ResolvedAliases` level
|
||||
/// so it's computed once at registry-load time rather than per-call.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct PrecomputedRemap {
|
||||
/// Chain of array navigations for nested `[*]` levels.
|
||||
pub array_chain: Vec<Vec<String>>,
|
||||
/// Field to read within each element.
|
||||
pub source_field: String,
|
||||
/// Field to write within each element.
|
||||
pub target_field: String,
|
||||
}
|
||||
|
||||
/// A precomputed reverse remap for denormalization, including the forward
|
||||
/// target field name for cleanup after remapping.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct PrecomputedReverseRemap {
|
||||
/// Chain of array navigations for nested `[*]` levels.
|
||||
pub array_chain: Vec<Vec<String>>,
|
||||
/// Field to read within each element (was the forward target).
|
||||
pub source_field: String,
|
||||
/// Field to write within each element (was the forward source).
|
||||
pub target_field: String,
|
||||
/// The forward target field to remove after remapping.
|
||||
pub cleanup_field: String,
|
||||
}
|
||||
|
||||
/// A resolved alias entry with its default path and optional versioned paths.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ResolvedEntry {
|
||||
/// The original-cased alias short name (e.g., `"accountType"`, not
|
||||
/// `"accounttype"`). The entries map uses lowercase keys for
|
||||
/// case-insensitive lookup, but the normalizer needs the original casing
|
||||
/// to write values at correctly-cased paths in the output.
|
||||
pub short_name: String,
|
||||
/// The default ARM JSON path.
|
||||
pub default_path: String,
|
||||
/// Versioned path overrides: `(api_version, arm_path)` pairs.
|
||||
pub versioned_paths: Vec<(String, String)>,
|
||||
/// Optional metadata from the alias catalog (type, modifiability).
|
||||
pub metadata: Option<AliasPathMetadata>,
|
||||
|
||||
// ── Precomputed fields (derived at registry-load time) ──────────────
|
||||
/// Whether `short_name` contains `[*]` (i.e., this is a wildcard/array alias).
|
||||
pub is_wildcard: bool,
|
||||
/// Precomputed `default_path.split('.').collect()` for fast ARM path navigation.
|
||||
pub default_path_segments: Vec<String>,
|
||||
/// Precomputed path segments for each versioned path, in the same order
|
||||
/// as `versioned_paths`.
|
||||
pub versioned_path_segments: Vec<Vec<String>>,
|
||||
}
|
||||
|
||||
impl ResolvedEntry {
|
||||
/// Build a `ResolvedEntry` and precompute derived fields.
|
||||
pub fn new(
|
||||
short_name: String,
|
||||
default_path: String,
|
||||
versioned_paths: Vec<(String, String)>,
|
||||
metadata: Option<AliasPathMetadata>,
|
||||
) -> Self {
|
||||
let is_wildcard = short_name.contains("[*]");
|
||||
let default_path_segments = default_path.split('.').map(String::from).collect();
|
||||
let versioned_path_segments = versioned_paths
|
||||
.iter()
|
||||
.map(|(_, p)| p.split('.').map(String::from).collect())
|
||||
.collect();
|
||||
Self {
|
||||
short_name,
|
||||
default_path,
|
||||
versioned_paths,
|
||||
metadata,
|
||||
is_wildcard,
|
||||
default_path_segments,
|
||||
versioned_path_segments,
|
||||
}
|
||||
}
|
||||
|
||||
/// Select the ARM path for a given API version.
|
||||
///
|
||||
/// If `api_version` is `Some` and matches a versioned path, returns that
|
||||
/// path. Otherwise returns the `default_path`.
|
||||
pub fn select_path(&self, api_version: Option<&str>) -> &str {
|
||||
if let Some(ver) = api_version {
|
||||
for (v, path) in &self.versioned_paths {
|
||||
if v.eq_ignore_ascii_case(ver) {
|
||||
return path;
|
||||
}
|
||||
}
|
||||
}
|
||||
&self.default_path
|
||||
}
|
||||
|
||||
/// Select pre-tokenized path segments for a given API version.
|
||||
///
|
||||
/// Returns the versioned segments if `api_version` matches, otherwise
|
||||
/// the default segments. This avoids per-call `split('.')` for both
|
||||
/// default and versioned scalar alias navigation.
|
||||
pub fn select_path_segments(&self, api_version: Option<&str>) -> &[String] {
|
||||
if let Some(ver) = api_version {
|
||||
for (i, (v, _)) in self.versioned_paths.iter().enumerate() {
|
||||
if v.eq_ignore_ascii_case(ver) {
|
||||
if let Some(segs) = self.versioned_path_segments.get(i) {
|
||||
return segs;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
&self.default_path_segments
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(clippy::indexing_slicing, clippy::unwrap_used)]
|
||||
mod tests {
|
||||
use alloc::string::ToString as _;
|
||||
use alloc::vec;
|
||||
|
||||
use super::*;
|
||||
|
||||
fn make_entry(short: &str, default: &str, versioned: Vec<(&str, &str)>) -> ResolvedEntry {
|
||||
ResolvedEntry::new(
|
||||
short.to_string(),
|
||||
default.to_string(),
|
||||
versioned
|
||||
.into_iter()
|
||||
.map(|(v, p)| (v.to_string(), p.to_string()))
|
||||
.collect(),
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_path_no_version_returns_default() {
|
||||
let entry = make_entry(
|
||||
"enabled",
|
||||
"properties.enabled",
|
||||
vec![("2020-01-01", "properties.isEnabled")],
|
||||
);
|
||||
assert_eq!(entry.select_path(None), "properties.enabled");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_path_matching_version() {
|
||||
let entry = make_entry(
|
||||
"enabled",
|
||||
"properties.enabled",
|
||||
vec![
|
||||
("2020-01-01", "properties.isEnabled"),
|
||||
("2021-06-01", "properties.enabled"),
|
||||
],
|
||||
);
|
||||
assert_eq!(
|
||||
entry.select_path(Some("2020-01-01")),
|
||||
"properties.isEnabled"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_path_no_matching_version_returns_default() {
|
||||
let entry = make_entry(
|
||||
"enabled",
|
||||
"properties.enabled",
|
||||
vec![("2020-01-01", "properties.isEnabled")],
|
||||
);
|
||||
assert_eq!(entry.select_path(Some("9999-01-01")), "properties.enabled");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_path_case_insensitive_version() {
|
||||
let entry = make_entry(
|
||||
"enabled",
|
||||
"properties.enabled",
|
||||
vec![("2020-01-01-Preview", "properties.isEnabled")],
|
||||
);
|
||||
assert_eq!(
|
||||
entry.select_path(Some("2020-01-01-preview")),
|
||||
"properties.isEnabled"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_path_empty_versioned_paths() {
|
||||
let entry = make_entry("enabled", "properties.enabled", vec![]);
|
||||
assert_eq!(entry.select_path(Some("2020-01-01")), "properties.enabled");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_provider_aliases() {
|
||||
let json = r#"{
|
||||
"namespace": "Microsoft.Storage",
|
||||
"resourceTypes": [
|
||||
{
|
||||
"resourceType": "storageAccounts",
|
||||
"aliases": [
|
||||
{
|
||||
"name": "Microsoft.Storage/storageAccounts/sku.name",
|
||||
"defaultPath": "sku.name",
|
||||
"defaultMetadata": { "type": "String", "attributes": "Modifiable" },
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Storage/storageAccounts/accessTier",
|
||||
"defaultPath": "properties.accessTier",
|
||||
"paths": [
|
||||
{
|
||||
"path": "properties.accessTier",
|
||||
"apiVersions": ["2021-01-01", "2020-08-01-preview"],
|
||||
"metadata": { "type": "String" }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}"#;
|
||||
|
||||
let provider: ProviderAliases = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(provider.namespace, "Microsoft.Storage");
|
||||
assert_eq!(provider.resource_types.len(), 1);
|
||||
|
||||
let rt = &provider.resource_types[0];
|
||||
assert_eq!(rt.resource_type, "storageAccounts");
|
||||
assert_eq!(rt.aliases.len(), 2);
|
||||
|
||||
let sku_alias = &rt.aliases[0];
|
||||
assert_eq!(sku_alias.default_path.as_deref(), Some("sku.name"));
|
||||
assert!(sku_alias.paths.is_empty());
|
||||
|
||||
let access_alias = &rt.aliases[1];
|
||||
assert_eq!(access_alias.paths.len(), 1);
|
||||
assert_eq!(access_alias.paths[0].api_versions.len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_alias_metadata() {
|
||||
let json = r#"{
|
||||
"name": "test/alias",
|
||||
"defaultPath": "properties.value",
|
||||
"defaultMetadata": { "type": "Integer", "attributes": "None" },
|
||||
"paths": []
|
||||
}"#;
|
||||
let entry: AliasEntry = serde_json::from_str(json).unwrap();
|
||||
let meta = entry.default_metadata.unwrap();
|
||||
assert_eq!(meta.kind.as_deref(), Some("Integer"));
|
||||
assert_eq!(meta.attributes.as_deref(), Some("None"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_az_cli_default_path_object() {
|
||||
let json = r#"{
|
||||
"name": "Microsoft.Compute/virtualMachines/sku.name",
|
||||
"defaultPath": {
|
||||
"path": "properties.hardwareProfile.vmSize",
|
||||
"apiVersions": ["2024-07-01"]
|
||||
},
|
||||
"paths": []
|
||||
}"#;
|
||||
let entry: AliasEntry = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(
|
||||
entry.default_path.as_deref(),
|
||||
Some("properties.hardwareProfile.vmSize")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alias_type_flags() {
|
||||
let json = r#"{
|
||||
"name": "test",
|
||||
"type": "Mask, Deprecated",
|
||||
"defaultMetadata": { "type": "String", "attributes": "Modifiable, SupportsCreate" },
|
||||
"paths": []
|
||||
}"#;
|
||||
let entry: AliasEntry = serde_json::from_str(json).unwrap();
|
||||
assert!(entry.is_secret());
|
||||
assert!(entry.is_deprecated());
|
||||
assert!(!entry.is_preview());
|
||||
assert!(entry.default_metadata.as_ref().unwrap().is_modifiable());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_auto_detects_format() {
|
||||
let array_json = r#"[{"namespace":"N","resourceTypes":[]}]"#;
|
||||
let arm_json = r#"{"value":[{"namespace":"N","resourceTypes":[]}]}"#;
|
||||
assert_eq!(load_auto(array_json).unwrap()[0].namespace, "N");
|
||||
assert_eq!(load_auto(arm_json).unwrap()[0].namespace, "N");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_pattern() {
|
||||
let json = r#"{
|
||||
"name": "test",
|
||||
"defaultPath": "p",
|
||||
"paths": [{
|
||||
"path": "p",
|
||||
"apiVersions": ["2020-01-01"],
|
||||
"pattern": {
|
||||
"phrase": "/Subscriptions/{sub}/Providers/{prov}",
|
||||
"variable": "prov",
|
||||
"type": "Extract"
|
||||
}
|
||||
}]
|
||||
}"#;
|
||||
let entry: AliasEntry = serde_json::from_str(json).unwrap();
|
||||
let pattern = entry.paths[0].pattern.as_ref().unwrap();
|
||||
assert_eq!(pattern.pattern_type.as_deref(), Some("Extract"));
|
||||
assert_eq!(pattern.variable.as_deref(), Some("prov"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn capabilities_preserved() {
|
||||
let json = r#"{
|
||||
"namespace": "NS",
|
||||
"resourceTypes": [{
|
||||
"resourceType": "rt",
|
||||
"capabilities": "SupportsTags, SupportsLocation",
|
||||
"aliases": []
|
||||
}]
|
||||
}"#;
|
||||
let provider: ProviderAliases = serde_json::from_str(json).unwrap();
|
||||
assert_eq!(
|
||||
provider.resource_types[0].capabilities.as_deref(),
|
||||
Some("SupportsTags, SupportsLocation")
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3,4 +3,6 @@
|
||||
|
||||
//! Azure Policy language support.
|
||||
|
||||
#[allow(clippy::pattern_type_mismatch)]
|
||||
pub mod aliases;
|
||||
pub mod strings;
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
//! Language-specific modules for specialized parsing and evaluation
|
||||
|
||||
#[cfg(feature = "azure-rbac")]
|
||||
pub mod azure_rbac;
|
||||
2900
tests/azure_policy/aliases/test_aliases.json
Normal file
2900
tests/azure_policy/aliases/test_aliases.json
Normal file
File diff suppressed because it is too large
Load Diff
230
tests/azure_policy/aliases/versioned_aliases.json
Normal file
230
tests/azure_policy/aliases/versioned_aliases.json
Normal file
@@ -0,0 +1,230 @@
|
||||
[
|
||||
{
|
||||
"namespace": "Microsoft.Test",
|
||||
"resourceTypes": [
|
||||
{
|
||||
"resourceType": "versionedResources",
|
||||
"aliases": [
|
||||
{
|
||||
"name": "Microsoft.Test/versionedResources/simpleProp",
|
||||
"defaultPath": "properties.simpleProp",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Test/versionedResources/accountType",
|
||||
"defaultPath": "properties.accountType",
|
||||
"paths": [
|
||||
{
|
||||
"path": "properties.accountType",
|
||||
"apiVersions": ["2015-06-15", "2015-01-01"]
|
||||
},
|
||||
{
|
||||
"path": "sku.name",
|
||||
"apiVersions": ["2020-01-01", "2019-06-01"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Test/versionedResources/config.threshold",
|
||||
"defaultPath": "properties.config.threshold",
|
||||
"paths": [
|
||||
{
|
||||
"path": "properties.config.properties.threshold",
|
||||
"apiVersions": ["2015-06-15", "2015-01-01"]
|
||||
},
|
||||
{
|
||||
"path": "properties.config.threshold",
|
||||
"apiVersions": ["2020-01-01", "2019-06-01"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Test/versionedResources/tier",
|
||||
"defaultPath": "sku.tier",
|
||||
"paths": [
|
||||
{
|
||||
"path": "properties.pricingTier",
|
||||
"apiVersions": ["2015-06-15", "2015-01-01"]
|
||||
},
|
||||
{
|
||||
"path": "sku.tier",
|
||||
"apiVersions": ["2020-01-01", "2019-06-01"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Test/versionedResources/operationMode",
|
||||
"defaultPath": "properties.mode",
|
||||
"paths": [
|
||||
{
|
||||
"path": "properties.legacyMode",
|
||||
"apiVersions": ["2015-01-01"]
|
||||
},
|
||||
{
|
||||
"path": "properties.settings.mode",
|
||||
"apiVersions": ["2017-06-01"]
|
||||
},
|
||||
{
|
||||
"path": "properties.mode",
|
||||
"apiVersions": ["2020-01-01"]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"resourceType": "complexResources",
|
||||
"aliases": [
|
||||
{
|
||||
"name": "Microsoft.Test/complexResources/enabled",
|
||||
"defaultPath": "properties.enabled",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Test/complexResources/tags",
|
||||
"defaultPath": "tags",
|
||||
"paths": []
|
||||
},
|
||||
|
||||
{
|
||||
"name": "Microsoft.Test/complexResources/rules",
|
||||
"defaultPath": "properties.rules",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Test/complexResources/rules[*]",
|
||||
"defaultPath": "properties.rules[*]",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Test/complexResources/rules[*].name",
|
||||
"defaultPath": "properties.rules[*].name",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Test/complexResources/rules[*].priority",
|
||||
"defaultPath": "properties.rules[*].properties.priority",
|
||||
"paths": [
|
||||
{
|
||||
"path": "properties.rules[*].properties.priority",
|
||||
"apiVersions": ["2015-01-01"]
|
||||
},
|
||||
{
|
||||
"path": "properties.rules[*].properties.prio",
|
||||
"apiVersions": ["2020-01-01"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Test/complexResources/rules[*].action",
|
||||
"defaultPath": "properties.rules[*].properties.action",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Test/complexResources/rules[*].protocol",
|
||||
"defaultPath": "properties.rules[*].properties.protocol",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Test/complexResources/rules[*].sourcePort",
|
||||
"defaultPath": "properties.rules[*].properties.sourcePort",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Test/complexResources/rules[*].destPort",
|
||||
"defaultPath": "properties.rules[*].properties.destPort",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Test/complexResources/rules[*].direction",
|
||||
"defaultPath": "properties.rules[*].properties.direction",
|
||||
"paths": []
|
||||
},
|
||||
|
||||
{
|
||||
"name": "Microsoft.Test/complexResources/rules[*].targets",
|
||||
"defaultPath": "properties.rules[*].properties.targets",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Test/complexResources/rules[*].targets[*]",
|
||||
"defaultPath": "properties.rules[*].properties.targets[*]",
|
||||
"paths": []
|
||||
},
|
||||
|
||||
{
|
||||
"name": "Microsoft.Test/complexResources/rules[*].filters",
|
||||
"defaultPath": "properties.rules[*].properties.filters",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Test/complexResources/rules[*].filters[*]",
|
||||
"defaultPath": "properties.rules[*].properties.filters[*]",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Test/complexResources/rules[*].filters[*].name",
|
||||
"defaultPath": "properties.rules[*].properties.filters[*].properties.name",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Test/complexResources/rules[*].filters[*].value",
|
||||
"defaultPath": "properties.rules[*].properties.filters[*].properties.value",
|
||||
"paths": []
|
||||
},
|
||||
|
||||
{
|
||||
"name": "Microsoft.Test/complexResources/allowedIPs",
|
||||
"defaultPath": "properties.allowedIPs",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Test/complexResources/allowedIPs[*]",
|
||||
"defaultPath": "properties.allowedIPs[*]",
|
||||
"paths": []
|
||||
},
|
||||
|
||||
{
|
||||
"name": "Microsoft.Test/complexResources/config.retryCount",
|
||||
"defaultPath": "properties.config.retryCount",
|
||||
"paths": [
|
||||
{
|
||||
"path": "properties.config.properties.retryCount",
|
||||
"apiVersions": ["2015-01-01"]
|
||||
},
|
||||
{
|
||||
"path": "properties.config.retryCount",
|
||||
"apiVersions": ["2020-01-01"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Test/complexResources/config.timeout",
|
||||
"defaultPath": "properties.config.timeout",
|
||||
"paths": []
|
||||
},
|
||||
|
||||
{
|
||||
"name": "Microsoft.Test/complexResources/settings.encryption.enabled",
|
||||
"defaultPath": "properties.settings.encryption.enabled",
|
||||
"paths": [
|
||||
{
|
||||
"path": "properties.settings.encryption.enabled",
|
||||
"apiVersions": ["2020-01-01"]
|
||||
},
|
||||
{
|
||||
"path": "properties.encryptionEnabled",
|
||||
"apiVersions": ["2015-01-01"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Test/complexResources/settings.encryption.keyVaultId",
|
||||
"defaultPath": "properties.settings.encryption.keyVaultId",
|
||||
"paths": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
4
tests/azure_policy/mod.rs
Normal file
4
tests/azure_policy/mod.rs
Normal file
@@ -0,0 +1,4 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
mod normalization;
|
||||
170
tests/azure_policy/normalization/cases/data_plane_advanced.yaml
Normal file
170
tests/azure_policy/normalization/cases/data_plane_advanced.yaml
Normal file
@@ -0,0 +1,170 @@
|
||||
# Extended data-plane manifest tests covering shapes beyond the basic
|
||||
# KeyVault case: multiple resource types, top-level aliases, nested
|
||||
# objects, array aliases, and schemaVersions.
|
||||
|
||||
data_manifest_json: |
|
||||
{
|
||||
"dataNamespace": "Microsoft.DataFactory.Data",
|
||||
"aliases": [
|
||||
{
|
||||
"name": "Microsoft.DataFactory.Data/factories/pipelines/enabled",
|
||||
"paths": [
|
||||
{
|
||||
"path": "enabled",
|
||||
"apiVersions": ["2018-06-01"]
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"resourceTypeAliases": [
|
||||
{
|
||||
"resourceType": "factories/pipelines",
|
||||
"aliases": [
|
||||
{
|
||||
"name": "Microsoft.DataFactory.Data/factories/pipelines/activities[*].type",
|
||||
"paths": [
|
||||
{
|
||||
"path": "activities[*].type",
|
||||
"apiVersions": ["2018-06-01"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.DataFactory.Data/factories/pipelines/activities[*].name",
|
||||
"paths": [
|
||||
{
|
||||
"path": "activities[*].name",
|
||||
"apiVersions": ["2018-06-01"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.DataFactory.Data/factories/pipelines/concurrency",
|
||||
"paths": [
|
||||
{
|
||||
"path": "concurrency",
|
||||
"apiVersions": ["2018-06-01"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.DataFactory.Data/factories/pipelines/folder.name",
|
||||
"paths": [
|
||||
{
|
||||
"path": "folder.name",
|
||||
"apiVersions": ["2018-06-01"]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"resourceType": "factories/datasets",
|
||||
"aliases": [
|
||||
{
|
||||
"name": "Microsoft.DataFactory.Data/factories/datasets/linkedServiceName.referenceName",
|
||||
"paths": [
|
||||
{
|
||||
"path": "linkedServiceName.referenceName",
|
||||
"schemaVersions": ["1"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.DataFactory.Data/factories/datasets/description",
|
||||
"paths": [
|
||||
{
|
||||
"path": "description",
|
||||
"schemaVersions": ["1"]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
cases:
|
||||
- note: data-plane pipeline with top-level alias and scalar fields
|
||||
use_registry_api: true
|
||||
input:
|
||||
type: "Microsoft.DataFactory.Data/factories/pipelines"
|
||||
enabled: true
|
||||
concurrency: 5
|
||||
folder:
|
||||
name: "etl-jobs"
|
||||
expected_normalized:
|
||||
type: "Microsoft.DataFactory.Data/factories/pipelines"
|
||||
enabled: true
|
||||
concurrency: 5
|
||||
folder:
|
||||
name: "etl-jobs"
|
||||
round_trip: true
|
||||
|
||||
- note: data-plane pipeline with array alias
|
||||
use_registry_api: true
|
||||
input:
|
||||
type: "Microsoft.DataFactory.Data/factories/pipelines"
|
||||
activities:
|
||||
- name: CopyActivity1
|
||||
type: Copy
|
||||
- name: WaitActivity1
|
||||
type: Wait
|
||||
expected_normalized:
|
||||
type: "Microsoft.DataFactory.Data/factories/pipelines"
|
||||
activities:
|
||||
- name: CopyActivity1
|
||||
type: Copy
|
||||
- name: WaitActivity1
|
||||
type: Wait
|
||||
round_trip: true
|
||||
|
||||
- note: data-plane dataset with nested alias path
|
||||
use_registry_api: true
|
||||
input:
|
||||
type: "Microsoft.DataFactory.Data/factories/datasets"
|
||||
description: "Sales data"
|
||||
linkedServiceName:
|
||||
referenceName: "AzureBlobStorage1"
|
||||
type: LinkedServiceReference
|
||||
expected_normalized:
|
||||
type: "Microsoft.DataFactory.Data/factories/datasets"
|
||||
description: "Sales data"
|
||||
linkedservicename:
|
||||
referencename: "AzureBlobStorage1"
|
||||
type: LinkedServiceReference
|
||||
round_trip: true
|
||||
expected_round_trip:
|
||||
type: "Microsoft.DataFactory.Data/factories/datasets"
|
||||
description: "Sales data"
|
||||
linkedServiceName:
|
||||
referenceName: "AzureBlobStorage1"
|
||||
type: LinkedServiceReference
|
||||
|
||||
- note: data-plane without aliases (unknown resource type in namespace)
|
||||
use_registry_api: true
|
||||
input:
|
||||
type: "Microsoft.DataFactory.Data/factories/triggers"
|
||||
recurrence:
|
||||
frequency: Day
|
||||
interval: 1
|
||||
expected_normalized:
|
||||
type: "Microsoft.DataFactory.Data/factories/triggers"
|
||||
recurrence:
|
||||
frequency: Day
|
||||
interval: 1
|
||||
round_trip: true
|
||||
|
||||
- note: data-plane denormalize dataset restores casing
|
||||
use_registry_api: true
|
||||
input:
|
||||
type: "Microsoft.DataFactory.Data/factories/datasets"
|
||||
description: "Sales data"
|
||||
linkedservicename:
|
||||
referencename: "AzureBlobStorage1"
|
||||
expected_denormalized:
|
||||
type: "Microsoft.DataFactory.Data/factories/datasets"
|
||||
description: "Sales data"
|
||||
linkedServiceName:
|
||||
referenceName: "AzureBlobStorage1"
|
||||
reverse_round_trip: true
|
||||
154
tests/azure_policy/normalization/cases/data_plane_breadth.yaml
Normal file
154
tests/azure_policy/normalization/cases/data_plane_breadth.yaml
Normal file
@@ -0,0 +1,154 @@
|
||||
# Broad data-plane compatibility tests covering multiple Azure data-plane
|
||||
# namespaces and resource shapes. Each case exercises a distinct namespace
|
||||
# to ensure the manifest-loading and data-plane normalization paths handle
|
||||
# a variety of real-world patterns.
|
||||
|
||||
data_manifest_json: |
|
||||
{
|
||||
"dataNamespace": "Microsoft.Kubernetes.Data",
|
||||
"aliases": [],
|
||||
"resourceTypeAliases": [
|
||||
{
|
||||
"resourceType": "namespaces",
|
||||
"aliases": [
|
||||
{
|
||||
"name": "Microsoft.Kubernetes.Data/namespaces/labels",
|
||||
"paths": [{ "path": "labels", "apiVersions": ["v1"] }]
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Kubernetes.Data/namespaces/annotations",
|
||||
"paths": [{ "path": "annotations", "apiVersions": ["v1"] }]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"resourceType": "pods",
|
||||
"aliases": [
|
||||
{
|
||||
"name": "Microsoft.Kubernetes.Data/pods/containers[*].image",
|
||||
"paths": [{ "path": "containers[*].image", "apiVersions": ["v1"] }]
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Kubernetes.Data/pods/containers[*].name",
|
||||
"paths": [{ "path": "containers[*].name", "apiVersions": ["v1"] }]
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Kubernetes.Data/pods/containers[*].resources.limits.cpu",
|
||||
"paths": [{ "path": "containers[*].resources.limits.cpu", "apiVersions": ["v1"] }]
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Kubernetes.Data/pods/hostNetwork",
|
||||
"paths": [{ "path": "hostNetwork", "apiVersions": ["v1"] }]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
cases:
|
||||
# ── Kubernetes namespaces: flat labels/annotations ──
|
||||
- note: "k8s namespace with labels and annotations"
|
||||
use_registry_api: true
|
||||
input:
|
||||
type: "Microsoft.Kubernetes.Data/namespaces"
|
||||
labels:
|
||||
app: web-frontend
|
||||
version: "v2"
|
||||
annotations:
|
||||
owner: team-alpha
|
||||
expected_normalized:
|
||||
type: "Microsoft.Kubernetes.Data/namespaces"
|
||||
labels:
|
||||
app: web-frontend
|
||||
version: "v2"
|
||||
annotations:
|
||||
owner: team-alpha
|
||||
round_trip: true
|
||||
|
||||
# ── Kubernetes pods: arrays with deeply nested fields ──
|
||||
- note: "k8s pod with containers array and resource limits"
|
||||
use_registry_api: true
|
||||
input:
|
||||
type: "Microsoft.Kubernetes.Data/pods"
|
||||
hostNetwork: false
|
||||
containers:
|
||||
- name: app
|
||||
image: "myregistry.azurecr.io/app:latest"
|
||||
resources:
|
||||
limits:
|
||||
cpu: "500m"
|
||||
memory: "256Mi"
|
||||
- name: sidecar
|
||||
image: "myregistry.azurecr.io/sidecar:v1"
|
||||
resources:
|
||||
limits:
|
||||
cpu: "100m"
|
||||
expected_normalized:
|
||||
type: "Microsoft.Kubernetes.Data/pods"
|
||||
hostnetwork: false
|
||||
containers:
|
||||
- name: app
|
||||
image: "myregistry.azurecr.io/app:latest"
|
||||
resources:
|
||||
limits:
|
||||
cpu: "500m"
|
||||
memory: "256Mi"
|
||||
- name: sidecar
|
||||
image: "myregistry.azurecr.io/sidecar:v1"
|
||||
resources:
|
||||
limits:
|
||||
cpu: "100m"
|
||||
round_trip: true
|
||||
|
||||
- note: "k8s pod denormalize restores casing"
|
||||
use_registry_api: true
|
||||
input:
|
||||
type: "Microsoft.Kubernetes.Data/pods"
|
||||
hostnetwork: true
|
||||
containers:
|
||||
- name: app
|
||||
image: "nginx"
|
||||
expected_denormalized:
|
||||
type: "Microsoft.Kubernetes.Data/pods"
|
||||
hostNetwork: true
|
||||
containers:
|
||||
- name: app
|
||||
image: "nginx"
|
||||
reverse_round_trip: true
|
||||
|
||||
# ── Unknown resource type in known namespace ──
|
||||
- note: "k8s unknown resource type passes through unchanged"
|
||||
use_registry_api: true
|
||||
input:
|
||||
type: "Microsoft.Kubernetes.Data/services"
|
||||
clusterIP: "10.0.0.1"
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 8080
|
||||
expected_normalized:
|
||||
type: "Microsoft.Kubernetes.Data/services"
|
||||
clusterip: "10.0.0.1"
|
||||
ports:
|
||||
- port: 80
|
||||
targetport: 8080
|
||||
round_trip: true
|
||||
# No aliases for this type, so casing is lost on round-trip.
|
||||
expected_round_trip:
|
||||
type: "Microsoft.Kubernetes.Data/services"
|
||||
clusterip: "10.0.0.1"
|
||||
ports:
|
||||
- port: 80
|
||||
targetport: 8080
|
||||
|
||||
# ── Empty containers array ──
|
||||
- note: "k8s pod with empty containers array"
|
||||
use_registry_api: true
|
||||
input:
|
||||
type: "Microsoft.Kubernetes.Data/pods"
|
||||
hostNetwork: false
|
||||
containers: []
|
||||
expected_normalized:
|
||||
type: "Microsoft.Kubernetes.Data/pods"
|
||||
hostnetwork: false
|
||||
containers: []
|
||||
round_trip: true
|
||||
@@ -0,0 +1,64 @@
|
||||
# Tests that exercise data-plane manifest loading and the data-plane
|
||||
# normalization path (resource type contains ".Data/").
|
||||
# This covers load_data_policy_manifest_json() and the data-plane
|
||||
# normalization branch in normalizer.rs.
|
||||
|
||||
data_manifest_json: |
|
||||
{
|
||||
"dataNamespace": "Microsoft.KeyVault.Data",
|
||||
"aliases": [],
|
||||
"resourceTypeAliases": [
|
||||
{
|
||||
"resourceType": "vaults/certificates",
|
||||
"aliases": [
|
||||
{
|
||||
"name": "Microsoft.KeyVault.Data/vaults/certificates/keySize",
|
||||
"paths": [
|
||||
{
|
||||
"path": "keySize",
|
||||
"apiVersions": ["7.0"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.KeyVault.Data/vaults/certificates/attributes.expiresOn",
|
||||
"paths": [
|
||||
{
|
||||
"path": "attributes.expiresOn",
|
||||
"apiVersions": ["7.0"]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
cases:
|
||||
- note: data-plane manifest normalize via registry API
|
||||
use_registry_api: true
|
||||
input:
|
||||
type: "Microsoft.KeyVault.Data/vaults/certificates"
|
||||
keySize: 2048
|
||||
attributes:
|
||||
expiresOn: "2025-01-01T00:00:00Z"
|
||||
expected_normalized:
|
||||
type: "Microsoft.KeyVault.Data/vaults/certificates"
|
||||
keysize: 2048
|
||||
attributes:
|
||||
expireson: "2025-01-01T00:00:00Z"
|
||||
round_trip: true
|
||||
|
||||
- note: data-plane manifest round-trip via registry API
|
||||
use_registry_api: true
|
||||
input:
|
||||
type: "Microsoft.KeyVault.Data/vaults/certificates"
|
||||
keySize: 2048
|
||||
attributes:
|
||||
expiresOn: "2025-01-01T00:00:00Z"
|
||||
round_trip: true
|
||||
expected_round_trip:
|
||||
type: "Microsoft.KeyVault.Data/vaults/certificates"
|
||||
keySize: 2048
|
||||
attributes:
|
||||
expiresOn: "2025-01-01T00:00:00Z"
|
||||
319
tests/azure_policy/normalization/cases/denormalize_aliases.yaml
Normal file
319
tests/azure_policy/normalization/cases/denormalize_aliases.yaml
Normal file
@@ -0,0 +1,319 @@
|
||||
aliases_json: |
|
||||
[
|
||||
{
|
||||
"namespace": "Microsoft.Storage",
|
||||
"resourceTypes": [
|
||||
{
|
||||
"resourceType": "storageAccounts",
|
||||
"aliases": [
|
||||
{
|
||||
"name": "Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly",
|
||||
"defaultPath": "properties.supportsHttpsTrafficOnly",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Storage/storageAccounts/accessTier",
|
||||
"defaultPath": "properties.accessTier",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Storage/storageAccounts/sku.name",
|
||||
"defaultPath": "sku.name",
|
||||
"paths": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"namespace": "Microsoft.Network",
|
||||
"resourceTypes": [
|
||||
{
|
||||
"resourceType": "networkSecurityGroups",
|
||||
"aliases": [
|
||||
{
|
||||
"name": "Microsoft.Network/networkSecurityGroups/securityRules[*].protocol",
|
||||
"defaultPath": "properties.securityRules[*].properties.protocol",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Network/networkSecurityGroups/securityRules[*].access",
|
||||
"defaultPath": "properties.securityRules[*].properties.access",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Network/networkSecurityGroups/securityRules[*].name",
|
||||
"defaultPath": "properties.securityRules[*].name",
|
||||
"paths": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"resourceType": "virtualNetworks",
|
||||
"aliases": [
|
||||
{
|
||||
"name": "Microsoft.Network/virtualNetworks/subnets[*].addressPrefix",
|
||||
"defaultPath": "properties.subnets[*].properties.addressPrefix",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Network/virtualNetworks/subnets[*].name",
|
||||
"defaultPath": "properties.subnets[*].name",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Network/virtualNetworks/subnets[*].ipConfigurations[*].privateIPAddress",
|
||||
"defaultPath": "properties.subnets[*].properties.ipConfigurations[*].properties.privateIPAddress",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Network/virtualNetworks/subnets[*].ipConfigurations[*].name",
|
||||
"defaultPath": "properties.subnets[*].properties.ipConfigurations[*].name",
|
||||
"paths": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"namespace": "Microsoft.Web",
|
||||
"resourceTypes": [
|
||||
{
|
||||
"resourceType": "sites",
|
||||
"aliases": [
|
||||
{
|
||||
"name": "Microsoft.Web/sites/isEnabled",
|
||||
"defaultPath": "properties.isEnabled",
|
||||
"paths": [
|
||||
{
|
||||
"path": "properties.enabled",
|
||||
"apiVersions": ["2020-01-01"]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"namespace": "test",
|
||||
"resourceTypes": [
|
||||
{
|
||||
"resourceType": "resource",
|
||||
"aliases": [
|
||||
{
|
||||
"name": "test/resource/type",
|
||||
"defaultPath": "properties.type",
|
||||
"paths": []
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"resourceType": "versionedEnvelope",
|
||||
"aliases": [
|
||||
{
|
||||
"name": "test/versionedEnvelope/items[*].status",
|
||||
"defaultPath": "properties.items[*].properties.status",
|
||||
"paths": [
|
||||
{
|
||||
"path": "properties.items[*].status",
|
||||
"apiVersions": ["2025-01-01"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "test/versionedEnvelope/items[*].name",
|
||||
"defaultPath": "properties.items[*].name",
|
||||
"paths": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
cases:
|
||||
- note: restores casing from aliases
|
||||
input:
|
||||
name: myStorage
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
supportshttpstrafficonly: true
|
||||
accesstier: Hot
|
||||
expected_denormalized:
|
||||
name: myStorage
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
properties:
|
||||
supportsHttpsTrafficOnly: true
|
||||
accessTier: Hot
|
||||
reverse_round_trip: true
|
||||
|
||||
- note: versioned path - default
|
||||
input:
|
||||
type: "Microsoft.Web/sites"
|
||||
isenabled: true
|
||||
expected_denormalized:
|
||||
type: "Microsoft.Web/sites"
|
||||
properties:
|
||||
isEnabled: true
|
||||
reverse_round_trip: true
|
||||
|
||||
- note: versioned path - matching version
|
||||
input:
|
||||
type: "Microsoft.Web/sites"
|
||||
isenabled: true
|
||||
api_version: "2020-01-01"
|
||||
expected_denormalized:
|
||||
type: "Microsoft.Web/sites"
|
||||
properties:
|
||||
enabled: true
|
||||
reverse_round_trip: true
|
||||
expected_reverse_round_trip:
|
||||
type: "Microsoft.Web/sites"
|
||||
enabled: true
|
||||
isenabled: true
|
||||
|
||||
- note: collision safe key
|
||||
input:
|
||||
type: "test/resource"
|
||||
_p_type: SubType
|
||||
expected_denormalized:
|
||||
type: "test/resource"
|
||||
properties:
|
||||
type: SubType
|
||||
reverse_round_trip: true
|
||||
|
||||
- note: sub-resource array
|
||||
input:
|
||||
name: myNsg
|
||||
type: "Microsoft.Network/networkSecurityGroups"
|
||||
securityrules:
|
||||
- name: rule1
|
||||
protocol: Tcp
|
||||
access: Allow
|
||||
- name: rule2
|
||||
protocol: "*"
|
||||
access: Deny
|
||||
expected_denormalized:
|
||||
name: myNsg
|
||||
type: "Microsoft.Network/networkSecurityGroups"
|
||||
properties:
|
||||
securityRules:
|
||||
- name: rule1
|
||||
properties:
|
||||
protocol: Tcp
|
||||
access: Allow
|
||||
- name: rule2
|
||||
properties:
|
||||
protocol: "*"
|
||||
access: Deny
|
||||
reverse_round_trip: true
|
||||
|
||||
- note: nested sub-resource arrays
|
||||
input:
|
||||
name: myVnet
|
||||
type: "Microsoft.Network/virtualNetworks"
|
||||
subnets:
|
||||
- name: subnet1
|
||||
addressprefix: "10.0.0.0/24"
|
||||
ipconfigurations:
|
||||
- name: ipconfig1
|
||||
privateipaddress: "10.0.0.4"
|
||||
expected_denormalized:
|
||||
name: myVnet
|
||||
type: "Microsoft.Network/virtualNetworks"
|
||||
properties:
|
||||
subnets:
|
||||
- name: subnet1
|
||||
properties:
|
||||
addressPrefix: "10.0.0.0/24"
|
||||
ipConfigurations:
|
||||
- name: ipconfig1
|
||||
properties:
|
||||
privateIPAddress: "10.0.0.4"
|
||||
reverse_round_trip: true
|
||||
|
||||
- note: root level alias (sku.name)
|
||||
input:
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
sku:
|
||||
name: Standard_LRS
|
||||
expected_denormalized:
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
sku:
|
||||
name: Standard_LRS
|
||||
reverse_round_trip: true
|
||||
|
||||
- note: versioned envelope classification — default (status under properties)
|
||||
resource_type: "test/versionedEnvelope"
|
||||
sub_resource_arrays: ["items"]
|
||||
input:
|
||||
type: "test/versionedEnvelope"
|
||||
items:
|
||||
- name: item1
|
||||
status: active
|
||||
expected_denormalized:
|
||||
type: "test/versionedEnvelope"
|
||||
properties:
|
||||
items:
|
||||
- name: item1
|
||||
properties:
|
||||
status: active
|
||||
|
||||
- note: versioned envelope classification — version promotes status to envelope
|
||||
resource_type: "test/versionedEnvelope"
|
||||
sub_resource_arrays: ["items"]
|
||||
api_version: "2025-01-01"
|
||||
input:
|
||||
type: "test/versionedEnvelope"
|
||||
items:
|
||||
- name: item1
|
||||
status: active
|
||||
expected_denormalized:
|
||||
type: "test/versionedEnvelope"
|
||||
properties:
|
||||
items:
|
||||
- name: item1
|
||||
status: active
|
||||
|
||||
- note: versioned casing restoration from versioned alias paths
|
||||
aliases_json: |
|
||||
[
|
||||
{
|
||||
"namespace": "Microsoft.Test",
|
||||
"resourceTypes": [
|
||||
{
|
||||
"resourceType": "widgets",
|
||||
"aliases": [
|
||||
{
|
||||
"name": "Microsoft.Test/widgets/items[*].rating",
|
||||
"defaultPath": "properties.items[*].properties.oldField",
|
||||
"paths": [
|
||||
{
|
||||
"path": "properties.items[*].properties.RenamedField",
|
||||
"apiVersions": ["2025-06-01"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Test/widgets/items[*].name",
|
||||
"defaultPath": "properties.items[*].name",
|
||||
"paths": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
api_version: "2025-06-01"
|
||||
input:
|
||||
type: "Microsoft.Test/widgets"
|
||||
items:
|
||||
- name: w1
|
||||
rating: 5
|
||||
expected_denormalized:
|
||||
type: "Microsoft.Test/widgets"
|
||||
properties:
|
||||
items:
|
||||
- name: w1
|
||||
properties:
|
||||
RenamedField: 5
|
||||
@@ -0,0 +1,83 @@
|
||||
cases:
|
||||
- note: wraps properties
|
||||
input:
|
||||
name: myStorage
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
location: westus2
|
||||
supportshttpstrafficonly: true
|
||||
ishnsenabled: false
|
||||
expected_denormalized:
|
||||
name: myStorage
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
location: westus2
|
||||
properties:
|
||||
supportshttpstrafficonly: true
|
||||
ishnsenabled: false
|
||||
reverse_round_trip: true
|
||||
|
||||
- note: non-object returns clone
|
||||
input: "just a string"
|
||||
expected_denormalized: "just a string"
|
||||
reverse_round_trip: true
|
||||
|
||||
- note: empty normalized
|
||||
input: {}
|
||||
expected_denormalized: {}
|
||||
reverse_round_trip: true
|
||||
|
||||
- note: preserves root fields
|
||||
input:
|
||||
name: r
|
||||
type: t
|
||||
location: l
|
||||
kind: k
|
||||
id: "/sub/rg/r"
|
||||
tags:
|
||||
env: prod
|
||||
identity:
|
||||
type: SystemAssigned
|
||||
principalid: pid-123
|
||||
userassignedidentities:
|
||||
/subscriptions/Sub/resourceGroups/Rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/Uai1: {}
|
||||
sku:
|
||||
name: Basic
|
||||
plan:
|
||||
name: p1
|
||||
zones: ["1", "2"]
|
||||
managedby: "/sub/other"
|
||||
etag: "W/\"abc\""
|
||||
apiversion: "2023-01-01"
|
||||
fullname: parent/child
|
||||
systemdata:
|
||||
createdby: admin
|
||||
extendedlocation:
|
||||
name: edge1
|
||||
type: EdgeZone
|
||||
expected_denormalized:
|
||||
name: r
|
||||
type: t
|
||||
location: l
|
||||
kind: k
|
||||
id: "/sub/rg/r"
|
||||
tags:
|
||||
env: prod
|
||||
identity:
|
||||
type: SystemAssigned
|
||||
principalId: pid-123
|
||||
userAssignedIdentities:
|
||||
/subscriptions/Sub/resourceGroups/Rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/Uai1: {}
|
||||
sku:
|
||||
name: Basic
|
||||
plan:
|
||||
name: p1
|
||||
zones: ["1", "2"]
|
||||
managedBy: "/sub/other"
|
||||
etag: "W/\"abc\""
|
||||
apiVersion: "2023-01-01"
|
||||
fullName: parent/child
|
||||
systemData:
|
||||
createdBy: admin
|
||||
extendedLocation:
|
||||
name: edge1
|
||||
type: EdgeZone
|
||||
reverse_round_trip: true
|
||||
201
tests/azure_policy/normalization/cases/edge_cases.yaml
Normal file
201
tests/azure_policy/normalization/cases/edge_cases.yaml
Normal file
@@ -0,0 +1,201 @@
|
||||
# Tests pinning behavior for ambiguous or malformed inputs that could
|
||||
# be produced by external callers rather than the normalizer itself.
|
||||
#
|
||||
# The normalizer / denormalizer make assumptions about their input shape
|
||||
# (e.g., keys are fully lowercased after normalization). These tests
|
||||
# document what happens when those assumptions are violated, without
|
||||
# asserting the behavior is "correct" per se — rather, they pin it so
|
||||
# regressions are detected.
|
||||
|
||||
aliases_json: |
|
||||
[
|
||||
{
|
||||
"namespace": "Microsoft.Storage",
|
||||
"resourceTypes": [
|
||||
{
|
||||
"resourceType": "storageAccounts",
|
||||
"aliases": [
|
||||
{
|
||||
"name": "Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly",
|
||||
"defaultPath": "properties.supportsHttpsTrafficOnly",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Storage/storageAccounts/accessTier",
|
||||
"defaultPath": "properties.accessTier",
|
||||
"paths": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"namespace": "Microsoft.Network",
|
||||
"resourceTypes": [
|
||||
{
|
||||
"resourceType": "networkSecurityGroups",
|
||||
"aliases": [
|
||||
{
|
||||
"name": "Microsoft.Network/networkSecurityGroups/securityRules[*].protocol",
|
||||
"defaultPath": "properties.securityRules[*].properties.protocol",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Network/networkSecurityGroups/securityRules[*].access",
|
||||
"defaultPath": "properties.securityRules[*].properties.access",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Network/networkSecurityGroups/securityRules[*].name",
|
||||
"defaultPath": "properties.securityRules[*].name",
|
||||
"paths": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"namespace": "Microsoft.Web",
|
||||
"resourceTypes": [
|
||||
{
|
||||
"resourceType": "sites",
|
||||
"aliases": [
|
||||
{
|
||||
"name": "Microsoft.Web/sites/isEnabled",
|
||||
"defaultPath": "properties.isEnabled",
|
||||
"paths": [
|
||||
{
|
||||
"path": "properties.enabled",
|
||||
"apiVersions": ["2020-01-01"]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
cases:
|
||||
# ── Normalize: ARM input with both root and properties having same field ──
|
||||
|
||||
- note: "normalize: both root 'name' and properties.name (root wins)"
|
||||
input:
|
||||
name: root-name
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
properties:
|
||||
name: props-name
|
||||
accessTier: Hot
|
||||
expected_normalized:
|
||||
name: root-name
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
accesstier: Hot
|
||||
|
||||
# ── Normalize: extra fields not in alias catalog or ROOT_FIELDS ──
|
||||
|
||||
- note: "normalize: extra non-aliased properties are kept (lowercased)"
|
||||
input:
|
||||
name: test
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
properties:
|
||||
supportsHttpsTrafficOnly: true
|
||||
customUnknownField: 42
|
||||
expected_normalized:
|
||||
name: test
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
supportshttpstrafficonly: true
|
||||
customunknownfield: 42
|
||||
|
||||
# ── Denormalize: input has mixed casing (externally constructed) ──
|
||||
|
||||
- note: "denormalize: mixed-case keys still resolve via aliases"
|
||||
input:
|
||||
name: test
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
SupportsHttpsTrafficOnly: true
|
||||
expected_denormalized:
|
||||
name: test
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
properties:
|
||||
supportsHttpsTrafficOnly: true
|
||||
|
||||
# ── Denormalize: input has extra fields not in alias catalog ──
|
||||
|
||||
- note: "denormalize: unknown fields go under properties (control-plane)"
|
||||
input:
|
||||
name: test
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
unknownfield: 123
|
||||
accesstier: Hot
|
||||
expected_denormalized:
|
||||
name: test
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
properties:
|
||||
unknownfield: 123
|
||||
accessTier: Hot
|
||||
|
||||
# ── Denormalize with versioned path: both alias-named and ARM-named present ──
|
||||
|
||||
- note: "denormalize: both isenabled and enabled present, alias wins"
|
||||
input:
|
||||
type: "Microsoft.Web/sites"
|
||||
isenabled: true
|
||||
enabled: false
|
||||
api_version: "2020-01-01"
|
||||
expected_denormalized:
|
||||
type: "Microsoft.Web/sites"
|
||||
properties:
|
||||
enabled: true
|
||||
|
||||
- note: "denormalize: both isenabled and enabled present, default path"
|
||||
input:
|
||||
type: "Microsoft.Web/sites"
|
||||
isenabled: true
|
||||
enabled: false
|
||||
expected_denormalized:
|
||||
type: "Microsoft.Web/sites"
|
||||
properties:
|
||||
isEnabled: true
|
||||
enabled: false
|
||||
|
||||
# ── Normalize: ARM resource with empty type ──
|
||||
|
||||
- note: "normalize: empty type field, no alias match"
|
||||
input:
|
||||
name: test
|
||||
type: ""
|
||||
properties:
|
||||
foo: bar
|
||||
expected_normalized:
|
||||
name: test
|
||||
type: ""
|
||||
foo: bar
|
||||
|
||||
# ── Normalize: ARM resource with null properties ──
|
||||
|
||||
- note: "normalize: null properties value"
|
||||
input:
|
||||
name: test
|
||||
properties: null
|
||||
expected_normalized:
|
||||
name: test
|
||||
|
||||
# ── Denormalize: sub-resource array with elements already containing properties ──
|
||||
|
||||
- note: "denormalize: elements already have properties wrapper (double-wrap)"
|
||||
input:
|
||||
name: myNsg
|
||||
type: "Microsoft.Network/networkSecurityGroups"
|
||||
securityrules:
|
||||
- name: rule1
|
||||
properties:
|
||||
protocol: Tcp
|
||||
expected_denormalized:
|
||||
name: myNsg
|
||||
type: "Microsoft.Network/networkSecurityGroups"
|
||||
properties:
|
||||
securityRules:
|
||||
- name: rule1
|
||||
properties:
|
||||
properties:
|
||||
protocol: Tcp
|
||||
@@ -0,0 +1,46 @@
|
||||
aliases_json: |
|
||||
[
|
||||
{
|
||||
"namespace": "Microsoft.Network",
|
||||
"resourceTypes": [
|
||||
{
|
||||
"resourceType": "networkSecurityGroups",
|
||||
"aliases": [
|
||||
{
|
||||
"name": "Microsoft.Network/networkSecurityGroups/securityRules[*].protocol",
|
||||
"defaultPath": "properties.securityRules[*].properties.protocol",
|
||||
"paths": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
cases:
|
||||
- note: normalize-and-wrap full pipeline
|
||||
input:
|
||||
name: myNsg
|
||||
type: "Microsoft.Network/networkSecurityGroups"
|
||||
properties:
|
||||
securityRules:
|
||||
- name: rule1
|
||||
properties:
|
||||
protocol: Tcp
|
||||
context:
|
||||
resourceGroup:
|
||||
name: rg1
|
||||
parameters:
|
||||
env: prod
|
||||
expected_envelope:
|
||||
resource:
|
||||
name: myNsg
|
||||
type: "Microsoft.Network/networkSecurityGroups"
|
||||
securityrules:
|
||||
- name: rule1
|
||||
protocol: Tcp
|
||||
context:
|
||||
resourceGroup:
|
||||
name: rg1
|
||||
parameters:
|
||||
env: prod
|
||||
233
tests/azure_policy/normalization/cases/malformed_input.yaml
Normal file
233
tests/azure_policy/normalization/cases/malformed_input.yaml
Normal file
@@ -0,0 +1,233 @@
|
||||
# Tests for malformed or unexpected *normalized* input fed to the
|
||||
# denormalizer. These pin the current behavior when the denormalizer
|
||||
# receives externally-produced JSON that violates the normalizer's
|
||||
# implicit contract (e.g., keys not lowercased, wrong value types,
|
||||
# missing type field, non-object input).
|
||||
#
|
||||
# These are NOT correctness assertions -- they document what the
|
||||
# denormalizer actually produces so regressions are caught.
|
||||
|
||||
aliases_json: |
|
||||
[
|
||||
{
|
||||
"namespace": "Microsoft.Storage",
|
||||
"resourceTypes": [
|
||||
{
|
||||
"resourceType": "storageAccounts",
|
||||
"aliases": [
|
||||
{
|
||||
"name": "Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly",
|
||||
"defaultPath": "properties.supportsHttpsTrafficOnly",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Storage/storageAccounts/accessTier",
|
||||
"defaultPath": "properties.accessTier",
|
||||
"paths": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"namespace": "Microsoft.Network",
|
||||
"resourceTypes": [
|
||||
{
|
||||
"resourceType": "networkSecurityGroups",
|
||||
"aliases": [
|
||||
{
|
||||
"name": "Microsoft.Network/networkSecurityGroups/securityRules[*].protocol",
|
||||
"defaultPath": "properties.securityRules[*].properties.protocol",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Network/networkSecurityGroups/securityRules[*].name",
|
||||
"defaultPath": "properties.securityRules[*].name",
|
||||
"paths": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
cases:
|
||||
# ── Non-object input ──
|
||||
# The denormalizer should return the value as-is when it's not an object.
|
||||
|
||||
- note: "denormalize: string input returned as-is"
|
||||
input: "just a string"
|
||||
expected_denormalized: "just a string"
|
||||
|
||||
- note: "denormalize: integer input returned as-is"
|
||||
input: 42
|
||||
expected_denormalized: 42
|
||||
|
||||
- note: "denormalize: array input returned as-is"
|
||||
input: [1, 2, 3]
|
||||
expected_denormalized: [1, 2, 3]
|
||||
|
||||
- note: "denormalize: null input returned as-is"
|
||||
input: null
|
||||
expected_denormalized: null
|
||||
|
||||
- note: "denormalize: boolean input returned as-is"
|
||||
input: true
|
||||
expected_denormalized: true
|
||||
|
||||
# ── Missing type field ──
|
||||
# Without a type, no alias resolution occurs; fields are still placed
|
||||
# under `properties` (control-plane default) but casing is not restored.
|
||||
|
||||
- note: "denormalize: no type field, fields go under properties"
|
||||
input:
|
||||
name: test
|
||||
unknownfield: 42
|
||||
expected_denormalized:
|
||||
name: test
|
||||
properties:
|
||||
unknownfield: 42
|
||||
|
||||
# ── Empty object ──
|
||||
|
||||
- note: "denormalize: empty object produces empty object"
|
||||
input: {}
|
||||
expected_denormalized: {}
|
||||
|
||||
# ── Object with only type field ──
|
||||
|
||||
- note: "denormalize: only type field, empty properties"
|
||||
input:
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
expected_denormalized:
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
|
||||
# ── Normalize: non-object input ──
|
||||
|
||||
- note: "normalize: string input returned as-is"
|
||||
input: "just a string"
|
||||
expected_normalized: "just a string"
|
||||
|
||||
- note: "normalize: integer input returned as-is"
|
||||
input: 42
|
||||
expected_normalized: 42
|
||||
|
||||
- note: "normalize: null input returned as-is"
|
||||
input: null
|
||||
expected_normalized: null
|
||||
|
||||
# ── Normalize: empty properties object ──
|
||||
|
||||
- note: "normalize: empty properties object"
|
||||
input:
|
||||
name: test
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
properties: {}
|
||||
expected_normalized:
|
||||
name: test
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
|
||||
# ── Normalize: properties is a non-object value ──
|
||||
|
||||
- note: "normalize: properties is a string (treated as non-object, ignored)"
|
||||
input:
|
||||
name: test
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
properties: "not an object"
|
||||
expected_normalized:
|
||||
name: test
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
|
||||
- note: "normalize: properties is an array (treated as non-object, ignored)"
|
||||
input:
|
||||
name: test
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
properties: [1, 2, 3]
|
||||
expected_normalized:
|
||||
name: test
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
|
||||
# ── Denormalize: value types preserved through round trip ──
|
||||
|
||||
- note: "denormalize: numeric value zero"
|
||||
input:
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
accesstier: 0
|
||||
expected_denormalized:
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
properties:
|
||||
accessTier: 0
|
||||
|
||||
- note: "denormalize: boolean false aliased field"
|
||||
input:
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
supportshttpstrafficonly: false
|
||||
expected_denormalized:
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
properties:
|
||||
supportsHttpsTrafficOnly: false
|
||||
|
||||
- note: "denormalize: null-valued aliased field"
|
||||
input:
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
supportshttpstrafficonly: null
|
||||
expected_denormalized:
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
properties:
|
||||
supportsHttpsTrafficOnly: null
|
||||
|
||||
# ── Sub-resource array with non-array value ──
|
||||
|
||||
- note: "denormalize: expected sub-resource array is a string"
|
||||
input:
|
||||
name: myNsg
|
||||
type: "Microsoft.Network/networkSecurityGroups"
|
||||
securityrules: "not an array"
|
||||
expected_denormalized:
|
||||
name: myNsg
|
||||
type: "Microsoft.Network/networkSecurityGroups"
|
||||
properties:
|
||||
securityRules: "not an array"
|
||||
|
||||
- note: "denormalize: expected sub-resource array is null"
|
||||
input:
|
||||
name: myNsg
|
||||
type: "Microsoft.Network/networkSecurityGroups"
|
||||
securityrules: null
|
||||
expected_denormalized:
|
||||
name: myNsg
|
||||
type: "Microsoft.Network/networkSecurityGroups"
|
||||
properties:
|
||||
securityRules: null
|
||||
|
||||
# ── Sub-resource array with non-object elements ──
|
||||
|
||||
- note: "denormalize: sub-resource contains scalar elements"
|
||||
input:
|
||||
name: myNsg
|
||||
type: "Microsoft.Network/networkSecurityGroups"
|
||||
securityrules:
|
||||
- "just a string"
|
||||
- 42
|
||||
expected_denormalized:
|
||||
name: myNsg
|
||||
type: "Microsoft.Network/networkSecurityGroups"
|
||||
properties:
|
||||
securityRules:
|
||||
- "just a string"
|
||||
- 42
|
||||
|
||||
# ── Deeply nested null in normalize path ──
|
||||
|
||||
- note: "normalize: deeply nested null value preserved"
|
||||
input:
|
||||
name: test
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
properties:
|
||||
supportsHttpsTrafficOnly: null
|
||||
accessTier: null
|
||||
expected_normalized:
|
||||
name: test
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
supportshttpstrafficonly: null
|
||||
accesstier: null
|
||||
258
tests/azure_policy/normalization/cases/normalize_basic.yaml
Normal file
258
tests/azure_policy/normalization/cases/normalize_basic.yaml
Normal file
@@ -0,0 +1,258 @@
|
||||
cases:
|
||||
- note: flattens root properties
|
||||
input:
|
||||
name: myStorage
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
location: westus2
|
||||
properties:
|
||||
supportsHttpsTrafficOnly: true
|
||||
isHnsEnabled: false
|
||||
expected_normalized:
|
||||
name: myStorage
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
location: westus2
|
||||
supportshttpstrafficonly: true
|
||||
ishnsenabled: false
|
||||
round_trip: true
|
||||
expected_round_trip:
|
||||
name: myStorage
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
location: westus2
|
||||
properties:
|
||||
supportshttpstrafficonly: true
|
||||
ishnsenabled: false
|
||||
|
||||
- note: preserves root level precedence
|
||||
input:
|
||||
name: root-name
|
||||
properties:
|
||||
name: props-name
|
||||
expected_normalized:
|
||||
name: root-name
|
||||
round_trip: true
|
||||
expected_round_trip:
|
||||
name: root-name
|
||||
|
||||
- note: leaves plain arrays alone
|
||||
input:
|
||||
name: test
|
||||
properties:
|
||||
networkAcls:
|
||||
ipRules:
|
||||
- value: "10.0.0.1"
|
||||
action: Allow
|
||||
- value: "10.0.0.2"
|
||||
action: Deny
|
||||
expected_normalized:
|
||||
name: test
|
||||
networkacls:
|
||||
iprules:
|
||||
- value: "10.0.0.1"
|
||||
action: Allow
|
||||
- value: "10.0.0.2"
|
||||
action: Deny
|
||||
round_trip: true
|
||||
expected_round_trip:
|
||||
name: test
|
||||
properties:
|
||||
networkacls:
|
||||
iprules:
|
||||
- value: "10.0.0.1"
|
||||
action: Allow
|
||||
- value: "10.0.0.2"
|
||||
action: Deny
|
||||
|
||||
- note: handles sku at root
|
||||
input:
|
||||
name: test
|
||||
sku:
|
||||
name: Standard_LRS
|
||||
tier: Standard
|
||||
properties:
|
||||
supportsHttpsTrafficOnly: true
|
||||
expected_normalized:
|
||||
name: test
|
||||
sku:
|
||||
name: Standard_LRS
|
||||
tier: Standard
|
||||
supportshttpstrafficonly: true
|
||||
round_trip: true
|
||||
expected_round_trip:
|
||||
name: test
|
||||
sku:
|
||||
name: Standard_LRS
|
||||
tier: Standard
|
||||
properties:
|
||||
supportshttpstrafficonly: true
|
||||
|
||||
- note: non-object returns clone
|
||||
input: "just a string"
|
||||
expected_normalized: "just a string"
|
||||
round_trip: true
|
||||
|
||||
- note: empty properties
|
||||
input:
|
||||
name: test
|
||||
properties: {}
|
||||
expected_normalized:
|
||||
name: test
|
||||
round_trip: true
|
||||
expected_round_trip:
|
||||
name: test
|
||||
|
||||
- note: missing properties
|
||||
input:
|
||||
name: test
|
||||
location: eastus
|
||||
expected_normalized:
|
||||
name: test
|
||||
location: eastus
|
||||
round_trip: true
|
||||
|
||||
- note: preserves all root fields
|
||||
input:
|
||||
name: r
|
||||
type: t
|
||||
location: l
|
||||
kind: k
|
||||
id: "/sub/rg/r"
|
||||
tags:
|
||||
env: prod
|
||||
identity:
|
||||
type: SystemAssigned
|
||||
principalId: pid-123
|
||||
userAssignedIdentities:
|
||||
/subscriptions/Sub/resourceGroups/Rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/Uai1: {}
|
||||
sku:
|
||||
name: Basic
|
||||
plan:
|
||||
name: p1
|
||||
zones: ["1", "2"]
|
||||
managedBy: "/sub/other"
|
||||
etag: "W/\"abc\""
|
||||
apiVersion: "2023-01-01"
|
||||
fullName: parent/child
|
||||
systemData:
|
||||
createdBy: admin
|
||||
extendedLocation:
|
||||
name: edge1
|
||||
type: EdgeZone
|
||||
properties:
|
||||
someProp: true
|
||||
expected_normalized:
|
||||
name: r
|
||||
type: t
|
||||
location: l
|
||||
kind: k
|
||||
id: "/sub/rg/r"
|
||||
tags:
|
||||
env: prod
|
||||
identity:
|
||||
type: SystemAssigned
|
||||
principalid: pid-123
|
||||
userassignedidentities:
|
||||
/subscriptions/Sub/resourceGroups/Rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/Uai1: {}
|
||||
sku:
|
||||
name: Basic
|
||||
plan:
|
||||
name: p1
|
||||
zones: ["1", "2"]
|
||||
managedby: "/sub/other"
|
||||
etag: "W/\"abc\""
|
||||
apiversion: "2023-01-01"
|
||||
fullname: parent/child
|
||||
systemdata:
|
||||
createdby: admin
|
||||
extendedlocation:
|
||||
name: edge1
|
||||
type: EdgeZone
|
||||
someprop: true
|
||||
round_trip: true
|
||||
expected_round_trip:
|
||||
name: r
|
||||
type: t
|
||||
location: l
|
||||
kind: k
|
||||
id: "/sub/rg/r"
|
||||
tags:
|
||||
env: prod
|
||||
identity:
|
||||
type: SystemAssigned
|
||||
principalId: pid-123
|
||||
userAssignedIdentities:
|
||||
/subscriptions/Sub/resourceGroups/Rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/Uai1: {}
|
||||
sku:
|
||||
name: Basic
|
||||
plan:
|
||||
name: p1
|
||||
zones: ["1", "2"]
|
||||
managedBy: "/sub/other"
|
||||
etag: "W/\"abc\""
|
||||
apiVersion: "2023-01-01"
|
||||
fullName: parent/child
|
||||
systemData:
|
||||
createdBy: admin
|
||||
extendedLocation:
|
||||
name: edge1
|
||||
type: EdgeZone
|
||||
properties:
|
||||
someprop: true
|
||||
|
||||
- note: primitive array pass through
|
||||
input:
|
||||
name: test
|
||||
properties:
|
||||
allowedIPs: ["10.0.0.1", "10.0.0.2", "10.0.0.3"]
|
||||
expected_normalized:
|
||||
name: test
|
||||
allowedips: ["10.0.0.1", "10.0.0.2", "10.0.0.3"]
|
||||
round_trip: true
|
||||
expected_round_trip:
|
||||
name: test
|
||||
properties:
|
||||
allowedips: ["10.0.0.1", "10.0.0.2", "10.0.0.3"]
|
||||
|
||||
- note: deeply nested object no sub-resource
|
||||
input:
|
||||
name: test
|
||||
properties:
|
||||
networkAcls:
|
||||
defaultAction: Deny
|
||||
virtualNetworkRules:
|
||||
- id: "/vnet/subnet1"
|
||||
action: Allow
|
||||
expected_normalized:
|
||||
name: test
|
||||
networkacls:
|
||||
defaultaction: Deny
|
||||
virtualnetworkrules:
|
||||
- id: "/vnet/subnet1"
|
||||
action: Allow
|
||||
round_trip: true
|
||||
expected_round_trip:
|
||||
name: test
|
||||
properties:
|
||||
networkacls:
|
||||
defaultaction: Deny
|
||||
virtualnetworkrules:
|
||||
- id: "/vnet/subnet1"
|
||||
action: Allow
|
||||
|
||||
- note: unknown root fields are dropped (not in ROOT_FIELDS)
|
||||
input:
|
||||
name: test
|
||||
type: "Microsoft.Foo/bars"
|
||||
fooExtension:
|
||||
barBaz: 1
|
||||
properties:
|
||||
enabled: true
|
||||
expected_normalized:
|
||||
name: test
|
||||
type: "Microsoft.Foo/bars"
|
||||
enabled: true
|
||||
round_trip: true
|
||||
expected_round_trip:
|
||||
name: test
|
||||
type: "Microsoft.Foo/bars"
|
||||
properties:
|
||||
enabled: true
|
||||
@@ -0,0 +1,26 @@
|
||||
cases:
|
||||
- note: envelope defaults
|
||||
input:
|
||||
name: x
|
||||
expected_envelope:
|
||||
resource:
|
||||
name: x
|
||||
context: {}
|
||||
parameters: {}
|
||||
|
||||
- note: envelope with context and parameters
|
||||
input:
|
||||
name: x
|
||||
context:
|
||||
resourceGroup:
|
||||
name: rg1
|
||||
parameters:
|
||||
env: prod
|
||||
expected_envelope:
|
||||
resource:
|
||||
name: x
|
||||
context:
|
||||
resourceGroup:
|
||||
name: rg1
|
||||
parameters:
|
||||
env: prod
|
||||
@@ -0,0 +1,123 @@
|
||||
cases:
|
||||
- note: flattens sub-resource arrays
|
||||
input:
|
||||
name: myNsg
|
||||
type: "Microsoft.Network/networkSecurityGroups"
|
||||
properties:
|
||||
securityRules:
|
||||
- name: rule1
|
||||
properties:
|
||||
protocol: Tcp
|
||||
access: Allow
|
||||
- name: rule2
|
||||
properties:
|
||||
protocol: "*"
|
||||
access: Deny
|
||||
sub_resource_arrays: ["securityRules"]
|
||||
resource_type: "Microsoft.Network/networkSecurityGroups"
|
||||
expected_normalized:
|
||||
name: myNsg
|
||||
type: "Microsoft.Network/networkSecurityGroups"
|
||||
securityrules:
|
||||
- name: rule1
|
||||
protocol: Tcp
|
||||
access: Allow
|
||||
- name: rule2
|
||||
protocol: "*"
|
||||
access: Deny
|
||||
round_trip: true
|
||||
expected_round_trip:
|
||||
name: myNsg
|
||||
type: "Microsoft.Network/networkSecurityGroups"
|
||||
properties:
|
||||
securityrules:
|
||||
- name: rule1
|
||||
properties:
|
||||
protocol: Tcp
|
||||
access: Allow
|
||||
- name: rule2
|
||||
properties:
|
||||
protocol: "*"
|
||||
access: Deny
|
||||
|
||||
- note: nested sub-resource arrays
|
||||
input:
|
||||
name: myVnet
|
||||
properties:
|
||||
subnets:
|
||||
- name: subnet1
|
||||
properties:
|
||||
addressPrefix: "10.0.0.0/24"
|
||||
ipConfigurations:
|
||||
- name: ipconfig1
|
||||
properties:
|
||||
privateIPAddress: "10.0.0.4"
|
||||
sub_resource_arrays: ["subnets", "subnets.ipConfigurations"]
|
||||
resource_type: "Microsoft.Network/virtualNetworks"
|
||||
expected_normalized:
|
||||
name: myVnet
|
||||
subnets:
|
||||
- name: subnet1
|
||||
addressprefix: "10.0.0.0/24"
|
||||
ipconfigurations:
|
||||
- name: ipconfig1
|
||||
privateipaddress: "10.0.0.4"
|
||||
round_trip: true
|
||||
expected_round_trip:
|
||||
name: myVnet
|
||||
properties:
|
||||
subnets:
|
||||
- name: subnet1
|
||||
properties:
|
||||
addressprefix: "10.0.0.0/24"
|
||||
ipconfigurations:
|
||||
- name: ipconfig1
|
||||
properties:
|
||||
privateipaddress: "10.0.0.4"
|
||||
|
||||
- note: sub-resource element without properties
|
||||
input:
|
||||
name: test
|
||||
properties:
|
||||
items:
|
||||
- name: plain-object
|
||||
enabled: true
|
||||
sub_resource_arrays: ["items"]
|
||||
resource_type: test
|
||||
expected_normalized:
|
||||
name: test
|
||||
items:
|
||||
- name: plain-object
|
||||
enabled: true
|
||||
round_trip: true
|
||||
expected_round_trip:
|
||||
name: test
|
||||
properties:
|
||||
items:
|
||||
- name: plain-object
|
||||
properties:
|
||||
enabled: true
|
||||
|
||||
- note: case-insensitive sub-resource match
|
||||
input:
|
||||
name: test
|
||||
properties:
|
||||
securityRules:
|
||||
- name: r1
|
||||
properties:
|
||||
protocol: Tcp
|
||||
sub_resource_arrays: ["SecurityRules"]
|
||||
resource_type: test
|
||||
expected_normalized:
|
||||
name: test
|
||||
securityrules:
|
||||
- name: r1
|
||||
protocol: Tcp
|
||||
round_trip: true
|
||||
expected_round_trip:
|
||||
name: test
|
||||
properties:
|
||||
securityrules:
|
||||
- name: r1
|
||||
properties:
|
||||
protocol: Tcp
|
||||
127
tests/azure_policy/normalization/cases/registry_api.yaml
Normal file
127
tests/azure_policy/normalization/cases/registry_api.yaml
Normal file
@@ -0,0 +1,127 @@
|
||||
# Tests that exercise the public AliasRegistry API (normalize, denormalize,
|
||||
# normalize_and_wrap) rather than the low-level *_with_aliases functions.
|
||||
# This ensures the registry lookup path and public entry points are covered.
|
||||
|
||||
aliases_json: |
|
||||
[
|
||||
{
|
||||
"namespace": "Microsoft.Storage",
|
||||
"resourceTypes": [
|
||||
{
|
||||
"resourceType": "storageAccounts",
|
||||
"aliases": [
|
||||
{
|
||||
"name": "Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly",
|
||||
"defaultPath": "properties.supportsHttpsTrafficOnly",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Storage/storageAccounts/isHnsEnabled",
|
||||
"defaultPath": "properties.isHnsEnabled",
|
||||
"paths": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"namespace": "Microsoft.Network",
|
||||
"resourceTypes": [
|
||||
{
|
||||
"resourceType": "networkSecurityGroups",
|
||||
"aliases": [
|
||||
{
|
||||
"name": "Microsoft.Network/networkSecurityGroups/securityRules[*].protocol",
|
||||
"defaultPath": "properties.securityRules[*].properties.protocol",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Network/networkSecurityGroups/securityRules[*].name",
|
||||
"defaultPath": "properties.securityRules[*].name",
|
||||
"paths": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
cases:
|
||||
- note: registry API normalize
|
||||
use_registry_api: true
|
||||
input:
|
||||
name: myStorage
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
location: westus2
|
||||
properties:
|
||||
supportsHttpsTrafficOnly: true
|
||||
isHnsEnabled: false
|
||||
expected_normalized:
|
||||
name: myStorage
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
location: westus2
|
||||
supportshttpstrafficonly: true
|
||||
ishnsenabled: false
|
||||
round_trip: true
|
||||
|
||||
- note: registry API denormalize
|
||||
use_registry_api: true
|
||||
input:
|
||||
name: myStorage
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
location: westus2
|
||||
supportshttpstrafficonly: true
|
||||
ishnsenabled: false
|
||||
expected_denormalized:
|
||||
name: myStorage
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
location: westus2
|
||||
properties:
|
||||
supportsHttpsTrafficOnly: true
|
||||
isHnsEnabled: false
|
||||
reverse_round_trip: true
|
||||
|
||||
- note: registry API normalize_and_wrap (envelope)
|
||||
use_registry_api: true
|
||||
input:
|
||||
name: myNsg
|
||||
type: "Microsoft.Network/networkSecurityGroups"
|
||||
properties:
|
||||
securityRules:
|
||||
- name: rule1
|
||||
properties:
|
||||
protocol: Tcp
|
||||
context:
|
||||
resourceGroup:
|
||||
name: rg1
|
||||
expected_envelope:
|
||||
resource:
|
||||
name: myNsg
|
||||
type: "Microsoft.Network/networkSecurityGroups"
|
||||
securityrules:
|
||||
- name: rule1
|
||||
protocol: Tcp
|
||||
context:
|
||||
resourceGroup:
|
||||
name: rg1
|
||||
parameters: {}
|
||||
|
||||
- note: registry API round-trip sub-resource
|
||||
use_registry_api: true
|
||||
input:
|
||||
name: myNsg
|
||||
type: "Microsoft.Network/networkSecurityGroups"
|
||||
properties:
|
||||
securityRules:
|
||||
- name: rule1
|
||||
properties:
|
||||
protocol: Tcp
|
||||
round_trip: true
|
||||
expected_round_trip:
|
||||
name: myNsg
|
||||
type: "Microsoft.Network/networkSecurityGroups"
|
||||
properties:
|
||||
securityRules:
|
||||
- name: rule1
|
||||
properties:
|
||||
protocol: Tcp
|
||||
449
tests/azure_policy/normalization/cases/round_trip.yaml
Normal file
449
tests/azure_policy/normalization/cases/round_trip.yaml
Normal file
@@ -0,0 +1,449 @@
|
||||
aliases_json: |
|
||||
[
|
||||
{
|
||||
"namespace": "Microsoft.Storage",
|
||||
"resourceTypes": [
|
||||
{
|
||||
"resourceType": "storageAccounts",
|
||||
"aliases": [
|
||||
{
|
||||
"name": "Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly",
|
||||
"defaultPath": "properties.supportsHttpsTrafficOnly",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Storage/storageAccounts/isHnsEnabled",
|
||||
"defaultPath": "properties.isHnsEnabled",
|
||||
"paths": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"namespace": "Microsoft.Network",
|
||||
"resourceTypes": [
|
||||
{
|
||||
"resourceType": "networkSecurityGroups",
|
||||
"aliases": [
|
||||
{
|
||||
"name": "Microsoft.Network/networkSecurityGroups/securityRules[*].protocol",
|
||||
"defaultPath": "properties.securityRules[*].properties.protocol",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Network/networkSecurityGroups/securityRules[*].access",
|
||||
"defaultPath": "properties.securityRules[*].properties.access",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Network/networkSecurityGroups/securityRules[*].name",
|
||||
"defaultPath": "properties.securityRules[*].name",
|
||||
"paths": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
cases:
|
||||
- note: round-trip simple resource
|
||||
input:
|
||||
name: myStorage
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
location: westus2
|
||||
sku:
|
||||
name: Standard_LRS
|
||||
properties:
|
||||
supportsHttpsTrafficOnly: true
|
||||
isHnsEnabled: false
|
||||
round_trip: true
|
||||
expected_round_trip:
|
||||
name: myStorage
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
location: westus2
|
||||
sku:
|
||||
name: Standard_LRS
|
||||
properties:
|
||||
supportsHttpsTrafficOnly: true
|
||||
isHnsEnabled: false
|
||||
|
||||
- note: round-trip sub-resource
|
||||
input:
|
||||
name: myNsg
|
||||
type: "Microsoft.Network/networkSecurityGroups"
|
||||
properties:
|
||||
securityRules:
|
||||
- name: rule1
|
||||
properties:
|
||||
protocol: Tcp
|
||||
access: Allow
|
||||
round_trip: true
|
||||
expected_round_trip:
|
||||
name: myNsg
|
||||
type: "Microsoft.Network/networkSecurityGroups"
|
||||
properties:
|
||||
securityRules:
|
||||
- name: rule1
|
||||
properties:
|
||||
protocol: Tcp
|
||||
access: Allow
|
||||
|
||||
- note: round-trip versioned array alias (element field remap)
|
||||
aliases_json: |
|
||||
[
|
||||
{
|
||||
"namespace": "Microsoft.Network",
|
||||
"resourceTypes": [
|
||||
{
|
||||
"resourceType": "firewallPolicies",
|
||||
"aliases": [
|
||||
{
|
||||
"name": "Microsoft.Network/firewallPolicies/rules[*].priority",
|
||||
"defaultPath": "properties.rules[*].properties.priority",
|
||||
"paths": [
|
||||
{
|
||||
"path": "properties.rules[*].properties.prio",
|
||||
"apiVersions": ["2021-01-01"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Network/firewallPolicies/rules[*].name",
|
||||
"defaultPath": "properties.rules[*].name",
|
||||
"paths": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
input:
|
||||
name: myPolicy
|
||||
type: "Microsoft.Network/firewallPolicies"
|
||||
properties:
|
||||
rules:
|
||||
- name: rule1
|
||||
properties:
|
||||
prio: 100
|
||||
api_version: "2021-01-01"
|
||||
round_trip: true
|
||||
expected_round_trip:
|
||||
name: myPolicy
|
||||
type: "Microsoft.Network/firewallPolicies"
|
||||
properties:
|
||||
rules:
|
||||
- name: rule1
|
||||
properties:
|
||||
prio: 100
|
||||
|
||||
# Regression: default and versioned paths both produce exactly one element
|
||||
# remap (same count), but the source field differs. A length-only comparison
|
||||
# would silently reuse the default aggregate, producing incorrect results for
|
||||
# the versioned API version.
|
||||
- note: round-trip versioned remap with same count but different source field
|
||||
aliases_json: |
|
||||
[
|
||||
{
|
||||
"namespace": "Microsoft.Network",
|
||||
"resourceTypes": [
|
||||
{
|
||||
"resourceType": "firewallPolicies",
|
||||
"aliases": [
|
||||
{
|
||||
"name": "Microsoft.Network/firewallPolicies/rules[*].priority",
|
||||
"defaultPath": "properties.rules[*].properties.oldA",
|
||||
"paths": [
|
||||
{
|
||||
"path": "properties.rules[*].properties.oldB",
|
||||
"apiVersions": ["2023-06-01"]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Network/firewallPolicies/rules[*].name",
|
||||
"defaultPath": "properties.rules[*].name",
|
||||
"paths": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
input:
|
||||
name: myPolicy
|
||||
type: "Microsoft.Network/firewallPolicies"
|
||||
properties:
|
||||
rules:
|
||||
- name: rule1
|
||||
properties:
|
||||
oldB: 100
|
||||
api_version: "2023-06-01"
|
||||
round_trip: true
|
||||
expected_round_trip:
|
||||
name: myPolicy
|
||||
type: "Microsoft.Network/firewallPolicies"
|
||||
properties:
|
||||
rules:
|
||||
- name: rule1
|
||||
properties:
|
||||
oldB: 100
|
||||
|
||||
- note: round-trip systemData and extendedLocation
|
||||
input:
|
||||
name: myStorage
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
location: westus2
|
||||
identity:
|
||||
type: SystemAssigned
|
||||
principalId: pid-123
|
||||
userAssignedIdentities:
|
||||
/subscriptions/Sub/resourceGroups/Rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/Uai1: {}
|
||||
systemData:
|
||||
createdBy: user@example.com
|
||||
createdByType: User
|
||||
createdAt: "2023-01-01T00:00:00Z"
|
||||
extendedLocation:
|
||||
name: edge-site-1
|
||||
type: EdgeZone
|
||||
properties:
|
||||
supportsHttpsTrafficOnly: true
|
||||
expected_normalized:
|
||||
name: myStorage
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
location: westus2
|
||||
identity:
|
||||
type: SystemAssigned
|
||||
principalid: pid-123
|
||||
userassignedidentities:
|
||||
/subscriptions/Sub/resourceGroups/Rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/Uai1: {}
|
||||
systemdata:
|
||||
createdby: user@example.com
|
||||
createdbytype: User
|
||||
createdat: "2023-01-01T00:00:00Z"
|
||||
extendedlocation:
|
||||
name: edge-site-1
|
||||
type: EdgeZone
|
||||
supportshttpstrafficonly: true
|
||||
round_trip: true
|
||||
expected_round_trip:
|
||||
name: myStorage
|
||||
type: "Microsoft.Storage/storageAccounts"
|
||||
location: westus2
|
||||
identity:
|
||||
type: SystemAssigned
|
||||
principalId: pid-123
|
||||
userAssignedIdentities:
|
||||
/subscriptions/Sub/resourceGroups/Rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/Uai1: {}
|
||||
systemData:
|
||||
createdBy: user@example.com
|
||||
createdByType: User
|
||||
createdAt: "2023-01-01T00:00:00Z"
|
||||
extendedLocation:
|
||||
name: edge-site-1
|
||||
type: EdgeZone
|
||||
properties:
|
||||
supportsHttpsTrafficOnly: true
|
||||
|
||||
# Regression: element remap must remove the stale ARM source field from the
|
||||
# normalized output. Without cleanup, both the alias short name AND the
|
||||
# original ARM leaf key survive; casing restoration during denormalization
|
||||
# then produces a duplicate key (e.g. both "prio" and "priority").
|
||||
- note: element remap removes stale ARM source field
|
||||
aliases_json: |
|
||||
[
|
||||
{
|
||||
"namespace": "Microsoft.Network",
|
||||
"resourceTypes": [
|
||||
{
|
||||
"resourceType": "firewallPolicies",
|
||||
"aliases": [
|
||||
{
|
||||
"name": "Microsoft.Network/firewallPolicies/rules[*].priority",
|
||||
"defaultPath": "properties.rules[*].properties.prio",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Network/firewallPolicies/rules[*].name",
|
||||
"defaultPath": "properties.rules[*].name",
|
||||
"paths": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
input:
|
||||
name: myPolicy
|
||||
type: "Microsoft.Network/firewallPolicies"
|
||||
properties:
|
||||
rules:
|
||||
- name: rule1
|
||||
properties:
|
||||
prio: 42
|
||||
# Normalize: "prio" (ARM leaf) → "priority" (alias short name).
|
||||
# The stale "prio" key must be removed from the normalized element.
|
||||
expected_normalized:
|
||||
name: myPolicy
|
||||
type: "Microsoft.Network/firewallPolicies"
|
||||
rules:
|
||||
- name: rule1
|
||||
priority: 42
|
||||
# Round-trip: normalize → denormalize should restore ARM structure.
|
||||
round_trip: true
|
||||
expected_round_trip:
|
||||
name: myPolicy
|
||||
type: "Microsoft.Network/firewallPolicies"
|
||||
properties:
|
||||
rules:
|
||||
- name: rule1
|
||||
properties:
|
||||
prio: 42
|
||||
|
||||
# Regression: same stale-field-cleanup scenario but with a dotted ARM leaf
|
||||
# path (e.g. "config.value" rather than a single segment "prio").
|
||||
# The remove_element_field helper must navigate to the parent object and
|
||||
# remove the leaf key via remove_at_dotted_path.
|
||||
- note: element remap removes stale dotted ARM source field
|
||||
aliases_json: |
|
||||
[
|
||||
{
|
||||
"namespace": "Microsoft.Test",
|
||||
"resourceTypes": [
|
||||
{
|
||||
"resourceType": "widgets",
|
||||
"aliases": [
|
||||
{
|
||||
"name": "Microsoft.Test/widgets/items[*].rating",
|
||||
"defaultPath": "properties.items[*].properties.config.score",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Test/widgets/items[*].name",
|
||||
"defaultPath": "properties.items[*].name",
|
||||
"paths": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
input:
|
||||
type: "Microsoft.Test/widgets"
|
||||
properties:
|
||||
items:
|
||||
- name: w1
|
||||
properties:
|
||||
config:
|
||||
score: 5
|
||||
# Normalize: "config.score" (dotted ARM leaf) → "rating" (alias short name).
|
||||
# The stale "config.score" path must be removed; only "rating" should remain.
|
||||
expected_normalized:
|
||||
type: "Microsoft.Test/widgets"
|
||||
items:
|
||||
- name: w1
|
||||
config: {}
|
||||
rating: 5
|
||||
# Round-trip: normalize → denormalize should restore ARM structure.
|
||||
round_trip: true
|
||||
expected_round_trip:
|
||||
type: "Microsoft.Test/widgets"
|
||||
properties:
|
||||
items:
|
||||
- name: w1
|
||||
properties:
|
||||
config:
|
||||
score: 5
|
||||
|
||||
# Regression: array base rename must move (not clone) the value so that
|
||||
# the stale ARM base key does not survive in the normalized output.
|
||||
# Without the removal, denormalization produces a duplicate key.
|
||||
- note: array base rename removes stale ARM base key
|
||||
aliases_json: |
|
||||
[
|
||||
{
|
||||
"namespace": "Microsoft.Test",
|
||||
"resourceTypes": [
|
||||
{
|
||||
"resourceType": "widgets",
|
||||
"aliases": [
|
||||
{
|
||||
"name": "Microsoft.Test/widgets/items[*].label",
|
||||
"defaultPath": "properties.entries[*].label",
|
||||
"paths": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
input:
|
||||
type: "Microsoft.Test/widgets"
|
||||
properties:
|
||||
entries:
|
||||
- label: hello
|
||||
# Normalize: ARM base "entries" → alias base "items".
|
||||
# The stale "entries" key must be removed; only "items" should remain.
|
||||
expected_normalized:
|
||||
type: "Microsoft.Test/widgets"
|
||||
items:
|
||||
- label: hello
|
||||
# Round-trip: normalize → denormalize should restore ARM structure.
|
||||
round_trip: true
|
||||
expected_round_trip:
|
||||
type: "Microsoft.Test/widgets"
|
||||
properties:
|
||||
entries:
|
||||
- label: hello
|
||||
|
||||
# Regression: denormalize reverse element remap with a dotted ARM target
|
||||
# must preserve restored casing (e.g. "Config.Score") rather than
|
||||
# re-lowercasing it.
|
||||
- note: round-trip dotted element remap preserves ARM casing
|
||||
aliases_json: |
|
||||
[
|
||||
{
|
||||
"namespace": "Microsoft.Test",
|
||||
"resourceTypes": [
|
||||
{
|
||||
"resourceType": "widgets",
|
||||
"aliases": [
|
||||
{
|
||||
"name": "Microsoft.Test/widgets/items[*].rating",
|
||||
"defaultPath": "properties.items[*].properties.Config.Score",
|
||||
"paths": []
|
||||
},
|
||||
{
|
||||
"name": "Microsoft.Test/widgets/items[*].name",
|
||||
"defaultPath": "properties.items[*].name",
|
||||
"paths": []
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
input:
|
||||
type: "Microsoft.Test/widgets"
|
||||
properties:
|
||||
items:
|
||||
- name: w1
|
||||
properties:
|
||||
Config:
|
||||
Score: 9
|
||||
expected_normalized:
|
||||
type: "Microsoft.Test/widgets"
|
||||
items:
|
||||
- name: w1
|
||||
config: {}
|
||||
rating: 9
|
||||
round_trip: true
|
||||
expected_round_trip:
|
||||
type: "Microsoft.Test/widgets"
|
||||
properties:
|
||||
items:
|
||||
- name: w1
|
||||
properties:
|
||||
Config:
|
||||
Score: 9
|
||||
322
tests/azure_policy/normalization/mod.rs
Normal file
322
tests/azure_policy/normalization/mod.rs
Normal file
@@ -0,0 +1,322 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
//! YAML-driven normalization / denormalization tests.
|
||||
//!
|
||||
//! Each YAML file contains a `cases` array. Every case specifies an `input`
|
||||
//! JSON value plus optional `aliases` and `api_version`. The runner then
|
||||
//! checks whichever of the following fields are present:
|
||||
//!
|
||||
//! * `expected_normalized` – result of normalizing `input`
|
||||
//! * `expected_denormalized` – result of denormalizing `input`
|
||||
//! * `round_trip` – normalize then denormalize; compare with `expected_round_trip`
|
||||
//! * `reverse_round_trip` – denormalize then normalize; compare with
|
||||
//! `expected_reverse_round_trip`
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
use serde::Deserialize;
|
||||
use test_generator::test_resources;
|
||||
|
||||
use regorus::languages::azure_policy::aliases::normalizer;
|
||||
use regorus::languages::azure_policy::aliases::types::ResolvedAliases;
|
||||
use regorus::languages::azure_policy::aliases::{denormalizer, AliasRegistry};
|
||||
use regorus::Value;
|
||||
|
||||
// ── YAML schema ──────────────────────────────────────────────────────────
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct YamlTest {
|
||||
#[serde(default)]
|
||||
aliases_json: Option<String>,
|
||||
#[serde(default)]
|
||||
aliases_file: Option<String>,
|
||||
#[serde(default)]
|
||||
data_manifest_json: Option<String>,
|
||||
#[serde(default)]
|
||||
data_manifest_file: Option<String>,
|
||||
cases: Vec<TestCase>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct TestCase {
|
||||
note: String,
|
||||
input: serde_json::Value,
|
||||
#[serde(default)]
|
||||
api_version: Option<String>,
|
||||
#[serde(default)]
|
||||
context: Option<serde_json::Value>,
|
||||
#[serde(default)]
|
||||
parameters: Option<serde_json::Value>,
|
||||
#[serde(default)]
|
||||
expected_normalized: Option<serde_json::Value>,
|
||||
#[serde(default)]
|
||||
expected_denormalized: Option<serde_json::Value>,
|
||||
#[serde(default)]
|
||||
expected_envelope: Option<serde_json::Value>,
|
||||
#[serde(default)]
|
||||
round_trip: bool,
|
||||
#[serde(default)]
|
||||
expected_round_trip: Option<serde_json::Value>,
|
||||
#[serde(default)]
|
||||
reverse_round_trip: bool,
|
||||
#[serde(default)]
|
||||
expected_reverse_round_trip: Option<serde_json::Value>,
|
||||
#[serde(default)]
|
||||
aliases_json: Option<String>,
|
||||
#[serde(default)]
|
||||
sub_resource_arrays: Option<Vec<String>>,
|
||||
#[serde(default)]
|
||||
resource_type: Option<String>,
|
||||
#[serde(default)]
|
||||
use_registry_api: bool,
|
||||
}
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────────
|
||||
|
||||
/// Convert a `serde_json::Value` to `regorus::Value`.
|
||||
fn to_regorus(v: &serde_json::Value) -> Value {
|
||||
Value::from(v.clone())
|
||||
}
|
||||
|
||||
fn load_registry(yaml_file: &str, test: &YamlTest) -> Result<Option<AliasRegistry>> {
|
||||
let mut reg = AliasRegistry::new();
|
||||
let mut loaded = false;
|
||||
|
||||
if let Some(ref inline) = test.aliases_json {
|
||||
reg.load_from_json(inline)?;
|
||||
loaded = true;
|
||||
}
|
||||
if let Some(ref relpath) = test.aliases_file {
|
||||
let base = Path::new(yaml_file).parent().unwrap_or(Path::new("."));
|
||||
let path = base.join(relpath);
|
||||
let json = std::fs::read_to_string(&path)?;
|
||||
reg.load_from_json(&json)?;
|
||||
loaded = true;
|
||||
}
|
||||
if let Some(ref inline) = test.data_manifest_json {
|
||||
reg.load_data_policy_manifest_json(inline)?;
|
||||
loaded = true;
|
||||
}
|
||||
if let Some(ref relpath) = test.data_manifest_file {
|
||||
let base = Path::new(yaml_file).parent().unwrap_or(Path::new("."));
|
||||
let path = base.join(relpath);
|
||||
let json = std::fs::read_to_string(&path)?;
|
||||
reg.load_data_policy_manifest_json(&json)?;
|
||||
loaded = true;
|
||||
}
|
||||
|
||||
Ok(if loaded { Some(reg) } else { None })
|
||||
}
|
||||
|
||||
fn case_override_registry(case: &TestCase) -> Result<Option<AliasRegistry>> {
|
||||
if let Some(ref inline) = case.aliases_json {
|
||||
let mut reg = AliasRegistry::new();
|
||||
reg.load_from_json(inline)?;
|
||||
return Ok(Some(reg));
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn resolve_aliases(
|
||||
registry: Option<&AliasRegistry>,
|
||||
case: &TestCase,
|
||||
input: &serde_json::Value,
|
||||
) -> Option<ResolvedAliases> {
|
||||
let resource_type = case
|
||||
.resource_type
|
||||
.clone()
|
||||
.or_else(|| input.get("type").and_then(|v| v.as_str()).map(String::from));
|
||||
|
||||
if let (Some(reg), Some(rt)) = (registry, resource_type.as_deref()) {
|
||||
if let Some(resolved) = reg.get(rt) {
|
||||
let mut r = resolved.clone();
|
||||
if let Some(ref subs) = case.sub_resource_arrays {
|
||||
r.sub_resource_arrays = subs.iter().map(|s| s.to_ascii_lowercase()).collect();
|
||||
}
|
||||
return Some(r);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref subs) = case.sub_resource_arrays {
|
||||
return Some(ResolvedAliases {
|
||||
resource_type: resource_type.unwrap_or_default(),
|
||||
entries: Default::default(),
|
||||
sub_resource_arrays: subs.iter().map(|s| s.to_ascii_lowercase()).collect(),
|
||||
default_aggregates: Default::default(),
|
||||
versioned_aggregates: Default::default(),
|
||||
});
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
fn pretty_regorus(v: &Value) -> String {
|
||||
v.to_json_str().unwrap_or_else(|_| format!("{v:?}"))
|
||||
}
|
||||
|
||||
// ── Runner ───────────────────────────────────────────────────────────────
|
||||
|
||||
fn run_yaml_test(file: &str) -> Result<()> {
|
||||
let yaml_str = std::fs::read_to_string(file)?;
|
||||
let test: YamlTest = serde_yaml::from_str(&yaml_str)?;
|
||||
let file_registry = load_registry(file, &test)?;
|
||||
|
||||
for case in &test.cases {
|
||||
print!(" case: {} … ", case.note);
|
||||
|
||||
let case_override = case_override_registry(case)?;
|
||||
let registry = case_override.as_ref().or(file_registry.as_ref());
|
||||
let resolved = resolve_aliases(registry, case, &case.input);
|
||||
let api_ver = case.api_version.as_deref();
|
||||
let input = to_regorus(&case.input);
|
||||
|
||||
// ── normalize ────────────────────────────────────────────────
|
||||
if let Some(ref expected) = case.expected_normalized {
|
||||
let expected = to_regorus(expected);
|
||||
let actual = if case.use_registry_api {
|
||||
normalizer::normalize(&input, registry, api_ver)
|
||||
} else {
|
||||
normalizer::normalize_with_aliases(&input, resolved.as_ref(), api_ver)
|
||||
};
|
||||
if actual != expected {
|
||||
bail!(
|
||||
"normalize mismatch in '{}':\nexpected:\n{}\nactual:\n{}",
|
||||
case.note,
|
||||
pretty_regorus(&expected),
|
||||
pretty_regorus(&actual),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── denormalize ──────────────────────────────────────────────
|
||||
if let Some(ref expected) = case.expected_denormalized {
|
||||
let expected = to_regorus(expected);
|
||||
let actual = if case.use_registry_api {
|
||||
denormalizer::denormalize(&input, registry, api_ver)
|
||||
} else {
|
||||
denormalizer::denormalize_with_aliases(&input, resolved.as_ref(), api_ver)
|
||||
};
|
||||
if actual != expected {
|
||||
bail!(
|
||||
"denormalize mismatch in '{}':\nexpected:\n{}\nactual:\n{}",
|
||||
case.note,
|
||||
pretty_regorus(&expected),
|
||||
pretty_regorus(&actual),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── envelope ─────────────────────────────────────────────────
|
||||
if let Some(ref expected) = case.expected_envelope {
|
||||
let expected = to_regorus(expected);
|
||||
let actual = if case.use_registry_api {
|
||||
if let Some(reg) = registry {
|
||||
reg.normalize_and_wrap(
|
||||
&input,
|
||||
api_ver,
|
||||
case.context.as_ref().map(to_regorus),
|
||||
case.parameters.as_ref().map(to_regorus),
|
||||
)
|
||||
} else {
|
||||
let norm = normalizer::normalize(&input, None, api_ver);
|
||||
normalizer::build_input_envelope(
|
||||
norm,
|
||||
case.context.as_ref().map(to_regorus),
|
||||
case.parameters.as_ref().map(to_regorus),
|
||||
)
|
||||
}
|
||||
} else {
|
||||
let norm = normalizer::normalize_with_aliases(&input, resolved.as_ref(), api_ver);
|
||||
normalizer::build_input_envelope(
|
||||
norm,
|
||||
case.context.as_ref().map(to_regorus),
|
||||
case.parameters.as_ref().map(to_regorus),
|
||||
)
|
||||
};
|
||||
if actual != expected {
|
||||
bail!(
|
||||
"envelope mismatch in '{}':\nexpected:\n{}\nactual:\n{}",
|
||||
case.note,
|
||||
pretty_regorus(&expected),
|
||||
pretty_regorus(&actual),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── round-trip ───────────────────────────────────────────────
|
||||
if case.round_trip {
|
||||
let (normalized, denormalized) = if case.use_registry_api {
|
||||
let n = normalizer::normalize(&input, registry, api_ver);
|
||||
let d = denormalizer::denormalize(&n, registry, api_ver);
|
||||
(n, d)
|
||||
} else {
|
||||
let n = normalizer::normalize_with_aliases(&input, resolved.as_ref(), api_ver);
|
||||
let d = denormalizer::denormalize_with_aliases(&n, resolved.as_ref(), api_ver);
|
||||
(n, d)
|
||||
};
|
||||
let _ = normalized;
|
||||
if let Some(ref expected) = case.expected_round_trip {
|
||||
let expected = to_regorus(expected);
|
||||
if denormalized != expected {
|
||||
bail!(
|
||||
"round-trip mismatch in '{}':\nexpected:\n{}\nactual:\n{}",
|
||||
case.note,
|
||||
pretty_regorus(&expected),
|
||||
pretty_regorus(&denormalized),
|
||||
);
|
||||
}
|
||||
} else if denormalized != input {
|
||||
bail!(
|
||||
"round-trip mismatch in '{}' (expected original input):\ninput:\n{}\nresult:\n{}",
|
||||
case.note,
|
||||
pretty_regorus(&input),
|
||||
pretty_regorus(&denormalized),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── reverse round-trip ─────────────────────────────────────
|
||||
if case.reverse_round_trip {
|
||||
let (denormalized, renormalized) = if case.use_registry_api {
|
||||
let d = denormalizer::denormalize(&input, registry, api_ver);
|
||||
let n = normalizer::normalize(&d, registry, api_ver);
|
||||
(d, n)
|
||||
} else {
|
||||
let d = denormalizer::denormalize_with_aliases(&input, resolved.as_ref(), api_ver);
|
||||
let n = normalizer::normalize_with_aliases(&d, resolved.as_ref(), api_ver);
|
||||
(d, n)
|
||||
};
|
||||
let _ = denormalized;
|
||||
if let Some(ref expected) = case.expected_reverse_round_trip {
|
||||
let expected = to_regorus(expected);
|
||||
if renormalized != expected {
|
||||
bail!(
|
||||
"reverse round-trip mismatch in '{}':\nexpected:\n{}\nactual:\n{}",
|
||||
case.note,
|
||||
pretty_regorus(&expected),
|
||||
pretty_regorus(&renormalized),
|
||||
);
|
||||
}
|
||||
} else if renormalized != input {
|
||||
bail!(
|
||||
"reverse round-trip mismatch in '{}' (expected original input):\ninput:\n{}\nresult:\n{}",
|
||||
case.note,
|
||||
pretty_regorus(&input),
|
||||
pretty_regorus(&renormalized),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
println!("ok");
|
||||
}
|
||||
|
||||
println!(" {} cases passed in {file}", test.cases.len());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test_resources("tests/azure_policy/normalization/cases/**/*.yaml")]
|
||||
fn run(path: &str) {
|
||||
run_yaml_test(path).unwrap()
|
||||
}
|
||||
@@ -12,5 +12,8 @@ mod lexer;
|
||||
mod parser;
|
||||
mod value;
|
||||
|
||||
#[cfg(feature = "azure_policy")]
|
||||
mod azure_policy;
|
||||
|
||||
#[cfg(feature = "rvm")]
|
||||
mod rvm;
|
||||
|
||||
Reference in New Issue
Block a user