mirror of
https://github.com/open-policy-agent/opa.git
synced 2026-08-12 19:32:48 -06:00
Add proto schemas for the IR plan and bundle manifest (#8775)
This adds two new proto schemas: * v1/bundle/manifest.proto * v1/ir/plan.proto --------- Signed-off-by: Sebastian Spaink <sebastianspaink@gmail.com>
This commit is contained in:
@@ -23,6 +23,7 @@ jobs:
|
||||
docs: ${{ steps.changes.outputs.docs }}
|
||||
rego: ${{ steps.changes.outputs.rego }}
|
||||
yaml: ${{ steps.changes.outputs.yaml }}
|
||||
proto: ${{ steps.changes.outputs.proto }}
|
||||
steps:
|
||||
- name: Check out repository code
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
@@ -46,6 +47,7 @@ jobs:
|
||||
echo "docs=true" >> $GITHUB_OUTPUT
|
||||
echo "rego=true" >> $GITHUB_OUTPUT
|
||||
echo "yaml=true" >> $GITHUB_OUTPUT
|
||||
echo "proto=true" >> $GITHUB_OUTPUT
|
||||
|
||||
# Get changed files: use git diff for merge_group, PR API for pull_request
|
||||
if [ -n "${{ github.event.merge_group.base_sha }}" ]; then
|
||||
@@ -54,13 +56,13 @@ jobs:
|
||||
elif ! curl -s -o changed_files.json -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
|
||||
"https://api.github.com/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/files"; then
|
||||
echo "Error: Failed to fetch changed files from GitHub API"
|
||||
echo "Defaulting to running all checks (go=true, wasm=true, docs=true, rego=true, yaml=true)"
|
||||
echo "Defaulting to running all checks (go=true, wasm=true, docs=true, rego=true, yaml=true, proto=true)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ ! -s changed_files.json ]; then
|
||||
echo "Warning: No changed files found"
|
||||
echo "Defaulting to running all checks (go=true, wasm=true, docs=true, rego=true, yaml=true)"
|
||||
echo "Defaulting to running all checks (go=true, wasm=true, docs=true, rego=true, yaml=true, proto=true)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@@ -78,12 +80,14 @@ jobs:
|
||||
docs_result=$(jq -r '.changes.docs // false' opa_result.json)
|
||||
rego_result=$(jq -r '.changes.rego // false' opa_result.json)
|
||||
yaml_result=$(jq -r '.changes.yaml // false' opa_result.json)
|
||||
proto_result=$(jq -r '.changes.proto // false' opa_result.json)
|
||||
|
||||
echo "go=${go_result}" >> $GITHUB_OUTPUT
|
||||
echo "wasm=${wasm_result}" >> $GITHUB_OUTPUT
|
||||
echo "docs=${docs_result}" >> $GITHUB_OUTPUT
|
||||
echo "rego=${rego_result}" >> $GITHUB_OUTPUT
|
||||
echo "yaml=${yaml_result}" >> $GITHUB_OUTPUT
|
||||
echo "proto=${proto_result}" >> $GITHUB_OUTPUT
|
||||
|
||||
echo "Final outputs:"
|
||||
echo " go=${go_result}"
|
||||
@@ -91,6 +95,7 @@ jobs:
|
||||
echo " docs=${docs_result}"
|
||||
echo " rego=${rego_result}"
|
||||
echo " yaml=${yaml_result}"
|
||||
echo " proto=${proto_result}"
|
||||
|
||||
# All jobs essentially re-create the `ci-release-test` make target, but are split
|
||||
# up for parallel runners for faster PR feedback and a nicer UX.
|
||||
@@ -264,6 +269,44 @@ jobs:
|
||||
env:
|
||||
YAML_LINT_FORMAT: github
|
||||
|
||||
proto-check:
|
||||
name: Proto Lint and Breaking
|
||||
runs-on: ubuntu-24.04
|
||||
needs: check-changes
|
||||
if: ${{ needs.check-changes.outputs.proto == 'true' }}
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Install buf
|
||||
uses: bufbuild/buf-setup-action@a47c93e0b1648d5651a065437926377d060baa99 # v1.50.0
|
||||
with:
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: buf lint
|
||||
run: buf lint
|
||||
|
||||
- name: buf breaking against base
|
||||
# Bootstrap PR has no buf.yaml on the comparison ref — skip then.
|
||||
env:
|
||||
BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.merge_group.base_sha }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -n "${BASE_SHA:-}" ]; then
|
||||
git fetch --no-tags --depth=1 origin "$BASE_SHA"
|
||||
ref="$BASE_SHA"
|
||||
else
|
||||
git fetch --no-tags --depth=1 origin main
|
||||
ref="$(git rev-parse FETCH_HEAD)"
|
||||
fi
|
||||
if git show "${ref}:buf.yaml" >/dev/null 2>&1; then
|
||||
buf breaking --against ".git#ref=${ref}"
|
||||
else
|
||||
echo "Comparison ref ${ref} has no buf.yaml — bootstrap PR, skipping breaking-change check."
|
||||
fi
|
||||
|
||||
gh-actions-lint:
|
||||
name: Github Actions Lint
|
||||
runs-on: ubuntu-24.04
|
||||
@@ -672,6 +715,7 @@ jobs:
|
||||
go-lint,
|
||||
yaml-lint,
|
||||
gh-actions-lint,
|
||||
proto-check,
|
||||
wasm,
|
||||
check-generated,
|
||||
race-detector,
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
# buf configuration for OPA's hand-authored .proto schemas.
|
||||
|
||||
version: v2
|
||||
modules:
|
||||
- path: v1/ir
|
||||
- path: v1/bundle
|
||||
lint:
|
||||
use:
|
||||
- MINIMAL
|
||||
except:
|
||||
- PACKAGE_DIRECTORY_MATCH
|
||||
breaking:
|
||||
use:
|
||||
- FILE
|
||||
@@ -63,6 +63,12 @@ go_root_files := [
|
||||
"main.go",
|
||||
]
|
||||
|
||||
# Paths covered by buf.yaml.
|
||||
proto_change_prefixes := [
|
||||
"v1/bundle/",
|
||||
"v1/ir/",
|
||||
]
|
||||
|
||||
changes.docs if {
|
||||
some changed_file in input
|
||||
startswith(changed_file.filename, "docs/")
|
||||
@@ -81,6 +87,9 @@ changes.go if {
|
||||
} else if {
|
||||
some changed_file in input
|
||||
changed_file.filename in go_root_files
|
||||
} else if {
|
||||
# .proto changes also run go-test (consistency tests live there).
|
||||
changes.proto
|
||||
}
|
||||
|
||||
changes.wasm if {
|
||||
@@ -104,6 +113,15 @@ changes.yaml if {
|
||||
strings.any_suffix_match(changed_file.filename, yaml_change_suffixes)
|
||||
}
|
||||
|
||||
changes.proto if {
|
||||
some changed_file in input
|
||||
strings.any_prefix_match(changed_file.filename, proto_change_prefixes)
|
||||
endswith(changed_file.filename, ".proto")
|
||||
} else if {
|
||||
some changed_file in input
|
||||
changed_file.filename == "buf.yaml"
|
||||
}
|
||||
|
||||
changes.bench contains "./v1/ast" if {
|
||||
some changed_file in input
|
||||
startswith(changed_file.filename, "v1/ast/")
|
||||
|
||||
@@ -55,6 +55,10 @@ example_bench_rego_changelist := [{"filename": "v1/rego/rego.go"}]
|
||||
|
||||
example_bench_no_match_changelist := [{"filename": "cmd/build.go"}]
|
||||
|
||||
example_proto_changelist := [{"filename": "v1/ir/plan.proto"}]
|
||||
|
||||
example_buf_yaml_changelist := [{"filename": "buf.yaml"}]
|
||||
|
||||
test_run_docs_check_expect if {
|
||||
pr_check.changes.docs with input as example_docs_changelist
|
||||
}
|
||||
@@ -119,3 +123,28 @@ test_bench_no_match if {
|
||||
pr_check.changes.go with input as example_bench_no_match_changelist
|
||||
pr_check.changes.bench == set() with input as example_bench_no_match_changelist
|
||||
}
|
||||
|
||||
test_run_proto_check_expect if {
|
||||
pr_check.changes.proto with input as example_proto_changelist
|
||||
}
|
||||
|
||||
test_run_proto_check_on_buf_yaml if {
|
||||
pr_check.changes.proto with input as example_buf_yaml_changelist
|
||||
}
|
||||
|
||||
test_run_no_proto_check_for_unrelated if {
|
||||
not pr_check.changes.proto with input as example_go_changelist
|
||||
}
|
||||
|
||||
# A .proto outside the configured buf.yaml modules must NOT trigger the
|
||||
# proto-check job — buf would silently ignore it and we'd ship false-green.
|
||||
test_run_no_proto_check_for_stray_proto if {
|
||||
stray := [{"filename": "docs/examples/foo.proto"}]
|
||||
not pr_check.changes.proto with input as stray
|
||||
}
|
||||
|
||||
# A .proto-only PR must still trigger the go-test job, since the
|
||||
# Go-vs-proto consistency tests live in v1/bundle / v1/ir.
|
||||
test_proto_only_pr_triggers_go_test if {
|
||||
pr_check.changes.go with input as example_proto_changelist
|
||||
}
|
||||
|
||||
@@ -27,6 +27,18 @@ When contributing please consider the following pointers:
|
||||
lightweight, and easily embedded. Vendoring may make features _easier_ to
|
||||
implement however they come with their own cost for both OPA developers and
|
||||
OPA users (e.g., vendoring conflicts, security, debugging, etc.)
|
||||
- **Wire-format schemas:** The IR plan and bundle manifest each have two
|
||||
published schemas: a JSON Schema generated from the Go types (drift-tested),
|
||||
and a hand-authored `.proto` (Edition 2023) checked for consistency with the
|
||||
Go types. When adding, renaming, or removing fields on `ir.Policy`,
|
||||
`ir.Stmt` / `ir.Val` kinds, or `bundle.Manifest`, update the matching
|
||||
`.proto` (`v1/ir/plan.proto` or `v1/bundle/manifest.proto`) in the same
|
||||
change set. Field numbers and `oneof` case numbers in the `.proto` are
|
||||
a permanent wire-format commitment — never reuse a number for a
|
||||
different field, and mark removed fields with `reserved`. The consistency tests
|
||||
(`e2e/proto/plan_test.go`, `e2e/proto/manifest_test.go`) catch
|
||||
drift; `buf breaking` (in CI) catches wire-incompatible
|
||||
changes.
|
||||
- **AI Tooling**: You can use generative AI tooling to assist your work on OPA,
|
||||
but please review the project's [AI Guidelines](#ai-guidelines) below before doing so to
|
||||
help maintainers help you.
|
||||
|
||||
+2
-1
@@ -6,6 +6,7 @@ go 1.25.7
|
||||
replace github.com/open-policy-agent/opa => ../
|
||||
|
||||
require (
|
||||
github.com/bufbuild/protocompile v0.14.1
|
||||
github.com/go-sql-driver/mysql v1.10.0
|
||||
github.com/google/go-cmp v0.7.0
|
||||
github.com/lib/pq v1.12.3
|
||||
@@ -13,6 +14,7 @@ require (
|
||||
github.com/open-policy-agent/opa v1.8.0
|
||||
github.com/rogpeppe/go-internal v1.15.0
|
||||
github.com/testcontainers/testcontainers-go v0.42.0
|
||||
google.golang.org/protobuf v1.36.11
|
||||
modernc.org/sqlite v1.51.0
|
||||
)
|
||||
|
||||
@@ -142,7 +144,6 @@ require (
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect
|
||||
google.golang.org/grpc v1.81.1 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
gopkg.in/ini.v1 v1.67.2 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
|
||||
@@ -30,6 +30,8 @@ github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig
|
||||
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw=
|
||||
github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c=
|
||||
github.com/bytecodealliance/wasmtime-go/v44 v44.0.0 h1:WRZXnLPIer/TWs5aYPaMlmVcOlzmR6Ur6wjLRIQOhTQ=
|
||||
github.com/bytecodealliance/wasmtime-go/v44 v44.0.0/go.mod h1:GP93piU+39CoFVCQ5xfHrPOUtL0APlMnkbblJ2d3YY0=
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
|
||||
|
||||
@@ -0,0 +1,288 @@
|
||||
// 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.
|
||||
|
||||
package proto
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/bufbuild/protocompile"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
|
||||
"github.com/open-policy-agent/opa/e2e/proto/protoroundtrip"
|
||||
"github.com/open-policy-agent/opa/e2e/proto/protoschemacheck"
|
||||
"github.com/open-policy-agent/opa/v1/ast"
|
||||
"github.com/open-policy-agent/opa/v1/ast/location"
|
||||
"github.com/open-policy-agent/opa/v1/bundle"
|
||||
)
|
||||
|
||||
func TestManifestProtoConsistency(t *testing.T) {
|
||||
protoschemacheck.Run(t, protoschemacheck.Spec{
|
||||
ProtoPath: "manifest.proto",
|
||||
ImportPaths: []string{"../../v1/bundle"},
|
||||
Messages: []protoschemacheck.MessageSpec{
|
||||
{
|
||||
Name: "Manifest",
|
||||
GoType: reflect.TypeOf(bundle.Manifest{}),
|
||||
},
|
||||
{
|
||||
Name: "WasmResolver",
|
||||
GoType: reflect.TypeOf(bundle.WasmResolver{}),
|
||||
},
|
||||
{
|
||||
Name: "Annotations",
|
||||
GoType: reflect.TypeOf(ast.Annotations{}),
|
||||
// `comments` and `node` are unexported on the Go side
|
||||
// and skipped by protoschemacheck's json-tag walk.
|
||||
},
|
||||
{
|
||||
Name: "SchemaAnnotation",
|
||||
GoType: reflect.TypeOf(ast.SchemaAnnotation{}),
|
||||
// ast.Ref is []*Term in Go; modeled as canonical-form
|
||||
// string in proto. *any Definition modeled as
|
||||
// google.protobuf.Value. Neither admits a reflect-level
|
||||
// type match against a proto string field.
|
||||
OpaqueProtoFields: []string{"path", "schema"},
|
||||
},
|
||||
{
|
||||
Name: "CompileAnnotation",
|
||||
GoType: reflect.TypeOf(ast.CompileAnnotation{}),
|
||||
// Same Ref → string mapping as SchemaAnnotation.
|
||||
OpaqueProtoFields: []string{"unknowns", "mask_rule"},
|
||||
},
|
||||
{
|
||||
Name: "AuthorAnnotation",
|
||||
GoType: reflect.TypeOf(ast.AuthorAnnotation{}),
|
||||
},
|
||||
{
|
||||
Name: "RelatedResourceAnnotation",
|
||||
GoType: reflect.TypeOf(ast.RelatedResourceAnnotation{}),
|
||||
// url.URL → string via its String() method.
|
||||
OpaqueProtoFields: []string{"ref"},
|
||||
},
|
||||
{
|
||||
Name: "Location",
|
||||
GoType: reflect.TypeOf(location.Location{}),
|
||||
// `Text`, `Offset`, `Tabs` are tagged `json:"-"` and
|
||||
// skipped by the json-tag walk.
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestManifestProtoRoundTrip(t *testing.T) {
|
||||
codec := loadManifestCodec(t)
|
||||
regoV1 := 1
|
||||
|
||||
tests := []struct {
|
||||
note string
|
||||
manifest bundle.Manifest
|
||||
}{
|
||||
{
|
||||
note: "empty manifest",
|
||||
manifest: bundle.Manifest{},
|
||||
},
|
||||
{
|
||||
note: "revision and roots",
|
||||
manifest: func() bundle.Manifest {
|
||||
m := bundle.Manifest{Revision: "abc123"}
|
||||
m.Init()
|
||||
m.AddRoot("roles")
|
||||
m.AddRoot("http/example/authz")
|
||||
return m
|
||||
}(),
|
||||
},
|
||||
{
|
||||
note: "rego version and per-file overrides",
|
||||
manifest: bundle.Manifest{
|
||||
Revision: "abc123",
|
||||
RegoVersion: ®oV1,
|
||||
FileRegoVersions: map[string]int{
|
||||
"/foo/*.rego": 0,
|
||||
"/policy1.rego": 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
note: "wasm resolvers",
|
||||
manifest: bundle.Manifest{
|
||||
Revision: "abc123",
|
||||
WasmResolvers: []bundle.WasmResolver{
|
||||
{Entrypoint: "http/example/authz/allow", Module: "/policy.wasm"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
note: "metadata",
|
||||
manifest: bundle.Manifest{
|
||||
Revision: "abc123",
|
||||
Metadata: map[string]any{
|
||||
"build_id": "ci-1234",
|
||||
"tags": []any{"prod", "us-east-1"},
|
||||
"nested": map[string]any{"k": 1.5, "ok": true},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
note: "wasm resolver with annotations",
|
||||
manifest: bundle.Manifest{
|
||||
Revision: "abc123",
|
||||
WasmResolvers: []bundle.WasmResolver{{
|
||||
Entrypoint: "http/example/authz/allow",
|
||||
Module: "/policy.wasm",
|
||||
Annotations: []*ast.Annotations{{
|
||||
Scope: "rule",
|
||||
Title: "Allow rule",
|
||||
Description: "Permits authorized requests",
|
||||
Entrypoint: true,
|
||||
Organizations: []string{"acme"},
|
||||
Authors: []*ast.AuthorAnnotation{
|
||||
{Name: "Alice", Email: "alice@example.com"},
|
||||
},
|
||||
Custom: map[string]any{"sla": "p99-100ms"},
|
||||
Labels: map[string]any{"team": "platform"},
|
||||
}},
|
||||
}},
|
||||
},
|
||||
},
|
||||
{
|
||||
note: "annotations with refs, schemas, compile, location",
|
||||
manifest: bundle.Manifest{
|
||||
Revision: "abc123",
|
||||
WasmResolvers: []bundle.WasmResolver{{
|
||||
Entrypoint: "http/example/authz/allow",
|
||||
Module: "/policy.wasm",
|
||||
Annotations: []*ast.Annotations{{
|
||||
Scope: "package",
|
||||
RelatedResources: []*ast.RelatedResourceAnnotation{
|
||||
{Ref: mustParseURL("https://example.com/docs"), Description: "design doc"},
|
||||
},
|
||||
Schemas: []*ast.SchemaAnnotation{
|
||||
{
|
||||
Path: ast.MustParseRef("input.user"),
|
||||
Schema: ast.MustParseRef("schema.user"),
|
||||
Definition: ptrAny(map[string]any{"type": "object"}),
|
||||
},
|
||||
},
|
||||
Compile: &ast.CompileAnnotation{
|
||||
Unknowns: []ast.Ref{ast.MustParseRef("input.x"), ast.MustParseRef("input.y")},
|
||||
MaskRule: ast.MustParseRef("data.policy.mask"),
|
||||
},
|
||||
Location: &location.Location{File: "policy.rego", Row: 3, Col: 1},
|
||||
}},
|
||||
}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
opts := []cmp.Option{
|
||||
cmpopts.EquateEmpty(),
|
||||
// ast.Annotations carries unexported `comments` and `node` that
|
||||
// aren't part of the wire shape; ignore them in the diff.
|
||||
cmpopts.IgnoreUnexported(ast.Annotations{}),
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.note, func(t *testing.T) {
|
||||
bs, err := codec.Encode(&tc.manifest)
|
||||
if err != nil {
|
||||
t.Fatalf("encode: %v", err)
|
||||
}
|
||||
var got bundle.Manifest
|
||||
if err := codec.Decode(bs, &got); err != nil {
|
||||
t.Fatalf("decode: %v", err)
|
||||
}
|
||||
// EquateEmpty handles nil-vs-empty slice/map, but not the
|
||||
// *[]string pointer case on Manifest.Roots.
|
||||
normalizeRoots(&tc.manifest)
|
||||
normalizeRoots(&got)
|
||||
if diff := cmp.Diff(tc.manifest, got, opts...); diff != "" {
|
||||
t.Fatalf("round-trip mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeRoots(m *bundle.Manifest) {
|
||||
if m.Roots != nil && len(*m.Roots) == 0 {
|
||||
m.Roots = nil
|
||||
}
|
||||
}
|
||||
|
||||
func mustParseURL(s string) url.URL {
|
||||
u, err := url.Parse(s)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return *u
|
||||
}
|
||||
|
||||
func ptrAny(v any) *any {
|
||||
return &v
|
||||
}
|
||||
|
||||
func loadManifestCodec(t *testing.T) *protoroundtrip.Codec {
|
||||
t.Helper()
|
||||
c := protocompile.Compiler{
|
||||
Resolver: protocompile.WithStandardImports(&protocompile.SourceResolver{
|
||||
ImportPaths: []string{"../../v1/bundle"},
|
||||
}),
|
||||
}
|
||||
files, err := c.Compile(context.Background(), "manifest.proto")
|
||||
if err != nil {
|
||||
t.Fatalf("compile manifest.proto: %v", err)
|
||||
}
|
||||
codec := protoroundtrip.NewCodec(files[0])
|
||||
codec.RegisterRoot(reflect.TypeOf(bundle.Manifest{}), "Manifest")
|
||||
codec.RegisterRoot(reflect.TypeOf(bundle.WasmResolver{}), "WasmResolver")
|
||||
codec.RegisterRoot(reflect.TypeOf(ast.Annotations{}), "Annotations")
|
||||
codec.RegisterRoot(reflect.TypeOf(ast.SchemaAnnotation{}), "SchemaAnnotation")
|
||||
codec.RegisterRoot(reflect.TypeOf(ast.CompileAnnotation{}), "CompileAnnotation")
|
||||
codec.RegisterRoot(reflect.TypeOf(ast.AuthorAnnotation{}), "AuthorAnnotation")
|
||||
codec.RegisterRoot(reflect.TypeOf(ast.RelatedResourceAnnotation{}), "RelatedResourceAnnotation")
|
||||
codec.RegisterRoot(reflect.TypeOf(location.Location{}), "Location")
|
||||
|
||||
// ast.Ref ↔ canonical dotted form. Empty refs round-trip through "".
|
||||
codec.RegisterScalarConverter(reflect.TypeOf(ast.Ref{}), protoroundtrip.ScalarConverter{
|
||||
Encode: func(rv reflect.Value) (string, error) {
|
||||
ref := rv.Interface().(ast.Ref)
|
||||
if len(ref) == 0 {
|
||||
return "", nil
|
||||
}
|
||||
return ref.String(), nil
|
||||
},
|
||||
Decode: func(s string) (reflect.Value, error) {
|
||||
if s == "" {
|
||||
return reflect.ValueOf(ast.Ref(nil)), nil
|
||||
}
|
||||
ref, err := ast.ParseRef(s)
|
||||
if err != nil {
|
||||
return reflect.Value{}, err
|
||||
}
|
||||
return reflect.ValueOf(ref), nil
|
||||
},
|
||||
})
|
||||
|
||||
// url.URL ↔ its String() form. url.Parse("") returns the zero URL
|
||||
// without error, so empty strings round-trip naturally.
|
||||
codec.RegisterScalarConverter(reflect.TypeOf(url.URL{}), protoroundtrip.ScalarConverter{
|
||||
Encode: func(rv reflect.Value) (string, error) {
|
||||
u := rv.Interface().(url.URL)
|
||||
return u.String(), nil
|
||||
},
|
||||
Decode: func(s string) (reflect.Value, error) {
|
||||
u, err := url.Parse(s)
|
||||
if err != nil {
|
||||
return reflect.Value{}, err
|
||||
}
|
||||
return reflect.ValueOf(*u), nil
|
||||
},
|
||||
})
|
||||
|
||||
return codec
|
||||
}
|
||||
@@ -0,0 +1,408 @@
|
||||
// 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.
|
||||
|
||||
package proto
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"unicode"
|
||||
|
||||
"github.com/bufbuild/protocompile"
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/google/go-cmp/cmp/cmpopts"
|
||||
|
||||
"github.com/open-policy-agent/opa/e2e/proto/protoroundtrip"
|
||||
"github.com/open-policy-agent/opa/e2e/proto/protoschemacheck"
|
||||
"github.com/open-policy-agent/opa/internal/planner"
|
||||
"github.com/open-policy-agent/opa/v1/ast"
|
||||
"github.com/open-policy-agent/opa/v1/ir"
|
||||
"github.com/open-policy-agent/opa/v1/test/cases"
|
||||
)
|
||||
|
||||
func TestPlanProtoConsistency(t *testing.T) {
|
||||
locationT := reflect.TypeOf(ir.Location{})
|
||||
|
||||
stmtMsg := func(name string, goType reflect.Type, opts ...func(*protoschemacheck.MessageSpec)) protoschemacheck.MessageSpec {
|
||||
m := protoschemacheck.MessageSpec{
|
||||
Name: name,
|
||||
GoType: goType,
|
||||
SkipEmbeddedTypes: []reflect.Type{locationT},
|
||||
}
|
||||
for _, opt := range opts {
|
||||
opt(&m)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
withFieldOverride := func(overrides map[string]string) func(*protoschemacheck.MessageSpec) {
|
||||
return func(m *protoschemacheck.MessageSpec) {
|
||||
m.FieldNameOverride = overrides
|
||||
}
|
||||
}
|
||||
|
||||
stmtBodies := []protoschemacheck.MessageSpec{
|
||||
stmtMsg("ArrayAppendStmt", reflect.TypeOf(ir.ArrayAppendStmt{})),
|
||||
stmtMsg("AssignIntStmt", reflect.TypeOf(ir.AssignIntStmt{})),
|
||||
stmtMsg("AssignVarOnceStmt", reflect.TypeOf(ir.AssignVarOnceStmt{})),
|
||||
stmtMsg("AssignVarStmt", reflect.TypeOf(ir.AssignVarStmt{})),
|
||||
stmtMsg("BlockStmt", reflect.TypeOf(ir.BlockStmt{})),
|
||||
stmtMsg("BreakStmt", reflect.TypeOf(ir.BreakStmt{})),
|
||||
stmtMsg("CallDynamicStmt", reflect.TypeOf(ir.CallDynamicStmt{})),
|
||||
// CallStmt.Func → proto "function" (avoids reserved-keyword
|
||||
// collision; mirrors Func.return → result).
|
||||
stmtMsg("CallStmt", reflect.TypeOf(ir.CallStmt{}),
|
||||
withFieldOverride(map[string]string{"func": "function"})),
|
||||
stmtMsg("DotStmt", reflect.TypeOf(ir.DotStmt{})),
|
||||
stmtMsg("EqualStmt", reflect.TypeOf(ir.EqualStmt{})),
|
||||
stmtMsg("IsArrayStmt", reflect.TypeOf(ir.IsArrayStmt{})),
|
||||
stmtMsg("IsDefinedStmt", reflect.TypeOf(ir.IsDefinedStmt{})),
|
||||
stmtMsg("IsObjectStmt", reflect.TypeOf(ir.IsObjectStmt{})),
|
||||
stmtMsg("IsSetStmt", reflect.TypeOf(ir.IsSetStmt{})),
|
||||
stmtMsg("IsUndefinedStmt", reflect.TypeOf(ir.IsUndefinedStmt{})),
|
||||
stmtMsg("LenStmt", reflect.TypeOf(ir.LenStmt{})),
|
||||
stmtMsg("MakeArrayStmt", reflect.TypeOf(ir.MakeArrayStmt{})),
|
||||
stmtMsg("MakeNullStmt", reflect.TypeOf(ir.MakeNullStmt{})),
|
||||
stmtMsg("MakeNumberIntStmt", reflect.TypeOf(ir.MakeNumberIntStmt{})),
|
||||
// MakeNumberRefStmt's Go field has no json tag, so its derived
|
||||
// JSON name is "Index"; the proto's canonical field is "index".
|
||||
// The runtime's MarshalJSON also emits a deprecated "Index" alias
|
||||
// — JSON-only contract, not modeled in the proto.
|
||||
stmtMsg("MakeNumberRefStmt", reflect.TypeOf(ir.MakeNumberRefStmt{}),
|
||||
withFieldOverride(map[string]string{"Index": "index"})),
|
||||
stmtMsg("MakeObjectStmt", reflect.TypeOf(ir.MakeObjectStmt{})),
|
||||
stmtMsg("MakeSetStmt", reflect.TypeOf(ir.MakeSetStmt{})),
|
||||
stmtMsg("NopStmt", reflect.TypeOf(ir.NopStmt{})),
|
||||
stmtMsg("NotEqualStmt", reflect.TypeOf(ir.NotEqualStmt{})),
|
||||
stmtMsg("NotStmt", reflect.TypeOf(ir.NotStmt{})),
|
||||
stmtMsg("ObjectInsertOnceStmt", reflect.TypeOf(ir.ObjectInsertOnceStmt{})),
|
||||
stmtMsg("ObjectInsertStmt", reflect.TypeOf(ir.ObjectInsertStmt{})),
|
||||
stmtMsg("ObjectMergeStmt", reflect.TypeOf(ir.ObjectMergeStmt{})),
|
||||
stmtMsg("ResetLocalStmt", reflect.TypeOf(ir.ResetLocalStmt{})),
|
||||
stmtMsg("ResultSetAddStmt", reflect.TypeOf(ir.ResultSetAddStmt{})),
|
||||
stmtMsg("ReturnLocalStmt", reflect.TypeOf(ir.ReturnLocalStmt{})),
|
||||
stmtMsg("ScanStmt", reflect.TypeOf(ir.ScanStmt{})),
|
||||
stmtMsg("SetAddStmt", reflect.TypeOf(ir.SetAddStmt{})),
|
||||
stmtMsg("WithStmt", reflect.TypeOf(ir.WithStmt{})),
|
||||
}
|
||||
|
||||
structural := []protoschemacheck.MessageSpec{
|
||||
{Name: "Policy", GoType: reflect.TypeOf(ir.Policy{})},
|
||||
{Name: "Static", GoType: reflect.TypeOf(ir.Static{})},
|
||||
{Name: "Plans", GoType: reflect.TypeOf(ir.Plans{})},
|
||||
{Name: "Funcs", GoType: reflect.TypeOf(ir.Funcs{})},
|
||||
// BuiltinFunc.Decl has no proto counterpart by design — see
|
||||
// the comment on `BuiltinFunc` in plan.proto.
|
||||
{
|
||||
Name: "BuiltinFunc",
|
||||
GoType: reflect.TypeOf(ir.BuiltinFunc{}),
|
||||
SkipGoFields: []string{"Decl"},
|
||||
},
|
||||
{Name: "Plan", GoType: reflect.TypeOf(ir.Plan{})},
|
||||
// Func.Return → proto "result" (avoids reserved-keyword collision).
|
||||
{
|
||||
Name: "Func",
|
||||
GoType: reflect.TypeOf(ir.Func{}),
|
||||
FieldNameOverride: map[string]string{"return": "result"},
|
||||
},
|
||||
{Name: "Block", GoType: reflect.TypeOf(ir.Block{})},
|
||||
{Name: "StringConst", GoType: reflect.TypeOf(ir.StringConst{})},
|
||||
{Name: "Operand", GoType: reflect.TypeOf(ir.Operand{})},
|
||||
// Stmt envelope: file/col/row come from ir.Location; oneof
|
||||
// validated via the OneofSpec below.
|
||||
{Name: "Stmt", GoType: locationT},
|
||||
}
|
||||
|
||||
messages := make([]protoschemacheck.MessageSpec, 0, len(structural)+len(stmtBodies))
|
||||
messages = append(messages, structural...)
|
||||
messages = append(messages, stmtBodies...)
|
||||
|
||||
stmtDiscToCase := map[string]string{}
|
||||
stmtDiscToGoType := map[string]reflect.Type{}
|
||||
for k, v := range ir.StmtKinds() {
|
||||
stmtDiscToCase[k] = camelToSnake(k)
|
||||
stmtDiscToGoType[k] = reflect.TypeOf(v)
|
||||
}
|
||||
|
||||
valDiscToCase := map[string]string{}
|
||||
valDiscToGoType := map[string]reflect.Type{}
|
||||
for k, v := range ir.ValKinds() {
|
||||
valDiscToCase[k] = camelToSnake(k)
|
||||
valDiscToGoType[k] = reflect.TypeOf(v)
|
||||
}
|
||||
|
||||
protoschemacheck.Run(t, protoschemacheck.Spec{
|
||||
ProtoPath: "plan.proto",
|
||||
ImportPaths: []string{"../../v1/ir"},
|
||||
Messages: messages,
|
||||
OpaqueMessages: []string{"Val"},
|
||||
Oneofs: []protoschemacheck.OneofSpec{
|
||||
{
|
||||
MessageName: "Stmt",
|
||||
OneofName: "kind",
|
||||
DiscriminatorToCase: stmtDiscToCase,
|
||||
DiscriminatorToGoType: stmtDiscToGoType,
|
||||
},
|
||||
{
|
||||
MessageName: "Val",
|
||||
OneofName: "kind",
|
||||
DiscriminatorToCase: valDiscToCase,
|
||||
DiscriminatorToGoType: valDiscToGoType,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func TestPlanProtoRoundTrip(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("too slow for testing.Short")
|
||||
}
|
||||
codec := loadPlanCodec(t)
|
||||
|
||||
tests := []struct {
|
||||
note string
|
||||
module string
|
||||
query string
|
||||
}{
|
||||
{
|
||||
note: "scalar comparison",
|
||||
module: `package test
|
||||
p if { input.foo == 7 }`,
|
||||
query: "data.test.p = true",
|
||||
},
|
||||
{
|
||||
note: "every / scan",
|
||||
module: `package test
|
||||
p if { every i in input.foo { i > 0 } }`,
|
||||
query: "data.test.p = true",
|
||||
},
|
||||
{
|
||||
note: "composite construction and negation",
|
||||
module: `package test
|
||||
p contains x if {
|
||||
some x in input.xs
|
||||
not x == "skip"
|
||||
}
|
||||
q := {k: v | some k, v in input.m}`,
|
||||
query: "data.test.p = x",
|
||||
},
|
||||
{
|
||||
note: "with override",
|
||||
module: `package test
|
||||
import data.lib
|
||||
p if { lib.allow with input as {"u": "alice"} }
|
||||
`,
|
||||
query: "data.test.p = true",
|
||||
},
|
||||
}
|
||||
|
||||
opts := roundTripCmpOpts()
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.note, func(t *testing.T) {
|
||||
plan := compileToPolicy(t, tc.module, tc.query)
|
||||
bs, err := codec.Encode(plan)
|
||||
if err != nil {
|
||||
t.Fatalf("encode: %v", err)
|
||||
}
|
||||
var got ir.Policy
|
||||
if err := codec.Decode(bs, &got); err != nil {
|
||||
t.Fatalf("decode: %v", err)
|
||||
}
|
||||
if diff := cmp.Diff(plan, &got, opts...); diff != "" {
|
||||
t.Fatalf("round-trip mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestPlanProtoRoundTripYAMLSuite runs the round-trip codec against every
|
||||
// case the planner accepts under v1/test/cases/testdata/v1.
|
||||
func TestPlanProtoRoundTripYAMLSuite(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("too slow for testing.Short")
|
||||
}
|
||||
codec := loadPlanCodec(t)
|
||||
corpus, err := cases.Load("../../v1/test/cases/testdata/v1")
|
||||
if err != nil {
|
||||
t.Fatalf("load YAML cases: %v", err)
|
||||
}
|
||||
if len(corpus.Cases) == 0 {
|
||||
t.Fatalf("no YAML cases loaded; did the test data move?")
|
||||
}
|
||||
|
||||
opts := roundTripCmpOpts()
|
||||
for _, tc := range corpus.Cases {
|
||||
t.Run(tc.Note, func(t *testing.T) {
|
||||
plan, err := planYAMLCase(tc)
|
||||
if err != nil {
|
||||
t.Skipf("planner did not accept case (not drift): %v", err)
|
||||
return
|
||||
}
|
||||
bs, err := codec.Encode(plan)
|
||||
if err != nil {
|
||||
t.Fatalf("encode: %v", err)
|
||||
}
|
||||
var got ir.Policy
|
||||
if err := codec.Decode(bs, &got); err != nil {
|
||||
t.Fatalf("decode: %v", err)
|
||||
}
|
||||
if diff := cmp.Diff(plan, &got, opts...); diff != "" {
|
||||
t.Fatalf("round-trip mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// roundTripCmpOpts returns the cmp.Options that ignore wire-format-irrelevant
|
||||
// fields (debug-only Location internals, BuiltinFunc.Decl absent from proto)
|
||||
// and equate nil with empty for slices and maps.
|
||||
func roundTripCmpOpts() []cmp.Option {
|
||||
return []cmp.Option{
|
||||
cmpopts.IgnoreUnexported(ir.Location{}),
|
||||
cmpopts.IgnoreFields(ir.BuiltinFunc{}, "Decl"),
|
||||
cmpopts.EquateEmpty(),
|
||||
}
|
||||
}
|
||||
|
||||
func planYAMLCase(tc cases.TestCase) (*ir.Policy, error) {
|
||||
if tc.Query == "" {
|
||||
return nil, errors.New("case has no query")
|
||||
}
|
||||
if len(tc.Modules) == 0 {
|
||||
return nil, errors.New("case has no modules")
|
||||
}
|
||||
moduleMap := map[string]string{}
|
||||
for i, m := range tc.Modules {
|
||||
moduleMap[fmt.Sprintf("module%d.rego", i)] = m
|
||||
}
|
||||
c, err := ast.CompileModules(moduleMap)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("compile modules: %w", err)
|
||||
}
|
||||
mods := make([]*ast.Module, 0, len(c.Modules))
|
||||
for _, m := range c.Modules {
|
||||
mods = append(mods, m)
|
||||
}
|
||||
body, err := ast.ParseBody(tc.Query)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("parse query: %w", err)
|
||||
}
|
||||
return planner.New().
|
||||
WithQueries([]planner.QuerySet{{Name: "main", Queries: []ast.Body{body}}}).
|
||||
WithModules(mods).
|
||||
WithBuiltinDecls(ast.BuiltinMap).
|
||||
Plan()
|
||||
}
|
||||
|
||||
func compileToPolicy(t *testing.T, module, query string) *ir.Policy {
|
||||
t.Helper()
|
||||
c, err := ast.CompileModules(map[string]string{"test.rego": module})
|
||||
if err != nil {
|
||||
t.Fatalf("compile modules: %v", err)
|
||||
}
|
||||
modules := make([]*ast.Module, 0, len(c.Modules))
|
||||
for _, m := range c.Modules {
|
||||
modules = append(modules, m)
|
||||
}
|
||||
plan, err := planner.New().
|
||||
WithQueries([]planner.QuerySet{{
|
||||
Name: "main",
|
||||
Queries: []ast.Body{ast.MustParseBody(query)},
|
||||
}}).
|
||||
WithModules(modules).
|
||||
WithBuiltinDecls(ast.BuiltinMap).
|
||||
Plan()
|
||||
if err != nil {
|
||||
t.Fatalf("plan: %v", err)
|
||||
}
|
||||
return plan
|
||||
}
|
||||
|
||||
func loadPlanCodec(t *testing.T) *protoroundtrip.Codec {
|
||||
t.Helper()
|
||||
c := protocompile.Compiler{
|
||||
Resolver: protocompile.WithStandardImports(&protocompile.SourceResolver{
|
||||
ImportPaths: []string{"../../v1/ir"},
|
||||
}),
|
||||
}
|
||||
files, err := c.Compile(context.Background(), "plan.proto")
|
||||
if err != nil {
|
||||
t.Fatalf("compile plan.proto: %v", err)
|
||||
}
|
||||
codec := protoroundtrip.NewCodec(files[0])
|
||||
|
||||
codec.RegisterRoot(reflect.TypeOf(ir.Policy{}), "Policy")
|
||||
codec.RegisterRoot(reflect.TypeOf(ir.Static{}), "Static")
|
||||
codec.RegisterRoot(reflect.TypeOf(ir.Plans{}), "Plans")
|
||||
codec.RegisterRoot(reflect.TypeOf(ir.Funcs{}), "Funcs")
|
||||
codec.RegisterRoot(reflect.TypeOf(ir.BuiltinFunc{}), "BuiltinFunc")
|
||||
codec.RegisterRoot(reflect.TypeOf(ir.Plan{}), "Plan")
|
||||
codec.RegisterRoot(reflect.TypeOf(ir.Func{}), "Func")
|
||||
codec.RegisterRoot(reflect.TypeOf(ir.Block{}), "Block")
|
||||
codec.RegisterRoot(reflect.TypeOf(ir.StringConst{}), "StringConst")
|
||||
codec.RegisterRoot(reflect.TypeOf(ir.Operand{}), "Operand")
|
||||
|
||||
codec.SkipGoFields(reflect.TypeOf(ir.BuiltinFunc{}), "Decl")
|
||||
codec.SetFieldNameOverride(reflect.TypeOf(ir.Func{}), map[string]string{
|
||||
"return": "result",
|
||||
})
|
||||
codec.SetFieldNameOverride(reflect.TypeOf(ir.CallStmt{}), map[string]string{
|
||||
"func": "function",
|
||||
})
|
||||
codec.SetFieldNameOverride(reflect.TypeOf(ir.MakeNumberRefStmt{}), map[string]string{
|
||||
"Index": "index",
|
||||
})
|
||||
|
||||
locationT := reflect.TypeOf(ir.Location{})
|
||||
for kind, body := range ir.StmtKinds() {
|
||||
bodyT := reflect.TypeOf(body)
|
||||
for bodyT.Kind() == reflect.Pointer {
|
||||
bodyT = bodyT.Elem()
|
||||
}
|
||||
codec.RegisterEnvelope(bodyT, protoroundtrip.EnvelopeSpec{
|
||||
Envelope: "Stmt",
|
||||
Oneof: "kind",
|
||||
Case: camelToSnake(kind),
|
||||
PromotedEmbed: locationT,
|
||||
})
|
||||
codec.SkipEmbedded(bodyT, locationT)
|
||||
}
|
||||
for kind, val := range ir.ValKinds() {
|
||||
valT := reflect.TypeOf(val)
|
||||
for valT.Kind() == reflect.Pointer {
|
||||
valT = valT.Elem()
|
||||
}
|
||||
codec.RegisterEnvelope(valT, protoroundtrip.EnvelopeSpec{
|
||||
Envelope: "Val",
|
||||
Oneof: "kind",
|
||||
Case: kind,
|
||||
})
|
||||
}
|
||||
|
||||
return codec
|
||||
}
|
||||
|
||||
// camelToSnake converts CamelCase to snake_case, handling acronym
|
||||
// boundaries: "AssignVarStmt" → "assign_var_stmt", "HTTPSendStmt" →
|
||||
// "http_send_stmt", "string_index" → "string_index" (unchanged).
|
||||
func camelToSnake(s string) string {
|
||||
rs := []rune(s)
|
||||
var b strings.Builder
|
||||
b.Grow(len(rs))
|
||||
for i, r := range rs {
|
||||
if i > 0 && unicode.IsUpper(r) {
|
||||
prevLower := unicode.IsLower(rs[i-1])
|
||||
nextLower := i+1 < len(rs) && unicode.IsLower(rs[i+1])
|
||||
if prevLower || nextLower {
|
||||
b.WriteByte('_')
|
||||
}
|
||||
}
|
||||
b.WriteRune(unicode.ToLower(r))
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,595 @@
|
||||
// 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.
|
||||
|
||||
// Package protoschemacheck verifies that a hand-authored .proto file
|
||||
// stays consistent with the Go types it mirrors. Powers the proto
|
||||
// consistency tests for v1/bundle/manifest.proto and v1/ir/plan.proto.
|
||||
package protoschemacheck
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"maps"
|
||||
"reflect"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/bufbuild/protocompile"
|
||||
"google.golang.org/protobuf/reflect/protoreflect"
|
||||
)
|
||||
|
||||
// loadTimeout caps how long we'll wait for protocompile to parse the
|
||||
// proto file so a pathological input fails fast.
|
||||
const loadTimeout = 30 * time.Second
|
||||
|
||||
// Spec describes a consistency check between a .proto file and a set of
|
||||
// Go types.
|
||||
type Spec struct {
|
||||
// ProtoPath is the .proto file relative to the test's working dir.
|
||||
ProtoPath string
|
||||
// ImportPaths are extra search dirs for imports; "." is added by default.
|
||||
ImportPaths []string
|
||||
// Messages enumerates proto messages to validate. Every message
|
||||
// declared in the .proto (including nested) must appear here or in
|
||||
// OpaqueMessages.
|
||||
Messages []MessageSpec
|
||||
// OpaqueMessages names polymorphic-union envelope messages with no
|
||||
// direct Go counterpart (content is checked via Oneofs). Every entry
|
||||
// must pair with an OneofSpec.
|
||||
OpaqueMessages []string
|
||||
// Oneofs enumerates polymorphic-union checks.
|
||||
Oneofs []OneofSpec
|
||||
}
|
||||
|
||||
// MessageSpec asserts that proto message Name matches GoType.
|
||||
type MessageSpec struct {
|
||||
Name string
|
||||
// GoType is the Go struct compared against the proto message.
|
||||
// Anonymous (embedded) struct fields are flattened recursively
|
||||
// (mirroring encoding/json) unless listed in SkipEmbeddedTypes.
|
||||
GoType reflect.Type
|
||||
// SkipEmbeddedTypes opts out specific embedded types from flattening
|
||||
// (their fields are validated on a different proto message — see
|
||||
// the IR plan schema where Stmt bodies embed Location but the proto
|
||||
// promotes Location's fields onto the Stmt envelope).
|
||||
SkipEmbeddedTypes []reflect.Type
|
||||
// FieldNameOverride remaps Go-side JSON name → proto field name
|
||||
// where the default mapping doesn't apply (e.g. ir.MakeNumberRefStmt's
|
||||
// Index Go field maps to proto field "index").
|
||||
FieldNameOverride map[string]string
|
||||
// OpaqueProtoFields names proto fields whose Go counterpart isn't
|
||||
// reflectable into proto (e.g. types.Function fields modeled as
|
||||
// google.protobuf.Struct). The check requires the Go field to exist
|
||||
// with a structural type — opaque is "no type check feasible", not
|
||||
// "no Go field expected".
|
||||
OpaqueProtoFields []string
|
||||
// SkipGoFields names Go fields (by Go field name, not JSON name)
|
||||
// that have no proto counterpart by design — e.g. ir.BuiltinFunc.Decl
|
||||
// is intentionally absent from the proto because consumers consult
|
||||
// their own registry for builtin signatures.
|
||||
SkipGoFields []string
|
||||
}
|
||||
|
||||
// OneofSpec asserts that the named oneof on MessageName has exactly the
|
||||
// listed cases with matching Go-side types.
|
||||
type OneofSpec struct {
|
||||
MessageName string
|
||||
OneofName string
|
||||
// DiscriminatorToCase maps each JSON-discriminator (runtime kind
|
||||
// name) to the proto oneof case name.
|
||||
DiscriminatorToCase map[string]string
|
||||
// DiscriminatorToGoType maps each discriminator to the Go type
|
||||
// implementing the union. Must have the same key set as
|
||||
// DiscriminatorToCase. Struct types check by name; scalar types
|
||||
// check by reflect.Kind.
|
||||
DiscriminatorToGoType map[string]reflect.Type
|
||||
}
|
||||
|
||||
// Run executes the spec, reporting every drift via t.Errorf.
|
||||
func Run(t *testing.T, spec Spec) {
|
||||
t.Helper()
|
||||
if err := validateSpec(spec); err != nil {
|
||||
t.Fatalf("invalid spec: %v", err)
|
||||
}
|
||||
file := loadProto(t, spec.ProtoPath, spec.ImportPaths)
|
||||
|
||||
declared := map[string]protoreflect.MessageDescriptor{}
|
||||
collectDeclared(file.Messages(), declared)
|
||||
|
||||
covered := map[string]bool{}
|
||||
oneofParents := map[string]bool{}
|
||||
for _, o := range spec.Oneofs {
|
||||
oneofParents[o.MessageName] = true
|
||||
}
|
||||
// Collect, per message, the names of oneofs covered by an OneofSpec.
|
||||
// Fields belonging to those oneofs are validated by checkOneof and
|
||||
// must NOT be reported as orphans by checkMessage's field walk.
|
||||
coveredOneofs := map[string]map[string]bool{}
|
||||
for _, o := range spec.Oneofs {
|
||||
if coveredOneofs[o.MessageName] == nil {
|
||||
coveredOneofs[o.MessageName] = map[string]bool{}
|
||||
}
|
||||
coveredOneofs[o.MessageName][o.OneofName] = true
|
||||
}
|
||||
for _, m := range spec.Messages {
|
||||
covered[m.Name] = true
|
||||
checkMessage(t, declared, m, coveredOneofs[m.Name])
|
||||
}
|
||||
for _, name := range spec.OpaqueMessages {
|
||||
covered[name] = true
|
||||
if _, ok := declared[name]; !ok {
|
||||
t.Errorf("opaque message %q listed in spec but not present in %s", name, spec.ProtoPath)
|
||||
}
|
||||
if !oneofParents[name] {
|
||||
t.Errorf("opaque message %q has no matching OneofSpec; OpaqueMessages is for polymorphic envelopes only — add an OneofSpec or move the message into Messages", name)
|
||||
}
|
||||
if hasMessageSpec(spec, name) {
|
||||
t.Errorf("message %q is listed in both Messages and OpaqueMessages — pick one", name)
|
||||
}
|
||||
}
|
||||
for name := range declared {
|
||||
if !covered[name] {
|
||||
t.Errorf("proto message %q is declared in %s but not covered by the spec; add a MessageSpec or list it in OpaqueMessages", name, spec.ProtoPath)
|
||||
}
|
||||
}
|
||||
|
||||
for _, o := range spec.Oneofs {
|
||||
checkOneof(t, declared, o)
|
||||
}
|
||||
}
|
||||
|
||||
func validateSpec(s Spec) error {
|
||||
if s.ProtoPath == "" {
|
||||
return errors.New("ProtoPath is required")
|
||||
}
|
||||
for _, o := range s.Oneofs {
|
||||
// EqualFunc with an always-true comparator collapses to a key-set
|
||||
// equality check, ignoring the differing value types.
|
||||
eq := maps.EqualFunc(o.DiscriminatorToCase, o.DiscriminatorToGoType,
|
||||
func(string, reflect.Type) bool { return true })
|
||||
if !eq {
|
||||
return fmt.Errorf("oneof %s.%s: DiscriminatorToCase and DiscriminatorToGoType have different key sets",
|
||||
o.MessageName, o.OneofName)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func hasMessageSpec(s Spec, name string) bool {
|
||||
for _, m := range s.Messages {
|
||||
if m.Name == name {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func collectDeclared(msgs protoreflect.MessageDescriptors, out map[string]protoreflect.MessageDescriptor) {
|
||||
for i := range msgs.Len() {
|
||||
m := msgs.Get(i)
|
||||
// Skip synthetic map-entry messages — proto generates them
|
||||
// implicitly for map<K,V> fields and they have no user-visible
|
||||
// counterpart in either the proto source or the Go types.
|
||||
if m.IsMapEntry() {
|
||||
continue
|
||||
}
|
||||
out[string(m.Name())] = m
|
||||
collectDeclared(m.Messages(), out)
|
||||
}
|
||||
}
|
||||
|
||||
func loadProto(t *testing.T, protoPath string, importPaths []string) protoreflect.FileDescriptor {
|
||||
t.Helper()
|
||||
paths := append([]string{"."}, importPaths...)
|
||||
c := protocompile.Compiler{
|
||||
Resolver: protocompile.WithStandardImports(&protocompile.SourceResolver{
|
||||
ImportPaths: paths,
|
||||
}),
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), loadTimeout)
|
||||
defer cancel()
|
||||
files, err := c.Compile(ctx, protoPath)
|
||||
if err != nil {
|
||||
t.Fatalf("compile %s: %v", protoPath, err)
|
||||
}
|
||||
if len(files) != 1 {
|
||||
t.Fatalf("compile %s: expected 1 file, got %d", protoPath, len(files))
|
||||
}
|
||||
return files[0]
|
||||
}
|
||||
|
||||
func checkMessage(t *testing.T, declared map[string]protoreflect.MessageDescriptor, m MessageSpec, coveredOneofs map[string]bool) {
|
||||
t.Helper()
|
||||
msg, ok := declared[m.Name]
|
||||
if !ok {
|
||||
t.Errorf("proto message %q not declared in any input file", m.Name)
|
||||
return
|
||||
}
|
||||
|
||||
override := m.FieldNameOverride
|
||||
skipEmbedded := map[reflect.Type]bool{}
|
||||
for _, et := range m.SkipEmbeddedTypes {
|
||||
skipEmbedded[et] = true
|
||||
}
|
||||
skipGo := map[string]bool{}
|
||||
for _, n := range m.SkipGoFields {
|
||||
skipGo[n] = true
|
||||
}
|
||||
|
||||
type expected struct {
|
||||
protoName string
|
||||
goType reflect.Type
|
||||
goName string
|
||||
}
|
||||
var want []expected
|
||||
visited := map[reflect.Type]bool{}
|
||||
var collect func(rt reflect.Type, qualifier string)
|
||||
collect = func(rt reflect.Type, qualifier string) {
|
||||
if rt.Kind() != reflect.Struct {
|
||||
return
|
||||
}
|
||||
if visited[rt] {
|
||||
return
|
||||
}
|
||||
visited[rt] = true
|
||||
for i := range rt.NumField() {
|
||||
f := rt.Field(i)
|
||||
if !f.IsExported() {
|
||||
continue
|
||||
}
|
||||
if skipGo[f.Name] {
|
||||
continue
|
||||
}
|
||||
if f.Anonymous {
|
||||
ft := f.Type
|
||||
for ft.Kind() == reflect.Pointer {
|
||||
ft = ft.Elem()
|
||||
}
|
||||
if ft.Kind() == reflect.Struct {
|
||||
if skipEmbedded[ft] {
|
||||
continue
|
||||
}
|
||||
collect(ft, qualifier)
|
||||
continue
|
||||
}
|
||||
}
|
||||
name, ok := jsonFieldName(f)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if remap, has := override[name]; has {
|
||||
name = remap
|
||||
}
|
||||
want = append(want, expected{
|
||||
protoName: name,
|
||||
goType: f.Type,
|
||||
goName: qualifier + "." + f.Name,
|
||||
})
|
||||
}
|
||||
}
|
||||
collect(m.GoType, m.GoType.Name())
|
||||
|
||||
// Validate SkipGoFields entries point at real Go fields.
|
||||
for _, n := range m.SkipGoFields {
|
||||
if !goFieldExists(m.GoType, n) {
|
||||
t.Errorf("%s: SkipGoFields entry %q does not match any Go field", m.Name, n)
|
||||
}
|
||||
}
|
||||
|
||||
opaque := map[string]bool{}
|
||||
for _, n := range m.OpaqueProtoFields {
|
||||
opaque[n] = true
|
||||
if msg.Fields().ByName(protoreflect.Name(n)) == nil {
|
||||
t.Errorf("%s: OpaqueProtoFields entry %q does not match any proto field on this message", m.Name, n)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate FieldNameOverride entries point at real Go fields.
|
||||
for goJSONName := range override {
|
||||
if !goJSONNameExists(m.GoType, goJSONName) {
|
||||
t.Errorf("%s: FieldNameOverride key %q does not match any Go field's JSON name", m.Name, goJSONName)
|
||||
}
|
||||
}
|
||||
|
||||
seen := map[string]bool{}
|
||||
for _, w := range want {
|
||||
seen[w.protoName] = true
|
||||
pf := msg.Fields().ByName(protoreflect.Name(w.protoName))
|
||||
if pf == nil {
|
||||
t.Errorf("%s: Go field %s maps to proto field %q but %s has no such field; add it with the next available field number", m.Name, w.goName, w.protoName, m.Name)
|
||||
continue
|
||||
}
|
||||
if opaque[w.protoName] {
|
||||
// Opaque means "no type check feasible" — but the Go side
|
||||
// must still be a structural type. A scalar Go field paired
|
||||
// with an opaque proto field is almost certainly a drift bug.
|
||||
switch unwrapPointer(w.goType).Kind() {
|
||||
case reflect.Bool,
|
||||
reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
|
||||
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
|
||||
reflect.Float32, reflect.Float64,
|
||||
reflect.String:
|
||||
t.Errorf("%s.%s (Go field %s): proto field is opaque but Go type %s is scalar; opaque is for unreflectable structural types only", m.Name, w.protoName, w.goName, w.goType)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if err := checkFieldType(w.goType, pf); err != nil {
|
||||
t.Errorf("%s.%s (Go field %s): %v", m.Name, w.protoName, w.goName, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Find proto fields that have no Go counterpart.
|
||||
for i := range msg.Fields().Len() {
|
||||
pf := msg.Fields().Get(i)
|
||||
name := string(pf.Name())
|
||||
if seen[name] {
|
||||
continue
|
||||
}
|
||||
if opaque[name] {
|
||||
continue
|
||||
}
|
||||
// Fields belonging to a oneof handled by an OneofSpec are
|
||||
// validated there, not here. Skip them so the orphan check
|
||||
// doesn't double-fire.
|
||||
if oo := pf.ContainingOneof(); oo != nil && coveredOneofs[string(oo.Name())] {
|
||||
continue
|
||||
}
|
||||
t.Errorf("%s: proto field %q (number %d) has no corresponding Go field; either remove it (and add `reserved %d`) or add a matching Go field", m.Name, name, pf.Number(), pf.Number())
|
||||
}
|
||||
}
|
||||
|
||||
func goJSONNameExists(rt reflect.Type, target string) bool {
|
||||
if rt.Kind() != reflect.Struct {
|
||||
return false
|
||||
}
|
||||
for i := range rt.NumField() {
|
||||
f := rt.Field(i)
|
||||
if !f.IsExported() {
|
||||
continue
|
||||
}
|
||||
if f.Anonymous {
|
||||
ft := f.Type
|
||||
for ft.Kind() == reflect.Pointer {
|
||||
ft = ft.Elem()
|
||||
}
|
||||
if ft.Kind() == reflect.Struct && goJSONNameExists(ft, target) {
|
||||
return true
|
||||
}
|
||||
continue
|
||||
}
|
||||
name, ok := jsonFieldName(f)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if name == target {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func goFieldExists(rt reflect.Type, target string) bool {
|
||||
if rt.Kind() != reflect.Struct {
|
||||
return false
|
||||
}
|
||||
for i := range rt.NumField() {
|
||||
f := rt.Field(i)
|
||||
if !f.IsExported() {
|
||||
continue
|
||||
}
|
||||
if f.Name == target {
|
||||
return true
|
||||
}
|
||||
if f.Anonymous {
|
||||
ft := f.Type
|
||||
for ft.Kind() == reflect.Pointer {
|
||||
ft = ft.Elem()
|
||||
}
|
||||
if ft.Kind() == reflect.Struct && goFieldExists(ft, target) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func checkOneof(t *testing.T, declared map[string]protoreflect.MessageDescriptor, o OneofSpec) {
|
||||
t.Helper()
|
||||
msg, ok := declared[o.MessageName]
|
||||
if !ok {
|
||||
t.Errorf("oneof check: proto message %q not declared", o.MessageName)
|
||||
return
|
||||
}
|
||||
oneof := msg.Oneofs().ByName(protoreflect.Name(o.OneofName))
|
||||
if oneof == nil {
|
||||
t.Errorf("oneof check: %s has no oneof named %q", o.MessageName, o.OneofName)
|
||||
return
|
||||
}
|
||||
|
||||
caseFields := map[string]protoreflect.FieldDescriptor{}
|
||||
for i := range oneof.Fields().Len() {
|
||||
f := oneof.Fields().Get(i)
|
||||
caseFields[string(f.Name())] = f
|
||||
}
|
||||
|
||||
discriminators := slices.Sorted(maps.Keys(o.DiscriminatorToCase))
|
||||
|
||||
expectedCases := map[string]bool{}
|
||||
for _, d := range discriminators {
|
||||
caseName := o.DiscriminatorToCase[d]
|
||||
expectedCases[caseName] = true
|
||||
f, ok := caseFields[caseName]
|
||||
if !ok {
|
||||
t.Errorf("%s.%s: discriminator %q maps to oneof case %q, but no such case exists", o.MessageName, o.OneofName, d, caseName)
|
||||
continue
|
||||
}
|
||||
goType := o.DiscriminatorToGoType[d]
|
||||
if goType == nil {
|
||||
t.Errorf("%s.%s.%s: discriminator %q has nil Go type — every discriminator must declare a concrete type so the proto kind can be checked", o.MessageName, o.OneofName, caseName, d)
|
||||
continue
|
||||
}
|
||||
gt := unwrapPointer(goType)
|
||||
gk := gt.Kind()
|
||||
if gk == reflect.Struct {
|
||||
if f.Kind() != protoreflect.MessageKind {
|
||||
t.Errorf("%s.%s.%s: discriminator %q expected message-typed oneof case but proto field is a scalar of kind %s", o.MessageName, o.OneofName, caseName, d, f.Kind())
|
||||
continue
|
||||
}
|
||||
want := gt.Name()
|
||||
got := string(f.Message().Name())
|
||||
if got != want {
|
||||
t.Errorf("%s.%s.%s: discriminator %q references Go type %s but proto case wraps message %s", o.MessageName, o.OneofName, caseName, d, want, got)
|
||||
}
|
||||
continue
|
||||
}
|
||||
// Scalar Go kind — validate proto kind compatibility.
|
||||
if err := checkScalarKind(gt, f); err != nil {
|
||||
t.Errorf("%s.%s.%s: discriminator %q: %v", o.MessageName, o.OneofName, caseName, d, err)
|
||||
}
|
||||
}
|
||||
|
||||
// Sort orphan-case names for deterministic error ordering.
|
||||
var orphanCases []string
|
||||
for caseName := range caseFields {
|
||||
if !expectedCases[caseName] {
|
||||
orphanCases = append(orphanCases, caseName)
|
||||
}
|
||||
}
|
||||
sort.Strings(orphanCases)
|
||||
for _, caseName := range orphanCases {
|
||||
f := caseFields[caseName]
|
||||
t.Errorf("%s.%s: proto case %q (number %d) has no corresponding discriminator in DiscriminatorToCase; either remove it (and `reserved %d` the number) or extend the spec", o.MessageName, o.OneofName, caseName, f.Number(), f.Number())
|
||||
}
|
||||
}
|
||||
|
||||
// jsonFieldName returns the JSON name of f and whether it should be included
|
||||
// (false for `json:"-"` fields).
|
||||
func jsonFieldName(f reflect.StructField) (string, bool) {
|
||||
tag := f.Tag.Get("json")
|
||||
if tag == "-" {
|
||||
return "", false
|
||||
}
|
||||
if tag == "" {
|
||||
return f.Name, true
|
||||
}
|
||||
name, _, _ := strings.Cut(tag, ",")
|
||||
if name == "" {
|
||||
name = f.Name
|
||||
}
|
||||
return name, true
|
||||
}
|
||||
|
||||
// checkFieldType verifies that the Go field type goType is compatible with
|
||||
// the proto field descriptor pf.
|
||||
func checkFieldType(goType reflect.Type, pf protoreflect.FieldDescriptor) error {
|
||||
goType = unwrapPointer(goType)
|
||||
|
||||
if pf.IsList() {
|
||||
if goType.Kind() != reflect.Slice && goType.Kind() != reflect.Array {
|
||||
return fmt.Errorf("proto field is repeated but Go type is %s", goType.Kind())
|
||||
}
|
||||
elem := unwrapPointer(goType.Elem())
|
||||
return checkScalarOrMessage(elem, pf)
|
||||
}
|
||||
if pf.IsMap() {
|
||||
if goType.Kind() != reflect.Map {
|
||||
return fmt.Errorf("proto field is a map but Go type is %s", goType.Kind())
|
||||
}
|
||||
if goType.Key().Kind() != reflect.String {
|
||||
return fmt.Errorf("proto field is a map but Go map key is %s, want string", goType.Key().Kind())
|
||||
}
|
||||
valField := pf.MapValue()
|
||||
valType := unwrapPointer(goType.Elem())
|
||||
return checkScalarOrMessage(valType, valField)
|
||||
}
|
||||
return checkScalarOrMessage(goType, pf)
|
||||
}
|
||||
|
||||
func checkScalarOrMessage(goType reflect.Type, pf protoreflect.FieldDescriptor) error {
|
||||
goType = unwrapPointer(goType)
|
||||
if pf.Kind() == protoreflect.MessageKind || pf.Kind() == protoreflect.GroupKind {
|
||||
// Accept any Go type for message-typed proto fields. The outer
|
||||
// message check verifies field-by-field shape; the
|
||||
// Stmt/Val/Operand/Block plumbing relies on this looseness.
|
||||
return nil
|
||||
}
|
||||
return checkScalarKind(goType, pf)
|
||||
}
|
||||
|
||||
// checkScalarKind verifies a Go type is compatible with a scalar proto kind.
|
||||
// For integer kinds we enforce that the Go bit-width fits in the proto
|
||||
// bit-width (so Go int64 cannot be silently mapped to proto int32). The
|
||||
// platform-dependent `int`/`uint` kinds are treated as 32-bit minimum and
|
||||
// accepted against either width — practical for OPA's bounded indices.
|
||||
func checkScalarKind(goType reflect.Type, pf protoreflect.FieldDescriptor) error {
|
||||
switch pf.Kind() {
|
||||
case protoreflect.BoolKind:
|
||||
if goType.Kind() != reflect.Bool {
|
||||
return fmt.Errorf("proto Kind=bool but Go type is %s", goType.Kind())
|
||||
}
|
||||
case protoreflect.StringKind:
|
||||
if goType.Kind() != reflect.String {
|
||||
return fmt.Errorf("proto Kind=string but Go type is %s", goType.Kind())
|
||||
}
|
||||
case protoreflect.BytesKind:
|
||||
if !(goType.Kind() == reflect.Slice && goType.Elem().Kind() == reflect.Uint8) {
|
||||
return fmt.Errorf("proto Kind=bytes but Go type is %s", goType.Kind())
|
||||
}
|
||||
case protoreflect.Int32Kind, protoreflect.Sint32Kind, protoreflect.Sfixed32Kind,
|
||||
protoreflect.Int64Kind, protoreflect.Sint64Kind, protoreflect.Sfixed64Kind:
|
||||
if !signedIntFits(goType.Kind(), pf.Kind()) {
|
||||
return fmt.Errorf("proto Kind=%s but Go type is %s (Go int64 cannot be safely narrowed)", pf.Kind(), goType.Kind())
|
||||
}
|
||||
case protoreflect.Uint32Kind, protoreflect.Fixed32Kind,
|
||||
protoreflect.Uint64Kind, protoreflect.Fixed64Kind:
|
||||
if !unsignedIntFits(goType.Kind(), pf.Kind()) {
|
||||
return fmt.Errorf("proto Kind=%s but Go type is %s (Go uint64 cannot be safely narrowed)", pf.Kind(), goType.Kind())
|
||||
}
|
||||
case protoreflect.FloatKind, protoreflect.DoubleKind:
|
||||
if goType.Kind() != reflect.Float32 && goType.Kind() != reflect.Float64 {
|
||||
return fmt.Errorf("proto Kind=%s but Go type is %s", pf.Kind(), goType.Kind())
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("unsupported proto Kind=%s", pf.Kind())
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// signedIntFits reports whether a Go signed integer kind fits in a proto
|
||||
// signed integer kind. Go's platform-dependent `int` is treated as
|
||||
// at-most 32-bit (lenient — accepted against either int32 or int64 proto
|
||||
// kinds). Fixed-width kinds (int8/int16/int32/int64) are checked strictly.
|
||||
func signedIntFits(goKind reflect.Kind, pfKind protoreflect.Kind) bool {
|
||||
proto64 := pfKind == protoreflect.Int64Kind || pfKind == protoreflect.Sint64Kind || pfKind == protoreflect.Sfixed64Kind
|
||||
switch goKind {
|
||||
case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int:
|
||||
return true // fits any signed proto int kind
|
||||
case reflect.Int64:
|
||||
return proto64
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// unsignedIntFits is the unsigned counterpart of signedIntFits.
|
||||
func unsignedIntFits(goKind reflect.Kind, pfKind protoreflect.Kind) bool {
|
||||
proto64 := pfKind == protoreflect.Uint64Kind || pfKind == protoreflect.Fixed64Kind
|
||||
switch goKind {
|
||||
case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint:
|
||||
return true
|
||||
case reflect.Uint64:
|
||||
return proto64
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func unwrapPointer(t reflect.Type) reflect.Type {
|
||||
for t.Kind() == reflect.Pointer {
|
||||
t = t.Elem()
|
||||
}
|
||||
return t
|
||||
}
|
||||
@@ -234,6 +234,10 @@ func (a *Annotations) MarshalJSON() ([]byte, error) {
|
||||
data["schemas"] = a.Schemas
|
||||
}
|
||||
|
||||
if a.Compile != nil {
|
||||
data["compile"] = a.Compile
|
||||
}
|
||||
|
||||
if len(a.Custom) > 0 {
|
||||
data["custom"] = a.Custom
|
||||
}
|
||||
|
||||
@@ -965,6 +965,50 @@ func TestAnnotations_MarshalJSON(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnnotations_MarshalJSON_Compile(t *testing.T) {
|
||||
// Regression: Annotations.MarshalJSON used to silently drop the
|
||||
// `Compile` field even though the struct tag is `compile,omitempty`.
|
||||
// Default-reflection unmarshal still reads `compile`, so the round-trip
|
||||
// was asymmetric until this was fixed.
|
||||
a := &Annotations{
|
||||
Scope: "rule",
|
||||
Compile: &CompileAnnotation{
|
||||
Unknowns: []Ref{MustParseRef("input.x"), MustParseRef("input.y")},
|
||||
MaskRule: MustParseRef("data.policy.mask"),
|
||||
},
|
||||
}
|
||||
|
||||
bs, err := json.Marshal(a)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal: %v", err)
|
||||
}
|
||||
|
||||
var got map[string]any
|
||||
if err := json.Unmarshal(bs, &got); err != nil {
|
||||
t.Fatalf("unmarshal: %v", err)
|
||||
}
|
||||
compile, ok := got["compile"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("expected `compile` key in marshaled output, got: %s", bs)
|
||||
}
|
||||
if _, ok := compile["unknowns"].([]any); !ok {
|
||||
t.Errorf("expected compile.unknowns to be a JSON array, got: %v", compile["unknowns"])
|
||||
}
|
||||
if _, ok := compile["mask_rule"].([]any); !ok {
|
||||
t.Errorf("expected compile.mask_rule to be a JSON array (Ref), got: %v", compile["mask_rule"])
|
||||
}
|
||||
|
||||
// nil Compile should not emit the key (`omitempty` semantics).
|
||||
a.Compile = nil
|
||||
bs, err = json.Marshal(a)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal nil compile: %v", err)
|
||||
}
|
||||
if strings.Contains(string(bs), "compile") {
|
||||
t.Errorf("expected nil Compile to be omitted, got: %s", bs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnnotationsRef_MarshalJSON(t *testing.T) {
|
||||
|
||||
testCases := map[string]struct {
|
||||
|
||||
@@ -130,6 +130,9 @@ func NewFile(name, hash, alg string) FileInfo {
|
||||
|
||||
// Manifest represents the manifest from a bundle. The manifest may contain
|
||||
// metadata such as the bundle revision.
|
||||
//
|
||||
// Schema mirror: manifest.proto + manifest_proto_test.go. Update both when
|
||||
// adding/renaming/removing fields; never reuse a proto field number.
|
||||
type Manifest struct {
|
||||
Revision string `json:"revision"`
|
||||
Roots *[]string `json:"roots,omitempty"`
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
// 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.
|
||||
|
||||
edition = "2023";
|
||||
|
||||
package opa.bundle.v1;
|
||||
|
||||
import "google/protobuf/struct.proto";
|
||||
|
||||
option go_package = "github.com/open-policy-agent/opa/v1/bundle/v1pb";
|
||||
option java_multiple_files = true;
|
||||
|
||||
// Manifest mirrors `bundle.Manifest` in v1/bundle/bundle.go.
|
||||
message Manifest {
|
||||
// Bundle revision string.
|
||||
string revision = 1;
|
||||
|
||||
// Root paths the bundle owns.
|
||||
repeated string roots = 2;
|
||||
|
||||
// Wasm resolvers attached to the bundle. JSON key is `wasm`.
|
||||
repeated WasmResolver wasm = 3;
|
||||
|
||||
// Global Rego version for the bundle. Currently 0 (RegoV0) or 1 (RegoV1).
|
||||
int32 rego_version = 4;
|
||||
|
||||
// Per-file Rego version overrides keyed by file path.
|
||||
map<string, int32> file_rego_versions = 5;
|
||||
|
||||
// Free-form metadata object. Modeled as `Struct` because the Go field
|
||||
// is `map[string]any`.
|
||||
google.protobuf.Struct metadata = 6;
|
||||
}
|
||||
|
||||
// WasmResolver mirrors `bundle.WasmResolver` in v1/bundle/bundle.go.
|
||||
message WasmResolver {
|
||||
// Entrypoint policy ref this resolver targets.
|
||||
string entrypoint = 1;
|
||||
|
||||
// Path to the wasm module within the bundle.
|
||||
string module = 2;
|
||||
|
||||
// Rego annotations attached to the entrypoint.
|
||||
repeated Annotations annotations = 3;
|
||||
}
|
||||
|
||||
// Annotations mirrors `ast.Annotations` in v1/ast/annotations.go.
|
||||
message Annotations {
|
||||
string scope = 1;
|
||||
string title = 2;
|
||||
bool entrypoint = 3;
|
||||
string description = 4;
|
||||
repeated string organizations = 5;
|
||||
repeated RelatedResourceAnnotation related_resources = 6;
|
||||
repeated AuthorAnnotation authors = 7;
|
||||
repeated SchemaAnnotation schemas = 8;
|
||||
CompileAnnotation compile = 9;
|
||||
|
||||
// `custom` and `labels` are `map[string]any` in Go — genuinely
|
||||
// free-form, so `Struct` is the right model.
|
||||
google.protobuf.Struct custom = 10;
|
||||
google.protobuf.Struct labels = 11;
|
||||
|
||||
Location location = 12;
|
||||
}
|
||||
|
||||
// SchemaAnnotation mirrors `ast.SchemaAnnotation`. Path/Schema are
|
||||
// `ast.Ref` in Go (a list of terms); the wire form is the canonical
|
||||
// dotted ref string (e.g. `data.foo.bar`). Modeling the term tree
|
||||
// faithfully would pull most of the AST into this schema and isn't
|
||||
// worth the cost for annotations.
|
||||
message SchemaAnnotation {
|
||||
string path = 1;
|
||||
string schema = 2;
|
||||
// `*any` on the Go side — a parsed JSON Schema document or any
|
||||
// JSON value. `Value` (not `Struct`) because the top level may be
|
||||
// a scalar, list, or null, not just an object.
|
||||
google.protobuf.Value definition = 3;
|
||||
}
|
||||
|
||||
// CompileAnnotation mirrors `ast.CompileAnnotation`. Refs are the
|
||||
// canonical dotted form; see SchemaAnnotation for the trade-off.
|
||||
message CompileAnnotation {
|
||||
repeated string unknowns = 1;
|
||||
string mask_rule = 2;
|
||||
}
|
||||
|
||||
// AuthorAnnotation mirrors `ast.AuthorAnnotation`.
|
||||
message AuthorAnnotation {
|
||||
string name = 1;
|
||||
string email = 2;
|
||||
}
|
||||
|
||||
// RelatedResourceAnnotation mirrors `ast.RelatedResourceAnnotation`.
|
||||
// `Ref` is a `url.URL` in Go, serialized to its `String()` form.
|
||||
message RelatedResourceAnnotation {
|
||||
string ref = 1;
|
||||
string description = 2;
|
||||
}
|
||||
|
||||
// Location mirrors `ast.Location` (= `location.Location`). Only the
|
||||
// File/Row/Col triple is wire-relevant; `Text`, `Offset`, and `Tabs`
|
||||
// are tagged `json:"-"` and intentionally absent.
|
||||
//
|
||||
// Distinct from `ir.Location`, which is promoted onto the `Stmt`
|
||||
// envelope in plan.proto. The two are independent Go types.
|
||||
message Location {
|
||||
string file = 1;
|
||||
int32 row = 2;
|
||||
int32 col = 3;
|
||||
}
|
||||
@@ -6,6 +6,10 @@
|
||||
//
|
||||
// The IR specifies an imperative execution model for Rego policies similar to a
|
||||
// query plan in traditional databases.
|
||||
//
|
||||
// Schema mirror: plan.proto + plan_proto_test.go. Update both when
|
||||
// adding/renaming/removing fields or Stmt/Val kinds; never reuse a proto
|
||||
// field number or oneof case.
|
||||
package ir
|
||||
|
||||
import (
|
||||
|
||||
@@ -0,0 +1,364 @@
|
||||
// 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.
|
||||
|
||||
edition = "2023";
|
||||
|
||||
package opa.ir.v1;
|
||||
|
||||
option go_package = "github.com/open-policy-agent/opa/v1/ir/v1pb";
|
||||
option java_multiple_files = true;
|
||||
|
||||
// Policy mirrors `ir.Policy` in v1/ir/ir.go.
|
||||
message Policy {
|
||||
Static static = 1;
|
||||
Plans plans = 2;
|
||||
Funcs funcs = 3;
|
||||
}
|
||||
|
||||
// Static mirrors `ir.Static` in v1/ir/ir.go.
|
||||
message Static {
|
||||
repeated StringConst strings = 1;
|
||||
repeated BuiltinFunc builtin_funcs = 2;
|
||||
repeated StringConst files = 3;
|
||||
}
|
||||
|
||||
// BuiltinFunc mirrors `ir.BuiltinFunc` in v1/ir/ir.go.
|
||||
//
|
||||
// `ir.BuiltinFunc.Decl` (the function's `types.Function` signature) is
|
||||
// intentionally not modeled. Consumers that execute plans need their
|
||||
// own builtin registry to dispatch host-language implementations, and
|
||||
// that registry is the source of truth for signatures — bundling
|
||||
// `Decl` here would be redundant and prone to drift.
|
||||
message BuiltinFunc {
|
||||
string name = 1;
|
||||
}
|
||||
|
||||
// Plans mirrors `ir.Plans` in v1/ir/ir.go.
|
||||
message Plans {
|
||||
repeated Plan plans = 1;
|
||||
}
|
||||
|
||||
// Funcs mirrors `ir.Funcs` in v1/ir/ir.go.
|
||||
message Funcs {
|
||||
repeated Func funcs = 1;
|
||||
}
|
||||
|
||||
// Func mirrors `ir.Func` in v1/ir/ir.go.
|
||||
//
|
||||
// Each parameter and the return slot are local-variable indices; see
|
||||
// `ir.Local` in v1/ir/ir.go.
|
||||
message Func {
|
||||
string name = 1;
|
||||
repeated int32 params = 2;
|
||||
// The local that holds the function's return value. Renamed from
|
||||
// `return` (the Go field is `Func.Return Local`, JSON-tagged `return`)
|
||||
// to avoid colliding with a reserved keyword in many target languages
|
||||
// when this proto is fed to protoc plugins. The JSON wire form
|
||||
// continues to use `return`; only the proto-side identifier differs.
|
||||
int32 result = 3;
|
||||
repeated Block blocks = 4;
|
||||
repeated string path = 5;
|
||||
}
|
||||
|
||||
// Plan mirrors `ir.Plan` in v1/ir/ir.go.
|
||||
message Plan {
|
||||
string name = 1;
|
||||
repeated Block blocks = 2;
|
||||
}
|
||||
|
||||
// Block mirrors `ir.Block` in v1/ir/ir.go.
|
||||
message Block {
|
||||
repeated Stmt stmts = 1;
|
||||
}
|
||||
|
||||
// StringConst mirrors `ir.StringConst` in v1/ir/ir.go.
|
||||
message StringConst {
|
||||
string value = 1;
|
||||
}
|
||||
|
||||
// Operand mirrors `ir.Operand` in v1/ir/ir.go. The `value` field is a
|
||||
// polymorphic `Val` union; see the `Val` message below.
|
||||
message Operand {
|
||||
Val value = 1;
|
||||
}
|
||||
|
||||
// Val mirrors the `ir.Val` interface in v1/ir/ir.go. Each oneof case
|
||||
// corresponds to a concrete `Val` implementation; the case names match
|
||||
// the JSON discriminator strings emitted by `*Operand.MarshalJSON`.
|
||||
//
|
||||
// Case-number assignments are a stability commitment. New cases must
|
||||
// be added with the next unused number; existing numbers must never
|
||||
// be repurposed.
|
||||
message Val {
|
||||
oneof kind {
|
||||
bool bool = 1;
|
||||
int32 local = 2;
|
||||
int32 string_index = 3;
|
||||
}
|
||||
}
|
||||
|
||||
// Stmt mirrors the `ir.Stmt` interface in v1/ir/ir.go. Every Stmt carries
|
||||
// the source-location triple (file, col, row) on this envelope; the body
|
||||
// messages below describe only the kind-specific payload.
|
||||
//
|
||||
// On the Go side, `ir.Location` is embedded into every concrete Stmt
|
||||
// implementation, so `encoding/json` flattens File/Col/Row into the
|
||||
// emitted JSON body. The proto promotes those fields to the envelope
|
||||
// because that's both more idiomatic protobuf and lets every body
|
||||
// message start its own field numbering at 1.
|
||||
//
|
||||
// Case-number assignments (4–37) are a stability commitment. Field
|
||||
// numbers 1–3 are reserved for the location triple. New cases must be
|
||||
// added with the next unused number; existing numbers must never be
|
||||
// repurposed.
|
||||
message Stmt {
|
||||
int32 file = 1;
|
||||
int32 col = 2;
|
||||
int32 row = 3;
|
||||
oneof kind {
|
||||
ArrayAppendStmt array_append_stmt = 4;
|
||||
AssignIntStmt assign_int_stmt = 5;
|
||||
AssignVarOnceStmt assign_var_once_stmt = 6;
|
||||
AssignVarStmt assign_var_stmt = 7;
|
||||
BlockStmt block_stmt = 8;
|
||||
BreakStmt break_stmt = 9;
|
||||
CallDynamicStmt call_dynamic_stmt = 10;
|
||||
CallStmt call_stmt = 11;
|
||||
DotStmt dot_stmt = 12;
|
||||
EqualStmt equal_stmt = 13;
|
||||
IsArrayStmt is_array_stmt = 14;
|
||||
IsDefinedStmt is_defined_stmt = 15;
|
||||
IsObjectStmt is_object_stmt = 16;
|
||||
IsSetStmt is_set_stmt = 17;
|
||||
IsUndefinedStmt is_undefined_stmt = 18;
|
||||
LenStmt len_stmt = 19;
|
||||
MakeArrayStmt make_array_stmt = 20;
|
||||
MakeNullStmt make_null_stmt = 21;
|
||||
MakeNumberIntStmt make_number_int_stmt = 22;
|
||||
MakeNumberRefStmt make_number_ref_stmt = 23;
|
||||
MakeObjectStmt make_object_stmt = 24;
|
||||
MakeSetStmt make_set_stmt = 25;
|
||||
NopStmt nop_stmt = 26;
|
||||
NotEqualStmt not_equal_stmt = 27;
|
||||
NotStmt not_stmt = 28;
|
||||
ObjectInsertOnceStmt object_insert_once_stmt = 29;
|
||||
ObjectInsertStmt object_insert_stmt = 30;
|
||||
ObjectMergeStmt object_merge_stmt = 31;
|
||||
ResetLocalStmt reset_local_stmt = 32;
|
||||
ResultSetAddStmt result_set_add_stmt = 33;
|
||||
ReturnLocalStmt return_local_stmt = 34;
|
||||
ScanStmt scan_stmt = 35;
|
||||
SetAddStmt set_add_stmt = 36;
|
||||
WithStmt with_stmt = 37;
|
||||
}
|
||||
}
|
||||
|
||||
// Stmt body messages start their own field numbering at 1; the
|
||||
// source-location triple lives on the parent `Stmt` envelope.
|
||||
|
||||
// ArrayAppendStmt mirrors `ir.ArrayAppendStmt` in v1/ir/ir.go.
|
||||
message ArrayAppendStmt {
|
||||
Operand value = 1;
|
||||
int32 array = 2;
|
||||
}
|
||||
|
||||
// AssignIntStmt mirrors `ir.AssignIntStmt` in v1/ir/ir.go.
|
||||
message AssignIntStmt {
|
||||
int64 value = 1;
|
||||
int32 target = 2;
|
||||
}
|
||||
|
||||
// AssignVarOnceStmt mirrors `ir.AssignVarOnceStmt` in v1/ir/ir.go.
|
||||
message AssignVarOnceStmt {
|
||||
Operand source = 1;
|
||||
int32 target = 2;
|
||||
}
|
||||
|
||||
// AssignVarStmt mirrors `ir.AssignVarStmt` in v1/ir/ir.go.
|
||||
message AssignVarStmt {
|
||||
Operand source = 1;
|
||||
int32 target = 2;
|
||||
}
|
||||
|
||||
// BlockStmt mirrors `ir.BlockStmt` in v1/ir/ir.go.
|
||||
message BlockStmt {
|
||||
repeated Block blocks = 1;
|
||||
}
|
||||
|
||||
// BreakStmt mirrors `ir.BreakStmt` in v1/ir/ir.go.
|
||||
message BreakStmt {
|
||||
uint32 index = 1;
|
||||
}
|
||||
|
||||
// CallDynamicStmt mirrors `ir.CallDynamicStmt` in v1/ir/ir.go.
|
||||
message CallDynamicStmt {
|
||||
repeated int32 args = 1;
|
||||
int32 result = 2;
|
||||
repeated Operand path = 3;
|
||||
}
|
||||
|
||||
// CallStmt mirrors `ir.CallStmt` in v1/ir/ir.go.
|
||||
message CallStmt {
|
||||
// Renamed from `func` (Go field `CallStmt.Func`, JSON-tagged `func`)
|
||||
// to avoid colliding with reserved keywords in target languages,
|
||||
// mirroring the Func.return → result rename above.
|
||||
string function = 1;
|
||||
repeated Operand args = 2;
|
||||
int32 result = 3;
|
||||
}
|
||||
|
||||
// DotStmt mirrors `ir.DotStmt` in v1/ir/ir.go.
|
||||
message DotStmt {
|
||||
Operand source = 1;
|
||||
Operand key = 2;
|
||||
int32 target = 3;
|
||||
}
|
||||
|
||||
// EqualStmt mirrors `ir.EqualStmt` in v1/ir/ir.go.
|
||||
message EqualStmt {
|
||||
Operand a = 1;
|
||||
Operand b = 2;
|
||||
}
|
||||
|
||||
// IsArrayStmt mirrors `ir.IsArrayStmt` in v1/ir/ir.go.
|
||||
message IsArrayStmt {
|
||||
Operand source = 1;
|
||||
}
|
||||
|
||||
// IsDefinedStmt mirrors `ir.IsDefinedStmt` in v1/ir/ir.go.
|
||||
message IsDefinedStmt {
|
||||
int32 source = 1;
|
||||
}
|
||||
|
||||
// IsObjectStmt mirrors `ir.IsObjectStmt` in v1/ir/ir.go.
|
||||
message IsObjectStmt {
|
||||
Operand source = 1;
|
||||
}
|
||||
|
||||
// IsSetStmt mirrors `ir.IsSetStmt` in v1/ir/ir.go.
|
||||
message IsSetStmt {
|
||||
Operand source = 1;
|
||||
}
|
||||
|
||||
// IsUndefinedStmt mirrors `ir.IsUndefinedStmt` in v1/ir/ir.go.
|
||||
message IsUndefinedStmt {
|
||||
int32 source = 1;
|
||||
}
|
||||
|
||||
// LenStmt mirrors `ir.LenStmt` in v1/ir/ir.go.
|
||||
message LenStmt {
|
||||
Operand source = 1;
|
||||
int32 target = 2;
|
||||
}
|
||||
|
||||
// MakeArrayStmt mirrors `ir.MakeArrayStmt` in v1/ir/ir.go.
|
||||
message MakeArrayStmt {
|
||||
int32 capacity = 1;
|
||||
int32 target = 2;
|
||||
}
|
||||
|
||||
// MakeNullStmt mirrors `ir.MakeNullStmt` in v1/ir/ir.go.
|
||||
message MakeNullStmt {
|
||||
int32 target = 1;
|
||||
}
|
||||
|
||||
// MakeNumberIntStmt mirrors `ir.MakeNumberIntStmt` in v1/ir/ir.go.
|
||||
message MakeNumberIntStmt {
|
||||
int64 value = 1;
|
||||
int32 target = 2;
|
||||
}
|
||||
|
||||
// MakeNumberRefStmt mirrors `ir.MakeNumberRefStmt` in v1/ir/ir.go.
|
||||
//
|
||||
// The Go field is named `Index` (no `json` tag). The historical JSON
|
||||
// shape emitted both `index` and `Index`; the canonical key going
|
||||
// forward is `index` and the deprecated `Index` alias will be removed
|
||||
// in a future major release. This proto models only the canonical
|
||||
// `index` field.
|
||||
message MakeNumberRefStmt {
|
||||
int32 index = 1;
|
||||
int32 target = 2;
|
||||
}
|
||||
|
||||
// MakeObjectStmt mirrors `ir.MakeObjectStmt` in v1/ir/ir.go.
|
||||
message MakeObjectStmt {
|
||||
int32 target = 1;
|
||||
}
|
||||
|
||||
// MakeSetStmt mirrors `ir.MakeSetStmt` in v1/ir/ir.go.
|
||||
message MakeSetStmt {
|
||||
int32 target = 1;
|
||||
}
|
||||
|
||||
// NopStmt mirrors `ir.NopStmt` in v1/ir/ir.go.
|
||||
message NopStmt {}
|
||||
|
||||
// NotEqualStmt mirrors `ir.NotEqualStmt` in v1/ir/ir.go.
|
||||
message NotEqualStmt {
|
||||
Operand a = 1;
|
||||
Operand b = 2;
|
||||
}
|
||||
|
||||
// NotStmt mirrors `ir.NotStmt` in v1/ir/ir.go.
|
||||
message NotStmt {
|
||||
Block block = 1;
|
||||
}
|
||||
|
||||
// ObjectInsertOnceStmt mirrors `ir.ObjectInsertOnceStmt` in v1/ir/ir.go.
|
||||
message ObjectInsertOnceStmt {
|
||||
Operand key = 1;
|
||||
Operand value = 2;
|
||||
int32 object = 3;
|
||||
}
|
||||
|
||||
// ObjectInsertStmt mirrors `ir.ObjectInsertStmt` in v1/ir/ir.go.
|
||||
message ObjectInsertStmt {
|
||||
Operand key = 1;
|
||||
Operand value = 2;
|
||||
int32 object = 3;
|
||||
}
|
||||
|
||||
// ObjectMergeStmt mirrors `ir.ObjectMergeStmt` in v1/ir/ir.go.
|
||||
message ObjectMergeStmt {
|
||||
int32 a = 1;
|
||||
int32 b = 2;
|
||||
int32 target = 3;
|
||||
}
|
||||
|
||||
// ResetLocalStmt mirrors `ir.ResetLocalStmt` in v1/ir/ir.go.
|
||||
message ResetLocalStmt {
|
||||
int32 target = 1;
|
||||
}
|
||||
|
||||
// ResultSetAddStmt mirrors `ir.ResultSetAddStmt` in v1/ir/ir.go.
|
||||
message ResultSetAddStmt {
|
||||
int32 value = 1;
|
||||
}
|
||||
|
||||
// ReturnLocalStmt mirrors `ir.ReturnLocalStmt` in v1/ir/ir.go.
|
||||
message ReturnLocalStmt {
|
||||
int32 source = 1;
|
||||
}
|
||||
|
||||
// ScanStmt mirrors `ir.ScanStmt` in v1/ir/ir.go.
|
||||
message ScanStmt {
|
||||
int32 source = 1;
|
||||
int32 key = 2;
|
||||
int32 value = 3;
|
||||
Block block = 4;
|
||||
}
|
||||
|
||||
// SetAddStmt mirrors `ir.SetAddStmt` in v1/ir/ir.go.
|
||||
message SetAddStmt {
|
||||
Operand value = 1;
|
||||
int32 set = 2;
|
||||
}
|
||||
|
||||
// WithStmt mirrors `ir.WithStmt` in v1/ir/ir.go.
|
||||
message WithStmt {
|
||||
int32 local = 1;
|
||||
repeated int32 path = 2;
|
||||
Operand value = 3;
|
||||
Block block = 4;
|
||||
}
|
||||
Reference in New Issue
Block a user