jarq 0.3.1

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 serde_json::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 - use sequential below this
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 = match parser::parse(filter_text) {
        Ok(f) => f,
        Err(parse_err) => {
            // Check if any input would cause eval error on a valid prefix
            for input in inputs {
                if let Some(eval_err) = try_eval_prefix(filter_text, input) {
                    return Err(FilterError::Eval(eval_err));
                }
            }
            return Err(FilterError::Parse(parse_err));
        }
    };

    // 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>>, (usize, EvalError)> = inputs
            .par_iter()
            .enumerate()
            .map(|(idx, input)| {
                eval::eval(&filter, input).map_err(|e| (idx, e))
            })
            .collect();

        match results {
            Ok(value_vecs) => Ok(value_vecs.into_iter().flatten().collect()),
            Err((idx, mut e)) => {
                let pos = find_eval_error_position(filter_text, &inputs[idx]);
                e.set_position(pos);
                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(mut e) => {
                    let pos = find_eval_error_position(filter_text, input);
                    e.set_position(pos);
                    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))
}

/// Find filter breakpoints (positions after `.`, `]`, `|`, or at start).
/// These are positions where we can split the filter to test partial evaluation.
fn filter_breakpoints(filter_text: &str) -> Vec<usize> {
    let mut breakpoints = vec![0];
    let mut in_string = false;
    let mut prev_char = ' ';

    for (i, c) in filter_text.char_indices() {
        if c == '"' && prev_char != '\\' {
            in_string = !in_string;
        }
        if !in_string && (c == '.' || prev_char == ']') && !breakpoints.contains(&i) {
            breakpoints.push(i);
        }
        // Track pipe positions - add position right before '|'
        if !in_string && c == '|' {
            breakpoints.push(i);
        }
        prev_char = c;
    }

    // Add position after last ] if present
    if filter_text.ends_with(']') {
        breakpoints.push(filter_text.len());
    }

    breakpoints.sort();
    breakpoints.dedup();
    breakpoints
}

/// Find approximate position where eval starts to fail.
/// This helps position error highlighting correctly in the UI.
fn find_eval_error_position(filter_text: &str, input: &Value) -> usize {
    let breakpoints = filter_breakpoints(filter_text);
    let mut last_good_pos = 0;

    for &pos in &breakpoints {
        if pos == 0 || pos > filter_text.len() {
            continue;
        }
        let prefix = &filter_text[..pos];
        if let Ok(filter) = parser::parse(prefix)
            && eval::eval(&filter, input).is_ok()
        {
            last_good_pos = pos;
        }
    }

    last_good_pos
}

/// Try to find a valid prefix that eval errors.
/// This allows showing eval errors even when the full filter doesn't parse.
fn try_eval_prefix(filter_text: &str, input: &Value) -> Option<EvalError> {
    let breakpoints = filter_breakpoints(filter_text);

    // Try breakpoints from longest to shortest
    for &pos in breakpoints.iter().rev() {
        if pos == 0 || pos > filter_text.len() {
            continue;
        }
        let prefix = &filter_text[..pos];
        if let Ok(filter) = parser::parse(prefix)
            && let Err(mut e) = eval::eval(&filter, input)
        {
            let error_pos = find_eval_error_position(prefix, input);
            e.set_position(error_pos);
            return Some(e);
        }
    }
    None
}