Add docs on JSON parsing.

This commit is contained in:
Stephen Chung 2020-04-10 15:18:26 +08:00
parent adbfceb5be
commit 5d611d1674
3 changed files with 220 additions and 40 deletions

101
README.md
View File

@ -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`]`::<Map>` method (or [`eval_expression_with_scope`]`::<Map>` 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::<Map>(&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::<INT>(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];

View File

@ -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::<Vec<_>>()
});
#[cfg(not(feature = "no_index"))]
self.register_fn("values", |map: Map| {
map.into_iter().map(|(_, v)| v).collect::<Vec<_>>()
});

View File

@ -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::<Map>(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::<INT>(), 1);
assert_eq!(b.cast::<bool>(), true);
assert_eq!(c.cast::<String>(), "hello");
assert_eq!(
x.get("a")
.cloned()
.expect("should have property a")
.cast::<INT>(),
1
);
assert_eq!(
x.get("b")
.cloned()
.expect("should have property b")
.cast::<bool>(),
true
);
assert_eq!(
x.get("c$")
.cloned()
.expect("should have property c$")
.cast::<String>(),
"hello"
);
Ok(())
}
@ -91,13 +106,28 @@ fn test_map_return() -> Result<(), EvalAltResult> {
let engine = Engine::new();
let x = engine.eval::<Map>(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::<INT>(), 1);
assert_eq!(b.cast::<bool>(), true);
assert_eq!(c.cast::<String>(), "hello");
assert_eq!(
x.get("a")
.cloned()
.expect("should have property a")
.cast::<INT>(),
1
);
assert_eq!(
x.get("b")
.cloned()
.expect("should have property b")
.cast::<bool>(),
true
);
assert_eq!(
x.get("c$")
.cloned()
.expect("should have property c$")
.cast::<String>(),
"hello"
);
Ok(())
}
@ -107,20 +137,101 @@ fn test_map_for() -> Result<(), EvalAltResult> {
let engine = Engine::new();
assert_eq!(
engine.eval::<INT>(
r#"
let map = #{a: 1, b: true, c: 123.456};
let s = "";
engine
.eval::<String>(
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::<Map>(&mut scope, &("#".to_string() + json))?;
assert!(!map.contains_key("x"));
assert_eq!(
map.get("a")
.cloned()
.expect("should have property a")
.cast::<INT>(),
1
);
assert_eq!(
map.get("b")
.cloned()
.expect("should have property b")
.cast::<bool>(),
true
);
assert_eq!(
map.get("c")
.cloned()
.expect("should have property a")
.cast::<INT>(),
42
);
assert_eq!(
map.get("$d e f!")
.cloned()
.expect("should have property $d e f!")
.cast::<String>(),
"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::<String>(
&mut scope,
r#"
let s = "";
for key in keys(map) {
s += key;
}
s
"#
)?
.len(),
11
);
}
Ok(())
}