diff --git a/ast/builtins.go b/ast/builtins.go
index 83fa6066e8..86847ec73a 100644
--- a/ast/builtins.go
+++ b/ast/builtins.go
@@ -157,6 +157,7 @@ var DefaultBuiltins = [...]*Builtin{
// Object Manipulation
ObjectUnion,
+ ObjectUnionN,
ObjectRemove,
ObjectFilter,
ObjectGet,
@@ -1330,6 +1331,20 @@ var ObjectUnion = &Builtin{
),
}
+// ObjectUnionN creates a new object that is the asymmetric union of all objects merged from left to right
+var ObjectUnionN = &Builtin{
+ Name: "object.union_n",
+ Decl: types.NewFunction(
+ types.Args(
+ types.NewArray(
+ nil,
+ types.NewObject(nil, types.NewDynamicProperty(types.A, types.A)),
+ ),
+ ),
+ types.A,
+ ),
+}
+
// ObjectRemove Removes specified keys from an object
var ObjectRemove = &Builtin{
Name: "object.remove",
diff --git a/capabilities.json b/capabilities.json
index 03339eb5e8..e8736d8d0f 100644
--- a/capabilities.json
+++ b/capabilities.json
@@ -2558,6 +2558,31 @@
"type": "function"
}
},
+ {
+ "name": "object.union_n",
+ "decl": {
+ "args": [
+ {
+ "dynamic": {
+ "dynamic": {
+ "key": {
+ "type": "any"
+ },
+ "value": {
+ "type": "any"
+ }
+ },
+ "type": "object"
+ },
+ "type": "array"
+ }
+ ],
+ "result": {
+ "type": "any"
+ },
+ "type": "function"
+ }
+ },
{
"name": "opa.runtime",
"decl": {
diff --git a/docs/content/policy-reference.md b/docs/content/policy-reference.md
index 2e603ec34f..853e47ee2d 100644
--- a/docs/content/policy-reference.md
+++ b/docs/content/policy-reference.md
@@ -343,7 +343,8 @@ complex types.
| ------- |-------------|---------------|
| `value := object.get(object, key, default)` | `value` is the value stored by the `object` at `key`. If no value is found, `default` is returned. | ✅ |
| `output := object.remove(object, keys)` | `output` is a new object which is the result of removing the specified `keys` from `object`. `keys` must be either an array, object, or set of keys. | ✅ |
-| `output := object.union(objectA, objectB)` | `output` is a new object which is the result of an asymmetric recursive union of two objects where conflicts are resolved by choosing the key from the right-hand object (`objectB`). For example: `object.union({"a": 1, "b": 2, "c": {"d": 3}}, {"a": 7, "c": {"d": 4, "e": 5}})` will result in `{"a": 7, "b": 2, "c": {"d": 4, "e": 5}}` | ✅ |
+| `output := object.union(objectA, objectB)` | `output` is a new object which is the result of an asymmetric recursive union of two objects where conflicts are resolved by choosing the key from the right-hand object (`objectB`). For example: `object.union({"a": 1, "b": 2, "c": {"d": 3}}, {"a": 7, "c": {"d": 4, "e": 5}})` will result in `{"a": 7, "b": 2, "c": {"d": 4, "e": 5}}` | ✅ |
+| `output := object.union_n(array)` | `output` is a new object which is the result of an asymmetric recursive union of all objects in `array`, merged from left to right, where conflicts are resolved by choosing the key from the right-hand object. For example: `object.union_n([{"a": 1}, {"b": 2}, {"a": 3}])` will result in `{"b": 2, "a": 3}` | ``SDK-dependent`` |
| `filtered := object.filter(object, keys)` | `filtered` is a new object with the remaining data from `object` with only keys specified in `keys` which is an array, object, or set of keys. For example: `object.filter({"a": {"b": "x", "c": "y"}, "d": "z"}, ["a"])` will result in `{"a": {"b": "x", "c": "y"}}`). | ✅ |
| `filtered := json.filter(object, paths)` | `filtered` is the remaining data from `object` with only keys specified in `paths` which is an array or set of JSON string paths. For example: `json.filter({"a": {"b": "x", "c": "y"}}, ["a/b"])` will result in `{"a": {"b": "x"}}`). Paths are not filtered in-order and are deduplicated before being evaluated. | ✅ |
| `output := json.remove(object, paths)` | `output` is a new object which is the result of removing all keys specified in `paths` which is an array or set of JSON string paths. For example: `json.remove({"a": {"b": "x", "c": "y"}}, ["a/b"])` will result in `{"a": {"c": "y"}}`. Paths are not removed in-order and are deduplicated before being evaluated. | ✅ |
diff --git a/test/cases/testdata/objectunionn/test-objectunionn-0001.yaml b/test/cases/testdata/objectunionn/test-objectunionn-0001.yaml
new file mode 100644
index 0000000000..50ade37d89
--- /dev/null
+++ b/test/cases/testdata/objectunionn/test-objectunionn-0001.yaml
@@ -0,0 +1,67 @@
+cases:
+- note: objectunionn/empty array
+ modules:
+ - |
+ package test
+
+ p := object.union_n([{}])
+
+ query: data.test.p = x
+ want_result:
+ - x: {}
+
+- note: objectunionn/single item array
+ modules:
+ - |
+ package test
+
+ p := object.union_n([{"foo": "bar"}])
+
+ query: data.test.p = x
+ want_result:
+ - x: {"foo": "bar"}
+
+- note: objectunionn/merge objects
+ modules:
+ - |
+ package test
+
+ x := object.union_n([{"foo": "bar"}, {"x": "y"}])
+
+ query: data.test.x = x
+ want_result:
+ - x: {"foo": "bar", "x": "y"}
+
+- note: objectunionn/merge objects conflict
+ modules:
+ - |
+ package test
+
+ x := object.union_n([{"foo": "bar"}, {"foo": "baz"}])
+
+ query: data.test.x = x
+ want_result:
+ - x: {"foo": "baz"}
+
+- note: objectunionn/merge objects extended
+ modules:
+ - |
+ package test
+
+ x := object.union_n([{
+ "a": 1,
+ "b": 2,
+ "c": 3,
+ }, {
+ "foo": "baz",
+ "a": "a",
+ "b": 2,
+ "d": 4,
+ }, {
+ "a": "final A!",
+ "e": 5.0,
+ }])
+
+ query: data.test.x = x
+ want_result:
+ - x: {"foo": "baz", "a": "final A!", "b": 2, "c": 3, "d": 4, "e": 5.0}
diff --git a/topdown/object.go b/topdown/object.go
index ff01b4f96b..4dfd9d6ae4 100644
--- a/topdown/object.go
+++ b/topdown/object.go
@@ -26,6 +26,25 @@ func builtinObjectUnion(_ BuiltinContext, operands []*ast.Term, iter func(*ast.T
return iter(ast.NewTerm(r))
}
+func builtinObjectUnionN(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
+ arr, err := builtins.ArrayOperand(operands[0].Value, 1)
+ if err != nil {
+ return err
+ }
+
+ r := ast.NewObject()
+ arr.Foreach(func(t *ast.Term) {
+ var o ast.Object
+ o, err = builtins.ObjectOperand(t.Value, 1)
+ r = mergeWithOverwrite(r, o)
+ })
+ if err != nil {
+ return err
+ }
+
+ return iter(ast.NewTerm(r))
+}
+
func builtinObjectRemove(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Term) error) error {
// Expect an object and an array/set/object of keys
obj, err := builtins.ObjectOperand(operands[0].Value, 1)
@@ -134,6 +153,7 @@ func mergeWithOverwrite(objA, objB ast.Object) ast.Object {
func init() {
RegisterBuiltinFunc(ast.ObjectUnion.Name, builtinObjectUnion)
+ RegisterBuiltinFunc(ast.ObjectUnionN.Name, builtinObjectUnionN)
RegisterBuiltinFunc(ast.ObjectRemove.Name, builtinObjectRemove)
RegisterBuiltinFunc(ast.ObjectFilter.Name, builtinObjectFilter)
RegisterBuiltinFunc(ast.ObjectGet.Name, builtinObjectGet)