jarq 0.8.2

An interactive jq-like JSON query tool with a TUI
Documentation
//! Miscellaneous operations: type, not, empty, format functions

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

use super::type_error;
use crate::error::EvalError;

pub fn eval_type(value: &Value) -> Value {
    let type_str = match value {
        Value::Static(StaticNode::Null) => "null",
        Value::Static(StaticNode::Bool(_)) => "boolean",
        Value::Static(StaticNode::I64(_) | StaticNode::U64(_) | StaticNode::F64(_)) => "number",
        Value::String(_) => "string",
        Value::Array(_) => "array",
        Value::Object(_) => "object",
    };
    Value::String(type_str.to_string())
}

pub fn eval_not(value: &Value) -> Value {
    // In jq, falsy values are: null and false
    let is_falsy = matches!(
        value,
        Value::Static(StaticNode::Null) | Value::Static(StaticNode::Bool(false))
    );
    Value::Static(StaticNode::Bool(is_falsy))
}

/// Convert an array to CSV format
/// Fields containing commas, quotes, or newlines are quoted
/// Double quotes within fields are escaped as ""
pub fn eval_csv(value: &Value) -> Result<Value, EvalError> {
    let arr = match value {
        Value::Array(a) => a,
        _ => {
            return Err(type_error(format!(
                "@csv requires array, got {}",
                type_name(value)
            )));
        }
    };

    let fields: Vec<String> = arr.iter().map(format_csv_field).collect();
    Ok(Value::String(fields.join(",")))
}

/// Convert an array to TSV format
/// Tabs, newlines, carriage returns, and backslashes are escaped
pub fn eval_tsv(value: &Value) -> Result<Value, EvalError> {
    let arr = match value {
        Value::Array(a) => a,
        _ => {
            return Err(type_error(format!(
                "@tsv requires array, got {}",
                type_name(value)
            )));
        }
    };

    let fields: Vec<String> = arr.iter().map(format_tsv_field).collect();
    Ok(Value::String(fields.join("\t")))
}

fn type_name(value: &Value) -> &'static str {
    match value {
        Value::Static(StaticNode::Null) => "null",
        Value::Static(StaticNode::Bool(_)) => "boolean",
        Value::Static(StaticNode::I64(_) | StaticNode::U64(_) | StaticNode::F64(_)) => "number",
        Value::String(_) => "string",
        Value::Array(_) => "array",
        Value::Object(_) => "object",
    }
}

fn format_csv_field(value: &Value) -> String {
    match value {
        // Strings are always quoted in CSV (per jq behavior)
        Value::String(s) => {
            format!("\"{}\"", s.replace('"', "\"\""))
        }
        // Non-strings are converted to string representation without quoting
        _ => value_to_string(value),
    }
}

fn format_tsv_field(value: &Value) -> String {
    let s = value_to_string(value);

    // Escape special characters per jq specification
    s.replace('\\', "\\\\")
        .replace('\t', "\\t")
        .replace('\n', "\\n")
        .replace('\r', "\\r")
}

