mirror of
https://github.com/microsoft/regorus.git
synced 2026-08-05 02:16:11 +00:00
Builds on #57. Swap Value::Object's payload from Rc<BTreeMap<Value, Value>> to Rc<Object> and migrate all call sites to the Object API. as_object / as_object_mut keep their names but return &Object / &mut Object. The mutable accessor handles Rc::make_mut internally, so callers no longer do it themselves. Object grows into_value() and From<Object> for Value. Value's serializer now delegates to Object::serialize, dropping a duplicate non-string-key stringification path. RVM IterationState::Object is rewritten around ObjectCursor: O(log n) steps over a shared Rc<Object>, no eager pair snapshot. Snapshot independence is preserved by Rc copy-on-write; setup_next_iteration advances the cursor inline and advance() becomes a no-op for this variant. A new iteration_state_object_is_snapshot_independent_of_source test covers CoW against a mutated alias. Value::Set still wraps Rc<BTreeSet<Value>>; the matching Set abstraction and its swap ship in follow-up PRs. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
496 lines
15 KiB
Rust
496 lines
15 KiB
Rust
// 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` (an `Object`) only at
|
|
//! the output boundary via [`make_value`].
|
|
|
|
use alloc::string::String;
|
|
use alloc::vec::Vec;
|
|
|
|
use hashbrown::HashMap;
|
|
|
|
use crate::value::Object;
|
|
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);
|
|
}
|
|
|
|
/// Insert a key-value pair using a pre-allocated `Rc<str>` key.
|
|
///
|
|
/// Avoids the `Rc::from(key)` heap allocation that [`obj_insert`] performs.
|
|
pub fn obj_insert_rc(map: &mut ObjMap, key: Rc<str>, val: Value) {
|
|
map.insert(key, val);
|
|
}
|
|
|
|
/// Lowercase a string, returning an `Rc<str>`.
|
|
///
|
|
/// Both paths allocate an `Rc<str>` (header + string bytes). The fast-path
|
|
/// avoids creating an intermediate lowercased `String` when the input is
|
|
/// already all-lowercase ASCII.
|
|
pub fn rc_lowercase(s: &str) -> Rc<str> {
|
|
if s.bytes().all(|b| !b.is_ascii_uppercase()) {
|
|
Rc::from(s)
|
|
} else {
|
|
Rc::from(s.to_ascii_lowercase())
|
|
}
|
|
}
|
|
|
|
/// Insert a key-value pair with the key lowercased, using [`rc_lowercase`]
|
|
/// for the allocation fast-path.
|
|
pub fn obj_insert_lc(map: &mut ObjMap, key: &str, val: Value) {
|
|
let lc = rc_lowercase(key);
|
|
map.insert(lc, 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
|
|
/// an `Object` to match the `Value::Object` representation.
|
|
pub fn make_value(map: ObjMap) -> Value {
|
|
let obj: Object = map
|
|
.into_iter()
|
|
.map(|(k, v)| (Value::String(k), v))
|
|
.collect();
|
|
Value::Object(Rc::new(obj))
|
|
}
|
|
|
|
/// 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` (Object) into an [`ObjMap`].
|
|
///
|
|
/// Non-string keys are silently skipped.
|
|
#[allow(dead_code)]
|
|
pub fn value_to_obj_map(value: &Value) -> Option<ObjMap> {
|
|
let obj = value.as_object().ok()?;
|
|
let mut map = ObjMap::with_capacity(obj.len());
|
|
for (k, v) in obj.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_lc(result, seg, 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: Rc<str> = if lowercase {
|
|
rc_lowercase(first)
|
|
} else {
|
|
Rc::from(first)
|
|
};
|
|
obj_insert_rc(obj, key, value);
|
|
return;
|
|
}
|
|
|
|
let seg: Rc<str> = if lowercase {
|
|
rc_lowercase(first)
|
|
} else {
|
|
Rc::from(first)
|
|
};
|
|
|
|
// Ensure an intermediate object exists at `seg`.
|
|
if !obj.contains_key(&*seg) {
|
|
obj_insert_rc(obj, Rc::clone(&seg), make_value(new_map()));
|
|
}
|
|
|
|
// Descend directly into the BTreeMap, avoiding ObjMap round-trip.
|
|
if let Some(Value::Object(inner_rc)) = obj.get_mut(&*seg) {
|
|
let inner_btree = Rc::make_mut(inner_rc);
|
|
set_nested(
|
|
inner_btree,
|
|
segments.get(1..).unwrap_or_default(),
|
|
value,
|
|
lowercase,
|
|
);
|
|
}
|
|
}
|
|
|
|
/// Set a value at a path directly in an `Object`, 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(obj: &mut Object, segments: &[&str], value: Value, lowercase: bool) {
|
|
let Some(&first) = segments.first() else {
|
|
return;
|
|
};
|
|
|
|
let key_rc: Rc<str> = if lowercase {
|
|
rc_lowercase(first)
|
|
} else {
|
|
Rc::from(first)
|
|
};
|
|
let key_val = Value::String(Rc::clone(&key_rc));
|
|
|
|
if segments.len() == 1 {
|
|
obj.insert(key_val, value);
|
|
return;
|
|
}
|
|
|
|
// Ensure an intermediate object exists.
|
|
if !obj.contains_key(&key_val) {
|
|
obj.insert(key_val.clone(), make_value(new_map()));
|
|
}
|
|
|
|
if let Some(Value::Object(inner_rc)) = obj.get_mut(&key_val) {
|
|
let inner = Rc::make_mut(inner_rc);
|
|
set_nested(
|
|
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",
|
|
];
|
|
|
|
const PROPERTIES_DOT: &[u8] = b"properties.";
|
|
|
|
/// 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.len() > PROPERTIES_DOT.len()
|
|
&& default_path
|
|
.as_bytes()
|
|
.get(..PROPERTIES_DOT.len())
|
|
.is_some_and(|prefix| {
|
|
prefix
|
|
.iter()
|
|
.zip(PROPERTIES_DOT)
|
|
.all(|(a, b)| a.to_ascii_lowercase() == *b)
|
|
})
|
|
}
|
|
|
|
/// 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_obj(inner_btree, array_chain, depth.saturating_add(1), field);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Object-native recursion for element-level field removal.
|
|
fn remove_field_at_depth_obj(
|
|
obj: &mut Object,
|
|
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(&Value::from(seg));
|
|
}
|
|
} else if segments.len() > 1 {
|
|
remove_at_dotted_path_obj(obj, &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 obj.get_mut(&key_val) {
|
|
Some(v) => v,
|
|
None => return,
|
|
}
|
|
} else {
|
|
let mut cur: &mut Value = match obj.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_obj(inner_btree, array_chain, depth.saturating_add(1), field);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Remove the leaf segment at a dotted path directly in an Object.
|
|
fn remove_at_dotted_path_obj(obj: &mut Object, segments: &[&str]) {
|
|
let Some((&leaf, parent_segs)) = segments.split_last() else {
|
|
return;
|
|
};
|
|
if parent_segs.is_empty() {
|
|
obj.remove(&Value::from(leaf));
|
|
return;
|
|
}
|
|
|
|
let Some(&first) = parent_segs.first() else {
|
|
return;
|
|
};
|
|
let first_key = Value::from(first);
|
|
let parent_val = match obj.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));
|
|
}
|
|
}
|
|
}
|