Handle #{ in Engine::parse_json, restrict to object hashes only.
This commit is contained in:
parent
111f5931b3
commit
c5360db185
@ -9,6 +9,7 @@ New features
|
|||||||
|
|
||||||
* Adds `Engine::register_get_result`, `Engine::register_set_result`, `Engine::register_indexer_get_result`, `Engine::register_indexer_set_result` API.
|
* Adds `Engine::register_get_result`, `Engine::register_set_result`, `Engine::register_indexer_get_result`, `Engine::register_indexer_set_result` API.
|
||||||
* Adds `Module::combine` to combine two modules.
|
* Adds `Module::combine` to combine two modules.
|
||||||
|
* `Engine::parse_json` now also accepts a JSON object starting with `#{`.
|
||||||
|
|
||||||
|
|
||||||
Version 0.18.1
|
Version 0.18.1
|
||||||
|
@ -3,11 +3,14 @@ Parse an Object Map from JSON
|
|||||||
|
|
||||||
{{#include ../links.md}}
|
{{#include ../links.md}}
|
||||||
|
|
||||||
The syntax for an [object map] is extremely similar to JSON, with the exception of `null` values which can
|
The syntax for an [object map] is extremely similar to the JSON representation of a object hash,
|
||||||
technically be mapped to [`()`]. A valid JSON string does not start with a hash character `#` while a
|
with the exception of `null` values which can technically be mapped to [`()`].
|
||||||
Rhai [object map] does - that's the major difference!
|
|
||||||
|
|
||||||
Use the `Engine::parse_json` method to parse a piece of JSON into an object map:
|
A valid JSON string does not start with a hash character `#` while a Rhai [object map] does - that's the major difference!
|
||||||
|
|
||||||
|
Use the `Engine::parse_json` method to parse a piece of JSON into an object map.
|
||||||
|
The JSON text must represent a single object hash (i.e. must be wrapped within "`{ .. }`")
|
||||||
|
otherwise it returns a syntax error.
|
||||||
|
|
||||||
```rust
|
```rust
|
||||||
// JSON string - notice that JSON property names are always quoted
|
// JSON string - notice that JSON property names are always quoted
|
||||||
@ -26,7 +29,7 @@ let json = r#"{
|
|||||||
// Set the second boolean parameter to true in order to map 'null' to '()'
|
// Set the second boolean parameter to true in order to map 'null' to '()'
|
||||||
let map = engine.parse_json(json, true)?;
|
let map = engine.parse_json(json, true)?;
|
||||||
|
|
||||||
map.len() == 6; // 'map' contains all properties in the JSON string
|
map.len() == 6; // 'map' contains all properties in the JSON string
|
||||||
|
|
||||||
// Put the object map into a 'Scope'
|
// Put the object map into a 'Scope'
|
||||||
let mut scope = Scope::new();
|
let mut scope = Scope::new();
|
||||||
@ -34,7 +37,7 @@ 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 successfully used in the script
|
result == 3; // the object map is successfully used in the script
|
||||||
```
|
```
|
||||||
|
|
||||||
Representation of Numbers
|
Representation of Numbers
|
||||||
@ -45,3 +48,30 @@ the [`no_float`] feature is not used. Most common generators of JSON data disti
|
|||||||
integer and floating-point values by always serializing a floating-point number with a decimal point
|
integer and floating-point values by always serializing a floating-point number with a decimal point
|
||||||
(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].
|
||||||
|
|
||||||
|
|
||||||
|
Parse JSON with Sub-Objects
|
||||||
|
--------------------------
|
||||||
|
|
||||||
|
`Engine::parse_json` depends on the fact that the [object map] literal syntax in Rhai is _almost_
|
||||||
|
the same as a JSON object. However, it is _almost_ because the syntax for a sub-object in JSON
|
||||||
|
(i.e. "`{ ... }`") is different from a Rhai [object map] literal (i.e. "`#{ ... }`").
|
||||||
|
|
||||||
|
When `Engine::parse_json` encounters JSON with sub-objects, it fails with a syntax error.
|
||||||
|
|
||||||
|
If it is certain that no text string in the JSON will ever contain the character '`{`',
|
||||||
|
then it is possible to parse it by first replacing all occupance of '`{`' with "`#{`".
|
||||||
|
|
||||||
|
A JSON object hash starting with `#{` is handled transparently by `Engine::parse_json`.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// JSON with sub-object 'b'.
|
||||||
|
let json = r#"{"a":1, "b":{"x":true, "y":false}}"#;
|
||||||
|
|
||||||
|
let new_json = json.replace("{" "#{");
|
||||||
|
|
||||||
|
// The leading '{' will also be replaced to '#{', but parse_json can handle this.
|
||||||
|
let map = engine.parse_json(&new_json, false)?;
|
||||||
|
|
||||||
|
map.len() == 2; // 'map' contains two properties: 'a' and 'b'
|
||||||
|
```
|
||||||
|
44
src/api.rs
44
src/api.rs
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
use crate::any::{Dynamic, Variant};
|
use crate::any::{Dynamic, Variant};
|
||||||
use crate::engine::{Engine, Imports, State};
|
use crate::engine::{Engine, Imports, State};
|
||||||
use crate::error::ParseError;
|
use crate::error::{ParseError, ParseErrorType};
|
||||||
use crate::fn_native::{IteratorFn, SendSync};
|
use crate::fn_native::{IteratorFn, SendSync};
|
||||||
use crate::module::{FuncReturn, Module};
|
use crate::module::{FuncReturn, Module};
|
||||||
use crate::optimize::OptimizationLevel;
|
use crate::optimize::OptimizationLevel;
|
||||||
@ -895,24 +895,39 @@ impl Engine {
|
|||||||
|
|
||||||
/// Parse a JSON string into a map.
|
/// Parse a JSON string into a map.
|
||||||
///
|
///
|
||||||
|
/// The JSON string must be an object hash. It cannot be a simple JavaScript primitive.
|
||||||
|
///
|
||||||
/// Set `has_null` to `true` in order to map `null` values to `()`.
|
/// 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.
|
/// Setting it to `false` will cause a _variable not found_ error during parsing.
|
||||||
///
|
///
|
||||||
|
/// # JSON With Sub-Objects
|
||||||
|
///
|
||||||
|
/// This method assumes no sub-objects in the JSON string. That is because the syntax
|
||||||
|
/// of a JSON sub-object (or object hash), `{ .. }`, is different from Rhai's syntax, `#{ .. }`.
|
||||||
|
/// Parsing a JSON string with sub-objects will cause a syntax error.
|
||||||
|
///
|
||||||
|
/// If it is certain that the character `{` never appears in any text string within the JSON object,
|
||||||
|
/// then globally replace `{` with `#{` before calling this method.
|
||||||
|
///
|
||||||
/// # Example
|
/// # Example
|
||||||
///
|
///
|
||||||
/// ```
|
/// ```
|
||||||
/// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
|
/// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
|
||||||
/// use rhai::Engine;
|
/// use rhai::{Engine, Map};
|
||||||
///
|
///
|
||||||
/// let engine = Engine::new();
|
/// let engine = Engine::new();
|
||||||
///
|
///
|
||||||
/// let map = engine.parse_json(r#"{"a":123, "b":42, "c":false, "d":null}"#, true)?;
|
/// let map = engine.parse_json(
|
||||||
|
/// r#"{"a":123, "b":42, "c":{"x":false, "y":true}, "d":null}"#
|
||||||
|
/// .replace("{", "#{").as_str(), true)?;
|
||||||
///
|
///
|
||||||
/// assert_eq!(map.len(), 4);
|
/// assert_eq!(map.len(), 4);
|
||||||
/// assert_eq!(map.get("a").cloned().unwrap().cast::<i64>(), 123);
|
/// assert_eq!(map["a"].as_int().unwrap(), 123);
|
||||||
/// assert_eq!(map.get("b").cloned().unwrap().cast::<i64>(), 42);
|
/// assert_eq!(map["b"].as_int().unwrap(), 42);
|
||||||
/// assert_eq!(map.get("c").cloned().unwrap().cast::<bool>(), false);
|
/// assert!(map["d"].is::<()>());
|
||||||
/// assert_eq!(map.get("d").cloned().unwrap().cast::<()>(), ());
|
///
|
||||||
|
/// let c = map["c"].read_lock::<Map>().unwrap();
|
||||||
|
/// assert_eq!(c["x"].as_bool().unwrap(), false);
|
||||||
/// # Ok(())
|
/// # Ok(())
|
||||||
/// # }
|
/// # }
|
||||||
/// ```
|
/// ```
|
||||||
@ -921,7 +936,20 @@ impl Engine {
|
|||||||
let mut scope = Scope::new();
|
let mut scope = Scope::new();
|
||||||
|
|
||||||
// Trims the JSON string and add a '#' in front
|
// Trims the JSON string and add a '#' in front
|
||||||
let scripts = ["#", json.trim()];
|
let json_text = json.trim_start();
|
||||||
|
let scripts = if json_text.starts_with(Token::MapStart.syntax().as_ref()) {
|
||||||
|
[json_text, ""]
|
||||||
|
} else if json_text.starts_with(Token::LeftBrace.syntax().as_ref()) {
|
||||||
|
["#", json_text]
|
||||||
|
} else {
|
||||||
|
return Err(ParseErrorType::MissingToken(
|
||||||
|
Token::LeftBrace.syntax().to_string(),
|
||||||
|
"to start a JSON object hash".to_string(),
|
||||||
|
)
|
||||||
|
.into_err(Position::new(1, (json.len() - json_text.len() + 1) as u16))
|
||||||
|
.into());
|
||||||
|
};
|
||||||
|
|
||||||
let stream = lex(
|
let stream = lex(
|
||||||
&scripts,
|
&scripts,
|
||||||
if has_null {
|
if has_null {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
#![cfg(not(feature = "no_object"))]
|
#![cfg(not(feature = "no_object"))]
|
||||||
|
|
||||||
use rhai::{Engine, EvalAltResult, Map, Scope, INT};
|
use rhai::{Engine, EvalAltResult, Map, ParseErrorType, Scope, INT};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_map_indexing() -> Result<(), Box<EvalAltResult>> {
|
fn test_map_indexing() -> Result<(), Box<EvalAltResult>> {
|
||||||
@ -182,6 +182,14 @@ fn test_map_json() -> Result<(), Box<EvalAltResult>> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
engine.parse_json(&format!("#{}", json), true)?;
|
||||||
|
|
||||||
|
assert!(matches!(
|
||||||
|
*engine.parse_json(" 123", true).expect_err("should error"),
|
||||||
|
EvalAltResult::ErrorParsing(ParseErrorType::MissingToken(token, _), pos)
|
||||||
|
if token == "{" && pos.position() == Some(4)
|
||||||
|
));
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user