robinpath 0.2.0

RobinPath - A lightweight, fast scripting language interpreter for automation and data processing
Documentation
use crate::executor::Environment;
use crate::value::Value;

/// Convert JSON5-like syntax to valid JSON (handles unquoted keys)
fn convert_json5_to_json(input: &str) -> String {
    let mut result = String::with_capacity(input.len() + 32);
    let chars: Vec<char> = input.chars().collect();
    let len = chars.len();
    let mut i = 0;

    while i < len {
        let c = chars[i];
        match c {
            '"' | '\'' => {
                // String literal — copy as-is (convert single quotes to double)
                let quote = c;
                result.push('"');
                i += 1;
                while i < len && chars[i] != quote {
                    if chars[i] == '\\' && i + 1 < len {
                        result.push(chars[i]);
                        result.push(chars[i + 1]);
                        i += 2;
                    } else {
                        if chars[i] == '"' && quote == '\'' {
                            result.push('\\');
                        }
                        result.push(chars[i]);
                        i += 1;
                    }
                }
                result.push('"');
                if i < len {
                    i += 1; // skip closing quote
                }
            }
            _ if c.is_alphabetic() || c == '_' || c == '$' => {
                // Check if this is an unquoted key (followed by :)
                let start = i;
                while i < len && (chars[i].is_alphanumeric() || chars[i] == '_' || chars[i] == '$') {
                    i += 1;
                }
                let word: String = chars[start..i].iter().collect();

                // Skip whitespace to see if followed by ':'
                let mut j = i;
                while j < len && chars[j].is_whitespace() {
                    j += 1;
                }

                if j < len && chars[j] == ':' {
                    // This is an unquoted key — quote it
                    result.push('"');
                    result.push_str(&word);
                    result.push('"');
                } else {
                    // Regular word (true, false, null, etc.)
                    result.push_str(&word);
                }
            }
            _ => {
                result.push(c);
                i += 1;
            }
        }
    }
    result
}

pub fn register(env: &mut Environment) {
    env.register_builtin("obj", |args, _| {
        // Parse JSON5-like string to object, or return empty object if no args
        if let Some(Value::String(s)) = args.first() {
            // Try standard JSON first, then try JSON5-like conversion
            match serde_json::from_str::<serde_json::Value>(s) {
                Ok(v) => Ok(Value::from(v)),
                Err(_) => {
                    // Try to convert JSON5-like syntax to valid JSON
                    let converted = convert_json5_to_json(s);
                    match serde_json::from_str::<serde_json::Value>(&converted) {
                        Ok(v) => Ok(Value::from(v)),
                        Err(e) => Err(format!("obj: Failed to parse: {}", e)),
                    }
                }
            }
        } else {
            Ok(Value::Object(indexmap::IndexMap::new()))
        }
    });

    env.register_builtin("array", |args, _| {
        Ok(Value::Array(args.to_vec()))
    });

    env.register_builtin("range", |args, _| {
        let start = args.first().and_then(|v| v.as_number()).unwrap_or(0.0) as i64;
        let end = args.get(1).and_then(|v| v.as_number()).unwrap_or(0.0) as i64;
        let step = args.get(2).and_then(|v| v.as_number()).unwrap_or(1.0) as i64;

        let mut arr = Vec::new();
        if step > 0 {
            let mut i = start;
            while i <= end {
                arr.push(Value::Number(i as f64));
                i += step;
            }
        } else if step < 0 {
            let mut i = start;
            while i >= end {
                arr.push(Value::Number(i as f64));
                i += step;
            }
        }
        Ok(Value::Array(arr))
    });

    env.register_builtin("set", |args, _| {
        // set is handled in executor, but register anyway
        args.first().cloned().ok_or_else(|| "set: missing value".to_string())
    });

    env.register_builtin("get", |args, _| {
        // get obj key (supports dot-path like "address.city")
        match (args.first(), args.get(1)) {
            (Some(val), Some(Value::String(key))) => {
                let mut current = val.clone();
                for part in key.split('.') {
                    current = match &current {
                        Value::Object(obj) => obj.get(part).cloned().unwrap_or(Value::Null),
                        Value::Array(arr) => {
                            if let Ok(idx) = part.parse::<usize>() {
                                arr.get(idx).cloned().unwrap_or(Value::Null)
                            } else {
                                Value::Null
                            }
                        }
                        _ => Value::Null,
                    };
                }
                Ok(current)
            }
            (Some(Value::Array(arr)), Some(Value::Number(idx))) => {
                let i = *idx as usize;
                Ok(arr.get(i).cloned().unwrap_or(Value::Null))
            }
            _ => Ok(Value::Null),
        }
    });

    env.register_builtin("getType", |args, _| {
        Ok(Value::String(
            args.first().map_or("null", |v| v.type_of()).to_string(),
        ))
    });

    env.register_builtin("repeat", |args, _| {
        let count = args.first().and_then(|v| v.as_number()).unwrap_or(0.0) as usize;
        let value = args.get(1).cloned().unwrap_or(Value::Null);
        Ok(Value::Array(vec![value; count]))
    });
}