Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 1 addition & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ default = ["full-opa", "arc", "rvm"]

arc = []
ast = []
azure_policy = ["dep:jsonschema", "dep:chrono", "dep:ipnet", "dep:icu_casemap", "dep:hashbrown", "arc", "dashmap"]
azure_policy = ["dep:jsonschema", "dep:chrono", "dep:ipnet", "dep:icu_casemap", "arc", "dashmap"]
azure-rbac = ["regex", "time", "net"]
base64 = ["dep:data-encoding"]
base64url = ["dep:data-encoding"]
Expand Down Expand Up @@ -99,7 +99,6 @@ 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 }

Expand Down
1 change: 0 additions & 1 deletion bindings/ffi/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 0 additions & 13 deletions src/languages/azure_policy/aliases/denormalizer/helpers.rs

This file was deleted.

200 changes: 90 additions & 110 deletions src/languages/azure_policy/aliases/denormalizer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,28 @@

//! Normalized `input.resource` → ARM JSON reverse transformation.

mod casing;
pub(crate) mod helpers;
mod sub_resource;
pub(crate) mod casing;
pub(crate) mod sub_resource;

#[cfg(test)]
mod tests;

use alloc::collections::BTreeSet;
use alloc::vec::Vec;

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,
extract_type_field, find_key_ci, get_path_exact_or_ci, make_value, new_map,
obj_get_exact_or_ci, obj_insert, remove_element_field_ci, set_nested, val_str, ROOT_FIELDS,
};
use super::types::ResolvedAliases;
use super::AliasRegistry;

use super::normalizer::{apply_element_remap, ElementRemap};
use super::normalizer::apply_element_remap_reverse;

use casing::{build_casing_map, default_casing_map, denormalize_value, restore_casing};
use helpers::find_key_ci;

use super::obj_map::remove_element_field;
use casing::{default_casing_map, denormalize_value, restore_casing};

/// Denormalize a normalized resource back to ARM JSON structure.
pub fn denormalize(
Expand All @@ -51,13 +47,12 @@ pub fn denormalize_with_aliases(
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 selected_agg = aliases.map(|a| a.select_aggregates(api_version));
let mut default_cm = None;
let casing_map = aliases.map_or_else(
|| &*default_cm.insert(default_casing_map()),
|a| &a.casing_map,
);
let is_data_plane =
extract_type_field(normalized).is_some_and(|t| t.to_ascii_lowercase().contains(".data/"));

Expand All @@ -67,17 +62,9 @@ pub fn denormalize_with_aliases(
// 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)
});
let found = obj_get_exact_or_ci(obj, &lc);
if let Some(val) = found {
let restored = denormalize_value(val, &casing_map);
let restored = denormalize_value(val, casing_map);
obj_insert(&mut result, field, restored);
}
}
Expand All @@ -94,132 +81,125 @@ pub fn denormalize_with_aliases(

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()));
let has_alias = selected_agg.is_some_and(|agg| {
agg.alias_owned_normalized_roots
.contains(lookup_key_lc.as_str())
});
if has_alias {
continue;
}

let denorm_val = denormalize_value(val, &casing_map);
let denorm_val = denormalize_value(val, casing_map);

if key_s.starts_with("_p_") {
let restored = restore_casing(lookup_key, &casing_map);
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);
let restored = restore_casing(key_s, casing_map);
obj_insert(&mut result, &restored, denorm_val);
} else {
let restored = restore_casing(key_s, &casing_map);
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)
});
if let Some(agg) = selected_agg {
for op in &agg.scalar_aliases_denormalize {
let val = get_path_exact_or_ci(obj, &op.normalized_path_segments);
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);
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);
if op.write_to_properties {
let props_path = op
.arm_path
.strip_prefix("properties.")
.unwrap_or(&op.arm_path);
set_nested(&mut properties, props_path, denorm_val, false);
} else {
set_nested_verbatim(&mut result, arm_path, denorm_val);
set_nested(&mut result, &op.arm_path, denorm_val, false);
}
}

// 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);
}
//
// For data-plane resources, non-root fields live in `result` (no
// "properties" wrapper) but Phases 2c/2d/3 all operate on
// `properties`. Stage data-plane fields into `properties` so the
// subsequent phases can process them uniformly; Phase 4 merges
// them back into `result` at the top level.
if is_data_plane {
let keys_to_move: Vec<Value> = result
.keys()
.filter(|k| {
val_str(k)
.is_some_and(|s| !ROOT_FIELDS.iter().any(|f| f.eq_ignore_ascii_case(s)))
})
.cloned()
.collect();
for k in keys_to_move {
if let Some(v) = result.remove(&k) {
properties.entry(k).or_insert(v);
}
}
}

// 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 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) {
obj_insert(&mut properties, arm_base, val);
}
}
}

// Phase 2d: Precomputed reverse element-level field remaps.
// target_field is already stored with original ARM casing.
// Uses case-insensitive lookups because Phase 2a restored key casing
// but array_chain/source_field are lowercased.
for rev in &agg.reverse_element_remaps {
apply_element_remap_reverse(
&mut properties,
&rev.array_chain,
&rev.source_field_segments,
&rev.target_field,
);
remove_element_field_ci(&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(
if let Some(agg) = selected_agg {
if !agg.sub_resource_rewraps.is_empty() {
sub_resource::rewrap_precomputed_sub_resource_arrays(
&mut properties,
&aliases.sub_resource_arrays,
&aliases.entries,
api_version,
&agg.sub_resource_rewraps,
);
}
}

// 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);
if is_data_plane {
// Data-plane resources have no "properties" wrapper; merge
// directly into the top-level result.
for (k, v) in properties {
existing.entry(Value::String(k)).or_insert(v);
result.entry(k).or_insert(v);
}
} else {
obj_insert(&mut result, "properties", make_value(properties));
let props_key = Value::from("properties");
if let Some(Value::Object(existing_rc)) = result.get_mut(&props_key) {
let existing = Rc::make_mut(existing_rc);
for (k, v) in properties {
existing.entry(k).or_insert(v);
}
} else {
obj_insert(&mut result, "properties", make_value(properties));
}
}
}

Expand Down
Loading
Loading