mirror of
https://github.com/open-policy-agent/opa.git
synced 2026-08-12 19:32:48 -06:00
Add support for Go 1.27 & jsonv2 (#8947)
Makes OPA build and pass its tests on Go 1.27, while keeping Go 1.25 and 1.26 working. JSON output is unchanged on every supported version. Go 1.27 json package honours `encoding.TextAppender`. Many v1 ast types implement AppendText to build their Rego string cheaply, so on 1.27 they would have marshalled as Rego text. Files built only with 1.27 now implement MarshalJSONTo. Library users should keep using `json.Marshal` etc. The MarshalJSONTo methods are implementation details, are absent from 1.25 and 1.26 builds, and may change. --------- Signed-off-by: Anders Eknert <anders.eknert@apple.com> Signed-off-by: Charlie Egan <charlie_egan@apple.com> Co-authored-by: Charlie Egan <charlie_egan@apple.com>
This commit is contained in:
@@ -502,6 +502,47 @@ jobs:
|
||||
env:
|
||||
DOCKER_RUNNING: 0
|
||||
|
||||
# TEMPORARY JOB - safe to delete once Go 1.27 is released and OPA is updated to it
|
||||
go-1-27-compat:
|
||||
name: Go 1.27 compat build/test (${{ matrix.version }})
|
||||
needs: [generate, check-changes]
|
||||
if: ${{ needs.check-changes.outputs.go == 'true' }}
|
||||
runs-on: ubuntu-24.04
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- version: "1.26.5"
|
||||
sha256: 5c2c3b16caefa1d968a94c1daca04a7ca301a496d9b086e17ad77bb81393f053
|
||||
- version: "1.27rc2"
|
||||
sha256: e2dfdfc2b2d4092bf23d5ffb0a11221c2f3eed2d8acfc51344066b9c83a368db
|
||||
steps:
|
||||
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Download generated artifacts
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: generated
|
||||
- name: Install Go ${{ matrix.version }}
|
||||
env:
|
||||
VERSION: ${{ matrix.version }}
|
||||
SHA256: ${{ matrix.sha256 }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
tarball="go${VERSION}.linux-amd64.tar.gz"
|
||||
curl -fsSL --retry 3 -o "${RUNNER_TEMP}/${tarball}" "https://go.dev/dl/${tarball}"
|
||||
echo "${SHA256} ${RUNNER_TEMP}/${tarball}" | sha256sum --check --strict -
|
||||
mkdir -p "${RUNNER_TEMP}/toolchain"
|
||||
tar -C "${RUNNER_TEMP}/toolchain" -xzf "${RUNNER_TEMP}/${tarball}"
|
||||
echo "${RUNNER_TEMP}/toolchain/go/bin" >> "${GITHUB_PATH}"
|
||||
- name: Report Go version
|
||||
run: go version
|
||||
- run: make go-test
|
||||
env:
|
||||
DOCKER_RUNNING: 0
|
||||
GOTOOLCHAIN: local
|
||||
|
||||
# Run PR metadata against Rego policies
|
||||
rego-check-pr:
|
||||
name: Rego PR checks
|
||||
@@ -713,6 +754,7 @@ jobs:
|
||||
smoke-test-docker-images,
|
||||
smoke-test-binaries,
|
||||
go-version-build,
|
||||
go-1-27-compat,
|
||||
rego-check-pr,
|
||||
docs-build,
|
||||
docs-fmt-check,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,5 @@
|
||||
//go:build !go1.27
|
||||
|
||||
// Copyright 2020 The OPA Authors. All rights reserved.
|
||||
// Use of this source code is governed by an Apache2
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,5 @@
|
||||
//go:build !go1.27
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
@@ -17,6 +19,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/open-policy-agent/opa/internal/file/archive"
|
||||
"github.com/open-policy-agent/opa/v1/ast"
|
||||
"github.com/open-policy-agent/opa/v1/loader"
|
||||
@@ -3415,3 +3418,67 @@ Warning: .manifest file found in %q but -b flag not specified. Manifest will be
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildPlanJSONOutputBytes(t *testing.T) {
|
||||
files := map[string]string{
|
||||
"test.rego": `
|
||||
package test
|
||||
p = 1
|
||||
`,
|
||||
}
|
||||
|
||||
test.WithTempFS(files, func(root string) {
|
||||
params := newBuildParams()
|
||||
if err := params.target.Set("plan"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
params.entrypoints.v = []string{"test"}
|
||||
params.outputFile = path.Join(root, "bundle.tar.gz")
|
||||
|
||||
if err := dobuild(params, []string{root}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
f, err := os.Open(params.outputFile)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
gr, err := gzip.NewReader(f)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
tr := tar.NewReader(gr)
|
||||
var planBytes []byte
|
||||
var found bool
|
||||
|
||||
for {
|
||||
h, err := tr.Next()
|
||||
if err == io.EOF {
|
||||
break
|
||||
} else if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if h.Name == "/plan.json" {
|
||||
found = true
|
||||
if planBytes, err = io.ReadAll(tr); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
t.Fatal("plan.json not found")
|
||||
}
|
||||
|
||||
got := strings.ReplaceAll(string(planBytes), root, "TEMPDIR")
|
||||
|
||||
expected := `{"static":{"strings":[{"value":"result"},{"value":"p"},{"value":"1"},{"value":"test"}],"files":[{"value":"TEMPDIR/test.rego"}]},"plans":{"plans":[{"name":"test","blocks":[{"stmts":[{"type":"MakeObjectStmt","stmt":{"target":2,"file":0,"col":0,"row":0}},{"type":"BlockStmt","stmt":{"blocks":[{"stmts":[{"type":"CallStmt","stmt":{"func":"g0.data.test.p","args":[{"type":"local","value":0},{"type":"local","value":1}],"result":3,"file":0,"col":0,"row":0}},{"type":"ObjectInsertStmt","stmt":{"key":{"type":"string_index","value":1},"value":{"type":"local","value":3},"object":2,"file":0,"col":0,"row":0}}]}],"file":0,"col":0,"row":0}},{"type":"BlockStmt","stmt":{"blocks":[{"stmts":[{"type":"BlockStmt","stmt":{"blocks":[{"stmts":[{"type":"DotStmt","stmt":{"source":{"type":"local","value":1},"key":{"type":"string_index","value":3},"target":5,"file":0,"col":0,"row":0}},{"type":"ObjectMergeStmt","stmt":{"a":5,"b":2,"target":4,"file":0,"col":0,"row":0}},{"type":"BreakStmt","stmt":{"index":1,"file":0,"col":0,"row":0}}]}],"file":0,"col":0,"row":0}},{"type":"AssignVarStmt","stmt":{"source":{"type":"local","value":2},"target":4,"file":0,"col":0,"row":0}}]}],"file":0,"col":0,"row":0}},{"type":"AssignVarStmt","stmt":{"source":{"type":"local","value":4},"target":6,"file":0,"col":0,"row":0}},{"type":"MakeObjectStmt","stmt":{"target":7,"file":0,"col":0,"row":0}},{"type":"ObjectInsertStmt","stmt":{"key":{"type":"string_index","value":0},"value":{"type":"local","value":6},"object":7,"file":0,"col":0,"row":0}},{"type":"ResultSetAddStmt","stmt":{"value":7,"file":0,"col":0,"row":0}}]}]}]},"funcs":{"funcs":[{"name":"g0.data.test.p","params":[0,1],"return":2,"blocks":[{"stmts":[{"type":"ResetLocalStmt","stmt":{"target":3,"file":0,"col":4,"row":3}},{"type":"MakeNumberRefStmt","stmt":{"file":0,"col":4,"row":3,"index":2,"Index":2,"target":4}},{"type":"AssignVarOnceStmt","stmt":{"source":{"type":"local","value":4},"target":3,"file":0,"col":4,"row":3}}]},{"stmts":[{"type":"IsDefinedStmt","stmt":{"source":3,"file":0,"col":4,"row":3}},{"type":"AssignVarOnceStmt","stmt":{"source":{"type":"local","value":3},"target":2,"file":0,"col":4,"row":3}}]},{"stmts":[{"type":"ReturnLocalStmt","stmt":{"source":2,"file":0,"col":4,"row":3}}]}],"path":["g0","test","p"]}]}}`
|
||||
|
||||
if diff := cmp.Diff(expected, got); diff != "" {
|
||||
t.Errorf("unexpected result (-want, +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
//go:build go1.27
|
||||
|
||||
// Copyright 2022 The OPA Authors. All rights reserved.
|
||||
// Use of this source code is governed by an Apache2
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"path"
|
||||
"slices"
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/open-policy-agent/opa/v1/ast"
|
||||
"github.com/open-policy-agent/opa/v1/util/test"
|
||||
)
|
||||
|
||||
func TestCapabilitiesNoArgs(t *testing.T) {
|
||||
t.Run("test with no arguments", func(t *testing.T) {
|
||||
_, err := doCapabilities(capabilitiesParams{})
|
||||
if err != nil {
|
||||
t.Fatal("expected success", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestCapabilitiesVersion(t *testing.T) {
|
||||
t.Run("test with version", func(t *testing.T) {
|
||||
params := capabilitiesParams{
|
||||
version: "v0.39.0",
|
||||
}
|
||||
_, err := doCapabilities(params)
|
||||
if err != nil {
|
||||
t.Fatal("expected success", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestCapabilitiesFile(t *testing.T) {
|
||||
t.Run("test with file", func(t *testing.T) {
|
||||
files := map[string]string{
|
||||
"test-capabilities.json": `
|
||||
{
|
||||
"builtins": [
|
||||
{
|
||||
"name": "plus",
|
||||
"infix": "+",
|
||||
"decl": {
|
||||
"type": "function",
|
||||
"args": [
|
||||
{
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"type": "number"
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
`,
|
||||
}
|
||||
|
||||
test.WithTempFS(files, func(root string) {
|
||||
params := capabilitiesParams{
|
||||
file: path.Join(root, "test-capabilities.json"),
|
||||
}
|
||||
_, err := doCapabilities(params)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal("expected success", err)
|
||||
}
|
||||
})
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
func TestCapabilitiesJSONOutputBytes(t *testing.T) {
|
||||
files := map[string]string{
|
||||
"test-capabilities.json": `
|
||||
{
|
||||
"builtins": [
|
||||
{
|
||||
"name": "plus",
|
||||
"infix": "+",
|
||||
"decl": {
|
||||
"type": "function",
|
||||
"args": [
|
||||
{
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"type": "number"
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
`,
|
||||
}
|
||||
|
||||
test.WithTempFS(files, func(root string) {
|
||||
params := capabilitiesParams{
|
||||
file: path.Join(root, "test-capabilities.json"),
|
||||
}
|
||||
got, err := doCapabilities(params)
|
||||
if err != nil {
|
||||
t.Fatal("expected success", err)
|
||||
}
|
||||
|
||||
expected := `{
|
||||
"builtins": [
|
||||
{
|
||||
"name": "plus",
|
||||
"decl": {
|
||||
"args": [
|
||||
{
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"type": "number"
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"type": "number"
|
||||
},
|
||||
"type": "function"
|
||||
},
|
||||
"infix": "+"
|
||||
}
|
||||
]
|
||||
}`
|
||||
|
||||
if diff := cmp.Diff(expected, got); diff != "" {
|
||||
t.Errorf("unexpected result (-want, +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestCapabilitiesCurrent(t *testing.T) {
|
||||
tests := []struct {
|
||||
note string
|
||||
v0Compatible bool
|
||||
expFeatures []string
|
||||
expFutureKeywords []string
|
||||
}{
|
||||
{
|
||||
note: "current",
|
||||
expFeatures: []string{
|
||||
ast.FeatureRegoV1,
|
||||
ast.FeatureKeywordsInRefs,
|
||||
ast.FeatureTemplateStrings,
|
||||
},
|
||||
expFutureKeywords: []string{
|
||||
"not",
|
||||
},
|
||||
},
|
||||
{
|
||||
note: "current --v0-compatible",
|
||||
v0Compatible: true,
|
||||
expFeatures: []string{
|
||||
ast.FeatureRefHeadStringPrefixes,
|
||||
ast.FeatureRefHeads,
|
||||
ast.FeatureRegoV1Import,
|
||||
ast.FeatureRegoV1,
|
||||
ast.FeatureKeywordsInRefs,
|
||||
},
|
||||
expFutureKeywords: []string{
|
||||
"in",
|
||||
"every",
|
||||
"contains",
|
||||
"if",
|
||||
"not",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.note, func(t *testing.T) {
|
||||
// These are sorted in the output
|
||||
sort.Strings(tc.expFutureKeywords)
|
||||
sort.Strings(tc.expFeatures)
|
||||
|
||||
params := capabilitiesParams{
|
||||
showCurrent: true,
|
||||
v0Compatible: tc.v0Compatible,
|
||||
}
|
||||
capsStr, err := doCapabilities(params)
|
||||
if err != nil {
|
||||
t.Fatal("expected success", err)
|
||||
}
|
||||
|
||||
caps, err := ast.LoadCapabilitiesJSON(bytes.NewReader([]byte(capsStr)))
|
||||
if err != nil {
|
||||
t.Fatal("expected success", err)
|
||||
}
|
||||
|
||||
if !slices.Equal(caps.Features, tc.expFeatures) {
|
||||
t.Errorf("expected features:\n\n%v\n\nbut got:\n\n%v", tc.expFeatures, caps.Features)
|
||||
}
|
||||
|
||||
if !slices.Equal(caps.FutureKeywords, tc.expFutureKeywords) {
|
||||
t.Errorf("expected future keywords:\n\n%v\n\nbut got:\n\n%v", tc.expFutureKeywords, caps.FutureKeywords)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build !go1.27
|
||||
|
||||
// Copyright 2022 The OPA Authors. All rights reserved.
|
||||
// Use of this source code is governed by an Apache2
|
||||
// license that can be found in the LICENSE file.
|
||||
@@ -11,6 +13,7 @@ import (
|
||||
"sort"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/open-policy-agent/opa/v1/ast"
|
||||
"github.com/open-policy-agent/opa/v1/util/test"
|
||||
)
|
||||
@@ -79,6 +82,72 @@ func TestCapabilitiesFile(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestCapabilitiesJSONOutputBytes(t *testing.T) {
|
||||
files := map[string]string{
|
||||
"test-capabilities.json": `
|
||||
{
|
||||
"builtins": [
|
||||
{
|
||||
"name": "plus",
|
||||
"infix": "+",
|
||||
"decl": {
|
||||
"type": "function",
|
||||
"args": [
|
||||
{
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"type": "number"
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"type": "number"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
`,
|
||||
}
|
||||
|
||||
test.WithTempFS(files, func(root string) {
|
||||
params := capabilitiesParams{
|
||||
file: path.Join(root, "test-capabilities.json"),
|
||||
}
|
||||
got, err := doCapabilities(params)
|
||||
if err != nil {
|
||||
t.Fatal("expected success", err)
|
||||
}
|
||||
|
||||
expected := `{
|
||||
"builtins": [
|
||||
{
|
||||
"name": "plus",
|
||||
"decl": {
|
||||
"args": [
|
||||
{
|
||||
"type": "number"
|
||||
},
|
||||
{
|
||||
"type": "number"
|
||||
}
|
||||
],
|
||||
"result": {
|
||||
"type": "number"
|
||||
},
|
||||
"type": "function"
|
||||
},
|
||||
"infix": "+"
|
||||
}
|
||||
]
|
||||
}`
|
||||
|
||||
if diff := cmp.Diff(expected, got); diff != "" {
|
||||
t.Errorf("unexpected result (-want, +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestCapabilitiesCurrent(t *testing.T) {
|
||||
tests := []struct {
|
||||
note string
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,5 @@
|
||||
//go:build !go1.27
|
||||
|
||||
// Copyright 2022 The OPA Authors. All rights reserved.
|
||||
// Use of this source code is governed by an Apache2
|
||||
// license that can be found in the LICENSE file.
|
||||
@@ -5,6 +7,7 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"maps"
|
||||
@@ -14,7 +17,10 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
|
||||
"github.com/open-policy-agent/opa/internal/file/archive"
|
||||
pr "github.com/open-policy-agent/opa/internal/presentation"
|
||||
"github.com/open-policy-agent/opa/v1/ast"
|
||||
"github.com/open-policy-agent/opa/v1/util/test"
|
||||
)
|
||||
@@ -292,6 +298,46 @@ func TestCheckFailsOnInvalidRego(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestCheckJSONOutputBytes(t *testing.T) {
|
||||
files := map[string]string{
|
||||
"test.rego": `package test
|
||||
{}`,
|
||||
}
|
||||
|
||||
test.WithTempFS(files, func(root string) {
|
||||
params := newCheckParams()
|
||||
|
||||
checkErr := checkModules(params, []string{root})
|
||||
if checkErr == nil {
|
||||
t.Fatal("expected error but received none")
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := pr.JSON(&buf, pr.Output{Errors: pr.NewOutputErrors(checkErr)}); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
expected := strings.ReplaceAll(`{
|
||||
"errors": [
|
||||
{
|
||||
"message": "object cannot be used for rule name",
|
||||
"code": "rego_parse_error",
|
||||
"location": {
|
||||
"file": "TEMPDIR/test.rego",
|
||||
"row": 2,
|
||||
"col": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
`, "TEMPDIR", root)
|
||||
|
||||
if diff := cmp.Diff(expected, buf.String()); diff != "" {
|
||||
t.Errorf("unexpected result (-want, +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Assert that 'schemas' annotations with schema refs are only informing the type checker when the --schema flag is used
|
||||
func TestCheckWithSchemasAnnotationButNoSchemaFlag(t *testing.T) {
|
||||
policiesWithSchemaRef := []string{`
|
||||
|
||||
@@ -0,0 +1,634 @@
|
||||
//go:build go1.27
|
||||
|
||||
// Copyright 2024 The OPA Authors. All rights reserved.
|
||||
// Use of this source code is governed by an Apache2
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"maps"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/open-policy-agent/opa/cmd/formats"
|
||||
"github.com/open-policy-agent/opa/internal/file/archive"
|
||||
"github.com/open-policy-agent/opa/v1/util/test"
|
||||
)
|
||||
|
||||
func TestDepsJSONOutputBytes(t *testing.T) {
|
||||
files := map[string]string{
|
||||
"test.rego": `package test
|
||||
p if { input.x }`,
|
||||
}
|
||||
|
||||
test.WithTempFS(files, func(rootPath string) {
|
||||
params := newDepsCommandParams()
|
||||
_ = params.outputFormat.Set(formats.JSON)
|
||||
|
||||
for f := range files {
|
||||
_ = params.dataPaths.Set(filepath.Join(rootPath, f))
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := deps([]string{"data.test.p"}, params, &buf); err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
expectedOutput := `{
|
||||
"base": [
|
||||
[
|
||||
{
|
||||
"type": "var",
|
||||
"value": "input"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"value": "x"
|
||||
}
|
||||
]
|
||||
],
|
||||
"virtual": [
|
||||
[
|
||||
{
|
||||
"type": "var",
|
||||
"value": "data"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"value": "test"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"value": "p"
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
`
|
||||
|
||||
if diff := cmp.Diff(expectedOutput, buf.String()); diff != "" {
|
||||
t.Errorf("unexpected result (-want, +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestDeps_DefaultRegoVersion(t *testing.T) {
|
||||
tests := []struct {
|
||||
note string
|
||||
module string
|
||||
query string
|
||||
expErrs []string
|
||||
}{
|
||||
{
|
||||
note: "v0 module",
|
||||
module: `package test
|
||||
a[x] {
|
||||
x := 42
|
||||
}`,
|
||||
query: `data.test.p`,
|
||||
expErrs: []string{
|
||||
"test.rego:2: rego_parse_error: `if` keyword is required before rule body",
|
||||
"test.rego:2: rego_parse_error: `contains` keyword is required for partial set rules",
|
||||
},
|
||||
},
|
||||
{
|
||||
note: "v1 module",
|
||||
module: `package test
|
||||
a contains x if {
|
||||
x := 42
|
||||
}`,
|
||||
query: `data.test.a`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.note, func(t *testing.T) {
|
||||
files := map[string]string{
|
||||
"test.rego": tc.module,
|
||||
}
|
||||
|
||||
test.WithTempFS(files, func(rootPath string) {
|
||||
params := newDepsCommandParams()
|
||||
_ = params.outputFormat.Set(formats.Pretty)
|
||||
|
||||
for f := range files {
|
||||
_ = params.dataPaths.Set(filepath.Join(rootPath, f))
|
||||
}
|
||||
|
||||
err := deps([]string{tc.query}, params, io.Discard)
|
||||
|
||||
if len(tc.expErrs) > 0 {
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error but got nil")
|
||||
}
|
||||
for _, expErr := range tc.expErrs {
|
||||
if !strings.Contains(err.Error(), expErr) {
|
||||
t.Fatalf("Expected error:\n\n%s\n\ngot:\n\n%s", expErr, err.Error())
|
||||
}
|
||||
}
|
||||
} else if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDepsCompatibleFlags(t *testing.T) {
|
||||
tests := []struct {
|
||||
note string
|
||||
v0Compatible bool
|
||||
v1Compatible bool
|
||||
module string
|
||||
query string
|
||||
expErrs []string
|
||||
}{
|
||||
{
|
||||
note: "v0, no keywords",
|
||||
v0Compatible: true,
|
||||
module: `package test
|
||||
p[3] {
|
||||
input.x = 1
|
||||
}`,
|
||||
query: `data.test.p`,
|
||||
},
|
||||
{
|
||||
note: "v0, keywords not imported, but used",
|
||||
v0Compatible: true,
|
||||
module: `package test
|
||||
p contains 3 if {
|
||||
input.x = 1
|
||||
}`,
|
||||
query: `data.test.p`,
|
||||
expErrs: []string{
|
||||
"rego_parse_error: var cannot be used for rule name",
|
||||
"rego_parse_error: number cannot be used for rule name",
|
||||
},
|
||||
},
|
||||
{
|
||||
note: "v0, keywords imported",
|
||||
v0Compatible: true,
|
||||
module: `package test
|
||||
import future.keywords
|
||||
p contains 3 if {
|
||||
input.x = 1
|
||||
}`,
|
||||
query: `data.test.p`,
|
||||
},
|
||||
{
|
||||
note: "v0, rego.v1 imported",
|
||||
v0Compatible: true,
|
||||
module: `package test
|
||||
import rego.v1
|
||||
p contains 3 if {
|
||||
input.x = 1
|
||||
}`,
|
||||
query: `data.test.p`,
|
||||
},
|
||||
{
|
||||
note: "v1, no keywords",
|
||||
v1Compatible: true,
|
||||
module: `package test
|
||||
p[3] {
|
||||
input.x = 1
|
||||
}`,
|
||||
query: `data.test.p`,
|
||||
expErrs: []string{
|
||||
"rego_parse_error: `if` keyword is required before rule body",
|
||||
"rego_parse_error: `contains` keyword is required for partial set rules",
|
||||
},
|
||||
},
|
||||
{
|
||||
note: "v1, no keyword imports",
|
||||
v1Compatible: true,
|
||||
module: `package test
|
||||
p contains 3 if {
|
||||
input.x = 1
|
||||
}`,
|
||||
query: `data.test.p`,
|
||||
},
|
||||
{
|
||||
note: "v1, keywords imported",
|
||||
v1Compatible: true,
|
||||
module: `package test
|
||||
import future.keywords
|
||||
p contains 3 if {
|
||||
input.x = 1
|
||||
}`,
|
||||
query: `data.test.p`,
|
||||
},
|
||||
{
|
||||
note: "v1, rego.v1 imported",
|
||||
v1Compatible: true,
|
||||
module: `package test
|
||||
import rego.v1
|
||||
p contains 3 if {
|
||||
input.x = 1
|
||||
}`,
|
||||
query: `data.test.p`,
|
||||
},
|
||||
// v0 takes precedence over v1
|
||||
{
|
||||
note: "v0+v1, no keywords",
|
||||
v0Compatible: true,
|
||||
v1Compatible: true,
|
||||
module: `package test
|
||||
p[3] {
|
||||
input.x = 1
|
||||
}`,
|
||||
query: `data.test.p`,
|
||||
},
|
||||
{
|
||||
note: "v0+v1, keywords not imported, but used",
|
||||
v0Compatible: true,
|
||||
v1Compatible: true,
|
||||
module: `package test
|
||||
p contains 3 if {
|
||||
input.x = 1
|
||||
}`,
|
||||
query: `data.test.p`,
|
||||
expErrs: []string{
|
||||
"rego_parse_error: var cannot be used for rule name",
|
||||
"rego_parse_error: number cannot be used for rule name",
|
||||
},
|
||||
},
|
||||
{
|
||||
note: "v0+v1, keywords imported",
|
||||
v0Compatible: true,
|
||||
v1Compatible: true,
|
||||
module: `package test
|
||||
import future.keywords
|
||||
p contains 3 if {
|
||||
input.x = 1
|
||||
}`,
|
||||
query: `data.test.p`,
|
||||
},
|
||||
{
|
||||
note: "v0+v1, rego.v1 imported",
|
||||
v0Compatible: true,
|
||||
v1Compatible: true,
|
||||
module: `package test
|
||||
import rego.v1
|
||||
p contains 3 if {
|
||||
input.x = 1
|
||||
}`,
|
||||
query: `data.test.p`,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.note, func(t *testing.T) {
|
||||
files := map[string]string{
|
||||
"test.rego": tc.module,
|
||||
}
|
||||
|
||||
test.WithTempFS(files, func(rootPath string) {
|
||||
params := newDepsCommandParams()
|
||||
params.v0Compatible = tc.v0Compatible
|
||||
params.v1Compatible = tc.v1Compatible
|
||||
_ = params.outputFormat.Set(formats.Pretty)
|
||||
|
||||
for f := range files {
|
||||
_ = params.dataPaths.Set(filepath.Join(rootPath, f))
|
||||
}
|
||||
|
||||
err := deps([]string{tc.query}, params, io.Discard)
|
||||
|
||||
if len(tc.expErrs) > 0 {
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error but got nil")
|
||||
}
|
||||
for _, expErr := range tc.expErrs {
|
||||
if !strings.Contains(err.Error(), expErr) {
|
||||
t.Fatalf("Expected error:\n\n%s\n\ngot:\n\n%s", expErr, err.Error())
|
||||
}
|
||||
}
|
||||
} else if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestDepsV1WithBundleRegoVersion(t *testing.T) {
|
||||
tests := []struct {
|
||||
note string
|
||||
files map[string]string
|
||||
query string
|
||||
expErrs []string
|
||||
}{
|
||||
{
|
||||
note: "v0.x bundle, no keywords",
|
||||
files: map[string]string{
|
||||
".manifest": `{"rego_version": 0}`,
|
||||
"policy.rego": `package test
|
||||
p[3] {
|
||||
input.x = 1
|
||||
}`,
|
||||
},
|
||||
query: `data.test.p`,
|
||||
},
|
||||
{
|
||||
note: "v0.x bundle, keywords not imported, but used",
|
||||
files: map[string]string{
|
||||
".manifest": `{"rego_version": 0}`,
|
||||
"policy.rego": `package test
|
||||
p contains 3 if {
|
||||
input.x = 1
|
||||
}`,
|
||||
},
|
||||
query: `data.test.p`,
|
||||
expErrs: []string{
|
||||
"rego_parse_error: var cannot be used for rule name",
|
||||
"rego_parse_error: number cannot be used for rule name",
|
||||
},
|
||||
},
|
||||
{
|
||||
note: "v0.x bundle, keywords imported",
|
||||
files: map[string]string{
|
||||
".manifest": `{"rego_version": 0}`,
|
||||
"policy.rego": `package test
|
||||
import future.keywords
|
||||
p contains 3 if {
|
||||
input.x = 1
|
||||
}`,
|
||||
},
|
||||
query: `data.test.p`,
|
||||
},
|
||||
{
|
||||
note: "v0.x bundle, rego.v1 imported",
|
||||
files: map[string]string{
|
||||
".manifest": `{"rego_version": 0}`,
|
||||
"policy.rego": `package test
|
||||
import rego.v1
|
||||
p contains 3 if {
|
||||
input.x = 1
|
||||
}`,
|
||||
},
|
||||
query: `data.test.p`,
|
||||
},
|
||||
{
|
||||
note: "v0 bundle, v1 per-file override",
|
||||
files: map[string]string{
|
||||
".manifest": `{
|
||||
"rego_version": 0,
|
||||
"file_rego_versions": {
|
||||
"/policy2.rego": 1
|
||||
}
|
||||
}`,
|
||||
"policy1.rego": `package test
|
||||
p[3] {
|
||||
input.x = 1
|
||||
}`,
|
||||
"policy2.rego": `package test
|
||||
p contains 4 if {
|
||||
input.x = 1
|
||||
}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
note: "v0 bundle, v1 per-file override (glob)",
|
||||
files: map[string]string{
|
||||
".manifest": `{
|
||||
"rego_version": 0,
|
||||
"file_rego_versions": {
|
||||
"/bar/*.rego": 1
|
||||
}
|
||||
}`,
|
||||
"foo/policy1.rego": `package test
|
||||
p[3] {
|
||||
input.x = 1
|
||||
}`,
|
||||
"bar/policy2.rego": `package test
|
||||
p contains 4 if {
|
||||
input.x = 1
|
||||
}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
note: "v0 bundle, v1 per-file override, incompliant",
|
||||
files: map[string]string{
|
||||
".manifest": `{
|
||||
"rego_version": 0,
|
||||
"file_rego_versions": {
|
||||
"/policy2.rego": 1
|
||||
}
|
||||
}`,
|
||||
"policy1.rego": `package test
|
||||
p[3] {
|
||||
input.x = 1
|
||||
}`,
|
||||
"policy2.rego": `package test
|
||||
p[4] {
|
||||
input.x = 1
|
||||
}`,
|
||||
},
|
||||
expErrs: []string{
|
||||
"rego_parse_error: `if` keyword is required before rule body",
|
||||
"rego_parse_error: `contains` keyword is required for partial set rules",
|
||||
},
|
||||
},
|
||||
{
|
||||
note: "v1.0 bundle, no keywords",
|
||||
files: map[string]string{
|
||||
".manifest": `{"rego_version": 1}`,
|
||||
"policy.rego": `package test
|
||||
p[3] {
|
||||
input.x = 1
|
||||
}`,
|
||||
},
|
||||
query: `data.test.p`,
|
||||
expErrs: []string{
|
||||
"rego_parse_error: `if` keyword is required before rule body",
|
||||
"rego_parse_error: `contains` keyword is required for partial set rules",
|
||||
},
|
||||
},
|
||||
{
|
||||
note: "v1.0 bundle, no keyword imports",
|
||||
files: map[string]string{
|
||||
".manifest": `{"rego_version": 1}`,
|
||||
"policy.rego": `package test
|
||||
p contains 3 if {
|
||||
input.x = 1
|
||||
}`,
|
||||
},
|
||||
query: `data.test.p`,
|
||||
},
|
||||
{
|
||||
note: "v1.0 bundle, keywords imported",
|
||||
files: map[string]string{
|
||||
".manifest": `{"rego_version": 1}`,
|
||||
"policy.rego": `package test
|
||||
import future.keywords
|
||||
p contains 3 if {
|
||||
input.x = 1
|
||||
}`,
|
||||
},
|
||||
query: `data.test.p`,
|
||||
},
|
||||
{
|
||||
note: "v1.0 bundle, rego.v1 imported",
|
||||
files: map[string]string{
|
||||
".manifest": `{"rego_version": 1}`,
|
||||
"policy.rego": `package test
|
||||
import rego.v1
|
||||
p contains 3 if {
|
||||
input.x = 1
|
||||
}`,
|
||||
},
|
||||
query: `data.test.p`,
|
||||
},
|
||||
{
|
||||
note: "v1 bundle, v0 per-file override",
|
||||
files: map[string]string{
|
||||
".manifest": `{
|
||||
"rego_version": 1,
|
||||
"file_rego_versions": {
|
||||
"/policy1.rego": 0
|
||||
}
|
||||
}`,
|
||||
"policy1.rego": `package test
|
||||
p[3] {
|
||||
input.x = 1
|
||||
}`,
|
||||
"policy2.rego": `package test
|
||||
p contains 4 if {
|
||||
input.x = 1
|
||||
}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
note: "v1 bundle, v0 per-file override (glob)",
|
||||
files: map[string]string{
|
||||
".manifest": `{
|
||||
"rego_version": 1,
|
||||
"file_rego_versions": {
|
||||
"/foo/*.rego": 0
|
||||
}
|
||||
}`,
|
||||
"foo/policy1.rego": `package test
|
||||
p[3] {
|
||||
input.x = 1
|
||||
}`,
|
||||
"bar/policy2.rego": `package test
|
||||
p contains 4 if {
|
||||
input.x = 1
|
||||
}`,
|
||||
},
|
||||
},
|
||||
{
|
||||
note: "v1 bundle, v0 per-file override, incompliant",
|
||||
files: map[string]string{
|
||||
".manifest": `{
|
||||
"rego_version": 1,
|
||||
"file_rego_versions": {
|
||||
"/policy1.rego": 0
|
||||
}
|
||||
}`,
|
||||
"policy1.rego": `package test
|
||||
p contains 3 if {
|
||||
input.x = 1
|
||||
}`,
|
||||
"policy2.rego": `package test
|
||||
p contains 4 if {
|
||||
input.x = 1
|
||||
}`,
|
||||
},
|
||||
expErrs: []string{
|
||||
"rego_parse_error: var cannot be used for rule name",
|
||||
"rego_parse_error: number cannot be used for rule name",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
bundleTypeCases := []struct {
|
||||
note string
|
||||
tar bool
|
||||
}{
|
||||
{
|
||||
"bundle dir", false,
|
||||
},
|
||||
{
|
||||
"bundle tar", true,
|
||||
},
|
||||
}
|
||||
|
||||
v1CompatibleFlagCases := []struct {
|
||||
note string
|
||||
used bool
|
||||
}{
|
||||
{
|
||||
"no --v1-compatible", false,
|
||||
},
|
||||
{
|
||||
"--v1-compatible", true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, bundleType := range bundleTypeCases {
|
||||
for _, v1CompatibleFlag := range v1CompatibleFlagCases {
|
||||
for _, tc := range tests {
|
||||
t.Run(fmt.Sprintf("%s, %s, %s", bundleType.note, v1CompatibleFlag.note, tc.note), func(t *testing.T) {
|
||||
files := map[string]string{}
|
||||
|
||||
if bundleType.tar {
|
||||
files["bundle.tar.gz"] = ""
|
||||
} else {
|
||||
maps.Copy(files, tc.files)
|
||||
}
|
||||
|
||||
test.WithTempFS(files, func(root string) {
|
||||
p := root
|
||||
if bundleType.tar {
|
||||
p = filepath.Join(root, "bundle.tar.gz")
|
||||
files := make([][2]string, 0, len(tc.files))
|
||||
for k, v := range tc.files {
|
||||
files = append(files, [2]string{k, v})
|
||||
}
|
||||
buf := archive.MustWriteTarGz(files)
|
||||
bf, err := os.Create(p)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
_, err = bf.Write(buf.Bytes())
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
params := newDepsCommandParams()
|
||||
if err := params.bundlePaths.Set(p); err != nil {
|
||||
t.Fatalf("Unexpected error: %s", err)
|
||||
}
|
||||
|
||||
params.v1Compatible = v1CompatibleFlag.used
|
||||
_ = params.outputFormat.Set(formats.Pretty)
|
||||
|
||||
err := deps([]string{tc.query}, params, io.Discard)
|
||||
|
||||
if len(tc.expErrs) > 0 {
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error but got nil")
|
||||
}
|
||||
for _, expErr := range tc.expErrs {
|
||||
if !strings.Contains(err.Error(), expErr) {
|
||||
t.Fatalf("Expected error:\n\n%s\n\ngot:\n\n%s", expErr, err.Error())
|
||||
}
|
||||
}
|
||||
} else if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build !go1.27
|
||||
|
||||
// Copyright 2024 The OPA Authors. All rights reserved.
|
||||
// Use of this source code is governed by an Apache2
|
||||
// license that can be found in the LICENSE file.
|
||||
@@ -5,6 +7,7 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"maps"
|
||||
@@ -13,11 +16,69 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/open-policy-agent/opa/cmd/formats"
|
||||
"github.com/open-policy-agent/opa/internal/file/archive"
|
||||
"github.com/open-policy-agent/opa/v1/util/test"
|
||||
)
|
||||
|
||||
func TestDepsJSONOutputBytes(t *testing.T) {
|
||||
files := map[string]string{
|
||||
"test.rego": `package test
|
||||
p if { input.x }`,
|
||||
}
|
||||
|
||||
test.WithTempFS(files, func(rootPath string) {
|
||||
params := newDepsCommandParams()
|
||||
_ = params.outputFormat.Set(formats.JSON)
|
||||
|
||||
for f := range files {
|
||||
_ = params.dataPaths.Set(filepath.Join(rootPath, f))
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := deps([]string{"data.test.p"}, params, &buf); err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
expectedOutput := `{
|
||||
"base": [
|
||||
[
|
||||
{
|
||||
"type": "var",
|
||||
"value": "input"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"value": "x"
|
||||
}
|
||||
]
|
||||
],
|
||||
"virtual": [
|
||||
[
|
||||
{
|
||||
"type": "var",
|
||||
"value": "data"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"value": "test"
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"value": "p"
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
`
|
||||
|
||||
if diff := cmp.Diff(expectedOutput, buf.String()); diff != "" {
|
||||
t.Errorf("unexpected result (-want, +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestDeps_DefaultRegoVersion(t *testing.T) {
|
||||
tests := []struct {
|
||||
note string
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Executable → Regular
+37
@@ -1,3 +1,5 @@
|
||||
//go:build !go1.27
|
||||
|
||||
// Copyright 2018 The OPA Authors. All rights reserved.
|
||||
// Use of this source code is governed by an Apache2
|
||||
// license that can be found in the LICENSE file.
|
||||
@@ -3946,3 +3948,38 @@ func TestWithQueryImports(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestEvalJSONOutputBytes(t *testing.T) {
|
||||
params := newEvalCommandParams()
|
||||
|
||||
var buf bytes.Buffer
|
||||
|
||||
defined, err := eval([]string{"1 == 1"}, params, &buf, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
if !defined {
|
||||
t.Fatal("expected result to be defined")
|
||||
}
|
||||
|
||||
expected := `{
|
||||
"result": [
|
||||
{
|
||||
"expressions": [
|
||||
{
|
||||
"value": true,
|
||||
"text": "1 == 1",
|
||||
"location": {
|
||||
"row": 1,
|
||||
"col": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
`
|
||||
if diff := cmp.Diff(expected, buf.String()); diff != "" {
|
||||
t.Fatalf("unexpected JSON output (-want +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,5 @@
|
||||
//go:build !go1.27
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
@@ -16,6 +18,8 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
|
||||
"github.com/open-policy-agent/opa/cmd/internal/exec"
|
||||
"github.com/open-policy-agent/opa/internal/file/archive"
|
||||
loggingtest "github.com/open-policy-agent/opa/v1/logging/test"
|
||||
@@ -1310,6 +1314,55 @@ func TestFailFlagCases(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestExecJSONOutputBytes(t *testing.T) {
|
||||
files := map[string]string{
|
||||
"files/test.json": `{"foo": 7}`,
|
||||
"bundle/x.rego": `package system
|
||||
import rego.v1
|
||||
|
||||
test_fun := x if {
|
||||
x = false
|
||||
x
|
||||
}
|
||||
|
||||
undefined_test if {
|
||||
test_fun
|
||||
}`,
|
||||
}
|
||||
|
||||
test.WithTempFS(files, func(dir string) {
|
||||
var buf bytes.Buffer
|
||||
params := exec.NewParams(&buf)
|
||||
_ = params.OutputFormat.Set("json")
|
||||
params.BundlePaths = []string{dir + "/bundle/"}
|
||||
params.Paths = append(params.Paths, dir+"/files/")
|
||||
params.FailDefined = true
|
||||
|
||||
if err := runExec(params); err != nil {
|
||||
t.Fatal("unexpected error in test:", err)
|
||||
}
|
||||
|
||||
actual := bytes.ReplaceAll(buf.Bytes(), []byte(dir), nil)
|
||||
|
||||
expected := `{
|
||||
"result": [
|
||||
{
|
||||
"path": "/files/test.json",
|
||||
"error": {
|
||||
"code": "opa_undefined_error",
|
||||
"message": "/system/main decision was undefined"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
`
|
||||
|
||||
if diff := cmp.Diff(expected, string(actual)); diff != "" {
|
||||
t.Errorf("unexpected result (-want, +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestExecWithInvalidInputOptions(t *testing.T) {
|
||||
tests := []struct {
|
||||
description string
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,5 @@
|
||||
//go:build !go1.27
|
||||
|
||||
// Copyright 2021 The OPA Authors. All rights reserved.
|
||||
// Use of this source code is governed by an Apache2
|
||||
// license that can be found in the LICENSE file.
|
||||
@@ -14,6 +16,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/open-policy-agent/opa/cmd/formats"
|
||||
"github.com/open-policy-agent/opa/internal/file/archive"
|
||||
"github.com/open-policy-agent/opa/v1/util"
|
||||
@@ -21,6 +24,62 @@ import (
|
||||
"github.com/open-policy-agent/opa/v1/util/test"
|
||||
)
|
||||
|
||||
func TestDoInspectJSONOutputBytes(t *testing.T) {
|
||||
files := [][2]string{
|
||||
{"/.manifest", `{"revision": "rev", "roots": ["foo", "bar", "fuz", "baz", "a", "x"]}`},
|
||||
{"/data.json", `{"x": {"y": true}, "a": {"b": {"z": true}}}`},
|
||||
{"/example/foo.rego", `package foo`},
|
||||
}
|
||||
|
||||
buf := archive.MustWriteTarGz(files)
|
||||
bundleFile := filepath.Join(t.TempDir(), "bundle.tar.gz")
|
||||
if err := os.WriteFile(bundleFile, buf.Bytes(), 0o644); err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
var out bytes.Buffer
|
||||
params := newInspectCommandParams()
|
||||
if err := params.outputFormat.Set(formats.JSON); err != nil {
|
||||
t.Fatalf("Unexpected error: %s", err)
|
||||
}
|
||||
|
||||
if err := doInspect(params, bundleFile, &out); err != nil {
|
||||
t.Fatalf("Unexpected error %v", err)
|
||||
}
|
||||
|
||||
expected := `{
|
||||
"manifest": {
|
||||
"revision": "rev",
|
||||
"roots": [
|
||||
"foo",
|
||||
"bar",
|
||||
"fuz",
|
||||
"baz",
|
||||
"a",
|
||||
"x"
|
||||
]
|
||||
},
|
||||
"signatures_config": {},
|
||||
"namespaces": {
|
||||
"data": [
|
||||
"/data.json"
|
||||
],
|
||||
"data.foo": [
|
||||
"/example/foo.rego"
|
||||
]
|
||||
},
|
||||
"capabilities": {
|
||||
"features": [
|
||||
"rego_v1"
|
||||
]
|
||||
}
|
||||
}
|
||||
`
|
||||
if diff := cmp.Diff(expected, out.String()); diff != "" {
|
||||
t.Errorf("unexpected result (-want, +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDoInspect(t *testing.T) {
|
||||
files := [][2]string{
|
||||
{"/.manifest", `{"revision": "rev", "roots": ["foo", "bar", "fuz", "baz", "a", "x"]}`},
|
||||
|
||||
@@ -0,0 +1,244 @@
|
||||
//go:build go1.27
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/open-policy-agent/opa/v1/util"
|
||||
"github.com/open-policy-agent/opa/v1/util/test"
|
||||
)
|
||||
|
||||
func TestOracleFindDefinition(t *testing.T) {
|
||||
cases := []struct {
|
||||
note string
|
||||
v0Compatible bool
|
||||
onDiskModule string
|
||||
stdin string
|
||||
paths []string
|
||||
}{
|
||||
{
|
||||
note: "v0",
|
||||
v0Compatible: true,
|
||||
onDiskModule: `package test
|
||||
|
||||
p { r }
|
||||
|
||||
r = true`,
|
||||
stdin: `package test
|
||||
|
||||
p { q }
|
||||
|
||||
q = true`,
|
||||
paths: []string{
|
||||
"test.rego:10",
|
||||
"test.rego:15",
|
||||
"test.rego:18",
|
||||
},
|
||||
},
|
||||
{
|
||||
note: "v1",
|
||||
onDiskModule: `package test
|
||||
|
||||
p if { r }
|
||||
|
||||
r = true`,
|
||||
stdin: `package test
|
||||
|
||||
p if { q }
|
||||
|
||||
q = true`,
|
||||
paths: []string{
|
||||
"test.rego:10",
|
||||
"test.rego:15",
|
||||
"test.rego:21",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.note, func(t *testing.T) {
|
||||
stdin := bytes.NewBufferString(tc.stdin)
|
||||
|
||||
files := map[string]string{
|
||||
"test.rego": tc.onDiskModule,
|
||||
"document.txt": "this should not be included",
|
||||
"ignore.json": `{"neither": "should this"}`,
|
||||
}
|
||||
|
||||
test.WithTempFS(files, func(rootDir string) {
|
||||
|
||||
params := findDefinitionParams{
|
||||
bundlePaths: repeatedStringFlag{
|
||||
v: []string{rootDir},
|
||||
isSet: true,
|
||||
},
|
||||
stdinBuffer: true,
|
||||
v0Compatible: tc.v0Compatible,
|
||||
}
|
||||
|
||||
stdout := bytes.NewBuffer(nil)
|
||||
|
||||
err := dofindDefinition(params, stdin, stdout, []string{path.Join(rootDir, tc.paths[0])})
|
||||
expectJSON(t, err, stdout, `{"error": {"code": "oracle_no_match_found"}}`)
|
||||
|
||||
err = dofindDefinition(params, stdin, stdout, []string{path.Join(rootDir, tc.paths[1])})
|
||||
expectJSON(t, err, stdout, `{"error": {"code": "oracle_no_definition_found"}}`)
|
||||
|
||||
err = dofindDefinition(params, stdin, stdout, []string{path.Join(rootDir, tc.paths[2])})
|
||||
expectJSON(t, err, stdout, fmt.Sprintf(`{"result": {
|
||||
"file": %q,
|
||||
"row": 5,
|
||||
"col": 1
|
||||
}}`, path.Join(rootDir, "test.rego")))
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestOracleFindDefinitionJSONOutputBytes(t *testing.T) {
|
||||
onDiskModule := `package test
|
||||
|
||||
p if { r }
|
||||
|
||||
r = true`
|
||||
stdin := bytes.NewBufferString(`package test
|
||||
|
||||
p if { q }
|
||||
|
||||
q = true`)
|
||||
|
||||
files := map[string]string{
|
||||
"test.rego": onDiskModule,
|
||||
"document.txt": "this should not be included",
|
||||
"ignore.json": `{"neither": "should this"}`,
|
||||
}
|
||||
|
||||
test.WithTempFS(files, func(rootDir string) {
|
||||
params := findDefinitionParams{
|
||||
bundlePaths: repeatedStringFlag{
|
||||
v: []string{rootDir},
|
||||
isSet: true,
|
||||
},
|
||||
stdinBuffer: true,
|
||||
}
|
||||
|
||||
stdout := bytes.NewBuffer(nil)
|
||||
|
||||
err := dofindDefinition(params, stdin, stdout, []string{path.Join(rootDir, "test.rego:10")})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
exp := `{
|
||||
"error": {
|
||||
"code": "oracle_no_match_found"
|
||||
}
|
||||
}
|
||||
`
|
||||
if diff := cmp.Diff(exp, stdout.String()); diff != "" {
|
||||
t.Errorf("unexpected result (-want, +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func expectJSON(t *testing.T, err error, buffer *bytes.Buffer, exp string) {
|
||||
t.Helper()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var x any
|
||||
if err := util.UnmarshalJSON(buffer.Bytes(), &x); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var y any
|
||||
if err := util.UnmarshalJSON([]byte(exp), &y); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(x, y) {
|
||||
t.Fatalf("expected %v but got %v", y, x)
|
||||
}
|
||||
buffer.Reset()
|
||||
}
|
||||
|
||||
func TestOracleParseFilenameOffset(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
input string
|
||||
wantFile string
|
||||
wantPos int
|
||||
}{
|
||||
{
|
||||
input: "x.rego:10",
|
||||
wantFile: "x.rego",
|
||||
wantPos: 10,
|
||||
},
|
||||
{
|
||||
input: "/x.rego:10",
|
||||
wantFile: "/x.rego",
|
||||
wantPos: 10,
|
||||
},
|
||||
{
|
||||
input: "x.rego:0x10",
|
||||
wantFile: "x.rego",
|
||||
wantPos: 16,
|
||||
},
|
||||
{
|
||||
input: "file://x.rego:10",
|
||||
wantFile: "x.rego",
|
||||
wantPos: 10,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.input, func(t *testing.T) {
|
||||
filename, pos, err := parseFilenameOffset(tc.input)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if tc.wantFile != filename || tc.wantPos != pos {
|
||||
t.Fatalf("expected %v:%v but got %v:%v", tc.wantFile, tc.wantPos, filename, pos)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func TestOracleParseFilenameOffsetError(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
input string
|
||||
wantErr error
|
||||
}{
|
||||
{
|
||||
input: "x.rego",
|
||||
wantErr: errors.New("expected <filename>:<offset> argument"),
|
||||
},
|
||||
{
|
||||
input: "x.rego:",
|
||||
wantErr: errors.New("invalid syntax"),
|
||||
},
|
||||
{
|
||||
input: "x.rego:3.14",
|
||||
wantErr: errors.New("invalid syntax"),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.input, func(t *testing.T) {
|
||||
_, _, err := parseFilenameOffset(tc.input)
|
||||
if err == nil || !strings.Contains(err.Error(), tc.wantErr.Error()) {
|
||||
t.Fatalf("expected %v but got %v", tc.wantErr, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build !go1.27
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
@@ -9,6 +11,7 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/open-policy-agent/opa/v1/util"
|
||||
"github.com/open-policy-agent/opa/v1/util/test"
|
||||
)
|
||||
@@ -101,6 +104,52 @@ q = true`,
|
||||
|
||||
}
|
||||
|
||||
func TestOracleFindDefinitionJSONOutputBytes(t *testing.T) {
|
||||
onDiskModule := `package test
|
||||
|
||||
p if { r }
|
||||
|
||||
r = true`
|
||||
stdin := bytes.NewBufferString(`package test
|
||||
|
||||
p if { q }
|
||||
|
||||
q = true`)
|
||||
|
||||
files := map[string]string{
|
||||
"test.rego": onDiskModule,
|
||||
"document.txt": "this should not be included",
|
||||
"ignore.json": `{"neither": "should this"}`,
|
||||
}
|
||||
|
||||
test.WithTempFS(files, func(rootDir string) {
|
||||
params := findDefinitionParams{
|
||||
bundlePaths: repeatedStringFlag{
|
||||
v: []string{rootDir},
|
||||
isSet: true,
|
||||
},
|
||||
stdinBuffer: true,
|
||||
}
|
||||
|
||||
stdout := bytes.NewBuffer(nil)
|
||||
|
||||
err := dofindDefinition(params, stdin, stdout, []string{path.Join(rootDir, "test.rego:10")})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
exp := `{
|
||||
"error": {
|
||||
"code": "oracle_no_match_found"
|
||||
}
|
||||
}
|
||||
`
|
||||
if diff := cmp.Diff(exp, stdout.String()); diff != "" {
|
||||
t.Errorf("unexpected result (-want, +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func expectJSON(t *testing.T, err error, buffer *bytes.Buffer, exp string) {
|
||||
t.Helper()
|
||||
if err != nil {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+20
-63
@@ -1,3 +1,5 @@
|
||||
//go:build !go1.27
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
@@ -12,7 +14,6 @@ import (
|
||||
)
|
||||
|
||||
func TestParseExit0(t *testing.T) {
|
||||
|
||||
files := map[string]string{
|
||||
"x.rego": `package x
|
||||
|
||||
@@ -62,7 +63,6 @@ func TestParseExit1(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseJSONOutput(t *testing.T) {
|
||||
|
||||
files := map[string]string{
|
||||
"x.rego": `package x
|
||||
|
||||
@@ -121,13 +121,12 @@ func TestParseJSONOutput(t *testing.T) {
|
||||
}
|
||||
`
|
||||
|
||||
if got, want := string(stdout), expectedOutput; got != want {
|
||||
t.Fatalf("Expected output\n%v\n, got\n%v", want, got)
|
||||
if diff := cmp.Diff(expectedOutput, string(stdout)); diff != "" {
|
||||
t.Errorf("unexpected result (-want, +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseJSONOutputWithLocations(t *testing.T) {
|
||||
|
||||
files := map[string]string{
|
||||
"x.rego": `package x
|
||||
|
||||
@@ -241,21 +240,8 @@ p = 1
|
||||
}
|
||||
`, "TEMPDIR", tempDirPath)
|
||||
|
||||
gotLines := strings.Split(string(stdout), "\n")
|
||||
wantLines := strings.Split(expectedOutput, "\n")
|
||||
min := len(gotLines)
|
||||
if len(wantLines) < min {
|
||||
min = len(wantLines)
|
||||
}
|
||||
|
||||
for i := range min {
|
||||
if gotLines[i] != wantLines[i] {
|
||||
t.Fatalf("Expected line %d to be\n%v\n, got\n%v", i, wantLines[i], gotLines[i])
|
||||
}
|
||||
}
|
||||
|
||||
if len(gotLines) != len(wantLines) {
|
||||
t.Fatalf("Expected %d lines, got %d", len(wantLines), len(gotLines))
|
||||
if diff := cmp.Diff(expectedOutput, string(stdout)); diff != "" {
|
||||
t.Errorf("unexpected result (-want, +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -633,7 +619,6 @@ func TestParseOutputWithNotImport(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestParseRefsJSONOutput(t *testing.T) {
|
||||
|
||||
files := map[string]string{
|
||||
"x.rego": `package x
|
||||
|
||||
@@ -700,13 +685,12 @@ func TestParseRefsJSONOutput(t *testing.T) {
|
||||
}
|
||||
`
|
||||
|
||||
if got, want := string(stdout), expectedOutput; got != want {
|
||||
t.Fatalf("Expected output\n%v\n, got\n%v", want, got)
|
||||
if diff := cmp.Diff(expectedOutput, string(stdout)); diff != "" {
|
||||
t.Errorf("unexpected result (-want, +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseRefsJSONOutputWithLocations(t *testing.T) {
|
||||
|
||||
files := map[string]string{
|
||||
"x.rego": `package x
|
||||
|
||||
@@ -840,21 +824,8 @@ a.b.c := true
|
||||
}
|
||||
`, "TEMPDIR", tempDirPath)
|
||||
|
||||
gotLines := strings.Split(string(stdout), "\n")
|
||||
wantLines := strings.Split(expectedOutput, "\n")
|
||||
min := len(gotLines)
|
||||
if len(wantLines) < min {
|
||||
min = len(wantLines)
|
||||
}
|
||||
|
||||
for i := range min {
|
||||
if gotLines[i] != wantLines[i] {
|
||||
t.Fatalf("Expected line %d to be\n%v\n, got\n%v", i, wantLines[i], gotLines[i])
|
||||
}
|
||||
}
|
||||
|
||||
if len(gotLines) != len(wantLines) {
|
||||
t.Fatalf("Expected %d lines, got %d", len(wantLines), len(gotLines))
|
||||
if diff := cmp.Diff(expectedOutput, string(stdout)); diff != "" {
|
||||
t.Errorf("unexpected result (-want, +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
func TestParseRulesBlockJSONOutputWithLocations(t *testing.T) {
|
||||
@@ -1301,26 +1272,12 @@ allow = true if {
|
||||
}
|
||||
`, "TEMPDIR", tempDirPath)
|
||||
|
||||
gotLines := strings.Split(string(stdout), "\n")
|
||||
wantLines := strings.Split(expectedOutput, "\n")
|
||||
min := len(gotLines)
|
||||
if len(wantLines) < min {
|
||||
min = len(wantLines)
|
||||
}
|
||||
|
||||
for i := range min {
|
||||
if gotLines[i] != wantLines[i] {
|
||||
t.Fatalf("Expected line %d to be\n%v\n, got\n%v", i, wantLines[i], gotLines[i])
|
||||
}
|
||||
}
|
||||
|
||||
if len(gotLines) != len(wantLines) {
|
||||
t.Fatalf("Expected %d lines, got %d", len(wantLines), len(gotLines))
|
||||
if diff := cmp.Diff(expectedOutput, string(stdout)); diff != "" {
|
||||
t.Errorf("unexpected result (-want, +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseJSONOutputComments(t *testing.T) {
|
||||
|
||||
files := map[string]string{
|
||||
"x.rego": `package x
|
||||
|
||||
@@ -1560,15 +1517,15 @@ func testParse(t *testing.T, files map[string]string, params *parseParams) (int,
|
||||
var errc int
|
||||
|
||||
var tempDirUsed string
|
||||
test.WithTempFS(files, func(path string) {
|
||||
args := make([]string, 0, len(files))
|
||||
for file := range files {
|
||||
args = append(args, filepath.Join(path, file))
|
||||
}
|
||||
errc = parse(args, params, stdout, stderr)
|
||||
path := test.TempDir(t, files)
|
||||
|
||||
tempDirUsed = path
|
||||
})
|
||||
args := make([]string, 0, len(files))
|
||||
for file := range files {
|
||||
args = append(args, filepath.Join(path, file))
|
||||
}
|
||||
errc = parse(args, params, stdout, stderr)
|
||||
|
||||
tempDirUsed = path
|
||||
|
||||
return errc, stdout.Bytes(), stderr.Bytes(), tempDirUsed
|
||||
}
|
||||
|
||||
@@ -0,0 +1,510 @@
|
||||
//go:build go1.27
|
||||
|
||||
// Copyright 2020 The OPA Authors. All rights reserved.
|
||||
// Use of this source code is governed by an Apache2
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
|
||||
internal_logging "github.com/open-policy-agent/opa/internal/logging"
|
||||
"github.com/open-policy-agent/opa/v1/logging"
|
||||
"github.com/open-policy-agent/opa/v1/repl"
|
||||
"github.com/open-policy-agent/opa/v1/storage/inmem"
|
||||
"github.com/open-policy-agent/opa/v1/test/e2e"
|
||||
"github.com/open-policy-agent/opa/v1/util/test"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func TestREPLJSONOutputBytes(t *testing.T) {
|
||||
store := inmem.New()
|
||||
var buf bytes.Buffer
|
||||
r := repl.New(store, "", &buf, "json", 0, "")
|
||||
|
||||
ctx := context.Background()
|
||||
if err := r.OneShot(ctx, "1 == 1"); err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
expected := `{
|
||||
"result": [
|
||||
{
|
||||
"expressions": [
|
||||
{
|
||||
"value": true,
|
||||
"text": "1 == 1",
|
||||
"location": {
|
||||
"row": 1,
|
||||
"col": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
`
|
||||
|
||||
if diff := cmp.Diff(expected, buf.String()); diff != "" {
|
||||
t.Errorf("unexpected result (-want, +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunServerBase(t *testing.T) {
|
||||
params := newTestRunParams()
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
|
||||
rt, err := initRuntime(ctx, params, nil, false)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
testRuntime := e2e.WrapRuntime(ctx, cancel, rt)
|
||||
|
||||
done := make(chan bool)
|
||||
go func() {
|
||||
err := rt.Serve(ctx)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %s", err)
|
||||
}
|
||||
done <- true
|
||||
}()
|
||||
|
||||
err = testRuntime.WaitForServer()
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %s", err)
|
||||
}
|
||||
|
||||
validateBasicServe(t, testRuntime)
|
||||
|
||||
cancel()
|
||||
<-done
|
||||
}
|
||||
|
||||
func TestRunServerBaseListenOnLocalhost(t *testing.T) {
|
||||
params := newTestRunParams()
|
||||
params.rt.V1Compatible = true
|
||||
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
|
||||
rt, err := initRuntime(ctx, params, nil, true)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
testRuntime := e2e.WrapRuntime(ctx, cancel, rt)
|
||||
|
||||
done := make(chan bool)
|
||||
go func() {
|
||||
err := rt.Serve(ctx)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %s", err)
|
||||
}
|
||||
done <- true
|
||||
}()
|
||||
|
||||
err = testRuntime.WaitForServer()
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %s", err)
|
||||
}
|
||||
|
||||
validateBasicServe(t, testRuntime)
|
||||
|
||||
if len(rt.Addrs()) != 1 {
|
||||
t.Fatalf("Expected 1 listening address but got %v", len(rt.Addrs()))
|
||||
}
|
||||
|
||||
expected := "127.0.0.1:8181"
|
||||
if rt.Addrs()[0] != expected {
|
||||
t.Fatalf("Expected listening address %v but got %v", expected, rt.Addrs()[0])
|
||||
}
|
||||
|
||||
cancel()
|
||||
<-done
|
||||
}
|
||||
|
||||
func TestRunServerWithDiagnosticAddr(t *testing.T) {
|
||||
params := newTestRunParams()
|
||||
params.rt.DiagnosticAddrs = &[]string{"localhost:0"}
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
|
||||
rt, err := initRuntime(ctx, params, nil, false)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
testRuntime := e2e.WrapRuntime(ctx, cancel, rt)
|
||||
|
||||
done := make(chan bool)
|
||||
go func() {
|
||||
err := rt.Serve(ctx)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %s", err)
|
||||
}
|
||||
done <- true
|
||||
}()
|
||||
|
||||
err = testRuntime.WaitForServer()
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %s", err)
|
||||
}
|
||||
|
||||
validateBasicServe(t, testRuntime)
|
||||
|
||||
diagURL, err := testRuntime.AddrToURL(rt.DiagnosticAddrs()[0])
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %s", err)
|
||||
}
|
||||
if err := testRuntime.HealthCheck(diagURL); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
cancel()
|
||||
<-done
|
||||
}
|
||||
|
||||
func TestInitRuntimeVerifyNonBundle(t *testing.T) {
|
||||
|
||||
params := newTestRunParams()
|
||||
params.pubKey = "secret"
|
||||
params.serverMode = false
|
||||
|
||||
_, err := initRuntime(t.Context(), params, nil, false)
|
||||
if err == nil {
|
||||
t.Fatal("Expected error but got nil")
|
||||
}
|
||||
|
||||
exp := "enable bundle mode (ie. --bundle) to verify bundle files or directories"
|
||||
if err.Error() != exp {
|
||||
t.Fatalf("expected error message %v but got %v", exp, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitRuntimeCipherSuites(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
cipherSuites []string
|
||||
expErr bool
|
||||
expCipherSuites []uint16
|
||||
}{
|
||||
{"no cipher suites", []string{}, false, []uint16{}},
|
||||
{"secure and insecure cipher suites", []string{"TLS_RSA_WITH_AES_128_CBC_SHA", "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", "TLS_RSA_WITH_RC4_128_SHA"}, false, []uint16{tls.TLS_RSA_WITH_AES_128_CBC_SHA, tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, tls.TLS_RSA_WITH_RC4_128_SHA}},
|
||||
{"invalid cipher suites", []string{"foo"}, true, []uint16{}},
|
||||
{"tls 1.3 cipher suite", []string{"TLS_AES_128_GCM_SHA256"}, true, []uint16{}},
|
||||
{"tls 1.2-1.3 cipher suite", []string{"TLS_RSA_WITH_AES_128_GCM_SHA256", "TLS_AES_128_GCM_SHA256"}, true, []uint16{}},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
|
||||
params := newTestRunParams()
|
||||
|
||||
if len(tc.cipherSuites) != 0 {
|
||||
params.cipherSuites = tc.cipherSuites
|
||||
}
|
||||
|
||||
rt, err := initRuntime(t.Context(), params, nil, false)
|
||||
fmt.Println(err)
|
||||
|
||||
if !tc.expErr && err != nil {
|
||||
t.Fatal("Unexpected error occurred:", err)
|
||||
} else if tc.expErr && err == nil {
|
||||
t.Fatal("Expected error but got nil")
|
||||
} else if err == nil {
|
||||
if len(tc.expCipherSuites) > 0 {
|
||||
if !slices.Equal(*rt.Params.CipherSuites, tc.expCipherSuites) {
|
||||
t.Fatalf("expected cipher suites %v but got %v", tc.expCipherSuites, *rt.Params.CipherSuites)
|
||||
}
|
||||
} else {
|
||||
if rt.Params.CipherSuites != nil {
|
||||
t.Fatal("expected no value defined for cipher suites")
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitRuntimeSkipKnownSchemaCheck(t *testing.T) {
|
||||
|
||||
fs := map[string]string{
|
||||
"test/authz.rego": `package system.authz
|
||||
import rego.v1
|
||||
|
||||
default allow := false
|
||||
|
||||
allow if {
|
||||
input.identty = "foo" # this is a typo
|
||||
}`,
|
||||
}
|
||||
|
||||
test.WithTempFS(fs, func(rootDir string) {
|
||||
rootDir = filepath.Join(rootDir, "test")
|
||||
|
||||
params := newTestRunParams()
|
||||
err := params.authorization.Set("basic")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = initRuntime(t.Context(), params, []string{rootDir}, false)
|
||||
if err == nil {
|
||||
t.Fatal("Expected error but got nil")
|
||||
}
|
||||
|
||||
if !strings.Contains(err.Error(), "undefined ref: input.identty") {
|
||||
t.Errorf("Expected error \"%v\" not found", "undefined ref: input.identty")
|
||||
}
|
||||
|
||||
// skip type checking for known input schemas
|
||||
params.skipKnownSchemaCheck = true
|
||||
_, err = initRuntime(t.Context(), params, []string{rootDir}, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestRunServerUploadPolicy(t *testing.T) {
|
||||
v0Policy := `package test
|
||||
p { q["a"] }
|
||||
q[x] {
|
||||
x = "a"
|
||||
}`
|
||||
|
||||
v1Policy := `package test
|
||||
p if { q["a"] }
|
||||
q contains x if {
|
||||
x = "a"
|
||||
}`
|
||||
|
||||
tests := []struct {
|
||||
note string
|
||||
v0Compatible bool
|
||||
module string
|
||||
expErr bool
|
||||
}{
|
||||
{
|
||||
note: "v0-compatible, v0 policy",
|
||||
v0Compatible: true,
|
||||
module: v0Policy,
|
||||
},
|
||||
{
|
||||
note: "v0-compatible, v1 policy",
|
||||
v0Compatible: true,
|
||||
module: v1Policy,
|
||||
expErr: true,
|
||||
},
|
||||
{
|
||||
note: "v1, v0 policy",
|
||||
v0Compatible: false,
|
||||
module: v0Policy,
|
||||
expErr: true,
|
||||
},
|
||||
{
|
||||
note: "v1, v1 policy",
|
||||
v0Compatible: false,
|
||||
module: v1Policy,
|
||||
},
|
||||
}
|
||||
|
||||
for i, tc := range tests {
|
||||
t.Run(tc.note, func(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
|
||||
params := newTestRunParams()
|
||||
params.rt.V0Compatible = tc.v0Compatible
|
||||
|
||||
rt, err := initRuntime(ctx, params, nil, false)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
testRuntime := e2e.WrapRuntime(ctx, cancel, rt)
|
||||
|
||||
done := make(chan bool)
|
||||
go func() {
|
||||
err := rt.Serve(ctx)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %s", err)
|
||||
}
|
||||
done <- true
|
||||
}()
|
||||
|
||||
err = testRuntime.WaitForServer()
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %s", err)
|
||||
}
|
||||
|
||||
// upload policy
|
||||
err = testRuntime.UploadPolicy(fmt.Sprintf("mod%d", i), bytes.NewBufferString(tc.module))
|
||||
|
||||
if tc.expErr {
|
||||
if err == nil {
|
||||
t.Fatalf("Expected error but got nil")
|
||||
}
|
||||
} else {
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
cancel()
|
||||
<-done
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunServerCheckLogTimestampFormat(t *testing.T) {
|
||||
for _, format := range []string{time.Kitchen, time.RFC3339Nano} {
|
||||
t.Run(format, func(t *testing.T) {
|
||||
t.Run("parameter", func(t *testing.T) {
|
||||
params := newTestRunParams()
|
||||
params.logTimestampFormat = format
|
||||
params.rt.Addrs = &[]string{"localhost:0"}
|
||||
checkLogTimeStampFormat(t, params, format)
|
||||
})
|
||||
t.Run("environment variable", func(t *testing.T) {
|
||||
t.Setenv("OPA_LOG_TIMESTAMP_FORMAT", format)
|
||||
params := newTestRunParams()
|
||||
params.rt.Addrs = &[]string{"localhost:0"}
|
||||
checkLogTimeStampFormat(t, params, format)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func checkLogTimeStampFormat(t *testing.T, params runCmdParams, format string) {
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
|
||||
// Pass a pre-configured StandardLogger to bypass BufferedLogger and capture logs directly.
|
||||
var buf bytes.Buffer
|
||||
stdLogger := logging.New()
|
||||
stdLogger.SetFormatter(internal_logging.GetFormatter(params.logFormat.String(), format))
|
||||
stdLogger.SetOutput(&buf)
|
||||
params.rt.Logger = stdLogger
|
||||
|
||||
rt, err := initRuntime(ctx, params, nil, false)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
testRuntime := e2e.WrapRuntime(ctx, cancel, rt)
|
||||
|
||||
done := make(chan bool)
|
||||
go func() {
|
||||
err := rt.Serve(ctx)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error: %s", err)
|
||||
}
|
||||
done <- true
|
||||
}()
|
||||
|
||||
err = testRuntime.WaitForServer()
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %s", err)
|
||||
}
|
||||
|
||||
validateBasicServe(t, testRuntime)
|
||||
|
||||
cancel()
|
||||
<-done
|
||||
|
||||
for line := range strings.SplitSeq(buf.String(), "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
var rec struct {
|
||||
Time string `json:"time"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(line), &rec); err != nil {
|
||||
t.Fatalf("incorrect log message %s: %v", line, err)
|
||||
}
|
||||
if rec.Time == "" {
|
||||
t.Fatalf("the time field is empty in log message: %s", line)
|
||||
}
|
||||
if _, err := time.Parse(format, rec.Time); err != nil {
|
||||
t.Fatalf("incorrect timestamp format %q: %v", rec.Time, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestInitRuntimeAddrSetByUser(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
addrValue string
|
||||
addrFlagSet bool
|
||||
}{
|
||||
{"AddrSetByUser_True", "localhost:8181", true},
|
||||
{"AddrSetByUser_False", "", false},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().String("addr", "", "set address")
|
||||
if tc.addrFlagSet {
|
||||
if err := cmd.Flags().Set("addr", tc.addrValue); err != nil {
|
||||
t.Fatalf("Failed to set addr flag: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
params := newTestRunParams()
|
||||
params.rt.Addrs = &[]string{"localhost:0"}
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
|
||||
rt, err := initRuntime(ctx, params, []string{}, cmd.Flags().Changed("addr"))
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
if rt.Params.AddrSetByUser != tc.addrFlagSet {
|
||||
t.Errorf("Expected AddrSetByUser to be %v, but got %v", tc.addrFlagSet, rt.Params.AddrSetByUser)
|
||||
}
|
||||
|
||||
cancel()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func newTestRunParams() runCmdParams {
|
||||
params := newRunParams()
|
||||
params.rt.GracefulShutdownPeriod = 1
|
||||
params.rt.Addrs = &[]string{"localhost:8181"}
|
||||
params.rt.DiagnosticAddrs = &[]string{}
|
||||
params.serverMode = true
|
||||
return params
|
||||
}
|
||||
|
||||
func validateBasicServe(t *testing.T, runtime *e2e.TestRuntime) {
|
||||
t.Helper()
|
||||
|
||||
err := runtime.UploadData(bytes.NewBufferString(`{"x": 1}`))
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %s", err)
|
||||
}
|
||||
|
||||
resp := struct {
|
||||
Result int `json:"result"`
|
||||
}{}
|
||||
err = runtime.GetDataWithInputTyped("x", nil, &resp)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error: %s", err)
|
||||
}
|
||||
|
||||
if resp.Result != 1 {
|
||||
t.Fatalf("Expected x to be 1, got %v", resp)
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build !go1.27
|
||||
|
||||
// Copyright 2020 The OPA Authors. All rights reserved.
|
||||
// Use of this source code is governed by an Apache2
|
||||
// license that can be found in the LICENSE file.
|
||||
@@ -16,13 +18,50 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
|
||||
internal_logging "github.com/open-policy-agent/opa/internal/logging"
|
||||
"github.com/open-policy-agent/opa/v1/logging"
|
||||
"github.com/open-policy-agent/opa/v1/repl"
|
||||
"github.com/open-policy-agent/opa/v1/storage/inmem"
|
||||
"github.com/open-policy-agent/opa/v1/test/e2e"
|
||||
"github.com/open-policy-agent/opa/v1/util/test"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func TestREPLJSONOutputBytes(t *testing.T) {
|
||||
store := inmem.New()
|
||||
var buf bytes.Buffer
|
||||
r := repl.New(store, "", &buf, "json", 0, "")
|
||||
|
||||
ctx := context.Background()
|
||||
if err := r.OneShot(ctx, "1 == 1"); err != nil {
|
||||
t.Fatalf("Unexpected error: %v", err)
|
||||
}
|
||||
|
||||
expected := `{
|
||||
"result": [
|
||||
{
|
||||
"expressions": [
|
||||
{
|
||||
"value": true,
|
||||
"text": "1 == 1",
|
||||
"location": {
|
||||
"row": 1,
|
||||
"col": 1
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
`
|
||||
|
||||
if diff := cmp.Diff(expected, buf.String()); diff != "" {
|
||||
t.Errorf("unexpected result (-want, +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunServerBase(t *testing.T) {
|
||||
params := newTestRunParams()
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
//go:build go1.27
|
||||
|
||||
// Copyright 2018 The OPA Authors. All rights reserved.
|
||||
// Use of this source code is governed by an Apache2
|
||||
// license that can be found in the LICENSE file.
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
|
||||
"github.com/open-policy-agent/opa/internal/file/archive"
|
||||
"github.com/open-policy-agent/opa/v1/bundle"
|
||||
"github.com/open-policy-agent/opa/v1/keys"
|
||||
"github.com/open-policy-agent/opa/v1/util/test"
|
||||
)
|
||||
|
||||
func TestWriteTokenToFile(t *testing.T) {
|
||||
|
||||
token := `eyJhbGciOiJSUzI1NiJ9.eyJmaWxlcyI6W3sibmFtZSI6ImJ1bmRsZS8ubWFuaWZlc3QiLCJoYXNoIjoiZWUwZWRiZGZkMjgzNTBjNDk2ZjA4ODI3Y2E1Y2VhYjgwMzA2NzI0YjYyZGY1ZjY0MDRlNzBjYjc2NjYxNWQ5ZCIsImFsZ29yaXRobSI6IlNIQTI1NiJ9LHsibmFtZSI6ImJ1bmRsZS9odHRwL2V4YW1wbGUvYXV0aHovYXV0aHoucmVnbyIsImhhc2giOiI2MDJiZTcwMWIyYmE4ZTc3YTljNTNmOWIzM2QwZTkwM2MzNGMwMGMzMDkzM2Y2NDZiYmU3NGI3YzE2NGY2OGM2IiwiYWxnb3JpdGhtIjoiU0hBMjU2In0seyJuYW1lIjoiYnVuZGxlL3JvbGVzL2JpbmRpbmcvZGF0YS5qc29uIiwiaGFzaCI6ImIxODg1NTViZjczMGVlNDdkZjBiY2Y4MzVlYTNmNTQ1MjlmMzc4N2Y0ODQxZjFhZGE2MDM5M2RhYWZhZmJkYzciLCJhbGdvcml0aG0iOiJTSEEyNTYifV0sImtleWlkIjoiZm9vIiwic2NvcGUiOiJyZWFkIn0.YojuPnGWutdlDL7lwFGBXqPfDtxOG2BuZmShN5zm-G9zfMprI1AMqKDoPoNv4tuCGIBNXwoNsYHYiK538CHfJEfY1v4iDX3JFEWQlwx_CfJWDonwqT9SY9tHUW7PUUrI_WgJXZ5zei8RAMYMymKSb9hpSAtfGg_PU0kZr52WzjbPUj4SRiB19Swi61r0CFXYjbfx3GDJdjrGTNBSWrUCMrdhHYLEWqJPfSQ-fYfRrgQVhq3BJLwJJe66dgBEGnHEgA7XMuxkNIOv7mj3Y_EChbv2tjrD9NJPekDcYH1zCEc4BycHjNCcsGiQXDE6sFtoNZiCXLB2D0sLqUnBx4TCw27wTPfcOuL2KauLPahZitnH5mYvQD8NI76Pm4NSyJfevwdWjSsrT7vf0DCLS-dU6r9dJ79xM_hJU7136CT8ARcmSrk-EvCqfkrH2c4WwZyAzdyyyFumMZh4CYc2vcC7ap0NANHJT193fTud1i23mx1PBslwXdsIqXvBGlTbR7nb9o661m-B_mxbHMkG4nIeoGpZoaBJw8RVaA6-4D55gtk8aaMyLJIlIIlV2_AKOLk3nPG3ACHiLSndasLDOIRIYkCluIEaM2FLEEPEtJfKNR6e1K-EK2TvNKMDAEUtJW71ggOuGQ3b5otYOoVVENJLwm-PsO7qb2Tq6PyAquI3ExU`
|
||||
expected := make(map[string]any)
|
||||
expected["signatures"] = []string{token}
|
||||
|
||||
files := map[string]string{}
|
||||
|
||||
test.WithTempFS(files, func(rootDir string) {
|
||||
err := writeTokenToFile(token, rootDir)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error %v", err)
|
||||
}
|
||||
|
||||
bs, err := os.ReadFile(filepath.Join(rootDir, ".signatures.json"))
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error %v", err)
|
||||
}
|
||||
|
||||
expectedBytes, err := json.MarshalIndent(expected, "", " ")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !bytes.Equal(expectedBytes, bs) {
|
||||
t.Fatal("Unexpected content in \".signatures.json\" file")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestWriteTokenToFileJSONOutputBytes(t *testing.T) {
|
||||
test.WithTempFS(map[string]string{}, func(rootDir string) {
|
||||
if err := writeTokenToFile("footoken", rootDir); err != nil {
|
||||
t.Fatalf("Unexpected error %v", err)
|
||||
}
|
||||
|
||||
gotBytes, err := os.ReadFile(filepath.Join(rootDir, ".signatures.json"))
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error %v", err)
|
||||
}
|
||||
|
||||
expected := "{\n \"signatures\": [\n \"footoken\"\n ]\n}"
|
||||
|
||||
if diff := cmp.Diff(expected, string(gotBytes)); diff != "" {
|
||||
t.Errorf("unexpected result (-want, +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestDoSign(t *testing.T) {
|
||||
files := map[string]string{
|
||||
"foo/bar/data.json": `{"y": 2}`,
|
||||
"/example/example.rego": `package example`,
|
||||
"/.signatures.json": `{"signatures": []}`,
|
||||
}
|
||||
test.WithTempFS(files, func(rootDir string) {
|
||||
params := signCmdParams{
|
||||
algorithm: "HS256",
|
||||
key: "mysecret",
|
||||
outputFilePath: rootDir,
|
||||
bundleMode: true,
|
||||
}
|
||||
|
||||
err := doSign([]string{rootDir}, params)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestBundleSignVerification(t *testing.T) {
|
||||
|
||||
// files to be included in the bundle
|
||||
files := map[string]string{
|
||||
"/.manifest": `{"revision": "quickbrownfaux"}`,
|
||||
"/a/b/c/data.json": "[1,2,3]",
|
||||
"/a/b/d/data.json": "true",
|
||||
"/a/b/y/data.yaml": `foo: 1`,
|
||||
"/example/example.rego": `package example`,
|
||||
"/policy.wasm": `modules-compiled-as-wasm-binary`,
|
||||
"/data.json": `{"x": {"y": true}, "a": {"b": {"z": true}}}`,
|
||||
}
|
||||
|
||||
test.WithTempFS(files, func(rootDir string) {
|
||||
params := signCmdParams{
|
||||
algorithm: "HS256",
|
||||
key: "mysecret",
|
||||
outputFilePath: rootDir,
|
||||
bundleMode: true,
|
||||
}
|
||||
|
||||
err := doSign([]string{rootDir}, params)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error %v", err)
|
||||
}
|
||||
|
||||
// create gzipped tarball
|
||||
var filesInBundle [][2]string
|
||||
err = filepath.Walk(rootDir, func(path string, info os.FileInfo, _ error) error {
|
||||
if !info.IsDir() {
|
||||
bs, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
filesInBundle = append(filesInBundle, [2]string{path, string(bs)})
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
buf := archive.MustWriteTarGz(filesInBundle)
|
||||
|
||||
// bundle verification config
|
||||
kc := keys.Config{
|
||||
Key: "mysecret",
|
||||
Algorithm: "HS256",
|
||||
}
|
||||
|
||||
bvc := bundle.NewVerificationConfig(map[string]*keys.Config{"foo": &kc}, "foo", "", nil)
|
||||
reader := bundle.NewReader(buf).WithBundleVerificationConfig(bvc).WithBaseDir(rootDir)
|
||||
|
||||
_, err = reader.Read()
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestValidateSignParams(t *testing.T) {
|
||||
|
||||
tests := map[string]struct {
|
||||
args []string
|
||||
params signCmdParams
|
||||
wantErr bool
|
||||
err error
|
||||
}{
|
||||
"no_args": {
|
||||
[]string{},
|
||||
newSignCmdParams(),
|
||||
true, errors.New("specify atleast one path containing policy and/or data files"),
|
||||
},
|
||||
"no_signing_key": {
|
||||
[]string{"foo"},
|
||||
newSignCmdParams(),
|
||||
true, errors.New("specify the secret (HMAC) or path of the PEM file containing the private key (RSA and ECDSA)"),
|
||||
},
|
||||
"empty_signing_key": {
|
||||
[]string{"foo"},
|
||||
signCmdParams{key: "", bundleMode: true},
|
||||
true, errors.New("specify the secret (HMAC) or path of the PEM file containing the private key (RSA and ECDSA)"),
|
||||
},
|
||||
"non_bundle_mode": {
|
||||
[]string{"foo"},
|
||||
signCmdParams{key: "foo"},
|
||||
true, errors.New("enable bundle mode (ie. --bundle) to sign bundle files or directories"),
|
||||
},
|
||||
"no_error": {
|
||||
[]string{"foo"},
|
||||
signCmdParams{key: "foo", bundleMode: true},
|
||||
false, nil,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range tests {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
|
||||
err := validateSignParams(tc.args, tc.params)
|
||||
|
||||
if tc.wantErr {
|
||||
if err == nil {
|
||||
t.Fatal("Expected error but got nil")
|
||||
}
|
||||
|
||||
if tc.err != nil && tc.err.Error() != err.Error() {
|
||||
t.Fatalf("Expected error message %v but got %v", tc.err.Error(), err.Error())
|
||||
}
|
||||
} else if err != nil {
|
||||
t.Fatalf("Unexpected error %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
//go:build !go1.27
|
||||
|
||||
// Copyright 2018 The OPA Authors. All rights reserved.
|
||||
// Use of this source code is governed by an Apache2
|
||||
// license that can be found in the LICENSE file.
|
||||
@@ -11,6 +13,8 @@ import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
|
||||
"github.com/open-policy-agent/opa/internal/file/archive"
|
||||
"github.com/open-policy-agent/opa/v1/bundle"
|
||||
"github.com/open-policy-agent/opa/v1/keys"
|
||||
@@ -47,6 +51,25 @@ func TestWriteTokenToFile(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestWriteTokenToFileJSONOutputBytes(t *testing.T) {
|
||||
test.WithTempFS(map[string]string{}, func(rootDir string) {
|
||||
if err := writeTokenToFile("footoken", rootDir); err != nil {
|
||||
t.Fatalf("Unexpected error %v", err)
|
||||
}
|
||||
|
||||
gotBytes, err := os.ReadFile(filepath.Join(rootDir, ".signatures.json"))
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error %v", err)
|
||||
}
|
||||
|
||||
expected := "{\n \"signatures\": [\n \"footoken\"\n ]\n}"
|
||||
|
||||
if diff := cmp.Diff(expected, string(gotBytes)); diff != "" {
|
||||
t.Errorf("unexpected result (-want, +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestDoSign(t *testing.T) {
|
||||
files := map[string]string{
|
||||
"foo/bar/data.json": `{"y": 2}`,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,5 @@
|
||||
//go:build !go1.27
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
@@ -15,6 +17,9 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
|
||||
"github.com/open-policy-agent/opa/cmd/formats"
|
||||
"github.com/open-policy-agent/opa/internal/file/archive"
|
||||
"github.com/open-policy-agent/opa/v1/ast"
|
||||
"github.com/open-policy-agent/opa/v1/bundle"
|
||||
@@ -3865,3 +3870,49 @@ func (*testPlugin) Eval(context.Context, *rego.EvalContext, ast.Value) (ast.Valu
|
||||
}
|
||||
|
||||
const targetPlugin = "rego_test_default_plugin"
|
||||
|
||||
var durationJSONRe = regexp.MustCompile(`"duration":\s*\d+`)
|
||||
|
||||
func TestOpaTestJSONOutputBytes(t *testing.T) {
|
||||
files := map[string]string{
|
||||
"test.rego": `package test
|
||||
|
||||
test_p if { true }
|
||||
`,
|
||||
}
|
||||
|
||||
var stdout bytes.Buffer
|
||||
var tempDirPath string
|
||||
test.WithTempFS(files, func(root string) {
|
||||
tempDirPath = root
|
||||
testParams := newTestCommandParams()
|
||||
testParams.count = 1
|
||||
testParams.outputFormat = formats.Flag(formats.JSON, formats.Pretty)
|
||||
testParams.output = &stdout
|
||||
testParams.errOutput = io.Discard
|
||||
|
||||
if exitCode := opaTest([]string{root}, testParams); exitCode != 0 {
|
||||
t.Fatalf("unexpected exit code: %d", exitCode)
|
||||
}
|
||||
})
|
||||
|
||||
normalized := durationJSONRe.ReplaceAll(stdout.Bytes(), []byte(`"duration":0`))
|
||||
|
||||
expectedOutput := strings.ReplaceAll(`[
|
||||
{
|
||||
"location": {
|
||||
"file": "TEMPDIR/test.rego",
|
||||
"row": 3,
|
||||
"col": 1
|
||||
},
|
||||
"package": "data.test",
|
||||
"name": "test_p",
|
||||
"duration":0
|
||||
}
|
||||
]
|
||||
`, "TEMPDIR", tempDirPath)
|
||||
|
||||
if diff := cmp.Diff(expectedOutput, string(normalized)); diff != "" {
|
||||
t.Errorf("unexpected result (-want, +got):\n%s", diff)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
// Copyright 2026 The OPA Authors. All rights reserved.
|
||||
// Use of this source code is governed by an Apache2
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build go1.27
|
||||
|
||||
package jsonv2
|
||||
|
||||
// This migration must not change OPA's JSON output, but json.Marshal defaults
|
||||
// to v2 semantics (no HTML escaping, non-deterministic map key order). So
|
||||
// every entry point into v2 here must establish v1 options (see
|
||||
// [jsonv1.DefaultOptionsV1]); nested encodes inherit them from the caller's
|
||||
// encoder. MarshalMarshalerTo is the only such entry point today, but that's
|
||||
// incidental — any new json.Marshal, json.MarshalWrite, or jsontext.NewEncoder
|
||||
// added here must do the same.
|
||||
|
||||
import (
|
||||
jsonv1 "encoding/json"
|
||||
"encoding/json/jsontext"
|
||||
"encoding/json/v2"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// WriteMarshalerToArray writes the JSON array of items to the encoder.
|
||||
func WriteMarshalerToArray[T json.MarshalerTo](e *jsontext.Encoder, items []T) error {
|
||||
e.WriteToken(jsontext.BeginArray)
|
||||
for _, item := range items {
|
||||
if err := item.MarshalJSONTo(e); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return e.WriteToken(jsontext.EndArray)
|
||||
}
|
||||
|
||||
// WriteField writes the object member name and then v's JSON encoding, so that
|
||||
// the member is written and checked in one statement. A nil v is written as
|
||||
// JSON null rather than dispatched to MarshalJSONTo: v1's reflection-based
|
||||
// encoder already does this for a nil pointer, so writing null here keeps
|
||||
// output identical to v1, rather than panicking on the types whose
|
||||
// MarshalJSONTo assumes a non-nil receiver.
|
||||
func WriteField[T json.MarshalerTo](e *jsontext.Encoder, name string, v T) error {
|
||||
e.WriteToken(jsontext.String(name))
|
||||
if rv := reflect.ValueOf(v); rv.Kind() == reflect.Pointer && rv.IsNil() {
|
||||
return e.WriteToken(jsontext.Null)
|
||||
}
|
||||
return v.MarshalJSONTo(e)
|
||||
}
|
||||
|
||||
// WriteFieldArray writes the object member name and then the JSON array of items.
|
||||
func WriteFieldArray[T json.MarshalerTo](e *jsontext.Encoder, name string, items []T) error {
|
||||
e.WriteToken(jsontext.String(name))
|
||||
return WriteMarshalerToArray(e, items)
|
||||
}
|
||||
|
||||
// WriteFieldValue is [WriteField] for values that don't implement [json.MarshalerTo].
|
||||
func WriteFieldValue(e *jsontext.Encoder, name string, v any) error {
|
||||
e.WriteToken(jsontext.String(name))
|
||||
return json.MarshalEncode(e, v)
|
||||
}
|
||||
|
||||
// WriteMarshalerToArrayOrNull is [WriteMarshalerToArray] but writes null for a nil
|
||||
// slice, as encoding/json v1 does. Types whose pre-1.27 MarshalJSON returns "[]"
|
||||
// for an empty value must keep using [WriteMarshalerToArray].
|
||||
func WriteMarshalerToArrayOrNull[T json.MarshalerTo](e *jsontext.Encoder, items []T) error {
|
||||
if items == nil {
|
||||
return e.WriteToken(jsontext.Null)
|
||||
}
|
||||
return WriteMarshalerToArray(e, items)
|
||||
}
|
||||
|
||||
// MarshalMarshalerTo provides a MarshalJSON implementation for any type that
|
||||
// implements json.MarshalerTo. json.Marshal dispatches to MarshalJSONTo, so this
|
||||
// doesn't recurse; the constraint is what guarantees that at compile time.
|
||||
//
|
||||
// This is the entry point into v2 that establishes v1 options, per the
|
||||
// package-level comment above.
|
||||
func MarshalMarshalerTo[T json.MarshalerTo](v T) ([]byte, error) {
|
||||
return json.Marshal(v, jsonv1.DefaultOptionsV1())
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// Copyright 2026 The OPA Authors. All rights reserved.
|
||||
// Use of this source code is governed by an Apache2
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build go1.27
|
||||
|
||||
package jsonv2
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json/jsontext"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// widget's MarshalJSONTo assumes a non-nil receiver, mirroring the ast
|
||||
// package's marshalers, to check that WriteField only calls it when v is
|
||||
// non-nil.
|
||||
type widget struct {
|
||||
Name string
|
||||
}
|
||||
|
||||
func (w *widget) MarshalJSONTo(e *jsontext.Encoder) error {
|
||||
e.WriteToken(jsontext.BeginObject)
|
||||
e.WriteToken(jsontext.String("name"))
|
||||
e.WriteToken(jsontext.String(w.Name))
|
||||
return e.WriteToken(jsontext.EndObject)
|
||||
}
|
||||
|
||||
func TestWriteFieldNilPointer(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
enc := jsontext.NewEncoder(&buf)
|
||||
|
||||
enc.WriteToken(jsontext.BeginObject)
|
||||
if err := WriteField(enc, "widget", (*widget)(nil)); err != nil {
|
||||
t.Fatalf("WriteField with nil pointer panicked or errored: %v", err)
|
||||
}
|
||||
enc.WriteToken(jsontext.EndObject)
|
||||
|
||||
if got, want := strings.TrimSpace(buf.String()), `{"widget":null}`; got != want {
|
||||
t.Fatalf("expected %q, got %q", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteFieldNonNilPointer(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
enc := jsontext.NewEncoder(&buf)
|
||||
|
||||
enc.WriteToken(jsontext.BeginObject)
|
||||
if err := WriteField(enc, "widget", &widget{Name: "foo"}); err != nil {
|
||||
t.Fatalf("WriteField: %v", err)
|
||||
}
|
||||
enc.WriteToken(jsontext.EndObject)
|
||||
|
||||
if got, want := strings.TrimSpace(buf.String()), `{"widget":{"name":"foo"}}`; got != want {
|
||||
t.Fatalf("expected %s, got %s", want, got)
|
||||
}
|
||||
}
|
||||
@@ -3,8 +3,10 @@
|
||||
// license that can be found in the LICENSE file.
|
||||
//
|
||||
// NOTE: Different go runtime metrics in pretty much
|
||||
// every Go version. Let's only test these on latest.
|
||||
//go:build go1.26
|
||||
// every Go version. The expected metrics below are those of Go 1.26, so pin
|
||||
// this test to exactly that version: it fails on both older and newer ones.
|
||||
|
||||
//go:build go1.26 && !go1.27
|
||||
|
||||
package prometheus
|
||||
|
||||
|
||||
@@ -101,10 +101,10 @@ func Compare(a, b string) int {
|
||||
return aV.Compare(bV)
|
||||
}
|
||||
|
||||
// AppendText appends the textual representation of the version to b and returns the extended buffer.
|
||||
// AppendString appends the textual representation of the version to b and returns the extended buffer.
|
||||
// This method conforms to the encoding.TextAppender interface, and is useful for serializing the Version
|
||||
// without allocating, provided the caller has pre-allocated sufficient space in b.
|
||||
func (v Version) AppendText(b []byte) ([]byte, error) {
|
||||
func (v Version) AppendString(b []byte) ([]byte, error) {
|
||||
if b == nil {
|
||||
b = make([]byte, 0, length(v))
|
||||
}
|
||||
@@ -126,7 +126,7 @@ func (v Version) AppendText(b []byte) ([]byte, error) {
|
||||
// String returns the string representation of the version.
|
||||
func (v Version) String() string {
|
||||
bs := make([]byte, 0, length(v))
|
||||
bs, _ = v.AppendText(bs)
|
||||
bs, _ = v.AppendString(bs)
|
||||
|
||||
return string(bs)
|
||||
}
|
||||
|
||||
@@ -161,18 +161,18 @@ func BenchmarkString(b *testing.B) {
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAppendText(b *testing.B) {
|
||||
func BenchmarkAppendString(b *testing.B) {
|
||||
v := MustParse("1.2.3-alpha.1+build.123")
|
||||
|
||||
for b.Loop() {
|
||||
_, err := v.AppendText(nil)
|
||||
_, err := v.AppendString(nil)
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkAppendTextPreAllocated(b *testing.B) {
|
||||
func BenchmarkAppendStringPreAllocated(b *testing.B) {
|
||||
v, err := Parse("1.2.3-alpha.1+build.123")
|
||||
if err != nil {
|
||||
b.Fatal(err)
|
||||
@@ -181,7 +181,7 @@ func BenchmarkAppendTextPreAllocated(b *testing.B) {
|
||||
buf := make([]byte, 0, 32)
|
||||
|
||||
for b.Loop() {
|
||||
if buf, err = v.AppendText(buf); err != nil {
|
||||
if buf, err = v.AppendString(buf); err != nil {
|
||||
b.Fatal(err)
|
||||
}
|
||||
if string(buf) != "1.2.3-alpha.1+build.123" {
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/open-policy-agent/opa/internal/deepcopy"
|
||||
astJSON "github.com/open-policy-agent/opa/v1/ast/json"
|
||||
"github.com/open-policy-agent/opa/v1/util"
|
||||
)
|
||||
|
||||
@@ -192,64 +191,6 @@ func (a *Annotations) GetTargetPath() Ref {
|
||||
}
|
||||
}
|
||||
|
||||
func (a *Annotations) MarshalJSON() ([]byte, error) {
|
||||
if a == nil {
|
||||
return []byte(`{"scope":""}`), nil
|
||||
}
|
||||
|
||||
data := map[string]any{
|
||||
"scope": a.Scope,
|
||||
}
|
||||
|
||||
if a.Title != "" {
|
||||
data["title"] = a.Title
|
||||
}
|
||||
|
||||
if a.Description != "" {
|
||||
data["description"] = a.Description
|
||||
}
|
||||
|
||||
if a.Entrypoint {
|
||||
data["entrypoint"] = a.Entrypoint
|
||||
}
|
||||
|
||||
if len(a.Organizations) > 0 {
|
||||
data["organizations"] = a.Organizations
|
||||
}
|
||||
|
||||
if len(a.RelatedResources) > 0 {
|
||||
data["related_resources"] = a.RelatedResources
|
||||
}
|
||||
|
||||
if len(a.Authors) > 0 {
|
||||
data["authors"] = a.Authors
|
||||
}
|
||||
|
||||
if len(a.Schemas) > 0 {
|
||||
data["schemas"] = a.Schemas
|
||||
}
|
||||
|
||||
if a.Compile != nil {
|
||||
data["compile"] = a.Compile
|
||||
}
|
||||
|
||||
if len(a.Custom) > 0 {
|
||||
data["custom"] = a.Custom
|
||||
}
|
||||
|
||||
if len(a.Labels) > 0 {
|
||||
data["labels"] = a.Labels
|
||||
}
|
||||
|
||||
if astJSON.GetOptions().MarshalOptions.IncludeLocation.Annotations {
|
||||
if a.Location != nil {
|
||||
data["location"] = a.Location
|
||||
}
|
||||
}
|
||||
|
||||
return json.Marshal(data)
|
||||
}
|
||||
|
||||
func NewAnnotationsRef(a *Annotations) *AnnotationsRef {
|
||||
var loc *Location
|
||||
if a.node != nil {
|
||||
@@ -284,34 +225,6 @@ func (ar *AnnotationsRef) GetRule() *Rule {
|
||||
}
|
||||
}
|
||||
|
||||
func (ar *AnnotationsRef) MarshalJSON() ([]byte, error) {
|
||||
data := map[string]any{
|
||||
"path": ar.Path,
|
||||
}
|
||||
|
||||
if ar.Annotations != nil {
|
||||
data["annotations"] = ar.Annotations
|
||||
}
|
||||
|
||||
if astJSON.GetOptions().MarshalOptions.IncludeLocation.AnnotationsRef {
|
||||
if ar.Location != nil {
|
||||
data["location"] = ar.Location
|
||||
}
|
||||
|
||||
// The location set for the schema ref terms is wrong (always set to
|
||||
// row 1) and not really useful anyway.. so strip it out before marshalling
|
||||
for _, schema := range ar.Annotations.Schemas {
|
||||
if schema.Path != nil {
|
||||
for _, term := range schema.Path {
|
||||
term.Location = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return json.Marshal(data)
|
||||
}
|
||||
|
||||
func scopeCompare(s1, s2 string) int {
|
||||
o1 := scopeOrder(s1)
|
||||
o2 := scopeOrder(s2)
|
||||
@@ -697,18 +610,6 @@ func (rr *RelatedResourceAnnotation) String() string {
|
||||
return string(bs)
|
||||
}
|
||||
|
||||
func (rr *RelatedResourceAnnotation) MarshalJSON() ([]byte, error) {
|
||||
d := map[string]any{
|
||||
"ref": rr.Ref.String(),
|
||||
}
|
||||
|
||||
if len(rr.Description) > 0 {
|
||||
d["description"] = rr.Description
|
||||
}
|
||||
|
||||
return json.Marshal(d)
|
||||
}
|
||||
|
||||
// Copy returns a deep copy of s.
|
||||
func (s *SchemaAnnotation) Copy() *SchemaAnnotation {
|
||||
cpy := *s
|
||||
|
||||
@@ -0,0 +1,124 @@
|
||||
//go:build !go1.27
|
||||
|
||||
package ast
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
astJSON "github.com/open-policy-agent/opa/v1/ast/json"
|
||||
)
|
||||
|
||||
func (a *Annotations) MarshalJSON() ([]byte, error) {
|
||||
if a == nil {
|
||||
return []byte(`{"scope":""}`), nil
|
||||
}
|
||||
|
||||
data := map[string]any{
|
||||
"scope": a.Scope,
|
||||
}
|
||||
|
||||
if a.Title != "" {
|
||||
data["title"] = a.Title
|
||||
}
|
||||
|
||||
if a.Description != "" {
|
||||
data["description"] = a.Description
|
||||
}
|
||||
|
||||
if a.Entrypoint {
|
||||
data["entrypoint"] = a.Entrypoint
|
||||
}
|
||||
|
||||
if len(a.Organizations) > 0 {
|
||||
data["organizations"] = a.Organizations
|
||||
}
|
||||
|
||||
if len(a.RelatedResources) > 0 {
|
||||
data["related_resources"] = a.RelatedResources
|
||||
}
|
||||
|
||||
if len(a.Authors) > 0 {
|
||||
data["authors"] = a.Authors
|
||||
}
|
||||
|
||||
if len(a.Schemas) > 0 {
|
||||
data["schemas"] = a.Schemas
|
||||
}
|
||||
|
||||
if a.Compile != nil {
|
||||
data["compile"] = a.Compile
|
||||
}
|
||||
|
||||
if len(a.Custom) > 0 {
|
||||
data["custom"] = a.Custom
|
||||
}
|
||||
|
||||
if len(a.Labels) > 0 {
|
||||
data["labels"] = a.Labels
|
||||
}
|
||||
|
||||
if astJSON.GetOptions().MarshalOptions.IncludeLocation.Annotations {
|
||||
if a.Location != nil {
|
||||
data["location"] = a.Location
|
||||
}
|
||||
}
|
||||
|
||||
return json.Marshal(data)
|
||||
}
|
||||
|
||||
func (rr *RelatedResourceAnnotation) MarshalJSON() ([]byte, error) {
|
||||
d := map[string]any{
|
||||
"ref": rr.Ref.String(),
|
||||
}
|
||||
|
||||
if len(rr.Description) > 0 {
|
||||
d["description"] = rr.Description
|
||||
}
|
||||
|
||||
return json.Marshal(d)
|
||||
}
|
||||
|
||||
func (ar *AnnotationsRef) MarshalJSON() ([]byte, error) {
|
||||
data := map[string]any{
|
||||
"path": ar.Path,
|
||||
}
|
||||
|
||||
if ar.Annotations != nil {
|
||||
data["annotations"] = ar.Annotations
|
||||
}
|
||||
|
||||
if astJSON.GetOptions().MarshalOptions.IncludeLocation.AnnotationsRef {
|
||||
if ar.Location != nil {
|
||||
data["location"] = ar.Location
|
||||
}
|
||||
}
|
||||
|
||||
return json.Marshal(data)
|
||||
}
|
||||
|
||||
// schemaAnnotationJSON mirrors SchemaAnnotation's JSON tags, with location-free
|
||||
// path terms.
|
||||
type schemaAnnotationJSON struct {
|
||||
Path []termJSON `json:"path"`
|
||||
Schema Ref `json:"schema,omitempty"`
|
||||
Definition *any `json:"definition,omitempty"`
|
||||
}
|
||||
|
||||
func (s *SchemaAnnotation) MarshalJSON() ([]byte, error) {
|
||||
d := schemaAnnotationJSON{
|
||||
Schema: s.Schema,
|
||||
Definition: s.Definition,
|
||||
}
|
||||
|
||||
if s.Path != nil {
|
||||
d.Path = make([]termJSON, len(s.Path))
|
||||
for i, t := range s.Path {
|
||||
// The location is omitted: path terms are parsed on their own from
|
||||
// the annotation's YAML key, so their locations are offsets into that
|
||||
// key (always row 1) rather than positions in the module.
|
||||
d.Path[i] = termJSON{Type: ValueName(t.Value), Value: t.Value}
|
||||
}
|
||||
}
|
||||
|
||||
return json.Marshal(d)
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
//go:build go1.27
|
||||
|
||||
package ast
|
||||
|
||||
import (
|
||||
"encoding/json/jsontext"
|
||||
"encoding/json/v2"
|
||||
"fmt"
|
||||
|
||||
"github.com/open-policy-agent/opa/internal/jsonv2"
|
||||
astJSON "github.com/open-policy-agent/opa/v1/ast/json"
|
||||
)
|
||||
|
||||
// These are exported types, so losing MarshalJSON here would be a breaking
|
||||
// API change even though callers should go through json.Marshal, not this
|
||||
// method directly.
|
||||
var (
|
||||
_ json.Marshaler = &Annotations{}
|
||||
_ json.Marshaler = &AnnotationsRef{}
|
||||
_ json.Marshaler = &SchemaAnnotation{}
|
||||
_ json.Marshaler = &RelatedResourceAnnotation{}
|
||||
)
|
||||
|
||||
func (a *Annotations) MarshalJSONTo(e *jsontext.Encoder) error {
|
||||
e.WriteToken(jsontext.BeginObject)
|
||||
|
||||
if a == nil {
|
||||
e.WriteToken(jsontext.String("scope"))
|
||||
e.WriteToken(jsontext.String(""))
|
||||
return e.WriteToken(jsontext.EndObject)
|
||||
}
|
||||
|
||||
if a.Description != "" {
|
||||
e.WriteToken(jsontext.String("description"))
|
||||
e.WriteToken(jsontext.String(a.Description))
|
||||
}
|
||||
|
||||
if a.Entrypoint {
|
||||
e.WriteToken(jsontext.String("entrypoint"))
|
||||
e.WriteToken(jsontext.True)
|
||||
}
|
||||
|
||||
if len(a.Organizations) > 0 {
|
||||
if err := jsonv2.WriteFieldValue(e, "organizations", a.Organizations); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(a.RelatedResources) > 0 {
|
||||
if err := jsonv2.WriteFieldArray(e, "related_resources", a.RelatedResources); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(a.Authors) > 0 {
|
||||
if err := jsonv2.WriteFieldValue(e, "authors", a.Authors); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(a.Schemas) > 0 {
|
||||
if err := jsonv2.WriteFieldArray(e, "schemas", a.Schemas); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if a.Compile != nil {
|
||||
if err := jsonv2.WriteFieldValue(e, "compile", a.Compile); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(a.Custom) > 0 {
|
||||
if err := jsonv2.WriteFieldValue(e, "custom", a.Custom); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(a.Labels) > 0 {
|
||||
if err := jsonv2.WriteFieldValue(e, "labels", a.Labels); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
e.WriteToken(jsontext.String("scope"))
|
||||
e.WriteToken(jsontext.String(a.Scope))
|
||||
|
||||
if a.Title != "" {
|
||||
e.WriteToken(jsontext.String("title"))
|
||||
e.WriteToken(jsontext.String(a.Title))
|
||||
}
|
||||
|
||||
if a.Location != nil && astJSON.GetOptions().MarshalOptions.IncludeLocation.Annotations {
|
||||
if err := jsonv2.WriteField(e, "location", a.Location); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return e.WriteToken(jsontext.EndObject)
|
||||
}
|
||||
|
||||
func (a *Annotations) MarshalJSON() ([]byte, error) {
|
||||
return jsonv2.MarshalMarshalerTo(a)
|
||||
}
|
||||
|
||||
func (ar *AnnotationsRef) MarshalJSONTo(e *jsontext.Encoder) error {
|
||||
e.WriteToken(jsontext.BeginObject)
|
||||
|
||||
if ar.Annotations != nil {
|
||||
if err := jsonv2.WriteField(e, "annotations", ar.Annotations); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if ar.Location != nil && astJSON.GetOptions().MarshalOptions.IncludeLocation.AnnotationsRef {
|
||||
if err := jsonv2.WriteField(e, "location", ar.Location); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := jsonv2.WriteField(e, "path", ar.Path); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return e.WriteToken(jsontext.EndObject)
|
||||
}
|
||||
|
||||
func (ar *AnnotationsRef) MarshalJSON() ([]byte, error) {
|
||||
return jsonv2.MarshalMarshalerTo(ar)
|
||||
}
|
||||
|
||||
func (s *SchemaAnnotation) MarshalJSON() ([]byte, error) {
|
||||
return jsonv2.MarshalMarshalerTo(s)
|
||||
}
|
||||
|
||||
func (s *SchemaAnnotation) MarshalJSONTo(e *jsontext.Encoder) error {
|
||||
// Token write errors are unchecked: an unbalanced value fails at the closing
|
||||
// token. A marshaller can fail having written a balanced value, so is checked.
|
||||
e.WriteToken(jsontext.BeginObject)
|
||||
|
||||
// Path has no omitempty tag, so it's always written. A nil ref is written
|
||||
// as null, matching encoding/json v1's treatment of a nil slice.
|
||||
e.WriteToken(jsontext.String("path"))
|
||||
if s.Path == nil {
|
||||
e.WriteToken(jsontext.Null)
|
||||
} else {
|
||||
e.WriteToken(jsontext.BeginArray)
|
||||
for _, t := range s.Path {
|
||||
// The location is omitted: path terms are parsed on their own from
|
||||
// the annotation's YAML key, so their locations are offsets into that
|
||||
// key (always row 1) rather than positions in the module.
|
||||
e.WriteToken(jsontext.BeginObject)
|
||||
e.WriteToken(jsontext.String("type"))
|
||||
e.WriteToken(jsontext.String(ValueName(t.Value)))
|
||||
e.WriteToken(jsontext.String("value"))
|
||||
if err := marshalValueTo(e, t.Value); err != nil {
|
||||
return fmt.Errorf("failed to marshal schema path term of %s: %w", ValueName(t.Value), err)
|
||||
}
|
||||
e.WriteToken(jsontext.EndObject)
|
||||
}
|
||||
e.WriteToken(jsontext.EndArray)
|
||||
}
|
||||
|
||||
if len(s.Schema) > 0 {
|
||||
if err := jsonv2.WriteField(e, "schema", s.Schema); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if s.Definition != nil {
|
||||
if err := jsonv2.WriteFieldValue(e, "definition", s.Definition); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return e.WriteToken(jsontext.EndObject)
|
||||
}
|
||||
|
||||
func (rr *RelatedResourceAnnotation) MarshalJSON() ([]byte, error) {
|
||||
return jsonv2.MarshalMarshalerTo(rr)
|
||||
}
|
||||
|
||||
func (rr *RelatedResourceAnnotation) MarshalJSONTo(e *jsontext.Encoder) error {
|
||||
e.WriteToken(jsontext.BeginObject)
|
||||
|
||||
e.WriteToken(jsontext.String("ref"))
|
||||
e.WriteToken(jsontext.String(rr.Ref.String()))
|
||||
|
||||
if len(rr.Description) > 0 {
|
||||
e.WriteToken(jsontext.String("description"))
|
||||
e.WriteToken(jsontext.String(rr.Description))
|
||||
}
|
||||
|
||||
return e.WriteToken(jsontext.EndObject)
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"fmt"
|
||||
"maps"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"weak"
|
||||
)
|
||||
@@ -1347,3 +1348,27 @@ allow if true
|
||||
t.Fatal("AnnotationSet was not garbage-collected: mergedLabels cache likely holds a retaining cycle")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnnotations_StringDeterministic(t *testing.T) {
|
||||
a := &Annotations{
|
||||
Scope: "rule",
|
||||
Description: "<b>&</b>",
|
||||
Custom: map[string]any{
|
||||
"zeta": 1, "alpha": 2, "mu": 3, "beta": 4, "omega": 5,
|
||||
},
|
||||
}
|
||||
|
||||
exp := a.String()
|
||||
for i := range 10 {
|
||||
if got := a.String(); got != exp {
|
||||
t.Fatalf("String() is not deterministic across calls:\ncall 0: %s\ncall %d: %s", exp, i+1, got)
|
||||
}
|
||||
}
|
||||
|
||||
if raw := "<b>&</b>"; strings.Contains(exp, raw) {
|
||||
t.Fatalf("expected HTML characters to be escaped, but found raw %s in %s", raw, exp)
|
||||
}
|
||||
if escaped := `\u003cb\u003e\u0026\u003c/b\u003e`; !strings.Contains(exp, escaped) {
|
||||
t.Fatalf("expected HTML characters to be escaped as %s, got %s", escaped, exp)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
// Copyright 2026 The OPA Authors. All rights reserved.
|
||||
// Use of this source code is governed by an Apache2
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !go1.27
|
||||
|
||||
package ast
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// assertJsonEqual fails the test unless exp and got are byte-for-byte equal.
|
||||
func assertJsonEqual[A, B string | []byte](t *testing.T, exp A, got B) {
|
||||
t.Helper()
|
||||
|
||||
if !bytes.Equal([]byte(exp), []byte(got)) {
|
||||
t.Errorf("expected JSON to be equal:\n%s\n%s", exp, got)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
// Copyright 2026 The OPA Authors. All rights reserved.
|
||||
// Use of this source code is governed by an Apache2
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build go1.27
|
||||
|
||||
package ast
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json/jsontext"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// assertJsonEqual fails the test unless the canonical JSON encoding of exp
|
||||
// and got are equal, meaning that they are compared without regard to
|
||||
// things like whitespace, key order, etc. For more details, see
|
||||
// [jsontext.Value.Canonicalize].
|
||||
func assertJsonEqual[A, B string | []byte](t *testing.T, exp A, got B) {
|
||||
t.Helper()
|
||||
|
||||
expVal, gotVal := jsontext.Value(exp), jsontext.Value(got)
|
||||
|
||||
expVal.Canonicalize()
|
||||
gotVal.Canonicalize()
|
||||
|
||||
if !bytes.Equal(expVal, gotVal) {
|
||||
t.Errorf("expected JSON to be equal:\n%s\n%s", expVal, gotVal)
|
||||
}
|
||||
}
|
||||
@@ -3,12 +3,10 @@ package location
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"unicode/utf8"
|
||||
|
||||
astJSON "github.com/open-policy-agent/opa/v1/ast/json"
|
||||
"github.com/open-policy-agent/opa/v1/util"
|
||||
)
|
||||
|
||||
@@ -150,41 +148,3 @@ func (loc *Location) Compare(other *Location) int {
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (loc *Location) MarshalJSON() ([]byte, error) {
|
||||
// structs are used here to preserve the field ordering of the original Location struct
|
||||
jsonOptions := astJSON.GetOptions().MarshalOptions
|
||||
if jsonOptions.ExcludeLocationFile {
|
||||
data := struct {
|
||||
Row int `json:"row"`
|
||||
Col int `json:"col"`
|
||||
Text []byte `json:"text,omitempty"`
|
||||
}{
|
||||
Row: loc.Row,
|
||||
Col: loc.Col,
|
||||
}
|
||||
|
||||
if jsonOptions.IncludeLocationText {
|
||||
data.Text = loc.Text
|
||||
}
|
||||
|
||||
return json.Marshal(data)
|
||||
}
|
||||
|
||||
data := struct {
|
||||
File string `json:"file"`
|
||||
Row int `json:"row"`
|
||||
Col int `json:"col"`
|
||||
Text []byte `json:"text,omitempty"`
|
||||
}{
|
||||
Row: loc.Row,
|
||||
Col: loc.Col,
|
||||
File: loc.File,
|
||||
}
|
||||
|
||||
if jsonOptions.IncludeLocationText {
|
||||
data.Text = loc.Text
|
||||
}
|
||||
|
||||
return json.Marshal(data)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
//go:build !go1.27
|
||||
|
||||
package location
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
astJSON "github.com/open-policy-agent/opa/v1/ast/json"
|
||||
)
|
||||
|
||||
func (loc *Location) MarshalJSON() ([]byte, error) {
|
||||
// structs are used here to preserve the field ordering of the original Location struct
|
||||
jsonOptions := astJSON.GetOptions().MarshalOptions
|
||||
if jsonOptions.ExcludeLocationFile {
|
||||
data := struct {
|
||||
Row int `json:"row"`
|
||||
Col int `json:"col"`
|
||||
Text []byte `json:"text,omitempty"`
|
||||
}{
|
||||
Row: loc.Row,
|
||||
Col: loc.Col,
|
||||
}
|
||||
|
||||
if jsonOptions.IncludeLocationText {
|
||||
data.Text = loc.Text
|
||||
}
|
||||
|
||||
return json.Marshal(data)
|
||||
}
|
||||
|
||||
data := struct {
|
||||
File string `json:"file"`
|
||||
Row int `json:"row"`
|
||||
Col int `json:"col"`
|
||||
Text []byte `json:"text,omitempty"`
|
||||
}{
|
||||
Row: loc.Row,
|
||||
Col: loc.Col,
|
||||
File: loc.File,
|
||||
}
|
||||
|
||||
if jsonOptions.IncludeLocationText {
|
||||
data.Text = loc.Text
|
||||
}
|
||||
|
||||
return json.Marshal(data)
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
// Copyright 2026 The OPA Authors. All rights reserved.
|
||||
// Use of this source code is governed by an Apache2
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build go1.27
|
||||
|
||||
package location
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json/jsontext"
|
||||
"encoding/json/v2"
|
||||
|
||||
"github.com/open-policy-agent/opa/internal/jsonv2"
|
||||
astJSON "github.com/open-policy-agent/opa/v1/ast/json"
|
||||
)
|
||||
|
||||
// Location is an exported type, so losing MarshalJSON here would be a
|
||||
// breaking API change even though callers should go through json.Marshal,
|
||||
// not this method directly.
|
||||
var _ json.Marshaler = &Location{}
|
||||
|
||||
// MarshalJSON returns the JSON encoding of loc.
|
||||
func (loc *Location) MarshalJSON() ([]byte, error) {
|
||||
return jsonv2.MarshalMarshalerTo(loc)
|
||||
}
|
||||
|
||||
func (loc *Location) MarshalJSONTo(e *jsontext.Encoder) (err error) {
|
||||
e.WriteToken(jsontext.BeginObject)
|
||||
|
||||
jsonOptions := astJSON.GetOptions().MarshalOptions
|
||||
if !jsonOptions.ExcludeLocationFile {
|
||||
e.WriteToken(jsontext.String("file"))
|
||||
e.WriteToken(jsontext.String(loc.File))
|
||||
}
|
||||
|
||||
e.WriteToken(jsontext.String("row"))
|
||||
e.WriteToken(jsontext.Int(int64(loc.Row)))
|
||||
e.WriteToken(jsontext.String("col"))
|
||||
e.WriteToken(jsontext.Int(int64(loc.Col)))
|
||||
|
||||
// NOTE: len check to match the `json:"text,omitempty"` behaviour of the
|
||||
// pre-go1.27 marshaller.
|
||||
if jsonOptions.IncludeLocationText && len(loc.Text) > 0 {
|
||||
e.WriteToken(jsontext.String("text"))
|
||||
e.WriteToken(jsontext.String(base64.StdEncoding.EncodeToString(loc.Text)))
|
||||
}
|
||||
|
||||
return e.WriteToken(jsontext.EndObject)
|
||||
}
|
||||
@@ -126,6 +126,19 @@ func TestLocationMarshal(t *testing.T) {
|
||||
},
|
||||
exp: `{"file":"file","row":1,"col":1,"text":"dGV4dA=="}`,
|
||||
},
|
||||
"including text, but no text present": {
|
||||
loc: &Location{
|
||||
File: "file",
|
||||
Row: 1,
|
||||
Col: 1,
|
||||
},
|
||||
options: astJSON.Options{
|
||||
MarshalOptions: astJSON.MarshalOptions{
|
||||
IncludeLocationText: true,
|
||||
},
|
||||
},
|
||||
exp: `{"file":"file","row":1,"col":1}`,
|
||||
},
|
||||
"excluding file": {
|
||||
loc: &Location{
|
||||
File: "file",
|
||||
@@ -157,6 +170,34 @@ func TestLocationMarshal(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocationUnmarshal(t *testing.T) {
|
||||
// Location has no custom unmarshaller on any Go version: decoding goes
|
||||
// through the struct tags, which means the ignored ("-") fields are not
|
||||
// populated and unknown keys are tolerated.
|
||||
in := `{"file":"p.rego","row":1,"col":2,"text":"dGVzdA==","tabs":[1],"unexpected":true}`
|
||||
|
||||
var loc Location
|
||||
if err := util.UnmarshalJSON([]byte(in), &loc); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if exp, act := "p.rego", loc.File; exp != act {
|
||||
t.Errorf("Expected file %q but got %q", exp, act)
|
||||
}
|
||||
if exp, act := 1, loc.Row; exp != act {
|
||||
t.Errorf("Expected row %v but got %v", exp, act)
|
||||
}
|
||||
if exp, act := 2, loc.Col; exp != act {
|
||||
t.Errorf("Expected col %v but got %v", exp, act)
|
||||
}
|
||||
if loc.Text != nil {
|
||||
t.Errorf("Expected no text but got %q", string(loc.Text))
|
||||
}
|
||||
if loc.Tabs != nil {
|
||||
t.Errorf("Expected no tabs but got %v", loc.Tabs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocationString(t *testing.T) {
|
||||
tests := []struct {
|
||||
loc *Location
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,3 +1,5 @@
|
||||
//go:build !go1.27
|
||||
|
||||
package ast
|
||||
|
||||
import (
|
||||
@@ -121,6 +123,10 @@ func TestTerm_MarshalJSON(t *testing.T) {
|
||||
}(),
|
||||
ExpectedJSON: `{"type":"string","value":"example"}`,
|
||||
},
|
||||
"ref with no parts": {
|
||||
Term: RefTerm(),
|
||||
ExpectedJSON: `{"type":"ref","value":null}`,
|
||||
},
|
||||
"location excluded": {
|
||||
Term: func() *Term {
|
||||
v, _ := InterfaceToValue("example")
|
||||
@@ -259,6 +265,19 @@ func TestPackage_MarshalJSON(t *testing.T) {
|
||||
},
|
||||
ExpectedJSON: `{"location":{"file":"example.rego","row":1,"col":2},"path":[]}`,
|
||||
},
|
||||
"location included, but nil": {
|
||||
Package: &Package{
|
||||
Path: EmptyRef(),
|
||||
},
|
||||
Options: astJSON.Options{
|
||||
MarshalOptions: astJSON.MarshalOptions{
|
||||
IncludeLocation: astJSON.NodeToggle{
|
||||
Package: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
ExpectedJSON: `{"path":[]}`,
|
||||
},
|
||||
}
|
||||
|
||||
for name, data := range testCases {
|
||||
@@ -277,6 +296,23 @@ func TestPackage_MarshalJSON(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestModule_MarshalJSON_PackageScopedAnnotations asserts that package-scoped
|
||||
// annotations are only emitted in the module's annotations list, and never
|
||||
// nested under the package object.
|
||||
func TestModule_MarshalJSON_PackageScopedAnnotations(t *testing.T) {
|
||||
module := &Module{
|
||||
Package: MustParsePackage("package foo"),
|
||||
Annotations: []*Annotations{{Scope: "package", Title: "pkg"}},
|
||||
}
|
||||
|
||||
exp := `{"package":{"path":[{"type":"var","value":"data"},{"type":"string","value":"foo"}]},` +
|
||||
`"annotations":[{"scope":"package","title":"pkg"}]}`
|
||||
|
||||
if got := string(util.MustMarshalJSON(module)); got != exp {
|
||||
t.Fatalf("expected:\n%s got\n%s", exp, got)
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Comment has inconsistent JSON field names starting with an upper case letter. Comment Location is
|
||||
// also always included for legacy reasons
|
||||
func TestComment_MarshalJSON(t *testing.T) {
|
||||
@@ -622,6 +658,10 @@ func TestExpr_MarshalJSON(t *testing.T) {
|
||||
Expr: expr,
|
||||
ExpectedJSON: `{"index":0,"terms":{"type":"boolean","value":true}}`,
|
||||
},
|
||||
"nil terms slice": {
|
||||
Expr: &Expr{Terms: []*Term(nil)},
|
||||
ExpectedJSON: `{"index":0,"terms":null}`,
|
||||
},
|
||||
"location excluded": {
|
||||
Expr: expr,
|
||||
Options: astJSON.Options{
|
||||
@@ -718,6 +758,34 @@ func TestExpr_UnmarshalJSON(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCall_MarshalJSON(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
Call Call
|
||||
ExpectedJSON string
|
||||
}{
|
||||
"base case": {
|
||||
Call: Call{VarTerm("eq"), NumberTerm("1")},
|
||||
ExpectedJSON: `[{"type":"var","value":"eq"},{"type":"number","value":1}]`,
|
||||
},
|
||||
"nil call": {
|
||||
Call: Call(nil),
|
||||
ExpectedJSON: `null`,
|
||||
},
|
||||
}
|
||||
|
||||
for name, data := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
bs := util.MustMarshalJSON(data.Call)
|
||||
got := string(bs)
|
||||
exp := data.ExpectedJSON
|
||||
|
||||
if got != exp {
|
||||
t.Fatalf("expected:\n%s got\n%s", exp, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSomeDecl_MarshalJSON(t *testing.T) {
|
||||
v, _ := InterfaceToValue("example")
|
||||
term := &Term{
|
||||
@@ -737,6 +805,10 @@ func TestSomeDecl_MarshalJSON(t *testing.T) {
|
||||
},
|
||||
ExpectedJSON: `{"symbols":[{"type":"string","value":"example"}]}`,
|
||||
},
|
||||
"nil symbols": {
|
||||
SomeDecl: &SomeDecl{},
|
||||
ExpectedJSON: `{"symbols":null}`,
|
||||
},
|
||||
"location excluded": {
|
||||
SomeDecl: &SomeDecl{
|
||||
Symbols: []*Term{term},
|
||||
@@ -1051,6 +1123,18 @@ func TestAnnotationsRef_MarshalJSON(t *testing.T) {
|
||||
},
|
||||
ExpectedJSON: `{"annotations":{"scope":""},"location":{"file":"example.rego","row":1,"col":4},"path":[]}`,
|
||||
},
|
||||
"no annotations, location included": {
|
||||
AnnotationsRef: &AnnotationsRef{
|
||||
Path: []*Term{},
|
||||
Location: NewLocation([]byte{}, "example.rego", 1, 4),
|
||||
},
|
||||
Options: astJSON.Options{
|
||||
MarshalOptions: astJSON.MarshalOptions{
|
||||
IncludeLocation: astJSON.NodeToggle{AnnotationsRef: true},
|
||||
},
|
||||
},
|
||||
ExpectedJSON: `{"location":{"file":"example.rego","row":1,"col":4},"path":[]}`,
|
||||
},
|
||||
}
|
||||
|
||||
for name, data := range testCases {
|
||||
@@ -1431,3 +1515,245 @@ func TestNot_UnmarshalJSON_Errors(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestArgs_MarshalJSON(t *testing.T) {
|
||||
x := VarTerm("x").SetLocation(NewLocation([]byte("x"), "example.rego", 1, 2))
|
||||
|
||||
testCases := map[string]struct {
|
||||
Args Args
|
||||
Options astJSON.Options
|
||||
ExpectedJSON string
|
||||
}{
|
||||
"nil": {
|
||||
Args: nil,
|
||||
ExpectedJSON: `null`,
|
||||
},
|
||||
"empty": {
|
||||
Args: Args{},
|
||||
ExpectedJSON: `[]`,
|
||||
},
|
||||
"base case": {
|
||||
Args: Args{x, VarTerm("y")},
|
||||
ExpectedJSON: `[{"type":"var","value":"x"},{"type":"var","value":"y"}]`,
|
||||
},
|
||||
"term location included": {
|
||||
Args: Args{x},
|
||||
Options: astJSON.Options{
|
||||
MarshalOptions: astJSON.MarshalOptions{IncludeLocation: astJSON.NodeToggle{Term: true}},
|
||||
},
|
||||
ExpectedJSON: `[{"location":{"file":"example.rego","row":1,"col":2},"type":"var","value":"x"}]`,
|
||||
},
|
||||
}
|
||||
|
||||
for name, data := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
astJSON.SetOptions(data.Options)
|
||||
t.Cleanup(resetJSONOptions)
|
||||
|
||||
bs := util.MustMarshalJSON(data.Args)
|
||||
got := string(bs)
|
||||
exp := data.ExpectedJSON
|
||||
|
||||
if got != exp {
|
||||
t.Fatalf("expected:\n%s got\n%s", exp, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestVar_MarshalJSON(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
Var Var
|
||||
ExpectedJSON string
|
||||
}{
|
||||
"base case": {
|
||||
Var: Var("x"),
|
||||
ExpectedJSON: `"x"`,
|
||||
},
|
||||
"empty": {
|
||||
Var: Var(""),
|
||||
ExpectedJSON: `""`,
|
||||
},
|
||||
"wildcard": {
|
||||
Var: Var("$01"),
|
||||
ExpectedJSON: `"$01"`,
|
||||
},
|
||||
}
|
||||
|
||||
for name, data := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
bs := util.MustMarshalJSON(data.Var)
|
||||
got := string(bs)
|
||||
exp := data.ExpectedJSON
|
||||
|
||||
if got != exp {
|
||||
t.Fatalf("expected:\n%s got\n%s", exp, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthorAnnotation_MarshalJSON(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
Author *AuthorAnnotation
|
||||
ExpectedJSON string
|
||||
}{
|
||||
"base case": {
|
||||
Author: &AuthorAnnotation{Name: "John Doe", Email: "john@example.com"},
|
||||
ExpectedJSON: `{"name":"John Doe","email":"john@example.com"}`,
|
||||
},
|
||||
"no email": {
|
||||
Author: &AuthorAnnotation{Name: "John Doe"},
|
||||
ExpectedJSON: `{"name":"John Doe"}`,
|
||||
},
|
||||
"empty": {
|
||||
Author: &AuthorAnnotation{},
|
||||
ExpectedJSON: `{"name":""}`,
|
||||
},
|
||||
}
|
||||
|
||||
for name, data := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
bs := util.MustMarshalJSON(data.Author)
|
||||
got := string(bs)
|
||||
exp := data.ExpectedJSON
|
||||
|
||||
if got != exp {
|
||||
t.Fatalf("expected:\n%s got\n%s", exp, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSchemaAnnotation_MarshalJSON(t *testing.T) {
|
||||
loc := NewLocation([]byte("input"), "example.rego", 1, 2)
|
||||
path := Ref{VarTerm("input").SetLocation(loc), StringTerm("foo").SetLocation(loc)}
|
||||
definition := any(map[string]any{"type": "boolean"})
|
||||
|
||||
testCases := map[string]struct {
|
||||
Schema *SchemaAnnotation
|
||||
Options astJSON.Options
|
||||
ExpectedJSON string
|
||||
}{
|
||||
"empty": {
|
||||
Schema: &SchemaAnnotation{},
|
||||
ExpectedJSON: `{"path":null}`,
|
||||
},
|
||||
"path and schema": {
|
||||
Schema: &SchemaAnnotation{Path: path, Schema: MustParseRef("schema.foo")},
|
||||
ExpectedJSON: `{"path":[{"type":"var","value":"input"},{"type":"string","value":"foo"}],"schema":[{"type":"var","value":"schema"},{"type":"string","value":"foo"}]}`,
|
||||
},
|
||||
"path and definition": {
|
||||
Schema: &SchemaAnnotation{Path: path, Definition: &definition},
|
||||
ExpectedJSON: `{"path":[{"type":"var","value":"input"},{"type":"string","value":"foo"}],"definition":{"type":"boolean"}}`,
|
||||
},
|
||||
"term location included": {
|
||||
Schema: &SchemaAnnotation{Path: path},
|
||||
Options: astJSON.Options{
|
||||
MarshalOptions: astJSON.MarshalOptions{IncludeLocation: astJSON.NodeToggle{Term: true}},
|
||||
},
|
||||
ExpectedJSON: `{"path":[{"type":"var","value":"input"},{"type":"string","value":"foo"}]}`,
|
||||
},
|
||||
}
|
||||
|
||||
for name, data := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
astJSON.SetOptions(data.Options)
|
||||
t.Cleanup(resetJSONOptions)
|
||||
|
||||
bs := util.MustMarshalJSON(data.Schema)
|
||||
got := string(bs)
|
||||
exp := data.ExpectedJSON
|
||||
|
||||
if got != exp {
|
||||
t.Fatalf("expected:\n%s got\n%s", exp, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("path terms are not mutated", func(t *testing.T) {
|
||||
astJSON.SetOptions(astJSON.Options{
|
||||
MarshalOptions: astJSON.MarshalOptions{IncludeLocation: astJSON.NodeToggle{Term: true, AnnotationsRef: true}},
|
||||
})
|
||||
t.Cleanup(resetJSONOptions)
|
||||
|
||||
s := &SchemaAnnotation{Path: Ref{VarTerm("input").SetLocation(loc)}}
|
||||
util.MustMarshalJSON(s)
|
||||
|
||||
if s.Path[0].Location != loc {
|
||||
t.Fatalf("expected path term location to be left alone, got %v", s.Path[0].Location)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestModule_UnmarshalJSON(t *testing.T) {
|
||||
mod := MustParseModule(`package test
|
||||
|
||||
p if { q }
|
||||
q := 1
|
||||
r := 2 if { input.x } else := 3 if { input.y }
|
||||
`)
|
||||
|
||||
bs := util.MustMarshalJSON(mod)
|
||||
|
||||
var roundtrip Module
|
||||
if err := util.UnmarshalJSON(bs, &roundtrip); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if exp, got := len(mod.Rules), len(roundtrip.Rules); exp != got {
|
||||
t.Fatalf("expected %d rules, got %d", exp, got)
|
||||
}
|
||||
|
||||
WalkRules(&roundtrip, func(rule *Rule) bool {
|
||||
if rule.Module != &roundtrip {
|
||||
t.Errorf("rule %v: expected module pointer to be set, got %v", rule.Head, rule.Module)
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
func TestTemplateString_MarshalJSON(t *testing.T) {
|
||||
testCases := map[string]struct {
|
||||
TemplateString *TemplateString
|
||||
ExpectedJSON string
|
||||
}{
|
||||
"nil parts": {
|
||||
TemplateString: &TemplateString{},
|
||||
ExpectedJSON: `{"parts":null,"multi_line":false}`,
|
||||
},
|
||||
"empty parts": {
|
||||
TemplateString: &TemplateString{Parts: []Node{}},
|
||||
ExpectedJSON: `{"parts":[],"multi_line":false}`,
|
||||
},
|
||||
"base case": {
|
||||
TemplateString: &TemplateString{Parts: []Node{StringTerm("foo"), VarTerm("x")}, MultiLine: true},
|
||||
ExpectedJSON: `{"parts":[{"type":"string","value":"foo"},{"type":"var","value":"x"}],"multi_line":true}`,
|
||||
},
|
||||
}
|
||||
|
||||
for name, data := range testCases {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
bs := util.MustMarshalJSON(data.TemplateString)
|
||||
got := string(bs)
|
||||
exp := data.ExpectedJSON
|
||||
|
||||
if got != exp {
|
||||
t.Fatalf("expected:\n%s got\n%s", exp, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSchemaAnnotation_MarshalJSON_InvalidDefinition(t *testing.T) {
|
||||
definition := any(func() {})
|
||||
|
||||
_, err := json.Marshal(&SchemaAnnotation{Path: MustParseRef("input.x"), Definition: &definition})
|
||||
if err == nil {
|
||||
t.Fatal("expected error")
|
||||
}
|
||||
// The encoder's wording differs between encoding/json v1 and v2.
|
||||
if exp := "func()"; !strings.Contains(err.Error(), exp) {
|
||||
t.Fatalf("expected error containing %q, got: %v", exp, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ package ast
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
@@ -409,26 +408,6 @@ func (mod *Module) RuleSet(name Var) RuleSet {
|
||||
return rs
|
||||
}
|
||||
|
||||
// UnmarshalJSON parses bs and stores the result in mod. The rules in the module
|
||||
// will have their module pointer set to mod.
|
||||
func (mod *Module) UnmarshalJSON(bs []byte) error {
|
||||
|
||||
// Declare a new type and use a type conversion to avoid recursively calling
|
||||
// Module#UnmarshalJSON.
|
||||
type module Module
|
||||
|
||||
if err := util.UnmarshalJSON(bs, (*module)(mod)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
WalkRules(mod, func(rule *Rule) bool {
|
||||
rule.Module = mod
|
||||
return false
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (mod *Module) regoV1Compatible() bool {
|
||||
return mod.regoVersion == RegoV1 || mod.regoVersion == RegoV0CompatV1
|
||||
}
|
||||
@@ -519,20 +498,6 @@ func (pkg *Package) String() string {
|
||||
return util.ByteSliceToString(buf)
|
||||
}
|
||||
|
||||
func (pkg *Package) MarshalJSON() ([]byte, error) {
|
||||
data := map[string]any{
|
||||
"path": pkg.Path,
|
||||
}
|
||||
|
||||
if astJSON.GetOptions().MarshalOptions.IncludeLocation.Package {
|
||||
if pkg.Location != nil {
|
||||
data["location"] = pkg.Location
|
||||
}
|
||||
}
|
||||
|
||||
return json.Marshal(data)
|
||||
}
|
||||
|
||||
// IsValidImportPath returns an error indicating if the import path is invalid.
|
||||
// If the import path is valid, err is nil.
|
||||
func IsValidImportPath(v Value) (err error) {
|
||||
@@ -623,24 +588,6 @@ func (imp *Import) String() string {
|
||||
return util.ByteSliceToString(buf)
|
||||
}
|
||||
|
||||
func (imp *Import) MarshalJSON() ([]byte, error) {
|
||||
data := map[string]any{
|
||||
"path": imp.Path,
|
||||
}
|
||||
|
||||
if len(imp.Alias) != 0 {
|
||||
data["alias"] = imp.Alias
|
||||
}
|
||||
|
||||
if astJSON.GetOptions().MarshalOptions.IncludeLocation.Import {
|
||||
if imp.Location != nil {
|
||||
data["location"] = imp.Location
|
||||
}
|
||||
}
|
||||
|
||||
return json.Marshal(data)
|
||||
}
|
||||
|
||||
// Compare returns an integer indicating whether rule is less than, equal to,
|
||||
// or greater than other.
|
||||
func (rule *Rule) Compare(other *Rule) int {
|
||||
@@ -754,42 +701,6 @@ func (rule *Rule) isFunction() bool {
|
||||
return len(rule.Head.Args) > 0
|
||||
}
|
||||
|
||||
// ruleJSON is used for JSON serialization of Rule to avoid map allocation overhead.
|
||||
// Field order is alphabetical to match previous map-based output.
|
||||
type ruleJSON struct {
|
||||
Annotations []*Annotations `json:"annotations,omitempty"`
|
||||
Body Body `json:"body"`
|
||||
Default bool `json:"default,omitempty"`
|
||||
Else *Rule `json:"else,omitempty"`
|
||||
Head *Head `json:"head"`
|
||||
Location *Location `json:"location,omitempty"`
|
||||
}
|
||||
|
||||
func (rule *Rule) MarshalJSON() ([]byte, error) {
|
||||
data := ruleJSON{
|
||||
Head: rule.Head,
|
||||
Body: rule.Body,
|
||||
}
|
||||
|
||||
if rule.Default {
|
||||
data.Default = true
|
||||
}
|
||||
|
||||
if rule.Else != nil {
|
||||
data.Else = rule.Else
|
||||
}
|
||||
|
||||
if astJSON.GetOptions().MarshalOptions.IncludeLocation.Rule {
|
||||
data.Location = rule.Location
|
||||
}
|
||||
|
||||
if len(rule.Annotations) != 0 {
|
||||
data.Annotations = rule.Annotations
|
||||
}
|
||||
|
||||
return json.Marshal(data)
|
||||
}
|
||||
|
||||
// NewHead returns a new Head object. If args are provided, the first will be
|
||||
// used for the key and the second will be used for the value.
|
||||
func NewHead(name Var, args ...*Term) *Head {
|
||||
@@ -952,27 +863,6 @@ func (head *Head) stringWithOpts(opts toStringOpts) string {
|
||||
return util.ByteSliceToString(buf)
|
||||
}
|
||||
|
||||
func (head *Head) MarshalJSON() ([]byte, error) {
|
||||
var loc *Location
|
||||
if astJSON.GetOptions().MarshalOptions.IncludeLocation.Head && head.Location != nil {
|
||||
loc = head.Location
|
||||
}
|
||||
|
||||
// NOTE(sr): we do this to override the rendering of `head.Reference`.
|
||||
// It's still what'll be used via the default means of encoding/json
|
||||
// for unmarshaling a json object into a Head struct!
|
||||
type h Head
|
||||
return json.Marshal(struct {
|
||||
h
|
||||
Ref Ref `json:"ref"`
|
||||
Location *Location `json:"location,omitempty"`
|
||||
}{
|
||||
h: h(*head),
|
||||
Ref: head.Ref(),
|
||||
Location: loc,
|
||||
})
|
||||
}
|
||||
|
||||
// Vars returns a set of vars found in the head.
|
||||
func (head *Head) Vars() VarSet {
|
||||
vis := NewVarVisitor()
|
||||
@@ -1051,17 +941,6 @@ func NewBody(exprs ...*Expr) Body {
|
||||
return Body(exprs)
|
||||
}
|
||||
|
||||
// MarshalJSON returns JSON encoded bytes representing body.
|
||||
func (body Body) MarshalJSON() ([]byte, error) {
|
||||
// Serialize empty Body to empty array. This handles both the empty case and the
|
||||
// nil case (whereas by default the result would be null if body was nil.)
|
||||
if len(body) == 0 {
|
||||
return []byte(`[]`), nil
|
||||
}
|
||||
ret, err := json.Marshal([]*Expr(body))
|
||||
return ret, err
|
||||
}
|
||||
|
||||
// Append adds the expr to the body and updates the expr's index accordingly.
|
||||
func (body *Body) Append(expr *Expr) {
|
||||
n := len(*body)
|
||||
@@ -1142,10 +1021,6 @@ func (body Body) String() string {
|
||||
return util.ByteSliceToString(buf)
|
||||
}
|
||||
|
||||
func (body Body) AppendText(buf []byte) ([]byte, error) {
|
||||
return AppendDelimeted(buf, body, "; ")
|
||||
}
|
||||
|
||||
// Vars returns a VarSet containing variables in body. The params can be set to
|
||||
// control which vars are included.
|
||||
func (body Body) Vars(params VarVisitorParams) VarSet {
|
||||
@@ -1521,51 +1396,6 @@ func (expr *Expr) String() string {
|
||||
return util.ByteSliceToString(buf)
|
||||
}
|
||||
|
||||
// exprJSON is used for JSON serialization of Expr to avoid map allocation overhead.
|
||||
// Field order is alphabetical to match previous map-based output.
|
||||
type exprJSON struct {
|
||||
Generated bool `json:"generated,omitempty"`
|
||||
Index int `json:"index"`
|
||||
Location *Location `json:"location,omitempty"`
|
||||
Negated bool `json:"negated,omitempty"`
|
||||
Terms any `json:"terms"`
|
||||
With []*With `json:"with,omitempty"`
|
||||
}
|
||||
|
||||
func (expr *Expr) MarshalJSON() ([]byte, error) {
|
||||
data := exprJSON{
|
||||
Index: expr.Index,
|
||||
Terms: expr.Terms,
|
||||
}
|
||||
|
||||
if len(expr.With) > 0 {
|
||||
data.With = expr.With
|
||||
}
|
||||
|
||||
if expr.Generated {
|
||||
data.Generated = true
|
||||
}
|
||||
|
||||
if expr.Negated {
|
||||
data.Negated = true
|
||||
}
|
||||
|
||||
if astJSON.GetOptions().MarshalOptions.IncludeLocation.Expr {
|
||||
data.Location = expr.Location
|
||||
}
|
||||
|
||||
return json.Marshal(data)
|
||||
}
|
||||
|
||||
// UnmarshalJSON parses the byte array and stores the result in expr.
|
||||
func (expr *Expr) UnmarshalJSON(bs []byte) error {
|
||||
v := map[string]any{}
|
||||
if err := util.UnmarshalJSON(bs, &v); err != nil {
|
||||
return err
|
||||
}
|
||||
return unmarshalExpr(expr, v)
|
||||
}
|
||||
|
||||
// Vars returns a VarSet containing variables in expr. The params can be set to
|
||||
// control which vars are included.
|
||||
func (expr *Expr) Vars(params VarVisitorParams) VarSet {
|
||||
@@ -1654,20 +1484,6 @@ func (d *SomeDecl) Hash() int {
|
||||
return termSliceHash(d.Symbols)
|
||||
}
|
||||
|
||||
func (d *SomeDecl) MarshalJSON() ([]byte, error) {
|
||||
data := map[string]any{
|
||||
"symbols": d.Symbols,
|
||||
}
|
||||
|
||||
if astJSON.GetOptions().MarshalOptions.IncludeLocation.SomeDecl {
|
||||
if d.Location != nil {
|
||||
data["location"] = d.Location
|
||||
}
|
||||
}
|
||||
|
||||
return json.Marshal(data)
|
||||
}
|
||||
|
||||
func (q *Every) String() string {
|
||||
if q.Key != nil {
|
||||
return fmt.Sprintf("every %s, %s in %s { %s }",
|
||||
@@ -1724,23 +1540,6 @@ func (q *Every) KeyValueVars() VarSet {
|
||||
return vis.vars
|
||||
}
|
||||
|
||||
func (q *Every) MarshalJSON() ([]byte, error) {
|
||||
data := map[string]any{
|
||||
"key": q.Key,
|
||||
"value": q.Value,
|
||||
"domain": q.Domain,
|
||||
"body": q.Body,
|
||||
}
|
||||
|
||||
if astJSON.GetOptions().MarshalOptions.IncludeLocation.Every {
|
||||
if q.Location != nil {
|
||||
data["location"] = q.Location
|
||||
}
|
||||
}
|
||||
|
||||
return json.Marshal(data)
|
||||
}
|
||||
|
||||
func (a *LogicalAnd) String() string {
|
||||
return formatBinaryLogical("and", a.Lhs, a.Rhs, a.ExplicitLhs, a.ExplicitRhs)
|
||||
}
|
||||
@@ -1774,36 +1573,6 @@ func (a *LogicalAnd) Hash() int {
|
||||
return a.Lhs.Hash() + a.Rhs.Hash()
|
||||
}
|
||||
|
||||
func (a *LogicalAnd) MarshalJSON() ([]byte, error) {
|
||||
data := map[string]any{
|
||||
"type": "and",
|
||||
"lhs": a.Lhs,
|
||||
"rhs": a.Rhs,
|
||||
}
|
||||
if a.ExplicitLhs {
|
||||
data["explicit_lhs"] = true
|
||||
}
|
||||
if a.ExplicitRhs {
|
||||
data["explicit_rhs"] = true
|
||||
}
|
||||
|
||||
if astJSON.GetOptions().MarshalOptions.IncludeLocation.And {
|
||||
if a.Location != nil {
|
||||
data["location"] = a.Location
|
||||
}
|
||||
}
|
||||
|
||||
return json.Marshal(data)
|
||||
}
|
||||
|
||||
func (a *LogicalAnd) UnmarshalJSON(bs []byte) error {
|
||||
v := map[string]any{}
|
||||
if err := util.UnmarshalJSON(bs, &v); err != nil {
|
||||
return err
|
||||
}
|
||||
return unmarshalLogical("and", &a.Lhs, &a.Rhs, &a.ExplicitLhs, &a.ExplicitRhs, v)
|
||||
}
|
||||
|
||||
func (o *LogicalOr) String() string {
|
||||
return formatBinaryLogical("or", o.Lhs, o.Rhs, o.ExplicitLhs, o.ExplicitRhs)
|
||||
}
|
||||
@@ -1837,75 +1606,6 @@ func (o *LogicalOr) Hash() int {
|
||||
return o.Lhs.Hash() + o.Rhs.Hash()
|
||||
}
|
||||
|
||||
func (o *LogicalOr) MarshalJSON() ([]byte, error) {
|
||||
data := map[string]any{
|
||||
"type": "or",
|
||||
"lhs": o.Lhs,
|
||||
"rhs": o.Rhs,
|
||||
}
|
||||
if o.ExplicitLhs {
|
||||
data["explicit_lhs"] = true
|
||||
}
|
||||
if o.ExplicitRhs {
|
||||
data["explicit_rhs"] = true
|
||||
}
|
||||
|
||||
if astJSON.GetOptions().MarshalOptions.IncludeLocation.Or {
|
||||
if o.Location != nil {
|
||||
data["location"] = o.Location
|
||||
}
|
||||
}
|
||||
|
||||
return json.Marshal(data)
|
||||
}
|
||||
|
||||
func (o *LogicalOr) UnmarshalJSON(bs []byte) error {
|
||||
v := map[string]any{}
|
||||
if err := util.UnmarshalJSON(bs, &v); err != nil {
|
||||
return err
|
||||
}
|
||||
return unmarshalLogical("or", &o.Lhs, &o.Rhs, &o.ExplicitLhs, &o.ExplicitRhs, v)
|
||||
}
|
||||
|
||||
func unmarshalLogical(typeName string, lhs, rhs *Body, explicitLhs, explicitRhs *bool, v map[string]any) error {
|
||||
lhsRaw, ok := v["lhs"].([]any)
|
||||
if !ok {
|
||||
return fmt.Errorf("ast: unable to unmarshal %s, invalid lhs field type: %T (expected list)", typeName, v["lhs"])
|
||||
}
|
||||
l, err := unmarshalBody(lhsRaw)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ast: unable to unmarshal %s lhs: %w", typeName, err)
|
||||
}
|
||||
*lhs = l
|
||||
|
||||
rhsRaw, ok := v["rhs"].([]any)
|
||||
if !ok {
|
||||
return fmt.Errorf("ast: unable to unmarshal %s, invalid rhs field type: %T (expected list)", typeName, v["rhs"])
|
||||
}
|
||||
r, err := unmarshalBody(rhsRaw)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ast: unable to unmarshal %s rhs: %w", typeName, err)
|
||||
}
|
||||
*rhs = r
|
||||
|
||||
if x, ok := v["explicit_lhs"]; ok {
|
||||
b, ok := x.(bool)
|
||||
if !ok {
|
||||
return fmt.Errorf("ast: unable to unmarshal %s explicit_lhs field with type: %T (expected true or false)", typeName, x)
|
||||
}
|
||||
*explicitLhs = b
|
||||
}
|
||||
if x, ok := v["explicit_rhs"]; ok {
|
||||
b, ok := x.(bool)
|
||||
if !ok {
|
||||
return fmt.Errorf("ast: unable to unmarshal %s explicit_rhs field with type: %T (expected true or false)", typeName, x)
|
||||
}
|
||||
*explicitRhs = b
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func formatBinaryLogical(op string, lhs, rhs Body, explicitLhs, explicitRhs bool) string {
|
||||
return formatLogicalOperand(lhs, explicitLhs, op, false) + " " + op + " " + formatLogicalOperand(rhs, explicitRhs, op, true)
|
||||
}
|
||||
@@ -2023,27 +1723,6 @@ func (w *With) SetLoc(loc *Location) {
|
||||
w.Location = loc
|
||||
}
|
||||
|
||||
// withJSON is used for JSON serialization of With to avoid map allocation overhead.
|
||||
// Field order is alphabetical to match previous map-based output.
|
||||
type withJSON struct {
|
||||
Location *Location `json:"location,omitempty"`
|
||||
Target *Term `json:"target"`
|
||||
Value *Term `json:"value"`
|
||||
}
|
||||
|
||||
func (w *With) MarshalJSON() ([]byte, error) {
|
||||
data := withJSON{
|
||||
Target: w.Target,
|
||||
Value: w.Value,
|
||||
}
|
||||
|
||||
if astJSON.GetOptions().MarshalOptions.IncludeLocation.With {
|
||||
data.Location = w.Location
|
||||
}
|
||||
|
||||
return json.Marshal(data)
|
||||
}
|
||||
|
||||
// Copy returns a deep copy of the AST node x. If x is not an AST node, x is returned unmodified.
|
||||
func Copy(x any) any {
|
||||
switch x := x.(type) {
|
||||
|
||||
@@ -223,6 +223,10 @@ func (a Args) AppendText(buf []byte) ([]byte, error) {
|
||||
return append(buf, ')'), nil
|
||||
}
|
||||
|
||||
func (body Body) AppendText(buf []byte) ([]byte, error) {
|
||||
return AppendDelimeted(buf, body, "; ")
|
||||
}
|
||||
|
||||
func (expr *Expr) AppendText(buf []byte) ([]byte, error) {
|
||||
if expr.Negated {
|
||||
buf = append(buf, "not "...)
|
||||
|
||||
@@ -253,3 +253,31 @@ func BenchmarkNoNodeTypeAllocatesOnAppend(b *testing.B) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestModuleStringAnnotationsDeterministic guards against a regression where
|
||||
// Module#String, which renders "# METADATA" comments by calling
|
||||
// Annotations#String (see Module#AppendText), produced a different rendering
|
||||
// of a metadata annotation's custom/labels map on every call, because the
|
||||
// underlying marshaler didn't fix the map key order. That would make repeated
|
||||
// formatting of the same module non-idempotent.
|
||||
func TestModuleStringAnnotationsDeterministic(t *testing.T) {
|
||||
module := ast.MustParseModuleWithOpts(`# METADATA
|
||||
# title: p
|
||||
# custom:
|
||||
# zeta: 1
|
||||
# alpha: 2
|
||||
# mu: 3
|
||||
# beta: 4
|
||||
# omega: 5
|
||||
package p
|
||||
|
||||
r = true`,
|
||||
ast.ParserOptions{ProcessAnnotation: true})
|
||||
|
||||
exp := module.String()
|
||||
for i := range 10 {
|
||||
if got := module.String(); got != exp {
|
||||
t.Fatalf("Module#String is not deterministic across calls:\ncall 0: %s\ncall %d: %s", exp, i+1, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,289 @@
|
||||
//go:build !go1.27
|
||||
|
||||
package ast
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
astJSON "github.com/open-policy-agent/opa/v1/ast/json"
|
||||
"github.com/open-policy-agent/opa/v1/util"
|
||||
)
|
||||
|
||||
// ruleJSON is used for JSON serialization of Rule to avoid map allocation overhead.
|
||||
// Field order is alphabetical to match previous map-based output.
|
||||
type ruleJSON struct {
|
||||
Annotations []*Annotations `json:"annotations,omitempty"`
|
||||
Body Body `json:"body"`
|
||||
Default bool `json:"default,omitempty"`
|
||||
Else *Rule `json:"else,omitempty"`
|
||||
Head *Head `json:"head"`
|
||||
Location *Location `json:"location,omitempty"`
|
||||
}
|
||||
|
||||
// exprJSON is used for JSON serialization of Expr to avoid map allocation overhead.
|
||||
// Field order is alphabetical to match previous map-based output.
|
||||
type exprJSON struct {
|
||||
Generated bool `json:"generated,omitempty"`
|
||||
Index int `json:"index"`
|
||||
Location *Location `json:"location,omitempty"`
|
||||
Negated bool `json:"negated,omitempty"`
|
||||
Terms any `json:"terms"`
|
||||
With []*With `json:"with,omitempty"`
|
||||
}
|
||||
|
||||
// withJSON is used for JSON serialization of With to avoid map allocation overhead.
|
||||
// Field order is alphabetical to match previous map-based output.
|
||||
type withJSON struct {
|
||||
Location *Location `json:"location,omitempty"`
|
||||
Target *Term `json:"target"`
|
||||
Value *Term `json:"value"`
|
||||
}
|
||||
|
||||
// UnmarshalJSON parses bs and stores the result in mod. The rules in the module
|
||||
// will have their module pointer set to mod.
|
||||
func (mod *Module) UnmarshalJSON(bs []byte) error {
|
||||
|
||||
// Declare a new type and use a type conversion to avoid recursively calling
|
||||
// Module#UnmarshalJSON.
|
||||
type module Module
|
||||
|
||||
if err := util.UnmarshalJSON(bs, (*module)(mod)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// The decoded rules have no module pointer, as it isn't part of the JSON
|
||||
// representation; without this, an unmarshalled module can't be compiled.
|
||||
WalkRules(mod, func(rule *Rule) bool {
|
||||
rule.Module = mod
|
||||
return false
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *SomeDecl) MarshalJSON() ([]byte, error) {
|
||||
data := map[string]any{
|
||||
"symbols": d.Symbols,
|
||||
}
|
||||
|
||||
if astJSON.GetOptions().MarshalOptions.IncludeLocation.SomeDecl {
|
||||
if d.Location != nil {
|
||||
data["location"] = d.Location
|
||||
}
|
||||
}
|
||||
|
||||
return json.Marshal(data)
|
||||
}
|
||||
|
||||
func (q *Every) MarshalJSON() ([]byte, error) {
|
||||
data := map[string]any{
|
||||
"key": q.Key,
|
||||
"value": q.Value,
|
||||
"domain": q.Domain,
|
||||
"body": q.Body,
|
||||
}
|
||||
|
||||
if astJSON.GetOptions().MarshalOptions.IncludeLocation.Every {
|
||||
if q.Location != nil {
|
||||
data["location"] = q.Location
|
||||
}
|
||||
}
|
||||
|
||||
return json.Marshal(data)
|
||||
}
|
||||
|
||||
func (a *LogicalAnd) MarshalJSON() ([]byte, error) {
|
||||
data := map[string]any{
|
||||
"type": "and",
|
||||
"lhs": a.Lhs,
|
||||
"rhs": a.Rhs,
|
||||
}
|
||||
if a.ExplicitLhs {
|
||||
data["explicit_lhs"] = true
|
||||
}
|
||||
if a.ExplicitRhs {
|
||||
data["explicit_rhs"] = true
|
||||
}
|
||||
|
||||
if astJSON.GetOptions().MarshalOptions.IncludeLocation.And {
|
||||
if a.Location != nil {
|
||||
data["location"] = a.Location
|
||||
}
|
||||
}
|
||||
|
||||
return json.Marshal(data)
|
||||
}
|
||||
|
||||
func (a *LogicalAnd) UnmarshalJSON(bs []byte) error {
|
||||
v := map[string]any{}
|
||||
if err := util.UnmarshalJSON(bs, &v); err != nil {
|
||||
return err
|
||||
}
|
||||
return unmarshalLogical("and", &a.Lhs, &a.Rhs, &a.ExplicitLhs, &a.ExplicitRhs, v)
|
||||
}
|
||||
|
||||
func (o *LogicalOr) MarshalJSON() ([]byte, error) {
|
||||
data := map[string]any{
|
||||
"type": "or",
|
||||
"lhs": o.Lhs,
|
||||
"rhs": o.Rhs,
|
||||
}
|
||||
if o.ExplicitLhs {
|
||||
data["explicit_lhs"] = true
|
||||
}
|
||||
if o.ExplicitRhs {
|
||||
data["explicit_rhs"] = true
|
||||
}
|
||||
|
||||
if astJSON.GetOptions().MarshalOptions.IncludeLocation.Or {
|
||||
if o.Location != nil {
|
||||
data["location"] = o.Location
|
||||
}
|
||||
}
|
||||
|
||||
return json.Marshal(data)
|
||||
}
|
||||
|
||||
func (o *LogicalOr) UnmarshalJSON(bs []byte) error {
|
||||
v := map[string]any{}
|
||||
if err := util.UnmarshalJSON(bs, &v); err != nil {
|
||||
return err
|
||||
}
|
||||
return unmarshalLogical("or", &o.Lhs, &o.Rhs, &o.ExplicitLhs, &o.ExplicitRhs, v)
|
||||
}
|
||||
|
||||
// UnmarshalJSON parses the byte array and stores the result in expr.
|
||||
func (expr *Expr) UnmarshalJSON(bs []byte) error {
|
||||
v := map[string]any{}
|
||||
if err := util.UnmarshalJSON(bs, &v); err != nil {
|
||||
return err
|
||||
}
|
||||
return unmarshalExpr(expr, v)
|
||||
}
|
||||
|
||||
func (expr *Expr) MarshalJSON() ([]byte, error) {
|
||||
data := exprJSON{
|
||||
Index: expr.Index,
|
||||
Terms: expr.Terms,
|
||||
}
|
||||
|
||||
if len(expr.With) > 0 {
|
||||
data.With = expr.With
|
||||
}
|
||||
|
||||
if expr.Generated {
|
||||
data.Generated = true
|
||||
}
|
||||
|
||||
if expr.Negated {
|
||||
data.Negated = true
|
||||
}
|
||||
|
||||
if astJSON.GetOptions().MarshalOptions.IncludeLocation.Expr {
|
||||
data.Location = expr.Location
|
||||
}
|
||||
|
||||
return json.Marshal(data)
|
||||
}
|
||||
|
||||
func (w *With) MarshalJSON() ([]byte, error) {
|
||||
data := withJSON{
|
||||
Target: w.Target,
|
||||
Value: w.Value,
|
||||
}
|
||||
|
||||
if astJSON.GetOptions().MarshalOptions.IncludeLocation.With {
|
||||
data.Location = w.Location
|
||||
}
|
||||
|
||||
return json.Marshal(data)
|
||||
}
|
||||
|
||||
func (pkg *Package) MarshalJSON() ([]byte, error) {
|
||||
data := map[string]any{
|
||||
"path": pkg.Path,
|
||||
}
|
||||
|
||||
if astJSON.GetOptions().MarshalOptions.IncludeLocation.Package {
|
||||
if pkg.Location != nil {
|
||||
data["location"] = pkg.Location
|
||||
}
|
||||
}
|
||||
|
||||
return json.Marshal(data)
|
||||
}
|
||||
|
||||
func (imp *Import) MarshalJSON() ([]byte, error) {
|
||||
data := map[string]any{
|
||||
"path": imp.Path,
|
||||
}
|
||||
|
||||
if len(imp.Alias) != 0 {
|
||||
data["alias"] = imp.Alias
|
||||
}
|
||||
|
||||
if astJSON.GetOptions().MarshalOptions.IncludeLocation.Import {
|
||||
if imp.Location != nil {
|
||||
data["location"] = imp.Location
|
||||
}
|
||||
}
|
||||
|
||||
return json.Marshal(data)
|
||||
}
|
||||
|
||||
func (rule *Rule) MarshalJSON() ([]byte, error) {
|
||||
data := ruleJSON{
|
||||
Head: rule.Head,
|
||||
Body: rule.Body,
|
||||
}
|
||||
|
||||
if rule.Default {
|
||||
data.Default = true
|
||||
}
|
||||
|
||||
if rule.Else != nil {
|
||||
data.Else = rule.Else
|
||||
}
|
||||
|
||||
if astJSON.GetOptions().MarshalOptions.IncludeLocation.Rule {
|
||||
data.Location = rule.Location
|
||||
}
|
||||
|
||||
if len(rule.Annotations) != 0 {
|
||||
data.Annotations = rule.Annotations
|
||||
}
|
||||
|
||||
return json.Marshal(data)
|
||||
}
|
||||
|
||||
func (head *Head) MarshalJSON() ([]byte, error) {
|
||||
var loc *Location
|
||||
if astJSON.GetOptions().MarshalOptions.IncludeLocation.Head && head.Location != nil {
|
||||
loc = head.Location
|
||||
}
|
||||
|
||||
// NOTE(sr): we do this to override the rendering of `head.Reference`.
|
||||
// It's still what'll be used via the default means of encoding/json
|
||||
// for unmarshaling a json object into a Head struct!
|
||||
type h Head
|
||||
return json.Marshal(struct {
|
||||
h
|
||||
Ref Ref `json:"ref"`
|
||||
Location *Location `json:"location,omitempty"`
|
||||
}{
|
||||
h: h(*head),
|
||||
Ref: head.Ref(),
|
||||
Location: loc,
|
||||
})
|
||||
}
|
||||
|
||||
// MarshalJSON returns JSON encoded bytes representing body.
|
||||
func (body Body) MarshalJSON() ([]byte, error) {
|
||||
// Serialize empty Body to empty array. This handles both the empty case and the
|
||||
// nil case (whereas by default the result would be null if body was nil.)
|
||||
if len(body) == 0 {
|
||||
return []byte(`[]`), nil
|
||||
}
|
||||
ret, err := json.Marshal([]*Expr(body))
|
||||
return ret, err
|
||||
}
|
||||
@@ -0,0 +1,524 @@
|
||||
//go:build go1.27
|
||||
|
||||
package ast
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json/jsontext"
|
||||
"encoding/json/v2"
|
||||
"fmt"
|
||||
|
||||
"github.com/open-policy-agent/opa/internal/jsonv2"
|
||||
astJSON "github.com/open-policy-agent/opa/v1/ast/json"
|
||||
"github.com/open-policy-agent/opa/v1/util"
|
||||
)
|
||||
|
||||
var (
|
||||
_ json.Unmarshaler = &Module{}
|
||||
|
||||
// These are exported types, so losing MarshalJSON here would be a breaking
|
||||
// API change even though callers should go through json.Marshal, not this
|
||||
// method directly.
|
||||
_ json.Marshaler = Body{}
|
||||
_ json.Marshaler = &Expr{}
|
||||
_ json.Marshaler = &Package{}
|
||||
_ json.Marshaler = &Import{}
|
||||
_ json.Marshaler = &Rule{}
|
||||
_ json.Marshaler = &Head{}
|
||||
_ json.Marshaler = &With{}
|
||||
_ json.Marshaler = &SomeDecl{}
|
||||
_ json.Marshaler = &Every{}
|
||||
_ json.Marshaler = &LogicalAnd{}
|
||||
_ json.Marshaler = &LogicalOr{}
|
||||
)
|
||||
|
||||
// UnmarshalJSON parses bs and stores the result in mod. The rules in the module
|
||||
// will have their module pointer set to mod.
|
||||
func (mod *Module) UnmarshalJSON(bs []byte) error {
|
||||
|
||||
// Declare a new type and use a type conversion to avoid recursively calling
|
||||
// Module#UnmarshalJSON.
|
||||
type module Module
|
||||
|
||||
if err := util.UnmarshalJSON(bs, (*module)(mod)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// The decoded rules have no module pointer, as it isn't part of the JSON
|
||||
// representation; without this, an unmarshalled module can't be compiled.
|
||||
WalkRules(mod, func(rule *Rule) bool {
|
||||
rule.Module = mod
|
||||
return false
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalJSONTo is here to ensure that we do not fall down to TextAppender,
|
||||
// which Go 1.27's encoding/json would otherwise use, encoding args as the Rego
|
||||
// representation of the argument list rather than as a JSON array.
|
||||
func (a Args) MarshalJSONTo(e *jsontext.Encoder) error {
|
||||
return jsonv2.WriteMarshalerToArrayOrNull(e, a)
|
||||
}
|
||||
|
||||
// MarshalJSONTo is here to ensure that we do not fall down to TextAppender,
|
||||
// which Go 1.27's encoding/json would otherwise use, encoding the module as
|
||||
// Rego source rather than as JSON. Module's own fields are fully described by
|
||||
// their struct tags, so the encoding is left to them, as it is pre-1.27. The
|
||||
// field types provide their own MarshalJSONTo where one is needed.
|
||||
func (m *Module) MarshalJSONTo(e *jsontext.Encoder) error {
|
||||
// Declare a new type and use a type conversion to avoid recursively calling
|
||||
// Module#MarshalJSONTo. It's the highest precedence marshaller, so there is
|
||||
// nothing below it to fall to, and the new type has no methods of its own.
|
||||
type module Module
|
||||
|
||||
return json.MarshalEncode(e, (*module)(m))
|
||||
}
|
||||
|
||||
func (pkg *Package) MarshalJSONTo(e *jsontext.Encoder) error {
|
||||
e.WriteToken(jsontext.BeginObject)
|
||||
|
||||
if astJSON.GetOptions().MarshalOptions.IncludeLocation.Package && pkg.Location != nil {
|
||||
if err := jsonv2.WriteField(e, "location", pkg.Location); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := jsonv2.WriteField(e, "path", pkg.Path); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return e.WriteToken(jsontext.EndObject)
|
||||
}
|
||||
|
||||
func (i *Import) MarshalJSONTo(e *jsontext.Encoder) error {
|
||||
e.WriteToken(jsontext.BeginObject)
|
||||
|
||||
if err := jsonv2.WriteField(e, "path", i.Path); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if astJSON.GetOptions().MarshalOptions.IncludeLocation.Import && i.Location != nil {
|
||||
if err := jsonv2.WriteField(e, "location", i.Location); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(i.Alias) > 0 {
|
||||
e.WriteToken(jsontext.String("alias"))
|
||||
e.WriteToken(jsontext.String(string(i.Alias)))
|
||||
}
|
||||
|
||||
return e.WriteToken(jsontext.EndObject)
|
||||
}
|
||||
|
||||
func (r *Rule) MarshalJSONTo(e *jsontext.Encoder) error {
|
||||
e.WriteToken(jsontext.BeginObject)
|
||||
|
||||
if r.Default {
|
||||
e.WriteToken(jsontext.String("default"))
|
||||
e.WriteToken(jsontext.True)
|
||||
}
|
||||
|
||||
if r.Else != nil {
|
||||
if err := jsonv2.WriteField(e, "else", r.Else); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := jsonv2.WriteField(e, "head", r.Head); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := jsonv2.WriteField(e, "body", r.Body); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(r.Annotations) > 0 {
|
||||
if err := jsonv2.WriteFieldArray(e, "annotations", r.Annotations); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if astJSON.GetOptions().MarshalOptions.IncludeLocation.Rule && r.Location != nil {
|
||||
if err := jsonv2.WriteField(e, "location", r.Location); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return e.WriteToken(jsontext.EndObject)
|
||||
}
|
||||
|
||||
func (h *Head) MarshalJSONTo(e *jsontext.Encoder) error {
|
||||
e.WriteToken(jsontext.BeginObject)
|
||||
|
||||
if h.Name != "" {
|
||||
e.WriteToken(jsontext.String("name"))
|
||||
e.WriteToken(jsontext.String(string(h.Name)))
|
||||
}
|
||||
|
||||
if err := jsonv2.WriteField(e, "ref", h.Ref()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(h.Args) > 0 {
|
||||
if err := jsonv2.WriteFieldArray(e, "args", h.Args); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if h.Key != nil {
|
||||
if err := jsonv2.WriteField(e, "key", h.Key); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if h.Value != nil {
|
||||
if err := jsonv2.WriteField(e, "value", h.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if h.Assign {
|
||||
e.WriteToken(jsontext.String("assign"))
|
||||
e.WriteToken(jsontext.True)
|
||||
}
|
||||
|
||||
if astJSON.GetOptions().MarshalOptions.IncludeLocation.Head && h.Location != nil {
|
||||
if err := jsonv2.WriteField(e, "location", h.Location); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return e.WriteToken(jsontext.EndObject)
|
||||
}
|
||||
|
||||
func (c Call) MarshalJSONTo(e *jsontext.Encoder) (err error) {
|
||||
return jsonv2.WriteMarshalerToArrayOrNull(e, c)
|
||||
}
|
||||
|
||||
func (c *Comment) MarshalJSONTo(e *jsontext.Encoder) error {
|
||||
// Token write errors are unchecked: an unbalanced value fails at the closing
|
||||
// token. A marshaller can fail having written a balanced value, so is checked.
|
||||
e.WriteToken(jsontext.BeginObject)
|
||||
|
||||
// Comment has no JSON tags, hence the capitalised keys, the base64 encoded
|
||||
// text, and the location being written even when it's nil.
|
||||
e.WriteToken(jsontext.String("Text"))
|
||||
|
||||
buf := make([]byte, base64.StdEncoding.EncodedLen(len(c.Text)))
|
||||
base64.StdEncoding.Encode(buf, c.Text)
|
||||
|
||||
e.WriteValue(append(append(append(e.AvailableBuffer(), '"'), buf...), '"'))
|
||||
|
||||
e.WriteToken(jsontext.String("Location"))
|
||||
if c.Location != nil {
|
||||
if err := c.Location.MarshalJSONTo(e); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
e.WriteToken(jsontext.Null)
|
||||
}
|
||||
|
||||
return e.WriteToken(jsontext.EndObject)
|
||||
}
|
||||
|
||||
func (q *Every) MarshalJSONTo(e *jsontext.Encoder) error {
|
||||
// Token write errors are unchecked: an unbalanced value fails at the closing
|
||||
// token. A marshaller can fail having written a balanced value, so is checked.
|
||||
e.WriteToken(jsontext.BeginObject)
|
||||
|
||||
e.WriteToken(jsontext.String("key"))
|
||||
if q.Key == nil {
|
||||
e.WriteToken(jsontext.Null)
|
||||
} else {
|
||||
if err := q.Key.MarshalJSONTo(e); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := jsonv2.WriteField(e, "value", q.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := jsonv2.WriteField(e, "domain", q.Domain); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := jsonv2.WriteField(e, "body", q.Body); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if astJSON.GetOptions().MarshalOptions.IncludeLocation.Every && q.Location != nil {
|
||||
if err := jsonv2.WriteField(e, "location", q.Location); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return e.WriteToken(jsontext.EndObject)
|
||||
}
|
||||
|
||||
func (b Body) MarshalJSONTo(e *jsontext.Encoder) error {
|
||||
return jsonv2.WriteMarshalerToArray(e, b)
|
||||
}
|
||||
|
||||
// MarshalJSON returns JSON encoded bytes representing body.
|
||||
func (body Body) MarshalJSON() ([]byte, error) {
|
||||
return jsonv2.MarshalMarshalerTo(body)
|
||||
}
|
||||
|
||||
func (expr *Expr) MarshalJSON() ([]byte, error) {
|
||||
return jsonv2.MarshalMarshalerTo(expr)
|
||||
}
|
||||
|
||||
// UnmarshalJSON parses the byte array and stores the result in expr.
|
||||
func (expr *Expr) UnmarshalJSON(bs []byte) error {
|
||||
v := map[string]any{}
|
||||
if err := util.UnmarshalJSON(bs, &v); err != nil {
|
||||
return err
|
||||
}
|
||||
return unmarshalExpr(expr, v)
|
||||
}
|
||||
|
||||
func (e *Expr) MarshalJSONTo(enc *jsontext.Encoder) error {
|
||||
enc.WriteToken(jsontext.BeginObject)
|
||||
|
||||
enc.WriteToken(jsontext.String("index"))
|
||||
enc.WriteToken(jsontext.Int(int64(e.Index)))
|
||||
|
||||
includeLocation := astJSON.GetOptions().MarshalOptions.IncludeLocation
|
||||
if e.Location != nil && includeLocation.Expr {
|
||||
if err := jsonv2.WriteField(enc, "location", e.Location); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if e.Negated {
|
||||
enc.WriteToken(jsontext.String("negated"))
|
||||
enc.WriteToken(jsontext.True)
|
||||
}
|
||||
|
||||
if e.Generated {
|
||||
enc.WriteToken(jsontext.String("generated"))
|
||||
enc.WriteToken(jsontext.True)
|
||||
}
|
||||
|
||||
enc.WriteToken(jsontext.String("terms"))
|
||||
var err error
|
||||
switch t := e.Terms.(type) {
|
||||
case []*Term:
|
||||
err = jsonv2.WriteMarshalerToArrayOrNull(enc, t)
|
||||
case json.MarshalerTo:
|
||||
err = t.MarshalJSONTo(enc)
|
||||
default:
|
||||
return fmt.Errorf("unsupported expr terms type: %T", e.Terms)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal expr terms: %w", err)
|
||||
}
|
||||
|
||||
if len(e.With) > 0 {
|
||||
if err := jsonv2.WriteFieldArray(enc, "with", e.With); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return enc.WriteToken(jsontext.EndObject)
|
||||
}
|
||||
|
||||
func (a *LogicalAnd) MarshalJSONTo(e *jsontext.Encoder) error {
|
||||
e.WriteToken(jsontext.BeginObject)
|
||||
e.WriteToken(jsontext.String("type"))
|
||||
e.WriteToken(jsontext.String("and"))
|
||||
if err := jsonv2.WriteField(e, "lhs", a.Lhs); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := jsonv2.WriteField(e, "rhs", a.Rhs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if a.ExplicitLhs {
|
||||
e.WriteToken(jsontext.String("explicit_lhs"))
|
||||
e.WriteToken(jsontext.True)
|
||||
}
|
||||
if a.ExplicitRhs {
|
||||
e.WriteToken(jsontext.String("explicit_rhs"))
|
||||
e.WriteToken(jsontext.True)
|
||||
}
|
||||
|
||||
if astJSON.GetOptions().MarshalOptions.IncludeLocation.And && a.Location != nil {
|
||||
if err := jsonv2.WriteField(e, "location", a.Location); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return e.WriteToken(jsontext.EndObject)
|
||||
}
|
||||
|
||||
func (a *LogicalAnd) UnmarshalJSON(bs []byte) error {
|
||||
v := map[string]any{}
|
||||
if err := util.UnmarshalJSON(bs, &v); err != nil {
|
||||
return err
|
||||
}
|
||||
return unmarshalLogical("and", &a.Lhs, &a.Rhs, &a.ExplicitLhs, &a.ExplicitRhs, v)
|
||||
}
|
||||
|
||||
func (o *LogicalOr) MarshalJSONTo(e *jsontext.Encoder) error {
|
||||
e.WriteToken(jsontext.BeginObject)
|
||||
|
||||
e.WriteToken(jsontext.String("type"))
|
||||
e.WriteToken(jsontext.String("or"))
|
||||
|
||||
if err := jsonv2.WriteField(e, "lhs", o.Lhs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := jsonv2.WriteField(e, "rhs", o.Rhs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if o.ExplicitLhs {
|
||||
e.WriteToken(jsontext.String("explicit_lhs"))
|
||||
e.WriteToken(jsontext.True)
|
||||
}
|
||||
if o.ExplicitRhs {
|
||||
e.WriteToken(jsontext.String("explicit_rhs"))
|
||||
e.WriteToken(jsontext.True)
|
||||
}
|
||||
|
||||
if astJSON.GetOptions().MarshalOptions.IncludeLocation.Or && o.Location != nil {
|
||||
if err := jsonv2.WriteField(e, "location", o.Location); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return e.WriteToken(jsontext.EndObject)
|
||||
}
|
||||
|
||||
func (o *LogicalOr) UnmarshalJSON(bs []byte) error {
|
||||
v := map[string]any{}
|
||||
if err := util.UnmarshalJSON(bs, &v); err != nil {
|
||||
return err
|
||||
}
|
||||
return unmarshalLogical("or", &o.Lhs, &o.Rhs, &o.ExplicitLhs, &o.ExplicitRhs, v)
|
||||
}
|
||||
|
||||
func (w *With) MarshalJSONTo(e *jsontext.Encoder) error {
|
||||
e.WriteToken(jsontext.BeginObject)
|
||||
|
||||
if err := jsonv2.WriteField(e, "target", w.Target); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := jsonv2.WriteField(e, "value", w.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if astJSON.GetOptions().MarshalOptions.IncludeLocation.With && w.Location != nil {
|
||||
if err := jsonv2.WriteField(e, "location", w.Location); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return e.WriteToken(jsontext.EndObject)
|
||||
}
|
||||
|
||||
func (d *SomeDecl) MarshalJSONTo(e *jsontext.Encoder) error {
|
||||
e.WriteToken(jsontext.BeginObject)
|
||||
|
||||
e.WriteToken(jsontext.String("symbols"))
|
||||
if err := jsonv2.WriteMarshalerToArrayOrNull(e, d.Symbols); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if d.Location != nil && astJSON.GetOptions().MarshalOptions.IncludeLocation.SomeDecl {
|
||||
if err := jsonv2.WriteField(e, "location", d.Location); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return e.WriteToken(jsontext.EndObject)
|
||||
}
|
||||
|
||||
func (ac *ArrayComprehension) MarshalJSONTo(e *jsontext.Encoder) error {
|
||||
e.WriteToken(jsontext.BeginObject)
|
||||
|
||||
if err := jsonv2.WriteField(e, "term", ac.Term); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := jsonv2.WriteField(e, "body", ac.Body); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return e.WriteToken(jsontext.EndObject)
|
||||
}
|
||||
|
||||
func (sc *SetComprehension) MarshalJSONTo(e *jsontext.Encoder) error {
|
||||
e.WriteToken(jsontext.BeginObject)
|
||||
|
||||
if err := jsonv2.WriteField(e, "term", sc.Term); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := jsonv2.WriteField(e, "body", sc.Body); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return e.WriteToken(jsontext.EndObject)
|
||||
}
|
||||
|
||||
func (oc *ObjectComprehension) MarshalJSONTo(e *jsontext.Encoder) error {
|
||||
e.WriteToken(jsontext.BeginObject)
|
||||
|
||||
if err := jsonv2.WriteField(e, "key", oc.Key); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := jsonv2.WriteField(e, "value", oc.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := jsonv2.WriteField(e, "body", oc.Body); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return e.WriteToken(jsontext.EndObject)
|
||||
}
|
||||
|
||||
func (pkg *Package) MarshalJSON() ([]byte, error) {
|
||||
return jsonv2.MarshalMarshalerTo(pkg)
|
||||
}
|
||||
|
||||
func (imp *Import) MarshalJSON() ([]byte, error) {
|
||||
return jsonv2.MarshalMarshalerTo(imp)
|
||||
}
|
||||
|
||||
func (rule *Rule) MarshalJSON() ([]byte, error) {
|
||||
return jsonv2.MarshalMarshalerTo(rule)
|
||||
}
|
||||
|
||||
func (head *Head) MarshalJSON() ([]byte, error) {
|
||||
return jsonv2.MarshalMarshalerTo(head)
|
||||
}
|
||||
|
||||
func (w *With) MarshalJSON() ([]byte, error) {
|
||||
return jsonv2.MarshalMarshalerTo(w)
|
||||
}
|
||||
|
||||
func (d *SomeDecl) MarshalJSON() ([]byte, error) {
|
||||
return jsonv2.MarshalMarshalerTo(d)
|
||||
}
|
||||
|
||||
func (q *Every) MarshalJSON() ([]byte, error) {
|
||||
return jsonv2.MarshalMarshalerTo(q)
|
||||
}
|
||||
|
||||
func (a *LogicalAnd) MarshalJSON() ([]byte, error) {
|
||||
return jsonv2.MarshalMarshalerTo(a)
|
||||
}
|
||||
|
||||
func (o *LogicalOr) MarshalJSON() ([]byte, error) {
|
||||
return jsonv2.MarshalMarshalerTo(o)
|
||||
}
|
||||
@@ -498,10 +498,8 @@ func TestLogicalAnd_MarshalJSON(t *testing.T) {
|
||||
astJSON.SetOptions(tc.options)
|
||||
t.Cleanup(resetJSONOptions)
|
||||
|
||||
got := string(util.MustMarshalJSON(tc.node))
|
||||
if got != tc.want {
|
||||
t.Fatalf("MarshalJSON:\nwant: %s\ngot: %s", tc.want, got)
|
||||
}
|
||||
got := util.MustMarshalJSON(tc.node)
|
||||
assertJsonEqual(t, tc.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -568,10 +566,8 @@ func TestLogicalOr_MarshalJSON(t *testing.T) {
|
||||
astJSON.SetOptions(tc.options)
|
||||
t.Cleanup(resetJSONOptions)
|
||||
|
||||
got := string(util.MustMarshalJSON(tc.node))
|
||||
if got != tc.want {
|
||||
t.Fatalf("MarshalJSON:\nwant: %s\ngot: %s", tc.want, got)
|
||||
}
|
||||
got := util.MustMarshalJSON(tc.node)
|
||||
assertJsonEqual(t, tc.want, got)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+5
-10
@@ -454,9 +454,8 @@ func TestRuleHeadJSON(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if exp, act := `{"body":[],"head":{"name":"allow","ref":[{"type":"var","value":"allow"}]}}`, string(bs); act != exp {
|
||||
t.Errorf("expected %q, got %q", exp, act)
|
||||
}
|
||||
exp := []byte(`{"body":[],"head":{"name":"allow","ref":[{"type":"var","value":"allow"}]}}`)
|
||||
assertJsonEqual(t, exp, bs)
|
||||
|
||||
var readRule Rule
|
||||
if err := json.Unmarshal(bs, &readRule); err != nil {
|
||||
@@ -469,9 +468,8 @@ func TestRuleHeadJSON(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if exp, act := string(bs), string(bs0); exp != act {
|
||||
t.Errorf("expected json repr to match %q, got %q", exp, act)
|
||||
}
|
||||
|
||||
assertJsonEqual(t, bs, bs0)
|
||||
|
||||
var readAgainRule Rule
|
||||
if err := json.Unmarshal(bs, &readAgainRule); err != nil {
|
||||
@@ -928,10 +926,7 @@ func TestAnnotationsString(t *testing.T) {
|
||||
// NOTE(tsandall): for now, annotations are represented as JSON objects
|
||||
// which are a subset of YAML. We could improve this in the future.
|
||||
exp := `{"authors":[{"name":"John Doe","email":"john@example.com"},{"name":"Jane Doe"}],"custom":{"flag":true,"list":[1,2,3],"map":{"one":1,"two":{"3":"three"}}},"description":"baz","organizations":["mi","fa"],"related_resources":[{"ref":"https://example.com"},{"description":"Some resource","ref":"https://example.com/2"}],"schemas":[{"path":[{"type":"var","value":"data"},{"type":"string","value":"bar"}],"schema":[{"type":"var","value":"schema"},{"type":"string","value":"baz"}]}],"scope":"foo","title":"bar"}`
|
||||
|
||||
if got := a.String(); exp != got {
|
||||
t.Fatalf("expected\n%s\nbut got\n%s", exp, got)
|
||||
}
|
||||
assertJsonEqual(t, exp, a.String())
|
||||
}
|
||||
|
||||
func mustParseURL(str string) url.URL {
|
||||
|
||||
+87
-130
@@ -19,7 +19,6 @@ import (
|
||||
"unicode"
|
||||
|
||||
"github.com/cespare/xxhash/v2"
|
||||
astJSON "github.com/open-policy-agent/opa/v1/ast/json"
|
||||
"github.com/open-policy-agent/opa/v1/ast/location"
|
||||
"github.com/open-policy-agent/opa/v1/util"
|
||||
)
|
||||
@@ -420,55 +419,10 @@ func (term *Term) IsGround() bool {
|
||||
return term.Value.IsGround()
|
||||
}
|
||||
|
||||
// termJSON is used to serialize Term to JSON without map allocation.
|
||||
type termJSON struct {
|
||||
Location *Location `json:"location,omitempty"`
|
||||
Type string `json:"type"`
|
||||
Value Value `json:"value"`
|
||||
}
|
||||
|
||||
// MarshalJSON returns the JSON encoding of the term.
|
||||
//
|
||||
// Specialized marshalling logic is required to include a type hint for Value.
|
||||
func (term *Term) MarshalJSON() ([]byte, error) {
|
||||
d := termJSON{
|
||||
Type: ValueName(term.Value),
|
||||
Value: term.Value,
|
||||
}
|
||||
jsonOptions := astJSON.GetOptions().MarshalOptions
|
||||
if jsonOptions.IncludeLocation.Term {
|
||||
d.Location = term.Location
|
||||
}
|
||||
return json.Marshal(d)
|
||||
}
|
||||
|
||||
func (term *Term) String() string {
|
||||
return term.Value.String()
|
||||
}
|
||||
|
||||
// UnmarshalJSON parses the byte array and stores the result in term.
|
||||
// Specialized unmarshalling is required to handle Value and Location.
|
||||
func (term *Term) UnmarshalJSON(bs []byte) error {
|
||||
v := map[string]any{}
|
||||
if err := util.UnmarshalJSON(bs, &v); err != nil {
|
||||
return err
|
||||
}
|
||||
val, err := unmarshalValue(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
term.Value = val
|
||||
|
||||
if loc, ok := v["location"].(map[string]any); ok {
|
||||
term.Location = &Location{}
|
||||
err := unmarshalLocation(term.Location, loc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Vars returns a VarSet with variables contained in this term.
|
||||
func (term *Term) Vars() VarSet {
|
||||
vis := NewVarVisitor()
|
||||
@@ -660,56 +614,6 @@ func (n *Not) String() string {
|
||||
return "not {" + n.Body.String() + "}"
|
||||
}
|
||||
|
||||
func (n *Not) MarshalJSON() ([]byte, error) {
|
||||
data := map[string]any{
|
||||
"type": "not",
|
||||
"body": n.Body,
|
||||
"explicit_body": n.ExplicitBody,
|
||||
}
|
||||
|
||||
if astJSON.GetOptions().MarshalOptions.IncludeLocation.Not {
|
||||
if n.Location != nil {
|
||||
data["location"] = n.Location
|
||||
}
|
||||
}
|
||||
|
||||
return json.Marshal(data)
|
||||
}
|
||||
|
||||
func (n *Not) UnmarshalJSON(bs []byte) error {
|
||||
v := map[string]any{}
|
||||
if err := util.UnmarshalJSON(bs, &v); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return unmarshalNot(n, v)
|
||||
}
|
||||
|
||||
func unmarshalNot(n *Not, v map[string]any) error {
|
||||
var eb bool
|
||||
if x, ok := v["explicit_body"]; ok {
|
||||
eb, ok = x.(bool)
|
||||
if !ok {
|
||||
return fmt.Errorf("ast: unable to unmarshal explicit_body field with type: %T (expected true or false)", v["explicit_body"])
|
||||
}
|
||||
}
|
||||
|
||||
b, ok := v["body"].([]any)
|
||||
if !ok {
|
||||
return fmt.Errorf("ast: unable to unmarshal not, invalid body field type: %T (expected list)", v["body"])
|
||||
}
|
||||
|
||||
body, err := unmarshalBody(b)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ast: unable to unmarshal not body: %w", err)
|
||||
}
|
||||
|
||||
n.ExplicitBody = eb
|
||||
n.Body = body
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Null represents the null value defined by JSON.
|
||||
type Null struct{}
|
||||
|
||||
@@ -909,11 +813,6 @@ func (Number) IsGround() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// MarshalJSON returns JSON encoded bytes representing num.
|
||||
func (num Number) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(json.Number(num))
|
||||
}
|
||||
|
||||
func (num Number) String() string {
|
||||
return string(num)
|
||||
}
|
||||
@@ -1692,14 +1591,6 @@ func (arr *Array) IsGround() bool {
|
||||
return arr.ground
|
||||
}
|
||||
|
||||
// MarshalJSON returns JSON encoded bytes representing arr.
|
||||
func (arr *Array) MarshalJSON() ([]byte, error) {
|
||||
if len(arr.elems) == 0 {
|
||||
return []byte(`[]`), nil
|
||||
}
|
||||
return json.Marshal(arr.elems)
|
||||
}
|
||||
|
||||
func (arr *Array) String() string {
|
||||
buf, _ := arr.AppendText(make([]byte, 0, arr.StringLength()))
|
||||
return util.ByteSliceToString(buf)
|
||||
@@ -2048,14 +1939,6 @@ func (s *set) Len() int {
|
||||
return len(s.keys)
|
||||
}
|
||||
|
||||
// MarshalJSON returns JSON encoded bytes representing s.
|
||||
func (s *set) MarshalJSON() ([]byte, error) {
|
||||
if s.keys == nil {
|
||||
return []byte(`[]`), nil
|
||||
}
|
||||
return json.Marshal(s.sortedKeys())
|
||||
}
|
||||
|
||||
// Sorted returns an Array that contains the sorted elements of s.
|
||||
func (s *set) Sorted() *Array {
|
||||
cpy := make([]*Term, len(s.keys))
|
||||
@@ -2224,10 +2107,6 @@ func (l *lazyObj) Map(f func(*Term, *Term) (*Term, *Term, error)) (Object, error
|
||||
return l.force().Map(f)
|
||||
}
|
||||
|
||||
func (l *lazyObj) MarshalJSON() ([]byte, error) {
|
||||
return l.force().(*object).MarshalJSON()
|
||||
}
|
||||
|
||||
func (l *lazyObj) Merge(other Object) (Object, bool) {
|
||||
return l.force().Merge(other)
|
||||
}
|
||||
@@ -2605,15 +2484,6 @@ func (obj *object) KeysIterator() ObjectKeysIterator {
|
||||
return newobjectKeysIterator(obj)
|
||||
}
|
||||
|
||||
// MarshalJSON returns JSON encoded bytes representing obj.
|
||||
func (obj *object) MarshalJSON() ([]byte, error) {
|
||||
sl := make([][2]*Term, obj.Len())
|
||||
for i, node := range obj.sortedKeys() {
|
||||
sl[i] = Item(node.key, node.value)
|
||||
}
|
||||
return json.Marshal(sl)
|
||||
}
|
||||
|
||||
// Merge returns a new Object containing the non-overlapping keys of obj and other. If there are
|
||||
// overlapping keys between obj and other, the values of associated with the keys are merged. Only
|
||||
// objects can be merged with other objects. If the values cannot be merged, the second turn value
|
||||
@@ -3187,6 +3057,29 @@ func isControlOrBackslash(r rune) bool {
|
||||
// on the happy path and treats all errors the same. If better error
|
||||
// reporting is needed, the error paths will need to be fleshed out.
|
||||
|
||||
// UnmarshalJSON parses the byte array and stores the result in term.
|
||||
// Specialized unmarshalling is required to handle Value and Location.
|
||||
func (term *Term) UnmarshalJSON(bs []byte) error {
|
||||
v := map[string]any{}
|
||||
if err := util.UnmarshalJSON(bs, &v); err != nil {
|
||||
return err
|
||||
}
|
||||
val, err := unmarshalValue(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
term.Value = val
|
||||
|
||||
if loc, ok := v["location"].(map[string]any); ok {
|
||||
term.Location = &Location{}
|
||||
err := unmarshalLocation(term.Location, loc)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func unmarshalBody(b []any) (Body, error) {
|
||||
buf := Body{}
|
||||
for _, e := range b {
|
||||
@@ -3391,6 +3284,45 @@ func unmarshalWith(i any) (*With, error) {
|
||||
return nil, errors.New(`ast: unable to unmarshal with modifier (expected {"target": {...}, "value": {...}})`)
|
||||
}
|
||||
|
||||
func unmarshalLogical(typeName string, lhs, rhs *Body, explicitLhs, explicitRhs *bool, v map[string]any) error {
|
||||
lhsRaw, ok := v["lhs"].([]any)
|
||||
if !ok {
|
||||
return fmt.Errorf("ast: unable to unmarshal %s, invalid lhs field type: %T (expected list)", typeName, v["lhs"])
|
||||
}
|
||||
l, err := unmarshalBody(lhsRaw)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ast: unable to unmarshal %s lhs: %w", typeName, err)
|
||||
}
|
||||
*lhs = l
|
||||
|
||||
rhsRaw, ok := v["rhs"].([]any)
|
||||
if !ok {
|
||||
return fmt.Errorf("ast: unable to unmarshal %s, invalid rhs field type: %T (expected list)", typeName, v["rhs"])
|
||||
}
|
||||
r, err := unmarshalBody(rhsRaw)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ast: unable to unmarshal %s rhs: %w", typeName, err)
|
||||
}
|
||||
*rhs = r
|
||||
|
||||
if x, ok := v["explicit_lhs"]; ok {
|
||||
b, ok := x.(bool)
|
||||
if !ok {
|
||||
return fmt.Errorf("ast: unable to unmarshal %s explicit_lhs field with type: %T (expected true or false)", typeName, x)
|
||||
}
|
||||
*explicitLhs = b
|
||||
}
|
||||
if x, ok := v["explicit_rhs"]; ok {
|
||||
b, ok := x.(bool)
|
||||
if !ok {
|
||||
return fmt.Errorf("ast: unable to unmarshal %s explicit_rhs field with type: %T (expected true or false)", typeName, x)
|
||||
}
|
||||
*explicitRhs = b
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func unmarshalValue(d map[string]any) (Value, error) {
|
||||
v := d["value"]
|
||||
switch d["type"] {
|
||||
@@ -3508,3 +3440,28 @@ func unmarshalValue(d map[string]any) (Value, error) {
|
||||
unmarshal_error:
|
||||
return nil, errors.New("ast: unable to unmarshal term")
|
||||
}
|
||||
|
||||
func unmarshalNot(n *Not, v map[string]any) error {
|
||||
var eb bool
|
||||
if x, ok := v["explicit_body"]; ok {
|
||||
eb, ok = x.(bool)
|
||||
if !ok {
|
||||
return fmt.Errorf("ast: unable to unmarshal explicit_body field with type: %T (expected true or false)", v["explicit_body"])
|
||||
}
|
||||
}
|
||||
|
||||
b, ok := v["body"].([]any)
|
||||
if !ok {
|
||||
return fmt.Errorf("ast: unable to unmarshal not, invalid body field type: %T (expected list)", v["body"])
|
||||
}
|
||||
|
||||
body, err := unmarshalBody(b)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ast: unable to unmarshal not body: %w", err)
|
||||
}
|
||||
|
||||
n.ExplicitBody = eb
|
||||
n.Body = body
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
// Copyright 2026 The OPA Authors. All rights reserved.
|
||||
// Use of this source code is governed by an Apache2
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build !go1.27
|
||||
|
||||
package ast
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
astJSON "github.com/open-policy-agent/opa/v1/ast/json"
|
||||
"github.com/open-policy-agent/opa/v1/util"
|
||||
)
|
||||
|
||||
// termJSON is used to serialize Term to JSON without map allocation.
|
||||
type termJSON struct {
|
||||
Location *Location `json:"location,omitempty"`
|
||||
Type string `json:"type"`
|
||||
Value Value `json:"value"`
|
||||
}
|
||||
|
||||
// MarshalJSON returns the JSON encoding of the term.
|
||||
//
|
||||
// Specialized marshalling logic is required to include a type hint for Value.
|
||||
func (term *Term) MarshalJSON() ([]byte, error) {
|
||||
d := termJSON{
|
||||
Type: ValueName(term.Value),
|
||||
Value: term.Value,
|
||||
}
|
||||
jsonOptions := astJSON.GetOptions().MarshalOptions
|
||||
if jsonOptions.IncludeLocation.Term {
|
||||
d.Location = term.Location
|
||||
}
|
||||
return json.Marshal(d)
|
||||
}
|
||||
|
||||
// MarshalJSON returns JSON encoded bytes representing arr.
|
||||
func (arr *Array) MarshalJSON() ([]byte, error) {
|
||||
if len(arr.elems) == 0 {
|
||||
return []byte(`[]`), nil
|
||||
}
|
||||
return json.Marshal(arr.elems)
|
||||
}
|
||||
|
||||
// MarshalJSON returns JSON encoded bytes representing num.
|
||||
func (num Number) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(json.Number(num))
|
||||
}
|
||||
|
||||
// MarshalJSON returns JSON encoded bytes representing obj.
|
||||
func (obj *object) MarshalJSON() ([]byte, error) {
|
||||
sl := make([][2]*Term, obj.Len())
|
||||
for i, node := range obj.sortedKeys() {
|
||||
sl[i] = Item(node.key, node.value)
|
||||
}
|
||||
return json.Marshal(sl)
|
||||
}
|
||||
|
||||
// MarshalJSON returns JSON encoded bytes representing s.
|
||||
func (s *set) MarshalJSON() ([]byte, error) {
|
||||
if s.keys == nil {
|
||||
return []byte(`[]`), nil
|
||||
}
|
||||
return json.Marshal(s.sortedKeys())
|
||||
}
|
||||
|
||||
func (l *lazyObj) MarshalJSON() ([]byte, error) {
|
||||
return l.force().(*object).MarshalJSON()
|
||||
}
|
||||
|
||||
func (n *Not) MarshalJSON() ([]byte, error) {
|
||||
data := map[string]any{
|
||||
"type": "not",
|
||||
"body": n.Body,
|
||||
"explicit_body": n.ExplicitBody,
|
||||
}
|
||||
|
||||
if astJSON.GetOptions().MarshalOptions.IncludeLocation.Not {
|
||||
if n.Location != nil {
|
||||
data["location"] = n.Location
|
||||
}
|
||||
}
|
||||
|
||||
return json.Marshal(data)
|
||||
}
|
||||
|
||||
func (n *Not) UnmarshalJSON(bs []byte) error {
|
||||
v := map[string]any{}
|
||||
if err := util.UnmarshalJSON(bs, &v); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return unmarshalNot(n, v)
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
//go:build go1.27
|
||||
|
||||
package ast
|
||||
|
||||
import (
|
||||
"encoding"
|
||||
"encoding/json/jsontext"
|
||||
"encoding/json/v2"
|
||||
"fmt"
|
||||
|
||||
"github.com/open-policy-agent/opa/internal/jsonv2"
|
||||
astJSON "github.com/open-policy-agent/opa/v1/ast/json"
|
||||
"github.com/open-policy-agent/opa/v1/util"
|
||||
)
|
||||
|
||||
var (
|
||||
_ json.MarshalerTo = &Term{}
|
||||
_ json.Unmarshaler = &LogicalOr{}
|
||||
_ json.MarshalerTo = &LogicalOr{}
|
||||
_ json.MarshalerTo = &Not{}
|
||||
_ json.MarshalerTo = &Array{}
|
||||
_ json.MarshalerTo = &set{}
|
||||
_ json.MarshalerTo = &object{}
|
||||
_ json.MarshalerTo = &TemplateString{}
|
||||
_ json.MarshalerTo = &Ref{}
|
||||
_ json.MarshalerTo = &lazyObj{}
|
||||
_ json.MarshalerTo = Args{}
|
||||
_ json.MarshalerTo = Boolean(false)
|
||||
_ json.MarshalerTo = Null{}
|
||||
_ json.MarshalerTo = Number("")
|
||||
_ json.MarshalerTo = String("")
|
||||
_ json.MarshalerTo = Var("")
|
||||
_ json.Unmarshaler = &Not{}
|
||||
|
||||
// These are exported types, so losing MarshalJSON here would be a breaking
|
||||
// API change even though callers should go through json.Marshal, not this
|
||||
// method directly.
|
||||
_ json.Marshaler = Number("")
|
||||
_ json.Marshaler = &Term{}
|
||||
_ json.Marshaler = &Not{}
|
||||
_ json.Marshaler = &lazyObj{}
|
||||
_ json.Marshaler = &object{}
|
||||
_ json.Marshaler = &Array{}
|
||||
_ json.Marshaler = &set{}
|
||||
)
|
||||
|
||||
// These are here to ensure that we do not fall down to TextAppender, which
|
||||
// Go 1.27's encoding/json would otherwise use, encoding these as JSON strings.
|
||||
|
||||
func (b Boolean) MarshalJSONTo(e *jsontext.Encoder) error {
|
||||
return e.WriteToken(jsontext.Bool(bool(b)))
|
||||
}
|
||||
|
||||
func (Null) MarshalJSONTo(e *jsontext.Encoder) error {
|
||||
// Encoded as an empty object rather than null, as that's the representation
|
||||
// callers have come to expect. See also [marshalValueTo].
|
||||
return e.WriteValue([]byte("{}"))
|
||||
}
|
||||
|
||||
func (v Var) MarshalJSONTo(e *jsontext.Encoder) error {
|
||||
// Must produce the var name as a JSON string, wildcard vars included: that's
|
||||
// what encoding/json v1 does for a type whose underlying kind is string.
|
||||
return e.WriteToken(jsontext.String(string(v)))
|
||||
}
|
||||
|
||||
func (num Number) MarshalJSONTo(e *jsontext.Encoder) error {
|
||||
if num == "" {
|
||||
// Matches encoding/json v1, which encodes an empty json.Number as 0.
|
||||
return e.WriteToken(jsontext.Int(0))
|
||||
}
|
||||
return e.WriteValue(jsontext.Value(num))
|
||||
}
|
||||
|
||||
// MarshalJSON returns JSON encoded bytes representing num.
|
||||
func (num Number) MarshalJSON() ([]byte, error) {
|
||||
return jsonv2.MarshalMarshalerTo(num)
|
||||
}
|
||||
|
||||
func (str String) MarshalJSONTo(e *jsontext.Encoder) error {
|
||||
return e.WriteToken(jsontext.String(string(str)))
|
||||
}
|
||||
|
||||
func (t *Term) MarshalJSONTo(e *jsontext.Encoder) (err error) {
|
||||
// Token write errors are unchecked: an unbalanced value fails at the closing
|
||||
// token. A marshaller can fail having written a balanced value, so is checked.
|
||||
e.WriteToken(jsontext.BeginObject)
|
||||
|
||||
includeLocation := astJSON.GetOptions().MarshalOptions.IncludeLocation
|
||||
if t.Location != nil && includeLocation.Term {
|
||||
if err := jsonv2.WriteField(e, "location", t.Location); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
e.WriteToken(jsontext.String("type"))
|
||||
e.WriteToken(jsontext.String(ValueName(t.Value)))
|
||||
|
||||
e.WriteToken(jsontext.String("value"))
|
||||
if err = marshalValueTo(e, t.Value); err != nil {
|
||||
return fmt.Errorf("failed to marshal term of %s: %w", ValueName(t.Value), err)
|
||||
}
|
||||
|
||||
return e.WriteToken(jsontext.EndObject)
|
||||
}
|
||||
|
||||
// MarshalJSON returns the JSON encoding of the term.
|
||||
func (term *Term) MarshalJSON() ([]byte, error) {
|
||||
return jsonv2.MarshalMarshalerTo(term)
|
||||
}
|
||||
|
||||
func (r Ref) MarshalJSONTo(e *jsontext.Encoder) (err error) {
|
||||
return jsonv2.WriteMarshalerToArrayOrNull(e, r)
|
||||
}
|
||||
|
||||
func (t *TemplateString) MarshalJSONTo(e *jsontext.Encoder) (err error) {
|
||||
// Token write errors are unchecked: an unbalanced value fails at the closing
|
||||
// token. A marshaller can fail having written a balanced value, so is checked.
|
||||
e.WriteToken(jsontext.BeginObject)
|
||||
e.WriteToken(jsontext.String("parts"))
|
||||
if t.Parts == nil {
|
||||
// Parts has no omitempty tag, so it's always written. Matches
|
||||
// encoding/json v1, which encodes a nil slice as null rather than as an
|
||||
// empty array.
|
||||
e.WriteToken(jsontext.Null)
|
||||
} else {
|
||||
e.WriteToken(jsontext.BeginArray)
|
||||
for _, p := range t.Parts {
|
||||
switch v := p.(type) {
|
||||
case *Expr:
|
||||
if err := v.MarshalJSONTo(e); err != nil {
|
||||
return err
|
||||
}
|
||||
case *Term:
|
||||
if err := v.MarshalJSONTo(e); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
e.WriteToken(jsontext.EndArray)
|
||||
}
|
||||
|
||||
e.WriteToken(jsontext.String("multi_line"))
|
||||
e.WriteToken(jsontext.Bool(t.MultiLine))
|
||||
|
||||
return e.WriteToken(jsontext.EndObject)
|
||||
}
|
||||
|
||||
func (n *Not) MarshalJSONTo(e *jsontext.Encoder) error {
|
||||
e.WriteToken(jsontext.BeginObject)
|
||||
e.WriteToken(jsontext.String("type"))
|
||||
e.WriteToken(jsontext.String("not"))
|
||||
|
||||
if err := jsonv2.WriteField(e, "body", n.Body); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
e.WriteToken(jsontext.String("explicit_body"))
|
||||
e.WriteToken(jsontext.Bool(n.ExplicitBody))
|
||||
|
||||
if astJSON.GetOptions().MarshalOptions.IncludeLocation.Not && n.Location != nil {
|
||||
if err := jsonv2.WriteField(e, "location", n.Location); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return e.WriteToken(jsontext.EndObject)
|
||||
}
|
||||
|
||||
func (n *Not) MarshalJSON() ([]byte, error) {
|
||||
return jsonv2.MarshalMarshalerTo(n)
|
||||
}
|
||||
|
||||
func (n *Not) UnmarshalJSON(bs []byte) error {
|
||||
v := map[string]any{}
|
||||
if err := util.UnmarshalJSON(bs, &v); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return unmarshalNot(n, v)
|
||||
}
|
||||
|
||||
func (obj *object) MarshalJSONTo(e *jsontext.Encoder) error {
|
||||
// Token write errors are unchecked: an unbalanced value fails at the closing
|
||||
// token. A marshaller can fail having written a balanced value, so is checked.
|
||||
e.WriteToken(jsontext.BeginArray)
|
||||
|
||||
for _, node := range obj.sortedKeys() {
|
||||
e.WriteToken(jsontext.BeginArray)
|
||||
if err := node.key.MarshalJSONTo(e); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := node.value.MarshalJSONTo(e); err != nil {
|
||||
return err
|
||||
}
|
||||
e.WriteToken(jsontext.EndArray)
|
||||
}
|
||||
return e.WriteToken(jsontext.EndArray)
|
||||
}
|
||||
|
||||
func (l *lazyObj) MarshalJSONTo(e *jsontext.Encoder) error {
|
||||
return l.force().(*object).MarshalJSONTo(e)
|
||||
}
|
||||
|
||||
func (l *lazyObj) MarshalJSON() ([]byte, error) {
|
||||
return l.force().(*object).MarshalJSON()
|
||||
}
|
||||
|
||||
// MarshalJSON returns JSON encoded bytes representing obj.
|
||||
func (obj *object) MarshalJSON() ([]byte, error) {
|
||||
return jsonv2.MarshalMarshalerTo(obj)
|
||||
}
|
||||
|
||||
func (a *Array) MarshalJSONTo(e *jsontext.Encoder) error {
|
||||
return jsonv2.WriteMarshalerToArray(e, a.elems)
|
||||
}
|
||||
|
||||
// MarshalJSON returns JSON encoded bytes representing arr.
|
||||
func (arr *Array) MarshalJSON() ([]byte, error) {
|
||||
return jsonv2.MarshalMarshalerTo(arr)
|
||||
}
|
||||
|
||||
func (s *set) MarshalJSONTo(e *jsontext.Encoder) error {
|
||||
return jsonv2.WriteMarshalerToArray(e, s.sortedKeys())
|
||||
}
|
||||
|
||||
// MarshalJSON returns JSON encoded bytes representing s.
|
||||
func (s *set) MarshalJSON() ([]byte, error) {
|
||||
return jsonv2.MarshalMarshalerTo(s)
|
||||
}
|
||||
|
||||
func marshalValueTo(e *jsontext.Encoder, val Value) (err error) {
|
||||
switch v := val.(type) {
|
||||
case json.MarshalerTo:
|
||||
err = v.MarshalJSONTo(e)
|
||||
case encoding.TextAppender:
|
||||
var text []byte
|
||||
if text, err = v.AppendText(nil); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if text, err = jsontext.AppendQuote(e.AvailableBuffer(), text); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = e.WriteValue(text)
|
||||
default:
|
||||
err = json.MarshalEncode(e, v)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
// Copyright 2026 The OPA Authors. All rights reserved.
|
||||
// Use of this source code is governed by an Apache2
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
//go:build go1.27
|
||||
|
||||
package ast
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json/jsontext"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// textAppenderValue is a minimal Value that only implements
|
||||
// encoding.TextAppender (not json.MarshalerTo), to exercise the
|
||||
// TextAppender fallback branch of marshalValueTo.
|
||||
type textAppenderValue struct {
|
||||
text string
|
||||
err error
|
||||
}
|
||||
|
||||
func (textAppenderValue) Compare(Value) int { return 0 }
|
||||
func (textAppenderValue) Find(Ref) (Value, error) { return nil, nil }
|
||||
func (textAppenderValue) Hash() int { return 0 }
|
||||
func (textAppenderValue) IsGround() bool { return true }
|
||||
func (v textAppenderValue) String() string { return v.text }
|
||||
func (v textAppenderValue) StringLength() int { return len(v.text) }
|
||||
|
||||
func (v textAppenderValue) AppendText(buf []byte) ([]byte, error) {
|
||||
if v.err != nil {
|
||||
return buf, v.err
|
||||
}
|
||||
return append(buf, v.text...), nil
|
||||
}
|
||||
|
||||
func TestMarshalValueToTextAppenderError(t *testing.T) {
|
||||
wantErr := errors.New("boom")
|
||||
v := textAppenderValue{err: wantErr}
|
||||
|
||||
enc := jsontext.NewEncoder(new(bytes.Buffer))
|
||||
err := marshalValueTo(enc, v)
|
||||
if err == nil {
|
||||
t.Fatalf("expected error from AppendText to be propagated, got nil")
|
||||
}
|
||||
if !errors.Is(err, wantErr) {
|
||||
t.Fatalf("expected wrapped error %v, got %v", wantErr, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarshalValueToTextAppenderQuoting(t *testing.T) {
|
||||
v := textAppenderValue{text: "2026-01-01T00:00:00Z"}
|
||||
|
||||
var sb bytes.Buffer
|
||||
enc := jsontext.NewEncoder(&sb)
|
||||
if err := marshalValueTo(enc, v); err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
got := strings.TrimSpace(sb.String())
|
||||
want := `"2026-01-01T00:00:00Z"`
|
||||
if got != want {
|
||||
t.Fatalf("expected quoted JSON string %q, got %q", want, got)
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -119,7 +119,7 @@ func TestInterfaceToValueStructs(t *testing.T) {
|
||||
var m brokenMarshaller
|
||||
|
||||
_, err = InterfaceToValue(m)
|
||||
if err == nil || err.Error() != "ast: interface conversion: json: error calling MarshalJSON for type ast.brokenMarshaller: broken" {
|
||||
if err == nil || !strings.Contains(err.Error(), "ast: interface conversion: json: error calling MarshalJSON for type") {
|
||||
t.Fatal("expected error but got:", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"maps"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/open-policy-agent/opa/v1/util/test"
|
||||
@@ -56,7 +57,7 @@ func TestParseKeysConfig(t *testing.T) {
|
||||
"invalid_raw_config": {
|
||||
`[1,2,3]`,
|
||||
nil,
|
||||
true, errors.New("json: cannot unmarshal array into Go value of type map[string]json.RawMessage"),
|
||||
true, errors.New("json: cannot unmarshal array into Go value of type"),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -68,8 +69,13 @@ func TestParseKeysConfig(t *testing.T) {
|
||||
t.Fatal("Expected error but got nil")
|
||||
}
|
||||
|
||||
if tc.err != nil && tc.err.Error() != err.Error() {
|
||||
t.Fatalf("Expected error message %v but got %v", tc.err.Error(), err.Error())
|
||||
if tc.err != nil {
|
||||
exp := tc.err.Error()
|
||||
got := err.Error()
|
||||
|
||||
if !strings.HasPrefix(got, exp) {
|
||||
t.Fatalf("Expected error message %v but got %v", exp, got)
|
||||
}
|
||||
}
|
||||
} else if err != nil {
|
||||
t.Fatalf("Unexpected error %v", err)
|
||||
|
||||
@@ -53,6 +53,10 @@ type chunkEncoder struct {
|
||||
uncompressedLimit int64
|
||||
uncompressedLimitScaleUpExponent float64
|
||||
uncompressedLimitScaleDownExponent float64
|
||||
|
||||
// scalingDown records that a scaleDown is already in progress further up the
|
||||
// stack, so a nested one that cannot lower the limit knows it would cycle
|
||||
scalingDown bool
|
||||
}
|
||||
|
||||
func newChunkEncoder(limit int64) *chunkEncoder {
|
||||
@@ -286,6 +290,8 @@ func (enc *chunkEncoder) Encode(event EventV1, eventBytes []byte) ([][]byte, err
|
||||
}
|
||||
|
||||
func (enc *chunkEncoder) scaleDown(events []EventV1) ([][]byte, error) {
|
||||
reduced := false
|
||||
|
||||
if enc.uncompressedLimit > enc.limit {
|
||||
enc.incrMetric(encUncompressedLimitScaleDownCounterName)
|
||||
enc.incrMetric(encSoftLimitScaleDownCounterName)
|
||||
@@ -300,11 +306,23 @@ func (enc *chunkEncoder) scaleDown(events []EventV1) ([][]byte, error) {
|
||||
if enc.uncompressedLimitScaleUpExponent > 0 {
|
||||
enc.uncompressedLimitScaleUpExponent -= uncompressedLimitExponentScaleFactor
|
||||
}
|
||||
|
||||
reduced = true
|
||||
}
|
||||
|
||||
// The uncompressed limit has grown too large the events need to be split up into multiple chunks
|
||||
enc.initialize()
|
||||
|
||||
// A nested call that can't lower the limit further would re-encode the same
|
||||
// events into the same branch, recursing until the stack is exhausted.
|
||||
// Closing the chunk per event avoids it, as Encode never then reaches that
|
||||
// branch. Surfaced by Go 1.27's compress/flate sizes, but not specific to it.
|
||||
oneChunkPerEvent := enc.scalingDown && !reduced
|
||||
|
||||
wasScalingDown := enc.scalingDown
|
||||
enc.scalingDown = true
|
||||
defer func() { enc.scalingDown = wasScalingDown }()
|
||||
|
||||
// split the events into multiple chunks
|
||||
var result [][]byte
|
||||
for i := range events {
|
||||
@@ -322,6 +340,16 @@ func (enc *chunkEncoder) scaleDown(events []EventV1) ([][]byte, error) {
|
||||
if chunks != nil {
|
||||
result = append(result, chunks...)
|
||||
}
|
||||
|
||||
if oneChunkPerEvent {
|
||||
chunk, err := enc.reset()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if chunk != nil {
|
||||
result = append(result, chunk)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
|
||||
@@ -215,7 +215,10 @@ func TestChunkEncoder(t *testing.T) {
|
||||
func TestChunkEncoderSizeLimit(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
enc := newChunkEncoder(90).WithMetrics(metrics.New())
|
||||
// The limit has to sit just above the compressed size of the smallest event
|
||||
// (87 bytes on Go 1.26, 95 on Go 1.27) for this test to exercise the
|
||||
// encoder's equilibrium path on every Go version.
|
||||
enc := newChunkEncoder(96).WithMetrics(metrics.New())
|
||||
var result any = false
|
||||
var expInput any = map[string]any{"method": "GET"}
|
||||
ts, err := time.Parse(time.RFC3339Nano, "2018-01-01T12:00:00.123456Z")
|
||||
@@ -243,9 +246,9 @@ func TestChunkEncoderSizeLimit(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// expect the event to be written because it fits the minimum event size
|
||||
expectedBufferSize := 78 // the compressed size of an absurd small event
|
||||
if enc.buf.Len() != expectedBufferSize {
|
||||
t.Errorf("Expected %v buffer size but got: %v", expectedBufferSize, enc.buf.Len())
|
||||
// No exact compressed size here: gzip output differs between Go versions.
|
||||
if enc.buf.Len() == 0 {
|
||||
t.Error("Expected the event to have been written to the buffer")
|
||||
}
|
||||
expectedBytesWritten := 69 // the uncompressed size of the event
|
||||
if enc.bytesWritten != expectedBytesWritten {
|
||||
@@ -293,10 +296,8 @@ func TestChunkEncoderSizeLimit(t *testing.T) {
|
||||
if err := enc.w.Flush(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
expectedBufferSize = 15
|
||||
if enc.buf.Len() != expectedBufferSize {
|
||||
t.Errorf("Expected %v buffer size but got: %v", expectedBufferSize, enc.buf.Len())
|
||||
}
|
||||
// Nothing was written to the fresh buffer, so only the gzip header is in it.
|
||||
// Its exact size isn't asserted, see above.
|
||||
expectedBytesWritten = 0
|
||||
if enc.bytesWritten != expectedBytesWritten {
|
||||
t.Errorf("Expected %v bytes written but got: %v", expectedBytesWritten, enc.bytesWritten)
|
||||
|
||||
@@ -231,9 +231,15 @@ func TestEventBuffer_Upload(t *testing.T) {
|
||||
numberOfEvents: 4,
|
||||
uploadSizeLimitBytes: 196, // Each test event is 195 bytes
|
||||
handleFunc: func(w http.ResponseWriter, r *http.Request) {
|
||||
// No. of events that fit in a chunk depends on how gzip packs
|
||||
// them, which varies between Go versions. So here we confirm
|
||||
// the len and that we get at least one event.
|
||||
if r.ContentLength > 196 {
|
||||
t.Errorf("uploaded chunk of %d bytes exceeds the limit of 196", r.ContentLength)
|
||||
}
|
||||
events := decodeLogEvent(t, r.Body)
|
||||
if len(events) != 1 {
|
||||
t.Errorf("expected 1 events, got %d", len(events))
|
||||
if len(events) == 0 {
|
||||
t.Error("expected a chunk to hold at least one event")
|
||||
}
|
||||
allEvents = append(allEvents, events...)
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
@@ -154,7 +154,10 @@ func TestHandlerOnEndpointsWithoutCompression(t *testing.T) {
|
||||
|
||||
func zipString(input string) []byte {
|
||||
var b bytes.Buffer
|
||||
gz := gzip.NewWriter(&b)
|
||||
gz, err := gzip.NewWriterLevel(&b, defaultCompressionLevel)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if _, err := gz.Write([]byte(input)); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -5722,7 +5722,7 @@ func newStreamedReqUnversioned(method string, path string, body io.Reader) *http
|
||||
|
||||
func mustUnmarshalTrace(t types.TraceV1) (trace types.TraceV1Raw) {
|
||||
if err := json.Unmarshal(t, &trace); err != nil {
|
||||
panic("not reached")
|
||||
panic(err)
|
||||
}
|
||||
return trace
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
//go:build go1.27
|
||||
|
||||
package topdown
|
||||
|
||||
import (
|
||||
"encoding/json/jsontext"
|
||||
"errors"
|
||||
|
||||
"github.com/open-policy-agent/opa/internal/jsonv2"
|
||||
)
|
||||
|
||||
func (e *Error) MarshalJSONTo(enc *jsontext.Encoder) (err error) {
|
||||
enc.WriteToken(jsontext.BeginObject)
|
||||
enc.WriteToken(jsontext.String("code"))
|
||||
enc.WriteToken(jsontext.String(e.Code))
|
||||
enc.WriteToken(jsontext.String("message"))
|
||||
enc.WriteToken(jsontext.String(e.Message))
|
||||
|
||||
if e.Location != nil {
|
||||
err = jsonv2.WriteField(enc, "location", e.Location)
|
||||
}
|
||||
|
||||
return errors.Join(err, enc.WriteToken(jsontext.EndObject))
|
||||
}
|
||||
@@ -169,12 +169,26 @@ func TestHTTPSendRetryRequest(t *testing.T) {
|
||||
}
|
||||
}))
|
||||
|
||||
defer ts.Close()
|
||||
t.Cleanup(ts.Close)
|
||||
|
||||
// delay server start to exercise retry logic
|
||||
// delay server start to exercise retry logic. Guarded because a
|
||||
// subtest can finish, and so close the server, before the delay
|
||||
// elapses: starting a closed server panics on Go 1.27+. Cleanups
|
||||
// run LIFO, so this marks the server closed before ts.Close runs.
|
||||
var mu sync.Mutex
|
||||
closed := false
|
||||
t.Cleanup(func() {
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
closed = true
|
||||
})
|
||||
go func() {
|
||||
time.Sleep(time.Second * 5)
|
||||
ts.Start()
|
||||
mu.Lock()
|
||||
defer mu.Unlock()
|
||||
if !closed {
|
||||
ts.Start()
|
||||
}
|
||||
}()
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
Reference in New Issue
Block a user