jarq 0.8.3

An interactive jq-like JSON query tool with a TUI
Documentation
//! Aggregate operations: length, min, max, add

use simd_json::OwnedValue as Value;
use simd_json::StaticNode;
use simd_json::prelude::*;

use super::{json_cmp, type_error};
use crate::error::EvalError;
use crate::utils::type_name;

pub fn eval_length(value: &Value) -> Result<Value, EvalError> {
    let len = match value {
        Value::Static(StaticNode::Null) => 0,
        Value::String(s) => s.chars().count(),
        Value::Array(arr) => arr.len(),
        Value::Object(obj) => obj.len(),
        Value::Static(
            StaticNode::Bool(_) | StaticNode::I64(_) | StaticNode::U64(_) | StaticNode::F64(_),
        ) => {
            return Err(type_error(format!("{} has no length", type_name(value))));
        }
    };
    Ok(Value::Static(StaticNode::U64(len as u64)))
}

pub fn eval_min(value: &Value) -> Result<Value, EvalError> {
    match value {
        Value::Array(arr) if arr.is_empty() => Ok(Value::Static(StaticNode::Null)),
        Value::Array(arr) => Ok(arr
            .iter()
            .min_by(|a, b| json_cmp(a, b))
            .cloned()
            .unwrap_or(Value::Static(StaticNode::Null))),
        _ => Err(type_error(format!(
            "{} cannot have its minimum taken, as it is not an array",
            type_name(value)
        ))),
    }
}

pub fn eval_max(value: &Value) -> Result<Value, EvalError> {
    match value {
        Value::Array(arr) if arr.is_empty() => Ok(Value::Static(StaticNode::Null)),
        Value::Array(arr) => Ok(arr
            .iter()
            .max_by(|a, b| json_cmp(a, b))
            .cloned()
            .unwrap_or(Value::Static(StaticNode::Null))),
        _ => Err(type_error(format!(
            "{} cannot have its maximum taken, as it is not an array",
            type_name(value)
        ))),
    }
}

fn is_null(v: &Value) -> bool {
    matches!(v, Value::Static(StaticNode::Null))
}

fn is_number(v: &Value) -> bool {
    matches!(
        v,
        Value::Static(StaticNode::I64(_) | StaticNode::U64(_) | StaticNode::F64(_))
    )
}

fn value_as_f64(v: &Value) -> Option<f64> {
    match v {
        Value::Static(StaticNode::I64(n)) => Some(*n as f64),
        Value::Static(StaticNode::U64(n)) => Some(*n as f64),
        Value::Static(StaticNode::F64(n)) => Some(*n),
        _ => None,
    }
}

pub fn eval_add(value: &Value) -> Result<Value, EvalError> {
    match value {
        Value::Array(arr) if arr.is_empty() => Ok(Value::Static(StaticNode::Null)),
        Value::Array(arr) => {
            // Determine type from first non-null element
            let first_type = arr.iter().find(|v| !is_null(v));
            Ok(match first_type {
                Some(v) if is_number(v) => {
                    let sum: f64 = arr.iter().filter_map(value_as_f64).sum();
                    Value::Static(StaticNode::F64(sum))
                }
                Some(Value::String(_)) => {
                    let concat: String = arr.iter().filter_map(|v| v.as_str()).collect();
                    Value::String(concat)
                }
                Some(Value::Array(_)) => {
                    let concat: Vec<Value> = arr
                        .iter()
                        .filter_map(|v| v.as_array())
                        .flatten()
                        .cloned()
                        .collect();
                    Value::Array(Box::new(concat))
                }
                Some(Value::Object(_)) => {
                    // Merge objects: later values override earlier ones
                    let mut merged = simd_json::owned::Object::new();
                    for v in arr.iter() {
                        if let Value::Object(obj) = v {
                            for (k, val) in obj.iter() {
                                merged.insert(k.clone(), val.clone());
                            }
                        }
                    }
                    Value::Object(Box::new(merged))
                }
                _ => Value::Static(StaticNode::Null),
            })
        }
        _ => Err(type_error(format!(
            "{} cannot be added, as it is not an array",
            type_name(value)
        ))),
    }
}

#[cfg(test)]
mod tests {
    use crate::filter::builtins::{Builtin, eval};
    use simd_json::json;

    // length tests
    #[test]
    fn test_length_array() {
        assert_eq!(
            eval(&Builtin::Length, &json!([1, 2, 3])).unwrap(),
            vec![json!(3)]
        );
    }

