From e8fd965eba7f16bbb90e17ee9955b30ab040a985 Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Thu, 29 Sep 2022 22:46:59 +0800 Subject: [PATCH] Add parse_json. --- CHANGELOG.md | 6 ++++ src/api/compile.rs | 2 +- src/api/eval.rs | 1 + src/api/json.rs | 5 +-- src/packages/lang_core.rs | 18 ++++++++++- src/packages/map_basic.rs | 7 +++-- src/parser.rs | 64 +++++++++++++++++++++++---------------- src/types/error.rs | 4 +-- tests/maps.rs | 5 +++ 9 files changed, 78 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 28fe4604..81e4c859 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,11 @@ New features * It is now possible to specify a fixed _seed_ for use with the `ahash` hasher, via an environment variable, in order to force stable (i.e. deterministic) hashes for function signatures. This is necessary when using Rhai across shared-library boundaries. * A build script is now used to extract the environment variable (`RHAI_AHASH_SEED`) and splice it into the source code before compilation. +Bug fixes +--------- + +* `Engine::parse_json` now returns an error on unquoted keys to be consistent with JSON specifications. + Enhancements ------------ @@ -19,6 +24,7 @@ Enhancements * Due to a code refactor, built-in operators for standard types now run even faster, in certain cases by 20-30%. * `Scope` is now serializable and deserializable via `serde`. * `Scope` now contains a const generic parameter that allows specifying how many entries to be kept inline. +* `parse_json` function is added to parse a JSON string into an object map. Version 1.10.1 diff --git a/src/api/compile.rs b/src/api/compile.rs index 5322dbeb..2d187160 100644 --- a/src/api/compile.rs +++ b/src/api/compile.rs @@ -295,6 +295,6 @@ impl Engine { let mut peekable = stream.peekable(); let mut state = ParseState::new(self, scope, Default::default(), tokenizer_control); - self.parse_global_expr(&mut peekable, &mut state, self.optimization_level) + self.parse_global_expr(&mut peekable, &mut state, |_| {}, self.optimization_level) } } diff --git a/src/api/eval.rs b/src/api/eval.rs index 4b22d99c..6808e2e6 100644 --- a/src/api/eval.rs +++ b/src/api/eval.rs @@ -122,6 +122,7 @@ impl Engine { let ast = self.parse_global_expr( &mut stream.peekable(), &mut state, + |_| {}, #[cfg(not(feature = "no_optimize"))] OptimizationLevel::None, #[cfg(feature = "no_optimize")] diff --git a/src/api/json.rs b/src/api/json.rs index 034062bf..24069fdd 100644 --- a/src/api/json.rs +++ b/src/api/json.rs @@ -23,7 +23,7 @@ impl Engine { /// 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). + /// without using the [`serde_json`](https://crates.io/crates/serde_json) crate (which is heavy). /// /// # Example /// @@ -122,6 +122,7 @@ impl Engine { let ast = self.parse_global_expr( &mut stream.peekable(), &mut state, + |s| s.allow_unquoted_map_properties = false, #[cfg(not(feature = "no_optimize"))] OptimizationLevel::None, #[cfg(feature = "no_optimize")] @@ -137,7 +138,7 @@ impl Engine { /// Not available under `no_std`. /// /// 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). +/// without using the [`serde_json`](https://crates.io/crates/serde_json) crate (which is heavy). /// /// # Data types /// diff --git a/src/packages/lang_core.rs b/src/packages/lang_core.rs index 5eaa5ea3..d423691d 100644 --- a/src/packages/lang_core.rs +++ b/src/packages/lang_core.rs @@ -88,7 +88,6 @@ mod core_functions { #[cfg(feature = "f32_float")] std::thread::sleep(std::time::Duration::from_secs_f32(seconds)); } - /// Block the current thread for a particular number of `seconds`. #[cfg(not(feature = "no_std"))] pub fn sleep(seconds: INT) { @@ -97,6 +96,23 @@ mod core_functions { } std::thread::sleep(std::time::Duration::from_secs(seconds as u64)); } + + /// Parse a JSON string into a value. + /// + /// # Example + /// + /// ```rhai + /// let m = parse_json(`{"a":1, "b":2, "c":3}`); + /// + /// print(m); // prints #{"a":1, "b":2, "c":3} + /// ``` + #[cfg(not(feature = "no_index"))] + #[cfg(not(feature = "no_object"))] + #[cfg(feature = "metadata")] + #[rhai_fn(return_raw)] + pub fn parse_json(_ctx: NativeCallContext, json: &str) -> RhaiResultOf { + serde_json::from_str(json).map_err(|err| err.to_string().into()) + } } #[cfg(not(feature = "no_function"))] diff --git a/src/packages/map_basic.rs b/src/packages/map_basic.rs index 955b8395..2d25641d 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, format_map_as_json, Dynamic, ImmutableString, Map, RhaiResultOf, INT}; +use crate::{def_package, Dynamic, ImmutableString, Map, NativeCallContext, RhaiResultOf, INT}; #[cfg(feature = "no_std")] use std::prelude::v1::*; @@ -304,6 +304,9 @@ mod map_functions { /// print(m.to_json()); // prints {"a":1, "b":2, "c":3} /// ``` pub fn to_json(map: &mut Map) -> String { - format_map_as_json(map) + #[cfg(feature = "metadata")] + return serde_json::to_string(map).unwrap_or_else(|_| "ERROR".into()); + #[cfg(not(feature = "metadata"))] + return crate::format_map_as_json(map); } } diff --git a/src/parser.rs b/src/parser.rs index d10f132c..0ec27614 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -287,26 +287,28 @@ impl<'e> ParseState<'e> { /// A type that encapsulates all the settings for a particular parsing function. #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] -struct ParseSettings { +pub(crate) struct ParseSettings { /// Is the construct being parsed located at global level? - at_global_level: bool, + pub at_global_level: bool, /// Is the construct being parsed located inside a function definition? #[cfg(not(feature = "no_function"))] - in_fn_scope: bool, + pub in_fn_scope: bool, /// Is the construct being parsed located inside a closure definition? #[cfg(not(feature = "no_function"))] #[cfg(not(feature = "no_closure"))] - in_closure: bool, + pub in_closure: bool, /// Is the construct being parsed located inside a breakable loop? - is_breakable: bool, + pub is_breakable: bool, /// Allow statements in blocks? - allow_statements: bool, + pub allow_statements: bool, + /// Allow unquoted map properties? + pub allow_unquoted_map_properties: bool, /// Language options in effect (overrides Engine options). - options: LangOptions, + pub options: LangOptions, /// Current expression nesting level. - level: usize, + pub level: usize, /// Current position. - pos: Position, + pub pos: Position, } impl ParseSettings { @@ -996,6 +998,9 @@ impl Engine { } let (name, pos) = match input.next().expect(NEVER_ENDS) { + (Token::Identifier(..), pos) if !settings.allow_unquoted_map_properties => { + return Err(PERR::PropertyExpected.into_err(pos)) + } (Token::Identifier(s) | Token::StringConstant(s), pos) => { if map.iter().any(|(p, ..)| **p == *s) { return Err(PERR::DuplicatedProperty(s.to_string()).into_err(pos)); @@ -3326,6 +3331,7 @@ impl Engine { in_closure: false, is_breakable: false, allow_statements: true, + allow_unquoted_map_properties: settings.allow_unquoted_map_properties, level: 0, options, pos, @@ -3793,6 +3799,7 @@ impl Engine { &self, input: &mut TokenStream, state: &mut ParseState, + process_settings: impl Fn(&mut ParseSettings), _optimization_level: OptimizationLevel, ) -> ParseResult { let mut functions = StraightHashMap::default(); @@ -3802,7 +3809,7 @@ impl Engine { #[cfg(not(feature = "no_function"))] options.remove(LangOptions::ANON_FN); - let settings = ParseSettings { + let mut settings = ParseSettings { at_global_level: true, #[cfg(not(feature = "no_function"))] in_fn_scope: false, @@ -3811,10 +3818,13 @@ impl Engine { in_closure: false, is_breakable: false, allow_statements: false, + allow_unquoted_map_properties: true, level: 0, options, - pos: Position::NONE, + pos: Position::START, }; + process_settings(&mut settings); + let expr = self.parse_expr(input, state, &mut functions, settings)?; assert!(functions.is_empty()); @@ -3853,25 +3863,27 @@ impl Engine { &self, input: &mut TokenStream, state: &mut ParseState, + process_settings: impl Fn(&mut ParseSettings), ) -> ParseResult<(StmtBlockContainer, StaticVec>)> { let mut statements = StmtBlockContainer::new_const(); let mut functions = StraightHashMap::default(); + let mut settings = ParseSettings { + at_global_level: true, + #[cfg(not(feature = "no_function"))] + in_fn_scope: false, + #[cfg(not(feature = "no_function"))] + #[cfg(not(feature = "no_closure"))] + in_closure: false, + is_breakable: false, + allow_statements: true, + allow_unquoted_map_properties: true, + options: self.options, + level: 0, + pos: Position::START, + }; + process_settings(&mut settings); while !input.peek().expect(NEVER_ENDS).0.is_eof() { - let settings = ParseSettings { - at_global_level: true, - #[cfg(not(feature = "no_function"))] - in_fn_scope: false, - #[cfg(not(feature = "no_function"))] - #[cfg(not(feature = "no_closure"))] - in_closure: false, - is_breakable: false, - allow_statements: true, - options: self.options, - level: 0, - pos: Position::NONE, - }; - let stmt = self.parse_stmt(input, state, &mut functions, settings)?; if stmt.is_noop() { @@ -3918,7 +3930,7 @@ impl Engine { state: &mut ParseState, _optimization_level: OptimizationLevel, ) -> ParseResult { - let (statements, _lib) = self.parse_global_level(input, state)?; + let (statements, _lib) = self.parse_global_level(input, state, |_| {})?; #[cfg(not(feature = "no_optimize"))] return Ok(crate::optimizer::optimize_into_ast( diff --git a/src/types/error.rs b/src/types/error.rs index ba9b7351..89bbedd8 100644 --- a/src/types/error.rs +++ b/src/types/error.rs @@ -251,9 +251,9 @@ impl> From for EvalAltResult { impl> From for Box { #[cold] - #[inline(never)] + #[inline(always)] fn from(err: T) -> Self { - EvalAltResult::ErrorRuntime(err.as_ref().to_string().into(), Position::NONE).into() + Into::::into(err).into() } } diff --git a/tests/maps.rs b/tests/maps.rs index 3cb414db..2620738d 100644 --- a/tests/maps.rs +++ b/tests/maps.rs @@ -276,6 +276,11 @@ fn test_map_json() -> Result<(), Box> { EvalAltResult::ErrorMismatchOutputType(..) )); + assert!(matches!( + *engine.parse_json("{a:42}", true).expect_err("should error"), + EvalAltResult::ErrorParsing(..) + )); + assert!(matches!( *engine .parse_json("#{a:123}", true)