Add to_json for maps.
This commit is contained in:
parent
4f2764d233
commit
c3d013bddc
@ -15,6 +15,8 @@ Enhancements
|
||||
* `Module::eval_ast_as_new_raw` is made public as a low-level API.
|
||||
* Improper `switch` case condition syntax is now caught at parse time.
|
||||
* `Engine::parse_json` now natively handles nested JSON inputs (using a token remap filter) without needing to replace `{` with `#{`.
|
||||
* `to_json` is added to object maps to cheaply convert it to JSON format (`()` is mapped to `null`, all other data types must be supported by JSON)
|
||||
* A global function `format_map_as_json` is provided which is the same as `to_json` for object maps.
|
||||
|
||||
|
||||
Version 1.6.1
|
||||
|
@ -308,132 +308,4 @@ impl Engine {
|
||||
self.options.optimization_level,
|
||||
)
|
||||
}
|
||||
/// Parse a JSON string into an [object map][crate::Map].
|
||||
/// This is a light-weight alternative to using, say,
|
||||
/// [`serde_json`](https://crates.io/crates/serde_json) to deserialize the JSON.
|
||||
///
|
||||
/// Not available under `no_object`.
|
||||
///
|
||||
/// The JSON string must be an object hash. It cannot be a simple primitive value.
|
||||
///
|
||||
/// Set `has_null` to `true` in order to map `null` values to `()`.
|
||||
/// Setting it to `false` causes a syntax error for any `null` value.
|
||||
///
|
||||
/// JSON sub-objects are handled transparently.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
|
||||
/// use rhai::{Engine, Map};
|
||||
///
|
||||
/// let engine = Engine::new();
|
||||
///
|
||||
/// let map = engine.parse_json(r#"
|
||||
/// {
|
||||
/// "a": 123,
|
||||
/// "b": 42,
|
||||
/// "c": {
|
||||
/// "x": false,
|
||||
/// "y": true,
|
||||
/// "z": '$'
|
||||
/// },
|
||||
/// "d": null
|
||||
/// }"#, true)?;
|
||||
///
|
||||
/// assert_eq!(map.len(), 4);
|
||||
/// assert_eq!(map["a"].as_int().expect("a should exist"), 123);
|
||||
/// assert_eq!(map["b"].as_int().expect("b should exist"), 42);
|
||||
/// assert_eq!(map["d"].as_unit().expect("d should exist"), ());
|
||||
///
|
||||
/// let c = map["c"].read_lock::<Map>().expect("c should exist");
|
||||
/// assert_eq!(c["x"].as_bool().expect("x should be bool"), false);
|
||||
/// assert_eq!(c["y"].as_bool().expect("y should be bool"), true);
|
||||
/// assert_eq!(c["z"].as_char().expect("z should be char"), '$');
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
#[cfg(not(feature = "no_object"))]
|
||||
#[inline(always)]
|
||||
pub fn parse_json(
|
||||
&self,
|
||||
json: impl AsRef<str>,
|
||||
has_null: bool,
|
||||
) -> crate::RhaiResultOf<crate::Map> {
|
||||
use crate::{tokenizer::Token, LexError};
|
||||
|
||||
let scripts = [json.as_ref()];
|
||||
|
||||
let (stream, tokenizer_control) = self.lex_raw(
|
||||
&scripts,
|
||||
if has_null {
|
||||
Some(&|token, _, _| {
|
||||
match token {
|
||||
// `null` => `()`
|
||||
Token::Reserved(s) if &*s == "null" => Token::Unit,
|
||||
// `{` => `#{`
|
||||
Token::LeftBrace => Token::MapStart,
|
||||
// Disallowed syntax
|
||||
t @ (Token::Unit | Token::MapStart) => Token::LexError(
|
||||
LexError::ImproperSymbol(
|
||||
t.literal_syntax().to_string(),
|
||||
"".to_string(),
|
||||
)
|
||||
.into(),
|
||||
),
|
||||
Token::InterpolatedString(..) => Token::LexError(
|
||||
LexError::ImproperSymbol(
|
||||
"interpolated string".to_string(),
|
||||
"".to_string(),
|
||||
)
|
||||
.into(),
|
||||
),
|
||||
// All others
|
||||
_ => token,
|
||||
}
|
||||
})
|
||||
} else {
|
||||
Some(&|token, _, _| {
|
||||
match token {
|
||||
Token::Reserved(s) if &*s == "null" => Token::LexError(
|
||||
LexError::ImproperSymbol("null".to_string(), "".to_string()).into(),
|
||||
),
|
||||
// `{` => `#{`
|
||||
Token::LeftBrace => Token::MapStart,
|
||||
// Disallowed syntax
|
||||
t @ (Token::Unit | Token::MapStart) => Token::LexError(
|
||||
LexError::ImproperSymbol(
|
||||
t.literal_syntax().to_string(),
|
||||
"".to_string(),
|
||||
)
|
||||
.into(),
|
||||
),
|
||||
Token::InterpolatedString(..) => Token::LexError(
|
||||
LexError::ImproperSymbol(
|
||||
"interpolated string".to_string(),
|
||||
"".to_string(),
|
||||
)
|
||||
.into(),
|
||||
),
|
||||
// All others
|
||||
_ => token,
|
||||
}
|
||||
})
|
||||
},
|
||||
);
|
||||
|
||||
let mut state = ParseState::new(self, tokenizer_control);
|
||||
|
||||
let ast = self.parse_global_expr(
|
||||
&mut stream.peekable(),
|
||||
&mut state,
|
||||
&Scope::new(),
|
||||
#[cfg(not(feature = "no_optimize"))]
|
||||
OptimizationLevel::None,
|
||||
#[cfg(feature = "no_optimize")]
|
||||
OptimizationLevel::default(),
|
||||
)?;
|
||||
|
||||
self.eval_ast(&ast)
|
||||
}
|
||||
}
|
||||
|
175
src/api/json.rs
Normal file
175
src/api/json.rs
Normal file
@ -0,0 +1,175 @@
|
||||
//! Module that defines JSON manipulation functions for [`Engine`].
|
||||
#![cfg(not(feature = "no_object"))]
|
||||
|
||||
use crate::{Engine, LexError, Map, OptimizationLevel, ParseState, RhaiResultOf, Scope, Token};
|
||||
|
||||
impl Engine {
|
||||
/// Parse a JSON string into an [object map][Map].
|
||||
///
|
||||
/// This is a light-weight alternative to using, say, [`serde_json`](https://crates.io/crates/serde_json)
|
||||
/// to deserialize the JSON.
|
||||
///
|
||||
/// Not available under `no_object`.
|
||||
///
|
||||
/// The JSON string must be an object hash. It cannot be a simple primitive value.
|
||||
///
|
||||
/// Set `has_null` to `true` in order to map `null` values to `()`.
|
||||
/// Setting it to `false` causes a syntax error for any `null` value.
|
||||
///
|
||||
/// JSON sub-objects are handled transparently.
|
||||
///
|
||||
/// This function can be used together with [`format_map_as_json`] to work with JSON texts
|
||||
/// without using the [`serde`](https://crates.io/crates/serde) crate (which is heavy).
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// # fn main() -> Result<(), Box<rhai::EvalAltResult>> {
|
||||
/// use rhai::{Engine, Map};
|
||||
///
|
||||
/// let engine = Engine::new();
|
||||
///
|
||||
/// let map = engine.parse_json(r#"
|
||||
/// {
|
||||
/// "a": 123,
|
||||
/// "b": 42,
|
||||
/// "c": {
|
||||
/// "x": false,
|
||||
/// "y": true,
|
||||
/// "z": '$'
|
||||
/// },
|
||||
/// "d": null
|
||||
/// }"#, true)?;
|
||||
///
|
||||
/// assert_eq!(map.len(), 4);
|
||||
/// assert_eq!(map["a"].as_int().expect("a should exist"), 123);
|
||||
/// assert_eq!(map["b"].as_int().expect("b should exist"), 42);
|
||||
/// assert_eq!(map["d"].as_unit().expect("d should exist"), ());
|
||||
///
|
||||
/// let c = map["c"].read_lock::<Map>().expect("c should exist");
|
||||
/// assert_eq!(c["x"].as_bool().expect("x should be bool"), false);
|
||||
/// assert_eq!(c["y"].as_bool().expect("y should be bool"), true);
|
||||
/// assert_eq!(c["z"].as_char().expect("z should be char"), '$');
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
#[cfg(not(feature = "no_object"))]
|
||||
#[inline(always)]
|
||||
pub fn parse_json(&self, json: impl AsRef<str>, has_null: bool) -> RhaiResultOf<Map> {
|
||||
let scripts = [json.as_ref()];
|
||||
|
||||
let (stream, tokenizer_control) = self.lex_raw(
|
||||
&scripts,
|
||||
if has_null {
|
||||
Some(&|token, _, _| {
|
||||
match token {
|
||||
// `null` => `()`
|
||||
Token::Reserved(s) if &*s == "null" => Token::Unit,
|
||||
// `{` => `#{`
|
||||
Token::LeftBrace => Token::MapStart,
|
||||
// Disallowed syntax
|
||||
t @ (Token::Unit | Token::MapStart) => Token::LexError(
|
||||
LexError::ImproperSymbol(
|
||||
t.literal_syntax().to_string(),
|
||||
"".to_string(),
|
||||
)
|
||||
.into(),
|
||||
),
|
||||
Token::InterpolatedString(..) => Token::LexError(
|
||||
LexError::ImproperSymbol(
|
||||
"interpolated string".to_string(),
|
||||
"".to_string(),
|
||||
)
|
||||
.into(),
|
||||
),
|
||||
// All others
|
||||
_ => token,
|
||||
}
|
||||
})
|
||||
} else {
|
||||
Some(&|token, _, _| {
|
||||
match token {
|
||||
Token::Reserved(s) if &*s == "null" => Token::LexError(
|
||||
LexError::ImproperSymbol("null".to_string(), "".to_string()).into(),
|
||||
),
|
||||
// `{` => `#{`
|
||||
Token::LeftBrace => Token::MapStart,
|
||||
// Disallowed syntax
|
||||
t @ (Token::Unit | Token::MapStart) => Token::LexError(
|
||||
LexError::ImproperSymbol(
|
||||
t.literal_syntax().to_string(),
|
||||
"Invalid JSON syntax".to_string(),
|
||||
)
|
||||
.into(),
|
||||
),
|
||||
Token::InterpolatedString(..) => Token::LexError(
|
||||
LexError::ImproperSymbol(
|
||||
"interpolated string".to_string(),
|
||||
"Invalid JSON syntax".to_string(),
|
||||
)
|
||||
.into(),
|
||||
),
|
||||
// All others
|
||||
_ => token,
|
||||
}
|
||||
})
|
||||
},
|
||||
);
|
||||
|
||||
let mut state = ParseState::new(self, tokenizer_control);
|
||||
|
||||
let ast = self.parse_global_expr(
|
||||
&mut stream.peekable(),
|
||||
&mut state,
|
||||
&Scope::new(),
|
||||
#[cfg(not(feature = "no_optimize"))]
|
||||
OptimizationLevel::None,
|
||||
#[cfg(feature = "no_optimize")]
|
||||
OptimizationLevel::default(),
|
||||
)?;
|
||||
|
||||
self.eval_ast(&ast)
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the JSON representation of an [object map][Map].
|
||||
///
|
||||
/// This function can be used together with [`Engine::parse_json`] to work with JSON texts
|
||||
/// without using the [`serde`](https://crates.io/crates/serde) crate (which is heavy).
|
||||
///
|
||||
/// # Data types
|
||||
///
|
||||
/// Only the following data types should be kept inside the object map: [`INT`][crate::INT],
|
||||
/// [`FLOAT`][crate::FLOAT], [`ImmutableString`][crate::ImmutableString], `char`, `bool`, `()`,
|
||||
/// [`Array`][crate::Array], [`Map`].
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Data types not supported by JSON serialize into formats that may invalidate the result.
|
||||
pub fn format_map_as_json(map: &Map) -> String {
|
||||
let mut result = String::from('{');
|
||||
|
||||
for (key, value) in map {
|
||||
if result.len() > 1 {
|
||||
result.push(',');
|
||||
}
|
||||
|
||||
result.push_str(&format!("{:?}", key));
|
||||
result.push(':');
|
||||
|
||||
if let Some(val) = value.read_lock::<Map>() {
|
||||
result.push_str(&format_map_as_json(&*val));
|
||||
continue;
|
||||
}
|
||||
|
||||
if value.is::<()>() {
|
||||
result.push_str("null");
|
||||
} else {
|
||||
result.push_str(&format!("{:?}", value));
|
||||
}
|
||||
}
|
||||
|
||||
result.push('}');
|
||||
|
||||
result
|
||||
}
|
@ -8,6 +8,8 @@ pub mod run;
|
||||
|
||||
pub mod compile;
|
||||
|
||||
pub mod json;
|
||||
|
||||
pub mod files;
|
||||
|
||||
pub mod register;
|
||||
|
@ -237,6 +237,9 @@ pub type Blob = Vec<u8>;
|
||||
#[cfg(not(feature = "no_object"))]
|
||||
pub type Map = std::collections::BTreeMap<Identifier, Dynamic>;
|
||||
|
||||
#[cfg(not(feature = "no_object"))]
|
||||
pub use api::json::format_map_as_json;
|
||||
|
||||
#[cfg(not(feature = "no_module"))]
|
||||
pub use module::ModuleResolver;
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
use crate::engine::OP_EQUALS;
|
||||
use crate::plugin::*;
|
||||
use crate::{def_package, Dynamic, ImmutableString, Map, RhaiResultOf, INT};
|
||||
use crate::{def_package, format_map_as_json, Dynamic, ImmutableString, Map, RhaiResultOf, INT};
|
||||
#[cfg(feature = "no_std")]
|
||||
use std::prelude::v1::*;
|
||||
|
||||
@ -266,4 +266,26 @@ mod map_functions {
|
||||
map.values().cloned().collect()
|
||||
}
|
||||
}
|
||||
/// Return the JSON representation of the object map.
|
||||
///
|
||||
/// # Data types
|
||||
///
|
||||
/// Only the following data types should be kept inside the object map:
|
||||
/// `INT`, `FLOAT`, `ImmutableString`, `char`, `bool`, `()`, `Array`, `Map`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Data types not supported by JSON serialize into formats that may
|
||||
/// invalidate the result.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rhai
|
||||
/// let m = #{a:1, b:2, c:3};
|
||||
///
|
||||
/// print(m.to_json()); // prints {"a":1, "b":2, "c":3}
|
||||
/// ```
|
||||
pub fn to_json(map: &mut Map) -> String {
|
||||
format_map_as_json(map)
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user