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