jarq 0.8.3

An interactive jq-like JSON query tool with a TUI
Documentation
//! Filter module for jq-like filter expressions.
//!
//! This module provides parsing, evaluation, and AST types for filter expressions.

mod ast;
pub mod builtins;
mod eval;
mod parser;

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

use crate::error::{EvalError, FilterError, ParseError};

/// Parse a filter expression to check syntax validity.
/// Returns Ok(()) if valid, Err with parse error if invalid.
pub fn parse_check(filter_text: &str) -> Result<(), ParseError> {
    if filter_text.trim().is_empty() {
        return Err(ParseError {
            message: "expected filter expression".to_string(),
            position: 0,
        });
    }
    parser::parse(filter_text).map(|_| ())
}

/// Threshold for parallel evaluation - arrays smaller than this use sequential iteration.
/// 10k balances rayon overhead vs parallelism gains for typical JSON processing.
pub(crate) const PARALLEL_THRESHOLD: usize = 10_000;

/// Evaluate a filter expression against multiple JSON input values.
///
/// The filter is applied to each input independently, and all results
/// are collected into a single Vec.
pub fn evaluate_all(filter_text: &str, inputs: &[Value]) -> Result<Vec<Value>, FilterError> {
    if filter_text.trim().is_empty() {
        return Err(FilterError::Parse(ParseError {
            message: "expected filter expression".to_string(),
            position: 0,
        }));
    }

    // Parse once, apply to each input
    let filter = parser::parse(filter_text)?;

    // Apply filter to each input, collect all results
    // Use parallel evaluation for large input counts
    if inputs.len() >= PARALLEL_THRESHOLD {
        let results: Result<Vec<Vec<Value>>, EvalError> = inputs
            .par_iter()
            .map(|input| eval::eval(&filter, input))
            .collect();

        match results {
            Ok(value_vecs) => Ok(value_vecs.into_iter().flatten().collect()),
            Err(e) => Err(FilterError::Eval(e)),
        }
    } else {
        let mut all_results = Vec::new();
        for input in inputs {
            match eval::eval(&filter, input) {
                Ok(values) => all_results.extend(values),
                Err(e) => {
                    // Error position is already set from filter.span during eval
                    return Err(FilterError::Eval(e));
                }
            }
        }
        Ok(all_results)
    }
}

/// Evaluate a filter expression against a single JSON input value.
///
/// Convenience wrapper around evaluate_all for single-value use cases.
#[allow(dead_code)]
pub fn evaluate(filter_text: &str, input: &Value) -> Result<Vec<Value>, FilterError> {
    evaluate_all(filter_text, std::slice::from_ref(input))
}

#[cfg(test)]
mod error_position_tests {
    use super::*;
    use simd_json::json;

    #[test]
    fn test_sort_by_error_position() {
        let filter_text = "[.[].name] | .[] | {name: .} | sort_by(.name)";
        let input = json!([{"name": "alice"}, {"name": "bob"}]);

        let result = evaluate_all(filter_text, &[input]);
        match result {
            Err(FilterError::Eval(e)) => {
                let pos = e.position();
                // sort_by(.name) starts at position 32
                println!("Error position: {}", pos);
                println!("Filter text from position: '{}'", &filter_text[pos..]);
                assert!(pos >= 31, "Expected position >= 31, got {}", pos);
            }
            Err(FilterError::Parse(e)) => {
                panic!("Unexpected parse error: {:?}", e);
            }
            Ok(_) => {
                panic!("Expected error but got success");
            }
        }
    }
}