Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ http = []
glob = ["dep:globset"]
graph = []
jsonschema = ["dep:jsonschema"]
jsonpatch = ["dep:json-patch"]
no_std = ["lazy_static/spin_no_std"]
opa-runtime = []
regex = ["dep:regex"]
Expand All @@ -49,6 +50,7 @@ full-opa = [
"hex",
"http",
"jsonschema",
"jsonpatch",
"opa-runtime",
"regex",
"semver",
Expand Down Expand Up @@ -100,6 +102,7 @@ semver = {version = "1.0.25", optional = true, default-features = false }
url = { version = "2.5.4", optional = true }
uuid = { version = "1.15.1", default-features = false, features = ["v4", "fast-rng"], optional = true }
jsonschema = { version = "0.30.0", default-features = false, optional = true }
json-patch = { version = "4.0.0", default-features = false, optional = true }
chrono = { version = "0.4.40", optional = true }
chrono-tz = { version = "0.10.1", optional = true }

Expand Down
51 changes: 50 additions & 1 deletion src/builtins/objects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ pub fn register(m: &mut builtins::BuiltinsMap<&'static str, builtins::BuiltinFcn
m.insert("json.match_schema", (json_match_schema, 2));
m.insert("json.verify_schema", (json_verify_schema, 1));
}

#[cfg(feature = "jsonpatch")]
{
m.insert("json.patch", (json_patch, 2));
}
}

fn json_filter_impl(v: &Value, filter: &Value) -> Value {
Expand Down Expand Up @@ -438,7 +443,6 @@ fn json_match_schema(
let name = "json.match_schema";
ensure_args_count(span, name, params, args, 2)?;

// The following is expected to succeed.
let document: serde_json::Value = serde_json::from_str(&args[0].to_json_str()?)
.map_err(|err| span.error(&format!("Failed to parse JSON: {err}")))?;

Expand All @@ -456,3 +460,48 @@ fn json_match_schema(
.to_vec(),
))
}

#[cfg(feature = "jsonpatch")]
fn json_patch(span: &Span, params: &[Ref<Expr>], args: &[Value], strict: bool) -> Result<Value> {
let name = "json.patch";
ensure_args_count(span, name, params, args, 2)?;

let object_str = args[0].to_json_str()?;
let mut object: serde_json::Value = serde_json::from_str(&object_str)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let mut object: serde_json::Value = serde_json::from_str(&object_str)
let mut object: serde_json::Value = serde_json::to_value(&args[0])

.map_err(|err| span.error(&format!("Failed to parse object as JSON: {err}")))?;

ensure_array(name, &params[1], args[1].clone())?;

let patches_str = args[1].to_json_str()?;
let patches_json: serde_json::Value = serde_json::from_str(&patches_str)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let patches_json: serde_json::Value = serde_json::from_str(&patches_str)
let patches_json: serde_json::Value = serde_json::to_value(&args[1])

.map_err(|err| span.error(&format!("Failed to parse patches as JSON: {err}")))?;

let patch: json_patch::Patch = serde_json::from_value(patches_json).map_err(|err| {
if strict {
params[1]
.span()
.error(&format!("Invalid patch format: {err}"))
} else {
span.error(&format!("Invalid patch format: {err}"))
}
})?;

match json_patch::patch(&mut object, &patch) {
Ok(_) => {
let result_str = serde_json::to_string(&object)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
let result_str = serde_json::to_string(&object)
let value : regorus::Value = serde_json::from_value&object)

.map_err(|err| span.error(&format!("Failed to serialize patched object: {err}")))?;
Value::from_json_str(&result_str).map_err(|err| {
span.error(&format!(
"Failed to convert patched object back to Value: {err}"
))
})
}
Err(err) => {
if strict {
bail!(span.error(&format!("Failed to apply patch: {err}")));
} else {
Ok(Value::Undefined)
}
}
}
}
212 changes: 212 additions & 0 deletions tests/interpreter/cases/builtins/objects/json.patch.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

cases:
- note: basic add operation
data: {}
modules:
- |
package test

