jarq 0.8.2

An interactive jq-like JSON query tool with a TUI
Documentation
//! Function call evaluation (map, select, sort_by, etc.)

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

use super::compare::compare_values;
use super::eval;
use crate::error::EvalError;
use crate::filter::ast::{Filter, FunctionCall};
use crate::utils::type_name;

pub fn eval_function(func: &FunctionCall, value: &Value) -> Result<Vec<Value>, EvalError> {
    match func {
        FunctionCall::Map(inner) => eval_map(inner, value),
        FunctionCall::Select(inner) => eval_select(inner, value),
        FunctionCall::SortBy(inner) => eval_sort_by(inner, value),
        FunctionCall::GroupBy(inner) => eval_group_by(inner, value),
        FunctionCall::UniqueBy(inner) => eval_unique_by(inner, value),
        FunctionCall::MinBy(inner) => eval_min_by(inner, value),
        FunctionCall::MaxBy(inner) => eval_max_by(inner, value),
        FunctionCall::Has(key) => eval_has(key, value),
        FunctionCall::Split(delim) => eval_split(delim, value),
        FunctionCall::Join(sep) => eval_join(sep, value),
        FunctionCall::StartsWith(prefix) => eval_startswith(prefix, value),
        FunctionCall::EndsWith(suffix) => eval_endswith(suffix, value),
        FunctionCall::Contains(inner) => eval_contains(inner, value),
        FunctionCall::WithEntries(inner) => eval_with_entries(inner, value),
    }
}

fn eval_has(key: &str, value: &Value) -> Result<Vec<Value>, EvalError> {
    match value {
        Value::Object(map) => Ok(vec![Value::Static(StaticNode::Bool(map.contains_key(key)))]),
        Value::Array(arr) => {
            // has(n) for arrays checks if index exists
            if let Ok(idx) = key.parse::<usize>() {
                Ok(vec![Value::Static(StaticNode::Bool(idx < arr.len()))])
            } else {
                Ok(vec![Value::Static(StaticNode::Bool(false))])
            }
        }
        _ => Ok(vec![Value::Static(StaticNode::Bool(false))]),
    }
}

fn eval_split(delim: &str, value: &Value) -> Result<Vec<Value>, EvalError> {
    match value {
        Value::String(s) => {
            // jq special cases
            if s.is_empty() {
                // Empty string split returns empty array
                return Ok(vec![Value::Array(Box::default())]);
            }
            if delim.is_empty() {
                // Empty delimiter splits into characters
                let parts: Vec<Value> = s.chars().map(|c| Value::String(c.to_string())).collect();
                return Ok(vec![Value::Array(Box::new(parts))]);
            }
            let parts: Vec<Value> = s
                .split(delim)
                .map(|p| Value::String(p.to_string()))
                .collect();
            Ok(vec![Value::Array(Box::new(parts))])
        }
        _ => Err(EvalError::TypeError {
            message: format!("split requires string, got {}", type_name(value)),
            position: 0,
        }),
    }
}

fn eval_join(sep: &str, value: &Value) -> Result<Vec<Value>, EvalError> {
    match value {
        Value::Array(arr) => {
            let mut strings = Vec::new();
            for v in arr.iter() {
                match v {
                    Value::String(s) => strings.push(s.clone()),
                    Value::Static(StaticNode::Null) => strings.push(String::new()), // jq treats null as empty string
                    _ => {
                        return Err(EvalError::TypeError {
                            message: format!(
                                "join requires array of strings, got {} in array",
                                type_name(v)
                            ),
                            position: 0,
                        });
                    }
                }
            }
            Ok(vec![Value::String(strings.join(sep))])
        }
        _ => Err(EvalError::TypeError {
            message: format!("join requires array, got {}", type_name(value)),
            position: 0,
        }),
    }
}

fn eval_startswith(prefix: &str, value: &Value) -> Result<Vec<Value>, EvalError> {
    match value {
        Value::String(s) => Ok(vec![Value::Static(StaticNode::Bool(s.starts_with(prefix)))]),
        _ => Err(EvalError::TypeError {
            message: format!("startswith requires string, got {}", type_name(value)),
            position: 0,
        }),
    }
}

fn eval_endswith(suffix: &str, value: &Value) -> Result<Vec<Value>, EvalError> {
    match value {
        Value::String(s) => Ok(vec![Value::Static(StaticNode::Bool(s.ends_with(suffix)))]),
        _ => Err(EvalError::TypeError {
            message: format!("endswith requires string, got {}", type_name(value)),
            position: 0,
        }),
    }
}

