tree-mumu 0.1.0-rc.2

Creates Linux `tree`-style renderings of MuMu values
Documentation
// src/share/to_string.rs
//
// Bridge + option parsing for `tree:to_string(options, data)`.
// - Partial application supported, including '_' placeholder.
// - options must be a keyed array:
//     ascii: bool           // ASCII fallback (default: false => UTF)
//     root:  string         // Root label (default: "<root>")
//
//     quote_strings: bool   // quote/escape string leaves (default: true)
//     show_types: bool      // append (type) on leaves (default: false)
//     index_labels: bool    // show [i] for array items (default: true)
//     max_depth: int        // depth cap (0+); omitted = unbounded
//
// Returns a single string with Linux `tree`-style lines.

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

use indexmap::IndexMap;
use mumu::parser::interpreter::Interpreter;
use mumu::parser::types::Value;

use super::glyphs::{GlyphSet, ASCII, UTF};
use super::render::render_lines;
use super::walk::{build_tree, WalkOpts};

pub fn to_string_bridge(_interp: &mut Interpreter, args: Vec<Value>) -> Result<Value, String> {
    call_entry(args)
}

fn is_placeholder(v: &Value) -> bool {
    matches!(v, Value::Placeholder)
        || matches!(v, Value::SingleString(s) if s == "_")
        || matches!(v, Value::StrArray(ss) if ss.len()==1 && ss[0] == "_")
}

#[derive(Debug, Clone)]
struct Opts {
    glyphs: GlyphSet,
    root: String,
    walk: WalkOpts,
}

impl Default for Opts {
    fn default() -> Self {
        Self {
            glyphs: UTF,
            root: "<root>".to_string(),
            walk: WalkOpts {
                quote_strings: true,
                show_types: false,
                index_labels: true,
                max_depth: None,
            },
        }
    }
}

fn parse_options(map: &IndexMap<String, Value>) -> Result<Opts, String> {
    let mut o = Opts::default();

    for (k, v) in map.iter() {
        match k.as_str() {
            "ascii" => o.glyphs = if expect_bool("ascii", v)? { ASCII } else { UTF },
            "root" => o.root = expect_string("root", v)?,
            "quote_strings" => o.walk.quote_strings = expect_bool("quote_strings", v)?,
            "show_types" => o.walk.show_types = expect_bool("show_types", v)?,
            "index_labels" => o.walk.index_labels = expect_bool("index_labels", v)?,
            "max_depth" => {
                let n = expect_int("max_depth", v)?;
                if n < 0 { return Err("tree:to_string => max_depth must be >= 0".to_string()); }
                o.walk.max_depth = Some(n as usize);
            }
            _ => { /* ignore unknown */ }
        }
    }

    Ok(o)
}

fn expect_bool(name: &str, v: &Value) -> Result<bool, String> {
    match v {
        Value::Bool(b) => Ok(*b),
        _ => Err(format!("tree:to_string => {} must be bool", name)),
    }
}

fn expect_string(name: &str, v: &Value) -> Result<String, String> {
    match v {
        Value::SingleString(s) => Ok(s.clone()),
        Value::StrArray(a) if a.len() == 1 => Ok(a[0].clone()),
        _ => Err(format!("tree:to_string => {} must be string", name)),
    }
}

fn expect_int(name: &str, v: &Value) -> Result<i32, String> {
    match v {
        Value::Int(i) => Ok(*i),
        _ => Err(format!("tree:to_string => {} must be int", name)),
    }
}

/// Compute the textual tree from options and data.
fn render_to_string(opts: &Opts, data: &Value) -> String {
    let root = build_tree(&opts.root, data, &opts.walk);
    let lines = render_lines(&root, &opts.glyphs);
    lines.join("\n")
}

/// Entry point dispatcher supporting partials and placeholders.
fn call_entry(mut args: Vec<Value>) -> Result<Value, String> {
    match args.len() {
        0 => Ok(make_partial(None, None)),
        1 => {
            let a0 = args.remove(0);
            if is_placeholder(&a0) {
                Ok(make_partial(None, None))
            } else if let Value::KeyedArray(map) = a0 {
                let _ = parse_options(&map)?; // validate early
                Ok(make_partial(Some(Value::KeyedArray(map)), None))
            } else {
                Err("tree:to_string => first argument must be options (keyed array) or '_'".to_string())
            }
        }
        2 => {
            let a0 = args.remove(0);
            let a1 = args.remove(0);

            // Placeholder cases
            let hole_left = is_placeholder(&a0);
            let hole_right = is_placeholder(&a1);

            if hole_left && hole_right {
                return Ok(make_partial(None, None));
            }
            if hole_left {
                return Ok(make_partial(None, Some(a1)));
            }
            if hole_right {
                if !matches!(a0, Value::KeyedArray(_)) {
                    return Err("tree:to_string => options must be a keyed array".to_string());
                }
                return Ok(make_partial(Some(a0), None));
            }

            // Final call
            let opts = match a0 {
                Value::KeyedArray(ref m) => parse_options(m)?,
                _ => return Err("tree:to_string => options must be a keyed array".to_string()),
            };
            let out = render_to_string(&opts, &a1);
            Ok(Value::SingleString(out))
        }
        n => Err(format!("tree:to_string => expected 0, 1 or 2 args, got {}", n)),
    }
}

fn make_partial(opts: Option<Value>, data: Option<Value>) -> Value {
    use mumu::parser::types::FunctionValue::RustClosure;

    let state = Arc::new(Mutex::new((opts, data)));

    let closure = RustClosure(
        "tree:to_string-partial".to_string(),
        Arc::new(Mutex::new(move |_interp: &mut Interpreter, new_args: Vec<Value>| {
            let mut st = state.lock().map_err(|_| "tree:to_string => partial lock error".to_string())?;

            for v in new_args {
                if st.0.is_none() {
                    if is_placeholder(&v) {
                        // remain None
                    } else if matches!(v, Value::KeyedArray(_)) {
                        st.0 = Some(v);
                    } else {
                        return Err("tree:to_string => options must be a keyed array or '_'".to_string());
                    }
                } else if st.1.is_none() {
                    if is_placeholder(&v) {
                        // remain None
                    } else {
                        st.1 = Some(v);
                    }
                } else {
                    return Err("tree:to_string => partial received too many arguments".to_string());
                }
            }

            if let (Some(o), Some(d)) = (&st.0, &st.1) {
                let opts = match o {
                    Value::KeyedArray(m) => parse_options(m)?,
                    _ => return Err("tree:to_string => options must be a keyed array".to_string()),
                };
                let out = render_to_string(&opts, d);
                return Ok(Value::SingleString(out));
            }

            // Keep returning a partial function that accumulates more args
            Ok(make_partial(st.0.clone(), st.1.clone()))
        })),
        0,
    );

    Value::Function(Box::new(closure))
}