pub mod denormalizer;
pub mod normalizer;
pub(crate) mod obj_map;
pub mod types;
use alloc::collections::{BTreeMap, BTreeSet};
use alloc::string::{String, ToString as _};
use alloc::vec::Vec;
use anyhow::Result;
use types::{
AliasEntry, AliasPath, DataPolicyManifest, PrecomputedRemap, PrecomputedReverseRemap,
ProviderAliases, ResolvedAliases, ResolvedEntry, VersionedAggregates,
};
use obj_map::{collision_safe_key, is_root_field_collision};
#[derive(Debug, Clone, Default)]
pub struct AliasRegistry {
types: BTreeMap<String, ResolvedAliases>,
alias_to_short: BTreeMap<String, String>,
alias_modifiable: BTreeMap<String, bool>,
}
impl AliasRegistry {
pub const fn new() -> Self {
Self {
types: BTreeMap::new(),
alias_to_short: BTreeMap::new(),
alias_modifiable: BTreeMap::new(),
}
}
pub fn load_from_json(&mut self, json: &str) -> Result<()> {
let providers: Vec<ProviderAliases> = types::load_auto(json)?;
for provider in providers {
self.load_provider(provider);
}
Ok(())
}
pub fn load_data_policy_manifest_json(&mut self, json: &str) -> Result<()> {
let manifest: DataPolicyManifest = serde_json::from_str(json)?;
self.load_data_policy_manifest(manifest);
Ok(())
}
pub fn load_data_policy_manifest(&mut self, manifest: DataPolicyManifest) {
let namespace = &manifest.data_namespace;
let known_rts: Vec<String> = manifest
.resource_type_aliases
.iter()
.map(|rta| rta.resource_type.clone())
.collect();
for rta in &manifest.resource_type_aliases {
let fq_type = alloc::format!("{}/{}", namespace, rta.resource_type);
let entries = convert_data_manifest_aliases(&fq_type, &rta.aliases);
self.ingest_alias_entries(&fq_type, &entries);
}
let mut grouped: BTreeMap<String, Vec<AliasEntry>> = BTreeMap::new();
let ns_prefix = alloc::format!("{}/", namespace);
for alias in &manifest.aliases {
let suffix = if alias.name.len() > ns_prefix.len()
&& alias.name[..ns_prefix.len()].eq_ignore_ascii_case(&ns_prefix)
{
&alias.name[ns_prefix.len()..]
} else {
continue;
};
let mut best_rt: Option<&str> = None;
let mut best_len = 0;
for rt in &known_rts {
if suffix.len() > rt.len()
&& suffix[..rt.len()].eq_ignore_ascii_case(rt)
&& suffix.as_bytes().get(rt.len()) == Some(&b'/')
&& rt.len() > best_len
{
best_rt = Some(rt.as_str());
best_len = rt.len();
}
}
let entry = data_alias_to_alias_entry(alias);
if let Some(rt) = best_rt {
let fq_type = alloc::format!("{}/{}", namespace, rt);
grouped.entry(fq_type).or_default().push(entry);
}
}
for (fq_type, entries) in &grouped {
self.ingest_alias_entries(fq_type, entries);
}
}
pub fn load_provider(&mut self, provider: ProviderAliases) {
let namespace = &provider.namespace;
for rt in provider.resource_types {
let fq_type = alloc::format!("{}/{}", namespace, rt.resource_type);
self.ingest_alias_entries(&fq_type, &rt.aliases);
}
}
fn ingest_alias_entries(&mut self, fq_type: &str, aliases: &[AliasEntry]) {
let prefix = alloc::format!("{}/", fq_type);
for alias in aliases {
if alias.default_path.is_none() {
continue;
}
let raw_short = if alias.name.len() > prefix.len()
&& alias
.name
.get(..prefix.len())
.is_some_and(|s| s.eq_ignore_ascii_case(&prefix))
{
alias
.name
.get(prefix.len()..)
.unwrap_or(&alias.name)
.to_string()
} else if let Some(rest) = alias
.name
.rfind('/')
.and_then(|idx| alias.name.get(idx.saturating_add(1)..))
{
rest.to_string()
} else {
continue;
};
let default_path = alias.default_path.as_deref().unwrap_or("");
let raw_short_normalized = normalize_short_name(&raw_short).to_string();
let short = if is_root_field_collision(&raw_short_normalized, default_path) {
collision_safe_key(&raw_short_normalized)
} else {
raw_short_normalized
};
let lc_name = alias.name.to_lowercase();
self.alias_to_short.insert(lc_name.clone(), short);
let is_modifiable = types::has_flag(
alias
.default_metadata
.as_ref()
.and_then(|m| m.attributes.as_deref()),
"Modifiable",
);
self.alias_modifiable.insert(lc_name, is_modifiable);
}
let resolved = resolve_resource_type(fq_type, aliases);
let lc_type = fq_type.to_lowercase();
if let Some(existing) = self.types.get_mut(&lc_type) {
for (key, entry) in resolved.entries {
existing.entries.insert(key, entry);
}
existing.sub_resource_arrays = redetect_sub_resource_arrays(&existing.entries);
let (default_agg, versioned_agg) =
precompute_aggregates(&existing.entries, &existing.sub_resource_arrays);
existing.default_aggregates = default_agg;
existing.versioned_aggregates = versioned_agg;
} else {
self.types.insert(lc_type, resolved);
}
}
pub fn get(&self, resource_type: &str) -> Option<&ResolvedAliases> {
self.types.get(&resource_type.to_lowercase())
}
pub fn len(&self) -> usize {
self.types.len()
}
pub fn is_empty(&self) -> bool {
self.types.is_empty()
}
pub fn resolve_alias(&self, fq_name: &str) -> Option<&str> {
self.alias_to_short
.get(&fq_name.to_lowercase())
.map(String::as_str)
}
pub const fn alias_map(&self) -> &BTreeMap<String, String> {
&self.alias_to_short
}
pub const fn alias_modifiable_map(&self) -> &BTreeMap<String, bool> {
&self.alias_modifiable
}
pub fn normalize_and_wrap(
&self,
arm_resource: &crate::Value,
api_version: Option<&str>,
context: Option<crate::Value>,
parameters: Option<crate::Value>,
) -> crate::Value {
let normalized = normalizer::normalize(arm_resource, Some(self), api_version);
normalizer::build_input_envelope(normalized, context, parameters)
}
pub fn denormalize(
&self,
normalized: &crate::Value,
api_version: Option<&str>,
) -> crate::Value {
denormalizer::denormalize(normalized, Some(self), api_version)
}
}
fn resolve_resource_type(fq_type: &str, aliases: &[types::AliasEntry]) -> ResolvedAliases {
let prefix = alloc::format!("{}/", fq_type);
let mut entries = BTreeMap::new();
let mut sub_resource_arrays: BTreeSet<String> = BTreeSet::new();
for alias in aliases {
let short_name = if alias.name.len() > prefix.len()
&& alias.name[..prefix.len()].eq_ignore_ascii_case(&prefix)
{
&alias.name[prefix.len()..]
} else if let Some(rest) = alias
.name
.rfind('/')
.and_then(|idx| alias.name.get(idx.saturating_add(1)..))
{
rest
} else {
&alias.name
};
let short_name = normalize_short_name(short_name);
let default_path = match &alias.default_path {
Some(p) => p.clone(),
None => continue, };
detect_sub_resource_array(short_name, &default_path, &mut sub_resource_arrays);
let versioned_paths: Vec<(String, String)> = alias
.paths
.iter()
.flat_map(|p| {
p.api_versions
.iter()
.map(move |v| (v.clone(), p.path.clone()))
})
.collect();
entries.insert(
short_name.to_lowercase(),
ResolvedEntry::new(
short_name.to_string(),
default_path,
versioned_paths,
alias.default_metadata.clone(),
),
);
}
let (default_aggregates, versioned_aggregates) =
precompute_aggregates(&entries, &sub_resource_arrays);
ResolvedAliases {
resource_type: fq_type.to_string(),
entries,
sub_resource_arrays,
default_aggregates,
versioned_aggregates,
}
}
type AggregateFields = (VersionedAggregates, BTreeMap<String, VersionedAggregates>);
fn precompute_aggregates(
entries: &BTreeMap<String, ResolvedEntry>,
sub_resource_arrays: &BTreeSet<String>,
) -> AggregateFields {
let mut all_versions = alloc::collections::BTreeSet::new();
for entry in entries.values() {
for (ver, _) in &entry.versioned_paths {
all_versions.insert(ver.to_lowercase());
}
}
let default_agg = compute_aggregates_for_version(entries, sub_resource_arrays, None);
let mut versioned_map = BTreeMap::new();
for ver in &all_versions {
let agg = compute_aggregates_for_version(entries, sub_resource_arrays, Some(ver.as_str()));
if agg != default_agg {
versioned_map.insert(ver.clone(), agg);
}
}
(default_agg, versioned_map)
}
fn compute_aggregates_for_version(
entries: &BTreeMap<String, ResolvedEntry>,
sub_resource_arrays: &BTreeSet<String>,
api_version: Option<&str>,
) -> VersionedAggregates {
let mut element_remaps = Vec::new();
let mut reverse_element_remaps = Vec::new();
let mut renames_norm: Vec<(String, String)> = Vec::new();
let mut renames_denorm: Vec<(String, String)> = Vec::new();
let mut seen_norm = alloc::collections::BTreeSet::new();
let mut seen_denorm = alloc::collections::BTreeSet::new();
for entry in entries.values() {
if !entry.is_wildcard {
continue;
}
if sub_resource_arrays.contains(&entry.short_name.to_ascii_lowercase()) {
continue;
}
let selected_path = entry.select_path(api_version);
let short_parts: Vec<&str> = entry.short_name.split("[*].").collect();
let arm_raw_parts: Vec<&str> = selected_path.split("[*].").collect();
if short_parts.len() >= 2 && arm_raw_parts.len() >= 2 {
if let (Some(short_leaf), Some(arm_leaf_raw)) =
(short_parts.last(), arm_raw_parts.last())
{
let arm_leaf = arm_leaf_raw
.strip_prefix("properties.")
.unwrap_or(arm_leaf_raw);
if !short_leaf.eq_ignore_ascii_case(arm_leaf) {
let array_chain: Vec<Vec<String>> = short_parts
.split_last()
.map(|(_, init)| init)
.unwrap_or_default()
.iter()
.map(|part| part.split('.').map(|s| s.to_ascii_lowercase()).collect())
.collect();
let source_lc = arm_leaf.to_ascii_lowercase();
let target_lc = short_leaf.to_ascii_lowercase();
element_remaps.push(PrecomputedRemap {
array_chain: array_chain.clone(),
source_field: source_lc.clone(),
target_field: target_lc.clone(),
});
reverse_element_remaps.push(PrecomputedReverseRemap {
array_chain: array_chain.clone(),
source_field: target_lc.clone(),
target_field: source_lc,
cleanup_field: target_lc,
});
}
}
}
if let (Some(short_base), Some(arm_base)) = (
entry.short_name.split("[*]").next(),
selected_path.split("[*]").next(),
) {
let arm_base_stripped = arm_base.strip_prefix("properties.").unwrap_or(arm_base);
if !short_base.eq_ignore_ascii_case(arm_base_stripped) {
let norm_pair = (
arm_base_stripped.to_ascii_lowercase(),
short_base.to_ascii_lowercase(),
);
if seen_norm.insert(norm_pair.clone()) {
renames_norm.push(norm_pair);
}
let denorm_pair = (
short_base.to_ascii_lowercase(),
arm_base_stripped.to_string(),
);
if seen_denorm.insert(denorm_pair.clone()) {
renames_denorm.push(denorm_pair);
}
}
}
}
VersionedAggregates {
element_remaps,
reverse_element_remaps,
array_renames_normalize: renames_norm,
array_renames_denormalize: renames_denorm,
}
}
fn detect_sub_resource_array(
short_name: &str,
default_path: &str,
sub_resource_arrays: &mut BTreeSet<String>,
) {
let short_parts: Vec<&str> = short_name.split("[*].").collect();
let path_parts: Vec<&str> = default_path.split("[*].").collect();
if short_parts.len() < 2 || path_parts.len() < 2 {
return; }
let mut accumulated_name = String::new();
for (i, array_field) in short_parts
.iter()
.enumerate()
.take(short_parts.len().saturating_sub(1))
{
if let Some(next_path_segment) = path_parts.get(i.saturating_add(1)) {
if next_path_segment.starts_with("properties.")
|| next_path_segment.starts_with("properties/")
{
let name = if accumulated_name.is_empty() {
array_field.to_string()
} else {
alloc::format!("{}.{}", accumulated_name, array_field)
};
sub_resource_arrays.insert(name.to_ascii_lowercase());
accumulated_name = name;
}
}
}
}
fn redetect_sub_resource_arrays(entries: &BTreeMap<String, ResolvedEntry>) -> BTreeSet<String> {
let mut sub_resource_arrays = BTreeSet::new();
for entry in entries.values() {
detect_sub_resource_array(
&entry.short_name,
&entry.default_path,
&mut sub_resource_arrays,
);
}
sub_resource_arrays
}
fn data_alias_to_alias_entry(dma: &types::DataManifestAlias) -> AliasEntry {
let default_path = dma.paths.first().map(|p| p.path.clone());
let paths: Vec<AliasPath> = dma
.paths
.iter()
.map(|p| {
let mut versions = p.api_versions.clone();
versions.extend(p.schema_versions.iter().cloned());
AliasPath {
path: p.path.clone(),
api_versions: versions,
metadata: None,
..Default::default()
}
})
.collect();
AliasEntry {
name: dma.name.clone(),
default_path,
default_metadata: None,
paths,
..Default::default()
}
}
fn convert_data_manifest_aliases(
_fq_type: &str,
aliases: &[types::DataManifestAlias],
) -> Vec<AliasEntry> {
aliases.iter().map(data_alias_to_alias_entry).collect()
}
fn normalize_short_name(short: &str) -> &str {
short
.strip_prefix("properties.")
.or_else(|| short.strip_prefix("properties/"))
.unwrap_or(short)
}
#[cfg(test)]
#[allow(clippy::indexing_slicing, clippy::unwrap_used, clippy::expect_used)]
mod tests {
use alloc::string::ToString as _;
use alloc::vec;
use super::*;
#[test]
fn test_detect_sub_resource_nsg() {
let mut subs = BTreeSet::new();
detect_sub_resource_array(
"securityRules[*].protocol",
"properties.securityRules[*].properties.protocol",
&mut subs,
);
assert_eq!(subs, BTreeSet::from(["securityrules".to_string()]));
}
#[test]
fn test_detect_no_sub_resource() {
let mut subs = BTreeSet::new();
detect_sub_resource_array(
"networkAcls.ipRules[*].value",
"properties.networkAcls.ipRules[*].value",
&mut subs,
);
assert!(subs.is_empty());
}
#[test]
fn test_detect_nested_sub_resource() {
let mut subs = BTreeSet::new();
detect_sub_resource_array(
"subnets[*].ipConfigurations[*].name",
"properties.subnets[*].properties.ipConfigurations[*].properties.name",
&mut subs,
);
let expected: BTreeSet<String> = BTreeSet::from([
"subnets".to_string(),
"subnets.ipconfigurations".to_string(),
]);
assert_eq!(subs, expected);
}
#[test]
fn test_resolve_resource_type_basic() {
let aliases = vec![
types::AliasEntry {
name: "Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly".to_string(),
default_path: Some("properties.supportsHttpsTrafficOnly".to_string()),
default_metadata: None,
paths: vec![],
..Default::default()
},
types::AliasEntry {
name: "Microsoft.Storage/storageAccounts/sku.name".to_string(),
default_path: Some("sku.name".to_string()),
default_metadata: None,
paths: vec![],
..Default::default()
},
];
let resolved = resolve_resource_type("Microsoft.Storage/storageAccounts", &aliases);
assert_eq!(resolved.entries.len(), 2);
let https_entry = resolved.entries.get("supportshttpstrafficonly").unwrap();
assert_eq!(
https_entry.default_path,
"properties.supportsHttpsTrafficOnly"
);
let sku_entry = resolved.entries.get("sku.name").unwrap();
assert_eq!(sku_entry.default_path, "sku.name");
assert!(resolved.sub_resource_arrays.is_empty());
}
#[test]
fn test_resolve_nsg_has_sub_resource_arrays() {
let aliases = vec![
types::AliasEntry {
name: "Microsoft.Network/networkSecurityGroups/securityRules[*].protocol"
.to_string(),
default_path: Some("properties.securityRules[*].properties.protocol".to_string()),
default_metadata: None,
paths: vec![],
..Default::default()
},
types::AliasEntry {
name: "Microsoft.Network/networkSecurityGroups/securityRules[*].access".to_string(),
default_path: Some("properties.securityRules[*].properties.access".to_string()),
default_metadata: None,
paths: vec![],
..Default::default()
},
];
let resolved = resolve_resource_type("Microsoft.Network/networkSecurityGroups", &aliases);
assert_eq!(
resolved.sub_resource_arrays,
BTreeSet::from(["securityrules".to_string()])
);
}
#[test]
fn test_load_from_json() {
let json = r#"[
{
"namespace": "Microsoft.Storage",
"resourceTypes": [
{
"resourceType": "storageAccounts",
"aliases": [
{
"name": "Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly",
"defaultPath": "properties.supportsHttpsTrafficOnly",
"paths": []
}
]
}
]
}
]"#;
let mut registry = AliasRegistry::new();
registry.load_from_json(json).unwrap();
assert_eq!(registry.len(), 1);
let resolved = registry.get("Microsoft.Storage/storageAccounts").unwrap();
assert_eq!(resolved.entries.len(), 1);
assert!(resolved.entries.contains_key("supportshttpstrafficonly"));
}
#[test]
fn test_registry_case_insensitive_get() {
let json = r#"[
{
"namespace": "Microsoft.Storage",
"resourceTypes": [
{
"resourceType": "storageAccounts",
"aliases": [
{
"name": "Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly",
"defaultPath": "properties.supportsHttpsTrafficOnly",
"paths": []
}
]
}
]
}
]"#;
let mut registry = AliasRegistry::new();
registry.load_from_json(json).unwrap();
assert!(registry.get("microsoft.storage/STORAGEACCOUNTS").is_some());
assert!(registry.get("MICROSOFT.STORAGE/storageAccounts").is_some());
}
#[test]
fn test_resolve_with_versioned_paths() {
let aliases = vec![types::AliasEntry {
name: "Microsoft.Web/sites/siteConfig.numberOfWorkers".to_string(),
default_path: Some("properties.siteConfig.numberOfWorkers".to_string()),
default_metadata: None,
paths: vec![
types::AliasPath {
path: "properties.siteConfig.properties.numberOfWorkers".to_string(),
api_versions: vec!["2014-04-01".to_string(), "2014-06-01".to_string()],
metadata: None,
..Default::default()
},
types::AliasPath {
path: "properties.siteConfig.numberOfWorkers".to_string(),
api_versions: vec!["2021-01-01".to_string()],
metadata: None,
..Default::default()
},
],
..Default::default()
}];
let resolved = resolve_resource_type("Microsoft.Web/sites", &aliases);
let entry = resolved.entries.get("siteconfig.numberofworkers").unwrap();
assert_eq!(entry.default_path, "properties.siteConfig.numberOfWorkers");
assert_eq!(entry.versioned_paths.len(), 3); assert_eq!(
entry.select_path(Some("2014-04-01")),
"properties.siteConfig.properties.numberOfWorkers"
);
assert_eq!(
entry.select_path(Some("2021-01-01")),
"properties.siteConfig.numberOfWorkers"
);
assert_eq!(
entry.select_path(Some("9999-01-01")),
"properties.siteConfig.numberOfWorkers"
);
}
#[test]
fn test_resolve_alias_without_default_path_skipped() {
let aliases = vec![
types::AliasEntry {
name: "Microsoft.Storage/storageAccounts/good".to_string(),
default_path: Some("properties.good".to_string()),
default_metadata: None,
paths: vec![],
..Default::default()
},
types::AliasEntry {
name: "Microsoft.Storage/storageAccounts/bad".to_string(),
default_path: None,
default_metadata: None,
paths: vec![],
..Default::default()
},
];
let resolved = resolve_resource_type("Microsoft.Storage/storageAccounts", &aliases);
assert_eq!(resolved.entries.len(), 1);
assert!(resolved.entries.contains_key("good"));
assert!(!resolved.entries.contains_key("bad"));
}
#[test]
fn test_empty_registry() {
let registry = AliasRegistry::new();
assert!(registry.is_empty());
assert_eq!(registry.len(), 0);
assert!(registry.get("Microsoft.Storage/storageAccounts").is_none());
}
#[test]
fn test_resolve_empty_aliases() {
let resolved = resolve_resource_type("Microsoft.Test/empty", &[]);
assert!(resolved.entries.is_empty());
assert!(resolved.sub_resource_arrays.is_empty());
}
#[test]
fn test_sub_resource_dedup() {
let aliases = vec![
types::AliasEntry {
name: "T/R/rules[*].a".to_string(),
default_path: Some("properties.rules[*].properties.a".to_string()),
default_metadata: None,
paths: vec![],
..Default::default()
},
types::AliasEntry {
name: "T/R/rules[*].b".to_string(),
default_path: Some("properties.rules[*].properties.b".to_string()),
default_metadata: None,
paths: vec![],
..Default::default()
},
types::AliasEntry {
name: "T/R/rules[*].c".to_string(),
default_path: Some("properties.rules[*].properties.c".to_string()),
default_metadata: None,
paths: vec![],
..Default::default()
},
];
let resolved = resolve_resource_type("T/R", &aliases);
assert_eq!(
resolved.sub_resource_arrays,
BTreeSet::from(["rules".to_string()])
);
}
#[test]
fn test_normalize_and_wrap_full_pipeline() {
let json = r#"[
{
"namespace": "Microsoft.Network",
"resourceTypes": [
{
"resourceType": "networkSecurityGroups",
"aliases": [
{
"name": "Microsoft.Network/networkSecurityGroups/securityRules[*].protocol",
"defaultPath": "properties.securityRules[*].properties.protocol",
"paths": []
}
]
}
]
}
]"#;
let mut registry = AliasRegistry::new();
registry.load_from_json(json).unwrap();
let arm_resource = crate::Value::from_json_str(
r#"{
"name": "myNsg",
"type": "Microsoft.Network/networkSecurityGroups",
"properties": {
"securityRules": [
{
"name": "rule1",
"properties": {
"protocol": "Tcp"
}
}
]
}
}"#,
)
.unwrap();
let context = crate::Value::from_json_str(r#"{"resourceGroup": {"name": "rg1"}}"#).unwrap();
let parameters = crate::Value::from_json_str(r#"{"env": "prod"}"#).unwrap();
let envelope =
registry.normalize_and_wrap(&arm_resource, None, Some(context), Some(parameters));
assert_eq!(envelope["resource"]["name"], crate::Value::from("myNsg"));
let rules = envelope["resource"]["securityrules"].as_array().unwrap();
assert_eq!(rules[0]["protocol"], crate::Value::from("Tcp"));
assert_eq!(rules[0]["properties"], crate::Value::Undefined);
assert_eq!(
envelope["context"]["resourceGroup"]["name"],
crate::Value::from("rg1")
);
assert_eq!(envelope["parameters"]["env"], crate::Value::from("prod"));
}
#[test]
fn test_load_test_aliases_json() {
let json = std::fs::read_to_string("tests/azure_policy/aliases/test_aliases.json")
.expect("test_aliases.json should exist");
let mut registry = AliasRegistry::new();
registry
.load_from_json(&json)
.expect("test_aliases.json should parse");
assert_eq!(registry.len(), 44);
let storage = registry
.get("Microsoft.Storage/storageAccounts")
.expect("Storage aliases should exist");
assert!(!storage.entries.is_empty());
assert!(storage.sub_resource_arrays.is_empty());
let nsg = registry
.get("Microsoft.Network/networkSecurityGroups")
.expect("NSG aliases should exist");
assert!(!nsg.entries.is_empty());
assert!(
nsg.sub_resource_arrays.contains("securityrules"),
"NSG should detect securityRules as sub-resource array"
);
assert!(registry.get("Microsoft.KeyVault/vaults").is_some());
assert!(registry.get("Microsoft.Sql/servers").is_some());
assert!(registry.get("Microsoft.Compute/virtualMachines").is_some());
assert!(registry.get("Microsoft.Web/sites").is_some());
assert!(registry
.get("Microsoft.ContainerService/managedClusters")
.is_some());
assert!(registry.get("Microsoft.Compute/disks").is_some());
let nic = registry
.get("Microsoft.Network/networkInterfaces")
.expect("NIC aliases should exist");
assert!(
nic.sub_resource_arrays.contains("ipconfigurations"),
"NIC should detect ipConfigurations as sub-resource array"
);
}
#[test]
fn test_resolve_alias_basic() {
let json = r#"[
{
"namespace": "Microsoft.Storage",
"resourceTypes": [
{
"resourceType": "storageAccounts",
"aliases": [
{
"name": "Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly",
"defaultPath": "properties.supportsHttpsTrafficOnly",
"paths": []
},
{
"name": "Microsoft.Storage/storageAccounts/sku.name",
"defaultPath": "sku.name",
"paths": []
}
]
}
]
}
]"#;
let mut registry = AliasRegistry::new();
registry.load_from_json(json).unwrap();
assert_eq!(
registry.resolve_alias("Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly"),
Some("supportsHttpsTrafficOnly")
);
assert_eq!(
registry.resolve_alias("Microsoft.Storage/storageAccounts/sku.name"),
Some("sku.name")
);
assert_eq!(
registry.resolve_alias("microsoft.storage/STORAGEACCOUNTS/supportsHttpsTrafficOnly"),
Some("supportsHttpsTrafficOnly")
);
assert_eq!(
registry.resolve_alias("Microsoft.Storage/storageAccounts/unknown"),
None
);
assert_eq!(registry.resolve_alias("supportsHttpsTrafficOnly"), None);
}
#[test]
fn test_alias_map_for_compiler() {
let json = r#"[
{
"namespace": "Microsoft.Network",
"resourceTypes": [
{
"resourceType": "networkSecurityGroups",
"aliases": [
{
"name": "Microsoft.Network/networkSecurityGroups/securityRules[*].protocol",
"defaultPath": "properties.securityRules[*].properties.protocol",
"paths": []
}
]
}
]
}
]"#;
let mut registry = AliasRegistry::new();
registry.load_from_json(json).unwrap();
let map = registry.alias_map();
assert_eq!(
map.get("microsoft.network/networksecuritygroups/securityrules[*].protocol"),
Some(&"securityRules[*].protocol".to_string())
);
}
#[test]
fn test_alias_modifiable_comma_separated_flags() {
let json = r#"[
{
"namespace": "Microsoft.Test",
"resourceTypes": [
{
"resourceType": "widgets",
"aliases": [
{
"name": "Microsoft.Test/widgets/singleFlag",
"defaultPath": "properties.singleFlag",
"paths": [],
"defaultMetadata": {
"attributes": "Modifiable"
}
},
{
"name": "Microsoft.Test/widgets/multiFlag",
"defaultPath": "properties.multiFlag",
"paths": [],
"defaultMetadata": {
"attributes": "Modifiable, SupportsCreate"
}
},
{
"name": "Microsoft.Test/widgets/notModifiable",
"defaultPath": "properties.notModifiable",
"paths": [],
"defaultMetadata": {
"attributes": "None"
}
},
{
"name": "Microsoft.Test/widgets/noMetadata",
"defaultPath": "properties.noMetadata",
"paths": []
}
]
}
]
}
]"#;
let mut registry = AliasRegistry::new();
registry.load_from_json(json).unwrap();
let modifiable = registry.alias_modifiable_map();
assert_eq!(
modifiable.get("microsoft.test/widgets/singleflag"),
Some(&true)
);
assert_eq!(
modifiable.get("microsoft.test/widgets/multiflag"),
Some(&true)
);
assert_eq!(
modifiable.get("microsoft.test/widgets/notmodifiable"),
Some(&false)
);
assert_eq!(
modifiable.get("microsoft.test/widgets/nometadata"),
Some(&false)
);
}
#[test]
fn test_overwrite_removes_stale_sub_resource_arrays() {
let json1 = r#"[
{
"namespace": "Microsoft.Test",
"resourceTypes": [
{
"resourceType": "firewalls",
"aliases": [
{
"name": "Microsoft.Test/firewalls/rules[*].protocol",
"defaultPath": "properties.rules[*].properties.protocol",
"paths": []
},
{
"name": "Microsoft.Test/firewalls/rules[*].port",
"defaultPath": "properties.rules[*].properties.port",
"paths": []
}
]
}
]
}
]"#;
let mut registry = AliasRegistry::new();
registry.load_from_json(json1).unwrap();
let fw = registry.get("Microsoft.Test/firewalls").unwrap();
assert!(
fw.sub_resource_arrays.contains("rules"),
"first load should detect 'rules' as a sub-resource array"
);
let json2 = r#"[
{
"namespace": "Microsoft.Test",
"resourceTypes": [
{
"resourceType": "firewalls",
"aliases": [
{
"name": "Microsoft.Test/firewalls/rules[*].protocol",
"defaultPath": "properties.rules[*].protocol",
"paths": []
},
{
"name": "Microsoft.Test/firewalls/rules[*].port",
"defaultPath": "properties.rules[*].port",
"paths": []
}
]
}
]
}
]"#;
registry.load_from_json(json2).unwrap();
let fw2 = registry.get("Microsoft.Test/firewalls").unwrap();
assert!(
!fw2.sub_resource_arrays.contains("rules"),
"after overwrite, 'rules' should no longer be a sub-resource array"
);
}
}