use crate::ast::{Expr, Ref};
use crate::builtins;
use crate::lexer::Span;
use crate::value::Value;
use crate::Rc;
use alloc::collections::BTreeMap;
use alloc::vec::Vec;
use anyhow::Result;
use super::helpers::is_undefined;
pub(super) fn register(m: &mut builtins::BuiltinsMap<&'static str, builtins::BuiltinFcn>) {
m.insert(
"azure.policy.fn.intersection",
(fn_intersection, super::MAX_VARIADIC_ARGS),
);
m.insert(
"azure.policy.fn.union",
(fn_union, super::MAX_VARIADIC_ARGS),
);
m.insert("azure.policy.fn.take", (fn_take, 2));
m.insert("azure.policy.fn.skip", (fn_skip, 2));
m.insert("azure.policy.fn.range", (fn_range, 2));
m.insert("azure.policy.fn.array", (fn_array, 1));
m.insert(
"azure.policy.fn.coalesce",
(fn_coalesce, super::MAX_VARIADIC_ARGS),
);
m.insert(
"azure.policy.fn.create_object",
(fn_create_object, super::MAX_VARIADIC_ARGS),
);
}
fn fn_intersection(
_span: &Span,
_params: &[Ref<Expr>],
args: &[Value],
_strict: bool,
) -> Result<Value> {
let Some(first) = args.first() else {
return Ok(Value::Undefined);
};
let rest = args.get(1..).unwrap_or_default();
match *first {
Value::Array(ref first) => {
let mut result: Vec<Value> = first.as_ref().clone();
for arg in rest {
let Value::Array(ref other) = *arg else {
return Ok(Value::Undefined);
};
result.retain(|item| other.contains(item));
}
Ok(Value::from(result))
}
Value::Object(ref first) => {
let mut result: BTreeMap<Value, Value> = first.as_ref().clone();
for arg in rest {
let Value::Object(ref other) = *arg else {
return Ok(Value::Undefined);
};
result.retain(|k, v| other.get(k).is_some_and(|ov| *ov == *v));
}
Ok(Value::Object(Rc::new(result)))
}
_ => Ok(Value::Undefined),
}
}
fn fn_union(_span: &Span, _params: &[Ref<Expr>], args: &[Value], _strict: bool) -> Result<Value> {
let Some(first) = args.first() else {
return Ok(Value::Undefined);
};
match *first {
Value::Array(_) => {
let mut seen = alloc::collections::BTreeSet::<&Value>::new();
let mut result = Vec::new();
for arg in args {
let Value::Array(ref arr) = *arg else {
return Ok(Value::Undefined);
};
for item in arr.iter() {
if seen.insert(item) {
result.push(item.clone());
}
}
}
Ok(Value::from(result))
}
Value::Object(_) => {
let mut result = BTreeMap::<Value, Value>::new();
for arg in args {
let Value::Object(ref obj) = *arg else {
return Ok(Value::Undefined);
};
for (k, v) in obj.iter() {
#[allow(clippy::needless_borrowed_reference)]
let merged = match (result.get(k), v) {
(Some(&Value::Object(ref prev)), &Value::Object(ref next)) => {
merge_objects(prev, next)
}
_ => v.clone(),
};
result.insert(k.clone(), merged);
}
}
Ok(Value::Object(Rc::new(result)))
}
_ => Ok(Value::Undefined),
}
}
fn fn_take(_span: &Span, _params: &[Ref<Expr>], args: &[Value], _strict: bool) -> Result<Value> {
#[allow(clippy::pattern_type_mismatch)]
let [original, count_val] = args
else {
return Ok(Value::Undefined);
};
let count = extract_usize(count_val).unwrap_or(0);
match *original {
Value::Array(ref arr) => {
let n = count.min(arr.len());
Ok(Value::from(arr.get(..n).unwrap_or_default().to_vec()))
}
Value::String(ref s) => {
let taken: alloc::string::String = s.chars().take(count).collect();
Ok(Value::from(taken))
}
_ => Ok(Value::Undefined),
}
}
fn fn_skip(_span: &Span, _params: &[Ref<Expr>], args: &[Value], _strict: bool) -> Result<Value> {
#[allow(clippy::pattern_type_mismatch)]
let [original, count_val] = args
else {
return Ok(Value::Undefined);
};
let count = extract_usize(count_val).unwrap_or(0);
match *original {
Value::Array(ref arr) => {
let n = count.min(arr.len());
Ok(Value::from(arr.get(n..).unwrap_or_default().to_vec()))
}
Value::String(ref s) => {
let skipped: alloc::string::String = s.chars().skip(count).collect();
Ok(Value::from(skipped))
}
_ => Ok(Value::Undefined),
}
}
fn fn_range(_span: &Span, _params: &[Ref<Expr>], args: &[Value], _strict: bool) -> Result<Value> {
#[allow(clippy::pattern_type_mismatch)]
let [start_val, count_val] = args
else {
return Ok(Value::Undefined);
};
let (Some(start), Some(count)) = (extract_i64(start_val), extract_i64(count_val)) else {
return Ok(Value::Undefined);
};
if count < 0 {
return Ok(Value::Undefined);
}
if count > 10_000 {
anyhow::bail!("range: count ({count}) exceeds maximum of 10000");
}
let end = start
.checked_add(count)
.ok_or_else(|| anyhow::anyhow!("range overflow"))?;
if end > 2_147_483_647 {
anyhow::bail!("range: startIndex + count ({end}) exceeds maximum of 2147483647");
}
let mut result = Vec::with_capacity(usize::try_from(count).unwrap_or(0));
for i in 0..count {
let val = start
.checked_add(i)
.ok_or_else(|| anyhow::anyhow!("range overflow"))?;
result.push(Value::from(val));
}
Ok(Value::from(result))
}
fn fn_array(_span: &Span, _params: &[Ref<Expr>], args: &[Value], _strict: bool) -> Result<Value> {
let Some(arg) = args.first() else {
return Ok(Value::from(Vec::<Value>::new()));
};
match *arg {
Value::Array(_) => Ok(arg.clone()),
_ => Ok(Value::from(alloc::vec![arg.clone()])),
}
}
fn fn_coalesce(
_span: &Span,
_params: &[Ref<Expr>],
args: &[Value],
_strict: bool,
) -> Result<Value> {
for arg in args {
if !is_undefined(arg) && !matches!(arg, Value::Null) {
return Ok(arg.clone());
}
}
Ok(Value::Null)
}
fn fn_create_object(
_span: &Span,
_params: &[Ref<Expr>],
args: &[Value],
_strict: bool,
) -> Result<Value> {
if !args.len().is_multiple_of(2) {
anyhow::bail!(
"createObject: expected an even number of arguments (key-value pairs), \
but received {}",
args.len()
);
}
let mut map = BTreeMap::<Value, Value>::new();
for pair in args.chunks(2) {
#[allow(clippy::pattern_type_mismatch)]
if let [key, value] = pair {
map.insert(key.clone(), value.clone());
}
}
Ok(Value::Object(Rc::new(map)))
}
fn merge_objects(base: &BTreeMap<Value, Value>, overlay: &BTreeMap<Value, Value>) -> Value {
let mut result = base.clone();
for (k, v) in overlay {
#[allow(clippy::needless_borrowed_reference)]
let merged = match (result.get(k), v) {
(Some(&Value::Object(ref prev)), &Value::Object(ref next)) => merge_objects(prev, next),
_ => v.clone(),
};
result.insert(k.clone(), merged);
}
Value::Object(Rc::new(result))
}
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_as_i64(x)).ok())),
_ => None,
}
}
fn extract_i64(v: &Value) -> Option<i64> {
match *v {
Value::Number(ref n) => n.as_i64().or_else(|| n.as_f64().map(f64_as_i64)),
_ => None,
}
}
#[expect(clippy::as_conversions)]
const fn f64_as_i64(x: f64) -> i64 {
x as i64
}