mumu-json 0.1.0

JSON tools and JSON Scheam plugin for the Lava language
Documentation
// FILE: json/src/schema.rs  (fully rewritten)

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

use indexmap::IndexMap;
use jsonschema::JSONSchema;
use mumu::parser::interpreter::{DynamicFn, DynamicFnInfo, Interpreter};
use mumu::parser::types::{FunctionValue, Value};
use serde_json::Value as JsonVal;

/* ────────────────────────────────────────────────────────────────────────────
   Public registration helper – called from plugin entry-point
   ──────────────────────────────────────────────────────────────────────────── */

pub fn register_json_schema(interp: &mut Interpreter) {
    let f: DynamicFn = Arc::new(Mutex::new(json_schema_bridge));
    let info = DynamicFnInfo::new(f, false);
    interp.register_dynamic_function_ex("json:schema", info);
    interp.set_variable(
        "json:schema",
        Value::Function(Box::new(FunctionValue::Named("json:schema".to_string()))),
    );
}

/* ────────────────────────────────────────────────────────────────────────────
   json:schema – two-argument runtime validator
   ──────────────────────────────────────────────────────────────────────────── */

fn json_schema_bridge(_intp: &mut Interpreter, args: Vec<Value>) -> Result<Value, String> {
    match args.len() {
        0 | 1 => Ok(build_partial(None, args.get(0).cloned())),
        2 => {
            let sch  = &args[0];
            let data = &args[1];

            match (is_placeholder(sch), is_placeholder(data)) {
                (false, false) => validate_pair(sch.clone(), data.clone()),
                _ => Ok(build_partial(Some(sch.clone()), Some(data.clone()))),
            }
        }
        n => Err(format!("json:schema ⇒ expected 0-2 arguments, got {n}")),
    }
}

/* ────────────────────────────────────────────────────────────────────────────
   Partial-application machinery
   ──────────────────────────────────────────────────────────────────────────── */

#[derive(Clone)]
struct PartialState {
    schema: Option<Value>,
    data:   Option<Value>,
}

fn build_partial(schema: Option<Value>, data: Option<Value>) -> Value {
    use FunctionValue::RustClosure;

    let shared = Arc::new(Mutex::new(PartialState { schema, data }));

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

            for v in new_args {
                if st.schema.is_none() && !is_placeholder(&v) {
                    st.schema = Some(v);
                    continue;
                }
                if st.data.is_none() && !is_placeholder(&v) {
                    st.data = Some(v);
                    continue;
                }
                return Err("json:schema partial ⇒ too many arguments".to_string());
            }

            match (&st.schema, &st.data) {
                (Some(s), Some(d)) if !is_placeholder(s) && !is_placeholder(d) => {
                    validate_pair(s.clone(), d.clone())
                }
                _ => Ok(build_partial(st.schema.clone(), st.data.clone())),
            }
        },
    ));

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

/* ────────────────────────────────────────────────────────────────────────────
   Core validation helper – now “soft” with aggregated diagnostics
   ──────────────────────────────────────────────────────────────────────────── */

fn validate_pair(schema_val: Value, data_val: Value) -> Result<Value, String> {
    let mut errors: Vec<String> = Vec::new();

    /* 1) Parse / coerce the *schema* JSON */
    let schema_json = match to_json(schema_val) {
        Ok(j)  => Some(j),
        Err(e) => {
            errors.push(format!("schema error: {e}"));
            None
        }
    };

    /* 2) Parse / coerce the *instance* JSON */
    let data_json = match to_json(data_val) {
        Ok(j)  => Some(j),
        Err(e) => {
            errors.push(format!("data error: {e}"));
            None
        }
    };

    /* 3) If both parsed, continue with compile + validate */
    if errors.is_empty() {
        if let Some(schema_doc) = schema_json {
            match JSONSchema::compile(&schema_doc) {
                Ok(compiled) => {
                    if let Some(instance_doc) = data_json {
                        if let Err(iter) = compiled.validate(&instance_doc) {
                            errors.extend(iter.map(|e| e.to_string()));
                        }
                    }
                }
                Err(e) => {
                    errors.push(format!("schema compile error: {e}"));
                }
            }
        }
    }

    /* 4) Build `{ ok: bool, errors: [...] }` result */
    let mut map = IndexMap::new();
    let ok_flag = errors.is_empty();
    map.insert("ok".to_string(), Value::Bool(ok_flag));
    map.insert("errors".to_string(), Value::StrArray(errors));

    Ok(Value::KeyedArray(map))
}

/* ────────────────────────────────────────────────────────────────────────────
   Helpers
   ──────────────────────────────────────────────────────────────────────────── */

/// Convert a Lava `Value` into serde-json `Value`.
/// Malformed text ⇒ Err(message) so that caller can decide soft vs hard fail.
fn to_json(v: Value) -> Result<JsonVal, String> {
    match v {
        Value::SingleString(s) => serde_json::from_str(&s)
            .map_err(|e| e.to_string()),

        Value::StrArray(sa) if sa.len() == 1 => serde_json::from_str(&sa[0])
            .map_err(|e| e.to_string()),

        Value::KeyedArray(_)
        | Value::IntArray(_)
        | Value::FloatArray(_)
        | Value::BoolArray(_)
        | Value::Int2DArray(_)
        | Value::Float2DArray(_) => Ok(crate::mumu_value_to_json(&v)),

        other => Err(format!("unsupported value {other:?}")),
    }
}

/// Treat `_` (placeholder) in any of its string forms as missing.
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,
    }
}