diff --git a/RELEASES.md b/RELEASES.md index b753bba3..89b05c36 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -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 `Module::combine` to combine two modules. +* `Engine::parse_json` now also accepts a JSON object starting with `#{`. Version 0.18.1 diff --git a/doc/src/SUMMARY.md b/doc/src/SUMMARY.md index 2ef05a0f..7a4fd884 100644 --- a/doc/src/SUMMARY.md +++ b/doc/src/SUMMARY.md @@ -88,6 +88,7 @@ The Rhai Scripting Language 4. [Create from AST](language/modules/ast.md) 5. [Module Resolvers](rust/modules/resolvers.md) 1. [Custom Implementation](rust/modules/imp-resolver.md) + 18. [Eval Statement](language/eval.md) 6. [Safety and Protection](safety/index.md) 1. [Checked Arithmetic](safety/checked.md) 2. [Sand-Boxing](safety/sandbox.md) @@ -119,7 +120,7 @@ The Rhai Scripting Language 1. [Disable Keywords and/or Operators](engine/disable.md) 2. [Custom Operators](engine/custom-op.md) 3. [Extending with Custom Syntax](engine/custom-syntax.md) - 7. [Eval Statement](language/eval.md) + 7. [Multiple Instantiation](patterns/multiple.md) 8. [Appendix](appendix/index.md) 1. [Keywords](appendix/keywords.md) 2. [Operators and Symbols](appendix/operators.md) diff --git a/doc/src/language/json.md b/doc/src/language/json.md index a5aaba2a..543ddfdb 100644 --- a/doc/src/language/json.md +++ b/doc/src/language/json.md @@ -7,7 +7,7 @@ The syntax for an [object map] is extremely similar to JSON, with the exception technically be mapped to [`()`]. 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: +Use the `Engine::parse_json` method to parse a piece of _simple_ JSON into an object map: ```rust // JSON string - notice that JSON property names are always quoted @@ -26,7 +26,7 @@ let json = r#"{ // Set the second boolean parameter to true in order to map 'null' to '()' 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' let mut scope = Scope::new(); @@ -34,7 +34,7 @@ scope.push("map", map); let result = engine.eval_with_scope::(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 @@ -45,3 +45,28 @@ 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 (i.e. `123.0` instead of `123` which is assumed to be an integer). This style can be used successfully 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 "`#{`". + +```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' +``` diff --git a/doc/src/patterns/multiple.md b/doc/src/patterns/multiple.md new file mode 100644 index 00000000..1abbe942 --- /dev/null +++ b/doc/src/patterns/multiple.md @@ -0,0 +1,89 @@ +Multiple Instantiation +====================== + +{{#include ../links.md}} + + +Background +---------- + +Rhai's [features] are not strictly additive. This is easily deduced from the [`no_std`] feature +which prepares the crate for `no-std` builds. Obviously, turning on this feature has a material +impact on how Rhai behaves. + +Many crates resolve this by going the opposite direction: build for `no-std` in default, +but add a `std` feature, included by default, which builds for the `stdlib`. + + +Rhai Language Features Are Not Additive +-------------------------------------- + +Rhai, however, is more complex. Language features cannot be easily made _additive_. + +That is because the _lack_ of a language feature is a feature by itself. + +For example, by including [`no_float`], a project sets the Rhai language to ignore floating-point math. +Floating-point numbers do not even parse under this case and will generate syntax errors. +Assume that the project expects this behavior (why? perhaps integers are all that make sense +within the project domain). + +Now, assume that a dependent crate also depends on Rhai. Under such circumstances, +unless _exact_ versioning is used and the dependent crate depends on a _different_ version +of Rhai, Cargo automatically _merges_ both dependencies, with the [`no_float`] feature turned on +because Cargo features are _additive_. + +This will break the dependent crate, which does not by itself specify [`no_float`] +and expects floating-point numbers and math to work normally. + +There is no way out of this dilemma. Reversing the [features] set with a `float` feature +causes the project to break because floating-point numbers are not rejected as expected. + + +Multiple Instantiations of Rhai Within The Same Project +------------------------------------------------------ + +The trick is to differentiate between multiple identical copies of Rhai, each having +a different [features] set, by their _sources_: + +* Different versions from [`crates.io`](https://crates.io/crates/rhai/) - The official crate. + +* Different releases from [`GitHub`](https://github.com/jonathandturner/rhai) - Crate source on GitHub. + +* Forked copy of [https://github.com/jonathandturner/rhai](https://github.com/jonathandturner/rhai) on GitHub. + +* Local copy of [https://github.com/jonathandturner/rhai](https://github.com/jonathandturner/rhai) downloaded form GitHub. + +Use the following configuration in `Cargo.toml` to pull in multiple copies of Rhai within the same project: + +```toml +[dependencies] +rhai = { version = "{{version}}", features = [ "no_float" ] } +rhai_github = { git = "https://github.com/jonathandturner/rhai", features = [ "unchecked" ] } +rhai_my_github = { git = "https://github.com/my_github/rhai", branch = "variation1", features = [ "serde", "no_closure" ] } +rhai_local = { path = "../rhai_copy" } +``` + +The example above creates four different modules: `rhai`, `rhai_github`, `rhai_my_github` and +`rhai_local`, each referring to a different Rhai copy with the appropriate [features] set. + +Only one crate of any particular version can be used from each source, because Cargo merges +all candidate cases within the same source, adding all [features] together. + +If more than four different instantiations of Rhai is necessary (why?), create more local repositories +or GitHub forks or branches. + + +Caveat - No Way To Avoid Dependency Conflicts +-------------------------------------------- + +Unfortunately, pulling in Rhai from different sources do not resolve the problem of +[features] conflict between dependencies. Even overriding `crates.io` via the `[patch]` manifest +section doesn't work - all dependencies will eventually find the only one copy. + +What is necessary - multiple copies of Rhai, one for each dependent crate that requires it, +together with their _unique_ [features] set intact. In other words, turning off Cargo's +crate merging feature _just for Rhai_. + +Unfortunately, as of this writing, there is no known method to achieve it. + +Therefore, moral of the story: avoid pulling in multiple crates that depend on Rhai. diff --git a/doc/src/start/features.md b/doc/src/start/features.md index 3201aa07..3e8067fa 100644 --- a/doc/src/start/features.md +++ b/doc/src/start/features.md @@ -52,3 +52,15 @@ no floating-point, is `Send + Sync` (so it can be safely used across threads), a nor loading external [modules]. This configuration is perfect for an expression parser in a 32-bit embedded system without floating-point hardware. + + +Caveat - Features Are Not Additive +--------------------------------- + +Rhai features are not strictly _additive_ - i.e. they do not only add optional functionalities. + +In fact, most features are _subtractive_ - i.e. they _remove_ functionalities. + +There is a reason for this design, because the _lack_ of a language feature by itself is a feature. + +See [here]({{rootUrl}}/patterns/multiple.md) for more details. diff --git a/src/api.rs b/src/api.rs index 8487370f..27a2d42b 100644 --- a/src/api.rs +++ b/src/api.rs @@ -898,21 +898,34 @@ impl Engine { /// 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. /// + /// # 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 /// /// ``` /// # fn main() -> Result<(), Box> { - /// use rhai::Engine; + /// use rhai::{Engine, Map}; /// /// 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.get("a").cloned().unwrap().cast::(), 123); - /// assert_eq!(map.get("b").cloned().unwrap().cast::(), 42); - /// assert_eq!(map.get("c").cloned().unwrap().cast::(), false); - /// assert_eq!(map.get("d").cloned().unwrap().cast::<()>(), ()); + /// assert_eq!(map["a"].as_int().unwrap(), 123); + /// assert_eq!(map["b"].as_int().unwrap(), 42); + /// assert!(map["d"].is::<()>()); + /// + /// let c = map["c"].read_lock::().unwrap(); + /// assert_eq!(c["x"].as_bool().unwrap(), false); /// # Ok(()) /// # } /// ``` @@ -921,7 +934,12 @@ impl Engine { let mut scope = Scope::new(); // Trims the JSON string and add a '#' in front - let scripts = ["#", json.trim()]; + let json = json.trim(); + let scripts = if json.starts_with(Token::MapStart.syntax().as_ref()) { + [json, ""] + } else { + ["#", json] + }; let stream = lex( &scripts, if has_null {