From 5d611d167484dc320d1445953352acc937f0f68c Mon Sep 17 00:00:00 2001 From: Stephen Chung Date: Fri, 10 Apr 2020 15:18:26 +0800 Subject: [PATCH] Add docs on JSON parsing. --- README.md | 101 +++++++++++++++++++++++++------ src/builtin.rs | 2 + tests/maps.rs | 157 +++++++++++++++++++++++++++++++++++++++++-------- 3 files changed, 220 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index 9393fd1b..b200f5ec 100644 --- a/README.md +++ b/README.md @@ -63,7 +63,7 @@ Optional features | ------------- | ------------------------------------------------------------------------------------------------------------------------------------- | | `unchecked` | Exclude arithmetic checking (such as overflows and division by zero). Beware that a bad script may panic the entire system! | | `no_function` | Disable script-defined functions if not needed. | -| `no_index` | Disable arrays and indexing features if not needed. | +| `no_index` | Disable [arrays] and indexing features if not needed. | | `no_object` | Disable support for custom types and objects. | | `no_float` | Disable floating-point numbers and math if not needed. | | `no_optimize` | Disable the script optimizer. | @@ -102,7 +102,7 @@ A number of examples can be found in the `examples` folder: | Example | Description | | ------------------------------------------------------------------ | --------------------------------------------------------------------------- | -| [`arrays_and_structs`](examples/arrays_and_structs.rs) | demonstrates registering a new type to Rhai and the usage of arrays on it | +| [`arrays_and_structs`](examples/arrays_and_structs.rs) | demonstrates registering a new type to Rhai and the usage of [arrays] on it | | [`custom_types_and_methods`](examples/custom_types_and_methods.rs) | shows how to register a type and methods for it | | [`hello`](examples/hello.rs) | simple example that evaluates an expression and prints the result | | [`no_std`](examples/no_std.rs) | example to test out `no-std` builds | @@ -127,7 +127,7 @@ There are also a number of examples scripts that showcase Rhai's features, all i | Language feature scripts | Description | | ---------------------------------------------------- | ------------------------------------------------------------- | -| [`array.rhai`](scripts/array.rhai) | arrays in Rhai | +| [`array.rhai`](scripts/array.rhai) | [arrays] in Rhai | | [`assignment.rhai`](scripts/assignment.rhai) | variable declarations | | [`comments.rhai`](scripts/comments.rhai) | just comments | | [`for1.rhai`](scripts/for1.rhai) | for loops | @@ -139,7 +139,7 @@ There are also a number of examples scripts that showcase Rhai's features, all i | [`op1.rhai`](scripts/op1.rhai) | just a simple addition | | [`op2.rhai`](scripts/op2.rhai) | simple addition and multiplication | | [`op3.rhai`](scripts/op3.rhai) | change evaluation order with parenthesis | -| [`string.rhai`](scripts/string.rhai) | string operations | +| [`string.rhai`](scripts/string.rhai) | [string] operations | | [`while.rhai`](scripts/while.rhai) | while loop | | Example scripts | Description | @@ -324,6 +324,9 @@ engine. Evaluate expressions only ------------------------- +[`eval_expression`]: #evaluate-expressions-only +[`eval_expression_with_scope`]: #evaluate-expressions-only + Sometimes a use case does not require a full-blown scripting _language_, but only needs to evaluate _expressions_. In these cases, use the `compile_expression` and `eval_expression` methods or their `_with_scope` variants. @@ -375,7 +378,7 @@ This is useful on some 32-bit systems where using 64-bit integers incurs a perfo If no floating-point is needed or supported, use the [`no_float`] feature to remove it. -The `to_string` function converts a standard type into a string for display purposes. +The `to_string` function converts a standard type into a [string] for display purposes. The `type_of` function detects the actual type of a value. This is useful because all variables are [`Dynamic`] in nature. @@ -429,8 +432,8 @@ if type_of(mystery) == "i64" { } ``` -In Rust, sometimes a `Dynamic` forms part of a returned value - a good example is an array with `Dynamic` elements, -or an object map with `Dynamic` property values. To get the _real_ values, the actual value types _must_ be known in advance. +In Rust, sometimes a `Dynamic` forms part of a returned value - a good example is an [array] with `Dynamic` elements, +or an [object map] with `Dynamic` property values. To get the _real_ values, the actual value types _must_ be known in advance. There is no easy way for Rust to decide, at run-time, what type the `Dynamic` value is (short of using the `type_name` function and match against the name). @@ -1063,6 +1066,10 @@ The following standard functions (defined in the standard library but excluded i Strings and Chars ----------------- +[string]: #strings-and-chars +[strings]: #strings-and-chars +[char]: #strings-and-chars + String and char literals follow C-style formatting, with support for Unicode ('`\u`_xxxx_' or '`\U`_xxxxxxxx_') and hex ('`\x`_xx_') escape sequences. @@ -1125,6 +1132,8 @@ record == "Bob X. Davis: age 42 ❤\n"; 'C' in record == false; ``` +### Built-in functions + The following standard functions (defined in the standard library but excluded if using a [raw `Engine`]) operate on strings: | Function | Description | @@ -1138,7 +1147,7 @@ The following standard functions (defined in the standard library but excluded i | `replace` | replaces a substring with another | | `trim` | trims the string | -Examples: +### Examples ```rust let full_name == " Bob C. Davis "; @@ -1170,6 +1179,10 @@ full_name.len() == 0; Arrays ------ +[array]: #arrays +[arrays]: #arrays +[`Array`]: #arrays + Arrays are first-class citizens in Rhai. Like C, arrays are accessed with zero-based, non-negative integer indices. Array literals are built within square brackets '`[`' ... '`]`' and separated by commas '`,`'. All elements stored in an array are [`Dynamic`], and the array can freely grow or shrink with elements added or removed. @@ -1178,6 +1191,8 @@ The Rust type of a Rhai array is `rhai::Array`. [`type_of()`] an array returns ` Arrays are disabled via the [`no_index`] feature. +### Built-in functions + The following functions (defined in the standard library but excluded if using a [raw `Engine`]) operate on arrays: | Function | Description | @@ -1192,7 +1207,7 @@ The following functions (defined in the standard library but excluded if using a | `clear` | empties the array | | `truncate` | cuts off the array at exactly a specified length (discarding all subsequent elements) | -Examples: +### Examples ```rust let y = [1, 2, 3]; // array literal with 3 elements @@ -1259,14 +1274,17 @@ engine.register_fn("push", |list: &mut Array, item: MyType| list.push(Box::new(i Object maps ----------- +[object map]: #object-maps +[object maps]: #object-maps + Object maps are dictionaries. Properties are all [`Dynamic`] and can be freely added and retrieved. Object map literals are built within braces '`#{`' ... '`}`' (_name_ `:` _value_ syntax similar to Rust) and separated by commas '`,`'. The property _name_ can be a simple variable name following the same -naming rules as [variables], or an arbitrary string literal. +naming rules as [variables], or an arbitrary [string] literal. Property values can be accessed via the dot notation (_object_ `.` _property_) or index notation (_object_ `[` _property_ `]`). The dot notation allows only property names that follow the same naming rules as [variables]. -The index notation allows setting/getting properties of arbitrary names (even the empty string). +The index notation allows setting/getting properties of arbitrary names (even the empty [string]). **Important:** Trying to read a non-existent property returns [`()`] instead of causing an error. @@ -1274,6 +1292,8 @@ The Rust type of a Rhai object map is `rhai::Map`. [`type_of()`] an object map r Object maps are disabled via the [`no_object`] feature. +### Built-in functions + The following functions (defined in the standard library but excluded if using a [raw `Engine`]) operate on object maps: | Function | Description | @@ -1283,10 +1303,10 @@ The following functions (defined in the standard library but excluded if using a | `clear` | empties the object map | | `mixin` | mixes in all the properties of the second object map to the first (values of properties with the same names replace the existing values) | | `+` operator | merges the first object map with the second | -| `keys` | returns an array of all the property names (in random order) | -| `values` | returns an array of all the property values (in random order) | +| `keys` | returns an [array] of all the property names (in random order) | +| `values` | returns an [array] of all the property values (in random order) | -Examples: +### Examples ```rust let y = #{ // object map literal with 3 properties @@ -1347,13 +1367,60 @@ y.clear(); // empty the object map print(y.len()); // prints 0 ``` +### Parsing from JSON + +The syntax for an object map is extremely similar to JSON, with the exception of `null` values which can +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! + +JSON numbers are all floating-point while Rhai supports integers (`INT`) and floating-point (`FLOAT`) if +the [`no_float`] feature is not turned on. Most common generators of JSON data distinguish between +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. + +Use the [`eval_expression`]`::` method (or [`eval_expression_with_scope`]`::` in order to +handle `null` values) to parse a piece of JSON (with the hash character `#` attached) into an object map: + +```rust +// JSON string - notice that JSON property names are always quoted +// notice also that comments are acceptable within the JSON string +let json = r#"{ + "a": 1, // <- this is an integer number + "b": true, + "c": 123.0, // <- this is a floating-point number + "$d e f!": "hello", // <- any text can be a property name + "^^^!!!": [1,42,"999"], // <- value can be array or another hash + "z": null // <- JSON 'null' value + } +"#; + +// Create a new scope +let mut scope = Scope::new(); +scope.push_constant("null", ()); // map 'null' to '()' + +// Parse the JSON expression as an object map by attaching '#' in front +let expr = format!("#{}", json); +let map = engine.eval_expression_with_scope::(&mut scope, expr)?; + +map.len() == 6; // 'map' contains all properties int the JSON string + +// Push the map back into the scope +scope.clear(); +scope.push("map", map); + +let result = engine.eval_with_scope::(r#"map["^^^!!!"].len()"#)?; + +result == 3; // the object map is used in a script +``` + Comparison operators -------------------- Comparing most values of the same data type work out-of-the-box for standard types supported by the system. However, if using a [raw `Engine`], comparisons can only be made between restricted system types - -`INT` (`i64` or `i32` depending on [`only_i32`] and [`only_i64`]), `f64` (if not [`no_float`]), string, array, `bool`, `char`. +`INT` (`i64` or `i32` depending on [`only_i32`] and [`only_i64`]), `f64` (if not [`no_float`]), [string], [array], `bool`, `char`. ```rust 42 == 42; // true @@ -1411,7 +1478,7 @@ number <<= 2; // number = number << 2 number >>= 1; // number = number >> 1 ``` -The `+=` operator can also be used to build strings: +The `+=` operator can also be used to build [strings]: ```rust let my_str = "abc"; @@ -1488,7 +1555,7 @@ loop { `for` loops ----------- -Iterating through a range or an array is provided by the `for` ... `in` loop. +Iterating through a range or an [array] is provided by the `for` ... `in` loop. ```rust let array = [1, 3, 5, 7, 9, 42]; diff --git a/src/builtin.rs b/src/builtin.rs index f63f4fd5..dfc709c2 100644 --- a/src/builtin.rs +++ b/src/builtin.rs @@ -631,12 +631,14 @@ impl Engine<'_> { }); // Register map access functions + #[cfg(not(feature = "no_index"))] self.register_fn("keys", |map: Map| { map.into_iter() .map(|(k, _)| k.into_dynamic()) .collect::>() }); + #[cfg(not(feature = "no_index"))] self.register_fn("values", |map: Map| { map.into_iter().map(|(_, v)| v).collect::>() }); diff --git a/tests/maps.rs b/tests/maps.rs index b979f331..f6bc45db 100644 --- a/tests/maps.rs +++ b/tests/maps.rs @@ -1,6 +1,6 @@ #![cfg(not(feature = "no_object"))] -use rhai::{AnyExt, Engine, EvalAltResult, Map, INT}; +use rhai::{AnyExt, Engine, EvalAltResult, Map, Scope, INT}; #[test] fn test_map_indexing() -> Result<(), EvalAltResult> { @@ -75,13 +75,28 @@ fn test_map_assign() -> Result<(), EvalAltResult> { let engine = Engine::new(); let x = engine.eval::(r#"let x = #{a: 1, b: true, "c$": "hello"}; x"#)?; - let a = x.get("a").cloned().expect("should have property a"); - let b = x.get("b").cloned().expect("should have property b"); - let c = x.get("c$").cloned().expect("should have property c$"); - assert_eq!(a.cast::(), 1); - assert_eq!(b.cast::(), true); - assert_eq!(c.cast::(), "hello"); + assert_eq!( + x.get("a") + .cloned() + .expect("should have property a") + .cast::(), + 1 + ); + assert_eq!( + x.get("b") + .cloned() + .expect("should have property b") + .cast::(), + true + ); + assert_eq!( + x.get("c$") + .cloned() + .expect("should have property c$") + .cast::(), + "hello" + ); Ok(()) } @@ -91,13 +106,28 @@ fn test_map_return() -> Result<(), EvalAltResult> { let engine = Engine::new(); let x = engine.eval::(r#"#{a: 1, b: true, "c$": "hello"}"#)?; - let a = x.get("a").cloned().expect("should have property a"); - let b = x.get("b").cloned().expect("should have property b"); - let c = x.get("c$").cloned().expect("should have property c$"); - assert_eq!(a.cast::(), 1); - assert_eq!(b.cast::(), true); - assert_eq!(c.cast::(), "hello"); + assert_eq!( + x.get("a") + .cloned() + .expect("should have property a") + .cast::(), + 1 + ); + assert_eq!( + x.get("b") + .cloned() + .expect("should have property b") + .cast::(), + true + ); + assert_eq!( + x.get("c$") + .cloned() + .expect("should have property c$") + .cast::(), + "hello" + ); Ok(()) } @@ -107,20 +137,101 @@ fn test_map_for() -> Result<(), EvalAltResult> { let engine = Engine::new(); assert_eq!( - engine.eval::( - r#" - let map = #{a: 1, b: true, c: 123.456}; - let s = ""; + engine + .eval::( + r#" + let map = #{a: 1, b_x: true, "$c d e!": "hello"}; + let s = ""; - for key in keys(map) { - s += key; - } + for key in keys(map) { + s += key; + } - s.len() + s "# - )?, - 3 + )? + .len(), + 11 ); Ok(()) } + +#[test] +/// Because a Rhai object map literal is almost the same as JSON, +/// it is possible to convert from JSON into a Rhai object map. +fn test_map_json() -> Result<(), EvalAltResult> { + 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 map = engine.eval_expression_with_scope::(&mut scope, &("#".to_string() + json))?; + + assert!(!map.contains_key("x")); + + assert_eq!( + map.get("a") + .cloned() + .expect("should have property a") + .cast::(), + 1 + ); + assert_eq!( + map.get("b") + .cloned() + .expect("should have property b") + .cast::(), + true + ); + assert_eq!( + map.get("c") + .cloned() + .expect("should have property a") + .cast::(), + 42 + ); + assert_eq!( + map.get("$d e f!") + .cloned() + .expect("should have property $d e f!") + .cast::(), + "hello" + ); + assert_eq!( + map.get("z") + .cloned() + .expect("should have property z") + .cast::<()>(), + () + ); + + #[cfg(not(feature = "no_index"))] + { + scope.clear(); + scope.push_constant("map", map); + + assert_eq!( + engine + .eval_with_scope::( + &mut scope, + r#" + let s = ""; + + for key in keys(map) { + s += key; + } + + s + "# + )? + .len(), + 11 + ); + } + + Ok(()) +}