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:
Anand Krishnamoorthi
2026-03-30 18:44:36 -05:00
committed by GitHub
parent 35fb5d5953
commit d36f952133
46 changed files with 11265 additions and 316 deletions

197
Cargo.lock generated
View File

@@ -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"

View File

@@ -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

View 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(&registry), 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(&registry), 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(&registry), None);
c.bench_function("denormalize/simple_storage", |b| {
b.iter(|| denormalizer::denormalize(black_box(&normalized), Some(&registry), 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(&registry), None);
group.bench_with_input(
BenchmarkId::from_parameter(rule_count),
&normalized,
|b, norm| b.iter(|| denormalizer::denormalize(black_box(norm), Some(&registry), 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(&registry), None);
denormalizer::denormalize(&n, Some(&registry), 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(&reg), 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(&registry), None);
denormalizer::denormalize(&n, Some(&registry), 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(&registry), 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(&registry), Some("2020-01-01"));
group.bench_with_input(
BenchmarkId::from_parameter(rule_count),
&normalized,
|b, norm| {
b.iter(|| {
denormalizer::denormalize(black_box(norm), Some(&registry), 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(&registry), Some("2020-01-01"));
denormalizer::denormalize(&n, Some(&registry), 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);

View 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");
}
}

View 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": &lt;normalized&gt;, "context": &lt;context&gt;, "parameters": &lt;params&gt; }</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);
}
}
}

View File

@@ -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
}

View File

@@ -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
View File

@@ -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"

View 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(&params_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);
}
}

View File

@@ -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.

View File

@@ -5,6 +5,7 @@
extern crate alloc;
mod alias_registry;
mod allocator;
mod common;
mod compile;

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View 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(),
}
}

View 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()
}

View 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)
}

View 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)
}

View 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()));
}

File diff suppressed because it is too large Load Diff

View File

@@ -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())
}

View 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())
}

View 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()))
}

View 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)
}

View 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));
}
}
}

View 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")
);
}
}

View File

@@ -3,4 +3,6 @@
//! Azure Policy language support.
#[allow(clippy::pattern_type_mismatch)]
pub mod aliases;
pub mod strings;

View File

@@ -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;

File diff suppressed because it is too large Load Diff

View 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": []
}
]
}
]
}
]

View File

@@ -0,0 +1,4 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
mod normalization;

View 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

View 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

View File

@@ -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"

View 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

View File

@@ -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

View 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

View File

@@ -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

View 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

View 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

View File

@@ -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

View File

@@ -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

View 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

View 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

View 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()
}

View File

@@ -12,5 +12,8 @@ mod lexer;
mod parser;
mod value;
#[cfg(feature = "azure_policy")]
mod azure_policy;
#[cfg(feature = "rvm")]
mod rvm;