jarq 0.8.2

An interactive jq-like JSON query tool with a TUI
Documentation
//! Built-in filter functions like length, keys, sort, etc.

mod aggregate;
mod array;
mod misc;
mod object;

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

use crate::error::EvalError;

pub(crate) fn type_error(message: String) -> EvalError {
    EvalError::TypeError {
        message,
        position: 0, // Position will be set by caller if needed
    }
}

#[derive(Debug, Clone, PartialEq)]
pub enum Builtin {
    Length,
    Keys,
    Values,
    Type,
    First,
    Last,
    Reverse,
    Sort,
    Min,
    Max,
    Unique,
    Flatten,
    Add,
    Empty,
    Not,
    ToEntries,
    FromEntries,
    // Format functions
    Csv,
    Tsv,
}

impl Builtin {
    /// Parse a builtin name, returns None if not a builtin
    pub fn from_name(name: &str) -> Option<Self> {
        match name {
            "length" => Some(Builtin::Length),
            "keys" => Some(Builtin::Keys),
            "values" => Some(Builtin::Values),
            "type" => Some(Builtin::Type),
            "first" => Some(Builtin::First),
            "last" => Some(Builtin::Last),
            "reverse" => Some(Builtin::Reverse),
            "sort" => Some(Builtin::Sort),
            "min" => Some(Builtin::Min),
            "max" => Some(Builtin::Max),
            "unique" => Some(Builtin::Unique),
            "flatten" => Some(Builtin::Flatten),
            "add" => Some(Builtin::Add),
            "empty" => Some(Builtin::Empty),
            "not" => Some(Builtin::Not),
            "to_entries" => Some(Builtin::ToEntries),
            "from_entries" => Some(Builtin::FromEntries),
            _ => None,
        }
    }
}

pub fn eval(builtin: &Builtin, value: &Value) -> Result<Vec<Value>, EvalError> {
    match builtin {
        Builtin::Length => Ok(vec![aggregate::eval_length(value)?]),
        Builtin::Keys => Ok(vec![object::eval_keys(value)?]),
        Builtin::Values => Ok(vec![object::eval_values(value)?]),
        Builtin::Type => Ok(vec![misc::eval_type(value)]),
        Builtin::First => Ok(vec![array::eval_first(value)?]),
        Builtin::Last => Ok(vec![array::eval_last(value)?]),
        Builtin::Reverse => Ok(vec![array::eval_reverse(value)?]),
        Builtin::Sort => Ok(vec![array::eval_sort(value)?]),
        Builtin::Min => Ok(vec![aggregate::eval_min(value)?]),
        Builtin::Max => Ok(vec![aggregate::eval_max(value)?]),
        Builtin::Unique => Ok(vec![array::eval_unique(value)?]),
        Builtin::Flatten => Ok(vec![array::eval_flatten(value)?]),
        Builtin::Add => Ok(vec![aggregate::eval_add(value)?]),
        Builtin::Empty => Ok(vec![]), // produces no output
        Builtin::Not => Ok(vec![misc::eval_not(value)]),
        Builtin::ToEntries => Ok(vec![object::eval_to_entries(value)?]),
        Builtin::FromEntries => Ok(vec![object::eval_from_entries(value)?]),
        Builtin::Csv => Ok(vec![misc::eval_csv(value)?]),
        Builtin::Tsv => Ok(vec![misc::eval_tsv(value)?]),
    }
}

/// Compare JSON values for sorting (jq ordering: null < false < true < numbers < strings < arrays < objects)
pub(crate) fn json_cmp(a: &Value, b: &Value) -> std::cmp::Ordering {
    use std::cmp::Ordering;

    fn type_order(v: &Value) -> u8 {
        match v {
            Value::Static(StaticNode::Null) => 0,
            Value::Static(StaticNode::Bool(false)) => 1,
            Value::Static(StaticNode::Bool(true)) => 2,
            Value::Static(StaticNode::I64(_) | StaticNode::U64(_) | StaticNode::F64(_)) => 3,
            Value::String(_) => 4,
            Value::Array(_) => 5,
            Value::Object(_) => 6,
        }
    }

    let type_cmp = type_order(a).cmp(&type_order(b));
    if type_cmp != Ordering::Equal {
        return type_cmp;
    }

    // Same type, compare values
    match (a, b) {
        (Value::Static(sa), Value::Static(sb)) => {
            // Compare numbers
            let af = match sa {
                StaticNode::I64(n) => *n as f64,
                StaticNode::U64(n) => *n as f64,
                StaticNode::F64(n) => *n,
                _ => return Ordering::Equal,
            };
            let bf = match sb {
                StaticNode::I64(n) => *n as f64,
                StaticNode::U64(n) => *n as f64,
                StaticNode::F64(n) => *n,
                _ => return Ordering::Equal,
            };
            af.partial_cmp(&bf).unwrap_or(Ordering::Equal)
        }
        (Value::String(a), Value::String(b)) => a.cmp(b),
        (Value::Array(a), Value::Array(b)) => {
            for (av, bv) in a.iter().zip(b.iter()) {
                let cmp = json_cmp(av, bv);
                if cmp != Ordering::Equal {
                    return cmp;
                }
            }
            a.len().cmp(&b.len())
        }
        (Value::Object(a), Value::Object(b)) => {
            // Compare objects by sorted keys, then by values in key order
            let mut keys_a: Vec<&String> = a.keys().collect();
            let mut keys_b: Vec<&String> = b.keys().collect();
            keys_a.sort();
            keys_b.sort();

            // First compare the key arrays
            for (ka, kb) in keys_a.iter().zip(keys_b.iter()) {
                let cmp = ka.cmp(kb);
                if cmp != Ordering::Equal {
                    return cmp;
                }
            }
            // If keys match so far, shorter object comes first
            let len_cmp = keys_a.len().cmp(&keys_b.len());
            if len_cmp != Ordering::Equal {
                return len_cmp;
            }

            // Same keys, compare values in sorted key order
            for key in keys_a {
                let cmp = json_cmp(&a[key.as_str()], &b[key.as_str()]);
                if cmp != Ordering::Equal {
                    return cmp;
                }
            }
            Ordering::Equal
        }
        _ => Ordering::Equal,
    }
}