use std::cmp::Ordering;
use serde_json::{Number, Value};
pub const MAX_EXPRESSION_LEN: usize = 512;
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
#[error("{message}")]
pub struct ExprError {
message: String,
}
impl ExprError {
fn new(message: impl Into<String>) -> Self {
Self {
message: message.into(),
}
}
#[must_use]
pub fn message(&self) -> &str {
&self.message
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Expr {
root: Bool,
}
impl Expr {
#[must_use]
pub fn eval(&self, value: &Value) -> bool {
eval_bool(&self.root, value)
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Reference {
segments: Vec<Segment>,
}
impl Reference {
#[must_use]
pub fn resolve<'a>(&self, value: &'a Value) -> Option<&'a Value> {
resolve_path(&self.segments, value)
}
}
pub fn parse_reference(input: &str) -> Result<Reference, ExprError> {
let chars: Vec<char> = input.chars().collect();
if chars.len() > MAX_EXPRESSION_LEN {
return Err(ExprError::new(format!(
"reference is {} characters long, which exceeds the {MAX_EXPRESSION_LEN}-character limit",
chars.len()
)));
}
let tokens = lex(&chars)?;
let mut parser = Parser {
tokens: &tokens,
pos: 0,
end: chars.len(),
};
let operand = parser.parse_operand()?;
parser.expect_end()?;
match operand {
Operand::Path(segments) => Ok(Reference { segments }),
Operand::Literal(_) => Err(ExprError::new(
"a reference must be a path into the routed value, not a literal",
)),
}
}
#[derive(Clone, Debug, PartialEq)]
enum Bool {
Or(Box<Bool>, Box<Bool>),
And(Box<Bool>, Box<Bool>),
Not(Box<Bool>),
Compare {
left: Operand,
op: CmpOp,
right: Operand,
},
Truthy(Operand),
}
#[derive(Clone, Debug, PartialEq)]
enum Operand {
Literal(Value),
Path(Vec<Segment>),
}
#[derive(Clone, Debug, PartialEq)]
enum Segment {
Key(String),
Index(usize),
}
#[derive(Clone, Copy, Debug, PartialEq)]
enum CmpOp {
Eq,
Ne,
Lt,
Le,
Gt,
Ge,
}
pub fn parse(input: &str) -> Result<Expr, ExprError> {
let chars: Vec<char> = input.chars().collect();
if chars.len() > MAX_EXPRESSION_LEN {
return Err(ExprError::new(format!(
"expression is {} characters long, which exceeds the {MAX_EXPRESSION_LEN}-character limit",
chars.len()
)));
}
let tokens = lex(&chars)?;
let mut parser = Parser {
tokens: &tokens,
pos: 0,
end: chars.len(),
};
let root = parser.parse_or()?;
parser.expect_end()?;
Ok(Expr { root })
}
#[derive(Clone, Debug, PartialEq)]
struct Spanned {
token: Token,
at: usize,
}
#[derive(Clone, Debug, PartialEq)]
enum Token {
Ident(String),
Number(Number),
Str(String),
True,
False,
Null,
Dot,
LParen,
RParen,
Bang,
AndAnd,
OrOr,
EqEq,
NotEq,
Lt,
Le,
Gt,
Ge,
}
fn is_ident_start(c: char) -> bool {
c.is_ascii_alphabetic() || c == '_'
}
fn is_ident_continue(c: char) -> bool {
c.is_ascii_alphanumeric() || c == '_'
}
fn lex(chars: &[char]) -> Result<Vec<Spanned>, ExprError> {
let mut tokens = Vec::new();
let mut i = 0;
let len = chars.len();
while i < len {
let c = chars[i];
let at = i;
if c.is_whitespace() {
i += 1;
continue;
}
match c {
'(' => {
tokens.push(Spanned {
token: Token::LParen,
at,
});
i += 1;
}
')' => {
tokens.push(Spanned {
token: Token::RParen,
at,
});
i += 1;
}
'.' => {
tokens.push(Spanned {
token: Token::Dot,
at,
});
i += 1;
}
'!' => {
if i + 1 < len && chars[i + 1] == '=' {
tokens.push(Spanned {
token: Token::NotEq,
at,
});
i += 2;
} else {
tokens.push(Spanned {
token: Token::Bang,
at,
});
i += 1;
}
}
'=' => {
if i + 1 < len && chars[i + 1] == '=' {
tokens.push(Spanned {
token: Token::EqEq,
at,
});
i += 2;
} else {
return Err(at_char(at, "a lone `=`; did you mean `==`?"));
}
}
'<' => {
if i + 1 < len && chars[i + 1] == '=' {
tokens.push(Spanned {
token: Token::Le,
at,
});
i += 2;
} else {
tokens.push(Spanned {
token: Token::Lt,
at,
});
i += 1;
}
}
'>' => {
if i + 1 < len && chars[i + 1] == '=' {
tokens.push(Spanned {
token: Token::Ge,
at,
});
i += 2;
} else {
tokens.push(Spanned {
token: Token::Gt,
at,
});
i += 1;
}
}
'&' => {
if i + 1 < len && chars[i + 1] == '&' {
tokens.push(Spanned {
token: Token::AndAnd,
at,
});
i += 2;
} else {
return Err(at_char(at, "a lone `&`; did you mean `&&`?"));
}
}
'|' => {
if i + 1 < len && chars[i + 1] == '|' {
tokens.push(Spanned {
token: Token::OrOr,
at,
});
i += 2;
} else {
return Err(at_char(at, "a lone `|`; did you mean `||`?"));
}
}
'"' => {
let (token, next) = lex_string(chars, i)?;
tokens.push(Spanned { token, at });
i = next;
}
_ if c.is_ascii_digit()
|| (c == '-' && i + 1 < len && chars[i + 1].is_ascii_digit()) =>
{
let (token, next) = lex_number(chars, i)?;
tokens.push(Spanned { token, at });
i = next;
}
_ if is_ident_start(c) => {
let (token, next) = lex_ident(chars, i);
tokens.push(Spanned { token, at });
i = next;
}
_ => {
return Err(at_char(at, format!("an unexpected character `{c}`")));
}
}
}
Ok(tokens)
}
fn lex_string(chars: &[char], start: usize) -> Result<(Token, usize), ExprError> {
let len = chars.len();
let mut i = start + 1;
let mut value = String::new();
while i < len {
let c = chars[i];
if c == '"' {
return Ok((Token::Str(value), i + 1));
}
if c == '\\' {
i += 1;
if i >= len {
break;
}
match chars[i] {
'"' => value.push('"'),
'\\' => value.push('\\'),
'/' => value.push('/'),
'n' => value.push('\n'),
't' => value.push('\t'),
'r' => value.push('\r'),
other => {
return Err(at_char(
i,
format!("an unsupported string escape `\\{other}`"),
));
}
}
i += 1;
} else {
value.push(c);
i += 1;
}
}
Err(at_char(start, "an unterminated string literal"))
}
fn lex_number(chars: &[char], start: usize) -> Result<(Token, usize), ExprError> {
let len = chars.len();
let mut i = start;
if chars[i] == '-' {
i += 1;
}
while i < len && chars[i].is_ascii_digit() {
i += 1;
}
if i + 1 < len && chars[i] == '.' && chars[i + 1].is_ascii_digit() {
i += 1;
while i < len && chars[i].is_ascii_digit() {
i += 1;
}
}
let text: String = chars[start..i].iter().collect();
let number: Number = serde_json::from_str(&text)
.map_err(|_| at_char(start, format!("a malformed number `{text}`")))?;
Ok((Token::Number(number), i))
}
fn lex_ident(chars: &[char], start: usize) -> (Token, usize) {
let len = chars.len();
let mut i = start;
while i < len && is_ident_continue(chars[i]) {
i += 1;
}
let text: String = chars[start..i].iter().collect();
let token = match text.as_str() {
"true" => Token::True,
"false" => Token::False,
"null" => Token::Null,
_ => Token::Ident(text),
};
(token, i)
}
fn at_char(position: usize, what: impl std::fmt::Display) -> ExprError {
ExprError::new(format!("found {what} at character {position}"))
}
struct Parser<'a> {
tokens: &'a [Spanned],
pos: usize,
end: usize,
}
impl Parser<'_> {
fn peek(&self) -> Option<&Token> {
self.tokens.get(self.pos).map(|s| &s.token)
}
fn position(&self) -> usize {
self.tokens.get(self.pos).map_or(self.end, |s| s.at)
}
fn advance(&mut self) {
self.pos += 1;
}
fn expect_end(&self) -> Result<(), ExprError> {
match self.peek() {
None => Ok(()),
Some(_) => Err(at_char(
self.position(),
"an unexpected trailing token; the expression already ended",
)),
}
}
fn parse_or(&mut self) -> Result<Bool, ExprError> {
let mut left = self.parse_and()?;
while matches!(self.peek(), Some(Token::OrOr)) {
self.advance();
let right = self.parse_and()?;
left = Bool::Or(Box::new(left), Box::new(right));
}
Ok(left)
}
fn parse_and(&mut self) -> Result<Bool, ExprError> {
let mut left = self.parse_unary()?;
while matches!(self.peek(), Some(Token::AndAnd)) {
self.advance();
let right = self.parse_unary()?;
left = Bool::And(Box::new(left), Box::new(right));
}
Ok(left)
}
fn parse_unary(&mut self) -> Result<Bool, ExprError> {
if matches!(self.peek(), Some(Token::Bang)) {
self.advance();
let inner = self.parse_unary()?;
Ok(Bool::Not(Box::new(inner)))
} else {
self.parse_atom()
}
}
fn parse_atom(&mut self) -> Result<Bool, ExprError> {
if matches!(self.peek(), Some(Token::LParen)) {
self.advance();
let inner = self.parse_or()?;
match self.peek() {
Some(Token::RParen) => {
self.advance();
Ok(inner)
}
_ => Err(at_char(self.position(), "a missing closing `)`")),
}
} else {
self.parse_comparison()
}
}
fn parse_comparison(&mut self) -> Result<Bool, ExprError> {
let left = self.parse_operand()?;
let op = match self.peek() {
Some(Token::EqEq) => CmpOp::Eq,
Some(Token::NotEq) => CmpOp::Ne,
Some(Token::Lt) => CmpOp::Lt,
Some(Token::Le) => CmpOp::Le,
Some(Token::Gt) => CmpOp::Gt,
Some(Token::Ge) => CmpOp::Ge,
_ => return Ok(Bool::Truthy(left)),
};
self.advance();
let right = self.parse_operand()?;
Ok(Bool::Compare { left, op, right })
}
fn parse_operand(&mut self) -> Result<Operand, ExprError> {
match self.peek() {
Some(Token::Number(n)) => {
let value = Operand::Literal(Value::Number(n.clone()));
self.advance();
Ok(value)
}
Some(Token::Str(s)) => {
let value = Operand::Literal(Value::String(s.clone()));
self.advance();
Ok(value)
}
Some(Token::True) => {
self.advance();
Ok(Operand::Literal(Value::Bool(true)))
}
Some(Token::False) => {
self.advance();
Ok(Operand::Literal(Value::Bool(false)))
}
Some(Token::Null) => {
self.advance();
Ok(Operand::Literal(Value::Null))
}
Some(Token::Ident(name)) => {
let name = name.clone();
self.advance();
self.parse_path(name)
}
_ => Err(at_char(
self.position(),
"a value or path where one was required",
)),
}
}
fn parse_path(&mut self, first: String) -> Result<Operand, ExprError> {
let mut segments = vec![Segment::Key(first)];
while matches!(self.peek(), Some(Token::Dot)) {
self.advance();
match self.peek() {
Some(Token::Ident(name)) => {
segments.push(Segment::Key(name.clone()));
self.advance();
}
Some(Token::Number(n)) => {
let index = n
.as_u64()
.and_then(|v| usize::try_from(v).ok())
.ok_or_else(|| {
at_char(
self.position(),
"a path segment that is not a non-negative integer index",
)
})?;
segments.push(Segment::Index(index));
self.advance();
}
_ => {
return Err(at_char(self.position(), "a missing path segment after `.`"));
}
}
}
Ok(Operand::Path(segments))
}
}
fn eval_bool(node: &Bool, root: &Value) -> bool {
match node {
Bool::Or(a, b) => eval_bool(a, root) || eval_bool(b, root),
Bool::And(a, b) => eval_bool(a, root) && eval_bool(b, root),
Bool::Not(a) => !eval_bool(a, root),
Bool::Compare { left, op, right } => eval_compare(left, *op, right, root),
Bool::Truthy(operand) => matches!(resolve(operand, root), Some(Value::Bool(true))),
}
}
fn resolve<'a>(operand: &'a Operand, root: &'a Value) -> Option<&'a Value> {
match operand {
Operand::Literal(value) => Some(value),
Operand::Path(segments) => resolve_path(segments, root),
}
}
fn resolve_path<'a>(segments: &[Segment], root: &'a Value) -> Option<&'a Value> {
let mut current = root;
for segment in segments {
current = match (current, segment) {
(Value::Object(map), Segment::Key(key)) => map.get(key)?,
(Value::Array(items), Segment::Index(index)) => items.get(*index)?,
_ => return None,
};
}
Some(current)
}
fn eval_compare(left: &Operand, op: CmpOp, right: &Operand, root: &Value) -> bool {
let (Some(l), Some(r)) = (resolve(left, root), resolve(right, root)) else {
return false;
};
match op {
CmpOp::Eq => values_equal(l, r),
CmpOp::Ne => !values_equal(l, r),
CmpOp::Lt => matches!(order(l, r), Some(Ordering::Less)),
CmpOp::Le => matches!(order(l, r), Some(Ordering::Less | Ordering::Equal)),
CmpOp::Gt => matches!(order(l, r), Some(Ordering::Greater)),
CmpOp::Ge => matches!(order(l, r), Some(Ordering::Greater | Ordering::Equal)),
}
}
fn values_equal(l: &Value, r: &Value) -> bool {
match (l, r) {
(Value::Number(a), Value::Number(b)) => number_cmp(a, b) == Some(Ordering::Equal),
_ => l == r,
}
}
fn order(l: &Value, r: &Value) -> Option<Ordering> {
match (l, r) {
(Value::Number(a), Value::Number(b)) => number_cmp(a, b),
(Value::String(a), Value::String(b)) => Some(a.cmp(b)),
_ => None,
}
}
fn number_cmp(a: &Number, b: &Number) -> Option<Ordering> {
if let (Some(x), Some(y)) = (as_i128(a), as_i128(b)) {
return Some(x.cmp(&y));
}
match (a.as_f64(), b.as_f64()) {
(Some(x), Some(y)) => x.partial_cmp(&y),
_ => None,
}
}
fn as_i128(n: &Number) -> Option<i128> {
n.as_u64()
.map(i128::from)
.or_else(|| n.as_i64().map(i128::from))
}
#[cfg(test)]
mod tests {
use super::*;
use proptest::prelude::*;
use serde_json::json;
fn eval(expr: &str, value: &Value) -> bool {
parse(expr)
.unwrap_or_else(|e| panic!("`{expr}` should parse: {e}"))
.eval(value)
}
#[test]
fn sample_score_expression() {
assert!(eval("score > 0.8", &json!({"score": 0.9})));
assert!(!eval("score > 0.8", &json!({"score": 0.5})));
}
#[test]
fn every_comparison_operator() {
let v = json!({"n": 5});
assert!(eval("n == 5", &v));
assert!(!eval("n == 4", &v));
assert!(eval("n != 4", &v));
assert!(!eval("n != 5", &v));
assert!(eval("n < 6", &v));
assert!(!eval("n < 5", &v));
assert!(eval("n <= 5", &v));
assert!(eval("n > 4", &v));
assert!(!eval("n > 5", &v));
assert!(eval("n >= 5", &v));
}
#[test]
fn boolean_operators() {
let v = json!({"a": true, "b": false});
assert!(eval("a && !b", &v));
assert!(eval("a || b", &v));
assert!(!eval("!a", &v));
assert!(eval("!b", &v));
assert!(!eval("a && b", &v));
}
#[test]
fn and_binds_tighter_than_or() {
let v = json!({});
assert!(eval("true || false && false", &v));
assert!(!eval("(true || false) && false", &v));
}
#[test]
fn not_binds_tighter_than_and() {
let v = json!({"a": false, "b": true});
assert!(eval("!a && b", &v));
}
#[test]
fn comparison_binds_tighter_than_not() {
assert!(eval("!score > 0.8", &json!({"score": 0.5})));
assert!(!eval("!score > 0.8", &json!({"score": 0.9})));
}
#[test]
fn parentheses_group_boolean_expressions() {
let v = json!({"a": true, "b": false, "c": true});
assert!(eval("a && (b || c)", &v));
assert!(!eval("(a && b) || (b && c)", &v));
}
#[test]
fn comparisons_do_not_chain() {
assert!(parse("1 < 2 < 3").is_err());
}
#[test]
fn a_boolean_group_is_not_a_comparison_operand() {
assert!(parse("(a > b) > c").is_err());
}
#[test]
fn nested_and_indexed_paths() {
let v = json!({"output": {"score": 0.9}, "items": [{"score": 1}, {"score": 2}]});
assert!(eval("output.score > 0.8", &v));
assert!(eval("items.0.score == 1", &v));
assert!(eval("items.1.score == 2", &v));
}
#[test]
fn bare_path_is_truthy_only_for_boolean_true() {
assert!(eval("flag", &json!({"flag": true})));
assert!(!eval("flag", &json!({"flag": false})));
assert!(!eval("flag", &json!({"flag": 1})));
assert!(!eval("flag", &json!({"flag": "true"})));
assert!(!eval("flag", &json!({})));
}
#[test]
fn missing_path_makes_every_comparison_false() {
let v = json!({});
assert!(!eval("missing == 1", &v));
assert!(!eval("missing != 1", &v));
assert!(!eval("missing < 1", &v));
assert!(!eval("missing > 1", &v));
assert!(eval("!(missing > 1)", &v));
}
#[test]
fn missing_is_distinct_from_null() {
assert!(eval("x == null", &json!({"x": null})));
assert!(!eval("missing == null", &json!({})));
}
#[test]
fn descending_into_a_non_container_is_missing() {
let v = json!({"x": 5});
assert!(!eval("x.y == 1", &v));
assert!(!eval("x.0 == 1", &v));
}
#[test]
fn cross_type_equality_is_never_equal() {
assert!(!eval("x == 5", &json!({"x": "5"})));
assert!(eval("x != 5", &json!({"x": "5"})));
assert!(!eval("x == 1", &json!({"x": true})));
}
#[test]
fn cross_type_ordering_is_false() {
assert!(!eval("x < 5", &json!({"x": "5"})));
assert!(!eval("x > 5", &json!({"x": "5"})));
assert!(!eval("x < 5", &json!({"x": true})));
assert!(!eval("x < 5", &json!({"x": null})));
}
#[test]
fn strings_order_lexicographically() {
assert!(eval("x < \"b\"", &json!({"x": "a"})));
assert!(!eval("x < \"a\"", &json!({"x": "b"})));
assert!(eval("x == \"hi\"", &json!({"x": "hi"})));
}
#[test]
fn integer_and_float_equality() {
assert!(eval("x == 1", &json!({"x": 1.0})));
assert!(eval("x == 1.0", &json!({"x": 1})));
assert!(eval("x >= 1", &json!({"x": 1.0})));
}
#[test]
fn large_integers_compare_exactly() {
let big = json!({"a": 9_007_199_254_740_993_i64});
assert!(eval("a == 9007199254740993", &big));
assert!(!eval("a == 9007199254740992", &big));
}
#[test]
fn negative_and_signed_comparison() {
assert!(eval("x < 0", &json!({"x": -3})));
assert!(eval("x == -0.5", &json!({"x": -0.5})));
assert!(eval("x > y", &json!({"x": 1, "y": -1})));
}
#[test]
fn the_length_cap_rejects_longer_input_and_names_it() {
let ok = "a".repeat(MAX_EXPRESSION_LEN);
assert!(parse(&ok).is_ok());
let too_long = "a".repeat(MAX_EXPRESSION_LEN + 1);
let err = parse(&too_long).expect_err("over the cap");
assert!(
err.message().contains(&MAX_EXPRESSION_LEN.to_string()),
"names the cap: {err}"
);
}
#[test]
fn assorted_syntax_errors() {
for bad in [
"",
"&&",
"a &&",
"a && (b",
"== 5",
"a = 5",
"a & b",
"a | b",
"1.2.3 == 1",
"\"unterminated",
"a.",
"a.-1 == 1",
] {
assert!(parse(bad).is_err(), "`{bad}` should be a parse error");
}
}
#[test]
fn a_reference_resolves_a_top_level_and_nested_array() {
let top = parse_reference("items").expect("`items` parses");
assert_eq!(
top.resolve(&json!({"items": [1, 2, 3]})),
Some(&json!([1, 2, 3]))
);
let nested = parse_reference("output.items").expect("`output.items` parses");
assert_eq!(
nested.resolve(&json!({"output": {"items": ["a"]}})),
Some(&json!(["a"]))
);
let indexed = parse_reference("results.0.items").expect("indexed path parses");
assert_eq!(
indexed.resolve(&json!({"results": [{"items": [true]}]})),
Some(&json!([true]))
);
}
#[test]
fn a_missing_reference_resolves_to_none() {
let reference = parse_reference("items").expect("parses");
assert_eq!(reference.resolve(&json!({})), None);
assert_eq!(reference.resolve(&json!({"other": [1]})), None);
let deep = parse_reference("x.items").expect("parses");
assert_eq!(deep.resolve(&json!({"x": 5})), None);
}
#[test]
fn a_reference_may_name_any_json_value_not_only_arrays() {
let reference = parse_reference("value").expect("parses");
assert_eq!(reference.resolve(&json!({"value": 5})), Some(&json!(5)));
assert_eq!(
reference.resolve(&json!({"value": {"k": 1}})),
Some(&json!({"k": 1}))
);
}
#[test]
fn a_literal_or_malformed_reference_is_rejected() {
assert!(
parse_reference("5").is_err(),
"a bare literal is not a path"
);
assert!(
parse_reference("\"x\"").is_err(),
"a string literal is not a path"
);
assert!(parse_reference("true").is_err(), "a keyword is not a path");
assert!(
parse_reference("items ==").is_err(),
"trailing tokens rejected"
);
assert!(
parse_reference("items.").is_err(),
"a dangling dot is rejected"
);
assert!(
parse_reference("").is_err(),
"an empty reference is rejected"
);
}
proptest! {
#[test]
fn parsing_never_panics(input in ".*") {
let _ = parse(&input);
}
#[test]
fn near_miss_parsing_never_panics(
input in "[a-z0-9_. ()!&|<>=\"'.-]{0,80}"
) {
let _ = parse(&input);
}
#[test]
fn cap_always_holds(input in "a{500,700}") {
let result = parse(&input);
if input.chars().count() > MAX_EXPRESSION_LEN {
prop_assert!(result.is_err());
}
}
#[test]
fn eval_never_panics(expr in expr_strategy(), value in json_strategy()) {
if let Ok(parsed) = parse(&expr) {
let _ = parsed.eval(&value);
}
}
}
fn expr_strategy() -> impl Strategy<Value = String> {
let leaf = prop_oneof![
Just("score".to_string()),
Just("output.score".to_string()),
Just("items.0.score".to_string()),
Just("flag".to_string()),
Just("0.8".to_string()),
Just("5".to_string()),
Just("-1".to_string()),
Just("true".to_string()),
Just("null".to_string()),
Just("\"hi\"".to_string()),
];
let comparison = (leaf.clone(), "==|!=|<|<=|>|>=", leaf.clone())
.prop_map(|(l, op, r)| format!("{l} {op} {r}"));
let atom = prop_oneof![leaf, comparison];
atom.prop_recursive(4, 32, 4, |inner| {
prop_oneof![
inner.clone().prop_map(|e| format!("!{e}")),
inner.clone().prop_map(|e| format!("({e})")),
(inner.clone(), inner.clone()).prop_map(|(a, b)| format!("{a} && {b}")),
(inner.clone(), inner).prop_map(|(a, b)| format!("{a} || {b}")),
]
})
}
fn json_strategy() -> impl Strategy<Value = Value> {
let leaf = prop_oneof![
Just(Value::Null),
any::<bool>().prop_map(Value::Bool),
any::<i64>().prop_map(|n| json!(n)),
any::<f64>()
.prop_filter("finite", |f| f.is_finite())
.prop_map(|f| json!(f)),
".*".prop_map(Value::String),
];
leaf.prop_recursive(3, 16, 4, |inner| {
prop_oneof![
prop::collection::vec(inner.clone(), 0..4).prop_map(Value::Array),
prop::collection::hash_map("[a-z]{1,5}", inner, 0..4)
.prop_map(|m| Value::Object(m.into_iter().collect())),
]
})
}
}