diff --git a/CHANGELOG.md b/CHANGELOG.md index 96d3285..7887148 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ### v0.6.4 +* New json_encode command #124 * New json_parse command #124 ### v0.6.3 (2020-07-24) diff --git a/docs/sdk.md b/docs/sdk.md index e1046f1..512b642 100644 --- a/docs/sdk.md +++ b/docs/sdk.md @@ -105,6 +105,7 @@ * [std::fs::TempFile (temp_file)](#std__fs__TempFile) * [std::fs::WriteBytes (writebinfile, write_binary_file)](#std__fs__WriteBytes) * [std::fs::WriteText (writefile, write_text_file)](#std__fs__WriteText) +* [std::json::Encode (json_encode)](#std__json__Encode) * [std::json::Parse (json_parse)](#std__json__Parse) * [std::lib::alias::Set (alias)](#std__lib__alias__Set) * [std::lib::alias::Unset (unalias)](#std__lib__alias__Unset) @@ -3888,6 +3889,34 @@ result = writefile ./target/tests/writefile.txt "line 1\nline 2" #### Aliases: writefile, write_text_file + +## std::json::Encode +```sh +string = json_encode var_name +``` + +This function will encode all variables, starting from the root variable as a JSON string.
+Since duckscript is untyped, all boolean and numeric values will be encoded as strings. + +#### Parameters + +The root variable name + +#### Return Value + +The JSON string + +#### Examples + +```sh +package = json_parse "{\"name\": \"my package\", \"version\": 1, \"publish\": false, \"keywords\": [\"test1\", \"test2\"], \"directories\": {\"test\": \"spec\"}}" +jsonstring = json_encode package +``` + + +#### Aliases: +json_encode + ## std::json::Parse ```sh @@ -3917,9 +3946,7 @@ The root value. ```sh package = json_parse "{\"name\": \"my package\", \"version\": 1, \"publish\": false, \"keywords\": [\"test1\", \"test2\"], \"directories\": {\"test\": \"spec\"}}" -defined = is_defined package -assert_false ${defined} - +assert_eq ${package} "[OBJECT]" assert_eq ${package.name} "my package" assert_eq ${package.version} 1 assert_eq ${package.publish} false diff --git a/duckscript_sdk/src/sdk/std/json/encode/help.md b/duckscript_sdk/src/sdk/std/json/encode/help.md new file mode 100644 index 0000000..ca10eab --- /dev/null +++ b/duckscript_sdk/src/sdk/std/json/encode/help.md @@ -0,0 +1,21 @@ +```sh +string = json_encode var_name +``` + +This function will encode all variables, starting from the root variable as a JSON string.
+Since duckscript is untyped, all boolean and numeric values will be encoded as strings. + +#### Parameters + +The root variable name + +#### Return Value + +The JSON string + +#### Examples + +```sh +package = json_parse "{\"name\": \"my package\", \"version\": 1, \"publish\": false, \"keywords\": [\"test1\", \"test2\"], \"directories\": {\"test\": \"spec\"}}" +jsonstring = json_encode package +``` diff --git a/duckscript_sdk/src/sdk/std/json/encode/mod.rs b/duckscript_sdk/src/sdk/std/json/encode/mod.rs new file mode 100755 index 0000000..440f1e7 --- /dev/null +++ b/duckscript_sdk/src/sdk/std/json/encode/mod.rs @@ -0,0 +1,165 @@ +use crate::sdk::std::json::OBJECT_VALUE; +use crate::utils::pckg; +use duckscript::types::command::{Command, CommandResult, Commands}; +use duckscript::types::instruction::Instruction; +use duckscript::types::runtime::StateValue; +use serde_json::map::Map; +use serde_json::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 { + match values.get(format!("{}.length", name).as_str()) { + Some(length_str) => match length_str.parse::() { + 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 { + 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 { + 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(name: &str, variables: &HashMap) -> Result { + 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), + } + } +} + +#[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 { + vec!["json_encode".to_string()] + } + + fn help(&self) -> String { + include_str!("help.md").to_string() + } + + fn clone_and_box(&self) -> Box { + Box::new((*self).clone()) + } + + fn requires_context(&self) -> bool { + true + } + + fn run_with_context( + &self, + arguments: Vec, + _state: &mut HashMap, + variables: &mut HashMap, + _output_variable: Option, + _instructions: &Vec, + _commands: &mut Commands, + _line: usize, + ) -> CommandResult { + if arguments.is_empty() { + CommandResult::Error("No JSON root variable name provided.".to_string()) + } else { + match encode(&arguments[0], variables) { + Ok(output) => CommandResult::Continue(Some(output)), + Err(error) => CommandResult::Error(error), + } + } + } +} + +pub(crate) fn create(package: &str) -> Box { + Box::new(CommandImpl { + package: package.to_string(), + }) +} diff --git a/duckscript_sdk/src/sdk/std/json/encode/mod_test.rs b/duckscript_sdk/src/sdk/std/json/encode/mod_test.rs new file mode 100644 index 0000000..6ff144d --- /dev/null +++ b/duckscript_sdk/src/sdk/std/json/encode/mod_test.rs @@ -0,0 +1,7 @@ +use super::*; +use crate::test; + +#[test] +fn common_functions() { + test::test_common_command_functions(create("")); +} diff --git a/duckscript_sdk/src/sdk/std/json/mod.rs b/duckscript_sdk/src/sdk/std/json/mod.rs index cf40232..addf1eb 100644 --- a/duckscript_sdk/src/sdk/std/json/mod.rs +++ b/duckscript_sdk/src/sdk/std/json/mod.rs @@ -1,3 +1,4 @@ +mod encode; mod parse; use crate::utils::pckg; @@ -6,9 +7,12 @@ use duckscript::types::error::ScriptError; static PACKAGE: &str = "json"; +pub(crate) static OBJECT_VALUE: &str = "[OBJECT]"; + pub(crate) fn load(commands: &mut Commands, parent: &str) -> Result<(), ScriptError> { let package = pckg::concat(parent, PACKAGE); + commands.set(encode::create(&package))?; commands.set(parse::create(&package))?; Ok(()) diff --git a/duckscript_sdk/src/sdk/std/json/parse/help.md b/duckscript_sdk/src/sdk/std/json/parse/help.md index 5a1d3de..c5fd921 100644 --- a/duckscript_sdk/src/sdk/std/json/parse/help.md +++ b/duckscript_sdk/src/sdk/std/json/parse/help.md @@ -25,9 +25,7 @@ The root value. ```sh package = json_parse "{\"name\": \"my package\", \"version\": 1, \"publish\": false, \"keywords\": [\"test1\", \"test2\"], \"directories\": {\"test\": \"spec\"}}" -defined = is_defined package -assert_false ${defined} - +assert_eq ${package} "[OBJECT]" assert_eq ${package.name} "my package" assert_eq ${package.version} 1 assert_eq ${package.publish} false diff --git a/duckscript_sdk/src/sdk/std/json/parse/mod.rs b/duckscript_sdk/src/sdk/std/json/parse/mod.rs index 090cf1b..1c90aea 100755 --- a/duckscript_sdk/src/sdk/std/json/parse/mod.rs +++ b/duckscript_sdk/src/sdk/std/json/parse/mod.rs @@ -1,3 +1,4 @@ +use crate::sdk::std::json::OBJECT_VALUE; use crate::utils::pckg; use duckscript::types::command::{Command, CommandResult, Commands}; use duckscript::types::instruction::Instruction; @@ -33,6 +34,8 @@ fn create_variables(data: Value, name: &str, variables: &mut HashMap { + variables.insert(name.to_string(), OBJECT_VALUE.to_string()); + for (key, value) in map { let child_name = format!("{}.{}", name, key); create_variables(value, &child_name, variables); diff --git a/duckscript_sdk/src/sdk/std/json/parse/mod_test.rs b/duckscript_sdk/src/sdk/std/json/parse/mod_test.rs index 5e52176..2cd4b64 100644 --- a/duckscript_sdk/src/sdk/std/json/parse/mod_test.rs +++ b/duckscript_sdk/src/sdk/std/json/parse/mod_test.rs @@ -24,7 +24,7 @@ out = json_parse "{\"name\": \"my package\", \"version\": 1, \"publish\": false, let variables = context.variables; - assert!(!variables.contains_key("out")); + assert_eq!(variables.get("out").unwrap(), "[OBJECT]"); assert_eq!(variables.get("out.name").unwrap(), "my package"); assert_eq!(variables.get("out.version").unwrap(), "1"); assert_eq!(variables.get("out.publish").unwrap(), "false"); diff --git a/test/std/json/json_encode_test.ds b/test/std/json/json_encode_test.ds new file mode 100644 index 0000000..cfb7eb1 --- /dev/null +++ b/test/std/json/json_encode_test.ds @@ -0,0 +1,47 @@ + +fn test_all_types + jsonstring = set "{\"name\": \"my package\", \"version\": 1, \"publish\": false, \"keywords\": [\"test1\", \"test2\"], \"directories\": {\"test\": \"spec\"}}" + package = json_parse ${jsonstring} + + assert_eq ${package} "[OBJECT]" + assert_eq ${package.name} "my package" + assert_eq ${package.version} 1 + assert_eq ${package.publish} false + assert_eq ${package.keywords.length} 2 + assert_eq ${package.keywords[0]} test1 + assert_eq ${package.keywords[1]} test2 + assert_eq ${package.directories.test} spec + + package.keywords[1] = json_parse ${jsonstring} + package.subpackage = json_parse ${jsonstring} + package.name = set "my package 2" + + update_jsonstring = json_encode package + + package2 = json_parse ${update_jsonstring} + + assert_eq ${package2} "[OBJECT]" + assert_eq ${package2.name} "my package 2" + assert_eq ${package2.version} 1 + assert_eq ${package2.publish} false + assert_eq ${package2.keywords.length} 2 + assert_eq ${package2.keywords[0]} test1 + assert_eq ${package2.keywords[1]} "[OBJECT]" + assert_eq ${package2.directories.test} spec + assert_eq ${package2.subpackage} "[OBJECT]" + assert_eq ${package2.subpackage.name} "my package" + assert_eq ${package2.subpackage.version} 1 + assert_eq ${package2.subpackage.publish} false + assert_eq ${package2.subpackage.keywords.length} 2 + assert_eq ${package2.subpackage.keywords[0]} test1 + assert_eq ${package2.subpackage.keywords[1]} test2 + assert_eq ${package2.subpackage.directories.test} spec + assert_eq ${package2.keywords[1].name} "my package" + assert_eq ${package2.keywords[1].version} 1 + assert_eq ${package2.keywords[1].publish} false + assert_eq ${package2.keywords[1].keywords.length} 2 + assert_eq ${package2.keywords[1].keywords[0]} test1 + assert_eq ${package2.keywords[1].keywords[1]} test2 + assert_eq ${package2.keywords[1].directories.test} spec +end + diff --git a/test/std/json/json_parse_test.ds b/test/std/json/json_parse_test.ds index 75e16d5..3110fd3 100644 --- a/test/std/json/json_parse_test.ds +++ b/test/std/json/json_parse_test.ds @@ -2,9 +2,7 @@ fn test_all_types package = json_parse "{\"name\": \"my package\", \"version\": 1, \"publish\": false, \"keywords\": [\"test1\", \"test2\"], \"directories\": {\"test\": \"spec\"}}" - defined = is_defined package - assert_false ${defined} - + assert_eq ${package} "[OBJECT]" assert_eq ${package.name} "my package" assert_eq ${package.version} 1 assert_eq ${package.publish} false