guartcl 0.5.0-beta4

Enhanced Jim Tcl.
Documentation
//! Traversal and manipulation functions.

use std::rc::Rc;

use indexmap::IndexMap;
use jimtcl::{
    Interp, JimError, JimObject, JimResult, check_or_error, custom::JimExtensionType,
    object::IntoJimObj, tcl_error,
};

use crate::typedata::TypedData;

/// Look up an object within the typed data.
pub fn td_get<'jim>(interp: &'jim Interp, args: &[JimObject<'jim>]) -> JimResult<JimObject<'jim>> {
    let usage = "usage: td get ?-native? <object> <key>...";
    check_or_error!(args.len() >= 3, usage);
    let (native, data, keys) = if args[1].as_str()? == "-native" {
        check_or_error!(args.len() >= 4, usage);
        (true, &args[2], &args[3..])
    } else {
        (false, &args[1], &args[2..])
    };

    let obj = TypedData::extract_ref(data)?;
    let data = td_walk_path(obj, keys)?;
    if native {
        data.to_native(interp)
    } else {
        // FIXME avoid clone
        Ok(data.clone().to_jim(interp))
    }
}

/// Set an object within the typed data.
pub fn td_set<'jim>(interp: &'jim Interp, args: &[JimObject<'jim>]) -> JimResult<JimObject<'jim>> {
    let usage = "usage: td set <object> <key>... <value>";
    check_or_error!(args.len() >= 4, usage);
    let var = args[1].as_str()?;
    let keys = &args[2..args.len() - 1];
    let val = TypedData::extract(&args[args.len() - 1])?;

    let mut obj = {
        let var_content = interp
            .get_variable(var)
            .ok_or_else(|| tcl_error!("variable {} not set", var))?;
        TypedData::extract(&var_content)?
    };

    let tgt = td_walk_mut(Rc::make_mut(&mut obj), keys)?;
    *tgt = (*val).clone();
    let obj = obj.to_jim(interp);
    interp.set_variable(var, &obj)?;
    Ok(obj)
}

/// Set an object within the typed data.
pub fn td_lappend<'jim>(
    interp: &'jim Interp,
    args: &[JimObject<'jim>],
) -> JimResult<JimObject<'jim>> {
    let usage = "usage: td lappend <object> ?<key>...? ?--? <value>...";
    check_or_error!(args.len() >= 3, usage);
    let var = args[1].as_str()?;
    let n = args.len();

    // look for the set options
    let (keys, tcl_vals) = {
        if let Some(pos) = args[2..n].iter().position(|s| s.as_bytes() == b"--") {
            (&args[2..pos + 2], &args[pos + 3..n])
        } else {
            (&args[2..n - 1], &args[n - 1..n])
        }
    };

    let mut values = Vec::with_capacity(tcl_vals.len());
    for o in tcl_vals {
        values.push(TypedData::extract(o)?);
    }

    let mut obj = {
        let var_content = interp
            .get_variable(var)
            .ok_or_else(|| tcl_error!("variable {} not set", var))?;
        TypedData::extract(&var_content)?
    };

    let tgt = td_walk_mut(Rc::make_mut(&mut obj), keys)?;
    match tgt {
        TypedData::List(list) => {
            Rc::make_mut(list).extend(values);
        }
        _ => return Err(tcl_error!("TypedData object is not a list")),
    }
    let obj = obj.to_jim(interp);
    interp.set_variable(var, &obj)?;
    Ok(obj)
}

fn td_walk_path<'jim: 'a, 'a>(
    mut obj: &'a TypedData,
    mut keys: &[JimObject<'jim>],
) -> JimResult<&'a TypedData> {
    while !keys.is_empty() {
        let key = &keys[0];
        let data: &TypedData = match obj {
            TypedData::List(list) => {
                let i = key.as_index()?;
                if i < 0 && ((-i) as usize) <= list.len() {
                    &list[(-i - 1) as usize]
                } else if i >= 0 && (i as usize) < list.len() {
                    &list[i as usize]
                } else {
                    return Err(format!("list index {} out of bounds", i).into());
                }
            }
            TypedData::Record(map) => {
                let k = key.as_str()?;
                map.get(k)
                    .ok_or_else(|| tcl_error!("key {} does not exist", k))?
            }
            _ => return Err(JimError::from("typed data is not a list or object")),
        };
        obj = data;
        keys = &keys[1..]
    }
    Ok(obj)
}

fn td_walk_mut<'jim: 'a, 'a>(
    mut obj: &'a mut TypedData,
    mut keys: &[JimObject<'jim>],
) -> JimResult<&'a mut TypedData> {
    while !keys.is_empty() {
        let key = &keys[0];
        let data: &mut TypedData = match obj {
            TypedData::List(list) => {
                let arr = Rc::make_mut(list);
                let i = key.as_index()?;
                if i < 0 && ((-i) as usize) <= arr.len() {
                    Rc::make_mut(&mut arr[(-i - 1) as usize])
                } else if i >= 0 && (i as usize) < arr.len() {
                    Rc::make_mut(&mut arr[i as usize])
                } else {
                    return Err(format!("list index {} out of bounds", i).into());
                }
            }
            TypedData::Record(map) => {
                let m = Rc::make_mut(map);
                let k = key.as_str()?;
                Rc::make_mut(
                    m.entry(k.to_string())
                        .or_insert_with(|| TypedData::Record(IndexMap::new().into()).into()),
                )
            }
            _ => return Err(JimError::from("typed data is not a list or object")),
        };
        obj = data;
        keys = &keys[1..]
    }
    Ok(obj)
}