obj = {"a": {"foo": 1}}
patches = [{"op": "add", "path": "/a/bar", "value": 2}]
result = json.patch(obj, patches)
query: data.test.result
want_result:
a:
foo: 1
bar: 2

- note: basic remove operation
data: {}
modules:
- |
package test

obj = {"a": {"foo": 1, "bar": 2}}
patches = [{"op": "remove", "path": "/a/bar"}]
result = json.patch(obj, patches)
query: data.test.result
want_result:
a:
foo: 1

- note: basic replace operation
data: {}
modules:
- |
package test

obj = {"a": {"foo": 1}}
patches = [{"op": "replace", "path": "/a/foo", "value": 42}]
result = json.patch(obj, patches)
query: data.test.result
want_result:
a:
foo: 42

- note: basic move operation
data: {}
modules:
- |
package test

obj = {"a": {"foo": 1}, "b": {}}
patches = [{"op": "move", "from": "/a/foo", "path": "/b/foo"}]
result = json.patch(obj, patches)
query: data.test.result
want_result:
a: {}
b:
foo: 1

- note: basic copy operation
data: {}
modules:
- |
package test

obj = {"a": {"foo": 1}, "b": {}}
patches = [{"op": "copy", "from": "/a/foo", "path": "/b/foo"}]
result = json.patch(obj, patches)
query: data.test.result
want_result:
a:
foo: 1
b:
foo: 1

- note: basic test operation (successful)
data: {}
modules:
- |
package test

obj = {"a": {"foo": 1}}
patches = [{"op": "test", "path": "/a/foo", "value": 1}]
result = json.patch(obj, patches)
query: data.test.result
want_result:
a:
foo: 1

- note: multiple operations
data: {}
modules:
- |
package test

obj = {"a": {"foo": 1}}
patches = [
{"op": "add", "path": "/a/bar", "value": 2},
{"op": "replace", "path": "/a/foo", "value": 42}
]
result = json.patch(obj, patches)
query: data.test.result
want_result:
a:
foo: 42
bar: 2

- note: array operations - add to array
data: {}
modules:
- |
package test

obj = {"arr": [1, 2, 3]}
patches = [{"op": "add", "path": "/arr/1", "value": "inserted"}]
result = json.patch(obj, patches)
query: data.test.result
want_result:
arr: [1, "inserted", 2, 3]

- note: array operations - add to end of array
data: {}
modules:
- |
package test

obj = {"arr": [1, 2, 3]}
patches = [{"op": "add", "path": "/arr/-", "value": 4}]
result = json.patch(obj, patches)
query: data.test.result
want_result:
arr: [1, 2, 3, 4]

- note: array operations - remove from array
data: {}
modules:
- |
package test

obj = {"arr": [1, 2, 3]}
patches = [{"op": "remove", "path": "/arr/1"}]
result = json.patch(obj, patches)
query: data.test.result
want_result:
arr: [1, 3]

- note: nested object operations
data: {}
modules:
- |
package test

obj = {"a": {"b": {"c": {"d": 1}}}}
patches = [{"op": "add", "path": "/a/b/c/e", "value": 2}]
result = json.patch(obj, patches)
query: data.test.result
want_result:
a:
b:
c:
d: 1
e: 2

- note: special characters in keys
data: {}
modules:
- |
package test

obj = {"foo/bar": {"baz~": 1}}
patches = [{"op": "add", "path": "/foo~1bar/baz~0test", "value": 2}]
result = json.patch(obj, patches)
query: data.test.result
want_result:
"foo/bar":
"baz~": 1
"baz~test": 2

- note: empty object and array handling
data: {}
modules:
- |
package test

obj = {}
patches = [{"op": "add", "path": "/newkey", "value": {"nested": []}}]
result = json.patch(obj, patches)
query: data.test.result
want_result:
newkey:
nested: []

- note: complex value types
data: {}
modules:
- |
package test

obj = {"data": null}
patches = [
{"op": "replace", "path": "/data", "value": {"bool": true, "num": 3.14, "str": "hello"}}
]
result = json.patch(obj, patches)
query: data.test.result
want_result:
data:
bool: true
num: 3.14
str: "hello"