use super::FilterFunction;
use crate::functions::metadata::{ArgumentMetadata, FunctionMetadata, SyntaxVariants};
use minijinja::value::Kwargs;
use minijinja::{Error, ErrorKind, Value};
use std::collections::HashSet;
const ARRAY_ARG: ArgumentMetadata = ArgumentMetadata {
name: "array",
arg_type: "array",
required: true,
default: None,
description: "The array to process",
};
fn extract_array(value: &Value, fn_name: &str) -> Result<Value, Error> {
if !matches!(value.kind(), minijinja::value::ValueKind::Seq) {
return Err(Error::new(
ErrorKind::InvalidOperation,
format!("{} requires an array", fn_name),
));
}
Ok(value.clone())
}
fn value_to_f64(item: &Value, fn_name: &str) -> Result<f64, Error> {
let json_value: serde_json::Value = serde_json::to_value(item).map_err(|e| {
Error::new(
ErrorKind::InvalidOperation,
format!("Failed to convert value: {}", e),
)
})?;
json_value.as_f64().ok_or_else(|| {
Error::new(
ErrorKind::InvalidOperation,
format!("{} requires numeric values, found: {}", fn_name, item),
)
})
}
fn format_number(num: f64) -> Value {
if num.fract() == 0.0 {
Value::from(num as i64)
} else {
Value::from(num)
}
}
pub struct ArraySum;
impl ArraySum {
fn compute(array: &Value) -> Result<Value, Error> {
let mut sum = 0.0_f64;
if let Ok(seq) = array.try_iter() {
for item in seq {
sum += value_to_f64(&item, "array_sum")?;
}
}
Ok(format_number(sum))
}
}
impl FilterFunction for ArraySum {
const NAME: &'static str = "array_sum";
const METADATA: FunctionMetadata = FunctionMetadata {
name: "array_sum",
category: "array",
description: "Calculate sum of array values",
arguments: &[ARRAY_ARG],
return_type: "number",
examples: &[
"{{ array_sum(array=numbers) }}",
"{{ [1, 2, 3, 4, 5] | array_sum }}",
],
syntax: SyntaxVariants::FUNCTION_AND_FILTER,
};
fn call_as_function(kwargs: Kwargs) -> Result<Value, Error> {
let array: Value = kwargs.get("array")?;
extract_array(&array, "array_sum")?;
Self::compute(&array)
}
fn call_as_filter(value: &Value, _kwargs: Kwargs) -> Result<Value, Error> {
extract_array(value, "array_sum")?;
Self::compute(value)
}
}
pub struct ArrayAvg;
impl ArrayAvg {
fn compute(array: &Value) -> Result<Value, Error> {
let mut sum = 0.0_f64;
let mut count = 0;
if let Ok(seq) = array.try_iter() {
for item in seq {
sum += value_to_f64(&item, "array_avg")?;
count += 1;
}
}
if count == 0 {
return Ok(Value::from(0));
}
Ok(format_number(sum / count as f64))
}
}
impl FilterFunction for ArrayAvg {
const NAME: &'static str = "array_avg";
const METADATA: FunctionMetadata = FunctionMetadata {
name: "array_avg",
category: "array",
description: "Calculate average of array values",
arguments: &[ARRAY_ARG],
return_type: "number",
examples: &["{{ array_avg(array=numbers) }}", "{{ scores | array_avg }}"],
syntax: SyntaxVariants::FUNCTION_AND_FILTER,
};
fn call_as_function(kwargs: Kwargs) -> Result<Value, Error> {
let array: Value = kwargs.get("array")?;
extract_array(&array, "array_avg")?;
Self::compute(&array)
}
fn call_as_filter(value: &Value, _kwargs: Kwargs) -> Result<Value, Error> {
extract_array(value, "array_avg")?;
Self::compute(value)
}
}
pub struct ArrayMedian;
impl ArrayMedian {
fn compute(array: &Value) -> Result<Value, Error> {
let mut numbers: Vec<f64> = Vec::new();
if let Ok(seq) = array.try_iter() {
for item in seq {
numbers.push(value_to_f64(&item, "array_median")?);
}
}
if numbers.is_empty() {
return Ok(Value::from(0));
}
numbers.sort_by(|a, b| a.partial_cmp(b).unwrap());
let len = numbers.len();
let median = if len.is_multiple_of(2) {
(numbers[len / 2 - 1] + numbers[len / 2]) / 2.0
} else {
numbers[len / 2]
};
Ok(format_number(median))
}
}
impl FilterFunction for ArrayMedian {
const NAME: &'static str = "array_median";
const METADATA: FunctionMetadata = FunctionMetadata {
name: "array_median",
category: "array",
description: "Calculate median of array values",
arguments: &[ARRAY_ARG],
return_type: "number",
examples: &[
"{{ array_median(array=numbers) }}",
"{{ [1, 3, 5, 7, 9] | array_median }}",
],
syntax: SyntaxVariants::FUNCTION_AND_FILTER,
};
fn call_as_function(kwargs: Kwargs) -> Result<Value, Error> {
let array: Value = kwargs.get("array")?;
extract_array(&array, "array_median")?;
Self::compute(&array)
}
fn call_as_filter(value: &Value, _kwargs: Kwargs) -> Result<Value, Error> {
extract_array(value, "array_median")?;
Self::compute(value)
}
}
pub struct ArrayMin;
impl ArrayMin {
fn compute(array: &Value) -> Result<Value, Error> {
let mut min_value: Option<f64> = None;
if let Ok(seq) = array.try_iter() {
for item in seq {
let num = value_to_f64(&item, "array_min")?;
min_value = Some(match min_value {
None => num,
Some(current_min) => num.min(current_min),
});
}
}
match min_value {
None => Err(Error::new(
ErrorKind::InvalidOperation,
"array_min requires a non-empty array",
)),
Some(min) => Ok(format_number(min)),
}
}
}
impl FilterFunction for ArrayMin {
const NAME: &'static str = "array_min";
const METADATA: FunctionMetadata = FunctionMetadata {
name: "array_min",
category: "array",
description: "Find minimum value in array",
arguments: &[ARRAY_ARG],
return_type: "number",
examples: &["{{ array_min(array=numbers) }}", "{{ prices | array_min }}"],
syntax: SyntaxVariants::FUNCTION_AND_FILTER,
};
fn call_as_function(kwargs: Kwargs) -> Result<Value, Error> {
let array: Value = kwargs.get("array")?;
extract_array(&array, "array_min")?;
Self::compute(&array)
}
fn call_as_filter(value: &Value, _kwargs: Kwargs) -> Result<Value, Error> {
extract_array(value, "array_min")?;
Self::compute(value)
}
}
pub struct ArrayMax;
impl ArrayMax {
fn compute(array: &Value) -> Result<Value, Error> {
let mut max_value: Option<f64> = None;
if let Ok(seq) = array.try_iter() {
for item in seq {
let num = value_to_f64(&item, "array_max")?;
max_value = Some(match max_value {
None => num,
Some(current_max) => num.max(current_max),
});
}
}
match max_value {
None => Err(Error::new(
ErrorKind::InvalidOperation,
"array_max requires a non-empty array",
)),
Some(max) => Ok(format_number(max)),
}
}
}
impl FilterFunction for ArrayMax {
const NAME: &'static str = "array_max";
const METADATA: FunctionMetadata = FunctionMetadata {
name: "array_max",
category: "array",
description: "Find maximum value in array",
arguments: &[ARRAY_ARG],
return_type: "number",
examples: &["{{ array_max(array=numbers) }}", "{{ prices | array_max }}"],
syntax: SyntaxVariants::FUNCTION_AND_FILTER,
};
fn call_as_function(kwargs: Kwargs) -> Result<Value, Error> {
let array: Value = kwargs.get("array")?;
extract_array(&array, "array_max")?;
Self::compute(&array)
}
fn call_as_filter(value: &Value, _kwargs: Kwargs) -> Result<Value, Error> {
extract_array(value, "array_max")?;
Self::compute(value)
}
}
pub struct ArrayUnique;
impl ArrayUnique {
fn compute(array: &Value) -> Result<Value, Error> {
let mut seen: HashSet<String> = HashSet::new();
let mut unique: Vec<serde_json::Value> = Vec::new();
if let Ok(seq) = array.try_iter() {
for item in seq {
let json_value: serde_json::Value = serde_json::to_value(&item).map_err(|e| {
Error::new(
ErrorKind::InvalidOperation,
format!("Failed to convert item: {}", e),
)
})?;
let item_str = serde_json::to_string(&json_value).unwrap_or_default();
if seen.insert(item_str) {
unique.push(json_value);
}
}
}
Ok(Value::from_serialize(unique))
}
}
impl FilterFunction for ArrayUnique {
const NAME: &'static str = "array_unique";
const METADATA: FunctionMetadata = FunctionMetadata {
name: "array_unique",
category: "array",
description: "Remove duplicate values from array",
arguments: &[ARRAY_ARG],
return_type: "array",
examples: &[
"{{ array_unique(array=items) }}",
"{{ [1, 2, 2, 3, 3, 3] | array_unique }}",
],
syntax: SyntaxVariants::FUNCTION_AND_FILTER,
};
fn call_as_function(kwargs: Kwargs) -> Result<Value, Error> {
let array: Value = kwargs.get("array")?;
extract_array(&array, "array_unique")?;
Self::compute(&array)
}
fn call_as_filter(value: &Value, _kwargs: Kwargs) -> Result<Value, Error> {
extract_array(value, "array_unique")?;
Self::compute(value)
}
}
pub struct ArrayFlatten;
impl ArrayFlatten {
fn compute(array: &Value) -> Result<Value, Error> {
let mut flattened: Vec<serde_json::Value> = Vec::new();
if let Ok(seq) = array.try_iter() {
for item in seq {
let json_value: serde_json::Value = serde_json::to_value(&item).map_err(|e| {
Error::new(
ErrorKind::InvalidOperation,
format!("Failed to convert item: {}", e),
)
})?;
if let Some(nested_array) = json_value.as_array() {
for nested_item in nested_array {
flattened.push(nested_item.clone());
}
} else {
flattened.push(json_value);
}
}
}
Ok(Value::from_serialize(flattened))
}
}
impl FilterFunction for ArrayFlatten {
const NAME: &'static str = "array_flatten";
const METADATA: FunctionMetadata = FunctionMetadata {
name: "array_flatten",
category: "array",
description: "Flatten nested arrays by one level",
arguments: &[ARRAY_ARG],
return_type: "array",
examples: &[
"{{ array_flatten(array=nested) }}",
"{{ [[1, 2], [3, 4]] | array_flatten }}",
],
syntax: SyntaxVariants::FUNCTION_AND_FILTER,
};
fn call_as_function(kwargs: Kwargs) -> Result<Value, Error> {
let array: Value = kwargs.get("array")?;
extract_array(&array, "array_flatten")?;
Self::compute(&array)
}
fn call_as_filter(value: &Value, _kwargs: Kwargs) -> Result<Value, Error> {
extract_array(value, "array_flatten")?;
Self::compute(value)
}
}