mumu-json 0.1.0

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

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

use mumu::parser::interpreter::{DynamicFn, DynamicFnInfo, Interpreter};
use mumu::parser::interpreter::apply::apply_n_ary_function_value;
use mumu::parser::types::{FunctionValue, Value, InkIteratorKind};
use serde_json::Value as JsonVal;

/* ═══════════════════════ public registration helpers ═══════════════════ */
pub fn register_json_decode(interp: &mut Interpreter) {
    let f: DynamicFn = Arc::new(Mutex::new(json_decode_bridge));
    let info = DynamicFnInfo::new(f, false);
    interp.register_dynamic_function_ex("json:decode", info);
    interp.set_variable(
        "json:decode",
        Value::Function(Box::new(FunctionValue::Named("json:decode".to_string()))),
    );
}

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

/* ═══════════════════════ json:decode implementation ════════════════════ */
fn json_decode_bridge(intp: &mut Interpreter, args: Vec<Value>) -> Result<Value, String> {
    match args.len() {
        0 => Ok(make_decode_partial(None)),
        1 => {
            if is_placeholder(&args[0]) {
                Ok(make_decode_partial(None))
            } else {
                finalize_decode(intp, args[0].clone())
            }
        }
        n => Err(format!("json:decode ⇒ expected ≤1 argument, got {n}")),
    }
}

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

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

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

    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:decode partial lock error".to_string())?;

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

            match st.arg.clone() {
                Some(v) => finalize_decode(intp, v),
                None => Ok(make_decode_partial(None)),
            }
        },
    ));

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

/* ---------- final decode ----------------------------------------------- */
fn finalize_decode(_intp: &mut Interpreter, src: Value) -> Result<Value, String> {
    match src {
        Value::SingleString(s) => parse_json_string(&s),
        Value::StrArray(sa) if sa.len() == 1 => parse_json_string(&sa[0]),

        /* stream-like forms ------------------------------------------------ */
        Value::InkIterator(handle) => {
            let mut txt = String::new();
            match &handle.kind {
                InkIteratorKind::Core(state_arc) => {
                    loop {
                        let ch_opt = {
                            let mut g = state_arc.lock().map_err(|_| "ink lock")?;
                            if g.done || g.current >= g.end {
                                g.done = true;
                                None
                            } else {
                                let c = g.current;
                                g.current += 1;
                                if g.current >= g.end {
                                    g.done = true;
                                }
                                Some(c)
                            }
                        };
                        match ch_opt {
                            Some(code) if (0..=127).contains(&code) => txt.push(code as u8 as char),
                            Some(code) => return Err(format!("json:decode ⇒ invalid char code {code}")),
                            None => break,
                        }
                    }
                }
                InkIteratorKind::Plugin(plugin_arc) => {
                    let mut plugin = plugin_arc.lock().map_err(|_| "ink plugin lock")?;
                    loop {
                        match plugin.next_value() {
                            Ok(Value::Int(code)) if (0..=127).contains(&code) => {
                                txt.push(code as u8 as char);
                            }
                            Ok(other) => return Err(format!("json:decode ⇒ plugin yielded non-int/invalid char: {:?}", other)),
                            Err(e) if e == "NO_MORE_DATA" => break,
                            Err(e) => return Err(e),
                        }
                    }
                }
            }
            parse_json_string(&txt)
        }
        Value::InkTransform(fb) => {
            let mut txt = String::new();
            loop {
                match apply_n_ary_function_value(_intp, fb.clone(), vec![]) {
                    Ok(chunk) => match chunk {
                        Value::SingleString(s) => txt.push_str(&s),
                        Value::Int(code) if (0..=127).contains(&code) => {
                            txt.push(code as u8 as char)
                        }
                        _ => {
                            return Err(
                                "json:decode transform ⇒ must yield strings or ASCII ints"
                                    .to_string(),
                            )
                        }
                    },
                    Err(e) if e == "NO_MORE_DATA" => break,
                    Err(e) => return Err(e),
                }
            }
            parse_json_string(&txt)
        }

        other => Err(format!(
            "json:decode ⇒ argument must be string/ink, got {other:?}"
        )),
    }
}

fn parse_json_string(text: &str) -> Result<Value, String> {
    serde_json::from_str::<JsonVal>(text)
        .map(|v| crate::jsonval_to_mumu(&v))
        .map_err(|e| format!("json:decode ⇒ parse error: {e}"))
}

/* ═══════════════════════ json:encode implementation ════════════════════ */
fn json_encode_bridge(_intp: &mut Interpreter, args: Vec<Value>) -> Result<Value, String> {
    match args.len() {
        1 => finalize_encode(&Value::Bool(false), &args[0]),
        2 => finalize_encode(&args[0], &args[1]),
        n => Err(format!("json:encode ⇒ expected 1-2 arguments, got {n}")),
    }
}

fn finalize_encode(opt: &Value, data: &Value) -> Result<Value, String> {
    let (pretty, indent) = parse_options(opt)?;
    let mut out = if pretty {
        serde_json::to_string_pretty(&crate::mumu_value_to_json(data)).map_err(|e| e.to_string())?
    } else {
        serde_json::to_string(&crate::mumu_value_to_json(data)).map_err(|e| e.to_string())?
    };

    if pretty {
        if let Some(indent_str) = indent {
            out = out.replace("  ", &indent_str);
        }
    }

    Ok(Value::SingleString(out))
}

fn parse_options(v: &Value) -> Result<(bool, Option<String>), String> {
    match v {
        Value::Bool(false) | Value::Placeholder => Ok((false, None)),
        Value::Bool(true) => Ok((true, None)),
        Value::KeyedArray(map) => {
            let pretty = match map.get("pretty") {
                Some(Value::Bool(b)) => *b,
                _ => false,
            };
            let indent = match map.get("indent") {
                Some(Value::Int(i)) if *i > 0 => Some(" ".repeat(*i as usize)),
                Some(Value::SingleString(s)) if !s.is_empty() => Some(s.clone()),
                _ => None,
            };
            Ok((pretty, indent))
        }
        other => Err(format!(
            "json:encode options must be bool or keyed array, got {other:?}"
        )),
    }
}

/* ═══════════════════════ small utilities ═══════════════════════════════ */
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,
    }
}