From c3d013bddcd2ba51374868aa3da1a708663aec52 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Thu, 21 Apr 2022 12:15:21 +0800 Subject: [PATCH] Add to_json for maps. --- CHANGELOG.md | 2 + src/api/compile.rs | 128 ---------------------------- src/api/json.rs | 175 ++++++++++++++++++++++++++++++++++++++ src/api/mod.rs | 2 + src/lib.rs | 3 + src/packages/map_basic.rs | 24 +++++- 6 files changed, 205 insertions(+), 129 deletions(-) create mode 100644 src/api/json.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index cb3e08b5..a2b740dd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ Enhancements * `Module::eval_ast_as_new_raw` is made public as a low-level API. * Improper `switch` case condition syntax is now caught at parse time. * `Engine::parse_json` now natively handles nested JSON inputs (using a token remap filter) without needing to replace `{` with `#{`. +* `to_json` is added to object maps to cheaply convert it to JSON format (`()` is mapped to `null`, all other data types must be supported by JSON) +* A global function `format_map_as_json` is provided which is the same as `to_json` for object maps. Version 1.6.1 diff --git a/src/api/compile.rs b/src/api/compile.rs index 7688a89c..2259ec92 100644 --- a/src/api/compile.rs +++ b/src/api/compile.rs @@ -308,132 +308,4 @@ impl Engine { self.options.optimization_level, ) } - /// Parse a JSON string into an [object map][crate::Map]. - /// This is a light-weight alternative to using, say, - /// [`serde_json`](https://crates.io/crates/serde_json) to deserialize the JSON. - /// - /// Not available under `no_object`. - /// - /// The JSON string must be an object hash. It cannot be a simple primitive value. - /// - /// Set `has_null` to `true` in order to map `null` values to `()`. - /// Setting it to `false` causes a syntax error for any `null` value. - /// - /// JSON sub-objects are handled transparently. - /// - /// # Example - /// - /// ``` - /// # fn main() -> Result<(), Box> { - /// use rhai::{Engine, Map}; - /// - /// let engine = Engine::new(); - /// - /// let map = engine.parse_json(r#" - /// { - /// "a": 123, - /// "b": 42, - /// "c": { - /// "x": false, - /// "y": true, - /// "z": '$' - /// }, - /// "d": null - /// }"#, true)?; - /// - /// assert_eq!(map.len(), 4); - /// assert_eq!(map["a"].as_int().expect("a should exist"), 123); - /// assert_eq!(map["b"].as_int().expect("b should exist"), 42); - /// assert_eq!(map["d"].as_unit().expect("d should exist"), ()); - /// - /// let c = map["c"].read_lock::().expect("c should exist"); - /// assert_eq!(c["x"].as_bool().expect("x should be bool"), false); - /// assert_eq!(c["y"].as_bool().expect("y should be bool"), true); - /// assert_eq!(c["z"].as_char().expect("z should be char"), '$'); - /// # Ok(()) - /// # } - /// ``` - #[cfg(not(feature = "no_object"))] - #[inline(always)] - pub fn parse_json( - &self, - json: impl AsRef, - has_null: bool, - ) -> crate::RhaiResultOf { - use crate::{tokenizer::Token, LexError}; - - let scripts = [json.as_ref()]; - - let (stream, tokenizer_control) = self.lex_raw( - &scripts, - if has_null { - Some(&|token, _, _| { - match token { - // `null` => `()` - Token::Reserved(s) if &*s == "null" => Token::Unit, - // `{` => `#{` - Token::LeftBrace => Token::MapStart, - // Disallowed syntax - t @ (Token::Unit | Token::MapStart) => Token::LexError( - LexError::ImproperSymbol( - t.literal_syntax().to_string(), - "".to_string(), - ) - .into(), - ), - Token::InterpolatedString(..) => Token::LexError( - LexError::ImproperSymbol( - "interpolated string".to_string(), - "".to_string(), - ) - .into(), - ), - // All others - _ => token, - } - }) - } else { - Some(&|token, _, _| { - match token { - Token::Reserved(s) if &*s == "null" => Token::LexError( - LexError::ImproperSymbol("null".to_string(), "".to_string()).into(), - ), - // `{` => `#{` - Token::LeftBrace => Token::MapStart, - // Disallowed syntax - t @ (Token::Unit | Token::MapStart) => Token::LexError( - LexError::ImproperSymbol( - t.literal_syntax().to_string(), - "".to_string(), - ) - .into(), - ), - Token::InterpolatedString(..) => Token::LexError( - LexError::ImproperSymbol( - "interpolated string".to_string(), - "".to_string(), - ) - .into(), - ), - // All others - _ => token, - } - }) - }, - ); - - let mut state = ParseState::new(self, tokenizer_control); - - let ast = self.parse_global_expr( - &mut stream.peekable(), - &mut state, - &Scope::new(), - #[cfg(not(feature = "no_optimize"))] - OptimizationLevel::None, - #[cfg(feature = "no_optimize")] - OptimizationLevel::default(), - )?; - - self.eval_ast(&ast) - } } diff --git a/src/api/json.rs b/src/api/json.rs new file mode 100644 index 00000000..4eba3eb6 --- /dev/null +++ b/src/api/json.rs @@ -0,0 +1,175 @@ +//! Module that defines JSON manipulation functions for [`Engine`]. +#![cfg(not(feature = "no_object"))] + +use crate::{Engine, LexError, Map, OptimizationLevel, ParseState, RhaiResultOf, Scope, Token}; + +impl Engine { + /// Parse a JSON string into an [object map][Map]. + /// + /// This is a light-weight alternative to using, say, [`serde_json`](https://crates.io/crates/serde_json) + /// to deserialize the JSON. + /// + /// Not available under `no_object`. + /// + /// The JSON string must be an object hash. It cannot be a simple primitive value. + /// + /// Set `has_null` to `true` in order to map `null` values to `()`. + /// Setting it to `false` causes a syntax error for any `null` value. + /// + /// JSON sub-objects are handled transparently. + /// + /// This function can be used together with [`format_map_as_json`] to work with JSON texts + /// without using the [`serde`](https://crates.io/crates/serde) crate (which is heavy). + /// + /// # Example + /// + /// ``` + /// # fn main() -> Result<(), Box> { + /// use rhai::{Engine, Map}; + /// + /// let engine = Engine::new(); + /// + /// let map = engine.parse_json(r#" + /// { + /// "a": 123, + /// "b": 42, + /// "c": { + /// "x": false, + /// "y": true, + /// "z": '$' + /// }, + /// "d": null + /// }"#, true)?; + /// + /// assert_eq!(map.len(), 4); + /// assert_eq!(map["a"].as_int().expect("a should exist"), 123); + /// assert_eq!(map["b"].as_int().expect("b should exist"), 42); + /// assert_eq!(map["d"].as_unit().expect("d should exist"), ()); + /// + /// let c = map["c"].read_lock::().expect("c should exist"); + /// assert_eq!(c["x"].as_bool().expect("x should be bool"), false); + /// assert_eq!(c["y"].as_bool().expect("y should be bool"), true); + /// assert_eq!(c["z"].as_char().expect("z should be char"), '$'); + /// # Ok(()) + /// # } + /// ``` + #[cfg(not(feature = "no_object"))] + #[inline(always)] + pub fn parse_json(&self, json: impl AsRef, has_null: bool) -> RhaiResultOf { + let scripts = [json.as_ref()]; + + let (stream, tokenizer_control) = self.lex_raw( + &scripts, + if has_null { + Some(&|token, _, _| { + match token { + // `null` => `()` + Token::Reserved(s) if &*s == "null" => Token::Unit, + // `{` => `#{` + Token::LeftBrace => Token::MapStart, + // Disallowed syntax + t @ (Token::Unit | Token::MapStart) => Token::LexError( + LexError::ImproperSymbol( + t.literal_syntax().to_string(), + "".to_string(), + ) + .into(), + ), + Token::InterpolatedString(..) => Token::LexError( + LexError::ImproperSymbol( + "interpolated string".to_string(), + "".to_string(), + ) + .into(), + ), + // All others + _ => token, + } + }) + } else { + Some(&|token, _, _| { + match token { + Token::Reserved(s) if &*s == "null" => Token::LexError( + LexError::ImproperSymbol("null".to_string(), "".to_string()).into(), + ), + // `{` => `#{` + Token::LeftBrace => Token::MapStart, + // Disallowed syntax + t @ (Token::Unit | Token::MapStart) => Token::LexError( + LexError::ImproperSymbol( + t.literal_syntax().to_string(), + "Invalid JSON syntax".to_string(), + ) + .into(), + ), + Token::InterpolatedString(..) => Token::LexError( + LexError::ImproperSymbol( + "interpolated string".to_string(), + "Invalid JSON syntax".to_string(), + ) + .into(), + ), + // All others + _ => token, + } + }) + }, + ); + + let mut state = ParseState::new(self, tokenizer_control); + + let ast = self.parse_global_expr( + &mut stream.peekable(), + &mut state, + &Scope::new(), + #[cfg(not(feature = "no_optimize"))] + OptimizationLevel::None, + #[cfg(feature = "no_optimize")] + OptimizationLevel::default(), + )?; + + self.eval_ast(&ast) + } +} + +/// Return the JSON representation of an [object map][Map]. +/// +/// This function can be used together with [`Engine::parse_json`] to work with JSON texts +/// without using the [`serde`](https://crates.io/crates/serde) crate (which is heavy). +/// +/// # Data types +/// +/// Only the following data types should be kept inside the object map: [`INT`][crate::INT], +/// [`FLOAT`][crate::FLOAT], [`ImmutableString`][crate::ImmutableString], `char`, `bool`, `()`, +/// [`Array`][crate::Array], [`Map`]. +/// +/// # Errors +/// +/// Data types not supported by JSON serialize into formats that may invalidate the result. +pub fn format_map_as_json(map: &Map) -> String { + let mut result = String::from('{'); + + for (key, value) in map { + if result.len() > 1 { + result.push(','); + } + + result.push_str(&format!("{:?}", key)); + result.push(':'); + + if let Some(val) = value.read_lock::() { + result.push_str(&format_map_as_json(&*val)); + continue; + } + + if value.is::<()>() { + result.push_str("null"); + } else { + result.push_str(&format!("{:?}", value)); + } + } + + result.push('}'); + + result +} diff --git a/src/api/mod.rs b/src/api/mod.rs index 25431175..554f48d5 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -8,6 +8,8 @@ pub mod run; pub mod compile; +pub mod json; + pub mod files; pub mod register; diff --git a/src/lib.rs b/src/lib.rs index 56958dd3..f10e2b20 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -237,6 +237,9 @@ pub type Blob = Vec; #[cfg(not(feature = "no_object"))] pub type Map = std::collections::BTreeMap; +#[cfg(not(feature = "no_object"))] +pub use api::json::format_map_as_json; + #[cfg(not(feature = "no_module"))] pub use module::ModuleResolver; diff --git a/src/packages/map_basic.rs b/src/packages/map_basic.rs index a3b0931f..771bc7aa 100644 --- a/src/packages/map_basic.rs +++ b/src/packages/map_basic.rs @@ -2,7 +2,7 @@ use crate::engine::OP_EQUALS; use crate::plugin::*; -use crate::{def_package, Dynamic, ImmutableString, Map, RhaiResultOf, INT}; +use crate::{def_package, format_map_as_json, Dynamic, ImmutableString, Map, RhaiResultOf, INT}; #[cfg(feature = "no_std")] use std::prelude::v1::*; @@ -266,4 +266,26 @@ mod map_functions { map.values().cloned().collect() } } + /// Return the JSON representation of the object map. + /// + /// # Data types + /// + /// Only the following data types should be kept inside the object map: + /// `INT`, `FLOAT`, `ImmutableString`, `char`, `bool`, `()`, `Array`, `Map`. + /// + /// # Errors + /// + /// Data types not supported by JSON serialize into formats that may + /// invalidate the result. + /// + /// # Example + /// + /// ```rhai + /// let m = #{a:1, b:2, c:3}; + /// + /// print(m.to_json()); // prints {"a":1, "b":2, "c":3} + /// ``` + pub fn to_json(map: &mut Map) -> String { + format_map_as_json(map) + } }