Alter object.get to support nested key array

This commit extends the go and wasm implementations of object.get to
allow a key to also be an array.

When passed an array, each element in the array will be used as a key in
turn. This allows values at deeply nested paths to be extracted from
objects.

It also supports getting indexes of nested arrays.

The functionality was originally inspired by Ruby's Hash.dig function:
https://ruby-doc.org/core-2.3.0_preview1/Hash.html#method-i-dig however
we opted to include the behavior in object.get instead after being
uncertain 'dig' was a commonly understood name.

Signed-off-by: Charlie Egan <charlieegan3@users.noreply.github.com>
This commit is contained in:
Charlie Egan
2021-12-22 15:20:47 +00:00
committed by Anders Eknert
parent 19d6bc4026
commit 301efc6997
5 changed files with 193 additions and 7 deletions
+1 -1
View File
@@ -341,7 +341,7 @@ complex types.
| Built-in | Description | Wasm Support |
| ------- |-------------|---------------|
| <span class="opa-keep-it-together">`value := object.get(object, key, default)`</span> | `value` is the value stored by the `object` at `key`. If no value is found, `default` is returned. | ✅ |
| <span class="opa-keep-it-together">`value := object.get(object, key, default)`</span> | `value` is the value stored by the `object` at `key`. If no value is found, `default` is returned. If the supplied `key` is an `array`, then `object.get` will search through a nested object or array using each key in turn. For example: `object.get({"a": [{ "b": true }]}, ["a", 0, "b"], false)` results in `true` | ✅ |
| <span class="opa-keep-it-together">`output := object.remove(object, keys)`</span> | `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. | ✅ |
| <span class="opa-keep-it-together">`output := object.union(objectA, objectB)`</span> | `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}}` | ✅ |
| <span class="opa-keep-it-together">`output := object.union_n(array)`</span> | `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`` |
+11
View File
@@ -26,3 +26,14 @@ func ParseDataPath(s string) (ast.Ref, error) {
return path.Ref(ast.DefaultRootDocument), nil
}
// ArrayPath will take an ast.Array and build an ast.Ref using the ast.Terms in the Array
func ArrayPath(a *ast.Array) ast.Ref {
var ref ast.Ref
a.Foreach(func(term *ast.Term) {
ref = append(ref, term)
})
return ref
}
+124
View File
@@ -0,0 +1,124 @@
cases:
- note: objectget/empty_path_returns_object
query: data.test.p = x
modules:
- |
package test
p = x {
x := object.get({"a": 1}, [], 2)
}
want_result: [{x: {a: 1}}]
- note: objectget/path_with_single_element
query: data.test.p = x
modules:
- |
package test
p = x {
x := object.get({ "a": 1 }, ["a"], 2)
}
want_result: [{x: 1}]
- note: objectget/path_with_two_elements
query: data.test.p = x
modules:
- |
package test
p = x {
x := object.get({ "a": { "b": 1 } }, ["a", "b"], 2)
}
want_result: [{x: 1}]
- note: objectget/path_with_three_elements
query: data.test.p = x
modules:
- |
package test
p = x {
x := object.get({ "a": { "b": { "c": 1 } } }, ["a", "b", "c"], 2)
}
want_result: [{x: 1}]
- note: objectget/path_with_single_element_no_result
query: data.test.p = x
modules:
- |
package test
p = x {
x := object.get({ "a": 1 }, ["b"], 2)
}
want_result: [{x: 2}]
- note: objectget/path_with_two_elements_no_result
query: data.test.p = x
modules:
- |
package test
p = x {
x := object.get({ "a": { "b": 1 } }, ["b", "a"], 2)
}
want_result: [{x: 2}]
- note: objectget/path_with_three_elements_no_result
query: data.test.p = x
modules:
- |
package test
p = x {
x := object.get({ "a": { "b": { "c": 1 } } }, ["a", "b", "a"], 2)
}
want_result: [{x: 2}]
- note: objectget/path_with_non_string_keys
query: data.test.p = x
modules:
- |
package test
p = x {
x := object.get({ 1: { "b": { [1,2,3]: 1 } } }, [1, "b", [1,2,3]], 2)
}
want_result: [{x: 1}]
- note: objectget/get_intermediate_non_object
query: data.test.p = x
modules:
- |
package test
p = x {
x := object.get({ "a": { "b": [1,2,3] } }, ["a", "b", "a"], 2)
}
want_result: [{x: 2}]
- note: objectget/get_intermediate_array
query: data.test.p = x
modules:
- |
package test
p = x {
x := object.get({ "a": { "b": [{"c": 1}] } }, ["a", "b", 0, "c"], 2)
}
want_result: [{x: 1}]
- note: objectget/get_for_non_object
query: data.test.p = x
input_term: '{"obj":"object"}'
modules:
- |
package test
p = x {
x := object.get(input.obj, ["a"], 2)
}
want_error: 'object.get: operand 1 must be object but got string'
want_error_code: eval_type_error
strict_error: true
+22 -3
View File
@@ -6,6 +6,7 @@ package topdown
import (
"github.com/open-policy-agent/opa/ast"
"github.com/open-policy-agent/opa/internal/ref"
"github.com/open-policy-agent/opa/topdown/builtins"
"github.com/open-policy-agent/opa/types"
)
@@ -100,11 +101,29 @@ func builtinObjectGet(_ BuiltinContext, operands []*ast.Term, iter func(*ast.Ter
return err
}
if ret := object.Get(operands[1]); ret != nil {
return iter(ret)
// if the get key is not an array, attempt to get the top level key for the operand value in the object
path, err := builtins.ArrayOperand(operands[1].Value, 2)
if err != nil {
if ret := object.Get(operands[1]); ret != nil {
return iter(ret)
}
return iter(operands[2])
}
return iter(operands[2])
// if the path is empty, then we skip selecting nested keys and return the whole object
if path.Len() == 0 {
return iter(operands[0])
}
// build an ast.Ref from the array and see if it matches within the object
pathRef := ref.ArrayPath(path)
value, err := object.Find(pathRef)
if err != nil {
return iter(operands[2])
}
return iter(ast.NewTerm(value))
}
// getObjectKeysParam returns a set of key values
+35 -3
View File
@@ -435,10 +435,42 @@ opa_value *builtin_object_get(opa_value *obj, opa_value *key, opa_value *value)
return NULL;
}
opa_object_elem_t *elem = opa_object_get(opa_cast_object(obj), key);
if (elem != NULL)
opa_value *elem;
// if the key is not an array, then we get that top level key from the object/array or return the default value
if (opa_value_type(key) != OPA_ARRAY) {
elem = opa_value_get(obj, key);
if (elem != NULL)
{
return elem;
}
return value;
}
size_t path_len = opa_cast_array(key)->len;
// if the path is empty, then we skip selecting nested keys and return the default
if (path_len == 0) {
return obj;
}
for (int i = 0; i < path_len; i++)
{
return elem->v;
opa_value *path_component = opa_cast_array(key)->elems[i].v;
elem = opa_value_get(obj, path_component);
if (elem == NULL)
{
return value;
}
if (i == path_len-1)
{
return elem;
}
obj = elem;
}
return value;