fn eval_contains(inner: &Filter, value: &Value) -> Result<Vec<Value>, EvalError> {
    let needle_results = eval(inner, value)?;
    let needle = needle_results
        .into_iter()
        .next()
        .unwrap_or(Value::Static(StaticNode::Null));

    Ok(vec![Value::Static(StaticNode::Bool(value_contains(
        value, &needle,
    )))])
}

/// Recursive contains check following jq semantics:
/// - strings: b is substring of a
/// - arrays: all elements of b are contained in some element of a
/// - objects: all key-value pairs of b have matching keys in a with contained values
/// - scalars: equality
fn value_contains(a: &Value, b: &Value) -> bool {
    match (a, b) {
        (Value::String(haystack), Value::String(needle)) => haystack.contains(needle.as_str()),
        (Value::Array(arr), Value::Array(needle_arr)) => {
            // All elements of needle must be contained in some element of arr
            needle_arr
                .iter()
                .all(|needle_elem| arr.iter().any(|elem| value_contains(elem, needle_elem)))
        }
        (Value::Object(obj), Value::Object(needle_obj)) => needle_obj
            .iter()
            .all(|(k, v)| obj.get(k).map(|ov| value_contains(ov, v)).unwrap_or(false)),
        _ => compare_values(a, b) == Some(std::cmp::Ordering::Equal),
    }
}

/// with_entries(f) is equivalent to: to_entries | map(f) | from_entries
fn eval_with_entries(inner: &Filter, value: &Value) -> Result<Vec<Value>, EvalError> {
    match value {
        Value::Object(obj) => {
            // Convert to entries
            let entries: Vec<Value> = obj
                .iter()
                .map(|(k, v)| {
                    let mut entry = simd_json::owned::Object::new();
                    entry.insert("key".to_string(), Value::String(k.clone()));
                    entry.insert("value".to_string(), v.clone());
                    Value::Object(Box::new(entry))
                })
                .collect();

            // Apply filter to each entry
            let mut transformed = Vec::new();
            for entry in entries {
                transformed.extend(eval(inner, &entry)?);
            }

            // Convert back to object
            let mut result = simd_json::owned::Object::new();
            for entry in transformed {
                // Extract key: try "key", "k", "name"
                let key = entry
                    .get("key")
                    .or_else(|| entry.get("k"))
                    .or_else(|| entry.get("name"))
                    .and_then(|k| k.as_str())
                    .ok_or_else(|| EvalError::TypeError {
                        message: "Cannot use null as object key".to_string(),
                        position: 0,
                    })?;

                // Extract value: try "value", "v" (default to null)
                let val = entry
                    .get("value")
                    .or_else(|| entry.get("v"))
                    .cloned()
                    .unwrap_or(Value::Static(StaticNode::Null));

                result.insert(key.to_string(), val);
            }

            Ok(vec![Value::Object(Box::new(result))])
        }
        _ => Err(EvalError::TypeError {
            message: format!("with_entries requires object, got {}", type_name(value)),
            position: 0,
        }),
    }
}

fn eval_map(inner: &Filter, value: &Value) -> Result<Vec<Value>, EvalError> {
    match value {
        Value::Array(arr) => {
            let mut results = Vec::new();
            for elem in arr.iter() {
                results.extend(eval(inner, elem)?);
            }
            Ok(vec![Value::Array(Box::new(results))])
        }
        _ => Err(EvalError::TypeError {
            message: format!("map requires array, got {}", type_name(value)),
            position: 0,
        }),
    }
}

fn eval_select(inner: &Filter, value: &Value) -> Result<Vec<Value>, EvalError> {
    let results = eval(inner, value)?;
    // In jq, select keeps values where the condition is truthy
    // Falsy values are: false and null
    // Everything else (including 0, "", [], {}) is truthy
    let is_truthy = results.iter().any(|v| {
        !matches!(
            v,
            Value::Static(StaticNode::Null) | Value::Static(StaticNode::Bool(false))
        )
    });

    if is_truthy {
        Ok(vec![value.clone()])
    } else {
        Ok(vec![]) // No output
    }
}