    #[test]
    fn test_length_string() {
        assert_eq!(
            eval(&Builtin::Length, &json!("hello")).unwrap(),
            vec![json!(5)]
        );
    }

    #[test]
    fn test_length_string_unicode() {
        assert_eq!(
            eval(&Builtin::Length, &json!("日本語")).unwrap(),
            vec![json!(3)]
        );
    }

    #[test]
    fn test_length_object() {
        assert_eq!(
            eval(&Builtin::Length, &json!({"a": 1, "b": 2})).unwrap(),
            vec![json!(2)]
        );
    }

    #[test]
    fn test_length_null() {
        assert_eq!(
            eval(&Builtin::Length, &json!(null)).unwrap(),
            vec![json!(0)]
        );
    }

    #[test]
    fn test_length_empty() {
        assert_eq!(eval(&Builtin::Length, &json!([])).unwrap(), vec![json!(0)]);
        assert_eq!(eval(&Builtin::Length, &json!("")).unwrap(), vec![json!(0)]);
        assert_eq!(eval(&Builtin::Length, &json!({})).unwrap(), vec![json!(0)]);
    }

    #[test]
    fn test_length_number_errors() {
        let result = eval(&Builtin::Length, &json!(42));
        assert!(result.is_err());
        assert!(result.unwrap_err().to_string().contains("has no length"));
    }

    #[test]
    fn test_length_bool_errors() {
        let result = eval(&Builtin::Length, &json!(true));
        assert!(result.is_err());
    }

    // min/max tests
    #[test]
    fn test_min() {
        assert_eq!(
            eval(&Builtin::Min, &json!([3, 1, 2])).unwrap(),
            vec![json!(1)]
        );
    }

    #[test]
    fn test_max() {
        assert_eq!(
            eval(&Builtin::Max, &json!([3, 1, 2])).unwrap(),
            vec![json!(3)]
        );
    }

    #[test]
    fn test_min_empty() {
        assert_eq!(eval(&Builtin::Min, &json!([])).unwrap(), vec![json!(null)]);
    }

    #[test]
    fn test_max_empty() {
        assert_eq!(eval(&Builtin::Max, &json!([])).unwrap(), vec![json!(null)]);
    }

    #[test]
    fn test_min_strings() {
        assert_eq!(
            eval(&Builtin::Min, &json!(["banana", "apple", "cherry"])).unwrap(),
            vec![json!("apple")]
        );
    }

    // add tests
    #[test]
    fn test_add_numbers() {
        assert_eq!(
            eval(&Builtin::Add, &json!([1, 2, 3])).unwrap(),
            vec![json!(6.0)]
        );
    }

    #[test]
    fn test_add_strings() {
        assert_eq!(
            eval(&Builtin::Add, &json!(["a", "b", "c"])).unwrap(),
            vec![json!("abc")]
        );
    }

    #[test]
    fn test_add_arrays() {
        assert_eq!(
            eval(&Builtin::Add, &json!([[1, 2], [3, 4]])).unwrap(),
            vec![json!([1, 2, 3, 4])]
        );
    }

    #[test]
    fn test_add_empty() {
        assert_eq!(eval(&Builtin::Add, &json!([])).unwrap(), vec![json!(null)]);
    }

    #[test]
    fn test_add_non_array_errors() {
        assert!(eval(&Builtin::Add, &json!("hello")).is_err());
    }

    #[test]
    fn test_min_max_non_array_errors() {
        assert!(eval(&Builtin::Min, &json!("hello")).is_err());
        assert!(eval(&Builtin::Max, &json!(42)).is_err());
    }

    // add objects tests
    #[test]
    fn test_add_objects() {
        assert_eq!(
            eval(&Builtin::Add, &json!([{"a": 1}, {"b": 2}])).unwrap(),
            vec![json!({"a": 1, "b": 2})]
        );
    }

    #[test]
    fn test_add_objects_override() {
        // Later values override earlier ones
        assert_eq!(
            eval(&Builtin::Add, &json!([{"a": 1}, {"a": 2}])).unwrap(),
            vec![json!({"a": 2})]
        );
    }

    #[test]
    fn test_add_objects_merge_multiple() {
        assert_eq!(
            eval(&Builtin::Add, &json!([{"a": 1}, {"b": 2}, {"c": 3}])).unwrap(),
            vec![json!({"a": 1, "b": 2, "c": 3})]
        );
    }
}