diff --git a/Cargo.toml b/Cargo.toml index 43a3a87..956421f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -101,6 +101,7 @@ rand-builtins = ["rng"] yaml-builtins = ["dep:serde_yaml"] urlquery-builtins = ["dep:form_urlencoded", "dep:urlencoding"] time-builtins = ["time", "dep:chrono-tz", "dep:duration-str", "dep:chronoutil"] +object-builtins = [] all-crypto-builtins = [ "crypto-digest-builtins", @@ -122,6 +123,7 @@ all-builtins = [ "yaml-builtins", "urlquery-builtins", "time-builtins", + "object-builtins", ] [[test]] diff --git a/src/builtins/impls/mod.rs b/src/builtins/impls/mod.rs index 8013b1a..d49f63f 100644 --- a/src/builtins/impls/mod.rs +++ b/src/builtins/impls/mod.rs @@ -32,6 +32,7 @@ pub mod io; #[cfg(feature = "json-builtins")] pub mod json; pub mod net; +#[cfg(feature = "object-builtins")] pub mod object; pub mod opa; #[cfg(feature = "rng")] diff --git a/src/builtins/impls/object.rs b/src/builtins/impls/object.rs index 82cdf65..4cc0a76 100644 --- a/src/builtins/impls/object.rs +++ b/src/builtins/impls/object.rs @@ -14,12 +14,40 @@ //! Builtins to help handling JSON objects -use anyhow::{bail, Result}; +use anyhow::Result; +use serde_json::Value; /// Creates a new object that is the asymmetric union of all objects merged from /// left to right. For example: `object.union_n([{"a": 1}, {"b": 2}, {"a": 3}])` /// will result in `{"b": 2, "a": 3}`. #[tracing::instrument(name = "object.union_n", err)] -pub fn union_n(objects: Vec) -> Result { - bail!("not implemented"); +pub fn union_n(objects: Vec) -> Result { + let mut result = serde_json::Value::Object(serde_json::Map::default()); + for object in objects { + merge_value(&mut result, &object); + } + + Ok(result) +} + +fn merge_value(a: &mut Value, b: &Value) { + match (a, b) { + (Value::Object(ref mut a), &Value::Object(ref b)) => { + for (k, v) in b { + merge_value(a.entry(k).or_insert(Value::Null), v); + } + } + (Value::Array(ref mut a), &Value::Array(ref b)) => { + *a = vec![]; + a.extend(b.clone()); + } + (Value::Array(ref mut a), &Value::Object(ref b)) => { + *a = vec![]; + a.extend([Value::Object(b.clone())]); + } + (_, Value::Null) => {} + (a, b) => { + *a = b.clone(); + } + } } diff --git a/src/builtins/mod.rs b/src/builtins/mod.rs index 392f7f6..c5c20ca 100644 --- a/src/builtins/mod.rs +++ b/src/builtins/mod.rs @@ -106,7 +106,10 @@ pub fn resolve(name: &str) -> Result>> "net.cidr_expand" => Ok(self::impls::net::cidr_expand.wrap()), "net.cidr_merge" => Ok(self::impls::net::cidr_merge.wrap()), "net.lookup_ip_addr" => Ok(self::impls::net::lookup_ip_addr.wrap()), + + #[cfg(feature = "object-builtins")] "object.union_n" => Ok(self::impls::object::union_n.wrap()), + "opa.runtime" => Ok(self::impls::opa::runtime.wrap()), #[cfg(feature = "rng")] diff --git a/tests/infra-fixtures/test-object.rego b/tests/infra-fixtures/test-object.rego new file mode 100644 index 0000000..75249e8 --- /dev/null +++ b/tests/infra-fixtures/test-object.rego @@ -0,0 +1,9 @@ +package test + +object_1 := object.union_n([{"a": 1}, {"b": 2}, {"a": 3}]) + +object_2 := object.union_n([{"a": 1}, {"b": 2}, {"a": 3, "b": 1}]) + +object_override_by_string := object.union_n([{"a": 1}, {"b": 2}, {"a": "3"}]) + +recursive := object.union_n([{"a": {"b": [1], "c": 1}}, {"a": {"b": [1, 2, 3]}}]) diff --git a/tests/smoke_test.rs b/tests/smoke_test.rs index cf70c51..3efade7 100644 --- a/tests/smoke_test.rs +++ b/tests/smoke_test.rs @@ -121,6 +121,7 @@ integration_test!(test_rand, "test-rand"); integration_test!(test_yaml, "test-yaml"); integration_test!(test_urlquery, "test-urlquery"); integration_test!(test_time, "test-time"); +integration_test!(test_object, "test-object"); /* #[tokio::test] diff --git a/tests/snapshots/smoke_test__object.snap b/tests/snapshots/smoke_test__object.snap new file mode 100644 index 0000000..0b99046 --- /dev/null +++ b/tests/snapshots/smoke_test__object.snap @@ -0,0 +1,21 @@ +--- +source: tests/smoke_test.rs +expression: "test_policy(\"test-object\", None).await.expect(\"error in test suite\")" +--- +- result: + object_1: + a: 3 + b: 2 + object_2: + a: 3 + b: 1 + object_override_by_string: + a: "3" + b: 2 + recursive: + a: + b: + - 1 + - 2 + - 3 + c: 1 \ No newline at end of file