reddb-io-tq 0.29.8

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

use super::super::ast::Expr;
use super::super::eval::{self, Env};
use super::Builtin;

pub(super) const BUILTINS: &[Builtin] = &[
    Builtin::new("ascii_downcase", 0, call_ascii_downcase),
    Builtin::new("ascii_upcase", 0, call_ascii_upcase),
    Builtin::new("endswith", 1, call_endswith),
    Builtin::new("explode", 0, call_explode),
    Builtin::new("implode", 0, call_implode),
    Builtin::new("join", 1, eval::call_join),
    Builtin::new("ltrimstr", 1, call_ltrimstr),
    Builtin::new("rtrimstr", 1, call_rtrimstr),
    Builtin::new("split", 1, eval::call_split),
    Builtin::new("startswith", 1, call_startswith),
    Builtin::new("trim", 0, call_trim).divergent("`trim/0` is not defined by jq 1.7.1"),
    Builtin::new("trimstr", 1, call_trimstr).divergent("`trimstr/1` is not defined by jq 1.7.1"),
];

fn call_ascii_downcase(_: &[Expr], input: &Value, _: &Env) -> Result<Vec<Value>, String> {
    let Value::String(value) = input else {
        return Err("explode input must be a string".to_owned());
    };
    Ok(vec![Value::String(value.to_ascii_lowercase())])
}

fn call_ascii_upcase(_: &[Expr], input: &Value, _: &Env) -> Result<Vec<Value>, String> {
    let Value::String(value) = input else {
        return Err("explode input must be a string".to_owned());
    };
    Ok(vec![Value::String(value.to_ascii_uppercase())])
}

fn call_ltrimstr(arguments: &[Expr], input: &Value, env: &Env) -> Result<Vec<Value>, String> {
    Ok(arguments[0]
        .eval(input, env)?
        .into_iter()
        .map(|prefix| match (input, prefix) {
            (Value::String(value), Value::String(prefix)) => {
                Value::String(value.strip_prefix(&prefix).unwrap_or(value).to_owned())
            }
            _ => input.clone(),
        })
        .collect())
}

fn call_rtrimstr(arguments: &[Expr], input: &Value, env: &Env) -> Result<Vec<Value>, String> {
    Ok(arguments[0]
        .eval(input, env)?
        .into_iter()
        .map(|suffix| match (input, suffix) {
            (Value::String(value), Value::String(suffix)) => {
                Value::String(value.strip_suffix(&suffix).unwrap_or(value).to_owned())
            }
            _ => input.clone(),
        })
        .collect())
}

fn call_trimstr(arguments: &[Expr], input: &Value, env: &Env) -> Result<Vec<Value>, String> {
    let (value, affix) = string_input_and_arg(arguments, input, env, "trimstr")?;
    let value = value.strip_prefix(&affix).unwrap_or(&value);
    Ok(vec![Value::String(
        value.strip_suffix(&affix).unwrap_or(value).to_owned(),
    )])
}

fn call_trim(_: &[Expr], input: &Value, _: &Env) -> Result<Vec<Value>, String> {
    let Value::String(value) = input else {
        return Err("trim input must be a string".to_owned());
    };
    Ok(vec![Value::String(value.trim().to_owned())])
}

fn call_startswith(arguments: &[Expr], input: &Value, env: &Env) -> Result<Vec<Value>, String> {
    let (value, prefix) = string_input_and_arg(arguments, input, env, "startswith()")?;
    Ok(vec![Value::Bool(value.starts_with(&prefix))])
}

fn call_endswith(arguments: &[Expr], input: &Value, env: &Env) -> Result<Vec<Value>, String> {
    let (value, suffix) = string_input_and_arg(arguments, input, env, "endswith()")?;
    Ok(vec![Value::Bool(value.ends_with(&suffix))])
}

fn call_explode(_: &[Expr], input: &Value, _: &Env) -> Result<Vec<Value>, String> {
    let Value::String(value) = input else {
        return Err("explode input must be a string".to_owned());
    };
    Ok(vec![Value::Array(Array::List(
        value
            .chars()
            .map(|character| Value::Number(u32::from(character).to_string()))
            .collect(),
    ))])
}

fn call_implode(_: &[Expr], input: &Value, _: &Env) -> Result<Vec<Value>, String> {
    let Value::Array(array) = input else {
        return Err("implode input must be an array".to_owned());
    };
    let mut output = String::new();
    for value in array.values() {
        let Value::Number(number) = value else {
            return Err("unicode codepoint needs to be numeric".to_owned());
        };
        let codepoint = number
            .parse::<f64>()
            .map_err(|_| "unicode codepoint needs to be numeric".to_owned())?;
        let character =
            if codepoint.is_finite() && codepoint >= 0.0 && codepoint <= f64::from(u32::MAX) {
                char::from_u32(codepoint.trunc() as u32).unwrap_or(char::REPLACEMENT_CHARACTER)
            } else {
                char::REPLACEMENT_CHARACTER
            };
        output.push(character);
    }
    Ok(vec![Value::String(output)])
}

fn string_input_and_arg(
    arguments: &[Expr],
    input: &Value,
    env: &Env,
    builtin: &str,
) -> Result<(String, String), String> {
    let Value::String(value) = input else {
        return Err(format!("{builtin} requires string inputs"));
    };
    let values = arguments[0].eval(input, env)?;
    match values.as_slice() {
        [Value::String(argument)] => Ok((value.clone(), argument.clone())),
        _ => Err(format!("{builtin} requires string inputs")),
    }
}