#[cfg(test)]
mod tests {
use super::super::common::vm_query;
use crate::compile::compiler::Compiler;
use crate::data::value::Val;
use crate::parse::ast::{Expr, PathStep};
use crate::parse::parser::parse;
use crate::vm::{Opcode, VM};
use indexmap::IndexMap;
use serde_json::json;
use std::sync::Arc;
#[test]
fn patch_simple_field_replace() {
let doc = json!({"name": "Alice", "age": 30});
let r = vm_query(r#"patch $ { name: "Bob" }"#, &doc).unwrap();
assert_eq!(r, json!({"name": "Bob", "age": 30}));
}
#[test]
fn patch_nested_field_replace() {
let doc = json!({"user": {"name": "Alice", "age": 30}});
let r = vm_query(r#"patch $ { user.name: "Bob" }"#, &doc).unwrap();
assert_eq!(r, json!({"user": {"name": "Bob", "age": 30}}));
}
#[test]
fn patch_delete_field() {
let doc = json!({"name": "Alice", "tmp": "remove-me", "age": 30});
let r = vm_query(r#"patch $ { tmp: DELETE }"#, &doc).unwrap();
assert_eq!(r, json!({"name": "Alice", "age": 30}));
}
#[test]
fn patch_add_new_field() {
let doc = json!({"name": "Alice"});
let r = vm_query(r#"patch $ { age: 42 }"#, &doc).unwrap();
assert_eq!(r, json!({"name": "Alice", "age": 42}));
}
#[test]
fn patch_wildcard_array() {
let doc = json!({"users": [
{"name": "Alice", "seen": false},
{"name": "Bob", "seen": false},
]});
let r = vm_query(r#"patch $ { users[*].seen: true }"#, &doc).unwrap();
assert_eq!(
r,
json!({"users": [
{"name": "Alice", "seen": true},
{"name": "Bob", "seen": true},
]})
);
}
#[test]
fn patch_wildcard_filter() {
let doc = json!({"users": [
{"name": "Alice", "active": true, "role": "user"},
{"name": "Bob", "active": false, "role": "user"},
{"name": "Cara", "active": true, "role": "user"},
]});
let r = vm_query(r#"patch $ { users[* if active].role: "admin" }"#, &doc).unwrap();
assert_eq!(
r,
json!({"users": [
{"name": "Alice", "active": true, "role": "admin"},
{"name": "Bob", "active": false, "role": "user"},
{"name": "Cara", "active": true, "role": "admin"},
]})
);
}
#[test]
fn patch_uses_current_value() {
let doc = json!({"users": [
{"name": "Alice", "email": "ALICE@X"},
{"name": "Bob", "email": "BOB@X"},
]});
let r = vm_query(r#"patch $ { users[*].email: @.lower() }"#, &doc).unwrap();
assert_eq!(
r,
json!({"users": [
{"name": "Alice", "email": "alice@x"},
{"name": "Bob", "email": "bob@x"},
]})
);
}
#[test]
fn patch_conditional_when_truthy() {
let doc = json!({"count": 5, "enabled": true});
let r = vm_query(r#"patch $ { count: @ + 1 when $.enabled }"#, &doc).unwrap();
assert_eq!(r, json!({"count": 6, "enabled": true}));
}
#[test]
fn patch_conditional_when_falsy_skips() {
let doc = json!({"count": 5, "enabled": false});
let r = vm_query(r#"patch $ { count: @ + 1 when $.enabled }"#, &doc).unwrap();
assert_eq!(r, json!({"count": 5, "enabled": false}));
}
#[test]
fn patch_multiple_ops_in_order() {
let doc = json!({"a": 1, "b": 2, "c": 3});
let r = vm_query(r#"patch $ { a: 10, b: DELETE, c: 30 }"#, &doc).unwrap();
assert_eq!(r, json!({"a": 10, "c": 30}));
}
#[test]
fn patch_index_access() {
let doc = json!({"items": [10, 20, 30]});
let r = vm_query(r#"patch $ { items[1]: 99 }"#, &doc).unwrap();
assert_eq!(r, json!({"items": [10, 99, 30]}));
}
#[test]
fn patch_delete_from_wildcard() {
let doc = json!({"users": [
{"name": "Alice", "active": true},
{"name": "Bob", "active": false},
{"name": "Cara", "active": true},
]});
let r = vm_query(r#"patch $ { users[* if not active]: DELETE }"#, &doc).unwrap();
assert_eq!(
r,
json!({"users": [
{"name": "Alice", "active": true},
{"name": "Cara", "active": true},
]})
);
}
#[test]
fn patch_composes_pipe() {
let doc = json!({"name": "Alice", "age": 30});
let r = vm_query(r#"patch $ { name: "Bob" } | @.name"#, &doc).unwrap();
assert_eq!(r, json!("Bob"));
}
#[test]
fn patch_composes_method_chain() {
let doc = json!({"name": "Alice", "age": 30});
let r = vm_query(r#"patch $ { name: "Bob" }.keys()"#, &doc).unwrap();
let mut keys = r.as_array().unwrap().clone();
keys.sort_by(|a, b| a.as_str().unwrap().cmp(b.as_str().unwrap()));
assert_eq!(keys, vec![json!("age"), json!("name")]);
}
#[test]
fn patch_composes_nested_in_object() {
let doc = json!({"name": "Alice"});
let r = vm_query(r#"{result: patch $ { name: "Bob" }}"#, &doc).unwrap();
assert_eq!(r, json!({"result": {"name": "Bob"}}));
}
#[test]
fn patch_composes_let_binding() {
let doc = json!({"name": "Alice", "age": 30});
let r = vm_query(r#"let x = patch $ { name: "Bob" } in x.name"#, &doc).unwrap();
assert_eq!(r, json!("Bob"));
}
#[test]
fn patch_composes_nested_patch() {
let doc = json!({"name": "Alice", "age": 30});
let r = vm_query(r#"patch (patch $ { name: "Bob" }) { age: 99 }"#, &doc).unwrap();
assert_eq!(r, json!({"name": "Bob", "age": 99}));
}
#[test]
fn patch_composes_inside_map() {
let doc = json!({"users": [{"n": 1}, {"n": 2}, {"n": 3}]});
let r = vm_query(r#"$.users.map(patch @ { n: @ * 10 })"#, &doc).unwrap();
assert_eq!(r, json!([{"n": 10}, {"n": 20}, {"n": 30}]));
}
#[test]
fn patch_delete_mark_outside_patch_errors() {
let doc = json!({});
let r = vm_query(r#"DELETE"#, &doc);
assert!(r.is_err());
}
#[test]
fn batched_patch_three_disjoint_writes() {
let doc = json!({"a": 0, "b": 0, "c": 0, "d": 0});
let r = vm_query(r#"patch $ { a: 1, b: 2, c: 3 }"#, &doc).unwrap();
assert_eq!(r, json!({"a": 1, "b": 2, "c": 3, "d": 0}));
}
#[test]
fn batched_patch_sibling_writes_share_parent() {
let doc = json!({"user": {"name": "?", "role": "?"}});
let r = vm_query(
r#"patch $ { user.name: "alice", user.role: "admin" }"#,
&doc,
)
.unwrap();
assert_eq!(r, json!({"user": {"name": "alice", "role": "admin"}}));
}
#[test]
fn batched_patch_nested_overlap_last_wins() {
let doc = json!({"a": 1});
let r = vm_query(r#"patch $ { a: {x: 1}, a.x: 2 }"#, &doc).unwrap();
assert_eq!(r, json!({"a": {"x": 2}}));
}
#[test]
fn batched_patch_array_index_writes() {
let doc = json!({"items": [0, 0, 0]});
let r = vm_query(r#"patch $ { items[0]: 10, items[1]: 20 }"#, &doc).unwrap();
assert_eq!(r, json!({"items": [10, 20, 0]}));
}
#[test]
fn batched_patch_delete_and_replace() {
let doc = json!({"a": 0, "b": 0});
let r = vm_query(r#"patch $ { a: DELETE, b: 1 }"#, &doc).unwrap();
assert_eq!(r, json!({"b": 1}));
}
#[test]
fn batched_patch_insert_missing_field() {
let doc = json!({"name": "Alice"});
let r = vm_query(r#"patch $ { meta.role: "admin", meta.active: true }"#, &doc).unwrap();
assert_eq!(
r,
json!({"name": "Alice", "meta": {"role": "admin", "active": true}})
);
}
#[test]
fn batched_patch_modify_uses_old_value() {
let doc = json!({"a": 5, "b": 10});
let r = vm_query(r#"patch $ { a: @ + 1, b: @ * 2 }"#, &doc).unwrap();
assert_eq!(r, json!({"a": 6, "b": 20}));
}
#[test]
fn batched_patch_conditional_op_via_trie() {
let doc = json!({"role": "admin", "id": 7});
let r = vm_query(
r#"patch $ { active: true when $.role == "admin", banned: true when $.id < 0 }"#,
&doc,
)
.unwrap();
assert_eq!(r, json!({"role": "admin", "id": 7, "active": true}));
}
#[test]
fn trie_handles_single_conditional_op_truthy() {
let doc = json!({"role": "admin", "active": false});
let r = vm_query(r#"patch $ { active: true when $.role == "admin" }"#, &doc).unwrap();
assert_eq!(r, json!({"role": "admin", "active": true}));
}
#[test]
fn trie_handles_single_conditional_op_falsy() {
let doc = json!({"role": "user", "active": false});
let r = vm_query(r#"patch $ { active: true when $.role == "admin" }"#, &doc).unwrap();
assert_eq!(r, json!({"role": "user", "active": false}));
}
#[test]
fn trie_handles_multiple_conditional_ops_mixed_truthiness() {
let doc = json!({"role": "admin", "score": 10});
let r = vm_query(
r#"patch $ {
active: true when $.role == "admin",
banned: true when $.score < 0
}"#,
&doc,
)
.unwrap();
assert_eq!(r, json!({"role": "admin", "score": 10, "active": true}));
}
#[test]
fn trie_handles_conditional_alongside_unconditional_ops() {
let doc = json!({"role": "admin", "id": 0, "extra": "stay"});
let r = vm_query(
r#"patch $ {
id: 7,
tag: "ok",
flagged: true when $.role == "admin"
}"#,
&doc,
)
.unwrap();
assert_eq!(
r,
json!({
"role": "admin",
"id": 7,
"extra": "stay",
"tag": "ok",
"flagged": true
})
);
}
#[test]
fn trie_conditional_op_evaluates_against_pre_batch_doc() {
let doc = json!({"id": 0, "flag": false});
let r = vm_query(r#"patch $ { id: 7, flag: true when $.id > 5 }"#, &doc).unwrap();
assert_eq!(r, json!({"id": 7, "flag": false}));
}
#[test]
fn trie_conditional_delete_op_falsy_keeps_field() {
let doc = json!({"a": 1, "b": 2});
let r = vm_query(r#"patch $ { a: DELETE when $.b > 100 }"#, &doc).unwrap();
assert_eq!(r, json!({"a": 1, "b": 2}));
}
#[test]
fn trie_conditional_delete_op_truthy_removes_field() {
let doc = json!({"a": 1, "b": 2, "c": 3});
let r = vm_query(r#"patch $ { a: DELETE when $.b > 1, c: 99 }"#, &doc).unwrap();
assert_eq!(r, json!({"b": 2, "c": 99}));
}
#[test]
fn batched_patch_wildcard_falls_back() {
let doc = json!({"users": [{"n": 1}, {"n": 2}], "tag": "x"});
let r = vm_query(r#"patch $ { users[*].n: @ + 100, tag: "y" }"#, &doc).unwrap();
assert_eq!(r, json!({"users": [{"n": 101}, {"n": 102}], "tag": "y"}));
}
#[test]
fn phaseb_pipe_chain_fuses_three_root_writes() {
let doc = json!({});
let r = vm_query(r#"$.a.set(1) | $.b.set(2) | $.c.set(3)"#, &doc).unwrap();
assert_eq!(r, json!({"a": 1, "b": 2, "c": 3}));
}
#[test]
fn phaseb_pipe_chain_fuses_at_rooted_stages() {
let doc = json!({});
let r = vm_query(r#"$.a.set(1) | @.b.set(2) | @.c.set(3)"#, &doc).unwrap();
assert_eq!(r, json!({"a": 1, "b": 2, "c": 3}));
}
#[test]
fn phaseb_read_between_writes_breaks_fusion() {
let doc = json!({"a": 5});
let r = vm_query(r#"$.a.set(10) | $.a + 100 | $.b.set(@)"#, &doc).unwrap();
assert!(r.get("b").is_some());
}
#[test]
fn phaseb_object_field_writes_fuse_to_let() {
let doc = json!({"x": 0, "y": 0});
let r = vm_query(r#"{a: $.x.set(1), b: $.y.set(2), c: 3}"#, &doc).unwrap();
assert_eq!(r["a"]["x"], json!(1));
assert_eq!(r["a"]["y"], json!(2));
assert_eq!(r["b"]["x"], json!(1));
assert_eq!(r["b"]["y"], json!(2));
assert_eq!(r["c"], json!(3));
}
#[test]
fn phaseb_object_field_with_root_read_skips_fusion() {
let doc = json!({"x": 0, "y": 0, "meta": "hi"});
let r = vm_query(r#"{a: $.x.set(1), b: $.y.set(2), m: $.meta}"#, &doc).unwrap();
assert_eq!(r["m"], json!("hi"));
assert_eq!(r["a"]["x"], json!(1));
}
#[test]
fn phaseb_let_init_body_fuses_via_alias() {
let doc = json!({});
let r = vm_query(r#"let x = $.a.set(1) in x.b.set(2)"#, &doc).unwrap();
assert_eq!(r, json!({"a": 1, "b": 2}));
}
#[test]
fn phaseb_lambda_body_writes_dont_leak_outside() {
let doc = json!({"list": [{"id": 1}, {"id": 2}]});
let r = vm_query(r#"$.list.map(lambda o: o.id.set(99))"#, &doc).unwrap();
assert_eq!(r, json!([99, 99]));
}
#[test]
fn phaseb_deep_overlap_resolves_in_source_order() {
let doc = json!({});
let r = vm_query(r#"$.a.set({x: 1}) | $.a.x.set(2)"#, &doc).unwrap();
assert_eq!(r, json!({"a": {"x": 2}}));
}
#[test]
fn phaseb_conditional_ops_not_fused() {
let doc = json!({"role": "admin", "id": 1});
let r = vm_query(r#"patch $ { active: true when $.role == "admin" }"#, &doc).unwrap();
assert_eq!(r["active"], json!(true));
}
#[test]
fn phaseb_sibling_writes_share_make_mut() {
let doc = json!({"user": {"name": "X", "role": "u"}});
let r = vm_query(
r#"$.user.name.set("Alice") | $.user.role.set("admin")"#,
&doc,
)
.unwrap();
assert_eq!(r, json!({"user": {"name": "Alice", "role": "admin"}}));
}
#[test]
fn phaseb_let_does_not_fuse_when_body_is_pure_read() {
let doc = json!({"a": 0, "k": "hi"});
let r = vm_query(r#"let x = $.a.set(1) in x.k"#, &doc).unwrap();
assert_eq!(r, json!("hi"));
}
#[test]
fn phaseb_object_three_writes_one_other_field() {
let doc = json!({"x": 0, "y": 0, "z": 0});
let r = vm_query(
r#"{a: $.x.set(1), b: $.y.set(2), c: $.z.set(3), tag: "lit"}"#,
&doc,
)
.unwrap();
assert_eq!(r["tag"], json!("lit"));
assert_eq!(r["a"]["x"], json!(1));
assert_eq!(r["a"]["y"], json!(2));
assert_eq!(r["a"]["z"], json!(3));
}
#[test]
fn batched_patch_preserves_arc_for_untouched_subtrees() {
let doc = json!({
"touched": {"x": 1, "y": 2},
"untouched": {"a": [1, 2, 3], "b": "string", "c": {"deep": true}}
});
let r = vm_query(r#"patch $ { touched.x: 99, touched.y: 100 }"#, &doc).unwrap();
assert_eq!(
r,
json!({
"touched": {"x": 99, "y": 100},
"untouched": {"a": [1, 2, 3], "b": "string", "c": {"deep": true}}
})
);
}
#[test]
fn functional_update_selected_objects_updates_each_match_once() {
let doc = json!({
"books": [
{"title": "Dune", "year": 1965, "tags": ["sf"]},
{"title": "Hyperion", "year": 1989, "tags": ["sf", "hugo"]}
],
"active": true
});
let r = vm_query(
r#"$.books[*].update({ tags: tags.append("test"), reviewed: true })"#,
&doc,
)
.unwrap();
assert_eq!(
r,
json!({
"books": [
{"title": "Dune", "year": 1965, "tags": ["sf", "test"], "reviewed": true},
{"title": "Hyperion", "year": 1989, "tags": ["sf", "hugo", "test"], "reviewed": true}
],
"active": true
})
);
}
#[test]
fn functional_update_parses_as_update_batch() {
let expr =
parse(r#"$.books[*].update({ tags: tags.append("test"), reviewed: true })"#).unwrap();
match expr {
Expr::UpdateBatch { selector, ops, .. } => {
assert_eq!(selector.len(), 2);
assert!(matches!(selector[0], PathStep::Field(ref f) if f == "books"));
assert!(matches!(selector[1], PathStep::Wildcard));
assert_eq!(ops.len(), 2);
assert!(matches!(ops[0].path[0], PathStep::Field(ref f) if f == "tags"));
assert!(matches!(ops[1].path[0], PathStep::Field(ref f) if f == "reviewed"));
}
other => panic!("expected UpdateBatch, got {other:?}"),
}
}
#[test]
fn functional_update_compiles_to_update_batch_opcode() {
let program =
Compiler::compile_str(r#"$.books[*].update({ tags: tags.append("test") })"#).unwrap();
assert!(program
.ops
.iter()
.any(|op| matches!(op, Opcode::UpdateBatchEval(_))));
assert!(!program
.ops
.iter()
.any(|op| matches!(op, Opcode::PatchEval(_))));
}
#[test]
fn rooted_write_terminal_parses_as_update_batch() {
let expr = parse(r#"$.books[*].tags.modify(@.append("test"))"#).unwrap();
match expr {
Expr::UpdateBatch { selector, ops, .. } => {
assert_eq!(selector.len(), 3);
assert_eq!(ops.len(), 1);
assert!(ops[0].path.is_empty());
}
other => panic!("expected UpdateBatch, got {other:?}"),
}
}
#[test]
fn functional_update_selected_objects_supports_when_guards() {
let doc = json!({
"books": [
{"title": "Dune", "year": 1965, "tags": ["sf"]},
{"title": "Hyperion", "year": 1989, "tags": ["sf"]}
]
});
let r = vm_query(
r#"$.books[*].update({ tags: tags.append("modern") when year > 1980 })"#,
&doc,
)
.unwrap();
assert_eq!(
r,
json!({
"books": [
{"title": "Dune", "year": 1965, "tags": ["sf"]},
{"title": "Hyperion", "year": 1989, "tags": ["sf", "modern"]}
]
})
);
}
#[test]
fn functional_update_root_batch_updates_unrelated_paths() {
let doc = json!({
"books": [
{"title": "Dune", "tags": ["sf"], "tmp": 1},
{"title": "Hyperion", "tags": ["sf"], "tmp": 2}
],
"active": true
});
let r = vm_query(
r#"$.update({ "books[*].tags": @.append("test"), "books[*].tmp": DELETE, active: false })"#,
&doc,
)
.unwrap();
assert_eq!(
r,
json!({
"books": [
{"title": "Dune", "tags": ["sf", "test"]},
{"title": "Hyperion", "tags": ["sf", "test"]}
],
"active": false
})
);
}
#[test]
fn functional_update_root_batch_supports_filtered_wildcards() {
let doc = json!({
"books": [
{"title": "Dune", "year": 1965, "tags": ["sf"]},
{"title": "Hyperion", "year": 1989, "tags": ["sf"]}
]
});
let r = vm_query(
r#"$.update({ "books[* if year > 1980].tags": @.append("modern") })"#,
&doc,
)
.unwrap();
assert_eq!(
r,
json!({
"books": [
{"title": "Dune", "year": 1965, "tags": ["sf"]},
{"title": "Hyperion", "year": 1989, "tags": ["sf", "modern"]}
]
})
);
}
#[test]
fn functional_update_selected_objects_supports_wildcard_filter_selector() {
let doc = json!({
"books": [
{"title": "Dune", "year": 1965, "tags": ["sf"]},
{"title": "Hyperion", "year": 1989, "tags": ["sf"]}
]
});
let r = vm_query(
r#"$.books[* if year > 1980].update({ tags: tags.append("modern") })"#,
&doc,
)
.unwrap();
assert_eq!(
r,
json!({
"books": [
{"title": "Dune", "year": 1965, "tags": ["sf"]},
{"title": "Hyperion", "year": 1989, "tags": ["sf", "modern"]}
]
})
);
}
#[test]
fn functional_update_selected_objects_can_read_root_values() {
let doc = json!({
"default_tag": "review",
"books": [
{"title": "Dune", "year": 1965, "tags": ["sf"]},
{"title": "Hyperion", "year": 1989, "tags": ["sf"]}
]
});
let r = vm_query(
r#"$.books[*].update({ tags: tags.append($.default_tag) when year > 1980 })"#,
&doc,
)
.unwrap();
assert_eq!(
r,
json!({
"default_tag": "review",
"books": [
{"title": "Dune", "year": 1965, "tags": ["sf"]},
{"title": "Hyperion", "year": 1989, "tags": ["sf", "review"]}
]
})
);
}
#[test]
fn functional_update_selected_objects_reads_original_snapshot() {
let doc = json!({
"books": [
{"title": "Dune", "slug": "dune"}
]
});
let r = vm_query(
r#"$.books[*].update({ title: "changed", original_title: title, slug: slug + "-v2" })"#,
&doc,
)
.unwrap();
assert_eq!(
r,
json!({
"books": [
{"title": "changed", "slug": "dune-v2", "original_title": "Dune"}
]
})
);
}
#[test]
fn functional_update_overlapping_writes_are_source_ordered() {
let doc = json!({
"books": [
{"title": "Dune", "meta": {"score": 10}}
]
});
let r = vm_query(
r#"$.books[0].update({ title: "draft", title: "final", meta: {}, "meta.score": 99 })"#,
&doc,
)
.unwrap();
assert_eq!(
r,
json!({
"books": [
{"title": "final", "meta": {"score": 99}}
]
})
);
}
#[test]
fn functional_update_root_overlapping_writes_are_source_ordered() {
let doc = json!({"active": true, "meta": {"old": true}});
let r = vm_query(
r#"$.update({ meta: {}, "meta.updated": true, active: false, active: true })"#,
&doc,
)
.unwrap();
assert_eq!(r, json!({"active": true, "meta": {"updated": true}}));
}
#[test]
fn functional_update_scalar_target_follows_patch_object_materialization() {
let doc = json!({"items": [1, 2]});
let r = vm_query(r#"$.items[*].update({ seen: true })"#, &doc).unwrap();
assert_eq!(
r,
json!({
"items": [
{"seen": true},
{"seen": true}
]
})
);
}
#[test]
fn functional_update_preserves_untouched_arc_subtrees() {
let mut untouched_map = IndexMap::new();
untouched_map.insert(Arc::<str>::from("deep"), Val::Bool(true));
let untouched = Val::obj(untouched_map);
let untouched_arc = match &untouched {
Val::Obj(map) => Arc::clone(map),
_ => unreachable!(),
};
let mut book = IndexMap::new();
book.insert(
Arc::<str>::from("tags"),
Val::arr(vec![Val::Str(Arc::from("sf"))]),
);
let books = Val::arr(vec![Val::obj(book)]);
let mut root = IndexMap::new();
root.insert(Arc::<str>::from("books"), books);
root.insert(Arc::<str>::from("untouched"), untouched);
let program =
Compiler::compile_str(r#"$.books[*].update({ tags: tags.append("test") })"#).unwrap();
let mut vm = VM::new();
let out = vm.execute_val_raw(&program, Val::obj(root)).unwrap();
let Val::Obj(out_map) = out else {
panic!("expected object result");
};
let Some(Val::Obj(out_untouched)) = out_map.get("untouched") else {
panic!("expected untouched object");
};
assert!(Arc::ptr_eq(&untouched_arc, out_untouched));
}
#[test]
fn functional_update_filtered_wildcard_preserves_unselected_object_arc() {
let mut first = IndexMap::new();
first.insert(Arc::<str>::from("id"), Val::Int(1));
first.insert(Arc::<str>::from("keep"), Val::Bool(false));
first.insert(
Arc::<str>::from("tags"),
Val::arr(vec![Val::Str(Arc::from("a"))]),
);
let first = Val::obj(first);
let first_arc = match &first {
Val::Obj(map) => Arc::clone(map),
_ => unreachable!(),
};
let mut second = IndexMap::new();
second.insert(Arc::<str>::from("id"), Val::Int(2));
second.insert(Arc::<str>::from("keep"), Val::Bool(true));
second.insert(
Arc::<str>::from("tags"),
Val::arr(vec![Val::Str(Arc::from("b"))]),
);
let mut root = IndexMap::new();
root.insert(
Arc::<str>::from("books"),
Val::arr(vec![first, Val::obj(second)]),
);
let program = Compiler::compile_str(
r#"$.books[* if keep].update({ tags: tags.append("x"), reviewed: true })"#,
)
.unwrap();
let mut vm = VM::new();
let out = vm.execute_val_raw(&program, Val::obj(root)).unwrap();
let Val::Obj(out_map) = out else {
panic!("expected object result");
};
let Some(Val::Arr(books)) = out_map.get("books") else {
panic!("expected books array");
};
let Val::Obj(out_first) = &books[0] else {
panic!("expected first book object");
};
assert!(Arc::ptr_eq(&first_arc, out_first));
assert!(matches!(books[1].get("reviewed"), Some(Val::Bool(true))));
}
#[test]
fn wildcard_filter_chain_delete_removes_selected_elements() {
let doc = json!({
"items": [
{"id": 1, "drop": false},
{"id": 2, "drop": true},
{"id": 3, "drop": false}
]
});
let r = vm_query(r#"$.items[* if drop].delete()"#, &doc).unwrap();
assert_eq!(
r,
json!({
"items": [
{"id": 1, "drop": false},
{"id": 3, "drop": false}
]
})
);
}
#[test]
fn wildcard_chain_modify_lowers_to_patch() {
let doc = json!({
"books": [
{"tags": ["sf"]},
{"tags": ["hugo"]}
]
});
let r = vm_query(r#"$.books[*].tags.modify(@.append("test"))"#, &doc).unwrap();
assert_eq!(
r,
json!({
"books": [
{"tags": ["sf", "test"]},
{"tags": ["hugo", "test"]}
]
})
);
}
}