duckscriptsdk 0.11.1

The duckscript SDK.
Documentation
use crate::sdk::std::json::OBJECT_VALUE;
use crate::utils::pckg;
use crate::utils::state::get_handles_sub_state;
use duckscript::types::command::{Command, CommandInvocationContext, CommandResult};
use duckscript::types::runtime::StateValue;
use serde_json::map::Map;
use serde_json::{Number, Value};
use std::collections::{HashMap, HashSet};

#[cfg(test)]
#[path = "./mod_test.rs"]
mod mod_test;

fn encode_array(name: &str, values: &HashMap<&str, &str>) -> Result<Value, String> {
    match values.get(format!("{}.length", name).as_str()) {
        Some(length_str) => match length_str.parse::<usize>() {
            Ok(length) => {
                let mut json_vec = vec![];

                for index in 0..length {
                    let array_item_name = format!("{}[{}]", name, index);
                    match encode_any(&array_item_name, values) {
                        Ok(array_item) => json_vec.push(array_item),
                        Err(error) => return Err(error),
                    }
                }

                Ok(Value::Array(json_vec))
            }
            Err(error) => Err(error.to_string()),
        },
        None => Err(format!(
            "{} is not a valid JSON array, missing length attribute.",
            name
        )),
    }
}

fn encode_object(name: &str, values: &HashMap<&str, &str>) -> Result<Value, String> {
    let child_prefix = format!("{}.", name);
    let child_prefix_end = child_prefix.len() - 1;
    let mut children: HashSet<&str> = HashSet::new();

    for (key, _) in values {
        if key.starts_with(&child_prefix) {
            let last_index = key.rfind('.').unwrap();

            if last_index == child_prefix_end {
                let array_key_end = key.rfind("[").unwrap_or(0);
                let next_key = if array_key_end > last_index && key.rfind("]").is_some() {
                    &key[0..array_key_end]
                } else {
                    key
                };
                children.insert(next_key);
            }
        }
    }

    let mut object = Map::new();
    let prefix_length = name.len() + 1;
    for key in children {
        match encode_any(key, values) {
            Ok(json_value) => {
                let child_key = &key[prefix_length..];
                object.insert(child_key.to_string(), json_value);
                ()
            }
            Err(error) => return Err(error),
        }
    }

    Ok(Value::Object(object))
}

fn encode_any(name: &str, values: &HashMap<&str, &str>) -> Result<Value, String> {
    match values.get(name) {
        Some(value) => {
            if *value == OBJECT_VALUE {
                encode_object(name, values)
            } else {
                Ok(Value::String(value.to_string()))
            }
        }
        None => {
            if values.contains_key(format!("{}.length", name).as_str()) {
                encode_array(name, values)
            } else {
                Ok(Value::Null)
            }
        }
    }
}

fn encode_from_variables(
    name: &str,
    variables: &HashMap<String, String>,
) -> Result<String, String> {
    let mut object_variables: HashMap<&str, &str> = HashMap::new();

    for (key, value) in variables {
        if key == name || key.starts_with(name) {
            object_variables.insert(key, value);
        }
    }

    if object_variables.is_empty() {
        Ok("".to_string())
    } else {
        match encode_any(name, &object_variables) {
            Ok(value) => Ok(value.to_string()),
            Err(error) => Err(error),
        }
    }
}

fn encode_from_state_value(
    state_value: &StateValue,
    state: &HashMap<String, StateValue>,
) -> Result<Value, String> {
    match state_value {
        StateValue::Boolean(value) => Ok(Value::Bool(*value)),
        StateValue::Number(value) => Ok(Value::Number(Number::from(*value))),
        StateValue::UnsignedNumber(value) => Ok(Value::Number(Number::from(*value))),
        StateValue::Number32Bit(value) => Ok(Value::Number(Number::from(*value))),
        StateValue::UnsignedNumber32Bit(value) => Ok(Value::Number(Number::from(*value))),
        StateValue::Number64Bit(value) => Ok(Value::Number(Number::from(*value))),
        StateValue::UnsignedNumber64Bit(value) => Ok(Value::Number(Number::from(*value))),
        StateValue::String(value) => match state.get(value) {
            Some(sub_state_value) => encode_from_state_value(sub_state_value, state),
            None => Ok(Value::String(value.to_string())),
        },
        StateValue::List(list) => {
            let mut items = vec![];

            for item in list {
                match encode_from_state_value(item, state) {
                    Ok(item_value) => {
                        items.push(item_value);
                    }
                    Err(error) => return Err(error),
                };
            }

            Ok(Value::Array(items))
        }
        StateValue::SubState(sub_state) => {
            let mut items = Map::new();

            for (key, value) in sub_state {
                match encode_from_state_value(value, state) {
                    Ok(item_value) => {
                        items.insert(key.to_string(), item_value);
                    }
                    Err(error) => return Err(error),
                }
            }

            Ok(Value::Object(items))
        }
        StateValue::ByteArray(_) => Err("Unsupported value type.".to_string()),
        StateValue::Set(_) => Err("Unsupported value type.".to_string()),
        StateValue::Any(_) => Err("Unsupported value type.".to_string()),
    }
}

fn encode_from_state(value: &str, state: &HashMap<String, StateValue>) -> Result<String, String> {
    let json_value = match state.get(value) {
        Some(state_value) => encode_from_state_value(state_value, state),
        None => Ok(Value::String(value.to_string())),
    };

    match json_value {
        Ok(json_value_obj) => Ok(json_value_obj.to_string()),
        Err(error) => Err(error.to_string()),
    }
}

#[derive(Clone)]
pub(crate) struct CommandImpl {
    package: String,
}

impl Command for CommandImpl {
    fn name(&self) -> String {
        pckg::concat(&self.package, "Encode")
    }

    fn aliases(&self) -> Vec<String> {
        vec!["json_encode".to_string()]
    }

    fn help(&self) -> String {
        include_str!("help.md").to_string()
    }

    fn clone_and_box(&self) -> Box<dyn Command> {
        Box::new((*self).clone())
    }

    fn run(&self, context: CommandInvocationContext) -> CommandResult {
        if context.arguments.is_empty() {
            CommandResult::Error("No JSON root variable name provided.".to_string())
        } else {
            let (start_index, as_state) =
                if context.arguments.len() > 1 && context.arguments[0] == "--collection" {
                    (1, true)
                } else {
                    (0, false)
                };

            if as_state {
                let state = get_handles_sub_state(context.state);

                match encode_from_state(&context.arguments[start_index], state) {
                    Ok(output) => CommandResult::Continue(Some(output)),
                    Err(error) => CommandResult::Error(error),
                }
            } else {
                match encode_from_variables(&context.arguments[start_index], context.variables) {
                    Ok(output) => CommandResult::Continue(Some(output)),
                    Err(error) => CommandResult::Error(error),
                }
            }
        }
    }
}

pub(crate) fn create(package: &str) -> Box<dyn Command> {
    Box::new(CommandImpl {
        package: package.to_string(),
    })
}