jarq 0.7.0

An interactive jq-like JSON query tool with a TUI
Documentation
//! Object operations: keys, values, to_entries, from_entries

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

use super::{type_error, type_name};
use crate::error::EvalError;

pub fn eval_keys(value: &Value) -> Result<Value, EvalError> {
    match value {
        Value::Object(obj) => {
            let mut keys: Vec<&String> = obj.keys().collect();
            keys.sort(); // jq returns sorted keys
            let keys: Vec<Value> = keys.into_iter().map(|k| Value::String(k.clone())).collect();
            Ok(Value::Array(Box::new(keys)))
        }
        Value::Array(arr) => {
            let indices: Vec<Value> = (0..arr.len())
                .map(|i| Value::Static(StaticNode::U64(i as u64)))
                .collect();
            Ok(Value::Array(Box::new(indices)))
        }
        _ => Err(type_error(format!("{} has no keys", type_name(value)))),
    }
}

pub fn eval_values(value: &Value) -> Result<Value, EvalError> {
    match value {
        Value::Object(obj) => {
            let vals: Vec<Value> = obj.values().cloned().collect();
            Ok(Value::Array(Box::new(vals)))
        }
        Value::Array(arr) => Ok(Value::Array(arr.clone())),
        _ => Err(type_error(format!("{} has no values", type_name(value)))),
    }
}

/// Convert object to array of {key, value} entries
/// {"a": 1, "b": 2} -> [{"key": "a", "value": 1}, {"key": "b", "value": 2}]
pub fn eval_to_entries(value: &Value) -> Result<Value, EvalError> {
    match value {
        Value::Object(obj) => {
            let mut 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();
            // Sort by key for consistent output (jq does this)
            entries.sort_by(|a, b| {
                let ka = a.get("key").and_then(|k| k.as_str()).unwrap_or("");
                let kb = b.get("key").and_then(|k| k.as_str()).unwrap_or("");
                ka.cmp(kb)
            });
            Ok(Value::Array(Box::new(entries)))
        }
        _ => Err(type_error(format!("{} has no keys", type_name(value)))),
    }
}

/// Convert array of {key/name/k, value/v} entries to object
/// [{"key": "a", "value": 1}] -> {"a": 1}
/// Also accepts "name" for key and "v" for value (jq compatibility)
pub fn eval_from_entries(value: &Value) -> Result<Value, EvalError> {
    match value {
        Value::Array(arr) => {
            let mut obj = simd_json::owned::Object::new();
            for entry in arr.iter() {
                // Extract key: try "key", "k", "name" in that order
                let key = entry
                    .get("key")
                    .or_else(|| entry.get("k"))
                    .or_else(|| entry.get("name"))
                    .and_then(|k| k.as_str())
                    .ok_or_else(|| type_error("Cannot use null as object key".to_string()))?;

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

                obj.insert(key.to_string(), val);
            }
            Ok(Value::Object(Box::new(obj)))
        }
        _ => Err(type_error(format!(
            "{} cannot be converted to object",
            type_name(value)
        ))),
    }
}

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

    #[test]
    fn test_keys_object() {
        let result = eval(&Builtin::Keys, &json!({"b": 1, "a": 2, "c": 3})).unwrap();
        assert_eq!(result, vec![json!(["a", "b", "c"])]); // sorted
    }

    #[test]
    fn test_keys_array() {
        assert_eq!(
            eval(&Builtin::Keys, &json!([10, 20, 30])).unwrap(),
            vec![json!([0, 1, 2])]
        );
    }

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

    #[test]
    fn test_values_object() {
        let result = eval(&Builtin::Values, &json!({"a": 1, "b": 2})).unwrap();
        assert_eq!(result.len(), 1);
        let arr = result[0].as_array().unwrap();
        assert!(arr.contains(&json!(1)));
        assert!(arr.contains(&json!(2)));
    }

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

    #[test]
    fn test_keys_non_container_errors() {
        assert!(eval(&Builtin::Keys, &json!("hello")).is_err());
        assert!(eval(&Builtin::Keys, &json!(42)).is_err());
        assert!(eval(&Builtin::Keys, &json!(null)).is_err());
    }

    // to_entries tests
    #[test]
    fn test_to_entries_basic() {
        let result = eval(&Builtin::ToEntries, &json!({"a": 1, "b": 2})).unwrap();
        assert_eq!(result.len(), 1);
        let arr = result[0].as_array().unwrap();
        assert_eq!(arr.len(), 2);
        // Sorted by key
        assert_eq!(arr[0], json!({"key": "a", "value": 1}));
        assert_eq!(arr[1], json!({"key": "b", "value": 2}));
    }

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

    #[test]
    fn test_to_entries_non_object_errors() {
        assert!(eval(&Builtin::ToEntries, &json!([1, 2])).is_err());
        assert!(eval(&Builtin::ToEntries, &json!("string")).is_err());
    }

    // from_entries tests
    #[test]
    fn test_from_entries_basic() {
        let result = eval(
            &Builtin::FromEntries,
            &json!([{"key": "a", "value": 1}, {"key": "b", "value": 2}]),
        )
        .unwrap();
        assert_eq!(result, vec![json!({"a": 1, "b": 2})]);
    }

    #[test]
    fn test_from_entries_name_key() {
        let result = eval(&Builtin::FromEntries, &json!([{"name": "x", "value": 42}])).unwrap();
        assert_eq!(result, vec![json!({"x": 42})]);
    }

    #[test]
    fn test_from_entries_k_v_keys() {
        let result = eval(&Builtin::FromEntries, &json!([{"k": "foo", "v": "bar"}])).unwrap();
        assert_eq!(result, vec![json!({"foo": "bar"})]);
    }

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

    #[test]
    fn test_from_entries_non_array_errors() {
        assert!(eval(&Builtin::FromEntries, &json!({"a": 1})).is_err());
    }
}