mumu-json 0.1.0

JSON tools and JSON Scheam plugin for the Lava language
Documentation
// FILE: json/src/validate.rs

//! `json:validate` – boolean “quick-check” for well-formed JSON text
//!
//! ```mumu
//! json:validate("{ bad: json }")   ;; false
//! json:validate("{\"ok\":42}")     ;; true
//! ```
//!
//! Any non-string `Value` is assumed already valid and returns `true`.

use std::sync::{Arc, Mutex};

use mumu::parser::interpreter::{DynamicFn, DynamicFnInfo, Interpreter};
use mumu::parser::types::{FunctionValue, Value};

pub fn register_json_validate(interp: &mut Interpreter) {
    let f: DynamicFn = Arc::new(Mutex::new(json_validate_bridge));
    let info = DynamicFnInfo::new(f, true); // pure
    interp.register_dynamic_function_ex("json:validate", info);
    interp.set_variable(
        "json:validate",
        Value::Function(Box::new(FunctionValue::Named("json:validate".to_string()))),
    );
}

fn json_validate_bridge(_intp: &mut Interpreter, args: Vec<Value>) -> Result<Value, String> {
    match args.len() {
        0 => Ok(make_partial(None)),
        1 => {
            if is_placeholder(&args[0]) {
                Ok(make_partial(None))
            } else {
                Ok(Value::Bool(is_valid_json_value(&args[0])))
            }
        }
        n => Err(format!("json:validate ⇒ expected ≤1 argument, got {n}")),
    }
}

/* ─────────────── partial application ─────────────────────────────────── */
#[derive(Clone)]
struct PartialState {
    arg: Option<Value>,
}

fn make_partial(arg: Option<Value>) -> Value {
    use FunctionValue::RustClosure;

    let shared = Arc::new(Mutex::new(PartialState { arg }));

    let closure: DynamicFn = Arc::new(Mutex::new(
        move |_intp: &mut Interpreter, new: Vec<Value>| -> Result<Value, String> {
            let mut st = shared
                .lock()
                .map_err(|_| "json:validate partial lock error".to_string())?;

            for v in new {
                if st.arg.is_none() && !is_placeholder(&v) {
                    st.arg = Some(v);
                } else {
                    return Err("json:validate partial ⇒ too many arguments".to_string());
                }
            }

            match st.arg.clone() {
                Some(v) if !is_placeholder(&v) => Ok(Value::Bool(is_valid_json_value(&v))),
                _ => Ok(make_partial(st.arg.clone())),
            }
        },
    ));

    Value::Function(Box::new(RustClosure(
        "json:validate-partial".to_string(),
        closure,
        0,
    )))
}

/* ─────────────── helpers ──────────────────────────────────────────────── */
fn is_valid_json_value(v: &Value) -> bool {
    match v {
        Value::SingleString(s) => serde_json::from_str::<serde_json::Value>(s).is_ok(),
        Value::StrArray(sa) if sa.len() == 1 => {
            serde_json::from_str::<serde_json::Value>(&sa[0]).is_ok()
        }
        _ => true, // non-string values are treated as already-valid JSON
    }
}

fn is_placeholder(v: &Value) -> bool {
    match v {
        Value::Placeholder => true,
        Value::SingleString(s) if s == "_" => true,
        Value::StrArray(sa) if sa.len() == 1 && sa[0] == "_" => true,
        _ => false,
    }
}