mirror of
https://github.com/microsoft/regorus.git
synced 2026-08-05 02:16:11 +00:00
d36f952133
* 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
228 lines
8.0 KiB
Rust
228 lines
8.0 KiB
Rust
// 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)
|
|
}
|