mirror of
https://github.com/open-policy-agent/opa.git
synced 2026-08-12 19:32:48 -06:00
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:
committed by
Anders Eknert
parent
19d6bc4026
commit
301efc6997
@@ -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`` |
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user