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>
198 lines
7.2 KiB
Rust
198 lines
7.2 KiB
Rust
// Copyright (c) Microsoft Corporation.
|
|
// Licensed under the MIT License.
|
|
|
|
//! ARM template miscellaneous function builtins for Azure Policy expressions.
|
|
//!
|
|
//! Implements: json, join, items, indexFromEnd, tryGet, tryIndexFromEnd.
|
|
|
|
use crate::ast::{Expr, Ref};
|
|
use crate::builtins;
|
|
use crate::lexer::Span;
|
|
use crate::value::Object;
|
|
use crate::value::Value;
|
|
use crate::Rc;
|
|
|
|
use alloc::string::{String, ToString as _};
|
|
use alloc::vec::Vec;
|
|
use anyhow::Result;
|
|
|
|
use super::helpers::as_str;
|
|
|
|
pub(super) fn register(m: &mut builtins::BuiltinsMap<&'static str, builtins::BuiltinFcn>) {
|
|
m.insert("azure.policy.fn.json", (fn_json, 1));
|
|
m.insert("azure.policy.fn.join", (fn_join, 2));
|
|
m.insert("azure.policy.fn.items", (fn_items, 1));
|
|
m.insert("azure.policy.fn.index_from_end", (fn_index_from_end, 2));
|
|
m.insert("azure.policy.fn.try_get", (fn_try_get, 2));
|
|
m.insert(
|
|
"azure.policy.fn.try_index_from_end",
|
|
(fn_try_index_from_end, 2),
|
|
);
|
|
// guid() and uniqueString() are not yet implemented. They are unsupported
|
|
// during template dispatch, and the compiler will raise a compile error if
|
|
// either function is encountered.
|
|
}
|
|
|
|
// ── json ──────────────────────────────────────────────────────────────
|
|
|
|
/// `json(arg)` → parses a JSON string into a typed value.
|
|
///
|
|
/// `json('null')` returns null, `json('{"a":1}')` returns an object, etc.
|
|
fn fn_json(_span: &Span, _params: &[Ref<Expr>], args: &[Value], _strict: bool) -> Result<Value> {
|
|
let Some(s) = args.first().and_then(as_str) else {
|
|
return Ok(Value::Undefined);
|
|
};
|
|
Value::from_json_str(s).map_err(|e| anyhow::anyhow!("json(): {}", e))
|
|
}
|
|
|
|
// ── join ──────────────────────────────────────────────────────────────
|
|
|
|
/// `join(inputArray, delimiter)` → joins array elements with delimiter.
|
|
fn fn_join(_span: &Span, _params: &[Ref<Expr>], args: &[Value], _strict: bool) -> Result<Value> {
|
|
#[allow(clippy::pattern_type_mismatch)]
|
|
let [arr_val, delim_val] = args
|
|
else {
|
|
return Ok(Value::Undefined);
|
|
};
|
|
let Value::Array(ref arr) = *arr_val else {
|
|
return Ok(Value::Undefined);
|
|
};
|
|
let Some(delim) = as_str(delim_val) else {
|
|
return Ok(Value::Undefined);
|
|
};
|
|
let parts: Vec<String> = arr
|
|
.iter()
|
|
.map(|v| match *v {
|
|
Value::String(ref s) => s.to_string(),
|
|
Value::Number(ref n) => n.format_decimal(),
|
|
Value::Bool(b) => b.to_string(),
|
|
Value::Null => "null".to_string(),
|
|
_ => v.to_string(),
|
|
})
|
|
.collect();
|
|
Ok(Value::from(parts.join(delim)))
|
|
}
|
|
|
|
// ── items ─────────────────────────────────────────────────────────────
|
|
|
|
/// `items(object)` → array of `{"key": k, "value": v}` pairs.
|
|
fn fn_items(_span: &Span, _params: &[Ref<Expr>], args: &[Value], _strict: bool) -> Result<Value> {
|
|
let Some(first) = args.first() else {
|
|
return Ok(Value::Undefined);
|
|
};
|
|
let Value::Object(ref obj) = *first else {
|
|
return Ok(Value::Undefined);
|
|
};
|
|
let mut result = Vec::with_capacity(obj.len());
|
|
for (k, v) in obj.iter_sorted() {
|
|
let mut entry = Object::new();
|
|
entry.insert(Value::from("key"), k.clone());
|
|
entry.insert(Value::from("value"), v.clone());
|
|
result.push(Value::Object(Rc::new(entry)));
|
|
}
|
|
Ok(Value::Array(Rc::new(result)))
|
|
}
|
|
|
|
// ── indexFromEnd ──────────────────────────────────────────────────────
|
|
|
|
/// `indexFromEnd(sourceArray, reverseIndex)` → element at 1-based reverse index.
|
|
///
|
|
/// `indexFromEnd([a,b,c,d], 2)` returns `c` (2nd from end).
|
|
fn fn_index_from_end(
|
|
_span: &Span,
|
|
_params: &[Ref<Expr>],
|
|
args: &[Value],
|
|
_strict: bool,
|
|
) -> Result<Value> {
|
|
#[allow(clippy::pattern_type_mismatch)]
|
|
let [arr_val, idx_val] = args
|
|
else {
|
|
return Ok(Value::Undefined);
|
|
};
|
|
let Value::Array(ref arr) = *arr_val else {
|
|
return Ok(Value::Undefined);
|
|
};
|
|
let Some(rev_idx) = extract_usize(idx_val) else {
|
|
return Ok(Value::Undefined);
|
|
};
|
|
if rev_idx == 0 || rev_idx > arr.len() {
|
|
anyhow::bail!("indexFromEnd: reverse index {} out of bounds", rev_idx);
|
|
}
|
|
let pos = arr
|
|
.len()
|
|
.checked_sub(rev_idx)
|
|
.ok_or_else(|| anyhow::anyhow!("indexFromEnd: arithmetic overflow"))?;
|
|
arr.get(pos)
|
|
.cloned()
|
|
.ok_or_else(|| anyhow::anyhow!("indexFromEnd: index out of bounds"))
|
|
}
|
|
|
|
// ── tryGet ────────────────────────────────────────────────────────────
|
|
|
|
/// `tryGet(itemToTest, keyOrIndex)` → value at key/index, or null if missing.
|
|
fn fn_try_get(_span: &Span, _params: &[Ref<Expr>], args: &[Value], _strict: bool) -> Result<Value> {
|
|
#[allow(clippy::pattern_type_mismatch)]
|
|
let [item, key_or_idx] = args
|
|
else {
|
|
return Ok(Value::Undefined);
|
|
};
|
|
match *item {
|
|
Value::Object(ref obj) => {
|
|
// Property lookup by string key
|
|
Ok(obj.get(key_or_idx).cloned().unwrap_or(Value::Null))
|
|
}
|
|
Value::Array(ref arr) => {
|
|
// Index lookup
|
|
let Some(idx) = extract_usize(key_or_idx) else {
|
|
return Ok(Value::Null);
|
|
};
|
|
Ok(arr.get(idx).cloned().unwrap_or(Value::Null))
|
|
}
|
|
_ => Ok(Value::Null),
|
|
}
|
|
}
|
|
|
|
// ── tryIndexFromEnd ───────────────────────────────────────────────────
|
|
|
|
/// `tryIndexFromEnd(sourceArray, reverseIndex)` → element or null if out of bounds.
|
|
fn fn_try_index_from_end(
|
|
_span: &Span,
|
|
_params: &[Ref<Expr>],
|
|
args: &[Value],
|
|
_strict: bool,
|
|
) -> Result<Value> {
|
|
#[allow(clippy::pattern_type_mismatch)]
|
|
let [arr_val, idx_val] = args
|
|
else {
|
|
return Ok(Value::Undefined);
|
|
};
|
|
let Value::Array(ref arr) = *arr_val else {
|
|
return Ok(Value::Null);
|
|
};
|
|
let Some(rev_idx) = extract_usize(idx_val) else {
|
|
return Ok(Value::Null);
|
|
};
|
|
if rev_idx == 0 || rev_idx > arr.len() {
|
|
return Ok(Value::Null);
|
|
}
|
|
let pos = arr.len().saturating_sub(rev_idx);
|
|
Ok(arr.get(pos).cloned().unwrap_or(Value::Null))
|
|
}
|
|
|
|
// ── Helpers ───────────────────────────────────────────────────────────
|
|
|
|
fn extract_usize(v: &Value) -> Option<usize> {
|
|
match *v {
|
|
Value::Number(ref n) => n
|
|
.as_i64()
|
|
.and_then(|x| usize::try_from(x).ok())
|
|
.or_else(|| n.as_f64().and_then(|x| usize::try_from(f64_to_i64(x)).ok())),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
#[expect(clippy::as_conversions)]
|
|
const fn f64_to_i64(x: f64) -> i64 {
|
|
x as i64
|
|
}
|