reddb-io-tq 0.29.8

jq-style CLI for TOON v4.1, TOONL v0.2, JSON, YAML, and XML
use reddb_io_toon::Value;

use super::ast::Expr;
use super::eval::Env;

pub(super) fn evaluate_index(
    base: &Expr,
    index: &Expr,
    input: &Value,
    env: &Env,
) -> Result<Vec<Value>, String> {
    let mut output = Vec::new();
    for value in base.eval(input, env)? {
        for key in index.eval(&value, env)? {
            output.push(index_value(&value, &key)?);
        }
    }
    Ok(output)
}

pub(super) fn evaluate_iteration(
    base: &Expr,
    input: &Value,
    env: &Env,
) -> Result<Vec<Value>, String> {
    let mut output = Vec::new();
    for value in base.eval(input, env)? {
        match value {
            Value::Array(array) => output.extend(array.values()),
            Value::Object(document) => output.extend(document.values().cloned()),
            value => return Err(format!("Cannot iterate over {}", value_kind(&value))),
        }
    }
    Ok(output)
}

pub(super) fn evaluate_slice(
    base: &Expr,
    start: Option<&Expr>,
    end: Option<&Expr>,
    input: &Value,
    env: &Env,
) -> Result<Vec<Value>, String> {
    let mut output = Vec::new();
    for value in base.eval(input, env)? {
        let starts = evaluate_bounds(start, &value, env)?;
        let ends = evaluate_bounds(end, &value, env)?;
        for start in &starts {
            for end in &ends {
                output.push(slice_value(&value, *start, *end)?);
            }
        }
    }
    Ok(output)
}

pub(super) fn index_value(input: &Value, key: &Value) -> Result<Value, String> {
    match (input, key) {
        (Value::Array(array), Value::Number(index)) => {
            let index = normalize_index(parse_number(index)?, array.len());
            Ok(index
                .and_then(|index| array.get(index))
                .unwrap_or(Value::Null))
        }
        (Value::Object(document), Value::String(key)) => {
            Ok(document.get(key).cloned().unwrap_or(Value::Null))
        }
        (Value::Object(_), Value::Number(_)) => Ok(Value::Null),
        (Value::Null, Value::Number(_) | Value::String(_)) => Ok(Value::Null),
        _ => Err(format!(
            "Cannot index {} with {}",
            value_kind(input),
            index_description(key)
        )),
    }
}

pub(super) fn evaluate_bounds(
    expression: Option<&Expr>,
    input: &Value,
    env: &Env,
) -> Result<Vec<Option<f64>>, String> {
    let Some(expression) = expression else {
        return Ok(vec![None]);
    };
    expression
        .eval(input, env)?
        .into_iter()
        .map(|value| match value {
            Value::Number(number) => parse_number(&number).map(Some),
            _ => Err("Array/string slice indices must be integers".to_owned()),
        })
        .collect()
}

pub(super) fn slice_value(
    input: &Value,
    start: Option<f64>,
    end: Option<f64>,
) -> Result<Value, String> {
    match input {
        Value::Array(array) => {
            let (start, end) = slice_bounds(array.len(), start, end);
            Ok(Value::Array(array.slice(Some(start), Some(end))))
        }
        Value::String(value) => {
            let characters = value.chars().collect::<Vec<_>>();
            let (start, end) = slice_bounds(characters.len(), start, end);
            Ok(Value::String(characters[start..end].iter().collect()))
        }
        Value::Null => Ok(Value::Null),
        value => Err(format!("Cannot index {} with object", value_kind(value))),
    }
}

fn normalize_index(index: f64, len: usize) -> Option<usize> {
    let index = index.trunc();
    let index = if index < 0.0 {
        len as f64 + index
    } else {
        index
    };
    (index >= 0.0 && index < len as f64).then_some(index as usize)
}

pub(super) fn slice_bounds(len: usize, start: Option<f64>, end: Option<f64>) -> (usize, usize) {
    let start = normalize_bound(start.unwrap_or(0.0), len);
    let end = normalize_bound(end.unwrap_or(len as f64), len).max(start);
    (start, end)
}

fn normalize_bound(bound: f64, len: usize) -> usize {
    let bound = bound.trunc();
    if bound < 0.0 {
        (len as f64 + bound).max(0.0) as usize
    } else {
        bound.min(len as f64) as usize
    }
}

fn parse_number(number: &str) -> Result<f64, String> {
    let number = number
        .parse::<f64>()
        .map_err(|_| format!("invalid array index `{number}`"))?;
    number
        .is_finite()
        .then_some(number)
        .ok_or_else(|| format!("invalid array index `{number}`"))
}

fn index_description(value: &Value) -> String {
    match value {
        Value::String(value) => format!(
            "string {}",
            serde_json::to_string(value).expect("strings always serialize")
        ),
        value => value_kind(value).to_owned(),
    }
}

pub(super) fn value_kind(value: &Value) -> &'static str {
    match value {
        Value::Array(_) => "array",
        Value::Bool(_) => "boolean",
        Value::Null => "null",
        Value::Number(_) => "number",
        Value::Object(_) => "object",
        Value::String(_) => "string",
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn json(input: &str) -> Value {
        Value::from_json_str(input).expect("valid json literal")
    }

    #[test]
    fn indexing_null_and_number_keyed_objects_yields_null() {
        assert_eq!(index_value(&Value::Null, &json("0")).unwrap(), Value::Null);
        assert_eq!(
            index_value(&Value::Null, &json("\"k\"")).unwrap(),
            Value::Null
        );
        assert_eq!(
            index_value(&json("{\"a\":1}"), &json("0")).unwrap(),
            Value::Null
        );
    }

    #[test]
    fn indexing_incompatible_kinds_names_both_sides() {
        let error = index_value(&json("true"), &json("0")).unwrap_err();
        assert_eq!(error, "Cannot index boolean with number");
        let error = index_value(&json("[1]"), &json("\"k\"")).unwrap_err();
        assert_eq!(error, "Cannot index array with string \"k\"");
        let error = index_value(&json("1"), &json("null")).unwrap_err();
        assert_eq!(error, "Cannot index number with null");
        let error = index_value(&json("\"s\""), &json("[1]")).unwrap_err();
        assert_eq!(error, "Cannot index string with array");
        let error = index_value(&json("{\"a\":1}"), &json("{}")).unwrap_err();
        assert_eq!(error, "Cannot index object with object");
    }

    #[test]
    fn slicing_null_passes_through_and_other_scalars_error() {
        assert_eq!(slice_value(&Value::Null, None, None).unwrap(), Value::Null);
        let error = slice_value(&json("true"), Some(0.0), None).unwrap_err();
        assert_eq!(error, "Cannot index boolean with object");
    }

    #[test]
    fn slice_bounds_reject_non_numbers() {
        let env = Env::default();
        let error =
            evaluate_bounds(Some(&Expr::Literal(json("\"x\""))), &Value::Null, &env).unwrap_err();
        assert_eq!(error, "Array/string slice indices must be integers");
    }
}