Add parse_json.

This commit is contained in:
Stephen Chung 2022-09-29 22:46:59 +08:00
parent 6c777e68d3
commit e8fd965eba
9 changed files with 78 additions and 34 deletions

View File

@ -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. * 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. * 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 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%. * 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` is now serializable and deserializable via `serde`.
* `Scope` now contains a const generic parameter that allows specifying how many entries to be kept inline. * `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 Version 1.10.1

View File

@ -295,6 +295,6 @@ impl Engine {
let mut peekable = stream.peekable(); let mut peekable = stream.peekable();
let mut state = ParseState::new(self, scope, Default::default(), tokenizer_control); 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)
} }
} }

View File

@ -122,6 +122,7 @@ impl Engine {
let ast = self.parse_global_expr( let ast = self.parse_global_expr(
&mut stream.peekable(), &mut stream.peekable(),
&mut state, &mut state,
|_| {},
#[cfg(not(feature = "no_optimize"))] #[cfg(not(feature = "no_optimize"))]
OptimizationLevel::None, OptimizationLevel::None,
#[cfg(feature = "no_optimize")] #[cfg(feature = "no_optimize")]

View File

@ -23,7 +23,7 @@ impl Engine {
/// JSON sub-objects are handled transparently. /// JSON sub-objects are handled transparently.
/// ///
/// This function can be used together with [`format_map_as_json`] to work with JSON texts /// 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 /// # Example
/// ///
@ -122,6 +122,7 @@ impl Engine {
let ast = self.parse_global_expr( let ast = self.parse_global_expr(
&mut stream.peekable(), &mut stream.peekable(),
&mut state, &mut state,
|s| s.allow_unquoted_map_properties = false,
#[cfg(not(feature = "no_optimize"))] #[cfg(not(feature = "no_optimize"))]
OptimizationLevel::None, OptimizationLevel::None,
#[cfg(feature = "no_optimize")] #[cfg(feature = "no_optimize")]
@ -137,7 +138,7 @@ impl Engine {
/// Not available under `no_std`. /// Not available under `no_std`.
/// ///
/// This function can be used together with [`Engine::parse_json`] to work with JSON texts /// 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 /// # Data types
/// ///

View File

@ -88,7 +88,6 @@ mod core_functions {
#[cfg(feature = "f32_float")] #[cfg(feature = "f32_float")]
std::thread::sleep(std::time::Duration::from_secs_f32(seconds)); std::thread::sleep(std::time::Duration::from_secs_f32(seconds));
} }
/// Block the current thread for a particular number of `seconds`. /// Block the current thread for a particular number of `seconds`.
#[cfg(not(feature = "no_std"))] #[cfg(not(feature = "no_std"))]
pub fn sleep(seconds: INT) { pub fn sleep(seconds: INT) {
@ -97,6 +96,23 @@ mod core_functions {
} }
std::thread::sleep(std::time::Duration::from_secs(seconds as u64)); 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<Dynamic> {
serde_json::from_str(json).map_err(|err| err.to_string().into())
}
} }
#[cfg(not(feature = "no_function"))] #[cfg(not(feature = "no_function"))]

View File

@ -2,7 +2,7 @@
use crate::engine::OP_EQUALS; use crate::engine::OP_EQUALS;
use crate::plugin::*; 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")] #[cfg(feature = "no_std")]
use std::prelude::v1::*; use std::prelude::v1::*;
@ -304,6 +304,9 @@ mod map_functions {
/// print(m.to_json()); // prints {"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 { 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);
} }
} }

View File

@ -287,26 +287,28 @@ impl<'e> ParseState<'e> {
/// A type that encapsulates all the settings for a particular parsing function. /// A type that encapsulates all the settings for a particular parsing function.
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
struct ParseSettings { pub(crate) struct ParseSettings {
/// Is the construct being parsed located at global level? /// 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? /// Is the construct being parsed located inside a function definition?
#[cfg(not(feature = "no_function"))] #[cfg(not(feature = "no_function"))]
in_fn_scope: bool, pub in_fn_scope: bool,
/// Is the construct being parsed located inside a closure definition? /// Is the construct being parsed located inside a closure definition?
#[cfg(not(feature = "no_function"))] #[cfg(not(feature = "no_function"))]
#[cfg(not(feature = "no_closure"))] #[cfg(not(feature = "no_closure"))]
in_closure: bool, pub in_closure: bool,
/// Is the construct being parsed located inside a breakable loop? /// Is the construct being parsed located inside a breakable loop?
is_breakable: bool, pub is_breakable: bool,
/// Allow statements in blocks? /// 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). /// Language options in effect (overrides Engine options).
options: LangOptions, pub options: LangOptions,
/// Current expression nesting level. /// Current expression nesting level.
level: usize, pub level: usize,
/// Current position. /// Current position.
pos: Position, pub pos: Position,
} }
impl ParseSettings { impl ParseSettings {
@ -996,6 +998,9 @@ impl Engine {
} }
let (name, pos) = match input.next().expect(NEVER_ENDS) { 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) => { (Token::Identifier(s) | Token::StringConstant(s), pos) => {
if map.iter().any(|(p, ..)| **p == *s) { if map.iter().any(|(p, ..)| **p == *s) {
return Err(PERR::DuplicatedProperty(s.to_string()).into_err(pos)); return Err(PERR::DuplicatedProperty(s.to_string()).into_err(pos));
@ -3326,6 +3331,7 @@ impl Engine {
in_closure: false, in_closure: false,
is_breakable: false, is_breakable: false,
allow_statements: true, allow_statements: true,
allow_unquoted_map_properties: settings.allow_unquoted_map_properties,
level: 0, level: 0,
options, options,
pos, pos,
@ -3793,6 +3799,7 @@ impl Engine {
&self, &self,
input: &mut TokenStream, input: &mut TokenStream,
state: &mut ParseState, state: &mut ParseState,
process_settings: impl Fn(&mut ParseSettings),
_optimization_level: OptimizationLevel, _optimization_level: OptimizationLevel,
) -> ParseResult<AST> { ) -> ParseResult<AST> {
let mut functions = StraightHashMap::default(); let mut functions = StraightHashMap::default();
@ -3802,7 +3809,7 @@ impl Engine {
#[cfg(not(feature = "no_function"))] #[cfg(not(feature = "no_function"))]
options.remove(LangOptions::ANON_FN); options.remove(LangOptions::ANON_FN);
let settings = ParseSettings { let mut settings = ParseSettings {
at_global_level: true, at_global_level: true,
#[cfg(not(feature = "no_function"))] #[cfg(not(feature = "no_function"))]
in_fn_scope: false, in_fn_scope: false,
@ -3811,10 +3818,13 @@ impl Engine {
in_closure: false, in_closure: false,
is_breakable: false, is_breakable: false,
allow_statements: false, allow_statements: false,
allow_unquoted_map_properties: true,
level: 0, level: 0,
options, options,
pos: Position::NONE, pos: Position::START,
}; };
process_settings(&mut settings);
let expr = self.parse_expr(input, state, &mut functions, settings)?; let expr = self.parse_expr(input, state, &mut functions, settings)?;
assert!(functions.is_empty()); assert!(functions.is_empty());
@ -3853,25 +3863,27 @@ impl Engine {
&self, &self,
input: &mut TokenStream, input: &mut TokenStream,
state: &mut ParseState, state: &mut ParseState,
process_settings: impl Fn(&mut ParseSettings),
) -> ParseResult<(StmtBlockContainer, StaticVec<Shared<ScriptFnDef>>)> { ) -> ParseResult<(StmtBlockContainer, StaticVec<Shared<ScriptFnDef>>)> {
let mut statements = StmtBlockContainer::new_const(); let mut statements = StmtBlockContainer::new_const();
let mut functions = StraightHashMap::default(); 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() { 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)?; let stmt = self.parse_stmt(input, state, &mut functions, settings)?;
if stmt.is_noop() { if stmt.is_noop() {
@ -3918,7 +3930,7 @@ impl Engine {
state: &mut ParseState, state: &mut ParseState,
_optimization_level: OptimizationLevel, _optimization_level: OptimizationLevel,
) -> ParseResult<AST> { ) -> ParseResult<AST> {
let (statements, _lib) = self.parse_global_level(input, state)?; let (statements, _lib) = self.parse_global_level(input, state, |_| {})?;
#[cfg(not(feature = "no_optimize"))] #[cfg(not(feature = "no_optimize"))]
return Ok(crate::optimizer::optimize_into_ast( return Ok(crate::optimizer::optimize_into_ast(

View File

@ -251,9 +251,9 @@ impl<T: AsRef<str>> From<T> for EvalAltResult {
impl<T: AsRef<str>> From<T> for Box<EvalAltResult> { impl<T: AsRef<str>> From<T> for Box<EvalAltResult> {
#[cold] #[cold]
#[inline(never)] #[inline(always)]
fn from(err: T) -> Self { fn from(err: T) -> Self {
EvalAltResult::ErrorRuntime(err.as_ref().to_string().into(), Position::NONE).into() Into::<EvalAltResult>::into(err).into()
} }
} }

View File

@ -276,6 +276,11 @@ fn test_map_json() -> Result<(), Box<EvalAltResult>> {
EvalAltResult::ErrorMismatchOutputType(..) EvalAltResult::ErrorMismatchOutputType(..)
)); ));
assert!(matches!(
*engine.parse_json("{a:42}", true).expect_err("should error"),
EvalAltResult::ErrorParsing(..)
));
assert!(matches!( assert!(matches!(
*engine *engine
.parse_json("#{a:123}", true) .parse_json("#{a:123}", true)