Add parse_json.

This commit is contained in:
Stephen Chung 2020-04-10 17:14:07 +08:00
parent 5d611d1674
commit ff8eca8a5e
5 changed files with 110 additions and 59 deletions

View File

@ -1379,8 +1379,7 @@ integer and floating-point values by always serializing a floating-point number
(i.e. `123.0` instead of `123` which is assumed to be an integer). This style can be used successfully (i.e. `123.0` instead of `123` which is assumed to be an integer). This style can be used successfully
with Rhai object maps. with Rhai object maps.
Use the [`eval_expression`]`::<Map>` method (or [`eval_expression_with_scope`]`::<Map>` in order to Use the `parse_json` method to parse a piece of JSON into an object map:
handle `null` values) to parse a piece of JSON (with the hash character `#` attached) into an object map:
```rust ```rust
// JSON string - notice that JSON property names are always quoted // JSON string - notice that JSON property names are always quoted
@ -1395,23 +1394,19 @@ let json = r#"{
} }
"#; "#;
// Create a new scope // Parse the JSON expression as an object map
let mut scope = Scope::new(); // Set the second boolean parameter to true in order to map 'null' to '()'
scope.push_constant("null", ()); // map 'null' to '()' let map = engine.parse_json(json, true)?;
// Parse the JSON expression as an object map by attaching '#' in front
let expr = format!("#{}", json);
let map = engine.eval_expression_with_scope::<Map>(&mut scope, expr)?;
map.len() == 6; // 'map' contains all properties int the JSON string map.len() == 6; // 'map' contains all properties int the JSON string
// Push the map back into the scope // Put the object map into a 'Scope'
scope.clear(); let mut scope = Scope::new();
scope.push("map", map); scope.push("map", map);
let result = engine.eval_with_scope::<INT>(r#"map["^^^!!!"].len()"#)?; let result = engine.eval_with_scope::<INT>(r#"map["^^^!!!"].len()"#)?;
result == 3; // the object map is used in a script result == 3; // the object map is successfully used in the script
``` ```
Comparison operators Comparison operators

View File

@ -1,7 +1,7 @@
//! Module that defines the extern API of `Engine`. //! Module that defines the extern API of `Engine`.
use crate::any::{Any, AnyExt, Dynamic}; use crate::any::{Any, AnyExt, Dynamic};
use crate::engine::{make_getter, make_setter, Engine, FnAny, FnSpec}; use crate::engine::{make_getter, make_setter, Engine, FnAny, FnSpec, Map};
use crate::error::ParseError; use crate::error::ParseError;
use crate::fn_call::FuncArgs; use crate::fn_call::FuncArgs;
use crate::fn_register::RegisterFn; use crate::fn_register::RegisterFn;
@ -395,14 +395,9 @@ impl<'e> Engine<'e> {
script: &str, script: &str,
optimization_level: OptimizationLevel, optimization_level: OptimizationLevel,
) -> Result<AST, ParseError> { ) -> Result<AST, ParseError> {
let tokens_stream = lex(script); let scripts = [script];
let stream = lex(&scripts);
parse( parse(&mut stream.peekable(), self, scope, optimization_level)
&mut tokens_stream.peekable(),
self,
scope,
optimization_level,
)
} }
/// Read the contents of a file into a string. /// Read the contents of a file into a string.
@ -483,6 +478,51 @@ impl<'e> Engine<'e> {
Self::read_file(path).and_then(|contents| Ok(self.compile_with_scope(scope, &contents)?)) Self::read_file(path).and_then(|contents| Ok(self.compile_with_scope(scope, &contents)?))
} }
/// Parse a JSON string into a map.
///
/// Set `has_null` to `true` in order to map `null` values to `()`.
/// Setting it to `false` will cause a _variable not found_ error during parsing.
///
/// # Example
///
/// ```
/// # fn main() -> Result<(), rhai::EvalAltResult> {
/// use rhai::{Engine, AnyExt};
///
/// let engine = Engine::new();
///
/// let map = engine.parse_json(r#"{"a":123, "b":42, "c":false, "d":null}"#, true)?;
///
/// assert_eq!(map.len(), 4);
/// assert_eq!(map.get("a").cloned().unwrap().cast::<i64>(), 123);
/// assert_eq!(map.get("b").cloned().unwrap().cast::<i64>(), 42);
/// assert_eq!(map.get("c").cloned().unwrap().cast::<bool>(), false);
/// assert_eq!(map.get("d").cloned().unwrap().cast::<()>(), ());
/// # Ok(())
/// # }
/// ```
#[cfg(not(feature = "no_object"))]
pub fn parse_json(&self, json: &str, has_null: bool) -> Result<Map, EvalAltResult> {
let mut scope = Scope::new();
// Trims the JSON string and add a '#' in front
let scripts = ["#", json.trim()];
let stream = lex(&scripts);
let ast = parse_global_expr(
&mut stream.peekable(),
self,
&scope,
OptimizationLevel::None,
)?;
// Handle null - map to ()
if has_null {
scope.push_constant("null", ());
}
self.eval_ast_with_scope(&mut scope, &ast)
}
/// Compile a string containing an expression into an `AST`, /// Compile a string containing an expression into an `AST`,
/// which can be used later for evaluation. /// which can be used later for evaluation.
/// ///
@ -551,8 +591,9 @@ impl<'e> Engine<'e> {
scope: &Scope, scope: &Scope,
script: &str, script: &str,
) -> Result<AST, ParseError> { ) -> Result<AST, ParseError> {
let tokens_stream = lex(script); let scripts = [script];
parse_global_expr(&mut tokens_stream.peekable(), self, scope) let stream = lex(&scripts);
parse_global_expr(&mut stream.peekable(), self, scope, self.optimization_level)
} }
/// Evaluate a script file. /// Evaluate a script file.
@ -807,15 +848,9 @@ impl<'e> Engine<'e> {
/// Evaluate a string with own scope, but throw away the result and only return error (if any). /// Evaluate a string with own scope, but throw away the result and only return error (if any).
/// Useful for when you don't need the result, but still need to keep track of possible errors. /// Useful for when you don't need the result, but still need to keep track of possible errors.
pub fn consume_with_scope(&self, scope: &mut Scope, script: &str) -> Result<(), EvalAltResult> { pub fn consume_with_scope(&self, scope: &mut Scope, script: &str) -> Result<(), EvalAltResult> {
let tokens_stream = lex(script); let scripts = [script];
let stream = lex(&scripts);
let ast = parse( let ast = parse(&mut stream.peekable(), self, scope, self.optimization_level)?;
&mut tokens_stream.peekable(),
self,
scope,
self.optimization_level,
)?;
self.consume_ast_with_scope(scope, &ast) self.consume_ast_with_scope(scope, &ast)
} }

View File

@ -1503,7 +1503,7 @@ impl Engine<'_> {
Expr::False(_) => Ok(false.into_dynamic()), Expr::False(_) => Ok(false.into_dynamic()),
Expr::Unit(_) => Ok(().into_dynamic()), Expr::Unit(_) => Ok(().into_dynamic()),
expr => panic!("should not appear: {:?}", expr), _ => panic!("should not appear: {:?}", expr),
} }
} }