fn eval_sort_by(inner: &Filter, value: &Value) -> Result<Vec<Value>, EvalError> {
    match value {
        Value::Array(arr) => {
            // Extract keys and pair with elements
            let mut keyed: Vec<(Value, Value)> = arr
                .iter()
                .map(|elem| {
                    let key = eval(inner, elem)?
                        .into_iter()
                        .next()
                        .unwrap_or(Value::Static(StaticNode::Null));
                    Ok((key, elem.clone()))
                })
                .collect::<Result<Vec<_>, EvalError>>()?;

            // Sort by key using existing compare_values (stable sort)
            keyed.sort_by(|(a, _), (b, _)| {
                compare_values(a, b).unwrap_or(std::cmp::Ordering::Equal)
            });

            let sorted: Vec<Value> = keyed.into_iter().map(|(_, v)| v).collect();
            Ok(vec![Value::Array(Box::new(sorted))])
        }
        _ => Err(EvalError::TypeError {
            message: format!("sort_by requires array, got {}", type_name(value)),
            position: 0,
        }),
    }
}

fn eval_group_by(inner: &Filter, value: &Value) -> Result<Vec<Value>, EvalError> {
    match value {
        Value::Array(arr) => {
            // Extract keys and group elements
            let mut groups: Vec<(Value, Vec<Value>)> = vec![];

            for elem in arr.iter() {
                let key = eval(inner, elem)?
                    .into_iter()
                    .next()
                    .unwrap_or(Value::Static(StaticNode::Null));

                // Find existing group with same key
                if let Some((_, group)) = groups
                    .iter_mut()
                    .find(|(k, _)| compare_values(k, &key) == Some(std::cmp::Ordering::Equal))
                {
                    group.push(elem.clone());
                } else {
                    groups.push((key, vec![elem.clone()]));
                }
            }

            // Sort groups by key (jq behavior)
            groups.sort_by(|(a, _), (b, _)| {
                compare_values(a, b).unwrap_or(std::cmp::Ordering::Equal)
            });

            // Return array of arrays
            let result: Vec<Value> = groups
                .into_iter()
                .map(|(_, group)| Value::Array(Box::new(group)))
                .collect();
            Ok(vec![Value::Array(Box::new(result))])
        }
        _ => Err(EvalError::TypeError {
            message: format!("group_by requires array, got {}", type_name(value)),
            position: 0,
        }),
    }
}

fn eval_unique_by(inner: &Filter, value: &Value) -> Result<Vec<Value>, EvalError> {
    match value {
        Value::Array(arr) => {
            let mut seen: Vec<Value> = vec![];
            let mut result: Vec<Value> = vec![];

            for elem in arr.iter() {
                let key = eval(inner, elem)?
                    .into_iter()
                    .next()
                    .unwrap_or(Value::Static(StaticNode::Null));

                // Check if we've seen this key
                let is_new = !seen
                    .iter()
                    .any(|k| compare_values(k, &key) == Some(std::cmp::Ordering::Equal));

                if is_new {
                    seen.push(key);
                    result.push(elem.clone());
                }
            }

            Ok(vec![Value::Array(Box::new(result))])
        }
        _ => Err(EvalError::TypeError {
            message: format!("unique_by requires array, got {}", type_name(value)),
            position: 0,
        }),
    }
}

fn eval_min_by(inner: &Filter, value: &Value) -> Result<Vec<Value>, EvalError> {
    eval_extremum_by(inner, value, true)
}

fn eval_max_by(inner: &Filter, value: &Value) -> Result<Vec<Value>, EvalError> {
    eval_extremum_by(inner, value, false)
}

fn eval_extremum_by(inner: &Filter, value: &Value, is_min: bool) -> Result<Vec<Value>, EvalError> {
    let func_name = if is_min { "min_by" } else { "max_by" };

    match value {
        Value::Array(arr) if arr.is_empty() => Ok(vec![Value::Static(StaticNode::Null)]),
        Value::Array(arr) => {
            let mut best_elem = &arr[0];
            let mut best_key = eval(inner, best_elem)?
                .into_iter()
                .next()
                .unwrap_or(Value::Static(StaticNode::Null));

            for elem in arr.iter().skip(1) {
                let key = eval(inner, elem)?
                    .into_iter()
                    .next()
                    .unwrap_or(Value::Static(StaticNode::Null));

                let cmp = compare_values(&key, &best_key);
                let should_replace = if is_min {
                    cmp == Some(std::cmp::Ordering::Less)
                } else {
                    cmp == Some(std::cmp::Ordering::Greater)
                };

                if should_replace {
                    best_elem = elem;
                    best_key = key;
                }
            }

            Ok(vec![best_elem.clone()])
        }
        _ => Err(EvalError::TypeError {
            message: format!("{} requires array, got {}", func_name, type_name(value)),
            position: 0,
        }),
    }
}