diff --git a/ast/check_test.go b/ast/check_test.go index 8dad52e2d3..27f6779ed9 100644 --- a/ast/check_test.go +++ b/ast/check_test.go @@ -101,14 +101,13 @@ func TestCheckInference(t *testing.T) { }}, {"object-object-key", `x = {{{}: 1}: 1}`, map[Var]types.Type{ Var("x"): types.NewObject( - nil, - types.NewDynamicProperty( - types.NewObject( - []*types.StaticProperty{types.NewStaticProperty(map[string]interface{}{}, types.N)}, - nil, - ), + []*types.StaticProperty{types.NewStaticProperty( + map[string]interface{}{ + "{}": json.Number("1"), + }, types.N, - ), + )}, + nil, ), }}, {"sets", `x = {1, 2}; y = {{"foo", 1}, x}`, map[Var]types.Type{ diff --git a/ast/term.go b/ast/term.go index d14e653141..ee0ee36d11 100644 --- a/ast/term.go +++ b/ast/term.go @@ -185,15 +185,20 @@ func ValueToInterface(v Value, resolver Resolver) (interface{}, error) { if err != nil { return err } - asStr, stringKey := ki.(string) - if !stringKey { - return fmt.Errorf("object value has non-string key (%T)", ki) + var str string + var ok bool + if str, ok = ki.(string); !ok { + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(ki); err != nil { + return err + } + str = strings.TrimSpace(buf.String()) } vi, err := ValueToInterface(v.Value, resolver) if err != nil { return err } - buf[asStr] = vi + buf[str] = vi return nil }) if err != nil { diff --git a/docs/content/policy-language.md b/docs/content/policy-language.md index 017af5687b..73781ae291 100644 --- a/docs/content/policy-language.md +++ b/docs/content/policy-language.md @@ -308,6 +308,38 @@ d := {"a": a, "x": [b, c]} By defining composite values in terms of variables and references, rules can define abstractions over raw data and other rules. +### Objects + +Objects are unordered key-value collections. In Rego, any value type can be +used as an object key. For example, the following assignment maps port **numbers** +to a list of IP addresses (represented as strings). + +```live:eg/objects:module:merge_down +ips_by_port := { + 80: ["1.1.1.1", "1.1.1.2"], + 443: ["2.2.2.1"], +} +``` +```live:eg/objects/lookup:query:merge_down +ips_by_port[80] +``` +```live:eg/objects/lookup:output:merge_down +``` +```live:eg/objects/iteration:query:merge_down +some port; ips_by_port[port][_] == "2.2.2.1" +``` +```live:eg/objects/iteration:output +``` + +When Rego values are converted to JSON non-string object keys are marshalled +as strings (because JSON does not support non-string object keys). + +```live:eg/objects/marshal:query:merge_down +ips_by_port +``` +```live:eg/objects/marshal:output +``` + ### Sets In addition to arrays and objects, Rego supports set values. Sets are unordered diff --git a/topdown/topdown_test.go b/topdown/topdown_test.go index 601b7a9ac4..d58806b86f 100644 --- a/topdown/topdown_test.go +++ b/topdown/topdown_test.go @@ -46,6 +46,7 @@ func TestTopDownCompleteDoc(t *testing.T) { {`object/nested composites: {"a": [1], "b": [2], "c": [3]}`, `p = {"a": [1], "b": [2], "c": [3]} { true }`, `{"a": [1], "b": [2], "c": [3]}`}, + {"object/non-string key:", `p = {1: 2, {3: 4}: 5} { true }`, `{"1": 2, "{\"3\":4}": 5}`}, {"set/nested: {{1,2},{2,3}}", `p = {{1, 2}, {2, 3}} { true }`, "[[1,2], [2,3]]"}, {"vars", `p = {"a": [x, y]} { x = 1; y = 2 }`, `{"a": [1,2]}`}, {"vars conflict", `p = {"a": [x, y]} { xs = [1, 2]; ys = [1, 2]; x = xs[_]; y = ys[_] }`, @@ -189,6 +190,7 @@ func TestTopDownPartialObjectDoc(t *testing.T) { "c": [3, {"v2": 4}] }`}, {"same key/value pair", `p[k] = 1 { ks = ["a", "b", "c", "a"]; ks[_] = k }`, `{"a":1,"b":1,"c":1}`}, + {"non-string key", `p[k] = 1 { ks = [1,{},null]; ks[_] = k }`, `{"1": 1, "{}": 1, "null": 1}`}, } data := loadSmallTestData() @@ -451,6 +453,7 @@ func TestTopDownVirtualDocs(t *testing.T) { {"input: object dereference ground 2", []string{`p[v] { x = "a"; q[x][y] = v }`, `q[k] = v { k = "a"; v = data.a }`}, "[1,2,3,4]"}, {"input: object defererence non-ground", []string{`p = true { q[0][x][y] = false }`, `q[i] = x { x = c[i] }`}, "true"}, {"input: object ground var key", []string{`p[y] { x = "b"; q[x] = y }`, `q[k] = v { x = {"a": 1, "b": 2}; x[k] = v }`}, "[2]"}, + {"input: object non-string key", []string{`p[y] { x = 1; q[x] = y }`, `q[k] = v { x = {2: 1, 1: 3}; x[k] = v }`}, "[3]"}, {"input: variable binding substitution", []string{ `p[x] = y { r[z] = y; q[x] = z }`, `r[k] = v { x = {"a": 1, "b": 2, "c": 3, "d": 4}; x[k] = v }`, @@ -465,6 +468,7 @@ func TestTopDownVirtualDocs(t *testing.T) { {"output: set dereference deep", []string{`p[y] { q[i][j][k][x] = y }`, `q[{{[1], [2]}, {[3], [4]}}] { true }`}, "[1,2,3,4]"}, {"output: set falsy values", []string{`p[x] { q[x] }`, `q = {0, "", false, null, [], {}, set()} { true }`}, `[0, "", null, [], {}, []]`}, {"output: object key", []string{`p[x] { q[x] = 4 }`, `q[i] = x { a[i] = x }`}, "[3]"}, + {"output: object non-string key", []string{`p[x] { q[x] = 1 }`, `q[k] = 1 { a[_] = k; k < 3 }`}, "[1,2]"}, {"output: object value", []string{`p[x] = y { q[x] = y }`, `q[k] = v { b[k] = v }`}, `{"v1": "hello", "v2": "goodbye"}`}, {"output: object embedded", []string{`p[k] = v { {k: [q[k]]} = {k: [v]} }`, `q[x] = y { b[x] = y }`}, `{"v1": "hello", "v2": "goodbye"}`}, {"output: object dereference ground", []string{`p[i] { q[i].x[1] = false }`, `q[i] = x { x = c[i] }`}, "[0]"}, @@ -1031,6 +1035,7 @@ func TestTopDownComprehensions(t *testing.T) { }, "[1,2,3,4]"}, {"object simple", []string{`p[i] { xs = {s: x | x = a[_]; format_int(x, 10, s)}; y = xs[i]; y > 1 }`}, `["2","3","4"]`}, + {"object non-string key", []string{`p[x] { xs = {k: 1 | a[_] = k}; xs[x]}`}, `[1,2,3,4]`}, {"object nested", []string{`p = r { r = {x: y | z = {i: q | i = b[q]}; x = z[y]}}`}, `{"v1": "hello", "v2": "goodbye"}`}, {"object embedded array", []string{`p[i] { xs = [{s: x | x = a[_]; format_int(x, 10, s)}]; xs[0][i] > 1 }`}, `["2","3","4"]`}, {"object embedded object", []string{`p[i] { xs = {"a": {s: x | x = a[_]; format_int(x, 10, s)}}; xs.a[i] > 1 }`}, `["2","3","4"]`},