View File

@ -828,16 +828,40 @@ pub struct TokenIterator<'a> {
can_be_unary: bool, can_be_unary: bool,
/// Current position. /// Current position.
pos: Position, pos: Position,
/// The input characters stream. /// The input character streams.
stream: Peekable<Chars<'a>>, streams: Vec<Peekable<Chars<'a>>>,
} }
impl<'a> TokenIterator<'a> { impl<'a> TokenIterator<'a> {
/// Consume the next character. /// Consume the next character.
fn eat_next(&mut self) { fn eat_next(&mut self) {
self.stream.next(); self.get_next();
self.advance(); self.advance();
} }
/// Get the next character
fn get_next(&mut self) -> Option<char> {
loop {
if self.streams.is_empty() {
return None;
} else if let Some(ch) = self.streams[0].next() {
return Some(ch);
} else {
let _ = self.streams.remove(0);
}
}
}
/// Peek the next character
fn peek_next(&mut self) -> Option<char> {
loop {
if self.streams.is_empty() {
return None;
} else if let Some(ch) = self.streams[0].peek() {
return Some(*ch);
} else {
let _ = self.streams.remove(0);
}
}
}
/// Move the current position one character ahead. /// Move the current position one character ahead.
fn advance(&mut self) { fn advance(&mut self) {
self.pos.advance(); self.pos.advance();
@ -864,7 +888,7 @@ impl<'a> TokenIterator<'a> {
let mut escape = String::with_capacity(12); let mut escape = String::with_capacity(12);
loop { loop {
let next_char = self.stream.next(); let next_char = self.get_next();
self.advance(); self.advance();
match next_char.ok_or((LERR::UnterminatedString, Position::eof()))? { match next_char.ok_or((LERR::UnterminatedString, Position::eof()))? {
@ -907,7 +931,7 @@ impl<'a> TokenIterator<'a> {
}; };
for _ in 0..len { for _ in 0..len {
let c = self.stream.next().ok_or_else(|| { let c = self.get_next().ok_or_else(|| {
(LERR::MalformedEscapeSequence(seq.to_string()), self.pos) (LERR::MalformedEscapeSequence(seq.to_string()), self.pos)
})?; })?;
@ -958,12 +982,12 @@ impl<'a> TokenIterator<'a> {
fn inner_next(&mut self) -> Option<(Token, Position)> { fn inner_next(&mut self) -> Option<(Token, Position)> {
let mut negated = false; let mut negated = false;
while let Some(c) = self.stream.next() { while let Some(c) = self.get_next() {
self.advance(); self.advance();
let pos = self.pos; let pos = self.pos;
match (c, self.stream.peek().copied().unwrap_or('\0')) { match (c, self.peek_next().unwrap_or('\0')) {
// \n // \n
('\n', _) => self.new_line(), ('\n', _) => self.new_line(),
@ -973,7 +997,7 @@ impl<'a> TokenIterator<'a> {
let mut radix_base: Option<u32> = None; let mut radix_base: Option<u32> = None;
result.push(c); result.push(c);
while let Some(&next_char) = self.stream.peek() { while let Some(next_char) = self.peek_next() {
match next_char { match next_char {
'0'..='9' | '_' => { '0'..='9' | '_' => {
result.push(next_char); result.push(next_char);
@ -983,7 +1007,7 @@ impl<'a> TokenIterator<'a> {
'.' => { '.' => {
result.push(next_char); result.push(next_char);
self.eat_next(); self.eat_next();
while let Some(&next_char_in_float) = self.stream.peek() { while let Some(next_char_in_float) = self.peek_next() {
match next_char_in_float { match next_char_in_float {
'0'..='9' | '_' => { '0'..='9' | '_' => {
result.push(next_char_in_float); result.push(next_char_in_float);
@ -1023,7 +1047,7 @@ impl<'a> TokenIterator<'a> {
_ => panic!("unexpected character {}", ch), _ => panic!("unexpected character {}", ch),
}); });
while let Some(&next_char_in_hex) = self.stream.peek() { while let Some(next_char_in_hex) = self.peek_next() {
if !valid.contains(&next_char_in_hex) { if !valid.contains(&next_char_in_hex) {
break; break;
} }
@ -1079,7 +1103,7 @@ impl<'a> TokenIterator<'a> {
let mut result = Vec::new(); let mut result = Vec::new();
result.push(c); result.push(c);
while let Some(&next_char) = self.stream.peek() { while let Some(next_char) = self.peek_next() {
match next_char { match next_char {
x if x.is_ascii_alphanumeric() || x == '_' => { x if x.is_ascii_alphanumeric() || x == '_' => {
result.push(x); result.push(x);
@ -1207,7 +1231,7 @@ impl<'a> TokenIterator<'a> {
('/', '/') => { ('/', '/') => {
self.eat_next(); self.eat_next();
while let Some(c) = self.stream.next() { while let Some(c) = self.get_next() {
if c == '\n' { if c == '\n' {
self.new_line(); self.new_line();
break; break;
@ -1221,18 +1245,18 @@ impl<'a> TokenIterator<'a> {
self.eat_next(); self.eat_next();
while let Some(c) = self.stream.next() { while let Some(c) = self.get_next() {
self.advance(); self.advance();
match c { match c {
'/' => { '/' => {
if self.stream.next() == Some('*') { if self.get_next() == Some('*') {
level += 1; level += 1;
} }
self.advance(); self.advance();
} }
'*' => { '*' => {
if self.stream.next() == Some('/') { if self.get_next() == Some('/') {
level -= 1; level -= 1;
} }
self.advance(); self.advance();
@ -1272,7 +1296,7 @@ impl<'a> TokenIterator<'a> {
self.eat_next(); self.eat_next();
return Some(( return Some((
if self.stream.peek() == Some(&'=') { if self.peek_next() == Some('=') {
self.eat_next(); self.eat_next();
Token::LeftShiftAssign Token::LeftShiftAssign
} else { } else {
@ -1291,7 +1315,7 @@ impl<'a> TokenIterator<'a> {
self.eat_next(); self.eat_next();
return Some(( return Some((
if self.stream.peek() == Some(&'=') { if self.peek_next() == Some('=') {
self.eat_next(); self.eat_next();
Token::RightShiftAssign Token::RightShiftAssign
} else { } else {
@ -1368,11 +1392,11 @@ impl<'a> Iterator for TokenIterator<'a> {
} }
/// Tokenize an input text stream. /// Tokenize an input text stream.
pub fn lex(input: &str) -> TokenIterator<'_> { pub fn lex<'a>(input: &'a [&'a str]) -> TokenIterator<'a> {
TokenIterator { TokenIterator {
can_be_unary: true, can_be_unary: true,
pos: Position::new(1, 0), pos: Position::new(1, 0),
stream: input.chars().peekable(), streams: input.iter().map(|s| s.chars().peekable()).collect(),
} }
} }
@ -2696,6 +2720,7 @@ pub fn parse_global_expr<'a, 'e>(
input: &mut Peekable<TokenIterator<'a>>, input: &mut Peekable<TokenIterator<'a>>,
engine: &Engine<'e>, engine: &Engine<'e>,
scope: &Scope, scope: &Scope,
optimization_level: OptimizationLevel,
) -> Result<AST, ParseError> { ) -> Result<AST, ParseError> {
let expr = parse_expr(input, false)?; let expr = parse_expr(input, false)?;
@ -2711,7 +2736,7 @@ pub fn parse_global_expr<'a, 'e>(
scope, scope,
vec![Stmt::Expr(Box::new(expr))], vec![Stmt::Expr(Box::new(expr))],
vec![], vec![],
engine.optimization_level, optimization_level,
), ),
) )
} }

View File

@ -163,13 +163,9 @@ fn test_map_for() -> Result<(), EvalAltResult> {
fn test_map_json() -> Result<(), EvalAltResult> { fn test_map_json() -> Result<(), EvalAltResult> {
let engine = Engine::new(); let engine = Engine::new();
let mut scope = Scope::new();
scope.push_constant("null", ());
scope.push_constant("undefined", ());
let json = r#"{"a":1, "b":true, "c":42, "$d e f!":"hello", "z":null}"#; let json = r#"{"a":1, "b":true, "c":42, "$d e f!":"hello", "z":null}"#;
let map = engine.eval_expression_with_scope::<Map>(&mut scope, &("#".to_string() + json))?; let map = engine.parse_json(json, true)?;
assert!(!map.contains_key("x")); assert!(!map.contains_key("x"));
@ -211,7 +207,7 @@ fn test_map_json() -> Result<(), EvalAltResult> {
#[cfg(not(feature = "no_index"))] #[cfg(not(feature = "no_index"))]
{ {
scope.clear(); let mut scope = Scope::new();
scope.push_constant("map", map); scope.push_constant("map", map);
assert_eq!( assert_eq!(