fn value_to_string(value: &Value) -> String {
    match value {
        Value::String(s) => s.clone(),
        Value::Static(StaticNode::Null) => String::new(),
        Value::Static(StaticNode::Bool(b)) => b.to_string(),
        Value::Static(StaticNode::I64(n)) => n.to_string(),
        Value::Static(StaticNode::U64(n)) => n.to_string(),
        Value::Static(StaticNode::F64(n)) => n.to_string(),
        // For arrays and objects, use JSON representation
        Value::Array(_) | Value::Object(_) => simd_json::to_string(value).unwrap_or_default(),
    }
}

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

    // type tests
    #[test]
    fn test_type_null() {
        assert_eq!(
            eval(&Builtin::Type, &json!(null)).unwrap(),
            vec![json!("null")]
        );
    }

    #[test]
    fn test_type_boolean() {
        assert_eq!(
            eval(&Builtin::Type, &json!(true)).unwrap(),
            vec![json!("boolean")]
        );
        assert_eq!(
            eval(&Builtin::Type, &json!(false)).unwrap(),
            vec![json!("boolean")]
        );
    }

    #[test]
    fn test_type_number() {
        assert_eq!(
            eval(&Builtin::Type, &json!(42)).unwrap(),
            vec![json!("number")]
        );
        assert_eq!(
            eval(&Builtin::Type, &json!(3.15)).unwrap(),
            vec![json!("number")]
        );
    }

    #[test]
    fn test_type_string() {
        assert_eq!(
            eval(&Builtin::Type, &json!("hello")).unwrap(),
            vec![json!("string")]
        );
    }

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

    #[test]
    fn test_type_object() {
        assert_eq!(
            eval(&Builtin::Type, &json!({"a": 1})).unwrap(),
            vec![json!("object")]
        );
    }

    // empty tests
    #[test]
    fn test_empty() {
        let empty: Vec<Value> = vec![];
        assert_eq!(
            eval(&Builtin::Empty, &json!({"anything": "here"})).unwrap(),
            empty
        );
        assert_eq!(eval(&Builtin::Empty, &json!(null)).unwrap(), empty);
    }

    // not tests
    #[test]
    fn test_not_null() {
        assert_eq!(
            eval(&Builtin::Not, &json!(null)).unwrap(),
            vec![json!(true)]
        );
    }

    #[test]
    fn test_not_false() {
        assert_eq!(
            eval(&Builtin::Not, &json!(false)).unwrap(),
            vec![json!(true)]
        );
    }

    #[test]
    fn test_not_true() {
        assert_eq!(
            eval(&Builtin::Not, &json!(true)).unwrap(),
            vec![json!(false)]
        );
    }

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

    // @csv tests
    #[test]
    fn test_csv_simple() {
        // Strings are always quoted in CSV (jq behavior)
        assert_eq!(
            eval(&Builtin::Csv, &json!(["Alice", 30, "NYC"])).unwrap(),
            vec![json!("\"Alice\",30,\"NYC\"")]
        );
    }

    #[test]
    fn test_csv_with_comma() {
        assert_eq!(
            eval(&Builtin::Csv, &json!(["Hello, World"])).unwrap(),
            vec![json!("\"Hello, World\"")]
        );
    }

    #[test]
    fn test_csv_with_quotes() {
        assert_eq!(
            eval(&Builtin::Csv, &json!(["She said \"Hi\""])).unwrap(),
            vec![json!("\"She said \"\"Hi\"\"\"")]
        );
    }

    #[test]
    fn test_csv_with_newline() {
        assert_eq!(
            eval(&Builtin::Csv, &json!(["Line1\nLine2"])).unwrap(),
            vec![json!("\"Line1\nLine2\"")]
        );
    }

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

    #[test]
    fn test_csv_null_value() {
        // null becomes empty string (unquoted), strings are quoted
        assert_eq!(
            eval(&Builtin::Csv, &json!([null, "x"])).unwrap(),
            vec![json!(",\"x\"")]
        );
    }

    #[test]
    fn test_csv_non_array_error() {
        assert!(eval(&Builtin::Csv, &json!("not an array")).is_err());
    }

    // @tsv tests
    #[test]
    fn test_tsv_simple() {
        assert_eq!(
            eval(&Builtin::Tsv, &json!(["Alice", 30, "NYC"])).unwrap(),
            vec![json!("Alice\t30\tNYC")]
        );
    }

    #[test]
    fn test_tsv_with_tab() {
        assert_eq!(
            eval(&Builtin::Tsv, &json!(["a\tb"])).unwrap(),
            vec![json!("a\\tb")]
        );
    }

    #[test]
    fn test_tsv_with_newline() {
        assert_eq!(
            eval(&Builtin::Tsv, &json!(["a\nb"])).unwrap(),
            vec![json!("a\\nb")]
        );
    }

    #[test]
    fn test_tsv_with_backslash() {
        assert_eq!(
            eval(&Builtin::Tsv, &json!(["a\\b"])).unwrap(),
            vec![json!("a\\\\b")]
        );
    }

    #[test]
    fn test_tsv_non_array_error() {
        assert!(eval(&Builtin::Tsv, &json!(42)).is_err());
    }
}