use crate::ast::{Expr, Ref};
use crate::builtins;
use crate::languages::azure_policy::strings::case_fold;
use crate::lexer::Span;
use crate::value::Value;
use alloc::string::{String, ToString as _};
use anyhow::Result;
use core::net::IpAddr;
use ipnet::IpNet;
use super::helpers::{as_str, try_coerce_to_number};
#[expect(clippy::as_conversions, reason = "no TryFrom<f64> for i64 in std")]
const fn truncate_f64_to_i64(f: f64) -> i64 {
f as i64
}
pub(super) fn register(m: &mut builtins::BuiltinsMap<&'static str, builtins::BuiltinFcn>) {
m.insert("azure.policy.fn.split", (fn_split, 2));
m.insert("azure.policy.fn.empty", (fn_empty, 1));
m.insert("azure.policy.fn.first", (fn_first, 1));
m.insert("azure.policy.fn.last", (fn_last, 1));
m.insert("azure.policy.fn.starts_with", (fn_starts_with, 2));
m.insert("azure.policy.fn.ends_with", (fn_ends_with, 2));
m.insert("azure.policy.fn.int", (fn_int, 1));
m.insert("azure.policy.fn.string", (fn_string, 1));
m.insert("azure.policy.fn.bool", (fn_bool, 1));
m.insert("azure.policy.fn.pad_left", (fn_pad_left, 3));
m.insert(
"azure.policy.fn.ip_range_contains",
(fn_ip_range_contains, 2),
);
}
fn fn_split(_span: &Span, _params: &[Ref<Expr>], args: &[Value], _strict: bool) -> Result<Value> {
#[allow(clippy::pattern_type_mismatch)]
let [input_val, delim_val] = args
else {
return Ok(Value::Undefined);
};
let Some(input) = as_str(input_val) else {
return Ok(Value::Undefined);
};
match *delim_val {
Value::String(ref delimiter) => {
if delimiter.is_empty() {
return Ok(Value::from(alloc::vec![Value::from(input)]));
}
let parts: alloc::vec::Vec<Value> = input
.split(delimiter.as_ref())
.map(|s| Value::from(s.to_string()))
.collect();
Ok(Value::from(parts))
}
Value::Array(ref delimiters) => {
let delims: alloc::vec::Vec<&str> = delimiters
.iter()
.filter_map(|v| match *v {
Value::String(ref s) => Some(s.as_ref()),
_ => None,
})
.collect();
if delims.is_empty() {
return Ok(Value::from(alloc::vec![Value::from(input)]));
}
let mut sorted_delims = delims.clone();
sorted_delims.sort_by_key(|d| core::cmp::Reverse(d.len()));
let mut parts = alloc::vec::Vec::new();
let mut current = String::new();
let bytes = input.as_bytes();
let mut i = 0;
while i < bytes.len() {
let mut matched = false;
for &d in &sorted_delims {
if !d.is_empty() && bytes.get(i..).is_some_and(|b| b.starts_with(d.as_bytes()))
{
parts.push(Value::from(core::mem::take(&mut current)));
i = i.wrapping_add(d.len());
matched = true;
break;
}
}
if !matched {
if let Some(ch) = input.get(i..).and_then(|s| s.chars().next()) {
current.push(ch);
i = i.wrapping_add(ch.len_utf8());
} else {
i = i.wrapping_add(1);
}
}
}
parts.push(Value::from(current));
Ok(Value::from(parts))
}
_ => Ok(Value::Undefined),
}
}
fn fn_empty(_span: &Span, _params: &[Ref<Expr>], args: &[Value], _strict: bool) -> Result<Value> {
let Some(arg) = args.first() else {
return Ok(Value::Bool(true));
};
let result = match *arg {
Value::String(ref s) => s.is_empty(),
Value::Array(ref a) => a.is_empty(),
Value::Object(ref o) => o.is_empty(),
Value::Null | Value::Undefined => true,
_ => false,
};
Ok(Value::Bool(result))
}
fn fn_first(_span: &Span, _params: &[Ref<Expr>], args: &[Value], _strict: bool) -> Result<Value> {
let Some(arg) = args.first() else {
return Ok(Value::Undefined);
};
match *arg {
Value::Array(ref a) => Ok(a.first().cloned().unwrap_or(Value::Null)),
Value::String(ref s) => Ok(s
.chars()
.next()
.map_or_else(|| Value::from(""), |ch| Value::from(ch.to_string()))),
_ => Ok(Value::Undefined),
}
}
fn fn_last(_span: &Span, _params: &[Ref<Expr>], args: &[Value], _strict: bool) -> Result<Value> {
let Some(arg) = args.first() else {
return Ok(Value::Undefined);
};
match *arg {
Value::Array(ref a) => Ok(a.last().cloned().unwrap_or(Value::Null)),
Value::String(ref s) => Ok(s
.chars()
.last()
.map_or_else(|| Value::from(""), |ch| Value::from(ch.to_string()))),
_ => Ok(Value::Undefined),
}
}
fn fn_starts_with(
_span: &Span,
_params: &[Ref<Expr>],
args: &[Value],
_strict: bool,
) -> Result<Value> {
#[allow(clippy::pattern_type_mismatch)]
let [hay_val, needle_val] = args
else {
return Ok(Value::Bool(false));
};
let (Some(haystack), Some(needle)) = (as_str(hay_val), as_str(needle_val)) else {
return Ok(Value::Bool(false));
};
let fh = case_fold::fold(haystack);
let fn_ = case_fold::fold(needle);
Ok(Value::Bool(fh.starts_with(&*fn_)))
}
fn fn_ends_with(
_span: &Span,
_params: &[Ref<Expr>],
args: &[Value],
_strict: bool,
) -> Result<Value> {
#[allow(clippy::pattern_type_mismatch)]
let [hay_val, needle_val] = args
else {
return Ok(Value::Bool(false));
};
let (Some(haystack), Some(needle)) = (as_str(hay_val), as_str(needle_val)) else {
return Ok(Value::Bool(false));
};
let fh = case_fold::fold(haystack);
let fn_ = case_fold::fold(needle);
Ok(Value::Bool(fh.ends_with(&*fn_)))
}
fn fn_int(_span: &Span, _params: &[Ref<Expr>], args: &[Value], _strict: bool) -> Result<Value> {
let Some(arg) = args.first() else {
return Ok(Value::Undefined);
};
match *arg {
Value::Number(ref n) => {
n.as_i64()
.map(Value::from)
.or_else(|| n.as_f64().map(|f| Value::from(truncate_f64_to_i64(f))))
.map_or(Ok(Value::Undefined), Ok)
}
Value::String(ref s) => try_coerce_to_number(s).map_or(Ok(Value::Undefined), |n| {
n.as_i64()
.map(Value::from)
.or_else(|| n.as_f64().map(|f| Value::from(truncate_f64_to_i64(f))))
.map_or(Ok(Value::Undefined), Ok)
}),
_ => Ok(Value::Undefined),
}
}
fn fn_string(_span: &Span, _params: &[Ref<Expr>], args: &[Value], _strict: bool) -> Result<Value> {
let Some(arg) = args.first() else {
return Ok(Value::Undefined);
};
match *arg {
Value::String(_) => Ok(arg.clone()),
Value::Bool(b) => Ok(Value::from(b.to_string())),
Value::Number(ref n) => Ok(Value::from(n.format_decimal())),
Value::Null => Ok(Value::from("null")),
Value::Undefined => Ok(Value::Undefined),
_ => Ok(Value::from(arg.to_string())),
}
}
fn fn_bool(_span: &Span, _params: &[Ref<Expr>], args: &[Value], _strict: bool) -> Result<Value> {
let Some(arg) = args.first() else {
return Ok(Value::Undefined);
};
match *arg {
Value::Bool(_) => Ok(arg.clone()),
Value::String(ref s) => match s.to_lowercase().as_str() {
"true" | "1" => Ok(Value::Bool(true)),
"false" | "0" => Ok(Value::Bool(false)),
_ => Ok(Value::Undefined),
},
Value::Number(ref n) => n
.as_i64()
.map(|i| Value::Bool(i != 0))
.or_else(|| n.as_f64().map(|f| Value::Bool(f != 0.0)))
.map_or(Ok(Value::Undefined), Ok),
_ => Ok(Value::Undefined),
}
}
fn fn_pad_left(
_span: &Span,
_params: &[Ref<Expr>],
args: &[Value],
_strict: bool,
) -> Result<Value> {
if args.len() < 2 || args.len() > 3 {
return Ok(Value::Undefined);
}
let Some(value) = args.first().and_then(as_str) else {
return Ok(Value::Undefined);
};
let width = args.get(1).and_then(|width_val| match *width_val {
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(truncate_f64_to_i64(x)).ok())
}),
Value::String(ref s) => try_coerce_to_number(s).and_then(|n| {
n.as_i64()
.and_then(|x| usize::try_from(x).ok())
.or_else(|| {
n.as_f64()
.and_then(|x| usize::try_from(truncate_f64_to_i64(x)).ok())
})
}),
_ => None,
});
let Some(total_width) = width else {
return Ok(Value::Undefined);
};
let pad_char = args
.get(2)
.and_then(as_str)
.and_then(|s| s.chars().next())
.unwrap_or(' ');
let value_len = value.chars().count();
if value_len >= total_width {
return Ok(Value::from(value));
}
let pad_count = total_width.saturating_sub(value_len);
let mut padded = String::with_capacity(total_width);
for _ in 0..pad_count {
padded.push(pad_char);
}
padded.push_str(value);
Ok(Value::from(padded))
}
fn fn_ip_range_contains(
_span: &Span,
_params: &[Ref<Expr>],
args: &[Value],
_strict: bool,
) -> Result<Value> {
#[allow(clippy::pattern_type_mismatch)]
let [range_val, target_val] = args
else {
return Ok(Value::Bool(false));
};
let (Some(range), Some(target)) = (as_str(range_val), as_str(target_val)) else {
return Ok(Value::Bool(false));
};
let Ok(net) = range.parse::<IpNet>() else {
return Ok(Value::Bool(false));
};
if target.contains('/') {
let Ok(target_net) = target.parse::<IpNet>() else {
return Ok(Value::Bool(false));
};
return Ok(Value::Bool(net.contains(&target_net)));
}
let Ok(target_ip) = target.parse::<IpAddr>() else {
return Ok(Value::Bool(false));
};
Ok(Value::Bool(net.contains(&target_ip)))
}