From 0e335b425cf253d61d3c57677c9c874f732d2a84 Mon Sep 17 00:00:00 2001 From: John Chadwick Date: Tue, 15 Oct 2024 17:30:47 -0400 Subject: [PATCH 01/24] Refactor validation errors --- .../cmd/protovalidate-conformance-go/main.go | 17 ++++- internal/constraints/cache.go | 1 + internal/errors/utils.go | 23 +++++-- internal/errors/utils_test.go | 28 ++++---- internal/errors/validation.go | 65 ++++++++++++++----- internal/errors/validation_test.go | 10 ++- internal/evaluator/any.go | 16 ++--- internal/evaluator/cel.go | 4 +- internal/evaluator/enum.go | 8 +-- internal/evaluator/field.go | 10 ++- internal/evaluator/oneof.go | 10 ++- internal/expression/ast.go | 19 ++++-- internal/expression/program.go | 33 +++++----- internal/expression/program_test.go | 33 ++++++---- validator.go | 11 ++-- validator_example_test.go | 3 +- validator_test.go | 2 +- 17 files changed, 178 insertions(+), 115 deletions(-) diff --git a/internal/cmd/protovalidate-conformance-go/main.go b/internal/cmd/protovalidate-conformance-go/main.go index 9126170..5eb159c 100644 --- a/internal/cmd/protovalidate-conformance-go/main.go +++ b/internal/cmd/protovalidate-conformance-go/main.go @@ -21,7 +21,9 @@ import ( "os" "strings" + "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" "github.com/bufbuild/protovalidate-go" + "github.com/bufbuild/protovalidate-go/internal/errors" "github.com/bufbuild/protovalidate-go/internal/gen/buf/validate/conformance/harness" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/reflect/protodesc" @@ -113,7 +115,7 @@ func TestCase(val *protovalidate.Validator, files *protoregistry.Files, testCase case *protovalidate.ValidationError: return &harness.TestResult{ Result: &harness.TestResult_ValidationError{ - ValidationError: res.ToProto(), + ValidationError: violationsToProto(res.Violations), }, } case *protovalidate.RuntimeError: @@ -133,6 +135,19 @@ func TestCase(val *protovalidate.Validator, files *protoregistry.Files, testCase } } +func violationsToProto(violations []errors.Violation) *validate.Violations { + result := make([]*validate.Violation, len(violations)) + for i := range violations { + result[i] = &validate.Violation{ + FieldPath: &violations[i].FieldPath, + ConstraintId: &violations[i].ConstraintID, + Message: &violations[i].Message, + ForKey: &violations[i].ForKey, + } + } + return &validate.Violations{Violations: result} +} + func unexpectedErrorResult(format string, args ...any) *harness.TestResult { return &harness.TestResult{ Result: &harness.TestResult_UnexpectedError{ diff --git a/internal/constraints/cache.go b/internal/constraints/cache.go index 1c1768a..c9e3453 100644 --- a/internal/constraints/cache.go +++ b/internal/constraints/cache.go @@ -88,6 +88,7 @@ func (c *Cache) Build( err = compileErr return false } + precomputedASTs.SetRuleValue(rule) asts = asts.Merge(precomputedASTs) return true }) diff --git a/internal/errors/utils.go b/internal/errors/utils.go index 35aa5c8..7b472bb 100644 --- a/internal/errors/utils.go +++ b/internal/errors/utils.go @@ -16,13 +16,12 @@ package errors import ( "errors" - - "google.golang.org/protobuf/proto" + "slices" ) -// Merge is a utility to resolve and combine errors resulting from +// Merge is a utility to resolve and combine Errors resulting from // evaluation. If ok is false, execution of validation should stop (either due -// to failFast or the result is not a ValidationError). +// to failFast or the result is not a ValidationErrors). // //nolint:errorlint func Merge(dst, src error, failFast bool) (ok bool, err error) { @@ -50,7 +49,7 @@ func Merge(dst, src error, failFast bool) (ok bool, err error) { } // PrefixErrorPaths prepends the formatted prefix to the violations of a -// ValidationError. +// ValidationErrors. func PrefixErrorPaths(err error, format string, args ...any) { var valErr *ValidationError if errors.As(err, &valErr) { @@ -61,8 +60,18 @@ func PrefixErrorPaths(err error, format string, args ...any) { func MarkForKey(err error) { var valErr *ValidationError if errors.As(err, &valErr) { - for _, violation := range valErr.Violations { - violation.ForKey = proto.Bool(true) + for i := range valErr.Violations { + valErr.Violations[i].ForKey = true } } } + +// EqualViolations returns true if the underlying violations are equal. +func EqualViolations(a, b []Violation) bool { + return slices.EqualFunc(a, b, EqualViolation) +} + +// EqualViolation returns true if the underlying violations are equal. +func EqualViolation(a, b Violation) bool { + return a.FieldPath == b.FieldPath && a.ConstraintID == b.ConstraintID && a.Message == b.Message && a.ForKey == b.ForKey +} diff --git a/internal/errors/utils_test.go b/internal/errors/utils_test.go index 122f3b9..56b3760 100644 --- a/internal/errors/utils_test.go +++ b/internal/errors/utils_test.go @@ -18,10 +18,8 @@ import ( "errors" "testing" - "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "google.golang.org/protobuf/proto" ) func TestMerge(t *testing.T) { @@ -53,15 +51,15 @@ func TestMerge(t *testing.T) { t.Run("validation", func(t *testing.T) { t.Parallel() - exErr := &ValidationError{Violations: []*validate.Violation{{ConstraintId: proto.String("foo")}}} + exErr := &ValidationError{Violations: []Violation{{ConstraintID: "foo"}}} ok, err := Merge(nil, exErr, true) var valErr *ValidationError require.ErrorAs(t, err, &valErr) - assert.True(t, proto.Equal(exErr.ToProto(), valErr.ToProto())) + assert.True(t, EqualViolations(exErr.Violations, valErr.Violations)) assert.False(t, ok) ok, err = Merge(nil, exErr, false) require.ErrorAs(t, err, &valErr) - assert.True(t, proto.Equal(exErr.ToProto(), valErr.ToProto())) + assert.True(t, EqualViolations(exErr.Violations, valErr.Violations)) assert.True(t, ok) }) }) @@ -72,7 +70,7 @@ func TestMerge(t *testing.T) { t.Run("non-validation dst", func(t *testing.T) { t.Parallel() dstErr := errors.New("some error") - srcErr := &ValidationError{Violations: []*validate.Violation{{ConstraintId: proto.String("foo")}}} + srcErr := &ValidationError{Violations: []Violation{{ConstraintID: "foo"}}} ok, err := Merge(dstErr, srcErr, true) assert.Equal(t, dstErr, err) assert.False(t, ok) @@ -83,7 +81,7 @@ func TestMerge(t *testing.T) { t.Run("non-validation src", func(t *testing.T) { t.Parallel() - dstErr := &ValidationError{Violations: []*validate.Violation{{ConstraintId: proto.String("foo")}}} + dstErr := &ValidationError{Violations: []Violation{{ConstraintID: "foo"}}} srcErr := errors.New("some error") ok, err := Merge(dstErr, srcErr, true) assert.Equal(t, srcErr, err) @@ -96,21 +94,21 @@ func TestMerge(t *testing.T) { t.Run("validation", func(t *testing.T) { t.Parallel() - dstErr := &ValidationError{Violations: []*validate.Violation{{ConstraintId: proto.String("foo")}}} - srcErr := &ValidationError{Violations: []*validate.Violation{{ConstraintId: proto.String("bar")}}} - exErr := &ValidationError{Violations: []*validate.Violation{ - {ConstraintId: proto.String("foo")}, - {ConstraintId: proto.String("bar")}, + dstErr := &ValidationError{Violations: []Violation{{ConstraintID: "foo"}}} + srcErr := &ValidationError{Violations: []Violation{{ConstraintID: ("bar")}}} + exErr := &ValidationError{Violations: []Violation{ + {ConstraintID: "foo"}, + {ConstraintID: "bar"}, }} ok, err := Merge(dstErr, srcErr, true) var valErr *ValidationError require.ErrorAs(t, err, &valErr) - assert.True(t, proto.Equal(exErr.ToProto(), valErr.ToProto())) + assert.True(t, EqualViolations(exErr.Violations, valErr.Violations)) assert.False(t, ok) - dstErr = &ValidationError{Violations: []*validate.Violation{{ConstraintId: proto.String("foo")}}} + dstErr = &ValidationError{Violations: []Violation{{ConstraintID: "foo"}}} ok, err = Merge(dstErr, srcErr, false) require.ErrorAs(t, err, &valErr) - assert.True(t, proto.Equal(exErr.ToProto(), valErr.ToProto())) + assert.True(t, EqualViolations(exErr.Violations, valErr.Violations)) assert.True(t, ok) }) }) diff --git a/internal/errors/validation.go b/internal/errors/validation.go index bd4712c..71dea06 100644 --- a/internal/errors/validation.go +++ b/internal/errors/validation.go @@ -18,47 +18,78 @@ import ( "fmt" "strings" - "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" - "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protoreflect" ) +// Violation represents a single instance where a validation rule was not met. +// It provides information about the field that caused the violation, the +// specific unfulfilled constraint, and a human-readable error message. +type Violation struct { + // FieldPath is a machine-readable identifier that points to the specific + // field that failed the validation. This could be a nested field, in which + // case the path will include all the parent fields leading to the actual + // field that caused the violation. + FieldPath string + + // FieldValue is the value of the specific field that failed validation. + FieldValue protoreflect.Value + + // ForKey` indicates whether the violation was caused by a map key, rather + // than a value. + ForKey bool + + // ConstraintID is the unique identifier of the constraint that was not + // fulfilled. + ConstraintID string + + // RuleValue is the value of the rule that specified the failed constraint. + // Only constraints that have a corresponding rule are set (i.e.: standard + // constraints and predefined constraints). In other cases, such as custom + // constraints, this will be an invalid value. + RuleValue protoreflect.Value + + // Message is a human-readable error message that describes the nature of + // the violation. This can be the default error message from the violated + // constraint, or it can be a custom message that gives more context about + // the violation. + Message string +} + // A ValidationError is returned if one or more constraint violations were // detected. -type ValidationError validate.Violations +type ValidationError struct { + Violations []Violation +} func (err *ValidationError) Error() string { bldr := &strings.Builder{} bldr.WriteString("validation error:") for _, violation := range err.Violations { bldr.WriteString("\n - ") - if fieldPath := violation.GetFieldPath(); fieldPath != "" { + if fieldPath := violation.FieldPath; fieldPath != "" { bldr.WriteString(fieldPath) bldr.WriteString(": ") } _, _ = fmt.Fprintf(bldr, "%s [%s]", - violation.GetMessage(), - violation.GetConstraintId()) + violation.Message, + violation.ConstraintID) } return bldr.String() } -// ToProto converts this error into its proto.Message form. -func (err *ValidationError) ToProto() *validate.Violations { - return (*validate.Violations)(err) -} - // PrefixFieldPaths prepends to the provided prefix to the error's internal // field paths. func PrefixFieldPaths(err *ValidationError, format string, args ...any) { prefix := fmt.Sprintf(format, args...) - for _, violation := range err.Violations { + for i := range err.Violations { + violation := &err.Violations[i] switch { - case violation.GetFieldPath() == "": // no existing field path - violation.FieldPath = proto.String(prefix) - case violation.GetFieldPath()[0] == '[': // field is a map/list - violation.FieldPath = proto.String(prefix + violation.GetFieldPath()) + case violation.FieldPath == "": // no existing field path + violation.FieldPath = prefix + case violation.FieldPath[0] == '[': // field is a map/list + violation.FieldPath = prefix + violation.FieldPath default: // any other field - violation.FieldPath = proto.String(fmt.Sprintf("%s.%s", prefix, violation.GetFieldPath())) + violation.FieldPath = fmt.Sprintf("%s.%s", prefix, violation.FieldPath) } } } diff --git a/internal/errors/validation_test.go b/internal/errors/validation_test.go index e864188..53ca8b4 100644 --- a/internal/errors/validation_test.go +++ b/internal/errors/validation_test.go @@ -17,9 +17,7 @@ package errors import ( "testing" - "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" "github.com/stretchr/testify/assert" - "google.golang.org/protobuf/proto" ) func TestPrefixFieldPaths(t *testing.T) { @@ -61,13 +59,13 @@ func TestPrefixFieldPaths(t *testing.T) { test := tc t.Run(test.expected, func(t *testing.T) { t.Parallel() - err := &ValidationError{Violations: []*validate.Violation{ - {FieldPath: proto.String(test.fieldPath)}, - {FieldPath: proto.String(test.fieldPath)}, + err := &ValidationError{Violations: []Violation{ + {FieldPath: test.fieldPath}, + {FieldPath: test.fieldPath}, }} PrefixFieldPaths(err, test.format, test.args...) for _, v := range err.Violations { - assert.Equal(t, test.expected, v.GetFieldPath()) + assert.Equal(t, test.expected, v.FieldPath) } }) } diff --git a/internal/evaluator/any.go b/internal/evaluator/any.go index 8b8c9c4..1c3bb44 100644 --- a/internal/evaluator/any.go +++ b/internal/evaluator/any.go @@ -15,9 +15,7 @@ package evaluator import ( - "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" "github.com/bufbuild/protovalidate-go/internal/errors" - "google.golang.org/protobuf/proto" "google.golang.org/protobuf/reflect/protoreflect" ) @@ -37,12 +35,12 @@ type anyPB struct { func (a anyPB) Evaluate(val protoreflect.Value, failFast bool) error { typeURL := val.Message().Get(a.TypeURLDescriptor).String() - err := &errors.ValidationError{Violations: []*validate.Violation{}} + err := &errors.ValidationError{} if len(a.In) > 0 { if _, ok := a.In[typeURL]; !ok { - err.Violations = append(err.Violations, &validate.Violation{ - ConstraintId: proto.String("any.in"), - Message: proto.String("type URL must be in the allow list"), + err.Violations = append(err.Violations, errors.Violation{ + ConstraintID: "any.in", + Message: "type URL must be in the allow list", }) if failFast { return err @@ -52,9 +50,9 @@ func (a anyPB) Evaluate(val protoreflect.Value, failFast bool) error { if len(a.NotIn) > 0 { if _, ok := a.NotIn[typeURL]; ok { - err.Violations = append(err.Violations, &validate.Violation{ - ConstraintId: proto.String("any.not_in"), - Message: proto.String("type URL must not be in the block list"), + err.Violations = append(err.Violations, errors.Violation{ + ConstraintID: "any.not_in", + Message: "type URL must not be in the block list", }) } } diff --git a/internal/evaluator/cel.go b/internal/evaluator/cel.go index 0461798..cc6dd5a 100644 --- a/internal/evaluator/cel.go +++ b/internal/evaluator/cel.go @@ -23,11 +23,11 @@ import ( type celPrograms expression.ProgramSet func (c celPrograms) Evaluate(val protoreflect.Value, failFast bool) error { - return expression.ProgramSet(c).Eval(val.Interface(), failFast) + return expression.ProgramSet(c).Eval(val, failFast) } func (c celPrograms) EvaluateMessage(msg protoreflect.Message, failFast bool) error { - return expression.ProgramSet(c).Eval(msg.Interface(), failFast) + return expression.ProgramSet(c).Eval(protoreflect.ValueOfMessage(msg), failFast) } func (c celPrograms) Tautology() bool { diff --git a/internal/evaluator/enum.go b/internal/evaluator/enum.go index e356cbe..cae629a 100644 --- a/internal/evaluator/enum.go +++ b/internal/evaluator/enum.go @@ -15,9 +15,7 @@ package evaluator import ( - "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" "github.com/bufbuild/protovalidate-go/internal/errors" - "google.golang.org/protobuf/proto" "google.golang.org/protobuf/reflect/protoreflect" ) @@ -31,9 +29,9 @@ type definedEnum struct { func (d definedEnum) Evaluate(val protoreflect.Value, _ bool) error { if d.ValueDescriptors.ByNumber(val.Enum()) == nil { - return &errors.ValidationError{Violations: []*validate.Violation{{ - ConstraintId: proto.String("enum.defined_only"), - Message: proto.String("value must be one of the defined enum values"), + return &errors.ValidationError{Violations: []errors.Violation{{ + ConstraintID: "enum.defined_only", + Message: "value must be one of the defined enum values", }}} } return nil diff --git a/internal/evaluator/field.go b/internal/evaluator/field.go index edfd053..726f30f 100644 --- a/internal/evaluator/field.go +++ b/internal/evaluator/field.go @@ -15,9 +15,7 @@ package evaluator import ( - "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" "github.com/bufbuild/protovalidate-go/internal/errors" - "google.golang.org/protobuf/proto" "google.golang.org/protobuf/reflect/protoreflect" ) @@ -47,10 +45,10 @@ func (f field) Evaluate(val protoreflect.Value, failFast bool) error { func (f field) EvaluateMessage(msg protoreflect.Message, failFast bool) (err error) { if f.Required && !msg.Has(f.Descriptor) { - return &errors.ValidationError{Violations: []*validate.Violation{{ - FieldPath: proto.String(string(f.Descriptor.Name())), - ConstraintId: proto.String("required"), - Message: proto.String("value is required"), + return &errors.ValidationError{Violations: []errors.Violation{{ + FieldPath: string(f.Descriptor.Name()), + ConstraintID: "required", + Message: "value is required", }}} } diff --git a/internal/evaluator/oneof.go b/internal/evaluator/oneof.go index 19e05da..70e9f88 100644 --- a/internal/evaluator/oneof.go +++ b/internal/evaluator/oneof.go @@ -15,9 +15,7 @@ package evaluator import ( - "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" "github.com/bufbuild/protovalidate-go/internal/errors" - "google.golang.org/protobuf/proto" "google.golang.org/protobuf/reflect/protoreflect" ) @@ -35,10 +33,10 @@ func (o oneof) Evaluate(val protoreflect.Value, failFast bool) error { func (o oneof) EvaluateMessage(msg protoreflect.Message, _ bool) error { if o.Required && msg.WhichOneof(o.Descriptor) == nil { - return &errors.ValidationError{Violations: []*validate.Violation{{ - FieldPath: proto.String(string(o.Descriptor.Name())), - ConstraintId: proto.String("required"), - Message: proto.String("exactly one field is required in oneof"), + return &errors.ValidationError{Violations: []errors.Violation{{ + FieldPath: string(o.Descriptor.Name()), + ConstraintID: "required", + Message: "exactly one field is required in oneof", }}} } return nil diff --git a/internal/expression/ast.go b/internal/expression/ast.go index 2f699df..b475047 100644 --- a/internal/expression/ast.go +++ b/internal/expression/ast.go @@ -18,6 +18,7 @@ import ( "github.com/bufbuild/protovalidate-go/internal/errors" "github.com/google/cel-go/cel" "github.com/google/cel-go/interpreter" + "google.golang.org/protobuf/reflect/protoreflect" ) // ASTSet represents a collection of compiledAST and their associated cel.Env. @@ -40,6 +41,14 @@ func (set ASTSet) Merge(other ASTSet) ASTSet { return out } +// SetRuleValue sets the rule value attached to the compiled ASTs. This will be +// returned with validation errors returned by these rules. +func (set ASTSet) SetRuleValue(ruleValue protoreflect.Value) { + for i := range set.asts { + set.asts[i].RuleValue = ruleValue + } +} + // ReduceResiduals generates a ProgramSet, performing a partial evaluation of // the ASTSet to optimize the expression. If the expression is optimized to // either a true or empty string constant result, no compiledProgram is @@ -110,8 +119,9 @@ func (set ASTSet) ToProgramSet(opts ...cel.ProgramOption) (out ProgramSet, err e } type compiledAST struct { - AST *cel.Ast - Source Expression + AST *cel.Ast + Source Expression + RuleValue protoreflect.Value } func (ast compiledAST) toProgram(env *cel.Env, opts ...cel.ProgramOption) (out compiledProgram, err error) { @@ -121,7 +131,8 @@ func (ast compiledAST) toProgram(env *cel.Env, opts ...cel.ProgramOption) (out c "failed to compile program %s: %w", ast.Source.GetId(), err) } return compiledProgram{ - Program: prog, - Source: ast.Source, + Program: prog, + Source: ast.Source, + RuleValue: ast.RuleValue, }, nil } diff --git a/internal/expression/program.go b/internal/expression/program.go index 6f01781..21d0f21 100644 --- a/internal/expression/program.go +++ b/internal/expression/program.go @@ -15,10 +15,8 @@ package expression import ( - "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" "github.com/bufbuild/protovalidate-go/internal/errors" "github.com/google/cel-go/cel" - "google.golang.org/protobuf/proto" "google.golang.org/protobuf/reflect/protoreflect" ) @@ -34,21 +32,21 @@ var nowPool = &NowPool{New: func() any { return &Now{} }} type ProgramSet []compiledProgram // Eval applies the contained expressions to the provided `this` val, returning -// either *errors.ValidationError if the input is invalid or errors.RuntimeError +// either errors.ValidationErrors if the input is invalid or errors.RuntimeError // if there is a type or range error. If failFast is true, execution stops at // the first failed expression. -func (s ProgramSet) Eval(val any, failFast bool) error { - binding := s.bindThis(val) +func (s ProgramSet) Eval(val protoreflect.Value, failFast bool) error { + binding := s.bindThis(val.Interface()) defer varPool.Put(binding) - var violations []*validate.Violation + var violations []errors.Violation for _, expr := range s { violation, err := expr.eval(binding) if err != nil { return err } if violation != nil { - violations = append(violations, violation) + violations = append(violations, *violation) if failFast { break } @@ -88,12 +86,13 @@ func (s ProgramSet) bindThis(val any) *Variable { // compiledProgram is a parsed and type-checked cel.Program along with the // source Expression. type compiledProgram struct { - Program cel.Program - Source Expression + Program cel.Program + Source Expression + RuleValue protoreflect.Value } //nolint:nilnil // non-existence of violations is intentional -func (expr compiledProgram) eval(bindings *Variable) (*validate.Violation, error) { +func (expr compiledProgram) eval(bindings *Variable) (*errors.Violation, error) { now := nowPool.Get() defer nowPool.Put(now) bindings.Next = now @@ -108,17 +107,19 @@ func (expr compiledProgram) eval(bindings *Variable) (*validate.Violation, error if val == "" { return nil, nil } - return &validate.Violation{ - ConstraintId: proto.String(expr.Source.GetId()), - Message: proto.String(val), + return &errors.Violation{ + ConstraintID: expr.Source.GetId(), + Message: val, + RuleValue: expr.RuleValue, }, nil case bool: if val { return nil, nil } - return &validate.Violation{ - ConstraintId: proto.String(expr.Source.GetId()), - Message: proto.String(expr.Source.GetMessage()), + return &errors.Violation{ + ConstraintID: expr.Source.GetId(), + Message: expr.Source.GetMessage(), + RuleValue: expr.RuleValue, }, nil default: return nil, errors.NewRuntimeErrorf( diff --git a/internal/expression/program_test.go b/internal/expression/program_test.go index 6b40d92..1125a49 100644 --- a/internal/expression/program_test.go +++ b/internal/expression/program_test.go @@ -27,6 +27,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protoreflect" "google.golang.org/protobuf/types/known/structpb" "google.golang.org/protobuf/types/known/timestamppb" ) @@ -38,7 +39,7 @@ func TestCompiled(t *testing.T) { name string prog cel.Program src *validate.Constraint - exViol *validate.Violation + exViol *pverr.Violation exErr bool }{ { @@ -53,13 +54,13 @@ func TestCompiled(t *testing.T) { name: "invalid bool", prog: mockProgram{Val: types.False}, src: &validate.Constraint{Id: proto.String("foo"), Message: proto.String("bar")}, - exViol: &validate.Violation{ConstraintId: proto.String("foo"), Message: proto.String("bar")}, + exViol: &pverr.Violation{ConstraintID: "foo", Message: "bar"}, }, { name: "invalid string", prog: mockProgram{Val: types.String("bar")}, src: &validate.Constraint{Id: proto.String("foo")}, - exViol: &validate.Violation{ConstraintId: proto.String("foo"), Message: proto.String("bar")}, + exViol: &pverr.Violation{ConstraintID: "foo", Message: "bar"}, }, { name: "eval error", @@ -85,7 +86,11 @@ func TestCompiled(t *testing.T) { if test.exErr { require.Error(t, err) } else { - assert.True(t, proto.Equal(test.exViol, violation)) + if test.exViol == nil { + assert.Nil(t, violation) + } else { + assert.True(t, pverr.EqualViolation(*test.exViol, *violation)) + } } }) } @@ -98,7 +103,7 @@ func TestSet(t *testing.T) { name string set ProgramSet failFast bool - exViols *pverr.ValidationError + exViols []pverr.Violation exErr bool }{ { @@ -143,10 +148,10 @@ func TestSet(t *testing.T) { Source: &validate.Constraint{Id: proto.String("bar")}, }, }, - exViols: &pverr.ValidationError{Violations: []*validate.Violation{ - {ConstraintId: proto.String("foo"), Message: proto.String("fizz")}, - {ConstraintId: proto.String("bar"), Message: proto.String("buzz")}, - }}, + exViols: []pverr.Violation{ + {ConstraintID: "foo", Message: "fizz"}, + {ConstraintID: "bar", Message: "buzz"}, + }, }, { name: "invalid fail fast", @@ -161,9 +166,9 @@ func TestSet(t *testing.T) { Source: &validate.Constraint{Id: proto.String("bar")}, }, }, - exViols: &pverr.ValidationError{Violations: []*validate.Violation{ - {ConstraintId: proto.String("foo"), Message: proto.String("fizz")}, - }}, + exViols: []pverr.Violation{ + {ConstraintID: "foo", Message: "fizz"}, + }, }, } @@ -172,12 +177,12 @@ func TestSet(t *testing.T) { t.Run(test.name, func(t *testing.T) { t.Parallel() - err := test.set.Eval(nil, test.failFast) + err := test.set.Eval(protoreflect.ValueOfBool(false), test.failFast) switch { case test.exViols != nil: var viols *pverr.ValidationError require.ErrorAs(t, err, &viols) - require.True(t, proto.Equal(test.exViols.ToProto(), viols.ToProto())) + require.True(t, pverr.EqualViolations(test.exViols, viols.Violations)) case test.exErr: require.Error(t, err) default: diff --git a/validator.go b/validator.go index 990f93e..b637b5c 100644 --- a/validator.go +++ b/validator.go @@ -31,18 +31,21 @@ import ( var getGlobalValidator = sync.OnceValues(func() (*Validator, error) { return New() }) type ( - // A ValidationError is returned if one or more constraints on a message are - // violated. This error type can be converted into a validate.Violations - // message via ToProto. + // ValidationError is returned if one or more constraints on a message are + // violated. This error type is a composite of multiple ValidationError + // instances. // // err = validator.Validate(msg) // var valErr *ValidationError // if ok := errors.As(err, &valErr); ok { - // pb := valErr.ToProto() + // violations := valErrs.Violations // // ... // } ValidationError = errors.ValidationError + // A Violation contains information about one constraint violation. + Violation = errors.Violation + // A CompilationError is returned if a CEL expression cannot be compiled & // type-checked or if invalid standard constraints are applied to a field. CompilationError = errors.CompilationError diff --git a/validator_example_test.go b/validator_example_test.go index ce8ad89..39786e6 100644 --- a/validator_example_test.go +++ b/validator_example_test.go @@ -156,8 +156,7 @@ func ExampleValidationError() { err = validator.Validate(loc) var valErr *ValidationError if ok := errors.As(err, &valErr); ok { - msg := valErr.ToProto() - fmt.Println(msg.GetViolations()[0].GetFieldPath(), msg.GetViolations()[0].GetConstraintId()) + fmt.Println(valErr.Violations[0].FieldPath, valErr.Violations[0].ConstraintID) } // output: lat double.gte_lte diff --git a/validator_test.go b/validator_test.go index c9aab01..53e42ff 100644 --- a/validator_test.go +++ b/validator_test.go @@ -228,7 +228,7 @@ func TestValidator_Validate_RepeatedItemCel(t *testing.T) { err = val.Validate(msg) valErr := &ValidationError{} require.ErrorAs(t, err, &valErr) - assert.Equal(t, "paths.no_space", valErr.Violations[0].GetConstraintId()) + assert.Equal(t, "paths.no_space", valErr.Violations[0].ConstraintID) } func TestValidator_Validate_Issue141(t *testing.T) { From 0f4c41c330f61353a765d909178573c7b2675b19 Mon Sep 17 00:00:00 2001 From: John Chadwick Date: Tue, 22 Oct 2024 12:02:07 -0400 Subject: [PATCH 02/24] WIP: RulePath support --- .../cmd/protovalidate-conformance-go/main.go | 17 +- internal/constraints/cache.go | 31 +- internal/constraints/cache_test.go | 2 +- internal/errors/fieldpath.go | 204 +++++ internal/errors/fieldpath_test.go | 169 +++++ internal/errors/utils.go | 11 - internal/errors/validation.go | 93 ++- internal/evaluator/any.go | 15 + internal/evaluator/builder.go | 34 +- internal/evaluator/enum.go | 13 + internal/evaluator/field.go | 5 + internal/evaluator/oneof.go | 1 + internal/expression/ast.go | 15 +- internal/expression/program.go | 3 + internal/gen/tests/example/v1/fieldpath.pb.go | 705 ++++++++++++++++++ proto/tests/example/v1/fieldpath.proto | 125 ++++ 16 files changed, 1393 insertions(+), 50 deletions(-) create mode 100644 internal/errors/fieldpath.go create mode 100644 internal/errors/fieldpath_test.go create mode 100644 internal/gen/tests/example/v1/fieldpath.pb.go create mode 100644 proto/tests/example/v1/fieldpath.proto diff --git a/internal/cmd/protovalidate-conformance-go/main.go b/internal/cmd/protovalidate-conformance-go/main.go index 5eb159c..9126170 100644 --- a/internal/cmd/protovalidate-conformance-go/main.go +++ b/internal/cmd/protovalidate-conformance-go/main.go @@ -21,9 +21,7 @@ import ( "os" "strings" - "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" "github.com/bufbuild/protovalidate-go" - "github.com/bufbuild/protovalidate-go/internal/errors" "github.com/bufbuild/protovalidate-go/internal/gen/buf/validate/conformance/harness" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/reflect/protodesc" @@ -115,7 +113,7 @@ func TestCase(val *protovalidate.Validator, files *protoregistry.Files, testCase case *protovalidate.ValidationError: return &harness.TestResult{ Result: &harness.TestResult_ValidationError{ - ValidationError: violationsToProto(res.Violations), + ValidationError: res.ToProto(), }, } case *protovalidate.RuntimeError: @@ -135,19 +133,6 @@ func TestCase(val *protovalidate.Validator, files *protoregistry.Files, testCase } } -func violationsToProto(violations []errors.Violation) *validate.Violations { - result := make([]*validate.Violation, len(violations)) - for i := range violations { - result[i] = &validate.Violation{ - FieldPath: &violations[i].FieldPath, - ConstraintId: &violations[i].ConstraintID, - Message: &violations[i].Message, - ForKey: &violations[i].ForKey, - } - } - return &validate.Violations{Violations: result} -} - func unexpectedErrorResult(format string, args ...any) *harness.TestResult { return &harness.TestResult{ Result: &harness.TestResult_UnexpectedError{ diff --git a/internal/constraints/cache.go b/internal/constraints/cache.go index c9e3453..9791694 100644 --- a/internal/constraints/cache.go +++ b/internal/constraints/cache.go @@ -47,9 +47,10 @@ func (c *Cache) Build( fieldConstraints *validate.FieldConstraints, extensionTypeResolver protoregistry.ExtensionTypeResolver, allowUnknownFields bool, + forMap bool, forItems bool, ) (set expression.ProgramSet, err error) { - constraints, done, err := c.resolveConstraints( + constraints, setOneof, done, err := c.resolveConstraints( fieldDesc, fieldConstraints, forItems, @@ -58,6 +59,17 @@ func (c *Cache) Build( return nil, err } + rulePath := "" + if forMap && forItems { + rulePath += "map.values." + } else if forMap && !forItems { + rulePath += "map.keys." + } else if forItems { + rulePath += "repeated.items." + } + + rulePath += string(setOneof.Name()) + "." + if err = reparseUnrecognized(extensionTypeResolver, constraints); err != nil { return nil, errors.NewCompilationErrorf("error reparsing message: %w", err) } @@ -71,12 +83,12 @@ func (c *Cache) Build( } var asts expression.ASTSet - constraints.Range(func(desc protoreflect.FieldDescriptor, rule protoreflect.Value) bool { + constraints.Range(func(desc protoreflect.FieldDescriptor, ruleValue protoreflect.Value) bool { fieldEnv, compileErr := env.Extend( cel.Constant( "rule", celext.ProtoFieldToCELType(desc, true, false), - celext.ProtoFieldToCELValue(desc, rule, false), + celext.ProtoFieldToCELValue(desc, ruleValue, false), ), ) if compileErr != nil { @@ -88,7 +100,8 @@ func (c *Cache) Build( err = compileErr return false } - precomputedASTs.SetRuleValue(rule) + rulePath := rulePath + desc.TextName() + precomputedASTs.SetRule(rulePath, ruleValue) asts = asts.Merge(precomputedASTs) return true }) @@ -109,15 +122,15 @@ func (c *Cache) resolveConstraints( fieldDesc protoreflect.FieldDescriptor, fieldConstraints *validate.FieldConstraints, forItems bool, -) (rules protoreflect.Message, done bool, err error) { +) (rules protoreflect.Message, fieldRule protoreflect.FieldDescriptor, done bool, err error) { constraints := fieldConstraints.ProtoReflect() setOneof := constraints.WhichOneof(fieldConstraintsOneofDesc) if setOneof == nil { - return nil, true, nil + return nil, nil, true, nil } expected, ok := c.getExpectedConstraintDescriptor(fieldDesc, forItems) if ok && setOneof.FullName() != expected.FullName() { - return nil, true, errors.NewCompilationErrorf( + return nil, nil, true, errors.NewCompilationErrorf( "expected constraint %q, got %q on field %q", expected.FullName(), setOneof.FullName(), @@ -125,10 +138,10 @@ func (c *Cache) resolveConstraints( ) } if !ok || !constraints.Has(setOneof) { - return nil, true, nil + return nil, nil, true, nil } rules = constraints.Get(setOneof).Message() - return rules, false, nil + return rules, setOneof, false, nil } // prepareEnvironment prepares the environment for compiling standard constraint diff --git a/internal/constraints/cache_test.go b/internal/constraints/cache_test.go index 0c13412..533128c 100644 --- a/internal/constraints/cache_test.go +++ b/internal/constraints/cache_test.go @@ -101,7 +101,7 @@ func TestCache_BuildStandardConstraints(t *testing.T) { require.NoError(t, err) c := NewCache() - set, err := c.Build(env, test.desc, test.cons, protoregistry.GlobalTypes, false, test.forItems) + set, err := c.Build(env, test.desc, test.cons, protoregistry.GlobalTypes, false, false, test.forItems) if test.exErr { assert.Error(t, err) } else { diff --git a/internal/errors/fieldpath.go b/internal/errors/fieldpath.go new file mode 100644 index 0000000..33d58c6 --- /dev/null +++ b/internal/errors/fieldpath.go @@ -0,0 +1,204 @@ +// Copyright 2023-2024 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package errors + +import ( + "errors" + "fmt" + "strconv" + "strings" + + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protoreflect" + "google.golang.org/protobuf/reflect/protoregistry" +) + +// getFieldValue returns the field value at a given path, using the provided +// registry to resolve extensions. +func getFieldValue( + registry protoregistry.ExtensionTypeResolver, + message proto.Message, + path string, +) (field protoreflect.Value, descriptor protoreflect.FieldDescriptor, err error) { + var name, subscript string + var atEnd, isExt bool + reflectMessage := message.ProtoReflect() + for !atEnd { + name, subscript, path, atEnd, isExt = parsePathElement(path) + if name == "" { + return protoreflect.Value{}, nil, errors.New("empty field name") + } + var descriptor protoreflect.FieldDescriptor + if isExt { + extension, err := registry.FindExtensionByName(protoreflect.FullName(name)) + if err != nil { + return protoreflect.Value{}, nil, fmt.Errorf("resolving extension: %w", err) + } + descriptor = extension.TypeDescriptor() + } else { + descriptor = reflectMessage.Descriptor().Fields().ByTextName(name) + } + if descriptor == nil { + return protoreflect.Value{}, nil, fmt.Errorf("field %s not found", name) + } + field = reflectMessage.Get(descriptor) + if subscript != "" { + descriptor, field, err = traverseSubscript(descriptor, subscript, field, name) + if err != nil { + return protoreflect.Value{}, nil, err + } + } else if descriptor.IsList() || descriptor.IsMap() { + if atEnd { + break + } + return protoreflect.Value{}, nil, fmt.Errorf("missing subscript on field %s", name) + } + if descriptor.Message() != nil { + reflectMessage = field.Message() + } + } + return field, descriptor, nil +} + +func traverseSubscript( + descriptor protoreflect.FieldDescriptor, + subscript string, + field protoreflect.Value, + name string, +) (protoreflect.FieldDescriptor, protoreflect.Value, error) { + switch { + case descriptor.IsList(): + i, err := strconv.Atoi(subscript) + if err != nil { + return nil, protoreflect.Value{}, fmt.Errorf("invalid list index: %s", subscript) + } + if !field.IsValid() || i >= field.List().Len() { + return nil, protoreflect.Value{}, fmt.Errorf("index %d out of bounds of field %s", i, name) + } + field = field.List().Get(i) + case descriptor.IsMap(): + key, err := parseMapKey(descriptor, subscript) + if err != nil { + return nil, protoreflect.Value{}, err + } + field = field.Map().Get(key) + if !field.IsValid() { + return nil, protoreflect.Value{}, fmt.Errorf("key %s not present on field %s", subscript, name) + } + descriptor = descriptor.MapValue() + default: + return nil, protoreflect.Value{}, fmt.Errorf("unexpected subscript on field %s", name) + } + return descriptor, field, nil +} + +func parseMapKey(mapDescriptor protoreflect.FieldDescriptor, subscript string) (protoreflect.MapKey, error) { + switch mapDescriptor.MapKey().Kind() { + case protoreflect.BoolKind: + if boolValue, err := strconv.ParseBool(subscript); err == nil { + return protoreflect.MapKey(protoreflect.ValueOfBool(boolValue)), nil + } + case protoreflect.Int32Kind, protoreflect.Sint32Kind, protoreflect.Sfixed32Kind: + if intValue, err := strconv.ParseInt(subscript, 10, 32); err == nil { + return protoreflect.MapKey(protoreflect.ValueOfInt32(int32(intValue))), nil + } + case protoreflect.Int64Kind, protoreflect.Sint64Kind, protoreflect.Sfixed64Kind: + if intValue, err := strconv.ParseInt(subscript, 10, 64); err == nil { + return protoreflect.MapKey(protoreflect.ValueOfInt64(intValue)), nil + } + case protoreflect.Uint32Kind, protoreflect.Fixed32Kind: + if intValue, err := strconv.ParseUint(subscript, 10, 32); err == nil { + return protoreflect.MapKey(protoreflect.ValueOfUint32(uint32(intValue))), nil + } + case protoreflect.Uint64Kind, protoreflect.Fixed64Kind: + if intValue, err := strconv.ParseUint(subscript, 10, 64); err == nil { + return protoreflect.MapKey(protoreflect.ValueOfUint64(intValue)), nil + } + case protoreflect.StringKind: + if stringValue, err := strconv.Unquote(subscript); err == nil { + return protoreflect.MapKey(protoreflect.ValueOfString(stringValue)), nil + } + case protoreflect.EnumKind, protoreflect.FloatKind, protoreflect.DoubleKind, + protoreflect.BytesKind, protoreflect.MessageKind, protoreflect.GroupKind: + fallthrough + default: + // This should not occur, but it might if the rules are relaxed in the + // future. + return protoreflect.MapKey{}, fmt.Errorf("unsupported map key type: %s", mapDescriptor.MapKey().Kind()) + } + return protoreflect.MapKey{}, fmt.Errorf("invalid map key: %s", subscript) +} + +// parsePathElement parses a single +func parsePathElement(path string) (name, subscript, rest string, atEnd bool, isExt bool) { + // Scan extension name. + if len(path) > 0 && path[0] == '[' { + if i := strings.IndexByte(path, ']'); i >= 0 { + isExt = true + name, path = path[1:i], path[i+1:] + } + } + // Scan field name. + if !isExt { + if i := strings.IndexAny(path, ".["); i >= 0 { + name, path = path[:i], path[i:] + } else { + name, path = path, "" + } + } + // No subscript: At end of path. + if len(path) == 0 { + return name, "", path, true, isExt + } + // No subscript: At end of path element. + if path[0] == '.' { + return name, "", path[1:], false, isExt + } + // Malformed subscript + if len(path) == 1 || path[1] == '.' { + name, path = name+path[:1], path[1:] + return name, "", path, true, isExt + } + switch path[1] { + case ']': + // Empty subscript + name, path = name+path[:2], path[2:] + case '`', '"', '\'': + // String subscript: must scan string. + var err error + subscript, err = strconv.QuotedPrefix(path[1:]) + if err == nil { + path = path[len(subscript)+2:] + } + default: + // Other subscript; can skip to next ] + if i := strings.IndexByte(path, ']'); i >= 0 { + subscript, path = path[1:i], path[i+1:] + } else { + // Unterminated subscript + return name + path, "", "", true, isExt + } + } + // No subscript: At end of path. + if len(path) == 0 { + return name, subscript, path, true, isExt + } + // No subscript: At end of path element. + if path[0] == '.' { + return name, subscript, path[1:], false, isExt + } + // Malformed element + return name, subscript, path, false, isExt +} diff --git a/internal/errors/fieldpath_test.go b/internal/errors/fieldpath_test.go new file mode 100644 index 0000000..0a9b0d2 --- /dev/null +++ b/internal/errors/fieldpath_test.go @@ -0,0 +1,169 @@ +// Copyright 2023-2024 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package errors + +import ( + "testing" + + pb "github.com/bufbuild/protovalidate-go/internal/gen/tests/example/v1" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protoregistry" +) + +func TestGetFieldValue(t *testing.T) { + t.Parallel() + + type ( + Simple = pb.FieldPathSimple + Nested = pb.FieldPathNested + Repeated = pb.FieldPathRepeated + Maps = pb.FieldPathMaps + ) + + withExtension := &Simple{} + proto.SetExtension(withExtension, pb.E_Ext, []uint64{10}) + + testGetFieldValueMatch(t, uint64(64), &Simple{Val: 64}, "val") + testGetFieldValueMatch(t, uint64(0), (*Simple)(nil), "val") + testGetFieldValueMatch(t, float64(1.0), &Simple{Nested: &Nested{Val: 1.0}}, "nested.val") + testGetFieldValueMatch(t, float64(0.0), (*Simple)(nil), "nested.val") + testGetFieldValueMatch(t, float64(0.0), &Simple{Nested: nil}, "nested.val") + testGetFieldValueMatch(t, int32(2), &Repeated{Val: []int32{1, 2, 3}}, "val[1]") + testGetFieldValueMatch(t, uint64(32), &Repeated{Msg: []*Simple{{Val: 64}, {Val: 32}}}, "msg[1].val") + testGetFieldValueMatch(t, int32(4), &Maps{Int32Int32Map: map[int32]int32{1: 2, 2: 4, 4: 8}}, "int32_int32_map[2]") + testGetFieldValueMatch(t, uint64(32), &Maps{Int32Map: map[int32]*Simple{1: {Val: 32}}}, "int32_map[1].val") + testGetFieldValueMatch(t, uint64(64), &Maps{Int64Map: map[int64]*Simple{2: {Val: 64}}}, "int64_map[2].val") + testGetFieldValueMatch(t, uint64(64), &Maps{Sint32Map: map[int32]*Simple{2: {Val: 64}}}, "sint32_map[2].val") + testGetFieldValueMatch(t, uint64(32), &Maps{Sint64Map: map[int64]*Simple{1: {Val: 32}}}, "sint64_map[1].val") + testGetFieldValueMatch(t, uint64(64), &Maps{Sfixed32Map: map[int32]*Simple{2: {Val: 64}}}, "sfixed32_map[2].val") + testGetFieldValueMatch(t, uint64(64), &Maps{Sfixed64Map: map[int64]*Simple{2: {Val: 64}}}, "sfixed64_map[2].val") + testGetFieldValueMatch(t, uint64(32), &Maps{Uint32Map: map[uint32]*Simple{1: {Val: 32}}}, "uint32_map[1].val") + testGetFieldValueMatch(t, uint64(32), &Maps{Uint64Map: map[uint64]*Simple{1: {Val: 32}}}, "uint64_map[1].val") + testGetFieldValueMatch(t, uint64(64), &Maps{Fixed32Map: map[uint32]*Simple{2: {Val: 64}}}, "fixed32_map[2].val") + testGetFieldValueMatch(t, uint64(64), &Maps{Fixed64Map: map[uint64]*Simple{2: {Val: 64}}}, "fixed64_map[2].val") + testGetFieldValueMatch(t, uint64(64), &Maps{StringMap: map[string]*Simple{"a": {Val: 64}}}, `string_map["a"].val`) + testGetFieldValueMatch(t, uint64(64), &Maps{StringMap: map[string]*Simple{`".[]][`: {Val: 64}}}, `string_map["\".[]]["].val`) + testGetFieldValueMatch(t, uint64(1), &Maps{BoolMap: map[bool]*Simple{true: {Val: 1}}}, "bool_map[true].val") + testGetFieldValueMatch(t, uint64(0), &Maps{Int32Map: map[int32]*Simple{1: nil}}, "int32_map[1].val") + testGetFieldValueMatch(t, uint64(10), withExtension, "[tests.example.v1.ext][0]") + testGetFieldValueMatch(t, uint64(10), &Repeated{Msg: []*Simple{withExtension}}, "msg[0].[tests.example.v1.ext][0]") + + testGetFieldValueError(t, "field nofield not found", &Simple{Val: 1}, "nofield") + testGetFieldValueError(t, "field nofield not found", &Simple{Val: 1}, "val.nofield") + testGetFieldValueError(t, "unexpected subscript on field val", &Simple{Val: 1}, "val[1]") + testGetFieldValueError(t, "empty field name", &Simple{Val: 1}, "nested.") + testGetFieldValueError(t, "empty field name", &Simple{Val: 1}, "nested..b") + testGetFieldValueError(t, "field ] not found", &Simple{Val: 1}, "]") + testGetFieldValueError(t, "field a] not found", &Simple{Val: 1}, "a]") + testGetFieldValueError(t, "field a[] not found", &Simple{Val: 1}, "a[]") + testGetFieldValueError(t, "field a not found", &Simple{Val: 1}, "a[1]") + testGetFieldValueError(t, "invalid list index: #", &Repeated{}, "val[#]") + testGetFieldValueError(t, "index 1 out of bounds of field val", &Repeated{}, "val[1]") + testGetFieldValueError(t, "index 1 out of bounds of field val", &Repeated{Val: []int32{1}}, "val[1]") + testGetFieldValueError(t, "missing subscript on field msg", &Repeated{}, "msg.val") + testGetFieldValueError(t, "key 2 not present on field int32_map", &Maps{Int32Map: map[int32]*Simple{1: {Val: 1}}}, "int32_map[2].val") + testGetFieldValueError(t, `missing subscript on field string_map`, &Maps{}, `string_map["not a string]`) + testGetFieldValueError(t, `invalid map key: #`, &Maps{}, "int32_map[#]") + testGetFieldValueError(t, `invalid map key: #`, &Maps{}, "int64_map[#]") + testGetFieldValueError(t, `invalid map key: #`, &Maps{}, "sint32_map[#]") + testGetFieldValueError(t, `invalid map key: #`, &Maps{}, "sint64_map[#]") + testGetFieldValueError(t, `invalid map key: #`, &Maps{}, "sfixed32_map[#]") + testGetFieldValueError(t, `invalid map key: #`, &Maps{}, "sfixed64_map[#]") + testGetFieldValueError(t, `invalid map key: #`, &Maps{}, "uint32_map[#]") + testGetFieldValueError(t, `invalid map key: #`, &Maps{}, "uint64_map[#]") + testGetFieldValueError(t, `invalid map key: #`, &Maps{}, "fixed32_map[#]") + testGetFieldValueError(t, `invalid map key: #`, &Maps{}, "fixed64_map[#]") + testGetFieldValueError(t, `invalid map key: #`, &Maps{}, "bool_map[#]") +} + +func BenchmarkGetFieldValue(b *testing.B) { + simpleMessage := &pb.FieldPathSimple{Val: 64} + nestedMessage := &pb.FieldPathSimple{ + Nested: &pb.FieldPathNested{Val: 1}, + } + repeatedMessage := &pb.FieldPathRepeated{ + Msg: []*pb.FieldPathSimple{{Val: 1}}, + } + mapMessage := &pb.FieldPathMaps{ + StringMap: map[string]*pb.FieldPathSimple{"abc": {Val: 1}}, + } + + b.Run("Simple", func(b *testing.B) { + b.ReportAllocs() + b.RunParallel(func(p *testing.PB) { + for p.Next() { + _, _, err := getFieldValue(protoregistry.GlobalTypes, simpleMessage, "val") + require.NoError(b, err) + } + }) + }) + + b.Run("Nested", func(b *testing.B) { + b.ReportAllocs() + b.RunParallel(func(p *testing.PB) { + for p.Next() { + _, _, err := getFieldValue(protoregistry.GlobalTypes, nestedMessage, "nested.val") + require.NoError(b, err) + } + }) + }) + + b.Run("Repeated", func(b *testing.B) { + b.ReportAllocs() + b.RunParallel(func(p *testing.PB) { + for p.Next() { + _, _, err := getFieldValue(protoregistry.GlobalTypes, repeatedMessage, "msg[0].val") + require.NoError(b, err) + } + }) + }) + + b.Run("Map", func(b *testing.B) { + b.ReportAllocs() + b.RunParallel(func(p *testing.PB) { + for p.Next() { + _, _, err := getFieldValue(protoregistry.GlobalTypes, mapMessage, `string_map["abc"].val`) + require.NoError(b, err) + } + }) + }) + + b.Run("Error", func(b *testing.B) { + b.ReportAllocs() + b.RunParallel(func(p *testing.PB) { + for p.Next() { + _, _, err := getFieldValue(protoregistry.GlobalTypes, mapMessage, `string_map["z"].val`) + require.Error(b, err) + } + }) + }) +} + +func testGetFieldValueMatch(t *testing.T, expected any, message proto.Message, path string) { + t.Helper() + + val, _, err := getFieldValue(protoregistry.GlobalTypes, message, path) + require.NoError(t, err) + assert.Equal(t, expected, val.Interface()) +} + +func testGetFieldValueError(t *testing.T, errString string, message proto.Message, path string) { + t.Helper() + + _, _, err := getFieldValue(protoregistry.GlobalTypes, message, path) + assert.EqualError(t, err, errString) +} diff --git a/internal/errors/utils.go b/internal/errors/utils.go index 7b472bb..6711a90 100644 --- a/internal/errors/utils.go +++ b/internal/errors/utils.go @@ -16,7 +16,6 @@ package errors import ( "errors" - "slices" ) // Merge is a utility to resolve and combine Errors resulting from @@ -65,13 +64,3 @@ func MarkForKey(err error) { } } } - -// EqualViolations returns true if the underlying violations are equal. -func EqualViolations(a, b []Violation) bool { - return slices.EqualFunc(a, b, EqualViolation) -} - -// EqualViolation returns true if the underlying violations are equal. -func EqualViolation(a, b Violation) bool { - return a.FieldPath == b.FieldPath && a.ConstraintID == b.ConstraintID && a.Message == b.Message && a.ForKey == b.ForKey -} diff --git a/internal/errors/validation.go b/internal/errors/validation.go index 71dea06..756d166 100644 --- a/internal/errors/validation.go +++ b/internal/errors/validation.go @@ -16,9 +16,13 @@ package errors import ( "fmt" + "slices" "strings" + "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" + "google.golang.org/protobuf/proto" "google.golang.org/protobuf/reflect/protoreflect" + "google.golang.org/protobuf/reflect/protoregistry" ) // Violation represents a single instance where a validation rule was not met. @@ -31,10 +35,16 @@ type Violation struct { // field that caused the violation. FieldPath string + // RulePath is a machine-readable identifier that points to the specific + // constraint rule that failed validation. This will be a nested field + // starting from the FieldConstraints of the field that failed validation. + // This value is only present for standard or predefined rules on fields. + RulePath string + // FieldValue is the value of the specific field that failed validation. FieldValue protoreflect.Value - // ForKey` indicates whether the violation was caused by a map key, rather + // ForKey indicates whether the violation was caused by a map key, rather // than a value. ForKey bool @@ -61,6 +71,60 @@ type ValidationError struct { Violations []Violation } +// FromProto converts the proto.Message form of the error back into native form. +func FromProto( + registry protoregistry.ExtensionTypeResolver, + message proto.Message, + violations *validate.Violations, +) (*ValidationError, error) { + valErr := &ValidationError{ + Violations: make([]Violation, len(violations.GetViolations())), + } + for i, violation := range violations.GetViolations() { + valErr.Violations[i] = Violation{ + FieldPath: violation.GetFieldPath(), + RulePath: violation.GetRulePath(), + ConstraintID: violation.GetConstraintId(), + Message: violation.GetMessage(), + ForKey: violation.GetForKey(), + } + if valErr.Violations[i].FieldPath == "" { + continue + } + fieldValue, descriptor, err := getFieldValue(registry, message, violation.GetFieldPath()) + if err != nil { + return nil, err + } + valErr.Violations[i].FieldValue = fieldValue + if valErr.Violations[i].RulePath == "" { + continue + } + ruleValue, _, err := getFieldValue(registry, descriptor.Options(), valErr.Violations[i].RulePath) + if err != nil { + return nil, err + } + valErr.Violations[i].RuleValue = ruleValue + } + return valErr, nil +} + +// ToProto converts this error into its proto.Message form. +func (err *ValidationError) ToProto() *validate.Violations { + violations := &validate.Violations{ + Violations: make([]*validate.Violation, len(err.Violations)), + } + for i, violation := range err.Violations { + violations.Violations[i] = &validate.Violation{ + FieldPath: proto.String(violation.FieldPath), + RulePath: proto.String(violation.RulePath), + ConstraintId: proto.String(violation.ConstraintID), + Message: proto.String(violation.Message), + ForKey: proto.Bool(violation.ForKey), + } + } + return violations +} + func (err *ValidationError) Error() string { bldr := &strings.Builder{} bldr.WriteString("validation error:") @@ -93,3 +157,30 @@ func PrefixFieldPaths(err *ValidationError, format string, args ...any) { } } } + +// PrefixRulePaths prepends to the provided prefix to the error's internal +// rule paths. +func PrefixRulePaths(err *ValidationError, format string, args ...any) { + prefix := fmt.Sprintf(format, args...) + for i := range err.Violations { + violation := &err.Violations[i] + switch { + case violation.RulePath == "": // no existing rule path + violation.RulePath = prefix + case violation.RulePath[0] == '[': // rule is a map/list + violation.RulePath = prefix + violation.RulePath + default: // any other rule + violation.RulePath = fmt.Sprintf("%s.%s", prefix, violation.RulePath) + } + } +} + +// EqualViolations returns true if the underlying violations are equal. +func EqualViolations(a, b []Violation) bool { + return slices.EqualFunc(a, b, EqualViolation) +} + +// EqualViolation returns true if the underlying violations are equal. +func EqualViolation(a, b Violation) bool { + return a.FieldPath == b.FieldPath && a.ConstraintID == b.ConstraintID && a.Message == b.Message && a.ForKey == b.ForKey +} diff --git a/internal/evaluator/any.go b/internal/evaluator/any.go index 1c3bb44..3ad69df 100644 --- a/internal/evaluator/any.go +++ b/internal/evaluator/any.go @@ -30,16 +30,30 @@ type anyPB struct { In map[string]struct{} // NotIn specifies which type URLs the value may not possess NotIn map[string]struct{} + // Whether the evaluator applies to either map keys or map items. + ForMap bool + // Whether the evaluator applies to items of a map or repeated field. + ForItems bool } func (a anyPB) Evaluate(val protoreflect.Value, failFast bool) error { typeURL := val.Message().Get(a.TypeURLDescriptor).String() + rulePathPrefix := "" + if a.ForMap && a.ForItems { + rulePathPrefix += "map.values." + } else if a.ForMap && !a.ForItems { + rulePathPrefix += "map.keys." + } else if a.ForItems { + rulePathPrefix += "repeated.items." + } + err := &errors.ValidationError{} if len(a.In) > 0 { if _, ok := a.In[typeURL]; !ok { err.Violations = append(err.Violations, errors.Violation{ ConstraintID: "any.in", + RulePath: rulePathPrefix + "any.in", Message: "type URL must be in the allow list", }) if failFast { @@ -52,6 +66,7 @@ func (a anyPB) Evaluate(val protoreflect.Value, failFast bool) error { if _, ok := a.NotIn[typeURL]; ok { err.Violations = append(err.Violations, errors.Violation{ ConstraintID: "any.not_in", + RulePath: rulePathPrefix + "any.not_in", Message: "type URL must not be in the block list", }) } diff --git a/internal/evaluator/builder.go b/internal/evaluator/builder.go index a795097..00aeb57 100644 --- a/internal/evaluator/builder.go +++ b/internal/evaluator/builder.go @@ -221,13 +221,14 @@ func (bldr *Builder) buildField( if fld.IgnoreDefault { fld.Zero = bldr.zeroValue(fieldDescriptor, false) } - err := bldr.buildValue(fieldDescriptor, fieldConstraints, false, &fld.Value, cache) + err := bldr.buildValue(fieldDescriptor, fieldConstraints, false, false, &fld.Value, cache) return fld, err } func (bldr *Builder) buildValue( fdesc protoreflect.FieldDescriptor, constraints *validate.FieldConstraints, + forMap bool, forItems bool, valEval *value, cache MessageCache, @@ -235,6 +236,7 @@ func (bldr *Builder) buildValue( steps := []func( fdesc protoreflect.FieldDescriptor, fieldConstraints *validate.FieldConstraints, + forMap bool, forItems bool, valEval *value, cache MessageCache, @@ -251,7 +253,7 @@ func (bldr *Builder) buildValue( } for _, step := range steps { - if err = step(fdesc, constraints, forItems, valEval, cache); err != nil { + if err = step(fdesc, constraints, forMap, forItems, valEval, cache); err != nil { return err } } @@ -261,13 +263,14 @@ func (bldr *Builder) buildValue( func (bldr *Builder) processIgnoreEmpty( fdesc protoreflect.FieldDescriptor, constraints *validate.FieldConstraints, + forMap bool, forItems bool, val *value, _ MessageCache, ) error { // the only time we need to ignore empty on a value is if it's evaluating a // field item (repeated element or map key/value). - val.IgnoreEmpty = forItems && bldr.shouldIgnoreEmpty(constraints) + val.IgnoreEmpty = (forMap || forItems) && bldr.shouldIgnoreEmpty(constraints) if val.IgnoreEmpty { val.Zero = bldr.zeroValue(fdesc, forItems) } @@ -277,6 +280,7 @@ func (bldr *Builder) processIgnoreEmpty( func (bldr *Builder) processFieldExpressions( fieldDesc protoreflect.FieldDescriptor, fieldConstraints *validate.FieldConstraints, + forMap bool, forItems bool, eval *value, _ MessageCache, @@ -304,6 +308,7 @@ func (bldr *Builder) processFieldExpressions( func (bldr *Builder) processEmbeddedMessage( fdesc protoreflect.FieldDescriptor, rules *validate.FieldConstraints, + forMap bool, forItems bool, valEval *value, cache MessageCache, @@ -329,6 +334,7 @@ func (bldr *Builder) processEmbeddedMessage( func (bldr *Builder) processWrapperConstraints( fdesc protoreflect.FieldDescriptor, rules *validate.FieldConstraints, + forMap bool, forItems bool, valEval *value, cache MessageCache, @@ -345,7 +351,7 @@ func (bldr *Builder) processWrapperConstraints( return nil } var unwrapped value - err := bldr.buildValue(fdesc.Message().Fields().ByName("value"), rules, true, &unwrapped, cache) + err := bldr.buildValue(fdesc.Message().Fields().ByName("value"), rules, forMap, forItems, &unwrapped, cache) if err != nil { return err } @@ -356,6 +362,7 @@ func (bldr *Builder) processWrapperConstraints( func (bldr *Builder) processStandardConstraints( fdesc protoreflect.FieldDescriptor, constraints *validate.FieldConstraints, + forMap bool, forItems bool, valEval *value, _ MessageCache, @@ -366,6 +373,7 @@ func (bldr *Builder) processStandardConstraints( constraints, bldr.extensionTypeResolver, bldr.allowUnknownFields, + forMap, forItems, ) if err != nil { @@ -378,6 +386,7 @@ func (bldr *Builder) processStandardConstraints( func (bldr *Builder) processAnyConstraints( fdesc protoreflect.FieldDescriptor, fieldConstraints *validate.FieldConstraints, + forMap bool, forItems bool, valEval *value, _ MessageCache, @@ -393,6 +402,8 @@ func (bldr *Builder) processAnyConstraints( TypeURLDescriptor: typeURLDesc, In: stringsToSet(fieldConstraints.GetAny().GetIn()), NotIn: stringsToSet(fieldConstraints.GetAny().GetNotIn()), + ForMap: forMap, + ForItems: forItems, } valEval.Append(anyEval) return nil @@ -401,7 +412,8 @@ func (bldr *Builder) processAnyConstraints( func (bldr *Builder) processEnumConstraints( fdesc protoreflect.FieldDescriptor, fieldConstraints *validate.FieldConstraints, - _ bool, + forMap bool, + forItems bool, valEval *value, _ MessageCache, ) error { @@ -409,7 +421,11 @@ func (bldr *Builder) processEnumConstraints( return nil } if fieldConstraints.GetEnum().GetDefinedOnly() { - valEval.Append(definedEnum{ValueDescriptors: fdesc.Enum().Values()}) + valEval.Append(definedEnum{ + ValueDescriptors: fdesc.Enum().Values(), + ForMap: forMap, + ForItems: forItems, + }) } return nil } @@ -418,6 +434,7 @@ func (bldr *Builder) processMapConstraints( fieldDesc protoreflect.FieldDescriptor, constraints *validate.FieldConstraints, _ bool, + _ bool, valEval *value, cache MessageCache, ) error { @@ -431,6 +448,7 @@ func (bldr *Builder) processMapConstraints( fieldDesc.MapKey(), constraints.GetMap().GetKeys(), true, + false, &mapEval.KeyConstraints, cache) if err != nil { @@ -443,6 +461,7 @@ func (bldr *Builder) processMapConstraints( fieldDesc.MapValue(), constraints.GetMap().GetValues(), true, + true, &mapEval.ValueConstraints, cache) if err != nil { @@ -458,6 +477,7 @@ func (bldr *Builder) processMapConstraints( func (bldr *Builder) processRepeatedConstraints( fdesc protoreflect.FieldDescriptor, fieldConstraints *validate.FieldConstraints, + forMap bool, forItems bool, valEval *value, cache MessageCache, @@ -467,7 +487,7 @@ func (bldr *Builder) processRepeatedConstraints( } var listEval listItems - err := bldr.buildValue(fdesc, fieldConstraints.GetRepeated().GetItems(), true, &listEval.ItemConstraints, cache) + err := bldr.buildValue(fdesc, fieldConstraints.GetRepeated().GetItems(), false, true, &listEval.ItemConstraints, cache) if err != nil { return errors.NewCompilationErrorf( "failed to compile items constraints for repeated %v: %w", fdesc.FullName(), err) diff --git a/internal/evaluator/enum.go b/internal/evaluator/enum.go index cae629a..d4efb64 100644 --- a/internal/evaluator/enum.go +++ b/internal/evaluator/enum.go @@ -25,12 +25,25 @@ import ( type definedEnum struct { // ValueDescriptors captures all the defined values for this enum ValueDescriptors protoreflect.EnumValueDescriptors + // Whether the evaluator applies to either map keys or map items. + ForMap bool + // Whether the evaluator applies to items of a map or repeated field. + ForItems bool } func (d definedEnum) Evaluate(val protoreflect.Value, _ bool) error { + rulePathPrefix := "" + if d.ForMap && d.ForItems { + rulePathPrefix += "map.values." + } else if d.ForMap && !d.ForItems { + rulePathPrefix += "map.keys." + } else if d.ForItems { + rulePathPrefix += "repeated.items." + } if d.ValueDescriptors.ByNumber(val.Enum()) == nil { return &errors.ValidationError{Violations: []errors.Violation{{ ConstraintID: "enum.defined_only", + RulePath: rulePathPrefix + "enum.defined_only", Message: "value must be one of the defined enum values", }}} } diff --git a/internal/evaluator/field.go b/internal/evaluator/field.go index 726f30f..24e7789 100644 --- a/internal/evaluator/field.go +++ b/internal/evaluator/field.go @@ -37,6 +37,10 @@ type field struct { IgnoreDefault bool // Zero is the default or zero-value for this value's type Zero protoreflect.Value + // Whether the evaluator applies to either map keys or map items. + ForMap bool + // Whether the evaluator applies to items of a map or repeated field. + ForItems bool } func (f field) Evaluate(val protoreflect.Value, failFast bool) error { @@ -48,6 +52,7 @@ func (f field) EvaluateMessage(msg protoreflect.Message, failFast bool) (err err return &errors.ValidationError{Violations: []errors.Violation{{ FieldPath: string(f.Descriptor.Name()), ConstraintID: "required", + RulePath: "required", Message: "value is required", }}} } diff --git a/internal/evaluator/oneof.go b/internal/evaluator/oneof.go index 70e9f88..4c326f0 100644 --- a/internal/evaluator/oneof.go +++ b/internal/evaluator/oneof.go @@ -36,6 +36,7 @@ func (o oneof) EvaluateMessage(msg protoreflect.Message, _ bool) error { return &errors.ValidationError{Violations: []errors.Violation{{ FieldPath: string(o.Descriptor.Name()), ConstraintID: "required", + RulePath: "required", Message: "exactly one field is required in oneof", }}} } diff --git a/internal/expression/ast.go b/internal/expression/ast.go index b475047..11a1814 100644 --- a/internal/expression/ast.go +++ b/internal/expression/ast.go @@ -41,10 +41,11 @@ func (set ASTSet) Merge(other ASTSet) ASTSet { return out } -// SetRuleValue sets the rule value attached to the compiled ASTs. This will be -// returned with validation errors returned by these rules. -func (set ASTSet) SetRuleValue(ruleValue protoreflect.Value) { +// SetRule sets the rule attached to the compiled ASTs. This will be returned +// with validation errors returned by these expressions. +func (set ASTSet) SetRule(rulePath string, ruleValue protoreflect.Value) { for i := range set.asts { + set.asts[i].RulePath = rulePath set.asts[i].RuleValue = ruleValue } } @@ -91,8 +92,10 @@ func (set ASTSet) ReduceResiduals(opts ...cel.ProgramOption) (ProgramSet, error) x := residual.Source().Content() _ = x residuals = append(residuals, compiledAST{ - AST: residual, - Source: ast.Source, + AST: residual, + Source: ast.Source, + RulePath: ast.RulePath, + RuleValue: ast.RuleValue, }) } } @@ -121,6 +124,7 @@ func (set ASTSet) ToProgramSet(opts ...cel.ProgramOption) (out ProgramSet, err e type compiledAST struct { AST *cel.Ast Source Expression + RulePath string RuleValue protoreflect.Value } @@ -133,6 +137,7 @@ func (ast compiledAST) toProgram(env *cel.Env, opts ...cel.ProgramOption) (out c return compiledProgram{ Program: prog, Source: ast.Source, + RulePath: ast.RulePath, RuleValue: ast.RuleValue, }, nil } diff --git a/internal/expression/program.go b/internal/expression/program.go index 21d0f21..f2f83d5 100644 --- a/internal/expression/program.go +++ b/internal/expression/program.go @@ -88,6 +88,7 @@ func (s ProgramSet) bindThis(val any) *Variable { type compiledProgram struct { Program cel.Program Source Expression + RulePath string RuleValue protoreflect.Value } @@ -110,6 +111,7 @@ func (expr compiledProgram) eval(bindings *Variable) (*errors.Violation, error) return &errors.Violation{ ConstraintID: expr.Source.GetId(), Message: val, + RulePath: expr.RulePath, RuleValue: expr.RuleValue, }, nil case bool: @@ -119,6 +121,7 @@ func (expr compiledProgram) eval(bindings *Variable) (*errors.Violation, error) return &errors.Violation{ ConstraintID: expr.Source.GetId(), Message: expr.Source.GetMessage(), + RulePath: expr.RulePath, RuleValue: expr.RuleValue, }, nil default: diff --git a/internal/gen/tests/example/v1/fieldpath.pb.go b/internal/gen/tests/example/v1/fieldpath.pb.go new file mode 100644 index 0000000..3c4c80e --- /dev/null +++ b/internal/gen/tests/example/v1/fieldpath.pb.go @@ -0,0 +1,705 @@ +// Copyright 2023-2024 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.34.1 +// protoc (unknown) +// source: tests/example/v1/fieldpath.proto + +package examplev1 + +import ( + _ "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type FieldPathSimple struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + extensionFields protoimpl.ExtensionFields + + Val uint64 `protobuf:"varint,1,opt,name=val" json:"val,omitempty"` + Nested *FieldPathNested `protobuf:"bytes,4,opt,name=nested" json:"nested,omitempty"` +} + +func (x *FieldPathSimple) Reset() { + *x = FieldPathSimple{} + if protoimpl.UnsafeEnabled { + mi := &file_tests_example_v1_fieldpath_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *FieldPathSimple) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FieldPathSimple) ProtoMessage() {} + +func (x *FieldPathSimple) ProtoReflect() protoreflect.Message { + mi := &file_tests_example_v1_fieldpath_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FieldPathSimple.ProtoReflect.Descriptor instead. +func (*FieldPathSimple) Descriptor() ([]byte, []int) { + return file_tests_example_v1_fieldpath_proto_rawDescGZIP(), []int{0} +} + +func (x *FieldPathSimple) GetVal() uint64 { + if x != nil { + return x.Val + } + return 0 +} + +func (x *FieldPathSimple) GetNested() *FieldPathNested { + if x != nil { + return x.Nested + } + return nil +} + +type FieldPathNested struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Val float64 `protobuf:"fixed64,1,opt,name=val" json:"val,omitempty"` +} + +func (x *FieldPathNested) Reset() { + *x = FieldPathNested{} + if protoimpl.UnsafeEnabled { + mi := &file_tests_example_v1_fieldpath_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *FieldPathNested) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FieldPathNested) ProtoMessage() {} + +func (x *FieldPathNested) ProtoReflect() protoreflect.Message { + mi := &file_tests_example_v1_fieldpath_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FieldPathNested.ProtoReflect.Descriptor instead. +func (*FieldPathNested) Descriptor() ([]byte, []int) { + return file_tests_example_v1_fieldpath_proto_rawDescGZIP(), []int{1} +} + +func (x *FieldPathNested) GetVal() float64 { + if x != nil { + return x.Val + } + return 0 +} + +type FieldPathMaps struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Int32Int32Map map[int32]int32 `protobuf:"bytes,1,rep,name=int32_int32_map,json=int32Int32Map" json:"int32_int32_map,omitempty" protobuf_key:"varint,1,opt,name=key" protobuf_val:"varint,2,opt,name=value"` + Int32Map map[int32]*FieldPathSimple `protobuf:"bytes,2,rep,name=int32_map,json=int32Map" json:"int32_map,omitempty" protobuf_key:"varint,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + Int64Map map[int64]*FieldPathSimple `protobuf:"bytes,3,rep,name=int64_map,json=int64Map" json:"int64_map,omitempty" protobuf_key:"varint,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + Sint32Map map[int32]*FieldPathSimple `protobuf:"bytes,4,rep,name=sint32_map,json=sint32Map" json:"sint32_map,omitempty" protobuf_key:"zigzag32,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + Sint64Map map[int64]*FieldPathSimple `protobuf:"bytes,5,rep,name=sint64_map,json=sint64Map" json:"sint64_map,omitempty" protobuf_key:"zigzag64,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + Sfixed32Map map[int32]*FieldPathSimple `protobuf:"bytes,6,rep,name=sfixed32_map,json=sfixed32Map" json:"sfixed32_map,omitempty" protobuf_key:"fixed32,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + Sfixed64Map map[int64]*FieldPathSimple `protobuf:"bytes,7,rep,name=sfixed64_map,json=sfixed64Map" json:"sfixed64_map,omitempty" protobuf_key:"fixed64,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + Uint32Map map[uint32]*FieldPathSimple `protobuf:"bytes,8,rep,name=uint32_map,json=uint32Map" json:"uint32_map,omitempty" protobuf_key:"varint,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + Uint64Map map[uint64]*FieldPathSimple `protobuf:"bytes,9,rep,name=uint64_map,json=uint64Map" json:"uint64_map,omitempty" protobuf_key:"varint,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + Fixed32Map map[uint32]*FieldPathSimple `protobuf:"bytes,10,rep,name=fixed32_map,json=fixed32Map" json:"fixed32_map,omitempty" protobuf_key:"fixed32,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + Fixed64Map map[uint64]*FieldPathSimple `protobuf:"bytes,11,rep,name=fixed64_map,json=fixed64Map" json:"fixed64_map,omitempty" protobuf_key:"fixed64,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + StringMap map[string]*FieldPathSimple `protobuf:"bytes,12,rep,name=string_map,json=stringMap" json:"string_map,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + BoolMap map[bool]*FieldPathSimple `protobuf:"bytes,13,rep,name=bool_map,json=boolMap" json:"bool_map,omitempty" protobuf_key:"varint,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` +} + +func (x *FieldPathMaps) Reset() { + *x = FieldPathMaps{} + if protoimpl.UnsafeEnabled { + mi := &file_tests_example_v1_fieldpath_proto_msgTypes[2] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *FieldPathMaps) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FieldPathMaps) ProtoMessage() {} + +func (x *FieldPathMaps) ProtoReflect() protoreflect.Message { + mi := &file_tests_example_v1_fieldpath_proto_msgTypes[2] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FieldPathMaps.ProtoReflect.Descriptor instead. +func (*FieldPathMaps) Descriptor() ([]byte, []int) { + return file_tests_example_v1_fieldpath_proto_rawDescGZIP(), []int{2} +} + +func (x *FieldPathMaps) GetInt32Int32Map() map[int32]int32 { + if x != nil { + return x.Int32Int32Map + } + return nil +} + +func (x *FieldPathMaps) GetInt32Map() map[int32]*FieldPathSimple { + if x != nil { + return x.Int32Map + } + return nil +} + +func (x *FieldPathMaps) GetInt64Map() map[int64]*FieldPathSimple { + if x != nil { + return x.Int64Map + } + return nil +} + +func (x *FieldPathMaps) GetSint32Map() map[int32]*FieldPathSimple { + if x != nil { + return x.Sint32Map + } + return nil +} + +func (x *FieldPathMaps) GetSint64Map() map[int64]*FieldPathSimple { + if x != nil { + return x.Sint64Map + } + return nil +} + +func (x *FieldPathMaps) GetSfixed32Map() map[int32]*FieldPathSimple { + if x != nil { + return x.Sfixed32Map + } + return nil +} + +func (x *FieldPathMaps) GetSfixed64Map() map[int64]*FieldPathSimple { + if x != nil { + return x.Sfixed64Map + } + return nil +} + +func (x *FieldPathMaps) GetUint32Map() map[uint32]*FieldPathSimple { + if x != nil { + return x.Uint32Map + } + return nil +} + +func (x *FieldPathMaps) GetUint64Map() map[uint64]*FieldPathSimple { + if x != nil { + return x.Uint64Map + } + return nil +} + +func (x *FieldPathMaps) GetFixed32Map() map[uint32]*FieldPathSimple { + if x != nil { + return x.Fixed32Map + } + return nil +} + +func (x *FieldPathMaps) GetFixed64Map() map[uint64]*FieldPathSimple { + if x != nil { + return x.Fixed64Map + } + return nil +} + +func (x *FieldPathMaps) GetStringMap() map[string]*FieldPathSimple { + if x != nil { + return x.StringMap + } + return nil +} + +func (x *FieldPathMaps) GetBoolMap() map[bool]*FieldPathSimple { + if x != nil { + return x.BoolMap + } + return nil +} + +type FieldPathRepeated struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Val []int32 `protobuf:"varint,1,rep,packed,name=val" json:"val,omitempty"` + Msg []*FieldPathSimple `protobuf:"bytes,2,rep,name=msg" json:"msg,omitempty"` +} + +func (x *FieldPathRepeated) Reset() { + *x = FieldPathRepeated{} + if protoimpl.UnsafeEnabled { + mi := &file_tests_example_v1_fieldpath_proto_msgTypes[3] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *FieldPathRepeated) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*FieldPathRepeated) ProtoMessage() {} + +func (x *FieldPathRepeated) ProtoReflect() protoreflect.Message { + mi := &file_tests_example_v1_fieldpath_proto_msgTypes[3] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use FieldPathRepeated.ProtoReflect.Descriptor instead. +func (*FieldPathRepeated) Descriptor() ([]byte, []int) { + return file_tests_example_v1_fieldpath_proto_rawDescGZIP(), []int{3} +} + +func (x *FieldPathRepeated) GetVal() []int32 { + if x != nil { + return x.Val + } + return nil +} + +func (x *FieldPathRepeated) GetMsg() []*FieldPathSimple { + if x != nil { + return x.Msg + } + return nil +} + +var file_tests_example_v1_fieldpath_proto_extTypes = []protoimpl.ExtensionInfo{ + { + ExtendedType: (*FieldPathSimple)(nil), + ExtensionType: ([]uint64)(nil), + Field: 1000, + Name: "tests.example.v1.ext", + Tag: "varint,1000,rep,packed,name=ext", + Filename: "tests/example/v1/fieldpath.proto", + }, +} + +// Extension fields to FieldPathSimple. +var ( + // repeated uint64 ext = 1000; + E_Ext = &file_tests_example_v1_fieldpath_proto_extTypes[0] +) + +var File_tests_example_v1_fieldpath_proto protoreflect.FileDescriptor + +var file_tests_example_v1_fieldpath_proto_rawDesc = []byte{ + 0x0a, 0x20, 0x74, 0x65, 0x73, 0x74, 0x73, 0x2f, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2f, + 0x76, 0x31, 0x2f, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x70, 0x61, 0x74, 0x68, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x12, 0x10, 0x74, 0x65, 0x73, 0x74, 0x73, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, + 0x65, 0x2e, 0x76, 0x31, 0x1a, 0x1b, 0x62, 0x75, 0x66, 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, + 0x74, 0x65, 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x22, 0x72, 0x0a, 0x0f, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x50, 0x61, 0x74, 0x68, 0x53, 0x69, + 0x6d, 0x70, 0x6c, 0x65, 0x12, 0x19, 0x0a, 0x03, 0x76, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x04, 0x42, 0x07, 0xba, 0x48, 0x04, 0x32, 0x02, 0x20, 0x01, 0x52, 0x03, 0x76, 0x61, 0x6c, 0x12, + 0x39, 0x0a, 0x06, 0x6e, 0x65, 0x73, 0x74, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x21, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x73, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, + 0x76, 0x31, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x50, 0x61, 0x74, 0x68, 0x4e, 0x65, 0x73, 0x74, + 0x65, 0x64, 0x52, 0x06, 0x6e, 0x65, 0x73, 0x74, 0x65, 0x64, 0x2a, 0x09, 0x08, 0xe8, 0x07, 0x10, + 0x80, 0x80, 0x80, 0x80, 0x02, 0x22, 0x3c, 0x0a, 0x0f, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x50, 0x61, + 0x74, 0x68, 0x4e, 0x65, 0x73, 0x74, 0x65, 0x64, 0x12, 0x29, 0x0a, 0x03, 0x76, 0x61, 0x6c, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x01, 0x42, 0x17, 0xba, 0x48, 0x14, 0x12, 0x12, 0x19, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0xf0, 0x3f, 0x29, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0xbf, 0x52, 0x03, + 0x76, 0x61, 0x6c, 0x22, 0xd9, 0x13, 0x0a, 0x0d, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x50, 0x61, 0x74, + 0x68, 0x4d, 0x61, 0x70, 0x73, 0x12, 0x70, 0x0a, 0x0f, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x5f, 0x69, + 0x6e, 0x74, 0x33, 0x32, 0x5f, 0x6d, 0x61, 0x70, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x32, + 0x2e, 0x74, 0x65, 0x73, 0x74, 0x73, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x76, + 0x31, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x50, 0x61, 0x74, 0x68, 0x4d, 0x61, 0x70, 0x73, 0x2e, + 0x49, 0x6e, 0x74, 0x33, 0x32, 0x49, 0x6e, 0x74, 0x33, 0x32, 0x4d, 0x61, 0x70, 0x45, 0x6e, 0x74, + 0x72, 0x79, 0x42, 0x14, 0xba, 0x48, 0x11, 0x9a, 0x01, 0x0e, 0x10, 0x03, 0x22, 0x04, 0x1a, 0x02, + 0x20, 0x01, 0x2a, 0x04, 0x1a, 0x02, 0x20, 0x01, 0x52, 0x0d, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x49, + 0x6e, 0x74, 0x33, 0x32, 0x4d, 0x61, 0x70, 0x12, 0x5a, 0x0a, 0x09, 0x69, 0x6e, 0x74, 0x33, 0x32, + 0x5f, 0x6d, 0x61, 0x70, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x74, 0x65, 0x73, + 0x74, 0x73, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, + 0x65, 0x6c, 0x64, 0x50, 0x61, 0x74, 0x68, 0x4d, 0x61, 0x70, 0x73, 0x2e, 0x49, 0x6e, 0x74, 0x33, + 0x32, 0x4d, 0x61, 0x70, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x42, 0x0e, 0xba, 0x48, 0x0b, 0x9a, 0x01, + 0x08, 0x10, 0x03, 0x22, 0x04, 0x1a, 0x02, 0x20, 0x01, 0x52, 0x08, 0x69, 0x6e, 0x74, 0x33, 0x32, + 0x4d, 0x61, 0x70, 0x12, 0x5a, 0x0a, 0x09, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x5f, 0x6d, 0x61, 0x70, + 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x73, 0x2e, 0x65, + 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x50, + 0x61, 0x74, 0x68, 0x4d, 0x61, 0x70, 0x73, 0x2e, 0x49, 0x6e, 0x74, 0x36, 0x34, 0x4d, 0x61, 0x70, + 0x45, 0x6e, 0x74, 0x72, 0x79, 0x42, 0x0e, 0xba, 0x48, 0x0b, 0x9a, 0x01, 0x08, 0x10, 0x03, 0x22, + 0x04, 0x22, 0x02, 0x20, 0x01, 0x52, 0x08, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x4d, 0x61, 0x70, 0x12, + 0x5d, 0x0a, 0x0a, 0x73, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x5f, 0x6d, 0x61, 0x70, 0x18, 0x04, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x73, 0x2e, 0x65, 0x78, 0x61, 0x6d, + 0x70, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x50, 0x61, 0x74, 0x68, + 0x4d, 0x61, 0x70, 0x73, 0x2e, 0x53, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x4d, 0x61, 0x70, 0x45, 0x6e, + 0x74, 0x72, 0x79, 0x42, 0x0e, 0xba, 0x48, 0x0b, 0x9a, 0x01, 0x08, 0x10, 0x03, 0x22, 0x04, 0x3a, + 0x02, 0x20, 0x02, 0x52, 0x09, 0x73, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x4d, 0x61, 0x70, 0x12, 0x5d, + 0x0a, 0x0a, 0x73, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x5f, 0x6d, 0x61, 0x70, 0x18, 0x05, 0x20, 0x03, + 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x73, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, + 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x50, 0x61, 0x74, 0x68, 0x4d, + 0x61, 0x70, 0x73, 0x2e, 0x53, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x4d, 0x61, 0x70, 0x45, 0x6e, 0x74, + 0x72, 0x79, 0x42, 0x0e, 0xba, 0x48, 0x0b, 0x9a, 0x01, 0x08, 0x10, 0x03, 0x22, 0x04, 0x42, 0x02, + 0x20, 0x02, 0x52, 0x09, 0x73, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x4d, 0x61, 0x70, 0x12, 0x66, 0x0a, + 0x0c, 0x73, 0x66, 0x69, 0x78, 0x65, 0x64, 0x33, 0x32, 0x5f, 0x6d, 0x61, 0x70, 0x18, 0x06, 0x20, + 0x03, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x73, 0x2e, 0x65, 0x78, 0x61, 0x6d, + 0x70, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x50, 0x61, 0x74, 0x68, + 0x4d, 0x61, 0x70, 0x73, 0x2e, 0x53, 0x66, 0x69, 0x78, 0x65, 0x64, 0x33, 0x32, 0x4d, 0x61, 0x70, + 0x45, 0x6e, 0x74, 0x72, 0x79, 0x42, 0x11, 0xba, 0x48, 0x0e, 0x9a, 0x01, 0x0b, 0x10, 0x03, 0x22, + 0x07, 0x5a, 0x05, 0x25, 0x01, 0x00, 0x00, 0x00, 0x52, 0x0b, 0x73, 0x66, 0x69, 0x78, 0x65, 0x64, + 0x33, 0x32, 0x4d, 0x61, 0x70, 0x12, 0x6a, 0x0a, 0x0c, 0x73, 0x66, 0x69, 0x78, 0x65, 0x64, 0x36, + 0x34, 0x5f, 0x6d, 0x61, 0x70, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x74, 0x65, + 0x73, 0x74, 0x73, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x46, + 0x69, 0x65, 0x6c, 0x64, 0x50, 0x61, 0x74, 0x68, 0x4d, 0x61, 0x70, 0x73, 0x2e, 0x53, 0x66, 0x69, + 0x78, 0x65, 0x64, 0x36, 0x34, 0x4d, 0x61, 0x70, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x42, 0x15, 0xba, + 0x48, 0x12, 0x9a, 0x01, 0x0f, 0x10, 0x03, 0x22, 0x0b, 0x62, 0x09, 0x21, 0x01, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x52, 0x0b, 0x73, 0x66, 0x69, 0x78, 0x65, 0x64, 0x36, 0x34, 0x4d, 0x61, + 0x70, 0x12, 0x5d, 0x0a, 0x0a, 0x75, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x5f, 0x6d, 0x61, 0x70, 0x18, + 0x08, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x73, 0x2e, 0x65, 0x78, + 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x50, 0x61, + 0x74, 0x68, 0x4d, 0x61, 0x70, 0x73, 0x2e, 0x55, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x4d, 0x61, 0x70, + 0x45, 0x6e, 0x74, 0x72, 0x79, 0x42, 0x0e, 0xba, 0x48, 0x0b, 0x9a, 0x01, 0x08, 0x10, 0x03, 0x22, + 0x04, 0x2a, 0x02, 0x20, 0x01, 0x52, 0x09, 0x75, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x4d, 0x61, 0x70, + 0x12, 0x5d, 0x0a, 0x0a, 0x75, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x5f, 0x6d, 0x61, 0x70, 0x18, 0x09, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x73, 0x2e, 0x65, 0x78, 0x61, + 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x50, 0x61, 0x74, + 0x68, 0x4d, 0x61, 0x70, 0x73, 0x2e, 0x55, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x4d, 0x61, 0x70, 0x45, + 0x6e, 0x74, 0x72, 0x79, 0x42, 0x0e, 0xba, 0x48, 0x0b, 0x9a, 0x01, 0x08, 0x10, 0x03, 0x22, 0x04, + 0x32, 0x02, 0x20, 0x01, 0x52, 0x09, 0x75, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x4d, 0x61, 0x70, 0x12, + 0x63, 0x0a, 0x0b, 0x66, 0x69, 0x78, 0x65, 0x64, 0x33, 0x32, 0x5f, 0x6d, 0x61, 0x70, 0x18, 0x0a, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2f, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x73, 0x2e, 0x65, 0x78, 0x61, + 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x50, 0x61, 0x74, + 0x68, 0x4d, 0x61, 0x70, 0x73, 0x2e, 0x46, 0x69, 0x78, 0x65, 0x64, 0x33, 0x32, 0x4d, 0x61, 0x70, + 0x45, 0x6e, 0x74, 0x72, 0x79, 0x42, 0x11, 0xba, 0x48, 0x0e, 0x9a, 0x01, 0x0b, 0x10, 0x03, 0x22, + 0x07, 0x4a, 0x05, 0x25, 0x01, 0x00, 0x00, 0x00, 0x52, 0x0a, 0x66, 0x69, 0x78, 0x65, 0x64, 0x33, + 0x32, 0x4d, 0x61, 0x70, 0x12, 0x67, 0x0a, 0x0b, 0x66, 0x69, 0x78, 0x65, 0x64, 0x36, 0x34, 0x5f, + 0x6d, 0x61, 0x70, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2f, 0x2e, 0x74, 0x65, 0x73, 0x74, + 0x73, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, 0x65, + 0x6c, 0x64, 0x50, 0x61, 0x74, 0x68, 0x4d, 0x61, 0x70, 0x73, 0x2e, 0x46, 0x69, 0x78, 0x65, 0x64, + 0x36, 0x34, 0x4d, 0x61, 0x70, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x42, 0x15, 0xba, 0x48, 0x12, 0x9a, + 0x01, 0x0f, 0x10, 0x03, 0x22, 0x0b, 0x52, 0x09, 0x21, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x52, 0x0a, 0x66, 0x69, 0x78, 0x65, 0x64, 0x36, 0x34, 0x4d, 0x61, 0x70, 0x12, 0x5d, 0x0a, + 0x0a, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x5f, 0x6d, 0x61, 0x70, 0x18, 0x0c, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x2e, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x73, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, + 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x50, 0x61, 0x74, 0x68, 0x4d, 0x61, + 0x70, 0x73, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x4d, 0x61, 0x70, 0x45, 0x6e, 0x74, 0x72, + 0x79, 0x42, 0x0e, 0xba, 0x48, 0x0b, 0x9a, 0x01, 0x08, 0x10, 0x03, 0x22, 0x04, 0x72, 0x02, 0x10, + 0x02, 0x52, 0x09, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x4d, 0x61, 0x70, 0x12, 0x51, 0x0a, 0x08, + 0x62, 0x6f, 0x6f, 0x6c, 0x5f, 0x6d, 0x61, 0x70, 0x18, 0x0d, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2c, + 0x2e, 0x74, 0x65, 0x73, 0x74, 0x73, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x76, + 0x31, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x50, 0x61, 0x74, 0x68, 0x4d, 0x61, 0x70, 0x73, 0x2e, + 0x42, 0x6f, 0x6f, 0x6c, 0x4d, 0x61, 0x70, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x42, 0x08, 0xba, 0x48, + 0x05, 0x9a, 0x01, 0x02, 0x10, 0x03, 0x52, 0x07, 0x62, 0x6f, 0x6f, 0x6c, 0x4d, 0x61, 0x70, 0x1a, + 0x40, 0x0a, 0x12, 0x49, 0x6e, 0x74, 0x33, 0x32, 0x49, 0x6e, 0x74, 0x33, 0x32, 0x4d, 0x61, 0x70, + 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x05, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, + 0x01, 0x1a, 0x5e, 0x0a, 0x0d, 0x49, 0x6e, 0x74, 0x33, 0x32, 0x4d, 0x61, 0x70, 0x45, 0x6e, 0x74, + 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, + 0x03, 0x6b, 0x65, 0x79, 0x12, 0x37, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x73, 0x2e, 0x65, 0x78, 0x61, 0x6d, + 0x70, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x50, 0x61, 0x74, 0x68, + 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, + 0x01, 0x1a, 0x5e, 0x0a, 0x0d, 0x49, 0x6e, 0x74, 0x36, 0x34, 0x4d, 0x61, 0x70, 0x45, 0x6e, 0x74, + 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x03, 0x6b, 0x65, 0x79, 0x12, 0x37, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x73, 0x2e, 0x65, 0x78, 0x61, 0x6d, + 0x70, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x50, 0x61, 0x74, 0x68, + 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, + 0x01, 0x1a, 0x5f, 0x0a, 0x0e, 0x53, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x4d, 0x61, 0x70, 0x45, 0x6e, + 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x11, + 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x37, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x73, 0x2e, 0x65, 0x78, 0x61, + 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x50, 0x61, 0x74, + 0x68, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, + 0x38, 0x01, 0x1a, 0x5f, 0x0a, 0x0e, 0x53, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x4d, 0x61, 0x70, 0x45, + 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x12, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x37, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x73, 0x2e, 0x65, 0x78, + 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x50, 0x61, + 0x74, 0x68, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, + 0x02, 0x38, 0x01, 0x1a, 0x61, 0x0a, 0x10, 0x53, 0x66, 0x69, 0x78, 0x65, 0x64, 0x33, 0x32, 0x4d, + 0x61, 0x70, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, + 0x20, 0x01, 0x28, 0x0f, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x37, 0x0a, 0x05, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x73, + 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, 0x65, 0x6c, + 0x64, 0x50, 0x61, 0x74, 0x68, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, + 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x61, 0x0a, 0x10, 0x53, 0x66, 0x69, 0x78, 0x65, 0x64, + 0x36, 0x34, 0x4d, 0x61, 0x70, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, + 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x10, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x37, 0x0a, 0x05, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x74, 0x65, + 0x73, 0x74, 0x73, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x46, + 0x69, 0x65, 0x6c, 0x64, 0x50, 0x61, 0x74, 0x68, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x05, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x5f, 0x0a, 0x0e, 0x55, 0x69, 0x6e, + 0x74, 0x33, 0x32, 0x4d, 0x61, 0x70, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, + 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x37, 0x0a, + 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x74, + 0x65, 0x73, 0x74, 0x73, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, + 0x46, 0x69, 0x65, 0x6c, 0x64, 0x50, 0x61, 0x74, 0x68, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x52, + 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x5f, 0x0a, 0x0e, 0x55, 0x69, + 0x6e, 0x74, 0x36, 0x34, 0x4d, 0x61, 0x70, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, + 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x37, + 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x21, 0x2e, + 0x74, 0x65, 0x73, 0x74, 0x73, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x76, 0x31, + 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x50, 0x61, 0x74, 0x68, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, + 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x60, 0x0a, 0x0f, 0x46, + 0x69, 0x78, 0x65, 0x64, 0x33, 0x32, 0x4d, 0x61, 0x70, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, + 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x07, 0x52, 0x03, 0x6b, 0x65, 0x79, + 0x12, 0x37, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, + 0x21, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x73, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, + 0x76, 0x31, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x50, 0x61, 0x74, 0x68, 0x53, 0x69, 0x6d, 0x70, + 0x6c, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x60, 0x0a, + 0x0f, 0x46, 0x69, 0x78, 0x65, 0x64, 0x36, 0x34, 0x4d, 0x61, 0x70, 0x45, 0x6e, 0x74, 0x72, 0x79, + 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x06, 0x52, 0x03, 0x6b, + 0x65, 0x79, 0x12, 0x37, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x21, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x73, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, + 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x50, 0x61, 0x74, 0x68, 0x53, 0x69, + 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, + 0x5f, 0x0a, 0x0e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x4d, 0x61, 0x70, 0x45, 0x6e, 0x74, 0x72, + 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, + 0x6b, 0x65, 0x79, 0x12, 0x37, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x73, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, + 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x50, 0x61, 0x74, 0x68, 0x53, + 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, + 0x1a, 0x5d, 0x0a, 0x0c, 0x42, 0x6f, 0x6f, 0x6c, 0x4d, 0x61, 0x70, 0x45, 0x6e, 0x74, 0x72, 0x79, + 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x6b, + 0x65, 0x79, 0x12, 0x37, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x21, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x73, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, + 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x50, 0x61, 0x74, 0x68, 0x53, 0x69, + 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, + 0x68, 0x0a, 0x11, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x50, 0x61, 0x74, 0x68, 0x52, 0x65, 0x70, 0x65, + 0x61, 0x74, 0x65, 0x64, 0x12, 0x1e, 0x0a, 0x03, 0x76, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x03, 0x28, + 0x05, 0x42, 0x0c, 0xba, 0x48, 0x09, 0x92, 0x01, 0x06, 0x22, 0x04, 0x1a, 0x02, 0x20, 0x01, 0x52, + 0x03, 0x76, 0x61, 0x6c, 0x12, 0x33, 0x0a, 0x03, 0x6d, 0x73, 0x67, 0x18, 0x02, 0x20, 0x03, 0x28, + 0x0b, 0x32, 0x21, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x73, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, + 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x50, 0x61, 0x74, 0x68, 0x53, 0x69, + 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x03, 0x6d, 0x73, 0x67, 0x3a, 0x42, 0x0a, 0x03, 0x65, 0x78, 0x74, + 0x12, 0x21, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x73, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, + 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x50, 0x61, 0x74, 0x68, 0x53, 0x69, 0x6d, + 0x70, 0x6c, 0x65, 0x18, 0xe8, 0x07, 0x20, 0x03, 0x28, 0x04, 0x42, 0x0c, 0xba, 0x48, 0x09, 0x92, + 0x01, 0x06, 0x22, 0x04, 0x32, 0x02, 0x20, 0x01, 0x52, 0x03, 0x65, 0x78, 0x74, 0x42, 0xdb, 0x01, + 0x0a, 0x14, 0x63, 0x6f, 0x6d, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x73, 0x2e, 0x65, 0x78, 0x61, 0x6d, + 0x70, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x42, 0x0e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x70, 0x61, 0x74, + 0x68, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x4c, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, + 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x62, 0x75, 0x66, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x2f, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x2d, 0x67, 0x6f, 0x2f, 0x69, + 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x74, 0x65, 0x73, 0x74, + 0x73, 0x2f, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2f, 0x76, 0x31, 0x3b, 0x65, 0x78, 0x61, + 0x6d, 0x70, 0x6c, 0x65, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x54, 0x45, 0x58, 0xaa, 0x02, 0x10, 0x54, + 0x65, 0x73, 0x74, 0x73, 0x2e, 0x45, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x56, 0x31, 0xca, + 0x02, 0x10, 0x54, 0x65, 0x73, 0x74, 0x73, 0x5c, 0x45, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x5c, + 0x56, 0x31, 0xe2, 0x02, 0x1c, 0x54, 0x65, 0x73, 0x74, 0x73, 0x5c, 0x45, 0x78, 0x61, 0x6d, 0x70, + 0x6c, 0x65, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, + 0x61, 0xea, 0x02, 0x12, 0x54, 0x65, 0x73, 0x74, 0x73, 0x3a, 0x3a, 0x45, 0x78, 0x61, 0x6d, 0x70, + 0x6c, 0x65, 0x3a, 0x3a, 0x56, 0x31, 0x92, 0x03, 0x02, 0x08, 0x02, 0x62, 0x08, 0x65, 0x64, 0x69, + 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x70, 0xe8, 0x07, +} + +var ( + file_tests_example_v1_fieldpath_proto_rawDescOnce sync.Once + file_tests_example_v1_fieldpath_proto_rawDescData = file_tests_example_v1_fieldpath_proto_rawDesc +) + +func file_tests_example_v1_fieldpath_proto_rawDescGZIP() []byte { + file_tests_example_v1_fieldpath_proto_rawDescOnce.Do(func() { + file_tests_example_v1_fieldpath_proto_rawDescData = protoimpl.X.CompressGZIP(file_tests_example_v1_fieldpath_proto_rawDescData) + }) + return file_tests_example_v1_fieldpath_proto_rawDescData +} + +var file_tests_example_v1_fieldpath_proto_msgTypes = make([]protoimpl.MessageInfo, 17) +var file_tests_example_v1_fieldpath_proto_goTypes = []interface{}{ + (*FieldPathSimple)(nil), // 0: tests.example.v1.FieldPathSimple + (*FieldPathNested)(nil), // 1: tests.example.v1.FieldPathNested + (*FieldPathMaps)(nil), // 2: tests.example.v1.FieldPathMaps + (*FieldPathRepeated)(nil), // 3: tests.example.v1.FieldPathRepeated + nil, // 4: tests.example.v1.FieldPathMaps.Int32Int32MapEntry + nil, // 5: tests.example.v1.FieldPathMaps.Int32MapEntry + nil, // 6: tests.example.v1.FieldPathMaps.Int64MapEntry + nil, // 7: tests.example.v1.FieldPathMaps.Sint32MapEntry + nil, // 8: tests.example.v1.FieldPathMaps.Sint64MapEntry + nil, // 9: tests.example.v1.FieldPathMaps.Sfixed32MapEntry + nil, // 10: tests.example.v1.FieldPathMaps.Sfixed64MapEntry + nil, // 11: tests.example.v1.FieldPathMaps.Uint32MapEntry + nil, // 12: tests.example.v1.FieldPathMaps.Uint64MapEntry + nil, // 13: tests.example.v1.FieldPathMaps.Fixed32MapEntry + nil, // 14: tests.example.v1.FieldPathMaps.Fixed64MapEntry + nil, // 15: tests.example.v1.FieldPathMaps.StringMapEntry + nil, // 16: tests.example.v1.FieldPathMaps.BoolMapEntry +} +var file_tests_example_v1_fieldpath_proto_depIdxs = []int32{ + 1, // 0: tests.example.v1.FieldPathSimple.nested:type_name -> tests.example.v1.FieldPathNested + 4, // 1: tests.example.v1.FieldPathMaps.int32_int32_map:type_name -> tests.example.v1.FieldPathMaps.Int32Int32MapEntry + 5, // 2: tests.example.v1.FieldPathMaps.int32_map:type_name -> tests.example.v1.FieldPathMaps.Int32MapEntry + 6, // 3: tests.example.v1.FieldPathMaps.int64_map:type_name -> tests.example.v1.FieldPathMaps.Int64MapEntry + 7, // 4: tests.example.v1.FieldPathMaps.sint32_map:type_name -> tests.example.v1.FieldPathMaps.Sint32MapEntry + 8, // 5: tests.example.v1.FieldPathMaps.sint64_map:type_name -> tests.example.v1.FieldPathMaps.Sint64MapEntry + 9, // 6: tests.example.v1.FieldPathMaps.sfixed32_map:type_name -> tests.example.v1.FieldPathMaps.Sfixed32MapEntry + 10, // 7: tests.example.v1.FieldPathMaps.sfixed64_map:type_name -> tests.example.v1.FieldPathMaps.Sfixed64MapEntry + 11, // 8: tests.example.v1.FieldPathMaps.uint32_map:type_name -> tests.example.v1.FieldPathMaps.Uint32MapEntry + 12, // 9: tests.example.v1.FieldPathMaps.uint64_map:type_name -> tests.example.v1.FieldPathMaps.Uint64MapEntry + 13, // 10: tests.example.v1.FieldPathMaps.fixed32_map:type_name -> tests.example.v1.FieldPathMaps.Fixed32MapEntry + 14, // 11: tests.example.v1.FieldPathMaps.fixed64_map:type_name -> tests.example.v1.FieldPathMaps.Fixed64MapEntry + 15, // 12: tests.example.v1.FieldPathMaps.string_map:type_name -> tests.example.v1.FieldPathMaps.StringMapEntry + 16, // 13: tests.example.v1.FieldPathMaps.bool_map:type_name -> tests.example.v1.FieldPathMaps.BoolMapEntry + 0, // 14: tests.example.v1.FieldPathRepeated.msg:type_name -> tests.example.v1.FieldPathSimple + 0, // 15: tests.example.v1.FieldPathMaps.Int32MapEntry.value:type_name -> tests.example.v1.FieldPathSimple + 0, // 16: tests.example.v1.FieldPathMaps.Int64MapEntry.value:type_name -> tests.example.v1.FieldPathSimple + 0, // 17: tests.example.v1.FieldPathMaps.Sint32MapEntry.value:type_name -> tests.example.v1.FieldPathSimple + 0, // 18: tests.example.v1.FieldPathMaps.Sint64MapEntry.value:type_name -> tests.example.v1.FieldPathSimple + 0, // 19: tests.example.v1.FieldPathMaps.Sfixed32MapEntry.value:type_name -> tests.example.v1.FieldPathSimple + 0, // 20: tests.example.v1.FieldPathMaps.Sfixed64MapEntry.value:type_name -> tests.example.v1.FieldPathSimple + 0, // 21: tests.example.v1.FieldPathMaps.Uint32MapEntry.value:type_name -> tests.example.v1.FieldPathSimple + 0, // 22: tests.example.v1.FieldPathMaps.Uint64MapEntry.value:type_name -> tests.example.v1.FieldPathSimple + 0, // 23: tests.example.v1.FieldPathMaps.Fixed32MapEntry.value:type_name -> tests.example.v1.FieldPathSimple + 0, // 24: tests.example.v1.FieldPathMaps.Fixed64MapEntry.value:type_name -> tests.example.v1.FieldPathSimple + 0, // 25: tests.example.v1.FieldPathMaps.StringMapEntry.value:type_name -> tests.example.v1.FieldPathSimple + 0, // 26: tests.example.v1.FieldPathMaps.BoolMapEntry.value:type_name -> tests.example.v1.FieldPathSimple + 0, // 27: tests.example.v1.ext:extendee -> tests.example.v1.FieldPathSimple + 28, // [28:28] is the sub-list for method output_type + 28, // [28:28] is the sub-list for method input_type + 28, // [28:28] is the sub-list for extension type_name + 27, // [27:28] is the sub-list for extension extendee + 0, // [0:27] is the sub-list for field type_name +} + +func init() { file_tests_example_v1_fieldpath_proto_init() } +func file_tests_example_v1_fieldpath_proto_init() { + if File_tests_example_v1_fieldpath_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_tests_example_v1_fieldpath_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*FieldPathSimple); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + case 3: + return &v.extensionFields + default: + return nil + } + } + file_tests_example_v1_fieldpath_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*FieldPathNested); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_tests_example_v1_fieldpath_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*FieldPathMaps); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_tests_example_v1_fieldpath_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*FieldPathRepeated); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_tests_example_v1_fieldpath_proto_rawDesc, + NumEnums: 0, + NumMessages: 17, + NumExtensions: 1, + NumServices: 0, + }, + GoTypes: file_tests_example_v1_fieldpath_proto_goTypes, + DependencyIndexes: file_tests_example_v1_fieldpath_proto_depIdxs, + MessageInfos: file_tests_example_v1_fieldpath_proto_msgTypes, + ExtensionInfos: file_tests_example_v1_fieldpath_proto_extTypes, + }.Build() + File_tests_example_v1_fieldpath_proto = out.File + file_tests_example_v1_fieldpath_proto_rawDesc = nil + file_tests_example_v1_fieldpath_proto_goTypes = nil + file_tests_example_v1_fieldpath_proto_depIdxs = nil +} diff --git a/proto/tests/example/v1/fieldpath.proto b/proto/tests/example/v1/fieldpath.proto new file mode 100644 index 0000000..86177a3 --- /dev/null +++ b/proto/tests/example/v1/fieldpath.proto @@ -0,0 +1,125 @@ +// Copyright 2023-2024 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +edition = "2023"; + +package tests.example.v1; + +import "buf/validate/validate.proto"; + +option features.field_presence = IMPLICIT; + +message FieldPathSimple { + uint64 val = 1 [(buf.validate.field).uint64.gt = 1]; + + FieldPathNested nested = 4; + + extensions 1000 to max; +} + +extend FieldPathSimple { + repeated uint64 ext = 1000 [(buf.validate.field).repeated.items.uint64.gt = 1]; +} + +message FieldPathNested { + double val = 1 [(buf.validate.field).double = { + gte: -1 + lte: 1 + }]; +} + +message FieldPathMaps { + map int32_int32_map = 1 [(buf.validate.field).map = { + max_pairs: 3 + keys: { + int32: {gt: 1} + } + values: { + int32: {gt: 1} + } + }]; + map int32_map = 2 [(buf.validate.field).map = { + max_pairs: 3 + keys: { + int32: {gt: 1} + } + }]; + map int64_map = 3 [(buf.validate.field).map = { + max_pairs: 3 + keys: { + int64: {gt: 1} + } + }]; + map sint32_map = 4 [(buf.validate.field).map = { + max_pairs: 3 + keys: { + sint32: {gt: 1} + } + }]; + map sint64_map = 5 [(buf.validate.field).map = { + max_pairs: 3 + keys: { + sint64: {gt: 1} + } + }]; + map sfixed32_map = 6 [(buf.validate.field).map = { + max_pairs: 3 + keys: { + sfixed32: {gt: 1} + } + }]; + map sfixed64_map = 7 [(buf.validate.field).map = { + max_pairs: 3 + keys: { + sfixed64: {gt: 1} + } + }]; + map uint32_map = 8 [(buf.validate.field).map = { + max_pairs: 3 + keys: { + uint32: {gt: 1} + } + }]; + map uint64_map = 9 [(buf.validate.field).map = { + max_pairs: 3 + keys: { + uint64: {gt: 1} + } + }]; + map fixed32_map = 10 [(buf.validate.field).map = { + max_pairs: 3 + keys: { + fixed32: {gt: 1} + } + }]; + map fixed64_map = 11 [(buf.validate.field).map = { + max_pairs: 3 + keys: { + fixed64: {gt: 1} + } + }]; + map string_map = 12 [(buf.validate.field).map = { + max_pairs: 3 + keys: { + string: {min_len: 2} + } + }]; + map bool_map = 13 [(buf.validate.field).map = {max_pairs: 3}]; +} + +message FieldPathRepeated { + repeated int32 val = 1 [(buf.validate.field).repeated.items.int32.gt = 1]; + + repeated FieldPathSimple msg = 2; +} From ab2e420b374947261ea29cb250ee99ee9e0d05d2 Mon Sep 17 00:00:00 2001 From: John Chadwick Date: Mon, 4 Nov 2024 19:52:58 -0500 Subject: [PATCH 03/24] Checkpoint --- internal/constraints/cache.go | 33 ++--- internal/constraints/cache_test.go | 8 +- internal/errors/fieldpath.go | 204 ---------------------------- internal/errors/fieldpath_test.go | 169 ----------------------- internal/errors/utils.go | 109 ++++++++++++++- internal/errors/validation.go | 130 +++++++----------- internal/errors/validation_test.go | 57 -------- internal/evaluator/any.go | 32 +++-- internal/evaluator/builder.go | 108 +++++++-------- internal/evaluator/enum.go | 23 ++-- internal/evaluator/evaluator.go | 4 + internal/evaluator/field.go | 27 ++-- internal/evaluator/map.go | 75 +++++++++- internal/evaluator/oneof.go | 7 +- internal/evaluator/repeated.go | 36 ++++- internal/expression/ast.go | 32 ++--- internal/expression/ast_test.go | 24 +++- internal/expression/compile.go | 50 +++---- internal/expression/compile_test.go | 30 ++-- internal/expression/program.go | 14 +- validator.go | 4 +- validator_example_test.go | 2 +- 22 files changed, 467 insertions(+), 711 deletions(-) delete mode 100644 internal/errors/fieldpath.go delete mode 100644 internal/errors/fieldpath_test.go diff --git a/internal/constraints/cache.go b/internal/constraints/cache.go index 9791694..8719d88 100644 --- a/internal/constraints/cache.go +++ b/internal/constraints/cache.go @@ -47,7 +47,6 @@ func (c *Cache) Build( fieldConstraints *validate.FieldConstraints, extensionTypeResolver protoregistry.ExtensionTypeResolver, allowUnknownFields bool, - forMap bool, forItems bool, ) (set expression.ProgramSet, err error) { constraints, setOneof, done, err := c.resolveConstraints( @@ -59,17 +58,6 @@ func (c *Cache) Build( return nil, err } - rulePath := "" - if forMap && forItems { - rulePath += "map.values." - } else if forMap && !forItems { - rulePath += "map.keys." - } else if forItems { - rulePath += "repeated.items." - } - - rulePath += string(setOneof.Name()) + "." - if err = reparseUnrecognized(extensionTypeResolver, constraints); err != nil { return nil, errors.NewCompilationErrorf("error reparsing message: %w", err) } @@ -95,13 +83,11 @@ func (c *Cache) Build( err = compileErr return false } - precomputedASTs, compileErr := c.loadOrCompileStandardConstraint(fieldEnv, desc) + precomputedASTs, compileErr := c.loadOrCompileStandardConstraint(fieldEnv, setOneof, desc) if compileErr != nil { err = compileErr return false } - rulePath := rulePath + desc.TextName() - precomputedASTs.SetRule(rulePath, ruleValue) asts = asts.Merge(precomputedASTs) return true }) @@ -171,16 +157,23 @@ func (c *Cache) prepareEnvironment( // CEL expressions. func (c *Cache) loadOrCompileStandardConstraint( env *cel.Env, + setOneOf protoreflect.FieldDescriptor, constraintFieldDesc protoreflect.FieldDescriptor, ) (set expression.ASTSet, err error) { if cachedConstraint, ok := c.cache[constraintFieldDesc]; ok { return cachedConstraint, nil } - exprs := extensions.Resolve[*validate.PredefinedConstraints]( - constraintFieldDesc.Options(), - validate.E_Predefined, - ) - set, err = expression.CompileASTs(exprs.GetCel(), env) + exprs := expression.Expressions{ + Constraints: extensions.Resolve[*validate.PredefinedConstraints]( + constraintFieldDesc.Options(), + validate.E_Predefined, + ).GetCel(), + RulePath: []*validate.FieldPathElement{ + errors.FieldPathElement(setOneOf), + errors.FieldPathElement(constraintFieldDesc), + }, + } + set, err = expression.CompileASTs(exprs, env) if err != nil { return set, errors.NewCompilationErrorf( "failed to compile standard constraint %q: %w", diff --git a/internal/constraints/cache_test.go b/internal/constraints/cache_test.go index 533128c..42bb447 100644 --- a/internal/constraints/cache_test.go +++ b/internal/constraints/cache_test.go @@ -101,7 +101,7 @@ func TestCache_BuildStandardConstraints(t *testing.T) { require.NoError(t, err) c := NewCache() - set, err := c.Build(env, test.desc, test.cons, protoregistry.GlobalTypes, false, false, test.forItems) + set, err := c.Build(env, test.desc, test.cons, protoregistry.GlobalTypes, false, test.forItems) if test.exErr { assert.Error(t, err) } else { @@ -118,6 +118,8 @@ func TestCache_LoadOrCompileStandardConstraint(t *testing.T) { env, err := celext.DefaultEnv(false) require.NoError(t, err) + constraints := &validate.FieldConstraints{} + oneOfDesc := constraints.ProtoReflect().Descriptor().Oneofs().ByName("type").Fields().ByName("float") msg := &cases.FloatIn{} desc := getFieldDesc(t, msg, "val") require.NotNil(t, desc) @@ -126,7 +128,7 @@ func TestCache_LoadOrCompileStandardConstraint(t *testing.T) { _, ok := cache.cache[desc] assert.False(t, ok) - asts, err := cache.loadOrCompileStandardConstraint(env, desc) + asts, err := cache.loadOrCompileStandardConstraint(env, oneOfDesc, desc) require.NoError(t, err) assert.NotNil(t, asts) @@ -134,7 +136,7 @@ func TestCache_LoadOrCompileStandardConstraint(t *testing.T) { assert.True(t, ok) assert.Equal(t, cached, asts) - asts, err = cache.loadOrCompileStandardConstraint(env, desc) + asts, err = cache.loadOrCompileStandardConstraint(env, oneOfDesc, desc) require.NoError(t, err) assert.Equal(t, cached, asts) } diff --git a/internal/errors/fieldpath.go b/internal/errors/fieldpath.go deleted file mode 100644 index 33d58c6..0000000 --- a/internal/errors/fieldpath.go +++ /dev/null @@ -1,204 +0,0 @@ -// Copyright 2023-2024 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package errors - -import ( - "errors" - "fmt" - "strconv" - "strings" - - "google.golang.org/protobuf/proto" - "google.golang.org/protobuf/reflect/protoreflect" - "google.golang.org/protobuf/reflect/protoregistry" -) - -// getFieldValue returns the field value at a given path, using the provided -// registry to resolve extensions. -func getFieldValue( - registry protoregistry.ExtensionTypeResolver, - message proto.Message, - path string, -) (field protoreflect.Value, descriptor protoreflect.FieldDescriptor, err error) { - var name, subscript string - var atEnd, isExt bool - reflectMessage := message.ProtoReflect() - for !atEnd { - name, subscript, path, atEnd, isExt = parsePathElement(path) - if name == "" { - return protoreflect.Value{}, nil, errors.New("empty field name") - } - var descriptor protoreflect.FieldDescriptor - if isExt { - extension, err := registry.FindExtensionByName(protoreflect.FullName(name)) - if err != nil { - return protoreflect.Value{}, nil, fmt.Errorf("resolving extension: %w", err) - } - descriptor = extension.TypeDescriptor() - } else { - descriptor = reflectMessage.Descriptor().Fields().ByTextName(name) - } - if descriptor == nil { - return protoreflect.Value{}, nil, fmt.Errorf("field %s not found", name) - } - field = reflectMessage.Get(descriptor) - if subscript != "" { - descriptor, field, err = traverseSubscript(descriptor, subscript, field, name) - if err != nil { - return protoreflect.Value{}, nil, err - } - } else if descriptor.IsList() || descriptor.IsMap() { - if atEnd { - break - } - return protoreflect.Value{}, nil, fmt.Errorf("missing subscript on field %s", name) - } - if descriptor.Message() != nil { - reflectMessage = field.Message() - } - } - return field, descriptor, nil -} - -func traverseSubscript( - descriptor protoreflect.FieldDescriptor, - subscript string, - field protoreflect.Value, - name string, -) (protoreflect.FieldDescriptor, protoreflect.Value, error) { - switch { - case descriptor.IsList(): - i, err := strconv.Atoi(subscript) - if err != nil { - return nil, protoreflect.Value{}, fmt.Errorf("invalid list index: %s", subscript) - } - if !field.IsValid() || i >= field.List().Len() { - return nil, protoreflect.Value{}, fmt.Errorf("index %d out of bounds of field %s", i, name) - } - field = field.List().Get(i) - case descriptor.IsMap(): - key, err := parseMapKey(descriptor, subscript) - if err != nil { - return nil, protoreflect.Value{}, err - } - field = field.Map().Get(key) - if !field.IsValid() { - return nil, protoreflect.Value{}, fmt.Errorf("key %s not present on field %s", subscript, name) - } - descriptor = descriptor.MapValue() - default: - return nil, protoreflect.Value{}, fmt.Errorf("unexpected subscript on field %s", name) - } - return descriptor, field, nil -} - -func parseMapKey(mapDescriptor protoreflect.FieldDescriptor, subscript string) (protoreflect.MapKey, error) { - switch mapDescriptor.MapKey().Kind() { - case protoreflect.BoolKind: - if boolValue, err := strconv.ParseBool(subscript); err == nil { - return protoreflect.MapKey(protoreflect.ValueOfBool(boolValue)), nil - } - case protoreflect.Int32Kind, protoreflect.Sint32Kind, protoreflect.Sfixed32Kind: - if intValue, err := strconv.ParseInt(subscript, 10, 32); err == nil { - return protoreflect.MapKey(protoreflect.ValueOfInt32(int32(intValue))), nil - } - case protoreflect.Int64Kind, protoreflect.Sint64Kind, protoreflect.Sfixed64Kind: - if intValue, err := strconv.ParseInt(subscript, 10, 64); err == nil { - return protoreflect.MapKey(protoreflect.ValueOfInt64(intValue)), nil - } - case protoreflect.Uint32Kind, protoreflect.Fixed32Kind: - if intValue, err := strconv.ParseUint(subscript, 10, 32); err == nil { - return protoreflect.MapKey(protoreflect.ValueOfUint32(uint32(intValue))), nil - } - case protoreflect.Uint64Kind, protoreflect.Fixed64Kind: - if intValue, err := strconv.ParseUint(subscript, 10, 64); err == nil { - return protoreflect.MapKey(protoreflect.ValueOfUint64(intValue)), nil - } - case protoreflect.StringKind: - if stringValue, err := strconv.Unquote(subscript); err == nil { - return protoreflect.MapKey(protoreflect.ValueOfString(stringValue)), nil - } - case protoreflect.EnumKind, protoreflect.FloatKind, protoreflect.DoubleKind, - protoreflect.BytesKind, protoreflect.MessageKind, protoreflect.GroupKind: - fallthrough - default: - // This should not occur, but it might if the rules are relaxed in the - // future. - return protoreflect.MapKey{}, fmt.Errorf("unsupported map key type: %s", mapDescriptor.MapKey().Kind()) - } - return protoreflect.MapKey{}, fmt.Errorf("invalid map key: %s", subscript) -} - -// parsePathElement parses a single -func parsePathElement(path string) (name, subscript, rest string, atEnd bool, isExt bool) { - // Scan extension name. - if len(path) > 0 && path[0] == '[' { - if i := strings.IndexByte(path, ']'); i >= 0 { - isExt = true - name, path = path[1:i], path[i+1:] - } - } - // Scan field name. - if !isExt { - if i := strings.IndexAny(path, ".["); i >= 0 { - name, path = path[:i], path[i:] - } else { - name, path = path, "" - } - } - // No subscript: At end of path. - if len(path) == 0 { - return name, "", path, true, isExt - } - // No subscript: At end of path element. - if path[0] == '.' { - return name, "", path[1:], false, isExt - } - // Malformed subscript - if len(path) == 1 || path[1] == '.' { - name, path = name+path[:1], path[1:] - return name, "", path, true, isExt - } - switch path[1] { - case ']': - // Empty subscript - name, path = name+path[:2], path[2:] - case '`', '"', '\'': - // String subscript: must scan string. - var err error - subscript, err = strconv.QuotedPrefix(path[1:]) - if err == nil { - path = path[len(subscript)+2:] - } - default: - // Other subscript; can skip to next ] - if i := strings.IndexByte(path, ']'); i >= 0 { - subscript, path = path[1:i], path[i+1:] - } else { - // Unterminated subscript - return name + path, "", "", true, isExt - } - } - // No subscript: At end of path. - if len(path) == 0 { - return name, subscript, path, true, isExt - } - // No subscript: At end of path element. - if path[0] == '.' { - return name, subscript, path[1:], false, isExt - } - // Malformed element - return name, subscript, path, false, isExt -} diff --git a/internal/errors/fieldpath_test.go b/internal/errors/fieldpath_test.go deleted file mode 100644 index 0a9b0d2..0000000 --- a/internal/errors/fieldpath_test.go +++ /dev/null @@ -1,169 +0,0 @@ -// Copyright 2023-2024 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package errors - -import ( - "testing" - - pb "github.com/bufbuild/protovalidate-go/internal/gen/tests/example/v1" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "google.golang.org/protobuf/proto" - "google.golang.org/protobuf/reflect/protoregistry" -) - -func TestGetFieldValue(t *testing.T) { - t.Parallel() - - type ( - Simple = pb.FieldPathSimple - Nested = pb.FieldPathNested - Repeated = pb.FieldPathRepeated - Maps = pb.FieldPathMaps - ) - - withExtension := &Simple{} - proto.SetExtension(withExtension, pb.E_Ext, []uint64{10}) - - testGetFieldValueMatch(t, uint64(64), &Simple{Val: 64}, "val") - testGetFieldValueMatch(t, uint64(0), (*Simple)(nil), "val") - testGetFieldValueMatch(t, float64(1.0), &Simple{Nested: &Nested{Val: 1.0}}, "nested.val") - testGetFieldValueMatch(t, float64(0.0), (*Simple)(nil), "nested.val") - testGetFieldValueMatch(t, float64(0.0), &Simple{Nested: nil}, "nested.val") - testGetFieldValueMatch(t, int32(2), &Repeated{Val: []int32{1, 2, 3}}, "val[1]") - testGetFieldValueMatch(t, uint64(32), &Repeated{Msg: []*Simple{{Val: 64}, {Val: 32}}}, "msg[1].val") - testGetFieldValueMatch(t, int32(4), &Maps{Int32Int32Map: map[int32]int32{1: 2, 2: 4, 4: 8}}, "int32_int32_map[2]") - testGetFieldValueMatch(t, uint64(32), &Maps{Int32Map: map[int32]*Simple{1: {Val: 32}}}, "int32_map[1].val") - testGetFieldValueMatch(t, uint64(64), &Maps{Int64Map: map[int64]*Simple{2: {Val: 64}}}, "int64_map[2].val") - testGetFieldValueMatch(t, uint64(64), &Maps{Sint32Map: map[int32]*Simple{2: {Val: 64}}}, "sint32_map[2].val") - testGetFieldValueMatch(t, uint64(32), &Maps{Sint64Map: map[int64]*Simple{1: {Val: 32}}}, "sint64_map[1].val") - testGetFieldValueMatch(t, uint64(64), &Maps{Sfixed32Map: map[int32]*Simple{2: {Val: 64}}}, "sfixed32_map[2].val") - testGetFieldValueMatch(t, uint64(64), &Maps{Sfixed64Map: map[int64]*Simple{2: {Val: 64}}}, "sfixed64_map[2].val") - testGetFieldValueMatch(t, uint64(32), &Maps{Uint32Map: map[uint32]*Simple{1: {Val: 32}}}, "uint32_map[1].val") - testGetFieldValueMatch(t, uint64(32), &Maps{Uint64Map: map[uint64]*Simple{1: {Val: 32}}}, "uint64_map[1].val") - testGetFieldValueMatch(t, uint64(64), &Maps{Fixed32Map: map[uint32]*Simple{2: {Val: 64}}}, "fixed32_map[2].val") - testGetFieldValueMatch(t, uint64(64), &Maps{Fixed64Map: map[uint64]*Simple{2: {Val: 64}}}, "fixed64_map[2].val") - testGetFieldValueMatch(t, uint64(64), &Maps{StringMap: map[string]*Simple{"a": {Val: 64}}}, `string_map["a"].val`) - testGetFieldValueMatch(t, uint64(64), &Maps{StringMap: map[string]*Simple{`".[]][`: {Val: 64}}}, `string_map["\".[]]["].val`) - testGetFieldValueMatch(t, uint64(1), &Maps{BoolMap: map[bool]*Simple{true: {Val: 1}}}, "bool_map[true].val") - testGetFieldValueMatch(t, uint64(0), &Maps{Int32Map: map[int32]*Simple{1: nil}}, "int32_map[1].val") - testGetFieldValueMatch(t, uint64(10), withExtension, "[tests.example.v1.ext][0]") - testGetFieldValueMatch(t, uint64(10), &Repeated{Msg: []*Simple{withExtension}}, "msg[0].[tests.example.v1.ext][0]") - - testGetFieldValueError(t, "field nofield not found", &Simple{Val: 1}, "nofield") - testGetFieldValueError(t, "field nofield not found", &Simple{Val: 1}, "val.nofield") - testGetFieldValueError(t, "unexpected subscript on field val", &Simple{Val: 1}, "val[1]") - testGetFieldValueError(t, "empty field name", &Simple{Val: 1}, "nested.") - testGetFieldValueError(t, "empty field name", &Simple{Val: 1}, "nested..b") - testGetFieldValueError(t, "field ] not found", &Simple{Val: 1}, "]") - testGetFieldValueError(t, "field a] not found", &Simple{Val: 1}, "a]") - testGetFieldValueError(t, "field a[] not found", &Simple{Val: 1}, "a[]") - testGetFieldValueError(t, "field a not found", &Simple{Val: 1}, "a[1]") - testGetFieldValueError(t, "invalid list index: #", &Repeated{}, "val[#]") - testGetFieldValueError(t, "index 1 out of bounds of field val", &Repeated{}, "val[1]") - testGetFieldValueError(t, "index 1 out of bounds of field val", &Repeated{Val: []int32{1}}, "val[1]") - testGetFieldValueError(t, "missing subscript on field msg", &Repeated{}, "msg.val") - testGetFieldValueError(t, "key 2 not present on field int32_map", &Maps{Int32Map: map[int32]*Simple{1: {Val: 1}}}, "int32_map[2].val") - testGetFieldValueError(t, `missing subscript on field string_map`, &Maps{}, `string_map["not a string]`) - testGetFieldValueError(t, `invalid map key: #`, &Maps{}, "int32_map[#]") - testGetFieldValueError(t, `invalid map key: #`, &Maps{}, "int64_map[#]") - testGetFieldValueError(t, `invalid map key: #`, &Maps{}, "sint32_map[#]") - testGetFieldValueError(t, `invalid map key: #`, &Maps{}, "sint64_map[#]") - testGetFieldValueError(t, `invalid map key: #`, &Maps{}, "sfixed32_map[#]") - testGetFieldValueError(t, `invalid map key: #`, &Maps{}, "sfixed64_map[#]") - testGetFieldValueError(t, `invalid map key: #`, &Maps{}, "uint32_map[#]") - testGetFieldValueError(t, `invalid map key: #`, &Maps{}, "uint64_map[#]") - testGetFieldValueError(t, `invalid map key: #`, &Maps{}, "fixed32_map[#]") - testGetFieldValueError(t, `invalid map key: #`, &Maps{}, "fixed64_map[#]") - testGetFieldValueError(t, `invalid map key: #`, &Maps{}, "bool_map[#]") -} - -func BenchmarkGetFieldValue(b *testing.B) { - simpleMessage := &pb.FieldPathSimple{Val: 64} - nestedMessage := &pb.FieldPathSimple{ - Nested: &pb.FieldPathNested{Val: 1}, - } - repeatedMessage := &pb.FieldPathRepeated{ - Msg: []*pb.FieldPathSimple{{Val: 1}}, - } - mapMessage := &pb.FieldPathMaps{ - StringMap: map[string]*pb.FieldPathSimple{"abc": {Val: 1}}, - } - - b.Run("Simple", func(b *testing.B) { - b.ReportAllocs() - b.RunParallel(func(p *testing.PB) { - for p.Next() { - _, _, err := getFieldValue(protoregistry.GlobalTypes, simpleMessage, "val") - require.NoError(b, err) - } - }) - }) - - b.Run("Nested", func(b *testing.B) { - b.ReportAllocs() - b.RunParallel(func(p *testing.PB) { - for p.Next() { - _, _, err := getFieldValue(protoregistry.GlobalTypes, nestedMessage, "nested.val") - require.NoError(b, err) - } - }) - }) - - b.Run("Repeated", func(b *testing.B) { - b.ReportAllocs() - b.RunParallel(func(p *testing.PB) { - for p.Next() { - _, _, err := getFieldValue(protoregistry.GlobalTypes, repeatedMessage, "msg[0].val") - require.NoError(b, err) - } - }) - }) - - b.Run("Map", func(b *testing.B) { - b.ReportAllocs() - b.RunParallel(func(p *testing.PB) { - for p.Next() { - _, _, err := getFieldValue(protoregistry.GlobalTypes, mapMessage, `string_map["abc"].val`) - require.NoError(b, err) - } - }) - }) - - b.Run("Error", func(b *testing.B) { - b.ReportAllocs() - b.RunParallel(func(p *testing.PB) { - for p.Next() { - _, _, err := getFieldValue(protoregistry.GlobalTypes, mapMessage, `string_map["z"].val`) - require.Error(b, err) - } - }) - }) -} - -func testGetFieldValueMatch(t *testing.T, expected any, message proto.Message, path string) { - t.Helper() - - val, _, err := getFieldValue(protoregistry.GlobalTypes, message, path) - require.NoError(t, err) - assert.Equal(t, expected, val.Interface()) -} - -func testGetFieldValueError(t *testing.T, errString string, message proto.Message, path string) { - t.Helper() - - _, _, err := getFieldValue(protoregistry.GlobalTypes, message, path) - assert.EqualError(t, err, errString) -} diff --git a/internal/errors/utils.go b/internal/errors/utils.go index 6711a90..4826846 100644 --- a/internal/errors/utils.go +++ b/internal/errors/utils.go @@ -16,6 +16,14 @@ package errors import ( "errors" + "slices" + "strconv" + "strings" + + "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protoreflect" + "google.golang.org/protobuf/types/descriptorpb" ) // Merge is a utility to resolve and combine Errors resulting from @@ -47,20 +55,109 @@ func Merge(dst, src error, failFast bool) (ok bool, err error) { return !(failFast && len(dstValErrs.Violations) > 0), dst } -// PrefixErrorPaths prepends the formatted prefix to the violations of a -// ValidationErrors. -func PrefixErrorPaths(err error, format string, args ...any) { +func MarkForKey(err error) { var valErr *ValidationError if errors.As(err, &valErr) { - PrefixFieldPaths(valErr, format, args...) + for i := range valErr.Violations { + valErr.Violations[i].ForKey = true + } } } -func MarkForKey(err error) { +func FieldPathElement(field protoreflect.FieldDescriptor) *validate.FieldPathElement { + return &validate.FieldPathElement{ + FieldNumber: proto.Int32(int32(field.Number())), + FieldName: proto.String(field.TextName()), + FieldType: descriptorpb.FieldDescriptorProto_Type(field.Kind()).Enum(), + } +} + +// AppendFieldPath appends an element to the end of each field path in err. +// As an exception, if skipSubscript is true, any field paths ending in a +// subscript element will not have a suffix element appended to them. +// +// Note that this function is ordinarily used to append field paths in reverse +// order, as the stack bubbles up through the evaluators. Then, at the end, the +// path is reversed. +func AppendFieldPath(err error, suffix *validate.FieldPathElement, skipSubscript bool) { var valErr *ValidationError if errors.As(err, &valErr) { for i := range valErr.Violations { - valErr.Violations[i].ForKey = true + violation := &valErr.Violations[i] + // Special case: Here we skip appending if the last element had a + // subscript. This is a weird special case that makes it + // significantly simpler to handle reverse-constructing paths with + // maps and slices. + if skipSubscript && + len(violation.FieldPath.path) > 0 && + violation.FieldPath.path[len(violation.FieldPath.path)-1].Subscript != nil { + continue + } + violation.FieldPath.path = append(violation.FieldPath.path, suffix) + } + } +} + +// PrependRulePath prepends some elements to the beginning of each rule path in +// err. Note that unlike field paths, the rule path is not appended in reverse +// order. This is because rule paths are very often fixed, simple paths, so it +// is better to avoid the copy instead if possible. This prepend is only used in +// the error case for nested rules (repeated.items, map.keys, map.values.) +func PrependRulePath(err error, prefix []*validate.FieldPathElement) { + var valErr *ValidationError + if errors.As(err, &valErr) { + for i := range valErr.Violations { + valErr.Violations[i].RulePath.path = append( + append([]*validate.FieldPathElement{}, prefix...), + valErr.Violations[i].RulePath.path..., + ) + } + } +} + +// ReverseFieldPaths reverses all field paths in the error. +func ReverseFieldPaths(err error) { + var valErr *ValidationError + if errors.As(err, &valErr) { + for i := range valErr.Violations { + violation := &valErr.Violations[i] + slices.Reverse(violation.FieldPath.path) + } + } +} + +// FieldPathString takes a FieldPath and encodes it to a string-based dotted +// field path. +func FieldPathString(path []*validate.FieldPathElement) string { + var result strings.Builder + if path == nil { + return "" + } + for i, element := range path { + if i > 0 { + result.WriteByte('.') + } + result.WriteString(element.GetFieldName()) + subscript := element.GetSubscript() + if subscript == nil { + continue + } + result.WriteByte('[') + switch value := subscript.(type) { + case *validate.FieldPathElement_Index: + result.WriteString(strconv.FormatUint(value.Index, 10)) + case *validate.FieldPathElement_BoolKey: + result.WriteString(strconv.FormatBool(value.BoolKey)) + case *validate.FieldPathElement_IntKey: + result.WriteString(strconv.FormatInt(value.IntKey, 10)) + case *validate.FieldPathElement_SintKey: + result.WriteString(strconv.FormatInt(value.SintKey, 10)) + case *validate.FieldPathElement_UintKey: + result.WriteString(strconv.FormatUint(value.UintKey, 10)) + case *validate.FieldPathElement_StringKey: + result.WriteString(strconv.Quote(value.StringKey)) } + result.WriteByte(']') } + return result.String() } diff --git a/internal/errors/validation.go b/internal/errors/validation.go index 756d166..d281372 100644 --- a/internal/errors/validation.go +++ b/internal/errors/validation.go @@ -22,24 +22,23 @@ import ( "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/reflect/protoreflect" - "google.golang.org/protobuf/reflect/protoregistry" ) // Violation represents a single instance where a validation rule was not met. // It provides information about the field that caused the violation, the // specific unfulfilled constraint, and a human-readable error message. type Violation struct { - // FieldPath is a machine-readable identifier that points to the specific - // field that failed the validation. This could be a nested field, in which - // case the path will include all the parent fields leading to the actual - // field that caused the violation. - FieldPath string + // FieldPath is an identifier that points to the specific field + // that failed the validation. This could be a nested field, in which case + // the path will include all the parent fields leading to the actual field + // that caused the violation. + FieldPath FieldPath // RulePath is a machine-readable identifier that points to the specific // constraint rule that failed validation. This will be a nested field // starting from the FieldConstraints of the field that failed validation. - // This value is only present for standard or predefined rules on fields. - RulePath string + // This value is only set for standard or predefined rules on fields. + RulePath FieldPath // FieldValue is the value of the specific field that failed validation. FieldValue protoreflect.Value @@ -71,55 +70,35 @@ type ValidationError struct { Violations []Violation } -// FromProto converts the proto.Message form of the error back into native form. -func FromProto( - registry protoregistry.ExtensionTypeResolver, - message proto.Message, - violations *validate.Violations, -) (*ValidationError, error) { - valErr := &ValidationError{ - Violations: make([]Violation, len(violations.GetViolations())), - } - for i, violation := range violations.GetViolations() { - valErr.Violations[i] = Violation{ - FieldPath: violation.GetFieldPath(), - RulePath: violation.GetRulePath(), - ConstraintID: violation.GetConstraintId(), - Message: violation.GetMessage(), - ForKey: violation.GetForKey(), - } - if valErr.Violations[i].FieldPath == "" { - continue - } - fieldValue, descriptor, err := getFieldValue(registry, message, violation.GetFieldPath()) - if err != nil { - return nil, err - } - valErr.Violations[i].FieldValue = fieldValue - if valErr.Violations[i].RulePath == "" { - continue - } - ruleValue, _, err := getFieldValue(registry, descriptor.Options(), valErr.Violations[i].RulePath) - if err != nil { - return nil, err - } - valErr.Violations[i].RuleValue = ruleValue - } - return valErr, nil -} - // ToProto converts this error into its proto.Message form. func (err *ValidationError) ToProto() *validate.Violations { violations := &validate.Violations{ Violations: make([]*validate.Violation, len(err.Violations)), } for i, violation := range err.Violations { + var fieldPath *validate.FieldPath + if len(violation.FieldPath.path) > 0 { + fieldPath = violation.FieldPath.ToProto() + } + var rulePath *validate.FieldPath + if len(violation.RulePath.path) > 0 { + rulePath = violation.RulePath.ToProto() + } + var fieldPathString *string + if fieldPath != nil { + fieldPathString = proto.String(FieldPathString(fieldPath.GetElements())) + } + var forKey *bool + if violation.ForKey { + forKey = proto.Bool(true) + } violations.Violations[i] = &validate.Violation{ - FieldPath: proto.String(violation.FieldPath), - RulePath: proto.String(violation.RulePath), - ConstraintId: proto.String(violation.ConstraintID), - Message: proto.String(violation.Message), - ForKey: proto.Bool(violation.ForKey), + FieldPathString: fieldPathString, + FieldPath: fieldPath, + RulePath: rulePath, + ConstraintId: proto.String(violation.ConstraintID), + Message: proto.String(violation.Message), + ForKey: forKey, } } return violations @@ -130,7 +109,7 @@ func (err *ValidationError) Error() string { bldr.WriteString("validation error:") for _, violation := range err.Violations { bldr.WriteString("\n - ") - if fieldPath := violation.FieldPath; fieldPath != "" { + if fieldPath := violation.FieldPath.String(); fieldPath != "" { bldr.WriteString(fieldPath) bldr.WriteString(": ") } @@ -141,40 +120,25 @@ func (err *ValidationError) Error() string { return bldr.String() } -// PrefixFieldPaths prepends to the provided prefix to the error's internal -// field paths. -func PrefixFieldPaths(err *ValidationError, format string, args ...any) { - prefix := fmt.Sprintf(format, args...) - for i := range err.Violations { - violation := &err.Violations[i] - switch { - case violation.FieldPath == "": // no existing field path - violation.FieldPath = prefix - case violation.FieldPath[0] == '[': // field is a map/list - violation.FieldPath = prefix + violation.FieldPath - default: // any other field - violation.FieldPath = fmt.Sprintf("%s.%s", prefix, violation.FieldPath) - } - } +// A FieldPath specifies a nested field inside of a protobuf message. +type FieldPath struct { + path []*validate.FieldPathElement } -// PrefixRulePaths prepends to the provided prefix to the error's internal -// rule paths. -func PrefixRulePaths(err *ValidationError, format string, args ...any) { - prefix := fmt.Sprintf(format, args...) - for i := range err.Violations { - violation := &err.Violations[i] - switch { - case violation.RulePath == "": // no existing rule path - violation.RulePath = prefix - case violation.RulePath[0] == '[': // rule is a map/list - violation.RulePath = prefix + violation.RulePath - default: // any other rule - violation.RulePath = fmt.Sprintf("%s.%s", prefix, violation.RulePath) - } +func NewFieldPath(elements []*validate.FieldPathElement) FieldPath { + return FieldPath{path: elements} +} + +func (f FieldPath) ToProto() *validate.FieldPath { + return &validate.FieldPath{ + Elements: f.path, } } +func (f FieldPath) String() string { + return FieldPathString(f.path) +} + // EqualViolations returns true if the underlying violations are equal. func EqualViolations(a, b []Violation) bool { return slices.EqualFunc(a, b, EqualViolation) @@ -182,5 +146,9 @@ func EqualViolations(a, b []Violation) bool { // EqualViolation returns true if the underlying violations are equal. func EqualViolation(a, b Violation) bool { - return a.FieldPath == b.FieldPath && a.ConstraintID == b.ConstraintID && a.Message == b.Message && a.ForKey == b.ForKey + return (a.ConstraintID == b.ConstraintID && + a.Message == b.Message && + a.ForKey == b.ForKey && + proto.Equal(a.FieldPath.ToProto(), b.FieldPath.ToProto()) && + proto.Equal(a.RulePath.ToProto(), b.RulePath.ToProto())) } diff --git a/internal/errors/validation_test.go b/internal/errors/validation_test.go index 53ca8b4..eb5ebd3 100644 --- a/internal/errors/validation_test.go +++ b/internal/errors/validation_test.go @@ -13,60 +13,3 @@ // limitations under the License. package errors - -import ( - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestPrefixFieldPaths(t *testing.T) { - t.Parallel() - - tests := []struct { - fieldPath string - format string - args []any - expected string - }{ - { - "", - "%s", - []any{"foo"}, - "foo", - }, - { - "bar", - "%s", - []any{"foo"}, - "foo.bar", - }, - { - "bar", - "[%d]", - []any{3}, - "[3].bar", - }, - { - "[3].bar", - "%s", - []any{"foo"}, - "foo[3].bar", - }, - } - - for _, tc := range tests { - test := tc - t.Run(test.expected, func(t *testing.T) { - t.Parallel() - err := &ValidationError{Violations: []Violation{ - {FieldPath: test.fieldPath}, - {FieldPath: test.fieldPath}, - }} - PrefixFieldPaths(err, test.format, test.args...) - for _, v := range err.Violations { - assert.Equal(t, test.expected, v.FieldPath) - } - }) - } -} diff --git a/internal/evaluator/any.go b/internal/evaluator/any.go index 3ad69df..4865a28 100644 --- a/internal/evaluator/any.go +++ b/internal/evaluator/any.go @@ -15,8 +15,23 @@ package evaluator import ( + "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" "github.com/bufbuild/protovalidate-go/internal/errors" + "google.golang.org/protobuf/proto" "google.golang.org/protobuf/reflect/protoreflect" + "google.golang.org/protobuf/types/descriptorpb" +) + +//nolint:gochecknoglobals +var ( + anyInRulePath = []*validate.FieldPathElement{ + {FieldName: proto.String("any"), FieldNumber: proto.Int32(20), FieldType: descriptorpb.FieldDescriptorProto_Type(11).Enum()}, + {FieldName: proto.String("in"), FieldNumber: proto.Int32(2), FieldType: descriptorpb.FieldDescriptorProto_Type(9).Enum()}, + } + anyNotInRulePath = []*validate.FieldPathElement{ + {FieldName: proto.String("any"), FieldNumber: proto.Int32(20), FieldType: descriptorpb.FieldDescriptorProto_Type(11).Enum()}, + {FieldName: proto.String("not_in"), FieldNumber: proto.Int32(3), FieldType: descriptorpb.FieldDescriptorProto_Type(9).Enum()}, + } ) // anyPB is a specialized evaluator for applying validate.AnyRules to an @@ -30,30 +45,17 @@ type anyPB struct { In map[string]struct{} // NotIn specifies which type URLs the value may not possess NotIn map[string]struct{} - // Whether the evaluator applies to either map keys or map items. - ForMap bool - // Whether the evaluator applies to items of a map or repeated field. - ForItems bool } func (a anyPB) Evaluate(val protoreflect.Value, failFast bool) error { typeURL := val.Message().Get(a.TypeURLDescriptor).String() - rulePathPrefix := "" - if a.ForMap && a.ForItems { - rulePathPrefix += "map.values." - } else if a.ForMap && !a.ForItems { - rulePathPrefix += "map.keys." - } else if a.ForItems { - rulePathPrefix += "repeated.items." - } - err := &errors.ValidationError{} if len(a.In) > 0 { if _, ok := a.In[typeURL]; !ok { err.Violations = append(err.Violations, errors.Violation{ + RulePath: errors.NewFieldPath(anyInRulePath), ConstraintID: "any.in", - RulePath: rulePathPrefix + "any.in", Message: "type URL must be in the allow list", }) if failFast { @@ -65,8 +67,8 @@ func (a anyPB) Evaluate(val protoreflect.Value, failFast bool) error { if len(a.NotIn) > 0 { if _, ok := a.NotIn[typeURL]; ok { err.Violations = append(err.Violations, errors.Violation{ + RulePath: errors.NewFieldPath(anyNotInRulePath), ConstraintID: "any.not_in", - RulePath: rulePathPrefix + "any.not_in", Message: "type URL must not be in the block list", }) } diff --git a/internal/evaluator/builder.go b/internal/evaluator/builder.go index 00aeb57..ba0f51b 100644 --- a/internal/evaluator/builder.go +++ b/internal/evaluator/builder.go @@ -154,8 +154,11 @@ func (bldr *Builder) processMessageExpressions( msgEval *message, _ MessageCache, ) { + exprs := expression.Expressions{ + Constraints: msgConstraints.GetCel(), + } compiledExprs, err := expression.Compile( - msgConstraints.GetCel(), + exprs, bldr.env, cel.Types(dynamicpb.NewMessage(desc)), cel.Variable("this", cel.ObjectType(string(desc.FullName()))), @@ -221,23 +224,21 @@ func (bldr *Builder) buildField( if fld.IgnoreDefault { fld.Zero = bldr.zeroValue(fieldDescriptor, false) } - err := bldr.buildValue(fieldDescriptor, fieldConstraints, false, false, &fld.Value, cache) + err := bldr.buildValue(fieldDescriptor, fieldConstraints, nil, &fld.Value, cache) return fld, err } func (bldr *Builder) buildValue( fdesc protoreflect.FieldDescriptor, constraints *validate.FieldConstraints, - forMap bool, - forItems bool, + itemsWrapper wrapper, valEval *value, cache MessageCache, ) (err error) { steps := []func( fdesc protoreflect.FieldDescriptor, fieldConstraints *validate.FieldConstraints, - forMap bool, - forItems bool, + itemsWrapper wrapper, valEval *value, cache MessageCache, ) error{ @@ -253,7 +254,7 @@ func (bldr *Builder) buildValue( } for _, step := range steps { - if err = step(fdesc, constraints, forMap, forItems, valEval, cache); err != nil { + if err = step(fdesc, constraints, itemsWrapper, valEval, cache); err != nil { return err } } @@ -263,16 +264,15 @@ func (bldr *Builder) buildValue( func (bldr *Builder) processIgnoreEmpty( fdesc protoreflect.FieldDescriptor, constraints *validate.FieldConstraints, - forMap bool, - forItems bool, + itemsWrapper wrapper, val *value, _ MessageCache, ) error { // the only time we need to ignore empty on a value is if it's evaluating a // field item (repeated element or map key/value). - val.IgnoreEmpty = (forMap || forItems) && bldr.shouldIgnoreEmpty(constraints) + val.IgnoreEmpty = itemsWrapper != nil && bldr.shouldIgnoreEmpty(constraints) if val.IgnoreEmpty { - val.Zero = bldr.zeroValue(fdesc, forItems) + val.Zero = bldr.zeroValue(fdesc, itemsWrapper != nil) } return nil } @@ -280,17 +280,15 @@ func (bldr *Builder) processIgnoreEmpty( func (bldr *Builder) processFieldExpressions( fieldDesc protoreflect.FieldDescriptor, fieldConstraints *validate.FieldConstraints, - forMap bool, - forItems bool, + itemsWrapper wrapper, eval *value, _ MessageCache, ) error { - exprs := fieldConstraints.GetCel() - if len(exprs) == 0 { - return nil + exprs := expression.Expressions{ + Constraints: fieldConstraints.GetCel(), } - celTyp := celext.ProtoFieldToCELType(fieldDesc, false, forItems) + celTyp := celext.ProtoFieldToCELType(fieldDesc, false, itemsWrapper != nil) opts := append( celext.RequiredCELEnvOptions(fieldDesc), cel.Variable("this", celTyp), @@ -308,15 +306,14 @@ func (bldr *Builder) processFieldExpressions( func (bldr *Builder) processEmbeddedMessage( fdesc protoreflect.FieldDescriptor, rules *validate.FieldConstraints, - forMap bool, - forItems bool, + itemsWrapper wrapper, valEval *value, cache MessageCache, ) error { if !isMessageField(fdesc) || bldr.shouldSkip(rules) || fdesc.IsMap() || - (fdesc.IsList() && !forItems) { + (fdesc.IsList() && itemsWrapper == nil) { return nil } @@ -326,7 +323,7 @@ func (bldr *Builder) processEmbeddedMessage( "failed to compile embedded type %s for %s: %w", fdesc.Message().FullName(), fdesc.FullName(), err) } - valEval.Append(embedEval) + appendEvaluator(valEval, embedEval, nil) return nil } @@ -334,15 +331,14 @@ func (bldr *Builder) processEmbeddedMessage( func (bldr *Builder) processWrapperConstraints( fdesc protoreflect.FieldDescriptor, rules *validate.FieldConstraints, - forMap bool, - forItems bool, + itemsWrapper wrapper, valEval *value, cache MessageCache, ) error { if !isMessageField(fdesc) || bldr.shouldSkip(rules) || fdesc.IsMap() || - (fdesc.IsList() && !forItems) { + (fdesc.IsList() && itemsWrapper == nil) { return nil } @@ -351,19 +347,18 @@ func (bldr *Builder) processWrapperConstraints( return nil } var unwrapped value - err := bldr.buildValue(fdesc.Message().Fields().ByName("value"), rules, forMap, forItems, &unwrapped, cache) + err := bldr.buildValue(fdesc.Message().Fields().ByName("value"), rules, nil, &unwrapped, cache) if err != nil { return err } - valEval.Append(unwrapped.Constraints) + appendEvaluator(valEval, unwrapped.Constraints, itemsWrapper) return nil } func (bldr *Builder) processStandardConstraints( fdesc protoreflect.FieldDescriptor, constraints *validate.FieldConstraints, - forMap bool, - forItems bool, + itemsWrapper wrapper, valEval *value, _ MessageCache, ) error { @@ -373,25 +368,23 @@ func (bldr *Builder) processStandardConstraints( constraints, bldr.extensionTypeResolver, bldr.allowUnknownFields, - forMap, - forItems, + itemsWrapper != nil, ) if err != nil { return err } - valEval.Append(celPrograms(stdConstraints)) + appendEvaluator(valEval, celPrograms(stdConstraints), itemsWrapper) return nil } func (bldr *Builder) processAnyConstraints( fdesc protoreflect.FieldDescriptor, fieldConstraints *validate.FieldConstraints, - forMap bool, - forItems bool, + itemsWrapper wrapper, valEval *value, _ MessageCache, ) error { - if (fdesc.IsList() && !forItems) || + if (fdesc.IsList() && itemsWrapper == nil) || !isMessageField(fdesc) || fdesc.Message().FullName() != "google.protobuf.Any" { return nil @@ -402,18 +395,15 @@ func (bldr *Builder) processAnyConstraints( TypeURLDescriptor: typeURLDesc, In: stringsToSet(fieldConstraints.GetAny().GetIn()), NotIn: stringsToSet(fieldConstraints.GetAny().GetNotIn()), - ForMap: forMap, - ForItems: forItems, } - valEval.Append(anyEval) + appendEvaluator(valEval, anyEval, itemsWrapper) return nil } func (bldr *Builder) processEnumConstraints( fdesc protoreflect.FieldDescriptor, fieldConstraints *validate.FieldConstraints, - forMap bool, - forItems bool, + itemsWrapper wrapper, valEval *value, _ MessageCache, ) error { @@ -421,11 +411,9 @@ func (bldr *Builder) processEnumConstraints( return nil } if fieldConstraints.GetEnum().GetDefinedOnly() { - valEval.Append(definedEnum{ + appendEvaluator(valEval, definedEnum{ ValueDescriptors: fdesc.Enum().Values(), - ForMap: forMap, - ForItems: forItems, - }) + }, itemsWrapper) } return nil } @@ -433,8 +421,7 @@ func (bldr *Builder) processEnumConstraints( func (bldr *Builder) processMapConstraints( fieldDesc protoreflect.FieldDescriptor, constraints *validate.FieldConstraints, - _ bool, - _ bool, + itemsWrapper wrapper, valEval *value, cache MessageCache, ) error { @@ -442,13 +429,14 @@ func (bldr *Builder) processMapConstraints( return nil } - var mapEval kvPairs + mapEval := kvPairs{ + Descriptor: fieldDesc, + } err := bldr.buildValue( fieldDesc.MapKey(), constraints.GetMap().GetKeys(), - true, - false, + newKeysWrapper, &mapEval.KeyConstraints, cache) if err != nil { @@ -460,8 +448,7 @@ func (bldr *Builder) processMapConstraints( err = bldr.buildValue( fieldDesc.MapValue(), constraints.GetMap().GetValues(), - true, - true, + newValuesWrapper, &mapEval.ValueConstraints, cache) if err != nil { @@ -470,24 +457,26 @@ func (bldr *Builder) processMapConstraints( fieldDesc.FullName(), err) } - valEval.Append(mapEval) + appendEvaluator(valEval, mapEval, itemsWrapper) return nil } func (bldr *Builder) processRepeatedConstraints( fdesc protoreflect.FieldDescriptor, fieldConstraints *validate.FieldConstraints, - forMap bool, - forItems bool, + itemsWrapper wrapper, valEval *value, cache MessageCache, ) error { - if !fdesc.IsList() || forItems { + if !fdesc.IsList() || itemsWrapper != nil { return nil } - var listEval listItems - err := bldr.buildValue(fdesc, fieldConstraints.GetRepeated().GetItems(), false, true, &listEval.ItemConstraints, cache) + listEval := listItems{ + Descriptor: fdesc, + } + + err := bldr.buildValue(fdesc, fieldConstraints.GetRepeated().GetItems(), newItemsWrapper, &listEval.ItemConstraints, cache) if err != nil { return errors.NewCompilationErrorf( "failed to compile items constraints for repeated %v: %w", fdesc.FullName(), err) @@ -539,6 +528,13 @@ func (c MessageCache) SyncTo(other MessageCache) { } } +func appendEvaluator(value *value, evaluator evaluator, wrapper wrapper) { + if wrapper != nil { + evaluator = wrapper(evaluator) + } + value.Append(evaluator) +} + // isMessageField returns true if the field descriptor fdesc describes a field // containing a submessage. Although they are represented differently on the // wire, group fields are treated like message fields in protoreflect and have diff --git a/internal/evaluator/enum.go b/internal/evaluator/enum.go index d4efb64..6c91f09 100644 --- a/internal/evaluator/enum.go +++ b/internal/evaluator/enum.go @@ -15,35 +15,32 @@ package evaluator import ( + "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" "github.com/bufbuild/protovalidate-go/internal/errors" + "google.golang.org/protobuf/proto" "google.golang.org/protobuf/reflect/protoreflect" + "google.golang.org/protobuf/types/descriptorpb" ) +//nolint:gochecknoglobals +var enumDefinedOnlyRulePath = []*validate.FieldPathElement{ + {FieldName: proto.String("enum"), FieldNumber: proto.Int32(16), FieldType: descriptorpb.FieldDescriptorProto_Type(11).Enum()}, + {FieldName: proto.String("defined_only"), FieldNumber: proto.Int32(2), FieldType: descriptorpb.FieldDescriptorProto_Type(8).Enum()}, +} + // definedEnum is an evaluator that checks an enum value being a member of // the defined values exclusively. This check is handled outside CEL as enums // are completely type erased to integers. type definedEnum struct { // ValueDescriptors captures all the defined values for this enum ValueDescriptors protoreflect.EnumValueDescriptors - // Whether the evaluator applies to either map keys or map items. - ForMap bool - // Whether the evaluator applies to items of a map or repeated field. - ForItems bool } func (d definedEnum) Evaluate(val protoreflect.Value, _ bool) error { - rulePathPrefix := "" - if d.ForMap && d.ForItems { - rulePathPrefix += "map.values." - } else if d.ForMap && !d.ForItems { - rulePathPrefix += "map.keys." - } else if d.ForItems { - rulePathPrefix += "repeated.items." - } if d.ValueDescriptors.ByNumber(val.Enum()) == nil { return &errors.ValidationError{Violations: []errors.Violation{{ + RulePath: errors.NewFieldPath(enumDefinedOnlyRulePath), ConstraintID: "enum.defined_only", - RulePath: rulePathPrefix + "enum.defined_only", Message: "value must be one of the defined enum values", }}} } diff --git a/internal/evaluator/evaluator.go b/internal/evaluator/evaluator.go index 132e095..359523d 100644 --- a/internal/evaluator/evaluator.go +++ b/internal/evaluator/evaluator.go @@ -102,6 +102,10 @@ func (m messageEvaluators) Tautology() bool { return true } +// wrapper wraps an evaluator, used to handle some logic that applies +// specifically to recursive constraint rules. +type wrapper func(evaluator) evaluator + var ( _ evaluator = evaluators(nil) _ MessageEvaluator = messageEvaluators(nil) diff --git a/internal/evaluator/field.go b/internal/evaluator/field.go index 24e7789..5185ff8 100644 --- a/internal/evaluator/field.go +++ b/internal/evaluator/field.go @@ -15,10 +15,18 @@ package evaluator import ( + "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" "github.com/bufbuild/protovalidate-go/internal/errors" + "google.golang.org/protobuf/proto" "google.golang.org/protobuf/reflect/protoreflect" + "google.golang.org/protobuf/types/descriptorpb" ) +//nolint:gochecknoglobals +var requiredRulePath = []*validate.FieldPathElement{ + {FieldName: proto.String("required"), FieldNumber: proto.Int32(25), FieldType: descriptorpb.FieldDescriptorProto_Type(8).Enum()}, +} + // field performs validation on a single message field, defined by its // descriptor. type field struct { @@ -37,10 +45,6 @@ type field struct { IgnoreDefault bool // Zero is the default or zero-value for this value's type Zero protoreflect.Value - // Whether the evaluator applies to either map keys or map items. - ForMap bool - // Whether the evaluator applies to items of a map or repeated field. - ForItems bool } func (f field) Evaluate(val protoreflect.Value, failFast bool) error { @@ -49,12 +53,15 @@ func (f field) Evaluate(val protoreflect.Value, failFast bool) error { func (f field) EvaluateMessage(msg protoreflect.Message, failFast bool) (err error) { if f.Required && !msg.Has(f.Descriptor) { - return &errors.ValidationError{Violations: []errors.Violation{{ - FieldPath: string(f.Descriptor.Name()), + err := &errors.ValidationError{Violations: []errors.Violation{{ + FieldPath: errors.NewFieldPath([]*validate.FieldPathElement{ + errors.FieldPathElement(f.Descriptor), + }), + RulePath: errors.NewFieldPath(requiredRulePath), ConstraintID: "required", - RulePath: "required", Message: "value is required", }}} + return err } if f.IgnoreEmpty && !msg.Has(f.Descriptor) { @@ -66,7 +73,11 @@ func (f field) EvaluateMessage(msg protoreflect.Message, failFast bool) (err err return nil } if err = f.Value.Evaluate(val, failFast); err != nil { - errors.PrefixErrorPaths(err, "%s", f.Descriptor.Name()) + errors.AppendFieldPath( + err, + errors.FieldPathElement(f.Descriptor), + f.Descriptor.Cardinality() == protoreflect.Repeated, + ) } return err } diff --git a/internal/evaluator/map.go b/internal/evaluator/map.go index 5fc51cf..168252f 100644 --- a/internal/evaluator/map.go +++ b/internal/evaluator/map.go @@ -18,12 +18,29 @@ import ( "fmt" "strconv" + "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" "github.com/bufbuild/protovalidate-go/internal/errors" + "google.golang.org/protobuf/proto" "google.golang.org/protobuf/reflect/protoreflect" + "google.golang.org/protobuf/types/descriptorpb" +) + +//nolint:gochecknoglobals +var ( + mapKeysRulePath = []*validate.FieldPathElement{ + {FieldName: proto.String("map"), FieldNumber: proto.Int32(19), FieldType: descriptorpb.FieldDescriptorProto_Type(11).Enum()}, + {FieldName: proto.String("keys"), FieldNumber: proto.Int32(4), FieldType: descriptorpb.FieldDescriptorProto_Type(11).Enum()}, + } + mapValuesRulePath = []*validate.FieldPathElement{ + {FieldName: proto.String("map"), FieldNumber: proto.Int32(19), FieldType: descriptorpb.FieldDescriptorProto_Type(11).Enum()}, + {FieldName: proto.String("values"), FieldNumber: proto.Int32(5), FieldType: descriptorpb.FieldDescriptorProto_Type(11).Enum()}, + } ) // kvPairs performs validation on a map field's KV Pairs. type kvPairs struct { + // Descriptor is the FieldDescriptor targeted by this evaluator + Descriptor protoreflect.FieldDescriptor // KeyConstraints are checked on the map keys KeyConstraints value // ValueConstraints are checked on the map values @@ -35,7 +52,29 @@ func (m kvPairs) Evaluate(val protoreflect.Value, failFast bool) (err error) { val.Map().Range(func(key protoreflect.MapKey, value protoreflect.Value) bool { evalErr := m.evalPairs(key, value, failFast) if evalErr != nil { - errors.PrefixErrorPaths(evalErr, "[%s]", m.formatKey(key.Interface())) + element := errors.FieldPathElement(m.Descriptor) + switch m.Descriptor.MapKey().Kind() { + case protoreflect.BoolKind: + element.Subscript = &validate.FieldPathElement_BoolKey{BoolKey: key.Bool()} + case protoreflect.Int32Kind, protoreflect.Int64Kind, + protoreflect.Sfixed32Kind, protoreflect.Sfixed64Kind: + element.Subscript = &validate.FieldPathElement_IntKey{IntKey: key.Int()} + case protoreflect.Sint32Kind, protoreflect.Sint64Kind: + element.Subscript = &validate.FieldPathElement_SintKey{SintKey: key.Int()} + case protoreflect.Uint32Kind, protoreflect.Uint64Kind, + protoreflect.Fixed32Kind, protoreflect.Fixed64Kind: + element.Subscript = &validate.FieldPathElement_UintKey{UintKey: key.Uint()} + case protoreflect.StringKind: + element.Subscript = &validate.FieldPathElement_StringKey{StringKey: key.String()} + case protoreflect.EnumKind, protoreflect.FloatKind, protoreflect.DoubleKind, + protoreflect.BytesKind, protoreflect.MessageKind, protoreflect.GroupKind: + err = errors.NewCompilationErrorf( + "unexpected map key type %s", + m.Descriptor.MapKey().Kind(), + ) + return false + } + errors.AppendFieldPath(evalErr, element, false) } ok, err = errors.Merge(err, evalErr, failFast) return ok @@ -70,4 +109,36 @@ func (m kvPairs) formatKey(key any) string { } } -var _ evaluator = kvPairs{} +// keysWrapper wraps the evaluation of nested map key rules. +type keysWrapper struct { + evaluator +} + +func newKeysWrapper(evaluator evaluator) evaluator { return keysWrapper{evaluator} } + +func (e keysWrapper) Evaluate(val protoreflect.Value, failFast bool) error { + err := e.evaluator.Evaluate(val, failFast) + errors.PrependRulePath(err, mapKeysRulePath) + return err +} + +// valuesWrapper wraps the evaluation of nested map value rules. +type valuesWrapper struct { + evaluator +} + +func newValuesWrapper(evaluator evaluator) evaluator { return valuesWrapper{evaluator} } + +func (e valuesWrapper) Evaluate(val protoreflect.Value, failFast bool) error { + err := e.evaluator.Evaluate(val, failFast) + errors.PrependRulePath(err, mapValuesRulePath) + return err +} + +var ( + _ evaluator = kvPairs{} + _ evaluator = keysWrapper{} + _ wrapper = newKeysWrapper + _ evaluator = valuesWrapper{} + _ wrapper = newValuesWrapper +) diff --git a/internal/evaluator/oneof.go b/internal/evaluator/oneof.go index 4c326f0..413695b 100644 --- a/internal/evaluator/oneof.go +++ b/internal/evaluator/oneof.go @@ -15,7 +15,9 @@ package evaluator import ( + "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" "github.com/bufbuild/protovalidate-go/internal/errors" + "google.golang.org/protobuf/proto" "google.golang.org/protobuf/reflect/protoreflect" ) @@ -34,9 +36,10 @@ func (o oneof) Evaluate(val protoreflect.Value, failFast bool) error { func (o oneof) EvaluateMessage(msg protoreflect.Message, _ bool) error { if o.Required && msg.WhichOneof(o.Descriptor) == nil { return &errors.ValidationError{Violations: []errors.Violation{{ - FieldPath: string(o.Descriptor.Name()), + FieldPath: errors.NewFieldPath([]*validate.FieldPathElement{{ + FieldName: proto.String(string(o.Descriptor.Name())), + }}), ConstraintID: "required", - RulePath: "required", Message: "exactly one field is required in oneof", }}} } diff --git a/internal/evaluator/repeated.go b/internal/evaluator/repeated.go index 14973bf..4a59e98 100644 --- a/internal/evaluator/repeated.go +++ b/internal/evaluator/repeated.go @@ -15,12 +15,23 @@ package evaluator import ( + "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" "github.com/bufbuild/protovalidate-go/internal/errors" + "google.golang.org/protobuf/proto" "google.golang.org/protobuf/reflect/protoreflect" + "google.golang.org/protobuf/types/descriptorpb" ) +//nolint:gochecknoglobals +var repeatedItemsFieldPath = []*validate.FieldPathElement{ + {FieldName: proto.String("repeated"), FieldNumber: proto.Int32(18), FieldType: descriptorpb.FieldDescriptorProto_Type(11).Enum()}, + {FieldName: proto.String("items"), FieldNumber: proto.Int32(4), FieldType: descriptorpb.FieldDescriptorProto_Type(11).Enum()}, +} + // listItems performs validation on the elements of a repeated field. type listItems struct { + // Descriptor is the FieldDescriptor targeted by this evaluator + Descriptor protoreflect.FieldDescriptor // ItemConstraints are checked on every item of the list ItemConstraints value } @@ -32,7 +43,9 @@ func (r listItems) Evaluate(val protoreflect.Value, failFast bool) error { for i := 0; i < list.Len(); i++ { itemErr := r.ItemConstraints.Evaluate(list.Get(i), failFast) if itemErr != nil { - errors.PrefixErrorPaths(itemErr, "[%d]", i) + element := errors.FieldPathElement(r.Descriptor) + element.Subscript = &validate.FieldPathElement_Index{Index: uint64(i)} + errors.AppendFieldPath(itemErr, element, false) } if ok, err = errors.Merge(err, itemErr, failFast); !ok { return err @@ -45,4 +58,23 @@ func (r listItems) Tautology() bool { return r.ItemConstraints.Tautology() } -var _ evaluator = listItems{} +// itemsWrapper wraps the evaluation of nested repeated field rules. +type itemsWrapper struct { + evaluator +} + +func newItemsWrapper(evaluator evaluator) evaluator { + return itemsWrapper{evaluator} +} + +func (e itemsWrapper) Evaluate(val protoreflect.Value, failFast bool) error { + err := e.evaluator.Evaluate(val, failFast) + errors.PrependRulePath(err, repeatedItemsFieldPath) + return err +} + +var ( + _ evaluator = listItems{} + _ evaluator = itemsWrapper{} + _ wrapper = newItemsWrapper +) diff --git a/internal/expression/ast.go b/internal/expression/ast.go index 11a1814..da2aa13 100644 --- a/internal/expression/ast.go +++ b/internal/expression/ast.go @@ -15,10 +15,10 @@ package expression import ( + "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" "github.com/bufbuild/protovalidate-go/internal/errors" "github.com/google/cel-go/cel" "github.com/google/cel-go/interpreter" - "google.golang.org/protobuf/reflect/protoreflect" ) // ASTSet represents a collection of compiledAST and their associated cel.Env. @@ -41,15 +41,6 @@ func (set ASTSet) Merge(other ASTSet) ASTSet { return out } -// SetRule sets the rule attached to the compiled ASTs. This will be returned -// with validation errors returned by these expressions. -func (set ASTSet) SetRule(rulePath string, ruleValue protoreflect.Value) { - for i := range set.asts { - set.asts[i].RulePath = rulePath - set.asts[i].RuleValue = ruleValue - } -} - // ReduceResiduals generates a ProgramSet, performing a partial evaluation of // the ASTSet to optimize the expression. If the expression is optimized to // either a true or empty string constant result, no compiledProgram is @@ -92,10 +83,9 @@ func (set ASTSet) ReduceResiduals(opts ...cel.ProgramOption) (ProgramSet, error) x := residual.Source().Content() _ = x residuals = append(residuals, compiledAST{ - AST: residual, - Source: ast.Source, - RulePath: ast.RulePath, - RuleValue: ast.RuleValue, + AST: residual, + Source: ast.Source, + Path: ast.Path, }) } } @@ -122,10 +112,9 @@ func (set ASTSet) ToProgramSet(opts ...cel.ProgramOption) (out ProgramSet, err e } type compiledAST struct { - AST *cel.Ast - Source Expression - RulePath string - RuleValue protoreflect.Value + AST *cel.Ast + Source *validate.Constraint + Path []*validate.FieldPathElement } func (ast compiledAST) toProgram(env *cel.Env, opts ...cel.ProgramOption) (out compiledProgram, err error) { @@ -135,9 +124,8 @@ func (ast compiledAST) toProgram(env *cel.Env, opts ...cel.ProgramOption) (out c "failed to compile program %s: %w", ast.Source.GetId(), err) } return compiledProgram{ - Program: prog, - Source: ast.Source, - RulePath: ast.RulePath, - RuleValue: ast.RuleValue, + Program: prog, + Source: ast.Source, + Path: ast.Path, }, nil } diff --git a/internal/expression/ast_test.go b/internal/expression/ast_test.go index 8f69fcc..ee4d85d 100644 --- a/internal/expression/ast_test.go +++ b/internal/expression/ast_test.go @@ -59,9 +59,15 @@ func TestASTSet_ToProgramSet(t *testing.T) { env, err := celext.DefaultEnv(false) require.NoError(t, err) - expr := &validate.Constraint{Expression: proto.String("foo")} - asts, err := CompileASTs([]*validate.Constraint{expr}, env, - cel.Variable("foo", cel.BoolType)) + asts, err := CompileASTs( + Expressions{ + Constraints: []*validate.Constraint{ + {Expression: proto.String("foo")}, + }, + }, + env, + cel.Variable("foo", cel.BoolType), + ) require.NoError(t, err) assert.Len(t, asts.asts, 1) set, err := asts.ToProgramSet() @@ -81,9 +87,15 @@ func TestASTSet_ReduceResiduals(t *testing.T) { env, err := celext.DefaultEnv(false) require.NoError(t, err) - expr := &validate.Constraint{Expression: proto.String("foo")} - asts, err := CompileASTs([]*validate.Constraint{expr}, env, - cel.Variable("foo", cel.BoolType)) + asts, err := CompileASTs( + Expressions{ + Constraints: []*validate.Constraint{ + {Expression: proto.String("foo")}, + }, + }, + env, + cel.Variable("foo", cel.BoolType), + ) require.NoError(t, err) assert.Len(t, asts.asts, 1) set, err := asts.ReduceResiduals(cel.Globals(&Variable{Name: "foo", Val: true})) diff --git a/internal/expression/compile.go b/internal/expression/compile.go index 6c902c5..a418a64 100644 --- a/internal/expression/compile.go +++ b/internal/expression/compile.go @@ -15,27 +15,27 @@ package expression import ( + "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" "github.com/bufbuild/protovalidate-go/internal/errors" "github.com/google/cel-go/cel" ) -// Expression is the read-only interface of either validate.Constraint or -// private.Constraint which can be the source of a CEL expression. -type Expression interface { - GetId() string - GetMessage() string - GetExpression() string +// Expression is a container for the information needed to compile and evaluate +// a list of CEL-based expression, originating from a validate.Constraint. +type Expressions struct { + Constraints []*validate.Constraint + RulePath []*validate.FieldPathElement } // Compile produces a ProgramSet from the provided expressions in the given // environment. If the generated cel.Program require cel.ProgramOption params, // use CompileASTs instead with a subsequent call to ASTSet.ToProgramSet. -func Compile[T Expression]( - expressions []T, +func Compile( + expressions Expressions, env *cel.Env, envOpts ...cel.EnvOption, ) (set ProgramSet, err error) { - if len(expressions) == 0 { + if len(expressions.Constraints) == 0 { return nil, nil } @@ -47,11 +47,12 @@ func Compile[T Expression]( } } - set = make(ProgramSet, len(expressions)) - for i, expr := range expressions { - set[i].Source = expr + set = make(ProgramSet, len(expressions.Constraints)) + for i, constraint := range expressions.Constraints { + set[i].Source = constraint + set[i].Path = expressions.RulePath - ast, err := compileAST(env, expr) + ast, err := compileAST(env, constraint, expressions.RulePath) if err != nil { return nil, err } @@ -69,13 +70,13 @@ func Compile[T Expression]( // ASTSet.ToProgramSet or ASTSet.ReduceResiduals. Use Compile instead if no // cel.ProgramOption args need to be provided or residuals do not need to be // computed. -func CompileASTs[T Expression]( - expressions []T, +func CompileASTs( + expressions Expressions, env *cel.Env, envOpts ...cel.EnvOption, ) (set ASTSet, err error) { set.env = env - if len(expressions) == 0 { + if len(expressions.Constraints) == 0 { return set, nil } @@ -87,9 +88,9 @@ func CompileASTs[T Expression]( } } - set.asts = make([]compiledAST, len(expressions)) - for i, expr := range expressions { - set.asts[i], err = compileAST(set.env, expr) + set.asts = make([]compiledAST, len(expressions.Constraints)) + for i, constraint := range expressions.Constraints { + set.asts[i], err = compileAST(set.env, constraint, expressions.RulePath) if err != nil { return set, err } @@ -98,22 +99,23 @@ func CompileASTs[T Expression]( return set, nil } -func compileAST(env *cel.Env, expr Expression) (out compiledAST, err error) { - ast, issues := env.Compile(expr.GetExpression()) +func compileAST(env *cel.Env, constraint *validate.Constraint, rulePath []*validate.FieldPathElement) (out compiledAST, err error) { + ast, issues := env.Compile(constraint.GetExpression()) if err := issues.Err(); err != nil { return out, errors.NewCompilationErrorf( - "failed to compile expression %s: %w", expr.GetId(), err) + "failed to compile expression %s: %w", constraint.GetId(), err) } outType := ast.OutputType() if !(outType.IsAssignableType(cel.BoolType) || outType.IsAssignableType(cel.StringType)) { return out, errors.NewCompilationErrorf( "expression %s outputs %s, wanted either bool or string", - expr.GetId(), outType.String()) + constraint.GetId(), outType.String()) } return compiledAST{ AST: ast, - Source: expr, + Source: constraint, + Path: rulePath, }, nil } diff --git a/internal/expression/compile_test.go b/internal/expression/compile_test.go index 60a623a..c3d5445 100644 --- a/internal/expression/compile_test.go +++ b/internal/expression/compile_test.go @@ -34,7 +34,7 @@ func TestCompile(t *testing.T) { t.Run("empty", func(t *testing.T) { t.Parallel() - var exprs []*validate.Constraint + var exprs Expressions set, err := Compile(exprs, baseEnv) assert.Nil(t, set) require.NoError(t, err) @@ -42,19 +42,23 @@ func TestCompile(t *testing.T) { t.Run("success", func(t *testing.T) { t.Parallel() - exprs := []*validate.Constraint{ - {Id: proto.String("foo"), Expression: proto.String("this == 123")}, - {Id: proto.String("bar"), Expression: proto.String("'a string'")}, + exprs := Expressions{ + Constraints: []*validate.Constraint{ + {Id: proto.String("foo"), Expression: proto.String("this == 123")}, + {Id: proto.String("bar"), Expression: proto.String("'a string'")}, + }, } set, err := Compile(exprs, baseEnv, cel.Variable("this", cel.IntType)) - assert.Len(t, set, len(exprs)) + assert.Len(t, set, len(exprs.Constraints)) require.NoError(t, err) }) t.Run("env extension err", func(t *testing.T) { t.Parallel() - exprs := []*validate.Constraint{ - {Id: proto.String("foo"), Expression: proto.String("0 != 0")}, + exprs := Expressions{ + Constraints: []*validate.Constraint{ + {Id: proto.String("foo"), Expression: proto.String("0 != 0")}, + }, } set, err := Compile(exprs, baseEnv, cel.Types(true)) assert.Nil(t, set) @@ -64,8 +68,10 @@ func TestCompile(t *testing.T) { t.Run("bad syntax", func(t *testing.T) { t.Parallel() - exprs := []*validate.Constraint{ - {Id: proto.String("foo"), Expression: proto.String("!@#$%^&")}, + exprs := Expressions{ + Constraints: []*validate.Constraint{ + {Id: proto.String("foo"), Expression: proto.String("!@#$%^&")}, + }, } set, err := Compile(exprs, baseEnv) assert.Nil(t, set) @@ -75,8 +81,10 @@ func TestCompile(t *testing.T) { t.Run("invalid output type", func(t *testing.T) { t.Parallel() - exprs := []*validate.Constraint{ - {Id: proto.String("foo"), Expression: proto.String("1.23")}, + exprs := Expressions{ + Constraints: []*validate.Constraint{ + {Id: proto.String("foo"), Expression: proto.String("1.23")}, + }, } set, err := Compile(exprs, baseEnv) assert.Nil(t, set) diff --git a/internal/expression/program.go b/internal/expression/program.go index f2f83d5..10b0686 100644 --- a/internal/expression/program.go +++ b/internal/expression/program.go @@ -15,6 +15,7 @@ package expression import ( + "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" "github.com/bufbuild/protovalidate-go/internal/errors" "github.com/google/cel-go/cel" "google.golang.org/protobuf/reflect/protoreflect" @@ -86,10 +87,9 @@ func (s ProgramSet) bindThis(val any) *Variable { // compiledProgram is a parsed and type-checked cel.Program along with the // source Expression. type compiledProgram struct { - Program cel.Program - Source Expression - RulePath string - RuleValue protoreflect.Value + Program cel.Program + Source *validate.Constraint + Path []*validate.FieldPathElement } //nolint:nilnil // non-existence of violations is intentional @@ -109,20 +109,18 @@ func (expr compiledProgram) eval(bindings *Variable) (*errors.Violation, error) return nil, nil } return &errors.Violation{ + RulePath: errors.NewFieldPath(expr.Path), ConstraintID: expr.Source.GetId(), Message: val, - RulePath: expr.RulePath, - RuleValue: expr.RuleValue, }, nil case bool: if val { return nil, nil } return &errors.Violation{ + RulePath: errors.NewFieldPath(expr.Path), ConstraintID: expr.Source.GetId(), Message: expr.Source.GetMessage(), - RulePath: expr.RulePath, - RuleValue: expr.RuleValue, }, nil default: return nil, errors.NewRuntimeErrorf( diff --git a/validator.go b/validator.go index b637b5c..bda75a0 100644 --- a/validator.go +++ b/validator.go @@ -107,7 +107,9 @@ func (v *Validator) Validate(msg proto.Message) error { } refl := msg.ProtoReflect() eval := v.builder.Load(refl.Descriptor()) - return eval.EvaluateMessage(refl, v.failFast) + err := eval.EvaluateMessage(refl, v.failFast) + errors.ReverseFieldPaths(err) + return err } // Validate uses a global instance of Validator constructed with no ValidatorOptions and diff --git a/validator_example_test.go b/validator_example_test.go index 39786e6..a1c06ee 100644 --- a/validator_example_test.go +++ b/validator_example_test.go @@ -156,7 +156,7 @@ func ExampleValidationError() { err = validator.Validate(loc) var valErr *ValidationError if ok := errors.As(err, &valErr); ok { - fmt.Println(valErr.Violations[0].FieldPath, valErr.Violations[0].ConstraintID) + fmt.Println(valErr.Violations[0].FieldPath.String(), valErr.Violations[0].ConstraintID) } // output: lat double.gte_lte From 9a9759b1e343600b512f00d35132bf3efd3e933d Mon Sep 17 00:00:00 2001 From: John Chadwick Date: Wed, 6 Nov 2024 16:04:40 -0500 Subject: [PATCH 04/24] checkpoint: minimize API, fix some extraneous diffs, etc. --- internal/constraints/cache.go | 5 +- internal/errors/utils.go | 65 +++++++------ internal/errors/utils_test.go | 25 ++--- internal/errors/validation.go | 143 +++++++++++----------------- internal/evaluator/any.go | 8 +- internal/evaluator/enum.go | 4 +- internal/evaluator/field.go | 8 +- internal/evaluator/oneof.go | 6 +- internal/expression/ast.go | 11 +++ internal/expression/program.go | 16 ++-- internal/expression/program_test.go | 26 ++--- validator.go | 8 +- validator_example_test.go | 3 +- validator_test.go | 2 +- 14 files changed, 167 insertions(+), 163 deletions(-) diff --git a/internal/constraints/cache.go b/internal/constraints/cache.go index 8719d88..c6e0689 100644 --- a/internal/constraints/cache.go +++ b/internal/constraints/cache.go @@ -71,12 +71,12 @@ func (c *Cache) Build( } var asts expression.ASTSet - constraints.Range(func(desc protoreflect.FieldDescriptor, ruleValue protoreflect.Value) bool { + constraints.Range(func(desc protoreflect.FieldDescriptor, rule protoreflect.Value) bool { fieldEnv, compileErr := env.Extend( cel.Constant( "rule", celext.ProtoFieldToCELType(desc, true, false), - celext.ProtoFieldToCELValue(desc, ruleValue, false), + celext.ProtoFieldToCELValue(desc, rule, false), ), ) if compileErr != nil { @@ -88,6 +88,7 @@ func (c *Cache) Build( err = compileErr return false } + precomputedASTs.SetRuleValue(rule) asts = asts.Merge(precomputedASTs) return true }) diff --git a/internal/errors/utils.go b/internal/errors/utils.go index 4826846..ec41929 100644 --- a/internal/errors/utils.go +++ b/internal/errors/utils.go @@ -55,15 +55,6 @@ func Merge(dst, src error, failFast bool) (ok bool, err error) { return !(failFast && len(dstValErrs.Violations) > 0), dst } -func MarkForKey(err error) { - var valErr *ValidationError - if errors.As(err, &valErr) { - for i := range valErr.Violations { - valErr.Violations[i].ForKey = true - } - } -} - func FieldPathElement(field protoreflect.FieldDescriptor) *validate.FieldPathElement { return &validate.FieldPathElement{ FieldNumber: proto.Int32(int32(field.Number())), @@ -82,18 +73,19 @@ func FieldPathElement(field protoreflect.FieldDescriptor) *validate.FieldPathEle func AppendFieldPath(err error, suffix *validate.FieldPathElement, skipSubscript bool) { var valErr *ValidationError if errors.As(err, &valErr) { - for i := range valErr.Violations { - violation := &valErr.Violations[i] - // Special case: Here we skip appending if the last element had a - // subscript. This is a weird special case that makes it - // significantly simpler to handle reverse-constructing paths with - // maps and slices. - if skipSubscript && - len(violation.FieldPath.path) > 0 && - violation.FieldPath.path[len(violation.FieldPath.path)-1].Subscript != nil { - continue + for _, violation := range valErr.Violations { + if violation, ok := violation.(*ViolationData); ok { + // Special case: Here we skip appending if the last element had a + // subscript. This is a weird special case that makes it + // significantly simpler to handle reverse-constructing paths with + // maps and slices. + if skipSubscript && + len(violation.FieldPath) > 0 && + violation.FieldPath[len(violation.FieldPath)-1].Subscript != nil { + continue + } + violation.FieldPath = append(violation.FieldPath, suffix) } - violation.FieldPath.path = append(violation.FieldPath.path, suffix) } } } @@ -106,11 +98,13 @@ func AppendFieldPath(err error, suffix *validate.FieldPathElement, skipSubscript func PrependRulePath(err error, prefix []*validate.FieldPathElement) { var valErr *ValidationError if errors.As(err, &valErr) { - for i := range valErr.Violations { - valErr.Violations[i].RulePath.path = append( - append([]*validate.FieldPathElement{}, prefix...), - valErr.Violations[i].RulePath.path..., - ) + for _, violation := range valErr.Violations { + if violation, ok := violation.(*ViolationData); ok { + violation.RulePath = append( + append([]*validate.FieldPathElement{}, prefix...), + violation.RulePath..., + ) + } } } } @@ -119,9 +113,10 @@ func PrependRulePath(err error, prefix []*validate.FieldPathElement) { func ReverseFieldPaths(err error) { var valErr *ValidationError if errors.As(err, &valErr) { - for i := range valErr.Violations { - violation := &valErr.Violations[i] - slices.Reverse(violation.FieldPath.path) + for _, violation := range valErr.Violations { + if violation, ok := violation.(*ViolationData); ok { + slices.Reverse(violation.FieldPath) + } } } } @@ -130,9 +125,6 @@ func ReverseFieldPaths(err error) { // field path. func FieldPathString(path []*validate.FieldPathElement) string { var result strings.Builder - if path == nil { - return "" - } for i, element := range path { if i > 0 { result.WriteByte('.') @@ -161,3 +153,14 @@ func FieldPathString(path []*validate.FieldPathElement) string { } return result.String() } + +func MarkForKey(err error) { + var valErr *ValidationError + if errors.As(err, &valErr) { + for _, violation := range valErr.Violations { + if violation, ok := violation.(*ViolationData); ok { + violation.ForKey = true + } + } + } +} diff --git a/internal/errors/utils_test.go b/internal/errors/utils_test.go index 56b3760..b21bc7f 100644 --- a/internal/errors/utils_test.go +++ b/internal/errors/utils_test.go @@ -20,6 +20,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" ) func TestMerge(t *testing.T) { @@ -51,15 +52,15 @@ func TestMerge(t *testing.T) { t.Run("validation", func(t *testing.T) { t.Parallel() - exErr := &ValidationError{Violations: []Violation{{ConstraintID: "foo"}}} + exErr := &ValidationError{Violations: []Violation{&ViolationData{ConstraintID: "foo"}}} ok, err := Merge(nil, exErr, true) var valErr *ValidationError require.ErrorAs(t, err, &valErr) - assert.True(t, EqualViolations(exErr.Violations, valErr.Violations)) + assert.True(t, proto.Equal(exErr.ToProto(), valErr.ToProto())) assert.False(t, ok) ok, err = Merge(nil, exErr, false) require.ErrorAs(t, err, &valErr) - assert.True(t, EqualViolations(exErr.Violations, valErr.Violations)) + assert.True(t, proto.Equal(exErr.ToProto(), valErr.ToProto())) assert.True(t, ok) }) }) @@ -70,7 +71,7 @@ func TestMerge(t *testing.T) { t.Run("non-validation dst", func(t *testing.T) { t.Parallel() dstErr := errors.New("some error") - srcErr := &ValidationError{Violations: []Violation{{ConstraintID: "foo"}}} + srcErr := &ValidationError{Violations: []Violation{&ViolationData{ConstraintID: "foo"}}} ok, err := Merge(dstErr, srcErr, true) assert.Equal(t, dstErr, err) assert.False(t, ok) @@ -81,7 +82,7 @@ func TestMerge(t *testing.T) { t.Run("non-validation src", func(t *testing.T) { t.Parallel() - dstErr := &ValidationError{Violations: []Violation{{ConstraintID: "foo"}}} + dstErr := &ValidationError{Violations: []Violation{&ViolationData{ConstraintID: "foo"}}} srcErr := errors.New("some error") ok, err := Merge(dstErr, srcErr, true) assert.Equal(t, srcErr, err) @@ -94,21 +95,21 @@ func TestMerge(t *testing.T) { t.Run("validation", func(t *testing.T) { t.Parallel() - dstErr := &ValidationError{Violations: []Violation{{ConstraintID: "foo"}}} - srcErr := &ValidationError{Violations: []Violation{{ConstraintID: ("bar")}}} + dstErr := &ValidationError{Violations: []Violation{&ViolationData{ConstraintID: "foo"}}} + srcErr := &ValidationError{Violations: []Violation{&ViolationData{ConstraintID: ("bar")}}} exErr := &ValidationError{Violations: []Violation{ - {ConstraintID: "foo"}, - {ConstraintID: "bar"}, + &ViolationData{ConstraintID: "foo"}, + &ViolationData{ConstraintID: "bar"}, }} ok, err := Merge(dstErr, srcErr, true) var valErr *ValidationError require.ErrorAs(t, err, &valErr) - assert.True(t, EqualViolations(exErr.Violations, valErr.Violations)) + assert.True(t, proto.Equal(exErr.ToProto(), valErr.ToProto())) assert.False(t, ok) - dstErr = &ValidationError{Violations: []Violation{{ConstraintID: "foo"}}} + dstErr = &ValidationError{Violations: []Violation{&ViolationData{ConstraintID: "foo"}}} ok, err = Merge(dstErr, srcErr, false) require.ErrorAs(t, err, &valErr) - assert.True(t, EqualViolations(exErr.Violations, valErr.Violations)) + assert.True(t, proto.Equal(exErr.ToProto(), valErr.ToProto())) assert.True(t, ok) }) }) diff --git a/internal/errors/validation.go b/internal/errors/validation.go index d281372..7a1bab6 100644 --- a/internal/errors/validation.go +++ b/internal/errors/validation.go @@ -16,7 +16,6 @@ package errors import ( "fmt" - "slices" "strings" "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" @@ -27,41 +26,20 @@ import ( // Violation represents a single instance where a validation rule was not met. // It provides information about the field that caused the violation, the // specific unfulfilled constraint, and a human-readable error message. -type Violation struct { - // FieldPath is an identifier that points to the specific field - // that failed the validation. This could be a nested field, in which case - // the path will include all the parent fields leading to the actual field - // that caused the violation. - FieldPath FieldPath - - // RulePath is a machine-readable identifier that points to the specific - // constraint rule that failed validation. This will be a nested field - // starting from the FieldConstraints of the field that failed validation. - // This value is only set for standard or predefined rules on fields. - RulePath FieldPath - - // FieldValue is the value of the specific field that failed validation. - FieldValue protoreflect.Value - - // ForKey indicates whether the violation was caused by a map key, rather - // than a value. - ForKey bool - - // ConstraintID is the unique identifier of the constraint that was not - // fulfilled. - ConstraintID string - - // RuleValue is the value of the rule that specified the failed constraint. - // Only constraints that have a corresponding rule are set (i.e.: standard - // constraints and predefined constraints). In other cases, such as custom - // constraints, this will be an invalid value. - RuleValue protoreflect.Value - - // Message is a human-readable error message that describes the nature of - // the violation. This can be the default error message from the violated - // constraint, or it can be a custom message that gives more context about - // the violation. - Message string +type Violation interface { + // GetFieldValue returns the value of the specific field that failed + // validation. If there was no value, this will return an invalid value. + GetFieldValue() protoreflect.Value + + // GetRuleValue returns the value of the rule that specified the failed + // constraint. Not all constraints have a value; only standard and + // predefined constraints have rule values. In violations caused by other + // kinds of constraints, like custom contraints, this will return an invalid + // value. + GetRuleValue() protoreflect.Value + + // ToProto converts this violation into its proto.Message form. + ToProto() *validate.Violation } // A ValidationError is returned if one or more constraint violations were @@ -76,30 +54,7 @@ func (err *ValidationError) ToProto() *validate.Violations { Violations: make([]*validate.Violation, len(err.Violations)), } for i, violation := range err.Violations { - var fieldPath *validate.FieldPath - if len(violation.FieldPath.path) > 0 { - fieldPath = violation.FieldPath.ToProto() - } - var rulePath *validate.FieldPath - if len(violation.RulePath.path) > 0 { - rulePath = violation.RulePath.ToProto() - } - var fieldPathString *string - if fieldPath != nil { - fieldPathString = proto.String(FieldPathString(fieldPath.GetElements())) - } - var forKey *bool - if violation.ForKey { - forKey = proto.Bool(true) - } - violations.Violations[i] = &validate.Violation{ - FieldPathString: fieldPathString, - FieldPath: fieldPath, - RulePath: rulePath, - ConstraintId: proto.String(violation.ConstraintID), - Message: proto.String(violation.Message), - ForKey: forKey, - } + violations.Violations[i] = violation.ToProto() } return violations } @@ -108,47 +63,65 @@ func (err *ValidationError) Error() string { bldr := &strings.Builder{} bldr.WriteString("validation error:") for _, violation := range err.Violations { + violation := violation.ToProto() bldr.WriteString("\n - ") - if fieldPath := violation.FieldPath.String(); fieldPath != "" { + if fieldPath := FieldPathString(violation.GetFieldPath().GetElements()); fieldPath != "" { bldr.WriteString(fieldPath) bldr.WriteString(": ") } _, _ = fmt.Fprintf(bldr, "%s [%s]", - violation.Message, - violation.ConstraintID) + violation.GetMessage(), + violation.GetConstraintId()) } return bldr.String() } -// A FieldPath specifies a nested field inside of a protobuf message. -type FieldPath struct { - path []*validate.FieldPathElement +// ViolationData is a simple implementation of Violation. +type ViolationData struct { + FieldPath []*validate.FieldPathElement + RulePath []*validate.FieldPathElement + FieldValue protoreflect.Value + RuleValue protoreflect.Value + FieldDescriptor protoreflect.FieldDescriptor + ForKey bool + ConstraintID string + Message string } -func NewFieldPath(elements []*validate.FieldPathElement) FieldPath { - return FieldPath{path: elements} +func (v *ViolationData) GetFieldValue() protoreflect.Value { + return v.FieldValue } -func (f FieldPath) ToProto() *validate.FieldPath { - return &validate.FieldPath{ - Elements: f.path, - } +func (v *ViolationData) GetRuleValue() protoreflect.Value { + return v.RuleValue } -func (f FieldPath) String() string { - return FieldPathString(f.path) +func (v *ViolationData) ToProto() *validate.Violation { + var fieldPathString *string + if len(v.FieldPath) > 0 { + fieldPathString = proto.String(FieldPathString(v.FieldPath)) + } + var forKey *bool + if v.ForKey { + forKey = proto.Bool(true) + } + return &validate.Violation{ + FieldPathString: fieldPathString, + FieldPath: fieldPathProto(v.FieldPath), + RulePath: fieldPathProto(v.RulePath), + ConstraintId: proto.String(v.ConstraintID), + Message: proto.String(v.Message), + ForKey: forKey, + } } -// EqualViolations returns true if the underlying violations are equal. -func EqualViolations(a, b []Violation) bool { - return slices.EqualFunc(a, b, EqualViolation) -} +var _ Violation = &ViolationData{} -// EqualViolation returns true if the underlying violations are equal. -func EqualViolation(a, b Violation) bool { - return (a.ConstraintID == b.ConstraintID && - a.Message == b.Message && - a.ForKey == b.ForKey && - proto.Equal(a.FieldPath.ToProto(), b.FieldPath.ToProto()) && - proto.Equal(a.RulePath.ToProto(), b.RulePath.ToProto())) +func fieldPathProto(elements []*validate.FieldPathElement) *validate.FieldPath { + if len(elements) == 0 { + return nil + } + return &validate.FieldPath{ + Elements: elements, + } } diff --git a/internal/evaluator/any.go b/internal/evaluator/any.go index 4865a28..e777209 100644 --- a/internal/evaluator/any.go +++ b/internal/evaluator/any.go @@ -53,8 +53,8 @@ func (a anyPB) Evaluate(val protoreflect.Value, failFast bool) error { err := &errors.ValidationError{} if len(a.In) > 0 { if _, ok := a.In[typeURL]; !ok { - err.Violations = append(err.Violations, errors.Violation{ - RulePath: errors.NewFieldPath(anyInRulePath), + err.Violations = append(err.Violations, &errors.ViolationData{ + RulePath: anyInRulePath, ConstraintID: "any.in", Message: "type URL must be in the allow list", }) @@ -66,8 +66,8 @@ func (a anyPB) Evaluate(val protoreflect.Value, failFast bool) error { if len(a.NotIn) > 0 { if _, ok := a.NotIn[typeURL]; ok { - err.Violations = append(err.Violations, errors.Violation{ - RulePath: errors.NewFieldPath(anyNotInRulePath), + err.Violations = append(err.Violations, &errors.ViolationData{ + RulePath: anyNotInRulePath, ConstraintID: "any.not_in", Message: "type URL must not be in the block list", }) diff --git a/internal/evaluator/enum.go b/internal/evaluator/enum.go index 6c91f09..653e4c4 100644 --- a/internal/evaluator/enum.go +++ b/internal/evaluator/enum.go @@ -38,8 +38,8 @@ type definedEnum struct { func (d definedEnum) Evaluate(val protoreflect.Value, _ bool) error { if d.ValueDescriptors.ByNumber(val.Enum()) == nil { - return &errors.ValidationError{Violations: []errors.Violation{{ - RulePath: errors.NewFieldPath(enumDefinedOnlyRulePath), + return &errors.ValidationError{Violations: []errors.Violation{&errors.ViolationData{ + RulePath: enumDefinedOnlyRulePath, ConstraintID: "enum.defined_only", Message: "value must be one of the defined enum values", }}} diff --git a/internal/evaluator/field.go b/internal/evaluator/field.go index 5185ff8..f06fded 100644 --- a/internal/evaluator/field.go +++ b/internal/evaluator/field.go @@ -53,11 +53,11 @@ func (f field) Evaluate(val protoreflect.Value, failFast bool) error { func (f field) EvaluateMessage(msg protoreflect.Message, failFast bool) (err error) { if f.Required && !msg.Has(f.Descriptor) { - err := &errors.ValidationError{Violations: []errors.Violation{{ - FieldPath: errors.NewFieldPath([]*validate.FieldPathElement{ + err := &errors.ValidationError{Violations: []errors.Violation{&errors.ViolationData{ + FieldPath: []*validate.FieldPathElement{ errors.FieldPathElement(f.Descriptor), - }), - RulePath: errors.NewFieldPath(requiredRulePath), + }, + RulePath: requiredRulePath, ConstraintID: "required", Message: "value is required", }}} diff --git a/internal/evaluator/oneof.go b/internal/evaluator/oneof.go index 413695b..f1864fe 100644 --- a/internal/evaluator/oneof.go +++ b/internal/evaluator/oneof.go @@ -35,10 +35,10 @@ func (o oneof) Evaluate(val protoreflect.Value, failFast bool) error { func (o oneof) EvaluateMessage(msg protoreflect.Message, _ bool) error { if o.Required && msg.WhichOneof(o.Descriptor) == nil { - return &errors.ValidationError{Violations: []errors.Violation{{ - FieldPath: errors.NewFieldPath([]*validate.FieldPathElement{{ + return &errors.ValidationError{Violations: []errors.Violation{&errors.ViolationData{ + FieldPath: []*validate.FieldPathElement{{ FieldName: proto.String(string(o.Descriptor.Name())), - }}), + }}, ConstraintID: "required", Message: "exactly one field is required in oneof", }}} diff --git a/internal/expression/ast.go b/internal/expression/ast.go index da2aa13..71f0924 100644 --- a/internal/expression/ast.go +++ b/internal/expression/ast.go @@ -19,6 +19,7 @@ import ( "github.com/bufbuild/protovalidate-go/internal/errors" "github.com/google/cel-go/cel" "github.com/google/cel-go/interpreter" + "google.golang.org/protobuf/reflect/protoreflect" ) // ASTSet represents a collection of compiledAST and their associated cel.Env. @@ -111,10 +112,19 @@ func (set ASTSet) ToProgramSet(opts ...cel.ProgramOption) (out ProgramSet, err e return out, nil } +// SetRuleValue sets the rule value for the programs in the ASTSet. +func (set *ASTSet) SetRuleValue(ruleValue protoreflect.Value) { + set.asts = append([]compiledAST{}, set.asts...) + for i := range set.asts { + set.asts[i].Value = ruleValue + } +} + type compiledAST struct { AST *cel.Ast Source *validate.Constraint Path []*validate.FieldPathElement + Value protoreflect.Value } func (ast compiledAST) toProgram(env *cel.Env, opts ...cel.ProgramOption) (out compiledProgram, err error) { @@ -127,5 +137,6 @@ func (ast compiledAST) toProgram(env *cel.Env, opts ...cel.ProgramOption) (out c Program: prog, Source: ast.Source, Path: ast.Path, + Value: ast.Value, }, nil } diff --git a/internal/expression/program.go b/internal/expression/program.go index 10b0686..39ebcac 100644 --- a/internal/expression/program.go +++ b/internal/expression/program.go @@ -47,7 +47,8 @@ func (s ProgramSet) Eval(val protoreflect.Value, failFast bool) error { return err } if violation != nil { - violations = append(violations, *violation) + violation.FieldValue = val + violations = append(violations, violation) if failFast { break } @@ -90,10 +91,11 @@ type compiledProgram struct { Program cel.Program Source *validate.Constraint Path []*validate.FieldPathElement + Value protoreflect.Value } //nolint:nilnil // non-existence of violations is intentional -func (expr compiledProgram) eval(bindings *Variable) (*errors.Violation, error) { +func (expr compiledProgram) eval(bindings *Variable) (*errors.ViolationData, error) { now := nowPool.Get() defer nowPool.Put(now) bindings.Next = now @@ -108,8 +110,9 @@ func (expr compiledProgram) eval(bindings *Variable) (*errors.Violation, error) if val == "" { return nil, nil } - return &errors.Violation{ - RulePath: errors.NewFieldPath(expr.Path), + return &errors.ViolationData{ + RulePath: expr.Path, + RuleValue: expr.Value, ConstraintID: expr.Source.GetId(), Message: val, }, nil @@ -117,8 +120,9 @@ func (expr compiledProgram) eval(bindings *Variable) (*errors.Violation, error) if val { return nil, nil } - return &errors.Violation{ - RulePath: errors.NewFieldPath(expr.Path), + return &errors.ViolationData{ + RulePath: expr.Path, + RuleValue: expr.Value, ConstraintID: expr.Source.GetId(), Message: expr.Source.GetMessage(), }, nil diff --git a/internal/expression/program_test.go b/internal/expression/program_test.go index 1125a49..2191f2b 100644 --- a/internal/expression/program_test.go +++ b/internal/expression/program_test.go @@ -39,7 +39,7 @@ func TestCompiled(t *testing.T) { name string prog cel.Program src *validate.Constraint - exViol *pverr.Violation + exViol *validate.Violation exErr bool }{ { @@ -54,13 +54,13 @@ func TestCompiled(t *testing.T) { name: "invalid bool", prog: mockProgram{Val: types.False}, src: &validate.Constraint{Id: proto.String("foo"), Message: proto.String("bar")}, - exViol: &pverr.Violation{ConstraintID: "foo", Message: "bar"}, + exViol: &validate.Violation{ConstraintId: proto.String("foo"), Message: proto.String("bar")}, }, { name: "invalid string", prog: mockProgram{Val: types.String("bar")}, src: &validate.Constraint{Id: proto.String("foo")}, - exViol: &pverr.Violation{ConstraintID: "foo", Message: "bar"}, + exViol: &validate.Violation{ConstraintId: proto.String("foo"), Message: proto.String("bar")}, }, { name: "eval error", @@ -89,7 +89,7 @@ func TestCompiled(t *testing.T) { if test.exViol == nil { assert.Nil(t, violation) } else { - assert.True(t, pverr.EqualViolation(*test.exViol, *violation)) + assert.True(t, proto.Equal(test.exViol, violation.ToProto())) } } }) @@ -103,7 +103,7 @@ func TestSet(t *testing.T) { name string set ProgramSet failFast bool - exViols []pverr.Violation + exViols *validate.Violations exErr bool }{ { @@ -148,9 +148,11 @@ func TestSet(t *testing.T) { Source: &validate.Constraint{Id: proto.String("bar")}, }, }, - exViols: []pverr.Violation{ - {ConstraintID: "foo", Message: "fizz"}, - {ConstraintID: "bar", Message: "buzz"}, + exViols: &validate.Violations{ + Violations: []*validate.Violation{ + {ConstraintId: proto.String("foo"), Message: proto.String("fizz")}, + {ConstraintId: proto.String("bar"), Message: proto.String("buzz")}, + }, }, }, { @@ -166,8 +168,10 @@ func TestSet(t *testing.T) { Source: &validate.Constraint{Id: proto.String("bar")}, }, }, - exViols: []pverr.Violation{ - {ConstraintID: "foo", Message: "fizz"}, + exViols: &validate.Violations{ + Violations: []*validate.Violation{ + {ConstraintId: proto.String("foo"), Message: proto.String("fizz")}, + }, }, }, } @@ -182,7 +186,7 @@ func TestSet(t *testing.T) { case test.exViols != nil: var viols *pverr.ValidationError require.ErrorAs(t, err, &viols) - require.True(t, pverr.EqualViolations(test.exViols, viols.Violations)) + require.True(t, proto.Equal(test.exViols, viols.ToProto())) case test.exErr: require.Error(t, err) default: diff --git a/validator.go b/validator.go index bda75a0..84e971a 100644 --- a/validator.go +++ b/validator.go @@ -43,7 +43,7 @@ type ( // } ValidationError = errors.ValidationError - // A Violation contains information about one constraint violation. + // A Violation provides information about one constraint violation. Violation = errors.Violation // A CompilationError is returned if a CEL expression cannot be compiled & @@ -124,6 +124,12 @@ func Validate(msg proto.Message) error { return globalValidator.Validate(msg) } +// FieldPathString returns a dotted path string for the provided +// validate.FieldPath. +func FieldPathString(path *validate.FieldPath) string { + return errors.FieldPathString(path.GetElements()) +} + type config struct { failFast bool useUTC bool diff --git a/validator_example_test.go b/validator_example_test.go index a1c06ee..e63ec0a 100644 --- a/validator_example_test.go +++ b/validator_example_test.go @@ -156,7 +156,8 @@ func ExampleValidationError() { err = validator.Validate(loc) var valErr *ValidationError if ok := errors.As(err, &valErr); ok { - fmt.Println(valErr.Violations[0].FieldPath.String(), valErr.Violations[0].ConstraintID) + violation := valErr.Violations[0].ToProto() + fmt.Println(FieldPathString(violation.GetFieldPath()), violation.GetConstraintId()) } // output: lat double.gte_lte diff --git a/validator_test.go b/validator_test.go index 53e42ff..f2055db 100644 --- a/validator_test.go +++ b/validator_test.go @@ -228,7 +228,7 @@ func TestValidator_Validate_RepeatedItemCel(t *testing.T) { err = val.Validate(msg) valErr := &ValidationError{} require.ErrorAs(t, err, &valErr) - assert.Equal(t, "paths.no_space", valErr.Violations[0].ConstraintID) + assert.Equal(t, "paths.no_space", valErr.Violations[0].ToProto().GetConstraintId()) } func TestValidator_Validate_Issue141(t *testing.T) { From 8c08015a87408f232e8c79127f0175c6aa7ef6d4 Mon Sep 17 00:00:00 2001 From: John Chadwick Date: Wed, 6 Nov 2024 17:14:49 -0500 Subject: [PATCH 05/24] checkpoint --- internal/errors/utils.go | 12 ++++++------ internal/errors/validation.go | 33 ++++++++++++++++----------------- internal/evaluator/any.go | 12 ++++++++++-- internal/evaluator/builder.go | 5 +++++ internal/evaluator/enum.go | 4 +++- internal/evaluator/field.go | 5 +++-- internal/evaluator/oneof.go | 2 +- internal/expression/ast.go | 1 + internal/expression/program.go | 4 ++-- validator_example_test.go | 7 +++++-- 10 files changed, 52 insertions(+), 33 deletions(-) diff --git a/internal/errors/utils.go b/internal/errors/utils.go index ec41929..ac23fd4 100644 --- a/internal/errors/utils.go +++ b/internal/errors/utils.go @@ -80,11 +80,11 @@ func AppendFieldPath(err error, suffix *validate.FieldPathElement, skipSubscript // significantly simpler to handle reverse-constructing paths with // maps and slices. if skipSubscript && - len(violation.FieldPath) > 0 && - violation.FieldPath[len(violation.FieldPath)-1].Subscript != nil { + len(violation.Field) > 0 && + violation.Field[len(violation.Field)-1].Subscript != nil { continue } - violation.FieldPath = append(violation.FieldPath, suffix) + violation.Field = append(violation.Field, suffix) } } } @@ -100,9 +100,9 @@ func PrependRulePath(err error, prefix []*validate.FieldPathElement) { if errors.As(err, &valErr) { for _, violation := range valErr.Violations { if violation, ok := violation.(*ViolationData); ok { - violation.RulePath = append( + violation.Rule = append( append([]*validate.FieldPathElement{}, prefix...), - violation.RulePath..., + violation.Rule..., ) } } @@ -115,7 +115,7 @@ func ReverseFieldPaths(err error) { if errors.As(err, &valErr) { for _, violation := range valErr.Violations { if violation, ok := violation.(*ViolationData); ok { - slices.Reverse(violation.FieldPath) + slices.Reverse(violation.Field) } } } diff --git a/internal/errors/validation.go b/internal/errors/validation.go index 7a1bab6..7343a67 100644 --- a/internal/errors/validation.go +++ b/internal/errors/validation.go @@ -65,7 +65,7 @@ func (err *ValidationError) Error() string { for _, violation := range err.Violations { violation := violation.ToProto() bldr.WriteString("\n - ") - if fieldPath := FieldPathString(violation.GetFieldPath().GetElements()); fieldPath != "" { + if fieldPath := FieldPathString(violation.GetField().GetElements()); fieldPath != "" { bldr.WriteString(fieldPath) bldr.WriteString(": ") } @@ -78,14 +78,13 @@ func (err *ValidationError) Error() string { // ViolationData is a simple implementation of Violation. type ViolationData struct { - FieldPath []*validate.FieldPathElement - RulePath []*validate.FieldPathElement - FieldValue protoreflect.Value - RuleValue protoreflect.Value - FieldDescriptor protoreflect.FieldDescriptor - ForKey bool - ConstraintID string - Message string + Field []*validate.FieldPathElement + Rule []*validate.FieldPathElement + FieldValue protoreflect.Value + RuleValue protoreflect.Value + ConstraintID string + Message string + ForKey bool } func (v *ViolationData) GetFieldValue() protoreflect.Value { @@ -98,20 +97,20 @@ func (v *ViolationData) GetRuleValue() protoreflect.Value { func (v *ViolationData) ToProto() *validate.Violation { var fieldPathString *string - if len(v.FieldPath) > 0 { - fieldPathString = proto.String(FieldPathString(v.FieldPath)) + if len(v.Field) > 0 { + fieldPathString = proto.String(FieldPathString(v.Field)) } var forKey *bool if v.ForKey { forKey = proto.Bool(true) } return &validate.Violation{ - FieldPathString: fieldPathString, - FieldPath: fieldPathProto(v.FieldPath), - RulePath: fieldPathProto(v.RulePath), - ConstraintId: proto.String(v.ConstraintID), - Message: proto.String(v.Message), - ForKey: forKey, + Field: fieldPathProto(v.Field), + Rule: fieldPathProto(v.Rule), + FieldPath: fieldPathString, + ConstraintId: proto.String(v.ConstraintID), + Message: proto.String(v.Message), + ForKey: forKey, } } diff --git a/internal/evaluator/any.go b/internal/evaluator/any.go index e777209..c770aa9 100644 --- a/internal/evaluator/any.go +++ b/internal/evaluator/any.go @@ -45,6 +45,10 @@ type anyPB struct { In map[string]struct{} // NotIn specifies which type URLs the value may not possess NotIn map[string]struct{} + // InValue contains the original `in` rule value. + InValue protoreflect.Value + // NotInValue contains the original `not_in` rule value. + NotInValue protoreflect.Value } func (a anyPB) Evaluate(val protoreflect.Value, failFast bool) error { @@ -54,7 +58,9 @@ func (a anyPB) Evaluate(val protoreflect.Value, failFast bool) error { if len(a.In) > 0 { if _, ok := a.In[typeURL]; !ok { err.Violations = append(err.Violations, &errors.ViolationData{ - RulePath: anyInRulePath, + Rule: anyInRulePath, + FieldValue: val, + RuleValue: a.InValue, ConstraintID: "any.in", Message: "type URL must be in the allow list", }) @@ -67,7 +73,9 @@ func (a anyPB) Evaluate(val protoreflect.Value, failFast bool) error { if len(a.NotIn) > 0 { if _, ok := a.NotIn[typeURL]; ok { err.Violations = append(err.Violations, &errors.ViolationData{ - RulePath: anyNotInRulePath, + Rule: anyNotInRulePath, + FieldValue: val, + RuleValue: a.NotInValue, ConstraintID: "any.not_in", Message: "type URL must not be in the block list", }) diff --git a/internal/evaluator/builder.go b/internal/evaluator/builder.go index ba0f51b..daaa81e 100644 --- a/internal/evaluator/builder.go +++ b/internal/evaluator/builder.go @@ -391,10 +391,15 @@ func (bldr *Builder) processAnyConstraints( } typeURLDesc := fdesc.Message().Fields().ByName("type_url") + anyPbDesc := (&validate.AnyRules{}).ProtoReflect().Descriptor() + inField := anyPbDesc.Fields().ByName("in") + notInField := anyPbDesc.Fields().ByName("not_in") anyEval := anyPB{ TypeURLDescriptor: typeURLDesc, In: stringsToSet(fieldConstraints.GetAny().GetIn()), NotIn: stringsToSet(fieldConstraints.GetAny().GetNotIn()), + InValue: fieldConstraints.GetAny().ProtoReflect().Get(inField), + NotInValue: fieldConstraints.GetAny().ProtoReflect().Get(notInField), } appendEvaluator(valEval, anyEval, itemsWrapper) return nil diff --git a/internal/evaluator/enum.go b/internal/evaluator/enum.go index 653e4c4..23032d3 100644 --- a/internal/evaluator/enum.go +++ b/internal/evaluator/enum.go @@ -39,7 +39,9 @@ type definedEnum struct { func (d definedEnum) Evaluate(val protoreflect.Value, _ bool) error { if d.ValueDescriptors.ByNumber(val.Enum()) == nil { return &errors.ValidationError{Violations: []errors.Violation{&errors.ViolationData{ - RulePath: enumDefinedOnlyRulePath, + Rule: enumDefinedOnlyRulePath, + FieldValue: val, + RuleValue: protoreflect.ValueOfBool(true), ConstraintID: "enum.defined_only", Message: "value must be one of the defined enum values", }}} diff --git a/internal/evaluator/field.go b/internal/evaluator/field.go index f06fded..9bd090b 100644 --- a/internal/evaluator/field.go +++ b/internal/evaluator/field.go @@ -54,10 +54,11 @@ func (f field) Evaluate(val protoreflect.Value, failFast bool) error { func (f field) EvaluateMessage(msg protoreflect.Message, failFast bool) (err error) { if f.Required && !msg.Has(f.Descriptor) { err := &errors.ValidationError{Violations: []errors.Violation{&errors.ViolationData{ - FieldPath: []*validate.FieldPathElement{ + Field: []*validate.FieldPathElement{ errors.FieldPathElement(f.Descriptor), }, - RulePath: requiredRulePath, + Rule: requiredRulePath, + RuleValue: protoreflect.ValueOfBool(true), ConstraintID: "required", Message: "value is required", }}} diff --git a/internal/evaluator/oneof.go b/internal/evaluator/oneof.go index f1864fe..4f1ef00 100644 --- a/internal/evaluator/oneof.go +++ b/internal/evaluator/oneof.go @@ -36,7 +36,7 @@ func (o oneof) Evaluate(val protoreflect.Value, failFast bool) error { func (o oneof) EvaluateMessage(msg protoreflect.Message, _ bool) error { if o.Required && msg.WhichOneof(o.Descriptor) == nil { return &errors.ValidationError{Violations: []errors.Violation{&errors.ViolationData{ - FieldPath: []*validate.FieldPathElement{{ + Field: []*validate.FieldPathElement{{ FieldName: proto.String(string(o.Descriptor.Name())), }}, ConstraintID: "required", diff --git a/internal/expression/ast.go b/internal/expression/ast.go index 71f0924..ee03fe9 100644 --- a/internal/expression/ast.go +++ b/internal/expression/ast.go @@ -87,6 +87,7 @@ func (set ASTSet) ReduceResiduals(opts ...cel.ProgramOption) (ProgramSet, error) AST: residual, Source: ast.Source, Path: ast.Path, + Value: ast.Value, }) } } diff --git a/internal/expression/program.go b/internal/expression/program.go index 39ebcac..b89d2b8 100644 --- a/internal/expression/program.go +++ b/internal/expression/program.go @@ -111,7 +111,7 @@ func (expr compiledProgram) eval(bindings *Variable) (*errors.ViolationData, err return nil, nil } return &errors.ViolationData{ - RulePath: expr.Path, + Rule: expr.Path, RuleValue: expr.Value, ConstraintID: expr.Source.GetId(), Message: val, @@ -121,7 +121,7 @@ func (expr compiledProgram) eval(bindings *Variable) (*errors.ViolationData, err return nil, nil } return &errors.ViolationData{ - RulePath: expr.Path, + Rule: expr.Path, RuleValue: expr.Value, ConstraintID: expr.Source.GetId(), Message: expr.Source.GetMessage(), diff --git a/validator_example_test.go b/validator_example_test.go index e63ec0a..b9d3608 100644 --- a/validator_example_test.go +++ b/validator_example_test.go @@ -156,9 +156,12 @@ func ExampleValidationError() { err = validator.Validate(loc) var valErr *ValidationError if ok := errors.As(err, &valErr); ok { - violation := valErr.Violations[0].ToProto() - fmt.Println(FieldPathString(violation.GetFieldPath()), violation.GetConstraintId()) + violation := valErr.Violations[0] + violationProto := violation.ToProto() + fmt.Println(FieldPathString(violationProto.GetField()), violationProto.GetConstraintId()) + fmt.Println(violation.GetRuleValue(), violation.GetFieldValue()) } // output: lat double.gte_lte + // -90 999.999 } From 28a495852cef7e02c3789cce2ee33e40de43746c Mon Sep 17 00:00:00 2001 From: John Chadwick Date: Wed, 6 Nov 2024 19:07:23 -0500 Subject: [PATCH 06/24] Remove no-longer-used fieldpath test protos --- internal/gen/tests/example/v1/fieldpath.pb.go | 705 ------------------ proto/tests/example/v1/fieldpath.proto | 125 ---- 2 files changed, 830 deletions(-) delete mode 100644 internal/gen/tests/example/v1/fieldpath.pb.go delete mode 100644 proto/tests/example/v1/fieldpath.proto diff --git a/internal/gen/tests/example/v1/fieldpath.pb.go b/internal/gen/tests/example/v1/fieldpath.pb.go deleted file mode 100644 index 3c4c80e..0000000 --- a/internal/gen/tests/example/v1/fieldpath.pb.go +++ /dev/null @@ -1,705 +0,0 @@ -// Copyright 2023-2024 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -// Code generated by protoc-gen-go. DO NOT EDIT. -// versions: -// protoc-gen-go v1.34.1 -// protoc (unknown) -// source: tests/example/v1/fieldpath.proto - -package examplev1 - -import ( - _ "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" - reflect "reflect" - sync "sync" -) - -const ( - // Verify that this generated code is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) - // Verify that runtime/protoimpl is sufficiently up-to-date. - _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) -) - -type FieldPathSimple struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - extensionFields protoimpl.ExtensionFields - - Val uint64 `protobuf:"varint,1,opt,name=val" json:"val,omitempty"` - Nested *FieldPathNested `protobuf:"bytes,4,opt,name=nested" json:"nested,omitempty"` -} - -func (x *FieldPathSimple) Reset() { - *x = FieldPathSimple{} - if protoimpl.UnsafeEnabled { - mi := &file_tests_example_v1_fieldpath_proto_msgTypes[0] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *FieldPathSimple) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*FieldPathSimple) ProtoMessage() {} - -func (x *FieldPathSimple) ProtoReflect() protoreflect.Message { - mi := &file_tests_example_v1_fieldpath_proto_msgTypes[0] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use FieldPathSimple.ProtoReflect.Descriptor instead. -func (*FieldPathSimple) Descriptor() ([]byte, []int) { - return file_tests_example_v1_fieldpath_proto_rawDescGZIP(), []int{0} -} - -func (x *FieldPathSimple) GetVal() uint64 { - if x != nil { - return x.Val - } - return 0 -} - -func (x *FieldPathSimple) GetNested() *FieldPathNested { - if x != nil { - return x.Nested - } - return nil -} - -type FieldPathNested struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Val float64 `protobuf:"fixed64,1,opt,name=val" json:"val,omitempty"` -} - -func (x *FieldPathNested) Reset() { - *x = FieldPathNested{} - if protoimpl.UnsafeEnabled { - mi := &file_tests_example_v1_fieldpath_proto_msgTypes[1] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *FieldPathNested) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*FieldPathNested) ProtoMessage() {} - -func (x *FieldPathNested) ProtoReflect() protoreflect.Message { - mi := &file_tests_example_v1_fieldpath_proto_msgTypes[1] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use FieldPathNested.ProtoReflect.Descriptor instead. -func (*FieldPathNested) Descriptor() ([]byte, []int) { - return file_tests_example_v1_fieldpath_proto_rawDescGZIP(), []int{1} -} - -func (x *FieldPathNested) GetVal() float64 { - if x != nil { - return x.Val - } - return 0 -} - -type FieldPathMaps struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Int32Int32Map map[int32]int32 `protobuf:"bytes,1,rep,name=int32_int32_map,json=int32Int32Map" json:"int32_int32_map,omitempty" protobuf_key:"varint,1,opt,name=key" protobuf_val:"varint,2,opt,name=value"` - Int32Map map[int32]*FieldPathSimple `protobuf:"bytes,2,rep,name=int32_map,json=int32Map" json:"int32_map,omitempty" protobuf_key:"varint,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` - Int64Map map[int64]*FieldPathSimple `protobuf:"bytes,3,rep,name=int64_map,json=int64Map" json:"int64_map,omitempty" protobuf_key:"varint,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` - Sint32Map map[int32]*FieldPathSimple `protobuf:"bytes,4,rep,name=sint32_map,json=sint32Map" json:"sint32_map,omitempty" protobuf_key:"zigzag32,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` - Sint64Map map[int64]*FieldPathSimple `protobuf:"bytes,5,rep,name=sint64_map,json=sint64Map" json:"sint64_map,omitempty" protobuf_key:"zigzag64,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` - Sfixed32Map map[int32]*FieldPathSimple `protobuf:"bytes,6,rep,name=sfixed32_map,json=sfixed32Map" json:"sfixed32_map,omitempty" protobuf_key:"fixed32,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` - Sfixed64Map map[int64]*FieldPathSimple `protobuf:"bytes,7,rep,name=sfixed64_map,json=sfixed64Map" json:"sfixed64_map,omitempty" protobuf_key:"fixed64,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` - Uint32Map map[uint32]*FieldPathSimple `protobuf:"bytes,8,rep,name=uint32_map,json=uint32Map" json:"uint32_map,omitempty" protobuf_key:"varint,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` - Uint64Map map[uint64]*FieldPathSimple `protobuf:"bytes,9,rep,name=uint64_map,json=uint64Map" json:"uint64_map,omitempty" protobuf_key:"varint,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` - Fixed32Map map[uint32]*FieldPathSimple `protobuf:"bytes,10,rep,name=fixed32_map,json=fixed32Map" json:"fixed32_map,omitempty" protobuf_key:"fixed32,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` - Fixed64Map map[uint64]*FieldPathSimple `protobuf:"bytes,11,rep,name=fixed64_map,json=fixed64Map" json:"fixed64_map,omitempty" protobuf_key:"fixed64,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` - StringMap map[string]*FieldPathSimple `protobuf:"bytes,12,rep,name=string_map,json=stringMap" json:"string_map,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` - BoolMap map[bool]*FieldPathSimple `protobuf:"bytes,13,rep,name=bool_map,json=boolMap" json:"bool_map,omitempty" protobuf_key:"varint,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` -} - -func (x *FieldPathMaps) Reset() { - *x = FieldPathMaps{} - if protoimpl.UnsafeEnabled { - mi := &file_tests_example_v1_fieldpath_proto_msgTypes[2] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *FieldPathMaps) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*FieldPathMaps) ProtoMessage() {} - -func (x *FieldPathMaps) ProtoReflect() protoreflect.Message { - mi := &file_tests_example_v1_fieldpath_proto_msgTypes[2] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use FieldPathMaps.ProtoReflect.Descriptor instead. -func (*FieldPathMaps) Descriptor() ([]byte, []int) { - return file_tests_example_v1_fieldpath_proto_rawDescGZIP(), []int{2} -} - -func (x *FieldPathMaps) GetInt32Int32Map() map[int32]int32 { - if x != nil { - return x.Int32Int32Map - } - return nil -} - -func (x *FieldPathMaps) GetInt32Map() map[int32]*FieldPathSimple { - if x != nil { - return x.Int32Map - } - return nil -} - -func (x *FieldPathMaps) GetInt64Map() map[int64]*FieldPathSimple { - if x != nil { - return x.Int64Map - } - return nil -} - -func (x *FieldPathMaps) GetSint32Map() map[int32]*FieldPathSimple { - if x != nil { - return x.Sint32Map - } - return nil -} - -func (x *FieldPathMaps) GetSint64Map() map[int64]*FieldPathSimple { - if x != nil { - return x.Sint64Map - } - return nil -} - -func (x *FieldPathMaps) GetSfixed32Map() map[int32]*FieldPathSimple { - if x != nil { - return x.Sfixed32Map - } - return nil -} - -func (x *FieldPathMaps) GetSfixed64Map() map[int64]*FieldPathSimple { - if x != nil { - return x.Sfixed64Map - } - return nil -} - -func (x *FieldPathMaps) GetUint32Map() map[uint32]*FieldPathSimple { - if x != nil { - return x.Uint32Map - } - return nil -} - -func (x *FieldPathMaps) GetUint64Map() map[uint64]*FieldPathSimple { - if x != nil { - return x.Uint64Map - } - return nil -} - -func (x *FieldPathMaps) GetFixed32Map() map[uint32]*FieldPathSimple { - if x != nil { - return x.Fixed32Map - } - return nil -} - -func (x *FieldPathMaps) GetFixed64Map() map[uint64]*FieldPathSimple { - if x != nil { - return x.Fixed64Map - } - return nil -} - -func (x *FieldPathMaps) GetStringMap() map[string]*FieldPathSimple { - if x != nil { - return x.StringMap - } - return nil -} - -func (x *FieldPathMaps) GetBoolMap() map[bool]*FieldPathSimple { - if x != nil { - return x.BoolMap - } - return nil -} - -type FieldPathRepeated struct { - state protoimpl.MessageState - sizeCache protoimpl.SizeCache - unknownFields protoimpl.UnknownFields - - Val []int32 `protobuf:"varint,1,rep,packed,name=val" json:"val,omitempty"` - Msg []*FieldPathSimple `protobuf:"bytes,2,rep,name=msg" json:"msg,omitempty"` -} - -func (x *FieldPathRepeated) Reset() { - *x = FieldPathRepeated{} - if protoimpl.UnsafeEnabled { - mi := &file_tests_example_v1_fieldpath_proto_msgTypes[3] - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - ms.StoreMessageInfo(mi) - } -} - -func (x *FieldPathRepeated) String() string { - return protoimpl.X.MessageStringOf(x) -} - -func (*FieldPathRepeated) ProtoMessage() {} - -func (x *FieldPathRepeated) ProtoReflect() protoreflect.Message { - mi := &file_tests_example_v1_fieldpath_proto_msgTypes[3] - if protoimpl.UnsafeEnabled && x != nil { - ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) - if ms.LoadMessageInfo() == nil { - ms.StoreMessageInfo(mi) - } - return ms - } - return mi.MessageOf(x) -} - -// Deprecated: Use FieldPathRepeated.ProtoReflect.Descriptor instead. -func (*FieldPathRepeated) Descriptor() ([]byte, []int) { - return file_tests_example_v1_fieldpath_proto_rawDescGZIP(), []int{3} -} - -func (x *FieldPathRepeated) GetVal() []int32 { - if x != nil { - return x.Val - } - return nil -} - -func (x *FieldPathRepeated) GetMsg() []*FieldPathSimple { - if x != nil { - return x.Msg - } - return nil -} - -var file_tests_example_v1_fieldpath_proto_extTypes = []protoimpl.ExtensionInfo{ - { - ExtendedType: (*FieldPathSimple)(nil), - ExtensionType: ([]uint64)(nil), - Field: 1000, - Name: "tests.example.v1.ext", - Tag: "varint,1000,rep,packed,name=ext", - Filename: "tests/example/v1/fieldpath.proto", - }, -} - -// Extension fields to FieldPathSimple. -var ( - // repeated uint64 ext = 1000; - E_Ext = &file_tests_example_v1_fieldpath_proto_extTypes[0] -) - -var File_tests_example_v1_fieldpath_proto protoreflect.FileDescriptor - -var file_tests_example_v1_fieldpath_proto_rawDesc = []byte{ - 0x0a, 0x20, 0x74, 0x65, 0x73, 0x74, 0x73, 0x2f, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2f, - 0x76, 0x31, 0x2f, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x70, 0x61, 0x74, 0x68, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x12, 0x10, 0x74, 0x65, 0x73, 0x74, 0x73, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, - 0x65, 0x2e, 0x76, 0x31, 0x1a, 0x1b, 0x62, 0x75, 0x66, 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, - 0x74, 0x65, 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x22, 0x72, 0x0a, 0x0f, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x50, 0x61, 0x74, 0x68, 0x53, 0x69, - 0x6d, 0x70, 0x6c, 0x65, 0x12, 0x19, 0x0a, 0x03, 0x76, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x04, 0x42, 0x07, 0xba, 0x48, 0x04, 0x32, 0x02, 0x20, 0x01, 0x52, 0x03, 0x76, 0x61, 0x6c, 0x12, - 0x39, 0x0a, 0x06, 0x6e, 0x65, 0x73, 0x74, 0x65, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x21, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x73, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, - 0x76, 0x31, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x50, 0x61, 0x74, 0x68, 0x4e, 0x65, 0x73, 0x74, - 0x65, 0x64, 0x52, 0x06, 0x6e, 0x65, 0x73, 0x74, 0x65, 0x64, 0x2a, 0x09, 0x08, 0xe8, 0x07, 0x10, - 0x80, 0x80, 0x80, 0x80, 0x02, 0x22, 0x3c, 0x0a, 0x0f, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x50, 0x61, - 0x74, 0x68, 0x4e, 0x65, 0x73, 0x74, 0x65, 0x64, 0x12, 0x29, 0x0a, 0x03, 0x76, 0x61, 0x6c, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x01, 0x42, 0x17, 0xba, 0x48, 0x14, 0x12, 0x12, 0x19, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0xf0, 0x3f, 0x29, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xf0, 0xbf, 0x52, 0x03, - 0x76, 0x61, 0x6c, 0x22, 0xd9, 0x13, 0x0a, 0x0d, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x50, 0x61, 0x74, - 0x68, 0x4d, 0x61, 0x70, 0x73, 0x12, 0x70, 0x0a, 0x0f, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x5f, 0x69, - 0x6e, 0x74, 0x33, 0x32, 0x5f, 0x6d, 0x61, 0x70, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x32, - 0x2e, 0x74, 0x65, 0x73, 0x74, 0x73, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x76, - 0x31, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x50, 0x61, 0x74, 0x68, 0x4d, 0x61, 0x70, 0x73, 0x2e, - 0x49, 0x6e, 0x74, 0x33, 0x32, 0x49, 0x6e, 0x74, 0x33, 0x32, 0x4d, 0x61, 0x70, 0x45, 0x6e, 0x74, - 0x72, 0x79, 0x42, 0x14, 0xba, 0x48, 0x11, 0x9a, 0x01, 0x0e, 0x10, 0x03, 0x22, 0x04, 0x1a, 0x02, - 0x20, 0x01, 0x2a, 0x04, 0x1a, 0x02, 0x20, 0x01, 0x52, 0x0d, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x49, - 0x6e, 0x74, 0x33, 0x32, 0x4d, 0x61, 0x70, 0x12, 0x5a, 0x0a, 0x09, 0x69, 0x6e, 0x74, 0x33, 0x32, - 0x5f, 0x6d, 0x61, 0x70, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x74, 0x65, 0x73, - 0x74, 0x73, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, - 0x65, 0x6c, 0x64, 0x50, 0x61, 0x74, 0x68, 0x4d, 0x61, 0x70, 0x73, 0x2e, 0x49, 0x6e, 0x74, 0x33, - 0x32, 0x4d, 0x61, 0x70, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x42, 0x0e, 0xba, 0x48, 0x0b, 0x9a, 0x01, - 0x08, 0x10, 0x03, 0x22, 0x04, 0x1a, 0x02, 0x20, 0x01, 0x52, 0x08, 0x69, 0x6e, 0x74, 0x33, 0x32, - 0x4d, 0x61, 0x70, 0x12, 0x5a, 0x0a, 0x09, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x5f, 0x6d, 0x61, 0x70, - 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x73, 0x2e, 0x65, - 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x50, - 0x61, 0x74, 0x68, 0x4d, 0x61, 0x70, 0x73, 0x2e, 0x49, 0x6e, 0x74, 0x36, 0x34, 0x4d, 0x61, 0x70, - 0x45, 0x6e, 0x74, 0x72, 0x79, 0x42, 0x0e, 0xba, 0x48, 0x0b, 0x9a, 0x01, 0x08, 0x10, 0x03, 0x22, - 0x04, 0x22, 0x02, 0x20, 0x01, 0x52, 0x08, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x4d, 0x61, 0x70, 0x12, - 0x5d, 0x0a, 0x0a, 0x73, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x5f, 0x6d, 0x61, 0x70, 0x18, 0x04, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x73, 0x2e, 0x65, 0x78, 0x61, 0x6d, - 0x70, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x50, 0x61, 0x74, 0x68, - 0x4d, 0x61, 0x70, 0x73, 0x2e, 0x53, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x4d, 0x61, 0x70, 0x45, 0x6e, - 0x74, 0x72, 0x79, 0x42, 0x0e, 0xba, 0x48, 0x0b, 0x9a, 0x01, 0x08, 0x10, 0x03, 0x22, 0x04, 0x3a, - 0x02, 0x20, 0x02, 0x52, 0x09, 0x73, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x4d, 0x61, 0x70, 0x12, 0x5d, - 0x0a, 0x0a, 0x73, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x5f, 0x6d, 0x61, 0x70, 0x18, 0x05, 0x20, 0x03, - 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x73, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, - 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x50, 0x61, 0x74, 0x68, 0x4d, - 0x61, 0x70, 0x73, 0x2e, 0x53, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x4d, 0x61, 0x70, 0x45, 0x6e, 0x74, - 0x72, 0x79, 0x42, 0x0e, 0xba, 0x48, 0x0b, 0x9a, 0x01, 0x08, 0x10, 0x03, 0x22, 0x04, 0x42, 0x02, - 0x20, 0x02, 0x52, 0x09, 0x73, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x4d, 0x61, 0x70, 0x12, 0x66, 0x0a, - 0x0c, 0x73, 0x66, 0x69, 0x78, 0x65, 0x64, 0x33, 0x32, 0x5f, 0x6d, 0x61, 0x70, 0x18, 0x06, 0x20, - 0x03, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x73, 0x2e, 0x65, 0x78, 0x61, 0x6d, - 0x70, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x50, 0x61, 0x74, 0x68, - 0x4d, 0x61, 0x70, 0x73, 0x2e, 0x53, 0x66, 0x69, 0x78, 0x65, 0x64, 0x33, 0x32, 0x4d, 0x61, 0x70, - 0x45, 0x6e, 0x74, 0x72, 0x79, 0x42, 0x11, 0xba, 0x48, 0x0e, 0x9a, 0x01, 0x0b, 0x10, 0x03, 0x22, - 0x07, 0x5a, 0x05, 0x25, 0x01, 0x00, 0x00, 0x00, 0x52, 0x0b, 0x73, 0x66, 0x69, 0x78, 0x65, 0x64, - 0x33, 0x32, 0x4d, 0x61, 0x70, 0x12, 0x6a, 0x0a, 0x0c, 0x73, 0x66, 0x69, 0x78, 0x65, 0x64, 0x36, - 0x34, 0x5f, 0x6d, 0x61, 0x70, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x30, 0x2e, 0x74, 0x65, - 0x73, 0x74, 0x73, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x46, - 0x69, 0x65, 0x6c, 0x64, 0x50, 0x61, 0x74, 0x68, 0x4d, 0x61, 0x70, 0x73, 0x2e, 0x53, 0x66, 0x69, - 0x78, 0x65, 0x64, 0x36, 0x34, 0x4d, 0x61, 0x70, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x42, 0x15, 0xba, - 0x48, 0x12, 0x9a, 0x01, 0x0f, 0x10, 0x03, 0x22, 0x0b, 0x62, 0x09, 0x21, 0x01, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x52, 0x0b, 0x73, 0x66, 0x69, 0x78, 0x65, 0x64, 0x36, 0x34, 0x4d, 0x61, - 0x70, 0x12, 0x5d, 0x0a, 0x0a, 0x75, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x5f, 0x6d, 0x61, 0x70, 0x18, - 0x08, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x73, 0x2e, 0x65, 0x78, - 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x50, 0x61, - 0x74, 0x68, 0x4d, 0x61, 0x70, 0x73, 0x2e, 0x55, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x4d, 0x61, 0x70, - 0x45, 0x6e, 0x74, 0x72, 0x79, 0x42, 0x0e, 0xba, 0x48, 0x0b, 0x9a, 0x01, 0x08, 0x10, 0x03, 0x22, - 0x04, 0x2a, 0x02, 0x20, 0x01, 0x52, 0x09, 0x75, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x4d, 0x61, 0x70, - 0x12, 0x5d, 0x0a, 0x0a, 0x75, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x5f, 0x6d, 0x61, 0x70, 0x18, 0x09, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x73, 0x2e, 0x65, 0x78, 0x61, - 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x50, 0x61, 0x74, - 0x68, 0x4d, 0x61, 0x70, 0x73, 0x2e, 0x55, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x4d, 0x61, 0x70, 0x45, - 0x6e, 0x74, 0x72, 0x79, 0x42, 0x0e, 0xba, 0x48, 0x0b, 0x9a, 0x01, 0x08, 0x10, 0x03, 0x22, 0x04, - 0x32, 0x02, 0x20, 0x01, 0x52, 0x09, 0x75, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x4d, 0x61, 0x70, 0x12, - 0x63, 0x0a, 0x0b, 0x66, 0x69, 0x78, 0x65, 0x64, 0x33, 0x32, 0x5f, 0x6d, 0x61, 0x70, 0x18, 0x0a, - 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2f, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x73, 0x2e, 0x65, 0x78, 0x61, - 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x50, 0x61, 0x74, - 0x68, 0x4d, 0x61, 0x70, 0x73, 0x2e, 0x46, 0x69, 0x78, 0x65, 0x64, 0x33, 0x32, 0x4d, 0x61, 0x70, - 0x45, 0x6e, 0x74, 0x72, 0x79, 0x42, 0x11, 0xba, 0x48, 0x0e, 0x9a, 0x01, 0x0b, 0x10, 0x03, 0x22, - 0x07, 0x4a, 0x05, 0x25, 0x01, 0x00, 0x00, 0x00, 0x52, 0x0a, 0x66, 0x69, 0x78, 0x65, 0x64, 0x33, - 0x32, 0x4d, 0x61, 0x70, 0x12, 0x67, 0x0a, 0x0b, 0x66, 0x69, 0x78, 0x65, 0x64, 0x36, 0x34, 0x5f, - 0x6d, 0x61, 0x70, 0x18, 0x0b, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2f, 0x2e, 0x74, 0x65, 0x73, 0x74, - 0x73, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, 0x65, - 0x6c, 0x64, 0x50, 0x61, 0x74, 0x68, 0x4d, 0x61, 0x70, 0x73, 0x2e, 0x46, 0x69, 0x78, 0x65, 0x64, - 0x36, 0x34, 0x4d, 0x61, 0x70, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x42, 0x15, 0xba, 0x48, 0x12, 0x9a, - 0x01, 0x0f, 0x10, 0x03, 0x22, 0x0b, 0x52, 0x09, 0x21, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x52, 0x0a, 0x66, 0x69, 0x78, 0x65, 0x64, 0x36, 0x34, 0x4d, 0x61, 0x70, 0x12, 0x5d, 0x0a, - 0x0a, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x5f, 0x6d, 0x61, 0x70, 0x18, 0x0c, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x2e, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x73, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, - 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x50, 0x61, 0x74, 0x68, 0x4d, 0x61, - 0x70, 0x73, 0x2e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x4d, 0x61, 0x70, 0x45, 0x6e, 0x74, 0x72, - 0x79, 0x42, 0x0e, 0xba, 0x48, 0x0b, 0x9a, 0x01, 0x08, 0x10, 0x03, 0x22, 0x04, 0x72, 0x02, 0x10, - 0x02, 0x52, 0x09, 0x73, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x4d, 0x61, 0x70, 0x12, 0x51, 0x0a, 0x08, - 0x62, 0x6f, 0x6f, 0x6c, 0x5f, 0x6d, 0x61, 0x70, 0x18, 0x0d, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2c, - 0x2e, 0x74, 0x65, 0x73, 0x74, 0x73, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x76, - 0x31, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x50, 0x61, 0x74, 0x68, 0x4d, 0x61, 0x70, 0x73, 0x2e, - 0x42, 0x6f, 0x6f, 0x6c, 0x4d, 0x61, 0x70, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x42, 0x08, 0xba, 0x48, - 0x05, 0x9a, 0x01, 0x02, 0x10, 0x03, 0x52, 0x07, 0x62, 0x6f, 0x6f, 0x6c, 0x4d, 0x61, 0x70, 0x1a, - 0x40, 0x0a, 0x12, 0x49, 0x6e, 0x74, 0x33, 0x32, 0x49, 0x6e, 0x74, 0x33, 0x32, 0x4d, 0x61, 0x70, - 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, - 0x28, 0x05, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, - 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, - 0x01, 0x1a, 0x5e, 0x0a, 0x0d, 0x49, 0x6e, 0x74, 0x33, 0x32, 0x4d, 0x61, 0x70, 0x45, 0x6e, 0x74, - 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, - 0x03, 0x6b, 0x65, 0x79, 0x12, 0x37, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x73, 0x2e, 0x65, 0x78, 0x61, 0x6d, - 0x70, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x50, 0x61, 0x74, 0x68, - 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, - 0x01, 0x1a, 0x5e, 0x0a, 0x0d, 0x49, 0x6e, 0x74, 0x36, 0x34, 0x4d, 0x61, 0x70, 0x45, 0x6e, 0x74, - 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, - 0x03, 0x6b, 0x65, 0x79, 0x12, 0x37, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x73, 0x2e, 0x65, 0x78, 0x61, 0x6d, - 0x70, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x50, 0x61, 0x74, 0x68, - 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, - 0x01, 0x1a, 0x5f, 0x0a, 0x0e, 0x53, 0x69, 0x6e, 0x74, 0x33, 0x32, 0x4d, 0x61, 0x70, 0x45, 0x6e, - 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x11, - 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x37, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x73, 0x2e, 0x65, 0x78, 0x61, - 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x50, 0x61, 0x74, - 0x68, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, - 0x38, 0x01, 0x1a, 0x5f, 0x0a, 0x0e, 0x53, 0x69, 0x6e, 0x74, 0x36, 0x34, 0x4d, 0x61, 0x70, 0x45, - 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, - 0x12, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x37, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, - 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x73, 0x2e, 0x65, 0x78, - 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x50, 0x61, - 0x74, 0x68, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, - 0x02, 0x38, 0x01, 0x1a, 0x61, 0x0a, 0x10, 0x53, 0x66, 0x69, 0x78, 0x65, 0x64, 0x33, 0x32, 0x4d, - 0x61, 0x70, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x0f, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x37, 0x0a, 0x05, 0x76, 0x61, 0x6c, - 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x73, - 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, 0x65, 0x6c, - 0x64, 0x50, 0x61, 0x74, 0x68, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, - 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x61, 0x0a, 0x10, 0x53, 0x66, 0x69, 0x78, 0x65, 0x64, - 0x36, 0x34, 0x4d, 0x61, 0x70, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, - 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x10, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x37, 0x0a, 0x05, - 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x74, 0x65, - 0x73, 0x74, 0x73, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x46, - 0x69, 0x65, 0x6c, 0x64, 0x50, 0x61, 0x74, 0x68, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x05, - 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x5f, 0x0a, 0x0e, 0x55, 0x69, 0x6e, - 0x74, 0x33, 0x32, 0x4d, 0x61, 0x70, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, - 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x37, 0x0a, - 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x74, - 0x65, 0x73, 0x74, 0x73, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, - 0x46, 0x69, 0x65, 0x6c, 0x64, 0x50, 0x61, 0x74, 0x68, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x52, - 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x5f, 0x0a, 0x0e, 0x55, 0x69, - 0x6e, 0x74, 0x36, 0x34, 0x4d, 0x61, 0x70, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, - 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x04, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x37, - 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x21, 0x2e, - 0x74, 0x65, 0x73, 0x74, 0x73, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x76, 0x31, - 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x50, 0x61, 0x74, 0x68, 0x53, 0x69, 0x6d, 0x70, 0x6c, 0x65, - 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x60, 0x0a, 0x0f, 0x46, - 0x69, 0x78, 0x65, 0x64, 0x33, 0x32, 0x4d, 0x61, 0x70, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, - 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x07, 0x52, 0x03, 0x6b, 0x65, 0x79, - 0x12, 0x37, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x21, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x73, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, - 0x76, 0x31, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x50, 0x61, 0x74, 0x68, 0x53, 0x69, 0x6d, 0x70, - 0x6c, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, 0x60, 0x0a, - 0x0f, 0x46, 0x69, 0x78, 0x65, 0x64, 0x36, 0x34, 0x4d, 0x61, 0x70, 0x45, 0x6e, 0x74, 0x72, 0x79, - 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x06, 0x52, 0x03, 0x6b, - 0x65, 0x79, 0x12, 0x37, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x21, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x73, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, - 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x50, 0x61, 0x74, 0x68, 0x53, 0x69, - 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x1a, - 0x5f, 0x0a, 0x0e, 0x53, 0x74, 0x72, 0x69, 0x6e, 0x67, 0x4d, 0x61, 0x70, 0x45, 0x6e, 0x74, 0x72, - 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, - 0x6b, 0x65, 0x79, 0x12, 0x37, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, - 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x73, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, - 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x50, 0x61, 0x74, 0x68, 0x53, - 0x69, 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, - 0x1a, 0x5d, 0x0a, 0x0c, 0x42, 0x6f, 0x6f, 0x6c, 0x4d, 0x61, 0x70, 0x45, 0x6e, 0x74, 0x72, 0x79, - 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x03, 0x6b, - 0x65, 0x79, 0x12, 0x37, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x0b, 0x32, 0x21, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x73, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, - 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x50, 0x61, 0x74, 0x68, 0x53, 0x69, - 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x3a, 0x02, 0x38, 0x01, 0x22, - 0x68, 0x0a, 0x11, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x50, 0x61, 0x74, 0x68, 0x52, 0x65, 0x70, 0x65, - 0x61, 0x74, 0x65, 0x64, 0x12, 0x1e, 0x0a, 0x03, 0x76, 0x61, 0x6c, 0x18, 0x01, 0x20, 0x03, 0x28, - 0x05, 0x42, 0x0c, 0xba, 0x48, 0x09, 0x92, 0x01, 0x06, 0x22, 0x04, 0x1a, 0x02, 0x20, 0x01, 0x52, - 0x03, 0x76, 0x61, 0x6c, 0x12, 0x33, 0x0a, 0x03, 0x6d, 0x73, 0x67, 0x18, 0x02, 0x20, 0x03, 0x28, - 0x0b, 0x32, 0x21, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x73, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, - 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x50, 0x61, 0x74, 0x68, 0x53, 0x69, - 0x6d, 0x70, 0x6c, 0x65, 0x52, 0x03, 0x6d, 0x73, 0x67, 0x3a, 0x42, 0x0a, 0x03, 0x65, 0x78, 0x74, - 0x12, 0x21, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x73, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, - 0x2e, 0x76, 0x31, 0x2e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x50, 0x61, 0x74, 0x68, 0x53, 0x69, 0x6d, - 0x70, 0x6c, 0x65, 0x18, 0xe8, 0x07, 0x20, 0x03, 0x28, 0x04, 0x42, 0x0c, 0xba, 0x48, 0x09, 0x92, - 0x01, 0x06, 0x22, 0x04, 0x32, 0x02, 0x20, 0x01, 0x52, 0x03, 0x65, 0x78, 0x74, 0x42, 0xdb, 0x01, - 0x0a, 0x14, 0x63, 0x6f, 0x6d, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x73, 0x2e, 0x65, 0x78, 0x61, 0x6d, - 0x70, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x42, 0x0e, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x70, 0x61, 0x74, - 0x68, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x4c, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, - 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x62, 0x75, 0x66, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x2f, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x2d, 0x67, 0x6f, 0x2f, 0x69, - 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x74, 0x65, 0x73, 0x74, - 0x73, 0x2f, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2f, 0x76, 0x31, 0x3b, 0x65, 0x78, 0x61, - 0x6d, 0x70, 0x6c, 0x65, 0x76, 0x31, 0xa2, 0x02, 0x03, 0x54, 0x45, 0x58, 0xaa, 0x02, 0x10, 0x54, - 0x65, 0x73, 0x74, 0x73, 0x2e, 0x45, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x56, 0x31, 0xca, - 0x02, 0x10, 0x54, 0x65, 0x73, 0x74, 0x73, 0x5c, 0x45, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x5c, - 0x56, 0x31, 0xe2, 0x02, 0x1c, 0x54, 0x65, 0x73, 0x74, 0x73, 0x5c, 0x45, 0x78, 0x61, 0x6d, 0x70, - 0x6c, 0x65, 0x5c, 0x56, 0x31, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, - 0x61, 0xea, 0x02, 0x12, 0x54, 0x65, 0x73, 0x74, 0x73, 0x3a, 0x3a, 0x45, 0x78, 0x61, 0x6d, 0x70, - 0x6c, 0x65, 0x3a, 0x3a, 0x56, 0x31, 0x92, 0x03, 0x02, 0x08, 0x02, 0x62, 0x08, 0x65, 0x64, 0x69, - 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x70, 0xe8, 0x07, -} - -var ( - file_tests_example_v1_fieldpath_proto_rawDescOnce sync.Once - file_tests_example_v1_fieldpath_proto_rawDescData = file_tests_example_v1_fieldpath_proto_rawDesc -) - -func file_tests_example_v1_fieldpath_proto_rawDescGZIP() []byte { - file_tests_example_v1_fieldpath_proto_rawDescOnce.Do(func() { - file_tests_example_v1_fieldpath_proto_rawDescData = protoimpl.X.CompressGZIP(file_tests_example_v1_fieldpath_proto_rawDescData) - }) - return file_tests_example_v1_fieldpath_proto_rawDescData -} - -var file_tests_example_v1_fieldpath_proto_msgTypes = make([]protoimpl.MessageInfo, 17) -var file_tests_example_v1_fieldpath_proto_goTypes = []interface{}{ - (*FieldPathSimple)(nil), // 0: tests.example.v1.FieldPathSimple - (*FieldPathNested)(nil), // 1: tests.example.v1.FieldPathNested - (*FieldPathMaps)(nil), // 2: tests.example.v1.FieldPathMaps - (*FieldPathRepeated)(nil), // 3: tests.example.v1.FieldPathRepeated - nil, // 4: tests.example.v1.FieldPathMaps.Int32Int32MapEntry - nil, // 5: tests.example.v1.FieldPathMaps.Int32MapEntry - nil, // 6: tests.example.v1.FieldPathMaps.Int64MapEntry - nil, // 7: tests.example.v1.FieldPathMaps.Sint32MapEntry - nil, // 8: tests.example.v1.FieldPathMaps.Sint64MapEntry - nil, // 9: tests.example.v1.FieldPathMaps.Sfixed32MapEntry - nil, // 10: tests.example.v1.FieldPathMaps.Sfixed64MapEntry - nil, // 11: tests.example.v1.FieldPathMaps.Uint32MapEntry - nil, // 12: tests.example.v1.FieldPathMaps.Uint64MapEntry - nil, // 13: tests.example.v1.FieldPathMaps.Fixed32MapEntry - nil, // 14: tests.example.v1.FieldPathMaps.Fixed64MapEntry - nil, // 15: tests.example.v1.FieldPathMaps.StringMapEntry - nil, // 16: tests.example.v1.FieldPathMaps.BoolMapEntry -} -var file_tests_example_v1_fieldpath_proto_depIdxs = []int32{ - 1, // 0: tests.example.v1.FieldPathSimple.nested:type_name -> tests.example.v1.FieldPathNested - 4, // 1: tests.example.v1.FieldPathMaps.int32_int32_map:type_name -> tests.example.v1.FieldPathMaps.Int32Int32MapEntry - 5, // 2: tests.example.v1.FieldPathMaps.int32_map:type_name -> tests.example.v1.FieldPathMaps.Int32MapEntry - 6, // 3: tests.example.v1.FieldPathMaps.int64_map:type_name -> tests.example.v1.FieldPathMaps.Int64MapEntry - 7, // 4: tests.example.v1.FieldPathMaps.sint32_map:type_name -> tests.example.v1.FieldPathMaps.Sint32MapEntry - 8, // 5: tests.example.v1.FieldPathMaps.sint64_map:type_name -> tests.example.v1.FieldPathMaps.Sint64MapEntry - 9, // 6: tests.example.v1.FieldPathMaps.sfixed32_map:type_name -> tests.example.v1.FieldPathMaps.Sfixed32MapEntry - 10, // 7: tests.example.v1.FieldPathMaps.sfixed64_map:type_name -> tests.example.v1.FieldPathMaps.Sfixed64MapEntry - 11, // 8: tests.example.v1.FieldPathMaps.uint32_map:type_name -> tests.example.v1.FieldPathMaps.Uint32MapEntry - 12, // 9: tests.example.v1.FieldPathMaps.uint64_map:type_name -> tests.example.v1.FieldPathMaps.Uint64MapEntry - 13, // 10: tests.example.v1.FieldPathMaps.fixed32_map:type_name -> tests.example.v1.FieldPathMaps.Fixed32MapEntry - 14, // 11: tests.example.v1.FieldPathMaps.fixed64_map:type_name -> tests.example.v1.FieldPathMaps.Fixed64MapEntry - 15, // 12: tests.example.v1.FieldPathMaps.string_map:type_name -> tests.example.v1.FieldPathMaps.StringMapEntry - 16, // 13: tests.example.v1.FieldPathMaps.bool_map:type_name -> tests.example.v1.FieldPathMaps.BoolMapEntry - 0, // 14: tests.example.v1.FieldPathRepeated.msg:type_name -> tests.example.v1.FieldPathSimple - 0, // 15: tests.example.v1.FieldPathMaps.Int32MapEntry.value:type_name -> tests.example.v1.FieldPathSimple - 0, // 16: tests.example.v1.FieldPathMaps.Int64MapEntry.value:type_name -> tests.example.v1.FieldPathSimple - 0, // 17: tests.example.v1.FieldPathMaps.Sint32MapEntry.value:type_name -> tests.example.v1.FieldPathSimple - 0, // 18: tests.example.v1.FieldPathMaps.Sint64MapEntry.value:type_name -> tests.example.v1.FieldPathSimple - 0, // 19: tests.example.v1.FieldPathMaps.Sfixed32MapEntry.value:type_name -> tests.example.v1.FieldPathSimple - 0, // 20: tests.example.v1.FieldPathMaps.Sfixed64MapEntry.value:type_name -> tests.example.v1.FieldPathSimple - 0, // 21: tests.example.v1.FieldPathMaps.Uint32MapEntry.value:type_name -> tests.example.v1.FieldPathSimple - 0, // 22: tests.example.v1.FieldPathMaps.Uint64MapEntry.value:type_name -> tests.example.v1.FieldPathSimple - 0, // 23: tests.example.v1.FieldPathMaps.Fixed32MapEntry.value:type_name -> tests.example.v1.FieldPathSimple - 0, // 24: tests.example.v1.FieldPathMaps.Fixed64MapEntry.value:type_name -> tests.example.v1.FieldPathSimple - 0, // 25: tests.example.v1.FieldPathMaps.StringMapEntry.value:type_name -> tests.example.v1.FieldPathSimple - 0, // 26: tests.example.v1.FieldPathMaps.BoolMapEntry.value:type_name -> tests.example.v1.FieldPathSimple - 0, // 27: tests.example.v1.ext:extendee -> tests.example.v1.FieldPathSimple - 28, // [28:28] is the sub-list for method output_type - 28, // [28:28] is the sub-list for method input_type - 28, // [28:28] is the sub-list for extension type_name - 27, // [27:28] is the sub-list for extension extendee - 0, // [0:27] is the sub-list for field type_name -} - -func init() { file_tests_example_v1_fieldpath_proto_init() } -func file_tests_example_v1_fieldpath_proto_init() { - if File_tests_example_v1_fieldpath_proto != nil { - return - } - if !protoimpl.UnsafeEnabled { - file_tests_example_v1_fieldpath_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*FieldPathSimple); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - case 3: - return &v.extensionFields - default: - return nil - } - } - file_tests_example_v1_fieldpath_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*FieldPathNested); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tests_example_v1_fieldpath_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*FieldPathMaps); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - file_tests_example_v1_fieldpath_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*FieldPathRepeated); i { - case 0: - return &v.state - case 1: - return &v.sizeCache - case 2: - return &v.unknownFields - default: - return nil - } - } - } - type x struct{} - out := protoimpl.TypeBuilder{ - File: protoimpl.DescBuilder{ - GoPackagePath: reflect.TypeOf(x{}).PkgPath(), - RawDescriptor: file_tests_example_v1_fieldpath_proto_rawDesc, - NumEnums: 0, - NumMessages: 17, - NumExtensions: 1, - NumServices: 0, - }, - GoTypes: file_tests_example_v1_fieldpath_proto_goTypes, - DependencyIndexes: file_tests_example_v1_fieldpath_proto_depIdxs, - MessageInfos: file_tests_example_v1_fieldpath_proto_msgTypes, - ExtensionInfos: file_tests_example_v1_fieldpath_proto_extTypes, - }.Build() - File_tests_example_v1_fieldpath_proto = out.File - file_tests_example_v1_fieldpath_proto_rawDesc = nil - file_tests_example_v1_fieldpath_proto_goTypes = nil - file_tests_example_v1_fieldpath_proto_depIdxs = nil -} diff --git a/proto/tests/example/v1/fieldpath.proto b/proto/tests/example/v1/fieldpath.proto deleted file mode 100644 index 86177a3..0000000 --- a/proto/tests/example/v1/fieldpath.proto +++ /dev/null @@ -1,125 +0,0 @@ -// Copyright 2023-2024 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -edition = "2023"; - -package tests.example.v1; - -import "buf/validate/validate.proto"; - -option features.field_presence = IMPLICIT; - -message FieldPathSimple { - uint64 val = 1 [(buf.validate.field).uint64.gt = 1]; - - FieldPathNested nested = 4; - - extensions 1000 to max; -} - -extend FieldPathSimple { - repeated uint64 ext = 1000 [(buf.validate.field).repeated.items.uint64.gt = 1]; -} - -message FieldPathNested { - double val = 1 [(buf.validate.field).double = { - gte: -1 - lte: 1 - }]; -} - -message FieldPathMaps { - map int32_int32_map = 1 [(buf.validate.field).map = { - max_pairs: 3 - keys: { - int32: {gt: 1} - } - values: { - int32: {gt: 1} - } - }]; - map int32_map = 2 [(buf.validate.field).map = { - max_pairs: 3 - keys: { - int32: {gt: 1} - } - }]; - map int64_map = 3 [(buf.validate.field).map = { - max_pairs: 3 - keys: { - int64: {gt: 1} - } - }]; - map sint32_map = 4 [(buf.validate.field).map = { - max_pairs: 3 - keys: { - sint32: {gt: 1} - } - }]; - map sint64_map = 5 [(buf.validate.field).map = { - max_pairs: 3 - keys: { - sint64: {gt: 1} - } - }]; - map sfixed32_map = 6 [(buf.validate.field).map = { - max_pairs: 3 - keys: { - sfixed32: {gt: 1} - } - }]; - map sfixed64_map = 7 [(buf.validate.field).map = { - max_pairs: 3 - keys: { - sfixed64: {gt: 1} - } - }]; - map uint32_map = 8 [(buf.validate.field).map = { - max_pairs: 3 - keys: { - uint32: {gt: 1} - } - }]; - map uint64_map = 9 [(buf.validate.field).map = { - max_pairs: 3 - keys: { - uint64: {gt: 1} - } - }]; - map fixed32_map = 10 [(buf.validate.field).map = { - max_pairs: 3 - keys: { - fixed32: {gt: 1} - } - }]; - map fixed64_map = 11 [(buf.validate.field).map = { - max_pairs: 3 - keys: { - fixed64: {gt: 1} - } - }]; - map string_map = 12 [(buf.validate.field).map = { - max_pairs: 3 - keys: { - string: {min_len: 2} - } - }]; - map bool_map = 13 [(buf.validate.field).map = {max_pairs: 3}]; -} - -message FieldPathRepeated { - repeated int32 val = 1 [(buf.validate.field).repeated.items.int32.gt = 1]; - - repeated FieldPathSimple msg = 2; -} From e948b95cd0e5d71bf35039a119a5ce2d0d367fc1 Mon Sep 17 00:00:00 2001 From: John Chadwick Date: Wed, 6 Nov 2024 19:07:52 -0500 Subject: [PATCH 07/24] DO NOT MERGE: Use proposed protovalidate protos --- go.mod | 3 +++ go.sum | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 304f252..96a5f34 100644 --- a/go.mod +++ b/go.mod @@ -25,3 +25,6 @@ require ( gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) + +// DO NOT MERGE! +replace buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go => buf.build/gen/go/jchadwick-buf/protovalidate/protocolbuffers/go v1.35.1-20241107000346-6ecee89ee0c9.1 diff --git a/go.sum b/go.sum index 2f6f861..3cc1216 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.35.1-20240920164238-5a7b106cbb87.1 h1:9wP6ZZYWnF2Z0TxmII7m3XNykxnP4/w8oXeth6ekcRI= -buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.35.1-20240920164238-5a7b106cbb87.1/go.mod h1:Duw/9JoXkXIydyASnLYIiufkzySThoqavOsF+IihqvM= +buf.build/gen/go/jchadwick-buf/protovalidate/protocolbuffers/go v1.35.1-20241107000346-6ecee89ee0c9.1 h1:AG/SqE0AfoVJpTJeiOIZ1HnfpR+nYWdbW/vDB3qAKlk= +buf.build/gen/go/jchadwick-buf/protovalidate/protocolbuffers/go v1.35.1-20241107000346-6ecee89ee0c9.1/go.mod h1:Cy8tc4XPqN8l0D+F/Jf4Hy9X9a3somHIH9BTkmJJTmo= github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= From 5e3ce17c392e90c26d32e91aed8f77284f85e7e4 Mon Sep 17 00:00:00 2001 From: John Chadwick Date: Mon, 25 Nov 2024 11:28:12 -0500 Subject: [PATCH 08/24] Add rule path for custom field constraints --- Makefile | 3 ++- internal/evaluator/builder.go | 21 +++++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 1ab5f63..1aa2b53 100644 --- a/Makefile +++ b/Makefile @@ -99,8 +99,9 @@ $(BIN)/golangci-lint: $(BIN) Makefile github.com/golangci/golangci-lint/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION) $(BIN)/protovalidate-conformance: $(BIN) Makefile + # TODO: DO NOT MERGE GOBIN=$(abspath $(BIN)) $(GO) install \ - github.com/bufbuild/protovalidate/tools/protovalidate-conformance@$(CONFORMANCE_VERSION) + github.com/bufbuild/protovalidate/tools/protovalidate-conformance@fab41785ae5758b533d64e40b3f400d84ebbba61 .PHONY: protovalidate-conformance-go protovalidate-conformance-go: $(BIN) diff --git a/internal/evaluator/builder.go b/internal/evaluator/builder.go index daaa81e..7ad7263 100644 --- a/internal/evaluator/builder.go +++ b/internal/evaluator/builder.go @@ -24,11 +24,20 @@ import ( "github.com/bufbuild/protovalidate-go/internal/errors" "github.com/bufbuild/protovalidate-go/internal/expression" "github.com/google/cel-go/cel" + "google.golang.org/protobuf/proto" "google.golang.org/protobuf/reflect/protoreflect" "google.golang.org/protobuf/reflect/protoregistry" + "google.golang.org/protobuf/types/descriptorpb" "google.golang.org/protobuf/types/dynamicpb" ) +//nolint:gochecknoglobals +var celRuleField = validate.FieldPathElement{ + FieldName: proto.String("cel"), + FieldNumber: proto.Int32(23), + FieldType: descriptorpb.FieldDescriptorProto_Type(11).Enum(), +} + // Builder is a build-through cache of message evaluators keyed off the provided // descriptor. type Builder struct { @@ -297,6 +306,18 @@ func (bldr *Builder) processFieldExpressions( if err != nil { return err } + for i := range compiledExpressions { + compiledExpressions[i].Path = []*validate.FieldPathElement{ + { + FieldNumber: proto.Int32(celRuleField.GetFieldNumber()), + FieldType: celRuleField.GetFieldType().Enum(), + FieldName: proto.String(celRuleField.GetFieldName()), + Subscript: &validate.FieldPathElement_Index{ + Index: uint64(i), + }, + }, + } + } if len(compiledExpressions) > 0 { eval.Constraints = append(eval.Constraints, celPrograms(compiledExpressions)) } From 6a86800994e0ce07fa7b2d7b376ab6adc1d54bf0 Mon Sep 17 00:00:00 2001 From: John Chadwick Date: Mon, 25 Nov 2024 12:25:17 -0500 Subject: [PATCH 09/24] Implement tweaked map support in FieldPathElement --- Makefile | 2 +- go.mod | 2 +- go.sum | 4 ++-- internal/errors/utils.go | 2 -- internal/evaluator/map.go | 7 ++++--- 5 files changed, 8 insertions(+), 9 deletions(-) diff --git a/Makefile b/Makefile index 1aa2b53..6d1191d 100644 --- a/Makefile +++ b/Makefile @@ -101,7 +101,7 @@ $(BIN)/golangci-lint: $(BIN) Makefile $(BIN)/protovalidate-conformance: $(BIN) Makefile # TODO: DO NOT MERGE GOBIN=$(abspath $(BIN)) $(GO) install \ - github.com/bufbuild/protovalidate/tools/protovalidate-conformance@fab41785ae5758b533d64e40b3f400d84ebbba61 + github.com/bufbuild/protovalidate/tools/protovalidate-conformance@a4d7e113cc9e8944fb22098123eddc5413500381 .PHONY: protovalidate-conformance-go protovalidate-conformance-go: $(BIN) diff --git a/go.mod b/go.mod index 85d8353..510454c 100644 --- a/go.mod +++ b/go.mod @@ -27,4 +27,4 @@ require ( ) // DO NOT MERGE! -replace buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go => buf.build/gen/go/jchadwick-buf/protovalidate/protocolbuffers/go v1.35.1-20241107000346-6ecee89ee0c9.1 +replace buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go => buf.build/gen/go/jchadwick-buf/protovalidate/protocolbuffers/go v1.35.2-20241125171308-fd74cad4128d.1 diff --git a/go.sum b/go.sum index 5e81f9c..dc03c7e 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -buf.build/gen/go/jchadwick-buf/protovalidate/protocolbuffers/go v1.35.1-20241107000346-6ecee89ee0c9.1 h1:AG/SqE0AfoVJpTJeiOIZ1HnfpR+nYWdbW/vDB3qAKlk= -buf.build/gen/go/jchadwick-buf/protovalidate/protocolbuffers/go v1.35.1-20241107000346-6ecee89ee0c9.1/go.mod h1:Cy8tc4XPqN8l0D+F/Jf4Hy9X9a3somHIH9BTkmJJTmo= +buf.build/gen/go/jchadwick-buf/protovalidate/protocolbuffers/go v1.35.2-20241125171308-fd74cad4128d.1 h1:0chXnVxmZm4tHhfMzhU9ge2ppgIDRon6qyKTJyq+hxo= +buf.build/gen/go/jchadwick-buf/protovalidate/protocolbuffers/go v1.35.2-20241125171308-fd74cad4128d.1/go.mod h1:o4wyuJgZD4vOujqo012KwMjMv9PqJw/jk6swwGw3XIo= cel.dev/expr v0.18.0 h1:CJ6drgk+Hf96lkLikr4rFf19WrU0BOWEihyZnI2TAzo= cel.dev/expr v0.18.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= diff --git a/internal/errors/utils.go b/internal/errors/utils.go index ac23fd4..3e6ddca 100644 --- a/internal/errors/utils.go +++ b/internal/errors/utils.go @@ -142,8 +142,6 @@ func FieldPathString(path []*validate.FieldPathElement) string { result.WriteString(strconv.FormatBool(value.BoolKey)) case *validate.FieldPathElement_IntKey: result.WriteString(strconv.FormatInt(value.IntKey, 10)) - case *validate.FieldPathElement_SintKey: - result.WriteString(strconv.FormatInt(value.SintKey, 10)) case *validate.FieldPathElement_UintKey: result.WriteString(strconv.FormatUint(value.UintKey, 10)) case *validate.FieldPathElement_StringKey: diff --git a/internal/evaluator/map.go b/internal/evaluator/map.go index 168252f..c3a76a4 100644 --- a/internal/evaluator/map.go +++ b/internal/evaluator/map.go @@ -53,14 +53,15 @@ func (m kvPairs) Evaluate(val protoreflect.Value, failFast bool) (err error) { evalErr := m.evalPairs(key, value, failFast) if evalErr != nil { element := errors.FieldPathElement(m.Descriptor) + element.KeyType = descriptorpb.FieldDescriptorProto_Type(m.Descriptor.MapKey().Kind()).Enum() + element.ValueType = descriptorpb.FieldDescriptorProto_Type(m.Descriptor.MapValue().Kind()).Enum() switch m.Descriptor.MapKey().Kind() { case protoreflect.BoolKind: element.Subscript = &validate.FieldPathElement_BoolKey{BoolKey: key.Bool()} case protoreflect.Int32Kind, protoreflect.Int64Kind, - protoreflect.Sfixed32Kind, protoreflect.Sfixed64Kind: + protoreflect.Sfixed32Kind, protoreflect.Sfixed64Kind, + protoreflect.Sint32Kind, protoreflect.Sint64Kind: element.Subscript = &validate.FieldPathElement_IntKey{IntKey: key.Int()} - case protoreflect.Sint32Kind, protoreflect.Sint64Kind: - element.Subscript = &validate.FieldPathElement_SintKey{SintKey: key.Int()} case protoreflect.Uint32Kind, protoreflect.Uint64Kind, protoreflect.Fixed32Kind, protoreflect.Fixed64Kind: element.Subscript = &validate.FieldPathElement_UintKey{UintKey: key.Uint()} From 2916d631fefb94df58374a6da349608bc69d81e0 Mon Sep 17 00:00:00 2001 From: John Chadwick Date: Wed, 27 Nov 2024 13:42:48 -0500 Subject: [PATCH 10/24] Update to protovalidate v0.9.0 --- Makefile | 5 ++--- go.mod | 5 +---- go.sum | 2 ++ 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/Makefile b/Makefile index 6d1191d..6a492e0 100644 --- a/Makefile +++ b/Makefile @@ -16,7 +16,7 @@ GOLANGCI_LINT_VERSION ?= v1.60.1 # Set to use a different version of protovalidate-conformance. # Should be kept in sync with the version referenced in proto/buf.lock and # 'buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go' in go.mod. -CONFORMANCE_VERSION ?= v0.8.2 +CONFORMANCE_VERSION ?= v0.9.0 .PHONY: help help: ## Describe useful make targets @@ -99,9 +99,8 @@ $(BIN)/golangci-lint: $(BIN) Makefile github.com/golangci/golangci-lint/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION) $(BIN)/protovalidate-conformance: $(BIN) Makefile - # TODO: DO NOT MERGE GOBIN=$(abspath $(BIN)) $(GO) install \ - github.com/bufbuild/protovalidate/tools/protovalidate-conformance@a4d7e113cc9e8944fb22098123eddc5413500381 + github.com/bufbuild/protovalidate/tools/protovalidate-conformance@$(CONFORMANCE_VERSION) .PHONY: protovalidate-conformance-go protovalidate-conformance-go: $(BIN) diff --git a/go.mod b/go.mod index 5c1ac5e..f5f9004 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/bufbuild/protovalidate-go go 1.21.1 require ( - buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.35.2-20240920164238-5a7b106cbb87.1 + buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.35.2-20241127180247-a33202765966.1 github.com/envoyproxy/protoc-gen-validate v1.1.0 github.com/google/cel-go v0.22.1 github.com/stretchr/testify v1.10.0 @@ -25,6 +25,3 @@ require ( gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) - -// DO NOT MERGE! -replace buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go => buf.build/gen/go/jchadwick-buf/protovalidate/protocolbuffers/go v1.35.2-20241125171308-fd74cad4128d.1 diff --git a/go.sum b/go.sum index 575040e..0d8d0aa 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.35.2-20241127180247-a33202765966.1 h1:jLd96rDDNJ+zIJxvV/L855VEtrjR0G4aePVDlCpf6kw= +buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.35.2-20241127180247-a33202765966.1/go.mod h1:mnHCFccv4HwuIAOHNGdiIc5ZYbBCvbTWZcodLN5wITI= buf.build/gen/go/jchadwick-buf/protovalidate/protocolbuffers/go v1.35.2-20241125171308-fd74cad4128d.1 h1:0chXnVxmZm4tHhfMzhU9ge2ppgIDRon6qyKTJyq+hxo= buf.build/gen/go/jchadwick-buf/protovalidate/protocolbuffers/go v1.35.2-20241125171308-fd74cad4128d.1/go.mod h1:o4wyuJgZD4vOujqo012KwMjMv9PqJw/jk6swwGw3XIo= cel.dev/expr v0.18.0 h1:CJ6drgk+Hf96lkLikr4rFf19WrU0BOWEihyZnI2TAzo= From ec3dec25765c3dd8d50e011ad2064f05f8f320a8 Mon Sep 17 00:00:00 2001 From: John Chadwick Date: Wed, 27 Nov 2024 13:53:35 -0500 Subject: [PATCH 11/24] Update to protovalidate v0.9.0. --- go.sum | 2 - .../custom_constraints.pb.go | 144 ++++++++++-------- 2 files changed, 82 insertions(+), 64 deletions(-) diff --git a/go.sum b/go.sum index 0d8d0aa..e3a9b0d 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,5 @@ buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.35.2-20241127180247-a33202765966.1 h1:jLd96rDDNJ+zIJxvV/L855VEtrjR0G4aePVDlCpf6kw= buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.35.2-20241127180247-a33202765966.1/go.mod h1:mnHCFccv4HwuIAOHNGdiIc5ZYbBCvbTWZcodLN5wITI= -buf.build/gen/go/jchadwick-buf/protovalidate/protocolbuffers/go v1.35.2-20241125171308-fd74cad4128d.1 h1:0chXnVxmZm4tHhfMzhU9ge2ppgIDRon6qyKTJyq+hxo= -buf.build/gen/go/jchadwick-buf/protovalidate/protocolbuffers/go v1.35.2-20241125171308-fd74cad4128d.1/go.mod h1:o4wyuJgZD4vOujqo012KwMjMv9PqJw/jk6swwGw3XIo= cel.dev/expr v0.18.0 h1:CJ6drgk+Hf96lkLikr4rFf19WrU0BOWEihyZnI2TAzo= cel.dev/expr v0.18.0/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw= github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI= diff --git a/internal/gen/buf/validate/conformance/cases/custom_constraints/custom_constraints.pb.go b/internal/gen/buf/validate/conformance/cases/custom_constraints/custom_constraints.pb.go index dddd74b..f5d9df2 100644 --- a/internal/gen/buf/validate/conformance/cases/custom_constraints/custom_constraints.pb.go +++ b/internal/gen/buf/validate/conformance/cases/custom_constraints/custom_constraints.pb.go @@ -237,6 +237,7 @@ type FieldExpressions struct { A int32 `protobuf:"varint,1,opt,name=a,proto3" json:"a,omitempty"` B Enum `protobuf:"varint,2,opt,name=b,proto3,enum=buf.validate.conformance.cases.custom_constraints.Enum" json:"b,omitempty"` C *FieldExpressions_Nested `protobuf:"bytes,3,opt,name=c,proto3" json:"c,omitempty"` + D int32 `protobuf:"varint,4,opt,name=d,proto3" json:"d,omitempty"` } func (x *FieldExpressions) Reset() { @@ -290,6 +291,13 @@ func (x *FieldExpressions) GetC() *FieldExpressions_Nested { return nil } +func (x *FieldExpressions) GetD() int32 { + if x != nil { + return x.D + } + return 0 +} + type MissingField struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -664,7 +672,7 @@ var file_buf_validate_conformance_cases_custom_constraints_custom_constraints_pr 0x65, 0x78, 0x70, 0x72, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x65, 0x6d, 0x62, 0x65, 0x64, 0x12, 0x12, 0x65, 0x2e, 0x61, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x65, 0x71, 0x75, 0x61, 0x6c, 0x20, 0x66, 0x2e, 0x61, 0x1a, 0x14, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x65, 0x2e, 0x61, 0x20, 0x3d, - 0x3d, 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x66, 0x2e, 0x61, 0x22, 0xf6, 0x03, 0x0a, 0x10, 0x46, + 0x3d, 0x20, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x66, 0x2e, 0x61, 0x22, 0xaa, 0x05, 0x0a, 0x10, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x45, 0x78, 0x70, 0x72, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x5a, 0x0a, 0x01, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x42, 0x4c, 0xba, 0x48, 0x49, 0xba, 0x01, 0x46, 0x0a, 0x17, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x5f, 0x65, 0x78, 0x70, 0x72, 0x65, 0x73, @@ -690,67 +698,79 @@ var file_buf_validate_conformance_cases_custom_constraints_custom_constraints_pr 0x6d, 0x62, 0x65, 0x64, 0x12, 0x1b, 0x63, 0x2e, 0x61, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x61, 0x20, 0x6d, 0x75, 0x6c, 0x74, 0x69, 0x70, 0x6c, 0x65, 0x20, 0x6f, 0x66, 0x20, 0x34, 0x1a, 0x0f, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x61, 0x20, 0x25, 0x20, 0x34, 0x20, 0x3d, 0x3d, - 0x20, 0x30, 0x52, 0x01, 0x63, 0x1a, 0x5c, 0x0a, 0x06, 0x4e, 0x65, 0x73, 0x74, 0x65, 0x64, 0x12, - 0x52, 0x0a, 0x01, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x42, 0x44, 0xba, 0x48, 0x41, 0xba, - 0x01, 0x3e, 0x0a, 0x17, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x5f, 0x65, 0x78, 0x70, 0x72, 0x65, 0x73, - 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x6e, 0x65, 0x73, 0x74, 0x65, 0x64, 0x1a, 0x23, 0x74, 0x68, 0x69, - 0x73, 0x20, 0x3e, 0x20, 0x30, 0x20, 0x3f, 0x20, 0x27, 0x27, 0x3a, 0x20, 0x27, 0x61, 0x20, 0x6d, - 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x27, - 0x52, 0x01, 0x61, 0x22, 0x52, 0x0a, 0x0c, 0x4d, 0x69, 0x73, 0x73, 0x69, 0x6e, 0x67, 0x46, 0x69, - 0x65, 0x6c, 0x64, 0x12, 0x0c, 0x0a, 0x01, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x01, - 0x61, 0x3a, 0x34, 0xba, 0x48, 0x31, 0x1a, 0x2f, 0x0a, 0x0d, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6e, - 0x67, 0x5f, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x12, 0x62, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, - 0x62, 0x65, 0x20, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x1a, 0x0a, 0x74, 0x68, 0x69, - 0x73, 0x2e, 0x62, 0x20, 0x3e, 0x20, 0x30, 0x22, 0x67, 0x0a, 0x0d, 0x49, 0x6e, 0x63, 0x6f, 0x72, - 0x72, 0x65, 0x63, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0c, 0x0a, 0x01, 0x61, 0x18, 0x01, 0x20, - 0x01, 0x28, 0x05, 0x52, 0x01, 0x61, 0x3a, 0x48, 0xba, 0x48, 0x45, 0x1a, 0x43, 0x0a, 0x0e, 0x69, - 0x6e, 0x63, 0x6f, 0x72, 0x72, 0x65, 0x63, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x12, 0x17, 0x61, - 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x73, 0x74, 0x61, 0x72, 0x74, 0x20, 0x77, 0x69, 0x74, 0x68, - 0x20, 0x27, 0x66, 0x6f, 0x6f, 0x27, 0x1a, 0x18, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x61, 0x2e, 0x73, - 0x74, 0x61, 0x72, 0x74, 0x73, 0x57, 0x69, 0x74, 0x68, 0x28, 0x27, 0x66, 0x6f, 0x6f, 0x27, 0x29, - 0x22, 0x7d, 0x0a, 0x0f, 0x44, 0x79, 0x6e, 0x52, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x45, 0x72, - 0x72, 0x6f, 0x72, 0x12, 0x0c, 0x0a, 0x01, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x01, - 0x61, 0x3a, 0x5c, 0xba, 0x48, 0x59, 0x1a, 0x57, 0x0a, 0x0f, 0x64, 0x79, 0x6e, 0x5f, 0x72, 0x75, - 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x65, 0x72, 0x72, 0x12, 0x2e, 0x64, 0x79, 0x6e, 0x61, 0x6d, - 0x69, 0x63, 0x20, 0x74, 0x79, 0x70, 0x65, 0x20, 0x74, 0x72, 0x69, 0x65, 0x73, 0x20, 0x74, 0x6f, - 0x20, 0x75, 0x73, 0x65, 0x20, 0x61, 0x20, 0x6e, 0x6f, 0x6e, 0x2d, 0x65, 0x78, 0x69, 0x73, 0x74, - 0x65, 0x6e, 0x74, 0x20, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x1a, 0x14, 0x64, 0x79, 0x6e, 0x28, 0x74, - 0x68, 0x69, 0x73, 0x29, 0x2e, 0x62, 0x20, 0x3d, 0x3d, 0x20, 0x27, 0x66, 0x6f, 0x6f, 0x27, 0x22, - 0x5c, 0x0a, 0x0c, 0x4e, 0x6f, 0x77, 0x45, 0x71, 0x75, 0x61, 0x6c, 0x73, 0x4e, 0x6f, 0x77, 0x3a, - 0x4c, 0xba, 0x48, 0x49, 0x1a, 0x47, 0x0a, 0x0e, 0x6e, 0x6f, 0x77, 0x5f, 0x65, 0x71, 0x75, 0x61, - 0x6c, 0x73, 0x5f, 0x6e, 0x6f, 0x77, 0x12, 0x29, 0x6e, 0x6f, 0x77, 0x20, 0x73, 0x68, 0x6f, 0x75, - 0x6c, 0x64, 0x20, 0x65, 0x71, 0x75, 0x61, 0x6c, 0x20, 0x6e, 0x6f, 0x77, 0x20, 0x77, 0x69, 0x74, - 0x68, 0x69, 0x6e, 0x20, 0x61, 0x6e, 0x20, 0x65, 0x78, 0x70, 0x72, 0x65, 0x73, 0x73, 0x69, 0x6f, - 0x6e, 0x1a, 0x0a, 0x6e, 0x6f, 0x77, 0x20, 0x3d, 0x3d, 0x20, 0x6e, 0x6f, 0x77, 0x2a, 0x2a, 0x0a, - 0x04, 0x45, 0x6e, 0x75, 0x6d, 0x12, 0x14, 0x0a, 0x10, 0x45, 0x4e, 0x55, 0x4d, 0x5f, 0x55, 0x4e, - 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x0c, 0x0a, 0x08, 0x45, - 0x4e, 0x55, 0x4d, 0x5f, 0x4f, 0x4e, 0x45, 0x10, 0x01, 0x42, 0x9a, 0x03, 0x0a, 0x35, 0x63, 0x6f, - 0x6d, 0x2e, 0x62, 0x75, 0x66, 0x2e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x2e, 0x63, - 0x6f, 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x63, 0x61, 0x73, 0x65, 0x73, - 0x2e, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5f, 0x63, 0x6f, 0x6e, 0x73, 0x74, 0x72, 0x61, 0x69, - 0x6e, 0x74, 0x73, 0x42, 0x16, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x43, 0x6f, 0x6e, 0x73, 0x74, - 0x72, 0x61, 0x69, 0x6e, 0x74, 0x73, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, 0x01, 0x5a, 0x63, 0x67, - 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x62, 0x75, 0x66, 0x62, 0x75, 0x69, - 0x6c, 0x64, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, - 0x2d, 0x67, 0x6f, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x67, 0x65, 0x6e, - 0x2f, 0x62, 0x75, 0x66, 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x2f, 0x63, 0x6f, - 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2f, 0x63, 0x61, 0x73, 0x65, 0x73, 0x2f, - 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5f, 0x63, 0x6f, 0x6e, 0x73, 0x74, 0x72, 0x61, 0x69, 0x6e, - 0x74, 0x73, 0xa2, 0x02, 0x05, 0x42, 0x56, 0x43, 0x43, 0x43, 0xaa, 0x02, 0x30, 0x42, 0x75, 0x66, - 0x2e, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x2e, 0x43, 0x6f, 0x6e, 0x66, 0x6f, 0x72, - 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x43, 0x61, 0x73, 0x65, 0x73, 0x2e, 0x43, 0x75, 0x73, 0x74, - 0x6f, 0x6d, 0x43, 0x6f, 0x6e, 0x73, 0x74, 0x72, 0x61, 0x69, 0x6e, 0x74, 0x73, 0xca, 0x02, 0x30, - 0x42, 0x75, 0x66, 0x5c, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x5c, 0x43, 0x6f, 0x6e, - 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x5c, 0x43, 0x61, 0x73, 0x65, 0x73, 0x5c, 0x43, - 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x43, 0x6f, 0x6e, 0x73, 0x74, 0x72, 0x61, 0x69, 0x6e, 0x74, 0x73, - 0xe2, 0x02, 0x3c, 0x42, 0x75, 0x66, 0x5c, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x5c, - 0x43, 0x6f, 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x5c, 0x43, 0x61, 0x73, 0x65, - 0x73, 0x5c, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x43, 0x6f, 0x6e, 0x73, 0x74, 0x72, 0x61, 0x69, - 0x6e, 0x74, 0x73, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0xea, - 0x02, 0x34, 0x42, 0x75, 0x66, 0x3a, 0x3a, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x3a, - 0x3a, 0x43, 0x6f, 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x3a, 0x3a, 0x43, 0x61, - 0x73, 0x65, 0x73, 0x3a, 0x3a, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x43, 0x6f, 0x6e, 0x73, 0x74, - 0x72, 0x61, 0x69, 0x6e, 0x74, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x20, 0x30, 0x52, 0x01, 0x63, 0x12, 0xb1, 0x01, 0x0a, 0x01, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x05, 0x42, 0xa2, 0x01, 0xba, 0x48, 0x9e, 0x01, 0xba, 0x01, 0x4c, 0x0a, 0x22, 0x66, 0x69, 0x65, + 0x6c, 0x64, 0x5f, 0x65, 0x78, 0x70, 0x72, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x73, 0x63, + 0x61, 0x6c, 0x61, 0x72, 0x5f, 0x6d, 0x75, 0x6c, 0x74, 0x69, 0x70, 0x6c, 0x65, 0x5f, 0x31, 0x1a, + 0x26, 0x74, 0x68, 0x69, 0x73, 0x20, 0x3c, 0x20, 0x31, 0x20, 0x3f, 0x20, 0x27, 0x27, 0x3a, 0x20, + 0x27, 0x64, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x6c, 0x65, 0x73, 0x73, 0x20, + 0x74, 0x68, 0x61, 0x6e, 0x20, 0x31, 0x27, 0xba, 0x01, 0x4c, 0x0a, 0x22, 0x66, 0x69, 0x65, 0x6c, + 0x64, 0x5f, 0x65, 0x78, 0x70, 0x72, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x73, 0x63, 0x61, + 0x6c, 0x61, 0x72, 0x5f, 0x6d, 0x75, 0x6c, 0x74, 0x69, 0x70, 0x6c, 0x65, 0x5f, 0x32, 0x1a, 0x26, + 0x74, 0x68, 0x69, 0x73, 0x20, 0x3c, 0x20, 0x32, 0x20, 0x3f, 0x20, 0x27, 0x27, 0x3a, 0x20, 0x27, + 0x64, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x6c, 0x65, 0x73, 0x73, 0x20, 0x74, + 0x68, 0x61, 0x6e, 0x20, 0x32, 0x27, 0x52, 0x01, 0x64, 0x1a, 0x5c, 0x0a, 0x06, 0x4e, 0x65, 0x73, + 0x74, 0x65, 0x64, 0x12, 0x52, 0x0a, 0x01, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x42, 0x44, + 0xba, 0x48, 0x41, 0xba, 0x01, 0x3e, 0x0a, 0x17, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x5f, 0x65, 0x78, + 0x70, 0x72, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x6e, 0x65, 0x73, 0x74, 0x65, 0x64, 0x1a, + 0x23, 0x74, 0x68, 0x69, 0x73, 0x20, 0x3e, 0x20, 0x30, 0x20, 0x3f, 0x20, 0x27, 0x27, 0x3a, 0x20, + 0x27, 0x61, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x70, 0x6f, 0x73, 0x69, 0x74, + 0x69, 0x76, 0x65, 0x27, 0x52, 0x01, 0x61, 0x22, 0x52, 0x0a, 0x0c, 0x4d, 0x69, 0x73, 0x73, 0x69, + 0x6e, 0x67, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x0c, 0x0a, 0x01, 0x61, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x05, 0x52, 0x01, 0x61, 0x3a, 0x34, 0xba, 0x48, 0x31, 0x1a, 0x2f, 0x0a, 0x0d, 0x6d, 0x69, + 0x73, 0x73, 0x69, 0x6e, 0x67, 0x5f, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x12, 0x62, 0x20, 0x6d, + 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x70, 0x6f, 0x73, 0x69, 0x74, 0x69, 0x76, 0x65, 0x1a, + 0x0a, 0x74, 0x68, 0x69, 0x73, 0x2e, 0x62, 0x20, 0x3e, 0x20, 0x30, 0x22, 0x67, 0x0a, 0x0d, 0x49, + 0x6e, 0x63, 0x6f, 0x72, 0x72, 0x65, 0x63, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x0c, 0x0a, 0x01, + 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x01, 0x61, 0x3a, 0x48, 0xba, 0x48, 0x45, 0x1a, + 0x43, 0x0a, 0x0e, 0x69, 0x6e, 0x63, 0x6f, 0x72, 0x72, 0x65, 0x63, 0x74, 0x5f, 0x74, 0x79, 0x70, + 0x65, 0x12, 0x17, 0x61, 0x20, 0x6d, 0x75, 0x73, 0x74, 0x20, 0x73, 0x74, 0x61, 0x72, 0x74, 0x20, + 0x77, 0x69, 0x74, 0x68, 0x20, 0x27, 0x66, 0x6f, 0x6f, 0x27, 0x1a, 0x18, 0x74, 0x68, 0x69, 0x73, + 0x2e, 0x61, 0x2e, 0x73, 0x74, 0x61, 0x72, 0x74, 0x73, 0x57, 0x69, 0x74, 0x68, 0x28, 0x27, 0x66, + 0x6f, 0x6f, 0x27, 0x29, 0x22, 0x7d, 0x0a, 0x0f, 0x44, 0x79, 0x6e, 0x52, 0x75, 0x6e, 0x74, 0x69, + 0x6d, 0x65, 0x45, 0x72, 0x72, 0x6f, 0x72, 0x12, 0x0c, 0x0a, 0x01, 0x61, 0x18, 0x01, 0x20, 0x01, + 0x28, 0x05, 0x52, 0x01, 0x61, 0x3a, 0x5c, 0xba, 0x48, 0x59, 0x1a, 0x57, 0x0a, 0x0f, 0x64, 0x79, + 0x6e, 0x5f, 0x72, 0x75, 0x6e, 0x74, 0x69, 0x6d, 0x65, 0x5f, 0x65, 0x72, 0x72, 0x12, 0x2e, 0x64, + 0x79, 0x6e, 0x61, 0x6d, 0x69, 0x63, 0x20, 0x74, 0x79, 0x70, 0x65, 0x20, 0x74, 0x72, 0x69, 0x65, + 0x73, 0x20, 0x74, 0x6f, 0x20, 0x75, 0x73, 0x65, 0x20, 0x61, 0x20, 0x6e, 0x6f, 0x6e, 0x2d, 0x65, + 0x78, 0x69, 0x73, 0x74, 0x65, 0x6e, 0x74, 0x20, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x1a, 0x14, 0x64, + 0x79, 0x6e, 0x28, 0x74, 0x68, 0x69, 0x73, 0x29, 0x2e, 0x62, 0x20, 0x3d, 0x3d, 0x20, 0x27, 0x66, + 0x6f, 0x6f, 0x27, 0x22, 0x5c, 0x0a, 0x0c, 0x4e, 0x6f, 0x77, 0x45, 0x71, 0x75, 0x61, 0x6c, 0x73, + 0x4e, 0x6f, 0x77, 0x3a, 0x4c, 0xba, 0x48, 0x49, 0x1a, 0x47, 0x0a, 0x0e, 0x6e, 0x6f, 0x77, 0x5f, + 0x65, 0x71, 0x75, 0x61, 0x6c, 0x73, 0x5f, 0x6e, 0x6f, 0x77, 0x12, 0x29, 0x6e, 0x6f, 0x77, 0x20, + 0x73, 0x68, 0x6f, 0x75, 0x6c, 0x64, 0x20, 0x65, 0x71, 0x75, 0x61, 0x6c, 0x20, 0x6e, 0x6f, 0x77, + 0x20, 0x77, 0x69, 0x74, 0x68, 0x69, 0x6e, 0x20, 0x61, 0x6e, 0x20, 0x65, 0x78, 0x70, 0x72, 0x65, + 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x1a, 0x0a, 0x6e, 0x6f, 0x77, 0x20, 0x3d, 0x3d, 0x20, 0x6e, 0x6f, + 0x77, 0x2a, 0x2a, 0x0a, 0x04, 0x45, 0x6e, 0x75, 0x6d, 0x12, 0x14, 0x0a, 0x10, 0x45, 0x4e, 0x55, + 0x4d, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, + 0x0c, 0x0a, 0x08, 0x45, 0x4e, 0x55, 0x4d, 0x5f, 0x4f, 0x4e, 0x45, 0x10, 0x01, 0x42, 0x9a, 0x03, + 0x0a, 0x35, 0x63, 0x6f, 0x6d, 0x2e, 0x62, 0x75, 0x66, 0x2e, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, + 0x74, 0x65, 0x2e, 0x63, 0x6f, 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x63, + 0x61, 0x73, 0x65, 0x73, 0x2e, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5f, 0x63, 0x6f, 0x6e, 0x73, + 0x74, 0x72, 0x61, 0x69, 0x6e, 0x74, 0x73, 0x42, 0x16, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x43, + 0x6f, 0x6e, 0x73, 0x74, 0x72, 0x61, 0x69, 0x6e, 0x74, 0x73, 0x50, 0x72, 0x6f, 0x74, 0x6f, 0x50, + 0x01, 0x5a, 0x63, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x62, 0x75, + 0x66, 0x62, 0x75, 0x69, 0x6c, 0x64, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x76, 0x61, 0x6c, 0x69, + 0x64, 0x61, 0x74, 0x65, 0x2d, 0x67, 0x6f, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, + 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x62, 0x75, 0x66, 0x2f, 0x76, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, + 0x65, 0x2f, 0x63, 0x6f, 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2f, 0x63, 0x61, + 0x73, 0x65, 0x73, 0x2f, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5f, 0x63, 0x6f, 0x6e, 0x73, 0x74, + 0x72, 0x61, 0x69, 0x6e, 0x74, 0x73, 0xa2, 0x02, 0x05, 0x42, 0x56, 0x43, 0x43, 0x43, 0xaa, 0x02, + 0x30, 0x42, 0x75, 0x66, 0x2e, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, 0x2e, 0x43, 0x6f, + 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x2e, 0x43, 0x61, 0x73, 0x65, 0x73, 0x2e, + 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x43, 0x6f, 0x6e, 0x73, 0x74, 0x72, 0x61, 0x69, 0x6e, 0x74, + 0x73, 0xca, 0x02, 0x30, 0x42, 0x75, 0x66, 0x5c, 0x56, 0x61, 0x6c, 0x69, 0x64, 0x61, 0x74, 0x65, + 0x5c, 0x43, 0x6f, 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x5c, 0x43, 0x61, 0x73, + 0x65, 0x73, 0x5c, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x43, 0x6f, 0x6e, 0x73, 0x74, 0x72, 0x61, + 0x69, 0x6e, 0x74, 0x73, 0xe2, 0x02, 0x3c, 0x42, 0x75, 0x66, 0x5c, 0x56, 0x61, 0x6c, 0x69, 0x64, + 0x61, 0x74, 0x65, 0x5c, 0x43, 0x6f, 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, 0x5c, + 0x43, 0x61, 0x73, 0x65, 0x73, 0x5c, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x43, 0x6f, 0x6e, 0x73, + 0x74, 0x72, 0x61, 0x69, 0x6e, 0x74, 0x73, 0x5c, 0x47, 0x50, 0x42, 0x4d, 0x65, 0x74, 0x61, 0x64, + 0x61, 0x74, 0x61, 0xea, 0x02, 0x34, 0x42, 0x75, 0x66, 0x3a, 0x3a, 0x56, 0x61, 0x6c, 0x69, 0x64, + 0x61, 0x74, 0x65, 0x3a, 0x3a, 0x43, 0x6f, 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x6e, 0x63, 0x65, + 0x3a, 0x3a, 0x43, 0x61, 0x73, 0x65, 0x73, 0x3a, 0x3a, 0x43, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x43, + 0x6f, 0x6e, 0x73, 0x74, 0x72, 0x61, 0x69, 0x6e, 0x74, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x33, } var ( From dd6d08d7f05520f98790a14ef08d07b3ef0d1a10 Mon Sep 17 00:00:00 2001 From: John Chadwick Date: Mon, 2 Dec 2024 12:50:36 -0500 Subject: [PATCH 12/24] Refactor Violation API one more time --- internal/errors/utils.go | 54 ++++++++++++-------- internal/errors/utils_test.go | 19 +++---- internal/errors/validation.go | 77 +++++------------------------ internal/evaluator/any.go | 28 ++++++----- internal/evaluator/enum.go | 14 +++--- internal/evaluator/field.go | 17 ++++--- internal/evaluator/oneof.go | 16 +++--- internal/expression/program.go | 36 +++++++++----- internal/expression/program_test.go | 2 +- validator.go | 1 + validator_example_test.go | 5 +- validator_test.go | 2 +- 12 files changed, 128 insertions(+), 143 deletions(-) diff --git a/internal/errors/utils.go b/internal/errors/utils.go index 3e6ddca..4bba641 100644 --- a/internal/errors/utils.go +++ b/internal/errors/utils.go @@ -74,18 +74,18 @@ func AppendFieldPath(err error, suffix *validate.FieldPathElement, skipSubscript var valErr *ValidationError if errors.As(err, &valErr) { for _, violation := range valErr.Violations { - if violation, ok := violation.(*ViolationData); ok { - // Special case: Here we skip appending if the last element had a - // subscript. This is a weird special case that makes it - // significantly simpler to handle reverse-constructing paths with - // maps and slices. - if skipSubscript && - len(violation.Field) > 0 && - violation.Field[len(violation.Field)-1].Subscript != nil { - continue - } - violation.Field = append(violation.Field, suffix) + // Special case: Here we skip appending if the last element had a + // subscript. This is a weird special case that makes it + // significantly simpler to handle reverse-constructing paths with + // maps and slices. + if elements := violation.Proto.GetField().GetElements(); skipSubscript && + len(elements) > 0 && elements[len(elements)-1].Subscript != nil { + continue } + if violation.Proto.GetField() == nil { + violation.Proto.Field = &validate.FieldPath{} + } + violation.Proto.Field.Elements = append(violation.Proto.Field.Elements, suffix) } } } @@ -99,12 +99,13 @@ func PrependRulePath(err error, prefix []*validate.FieldPathElement) { var valErr *ValidationError if errors.As(err, &valErr) { for _, violation := range valErr.Violations { - if violation, ok := violation.(*ViolationData); ok { - violation.Rule = append( - append([]*validate.FieldPathElement{}, prefix...), - violation.Rule..., - ) + if violation.Proto.GetRule() == nil { + violation.Proto.Rule = &validate.FieldPath{} } + violation.Proto.Rule.Elements = append( + append([]*validate.FieldPathElement{}, prefix...), + violation.Proto.GetRule().GetElements()..., + ) } } } @@ -114,8 +115,21 @@ func ReverseFieldPaths(err error) { var valErr *ValidationError if errors.As(err, &valErr) { for _, violation := range valErr.Violations { - if violation, ok := violation.(*ViolationData); ok { - slices.Reverse(violation.Field) + if violation.Proto.GetField() != nil { + slices.Reverse(violation.Proto.GetField().GetElements()) + } + } + } +} + +// PopulateFieldPathStrings populates the field path strings in the error. +func PopulateFieldPathStrings(err error) { + var valErr *ValidationError + if errors.As(err, &valErr) { + for _, violation := range valErr.Violations { + if violation.Proto.GetField() != nil { + //nolint:staticcheck // Intentional use of deprecated field + violation.Proto.FieldPath = proto.String(FieldPathString(violation.Proto.GetField().GetElements())) } } } @@ -156,9 +170,7 @@ func MarkForKey(err error) { var valErr *ValidationError if errors.As(err, &valErr) { for _, violation := range valErr.Violations { - if violation, ok := violation.(*ViolationData); ok { - violation.ForKey = true - } + violation.Proto.ForKey = proto.Bool(true) } } } diff --git a/internal/errors/utils_test.go b/internal/errors/utils_test.go index b21bc7f..3963b2a 100644 --- a/internal/errors/utils_test.go +++ b/internal/errors/utils_test.go @@ -18,6 +18,7 @@ import ( "errors" "testing" + "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/protobuf/proto" @@ -52,7 +53,7 @@ func TestMerge(t *testing.T) { t.Run("validation", func(t *testing.T) { t.Parallel() - exErr := &ValidationError{Violations: []Violation{&ViolationData{ConstraintID: "foo"}}} + exErr := &ValidationError{Violations: []*Violation{{Proto: &validate.Violation{ConstraintId: proto.String("foo")}}}} ok, err := Merge(nil, exErr, true) var valErr *ValidationError require.ErrorAs(t, err, &valErr) @@ -71,7 +72,7 @@ func TestMerge(t *testing.T) { t.Run("non-validation dst", func(t *testing.T) { t.Parallel() dstErr := errors.New("some error") - srcErr := &ValidationError{Violations: []Violation{&ViolationData{ConstraintID: "foo"}}} + srcErr := &ValidationError{Violations: []*Violation{{Proto: &validate.Violation{ConstraintId: proto.String("foo")}}}} ok, err := Merge(dstErr, srcErr, true) assert.Equal(t, dstErr, err) assert.False(t, ok) @@ -82,7 +83,7 @@ func TestMerge(t *testing.T) { t.Run("non-validation src", func(t *testing.T) { t.Parallel() - dstErr := &ValidationError{Violations: []Violation{&ViolationData{ConstraintID: "foo"}}} + dstErr := &ValidationError{Violations: []*Violation{{Proto: &validate.Violation{ConstraintId: proto.String("foo")}}}} srcErr := errors.New("some error") ok, err := Merge(dstErr, srcErr, true) assert.Equal(t, srcErr, err) @@ -95,18 +96,18 @@ func TestMerge(t *testing.T) { t.Run("validation", func(t *testing.T) { t.Parallel() - dstErr := &ValidationError{Violations: []Violation{&ViolationData{ConstraintID: "foo"}}} - srcErr := &ValidationError{Violations: []Violation{&ViolationData{ConstraintID: ("bar")}}} - exErr := &ValidationError{Violations: []Violation{ - &ViolationData{ConstraintID: "foo"}, - &ViolationData{ConstraintID: "bar"}, + dstErr := &ValidationError{Violations: []*Violation{{Proto: &validate.Violation{ConstraintId: proto.String("foo")}}}} + srcErr := &ValidationError{Violations: []*Violation{{Proto: &validate.Violation{ConstraintId: proto.String("bar")}}}} + exErr := &ValidationError{Violations: []*Violation{ + {Proto: &validate.Violation{ConstraintId: proto.String("foo")}}, + {Proto: &validate.Violation{ConstraintId: proto.String("bar")}}, }} ok, err := Merge(dstErr, srcErr, true) var valErr *ValidationError require.ErrorAs(t, err, &valErr) assert.True(t, proto.Equal(exErr.ToProto(), valErr.ToProto())) assert.False(t, ok) - dstErr = &ValidationError{Violations: []Violation{&ViolationData{ConstraintID: "foo"}}} + dstErr = &ValidationError{Violations: []*Violation{{Proto: &validate.Violation{ConstraintId: proto.String("foo")}}}} ok, err = Merge(dstErr, srcErr, false) require.ErrorAs(t, err, &valErr) assert.True(t, proto.Equal(exErr.ToProto(), valErr.ToProto())) diff --git a/internal/errors/validation.go b/internal/errors/validation.go index 7343a67..459aa31 100644 --- a/internal/errors/validation.go +++ b/internal/errors/validation.go @@ -19,33 +19,32 @@ import ( "strings" "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" - "google.golang.org/protobuf/proto" "google.golang.org/protobuf/reflect/protoreflect" ) // Violation represents a single instance where a validation rule was not met. // It provides information about the field that caused the violation, the // specific unfulfilled constraint, and a human-readable error message. -type Violation interface { - // GetFieldValue returns the value of the specific field that failed +type Violation struct { + // Proto contains the violation's proto.Message form. + Proto *validate.Violation + + // FieldValue returns the value of the specific field that failed // validation. If there was no value, this will return an invalid value. - GetFieldValue() protoreflect.Value + FieldValue protoreflect.Value - // GetRuleValue returns the value of the rule that specified the failed + // RuleValue returns the value of the rule that specified the failed // constraint. Not all constraints have a value; only standard and // predefined constraints have rule values. In violations caused by other // kinds of constraints, like custom contraints, this will return an invalid // value. - GetRuleValue() protoreflect.Value - - // ToProto converts this violation into its proto.Message form. - ToProto() *validate.Violation + RuleValue protoreflect.Value } // A ValidationError is returned if one or more constraint violations were // detected. type ValidationError struct { - Violations []Violation + Violations []*Violation } // ToProto converts this error into its proto.Message form. @@ -54,7 +53,7 @@ func (err *ValidationError) ToProto() *validate.Violations { Violations: make([]*validate.Violation, len(err.Violations)), } for i, violation := range err.Violations { - violations.Violations[i] = violation.ToProto() + violations.Violations[i] = violation.Proto } return violations } @@ -63,64 +62,14 @@ func (err *ValidationError) Error() string { bldr := &strings.Builder{} bldr.WriteString("validation error:") for _, violation := range err.Violations { - violation := violation.ToProto() bldr.WriteString("\n - ") - if fieldPath := FieldPathString(violation.GetField().GetElements()); fieldPath != "" { + if fieldPath := FieldPathString(violation.Proto.GetField().GetElements()); fieldPath != "" { bldr.WriteString(fieldPath) bldr.WriteString(": ") } _, _ = fmt.Fprintf(bldr, "%s [%s]", - violation.GetMessage(), - violation.GetConstraintId()) + violation.Proto.GetMessage(), + violation.Proto.GetConstraintId()) } return bldr.String() } - -// ViolationData is a simple implementation of Violation. -type ViolationData struct { - Field []*validate.FieldPathElement - Rule []*validate.FieldPathElement - FieldValue protoreflect.Value - RuleValue protoreflect.Value - ConstraintID string - Message string - ForKey bool -} - -func (v *ViolationData) GetFieldValue() protoreflect.Value { - return v.FieldValue -} - -func (v *ViolationData) GetRuleValue() protoreflect.Value { - return v.RuleValue -} - -func (v *ViolationData) ToProto() *validate.Violation { - var fieldPathString *string - if len(v.Field) > 0 { - fieldPathString = proto.String(FieldPathString(v.Field)) - } - var forKey *bool - if v.ForKey { - forKey = proto.Bool(true) - } - return &validate.Violation{ - Field: fieldPathProto(v.Field), - Rule: fieldPathProto(v.Rule), - FieldPath: fieldPathString, - ConstraintId: proto.String(v.ConstraintID), - Message: proto.String(v.Message), - ForKey: forKey, - } -} - -var _ Violation = &ViolationData{} - -func fieldPathProto(elements []*validate.FieldPathElement) *validate.FieldPath { - if len(elements) == 0 { - return nil - } - return &validate.FieldPath{ - Elements: elements, - } -} diff --git a/internal/evaluator/any.go b/internal/evaluator/any.go index c770aa9..a408083 100644 --- a/internal/evaluator/any.go +++ b/internal/evaluator/any.go @@ -57,12 +57,14 @@ func (a anyPB) Evaluate(val protoreflect.Value, failFast bool) error { err := &errors.ValidationError{} if len(a.In) > 0 { if _, ok := a.In[typeURL]; !ok { - err.Violations = append(err.Violations, &errors.ViolationData{ - Rule: anyInRulePath, - FieldValue: val, - RuleValue: a.InValue, - ConstraintID: "any.in", - Message: "type URL must be in the allow list", + err.Violations = append(err.Violations, &errors.Violation{ + Proto: &validate.Violation{ + Rule: &validate.FieldPath{Elements: anyInRulePath}, + ConstraintId: proto.String("any.in"), + Message: proto.String("type URL must be in the allow list"), + }, + FieldValue: val, + RuleValue: a.InValue, }) if failFast { return err @@ -72,12 +74,14 @@ func (a anyPB) Evaluate(val protoreflect.Value, failFast bool) error { if len(a.NotIn) > 0 { if _, ok := a.NotIn[typeURL]; ok { - err.Violations = append(err.Violations, &errors.ViolationData{ - Rule: anyNotInRulePath, - FieldValue: val, - RuleValue: a.NotInValue, - ConstraintID: "any.not_in", - Message: "type URL must not be in the block list", + err.Violations = append(err.Violations, &errors.Violation{ + Proto: &validate.Violation{ + Rule: &validate.FieldPath{Elements: anyNotInRulePath}, + ConstraintId: proto.String("any.not_in"), + Message: proto.String("type URL must not be in the block list"), + }, + FieldValue: val, + RuleValue: a.NotInValue, }) } } diff --git a/internal/evaluator/enum.go b/internal/evaluator/enum.go index 23032d3..7c70583 100644 --- a/internal/evaluator/enum.go +++ b/internal/evaluator/enum.go @@ -38,12 +38,14 @@ type definedEnum struct { func (d definedEnum) Evaluate(val protoreflect.Value, _ bool) error { if d.ValueDescriptors.ByNumber(val.Enum()) == nil { - return &errors.ValidationError{Violations: []errors.Violation{&errors.ViolationData{ - Rule: enumDefinedOnlyRulePath, - FieldValue: val, - RuleValue: protoreflect.ValueOfBool(true), - ConstraintID: "enum.defined_only", - Message: "value must be one of the defined enum values", + return &errors.ValidationError{Violations: []*errors.Violation{{ + Proto: &validate.Violation{ + Rule: &validate.FieldPath{Elements: enumDefinedOnlyRulePath}, + ConstraintId: proto.String("enum.defined_only"), + Message: proto.String("value must be one of the defined enum values"), + }, + FieldValue: val, + RuleValue: protoreflect.ValueOfBool(true), }}} } return nil diff --git a/internal/evaluator/field.go b/internal/evaluator/field.go index 9bd090b..5931a25 100644 --- a/internal/evaluator/field.go +++ b/internal/evaluator/field.go @@ -53,16 +53,17 @@ func (f field) Evaluate(val protoreflect.Value, failFast bool) error { func (f field) EvaluateMessage(msg protoreflect.Message, failFast bool) (err error) { if f.Required && !msg.Has(f.Descriptor) { - err := &errors.ValidationError{Violations: []errors.Violation{&errors.ViolationData{ - Field: []*validate.FieldPathElement{ - errors.FieldPathElement(f.Descriptor), + return &errors.ValidationError{Violations: []*errors.Violation{{ + Proto: &validate.Violation{ + Field: &validate.FieldPath{Elements: []*validate.FieldPathElement{ + errors.FieldPathElement(f.Descriptor), + }}, + Rule: &validate.FieldPath{Elements: requiredRulePath}, + ConstraintId: proto.String("required"), + Message: proto.String("value is required"), }, - Rule: requiredRulePath, - RuleValue: protoreflect.ValueOfBool(true), - ConstraintID: "required", - Message: "value is required", + RuleValue: protoreflect.ValueOfBool(true), }}} - return err } if f.IgnoreEmpty && !msg.Has(f.Descriptor) { diff --git a/internal/evaluator/oneof.go b/internal/evaluator/oneof.go index 4f1ef00..355fbb2 100644 --- a/internal/evaluator/oneof.go +++ b/internal/evaluator/oneof.go @@ -35,12 +35,16 @@ func (o oneof) Evaluate(val protoreflect.Value, failFast bool) error { func (o oneof) EvaluateMessage(msg protoreflect.Message, _ bool) error { if o.Required && msg.WhichOneof(o.Descriptor) == nil { - return &errors.ValidationError{Violations: []errors.Violation{&errors.ViolationData{ - Field: []*validate.FieldPathElement{{ - FieldName: proto.String(string(o.Descriptor.Name())), - }}, - ConstraintID: "required", - Message: "exactly one field is required in oneof", + return &errors.ValidationError{Violations: []*errors.Violation{{ + Proto: &validate.Violation{ + Field: &validate.FieldPath{ + Elements: []*validate.FieldPathElement{{ + FieldName: proto.String(string(o.Descriptor.Name())), + }}, + }, + ConstraintId: proto.String("required"), + Message: proto.String("exactly one field is required in oneof"), + }, }}} } return nil diff --git a/internal/expression/program.go b/internal/expression/program.go index b89d2b8..de76043 100644 --- a/internal/expression/program.go +++ b/internal/expression/program.go @@ -18,6 +18,7 @@ import ( "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" "github.com/bufbuild/protovalidate-go/internal/errors" "github.com/google/cel-go/cel" + "google.golang.org/protobuf/proto" "google.golang.org/protobuf/reflect/protoreflect" ) @@ -40,7 +41,7 @@ func (s ProgramSet) Eval(val protoreflect.Value, failFast bool) error { binding := s.bindThis(val.Interface()) defer varPool.Put(binding) - var violations []errors.Violation + var violations []*errors.Violation for _, expr := range s { violation, err := expr.eval(binding) if err != nil { @@ -95,7 +96,7 @@ type compiledProgram struct { } //nolint:nilnil // non-existence of violations is intentional -func (expr compiledProgram) eval(bindings *Variable) (*errors.ViolationData, error) { +func (expr compiledProgram) eval(bindings *Variable) (*errors.Violation, error) { now := nowPool.Get() defer nowPool.Put(now) bindings.Next = now @@ -110,24 +111,35 @@ func (expr compiledProgram) eval(bindings *Variable) (*errors.ViolationData, err if val == "" { return nil, nil } - return &errors.ViolationData{ - Rule: expr.Path, - RuleValue: expr.Value, - ConstraintID: expr.Source.GetId(), - Message: val, + return &errors.Violation{ + Proto: &validate.Violation{ + Rule: expr.rule(), + ConstraintId: proto.String(expr.Source.GetId()), + Message: proto.String(val), + }, + RuleValue: expr.Value, }, nil case bool: if val { return nil, nil } - return &errors.ViolationData{ - Rule: expr.Path, - RuleValue: expr.Value, - ConstraintID: expr.Source.GetId(), - Message: expr.Source.GetMessage(), + return &errors.Violation{ + Proto: &validate.Violation{ + Rule: expr.rule(), + ConstraintId: proto.String(expr.Source.GetId()), + Message: proto.String(expr.Source.GetMessage()), + }, + RuleValue: expr.Value, }, nil default: return nil, errors.NewRuntimeErrorf( "resolved to an unexpected type %T", val) } } + +func (expr compiledProgram) rule() *validate.FieldPath { + if len(expr.Path) > 0 { + return &validate.FieldPath{Elements: expr.Path} + } + return nil +} diff --git a/internal/expression/program_test.go b/internal/expression/program_test.go index 2191f2b..54986c2 100644 --- a/internal/expression/program_test.go +++ b/internal/expression/program_test.go @@ -89,7 +89,7 @@ func TestCompiled(t *testing.T) { if test.exViol == nil { assert.Nil(t, violation) } else { - assert.True(t, proto.Equal(test.exViol, violation.ToProto())) + assert.True(t, proto.Equal(test.exViol, violation.Proto)) } } }) diff --git a/validator.go b/validator.go index 84e971a..d8e1aa5 100644 --- a/validator.go +++ b/validator.go @@ -109,6 +109,7 @@ func (v *Validator) Validate(msg proto.Message) error { eval := v.builder.Load(refl.Descriptor()) err := eval.EvaluateMessage(refl, v.failFast) errors.ReverseFieldPaths(err) + errors.PopulateFieldPathStrings(err) return err } diff --git a/validator_example_test.go b/validator_example_test.go index b9d3608..1847de2 100644 --- a/validator_example_test.go +++ b/validator_example_test.go @@ -157,9 +157,8 @@ func ExampleValidationError() { var valErr *ValidationError if ok := errors.As(err, &valErr); ok { violation := valErr.Violations[0] - violationProto := violation.ToProto() - fmt.Println(FieldPathString(violationProto.GetField()), violationProto.GetConstraintId()) - fmt.Println(violation.GetRuleValue(), violation.GetFieldValue()) + fmt.Println(FieldPathString(violation.Proto.GetField()), violation.Proto.GetConstraintId()) + fmt.Println(violation.RuleValue, violation.FieldValue) } // output: lat double.gte_lte diff --git a/validator_test.go b/validator_test.go index f2055db..977fb04 100644 --- a/validator_test.go +++ b/validator_test.go @@ -228,7 +228,7 @@ func TestValidator_Validate_RepeatedItemCel(t *testing.T) { err = val.Validate(msg) valErr := &ValidationError{} require.ErrorAs(t, err, &valErr) - assert.Equal(t, "paths.no_space", valErr.Violations[0].ToProto().GetConstraintId()) + assert.Equal(t, "paths.no_space", valErr.Violations[0].Proto.GetConstraintId()) } func TestValidator_Validate_Issue141(t *testing.T) { From 6881184b01bef45234e2d23a09cf8f820662fe75 Mon Sep 17 00:00:00 2001 From: John Chadwick Date: Mon, 2 Dec 2024 14:36:52 -0500 Subject: [PATCH 13/24] Remove hardcoded field path elements, provide FieldDescriptor for values --- internal/constraints/cache.go | 2 +- internal/errors/validation.go | 18 +++++++++++++----- internal/evaluator/any.go | 30 +++++++++++++++++++----------- internal/evaluator/builder.go | 13 +++++++------ internal/evaluator/enum.go | 21 ++++++++++++++------- internal/evaluator/field.go | 15 ++++++++++----- internal/evaluator/map.go | 16 +++++++++------- internal/evaluator/repeated.go | 14 ++++++++------ internal/expression/ast.go | 33 ++++++++++++++++++++------------- internal/expression/program.go | 15 +++++++++------ 10 files changed, 110 insertions(+), 67 deletions(-) diff --git a/internal/constraints/cache.go b/internal/constraints/cache.go index c6e0689..d4777e1 100644 --- a/internal/constraints/cache.go +++ b/internal/constraints/cache.go @@ -88,7 +88,7 @@ func (c *Cache) Build( err = compileErr return false } - precomputedASTs.SetRuleValue(rule) + precomputedASTs.SetRuleValue(rule, desc) asts = asts.Merge(precomputedASTs) return true }) diff --git a/internal/errors/validation.go b/internal/errors/validation.go index 459aa31..22390fb 100644 --- a/internal/errors/validation.go +++ b/internal/errors/validation.go @@ -29,16 +29,24 @@ type Violation struct { // Proto contains the violation's proto.Message form. Proto *validate.Violation - // FieldValue returns the value of the specific field that failed - // validation. If there was no value, this will return an invalid value. + // FieldValue contains the value of the specific field that failed + // validation. If there was no value, this will contain an invalid value. FieldValue protoreflect.Value - // RuleValue returns the value of the rule that specified the failed + // FieldDescriptor contains the field descriptor corresponding to the + // FieldValue, if there is a field value. + FieldDescriptor protoreflect.FieldDescriptor + + // RuleValue contains the value of the rule that specified the failed // constraint. Not all constraints have a value; only standard and // predefined constraints have rule values. In violations caused by other - // kinds of constraints, like custom contraints, this will return an invalid - // value. + // kinds of constraints, like custom contraints, this will contain an + // invalid value. RuleValue protoreflect.Value + + // RuleDescriptor contains the field descriptor corresponding to the + // Rulevalue, if there is a rule value. + RuleDescriptor protoreflect.FieldDescriptor } // A ValidationError is returned if one or more constraint violations were diff --git a/internal/evaluator/any.go b/internal/evaluator/any.go index a408083..e934525 100644 --- a/internal/evaluator/any.go +++ b/internal/evaluator/any.go @@ -19,18 +19,20 @@ import ( "github.com/bufbuild/protovalidate-go/internal/errors" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/reflect/protoreflect" - "google.golang.org/protobuf/types/descriptorpb" ) //nolint:gochecknoglobals var ( - anyInRulePath = []*validate.FieldPathElement{ - {FieldName: proto.String("any"), FieldNumber: proto.Int32(20), FieldType: descriptorpb.FieldDescriptorProto_Type(11).Enum()}, - {FieldName: proto.String("in"), FieldNumber: proto.Int32(2), FieldType: descriptorpb.FieldDescriptorProto_Type(9).Enum()}, + anyRuleDescriptor = (&validate.FieldConstraints{}).ProtoReflect().Descriptor().Fields().ByName("any") + anyInRuleDescriptor = (&validate.AnyRules{}).ProtoReflect().Descriptor().Fields().ByName("in") + anyInRulePath = []*validate.FieldPathElement{ + errors.FieldPathElement(anyRuleDescriptor), + errors.FieldPathElement(anyInRuleDescriptor), } - anyNotInRulePath = []*validate.FieldPathElement{ - {FieldName: proto.String("any"), FieldNumber: proto.Int32(20), FieldType: descriptorpb.FieldDescriptorProto_Type(11).Enum()}, - {FieldName: proto.String("not_in"), FieldNumber: proto.Int32(3), FieldType: descriptorpb.FieldDescriptorProto_Type(9).Enum()}, + anyNotInDescriptor = (&validate.AnyRules{}).ProtoReflect().Descriptor().Fields().ByName("not_in") + anyNotInRulePath = []*validate.FieldPathElement{ + errors.FieldPathElement(anyRuleDescriptor), + errors.FieldPathElement(anyNotInDescriptor), } ) @@ -39,6 +41,8 @@ var ( // hydrate anyPB's within an expression, breaking evaluation if the type is // unknown at runtime. type anyPB struct { + // Descriptor is the FieldDescriptor targeted by this evaluator + Descriptor protoreflect.FieldDescriptor // TypeURLDescriptor is the descriptor for the TypeURL field TypeURLDescriptor protoreflect.FieldDescriptor // In specifies which type URLs the value may possess @@ -63,8 +67,10 @@ func (a anyPB) Evaluate(val protoreflect.Value, failFast bool) error { ConstraintId: proto.String("any.in"), Message: proto.String("type URL must be in the allow list"), }, - FieldValue: val, - RuleValue: a.InValue, + FieldValue: val, + FieldDescriptor: a.Descriptor, + RuleValue: a.InValue, + RuleDescriptor: anyInRuleDescriptor, }) if failFast { return err @@ -80,8 +86,10 @@ func (a anyPB) Evaluate(val protoreflect.Value, failFast bool) error { ConstraintId: proto.String("any.not_in"), Message: proto.String("type URL must not be in the block list"), }, - FieldValue: val, - RuleValue: a.NotInValue, + FieldValue: val, + FieldDescriptor: a.Descriptor, + RuleValue: a.NotInValue, + RuleDescriptor: anyNotInDescriptor, }) } } diff --git a/internal/evaluator/builder.go b/internal/evaluator/builder.go index 7ad7263..998e683 100644 --- a/internal/evaluator/builder.go +++ b/internal/evaluator/builder.go @@ -27,16 +27,14 @@ import ( "google.golang.org/protobuf/proto" "google.golang.org/protobuf/reflect/protoreflect" "google.golang.org/protobuf/reflect/protoregistry" - "google.golang.org/protobuf/types/descriptorpb" "google.golang.org/protobuf/types/dynamicpb" ) //nolint:gochecknoglobals -var celRuleField = validate.FieldPathElement{ - FieldName: proto.String("cel"), - FieldNumber: proto.Int32(23), - FieldType: descriptorpb.FieldDescriptorProto_Type(11).Enum(), -} +var ( + celRuleDescriptor = (&validate.FieldConstraints{}).ProtoReflect().Descriptor().Fields().ByName("cel") + celRuleField = errors.FieldPathElement(celRuleDescriptor) +) // Builder is a build-through cache of message evaluators keyed off the provided // descriptor. @@ -317,6 +315,7 @@ func (bldr *Builder) processFieldExpressions( }, }, } + compiledExpressions[i].Descriptor = celRuleDescriptor } if len(compiledExpressions) > 0 { eval.Constraints = append(eval.Constraints, celPrograms(compiledExpressions)) @@ -416,6 +415,7 @@ func (bldr *Builder) processAnyConstraints( inField := anyPbDesc.Fields().ByName("in") notInField := anyPbDesc.Fields().ByName("not_in") anyEval := anyPB{ + Descriptor: fdesc, TypeURLDescriptor: typeURLDesc, In: stringsToSet(fieldConstraints.GetAny().GetIn()), NotIn: stringsToSet(fieldConstraints.GetAny().GetNotIn()), @@ -438,6 +438,7 @@ func (bldr *Builder) processEnumConstraints( } if fieldConstraints.GetEnum().GetDefinedOnly() { appendEvaluator(valEval, definedEnum{ + Descriptor: fdesc, ValueDescriptors: fdesc.Enum().Values(), }, itemsWrapper) } diff --git a/internal/evaluator/enum.go b/internal/evaluator/enum.go index 7c70583..5ce1156 100644 --- a/internal/evaluator/enum.go +++ b/internal/evaluator/enum.go @@ -19,19 +19,24 @@ import ( "github.com/bufbuild/protovalidate-go/internal/errors" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/reflect/protoreflect" - "google.golang.org/protobuf/types/descriptorpb" ) //nolint:gochecknoglobals -var enumDefinedOnlyRulePath = []*validate.FieldPathElement{ - {FieldName: proto.String("enum"), FieldNumber: proto.Int32(16), FieldType: descriptorpb.FieldDescriptorProto_Type(11).Enum()}, - {FieldName: proto.String("defined_only"), FieldNumber: proto.Int32(2), FieldType: descriptorpb.FieldDescriptorProto_Type(8).Enum()}, -} +var ( + enumRuleDescriptor = (&validate.FieldConstraints{}).ProtoReflect().Descriptor().Fields().ByName("enum") + enumDefinedOnlyRuleDescriptor = (&validate.EnumRules{}).ProtoReflect().Descriptor().Fields().ByName("defined_only") + enumDefinedOnlyRulePath = []*validate.FieldPathElement{ + errors.FieldPathElement(enumRuleDescriptor), + errors.FieldPathElement(enumDefinedOnlyRuleDescriptor), + } +) // definedEnum is an evaluator that checks an enum value being a member of // the defined values exclusively. This check is handled outside CEL as enums // are completely type erased to integers. type definedEnum struct { + // Descriptor is the FieldDescriptor targeted by this evaluator + Descriptor protoreflect.FieldDescriptor // ValueDescriptors captures all the defined values for this enum ValueDescriptors protoreflect.EnumValueDescriptors } @@ -44,8 +49,10 @@ func (d definedEnum) Evaluate(val protoreflect.Value, _ bool) error { ConstraintId: proto.String("enum.defined_only"), Message: proto.String("value must be one of the defined enum values"), }, - FieldValue: val, - RuleValue: protoreflect.ValueOfBool(true), + FieldValue: val, + FieldDescriptor: d.Descriptor, + RuleValue: protoreflect.ValueOfBool(true), + RuleDescriptor: enumDefinedOnlyRuleDescriptor, }}} } return nil diff --git a/internal/evaluator/field.go b/internal/evaluator/field.go index 5931a25..865ed59 100644 --- a/internal/evaluator/field.go +++ b/internal/evaluator/field.go @@ -19,13 +19,15 @@ import ( "github.com/bufbuild/protovalidate-go/internal/errors" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/reflect/protoreflect" - "google.golang.org/protobuf/types/descriptorpb" ) //nolint:gochecknoglobals -var requiredRulePath = []*validate.FieldPathElement{ - {FieldName: proto.String("required"), FieldNumber: proto.Int32(25), FieldType: descriptorpb.FieldDescriptorProto_Type(8).Enum()}, -} +var ( + requiredRuleDescriptor = (&validate.FieldConstraints{}).ProtoReflect().Descriptor().Fields().ByName("required") + requiredRulePath = []*validate.FieldPathElement{ + errors.FieldPathElement(requiredRuleDescriptor), + } +) // field performs validation on a single message field, defined by its // descriptor. @@ -62,7 +64,10 @@ func (f field) EvaluateMessage(msg protoreflect.Message, failFast bool) (err err ConstraintId: proto.String("required"), Message: proto.String("value is required"), }, - RuleValue: protoreflect.ValueOfBool(true), + FieldValue: protoreflect.Value{}, + FieldDescriptor: f.Descriptor, + RuleValue: protoreflect.ValueOfBool(true), + RuleDescriptor: requiredRuleDescriptor, }}} } diff --git a/internal/evaluator/map.go b/internal/evaluator/map.go index c3a76a4..c1c708e 100644 --- a/internal/evaluator/map.go +++ b/internal/evaluator/map.go @@ -20,20 +20,22 @@ import ( "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" "github.com/bufbuild/protovalidate-go/internal/errors" - "google.golang.org/protobuf/proto" "google.golang.org/protobuf/reflect/protoreflect" "google.golang.org/protobuf/types/descriptorpb" ) //nolint:gochecknoglobals var ( - mapKeysRulePath = []*validate.FieldPathElement{ - {FieldName: proto.String("map"), FieldNumber: proto.Int32(19), FieldType: descriptorpb.FieldDescriptorProto_Type(11).Enum()}, - {FieldName: proto.String("keys"), FieldNumber: proto.Int32(4), FieldType: descriptorpb.FieldDescriptorProto_Type(11).Enum()}, + mapRuleDescriptor = (&validate.FieldConstraints{}).ProtoReflect().Descriptor().Fields().ByName("map") + mapKeysRuleDescriptor = (&validate.MapRules{}).ProtoReflect().Descriptor().Fields().ByName("keys") + mapKeysRulePath = []*validate.FieldPathElement{ + errors.FieldPathElement(mapRuleDescriptor), + errors.FieldPathElement(mapKeysRuleDescriptor), } - mapValuesRulePath = []*validate.FieldPathElement{ - {FieldName: proto.String("map"), FieldNumber: proto.Int32(19), FieldType: descriptorpb.FieldDescriptorProto_Type(11).Enum()}, - {FieldName: proto.String("values"), FieldNumber: proto.Int32(5), FieldType: descriptorpb.FieldDescriptorProto_Type(11).Enum()}, + mapValuesDescriptor = (&validate.MapRules{}).ProtoReflect().Descriptor().Fields().ByName("values") + mapValuesRulePath = []*validate.FieldPathElement{ + errors.FieldPathElement(mapRuleDescriptor), + errors.FieldPathElement(mapValuesDescriptor), } ) diff --git a/internal/evaluator/repeated.go b/internal/evaluator/repeated.go index 4a59e98..7d942cd 100644 --- a/internal/evaluator/repeated.go +++ b/internal/evaluator/repeated.go @@ -17,16 +17,18 @@ package evaluator import ( "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" "github.com/bufbuild/protovalidate-go/internal/errors" - "google.golang.org/protobuf/proto" "google.golang.org/protobuf/reflect/protoreflect" - "google.golang.org/protobuf/types/descriptorpb" ) //nolint:gochecknoglobals -var repeatedItemsFieldPath = []*validate.FieldPathElement{ - {FieldName: proto.String("repeated"), FieldNumber: proto.Int32(18), FieldType: descriptorpb.FieldDescriptorProto_Type(11).Enum()}, - {FieldName: proto.String("items"), FieldNumber: proto.Int32(4), FieldType: descriptorpb.FieldDescriptorProto_Type(11).Enum()}, -} +var ( + repeatedRuleDescriptor = (&validate.FieldConstraints{}).ProtoReflect().Descriptor().Fields().ByName("repeated") + repeatedItemsRuleDescriptor = (&validate.RepeatedRules{}).ProtoReflect().Descriptor().Fields().ByName("items") + repeatedItemsFieldPath = []*validate.FieldPathElement{ + errors.FieldPathElement(repeatedRuleDescriptor), + errors.FieldPathElement(repeatedItemsRuleDescriptor), + } +) // listItems performs validation on the elements of a repeated field. type listItems struct { diff --git a/internal/expression/ast.go b/internal/expression/ast.go index ee03fe9..a6c2458 100644 --- a/internal/expression/ast.go +++ b/internal/expression/ast.go @@ -84,10 +84,11 @@ func (set ASTSet) ReduceResiduals(opts ...cel.ProgramOption) (ProgramSet, error) x := residual.Source().Content() _ = x residuals = append(residuals, compiledAST{ - AST: residual, - Source: ast.Source, - Path: ast.Path, - Value: ast.Value, + AST: residual, + Source: ast.Source, + Path: ast.Path, + Value: ast.Value, + Descriptor: ast.Descriptor, }) } } @@ -114,18 +115,23 @@ func (set ASTSet) ToProgramSet(opts ...cel.ProgramOption) (out ProgramSet, err e } // SetRuleValue sets the rule value for the programs in the ASTSet. -func (set *ASTSet) SetRuleValue(ruleValue protoreflect.Value) { +func (set *ASTSet) SetRuleValue( + ruleValue protoreflect.Value, + ruleDescriptor protoreflect.FieldDescriptor, +) { set.asts = append([]compiledAST{}, set.asts...) for i := range set.asts { set.asts[i].Value = ruleValue + set.asts[i].Descriptor = ruleDescriptor } } type compiledAST struct { - AST *cel.Ast - Source *validate.Constraint - Path []*validate.FieldPathElement - Value protoreflect.Value + AST *cel.Ast + Source *validate.Constraint + Path []*validate.FieldPathElement + Value protoreflect.Value + Descriptor protoreflect.FieldDescriptor } func (ast compiledAST) toProgram(env *cel.Env, opts ...cel.ProgramOption) (out compiledProgram, err error) { @@ -135,9 +141,10 @@ func (ast compiledAST) toProgram(env *cel.Env, opts ...cel.ProgramOption) (out c "failed to compile program %s: %w", ast.Source.GetId(), err) } return compiledProgram{ - Program: prog, - Source: ast.Source, - Path: ast.Path, - Value: ast.Value, + Program: prog, + Source: ast.Source, + Path: ast.Path, + Value: ast.Value, + Descriptor: ast.Descriptor, }, nil } diff --git a/internal/expression/program.go b/internal/expression/program.go index de76043..7843c33 100644 --- a/internal/expression/program.go +++ b/internal/expression/program.go @@ -89,10 +89,11 @@ func (s ProgramSet) bindThis(val any) *Variable { // compiledProgram is a parsed and type-checked cel.Program along with the // source Expression. type compiledProgram struct { - Program cel.Program - Source *validate.Constraint - Path []*validate.FieldPathElement - Value protoreflect.Value + Program cel.Program + Source *validate.Constraint + Path []*validate.FieldPathElement + Value protoreflect.Value + Descriptor protoreflect.FieldDescriptor } //nolint:nilnil // non-existence of violations is intentional @@ -117,7 +118,8 @@ func (expr compiledProgram) eval(bindings *Variable) (*errors.Violation, error) ConstraintId: proto.String(expr.Source.GetId()), Message: proto.String(val), }, - RuleValue: expr.Value, + RuleValue: expr.Value, + RuleDescriptor: expr.Descriptor, }, nil case bool: if val { @@ -129,7 +131,8 @@ func (expr compiledProgram) eval(bindings *Variable) (*errors.Violation, error) ConstraintId: proto.String(expr.Source.GetId()), Message: proto.String(expr.Source.GetMessage()), }, - RuleValue: expr.Value, + RuleValue: expr.Value, + RuleDescriptor: expr.Descriptor, }, nil default: return nil, errors.NewRuntimeErrorf( From 0ba11b58f7c613a1451b8723da9dcc0c943e21e1 Mon Sep 17 00:00:00 2001 From: John Chadwick Date: Mon, 2 Dec 2024 18:10:26 -0500 Subject: [PATCH 14/24] Refactor: reduce allocs, remove wrappers --- internal/errors/utils.go | 32 ++++++---- internal/evaluator/any.go | 30 ++++++---- internal/evaluator/base.go | 90 ++++++++++++++++++++++++++++ internal/evaluator/builder.go | 103 +++++++++++++++++--------------- internal/evaluator/cel.go | 30 ++++++++-- internal/evaluator/enum.go | 17 +++--- internal/evaluator/evaluator.go | 9 --- internal/evaluator/field.go | 31 ++++------ internal/evaluator/map.go | 68 ++++++++------------- internal/evaluator/message.go | 27 ++++++++- internal/evaluator/repeated.go | 40 +++++-------- internal/evaluator/value.go | 23 ++++++- internal/expression/program.go | 1 - 13 files changed, 315 insertions(+), 186 deletions(-) create mode 100644 internal/evaluator/base.go diff --git a/internal/errors/utils.go b/internal/errors/utils.go index 4bba641..a86c8ac 100644 --- a/internal/errors/utils.go +++ b/internal/errors/utils.go @@ -56,6 +56,9 @@ func Merge(dst, src error, failFast bool) (ok bool, err error) { } func FieldPathElement(field protoreflect.FieldDescriptor) *validate.FieldPathElement { + if field == nil { + return nil + } return &validate.FieldPathElement{ FieldNumber: proto.Int32(int32(field.Number())), FieldName: proto.String(field.TextName()), @@ -63,25 +66,29 @@ func FieldPathElement(field protoreflect.FieldDescriptor) *validate.FieldPathEle } } +func FieldPath(field protoreflect.FieldDescriptor) *validate.FieldPath { + if field == nil { + return nil + } + return &validate.FieldPath{ + Elements: []*validate.FieldPathElement{ + FieldPathElement(field), + }, + } +} + // AppendFieldPath appends an element to the end of each field path in err. -// As an exception, if skipSubscript is true, any field paths ending in a -// subscript element will not have a suffix element appended to them. // // Note that this function is ordinarily used to append field paths in reverse // order, as the stack bubbles up through the evaluators. Then, at the end, the // path is reversed. -func AppendFieldPath(err error, suffix *validate.FieldPathElement, skipSubscript bool) { +func AppendFieldPath(err error, suffix *validate.FieldPathElement) { + if suffix == nil { + return + } var valErr *ValidationError if errors.As(err, &valErr) { for _, violation := range valErr.Violations { - // Special case: Here we skip appending if the last element had a - // subscript. This is a weird special case that makes it - // significantly simpler to handle reverse-constructing paths with - // maps and slices. - if elements := violation.Proto.GetField().GetElements(); skipSubscript && - len(elements) > 0 && elements[len(elements)-1].Subscript != nil { - continue - } if violation.Proto.GetField() == nil { violation.Proto.Field = &validate.FieldPath{} } @@ -96,6 +103,9 @@ func AppendFieldPath(err error, suffix *validate.FieldPathElement, skipSubscript // is better to avoid the copy instead if possible. This prepend is only used in // the error case for nested rules (repeated.items, map.keys, map.values.) func PrependRulePath(err error, prefix []*validate.FieldPathElement) { + if len(prefix) == 0 { + return + } var valErr *ValidationError if errors.As(err, &valErr) { for _, violation := range valErr.Violations { diff --git a/internal/evaluator/any.go b/internal/evaluator/any.go index e934525..8a46c43 100644 --- a/internal/evaluator/any.go +++ b/internal/evaluator/any.go @@ -25,14 +25,18 @@ import ( var ( anyRuleDescriptor = (&validate.FieldConstraints{}).ProtoReflect().Descriptor().Fields().ByName("any") anyInRuleDescriptor = (&validate.AnyRules{}).ProtoReflect().Descriptor().Fields().ByName("in") - anyInRulePath = []*validate.FieldPathElement{ - errors.FieldPathElement(anyRuleDescriptor), - errors.FieldPathElement(anyInRuleDescriptor), + anyInRulePath = &validate.FieldPath{ + Elements: []*validate.FieldPathElement{ + errors.FieldPathElement(anyRuleDescriptor), + errors.FieldPathElement(anyInRuleDescriptor), + }, } anyNotInDescriptor = (&validate.AnyRules{}).ProtoReflect().Descriptor().Fields().ByName("not_in") - anyNotInRulePath = []*validate.FieldPathElement{ - errors.FieldPathElement(anyRuleDescriptor), - errors.FieldPathElement(anyNotInDescriptor), + anyNotInRulePath = &validate.FieldPath{ + Elements: []*validate.FieldPathElement{ + errors.FieldPathElement(anyRuleDescriptor), + errors.FieldPathElement(anyNotInDescriptor), + }, } ) @@ -41,8 +45,8 @@ var ( // hydrate anyPB's within an expression, breaking evaluation if the type is // unknown at runtime. type anyPB struct { - // Descriptor is the FieldDescriptor targeted by this evaluator - Descriptor protoreflect.FieldDescriptor + base base + // TypeURLDescriptor is the descriptor for the TypeURL field TypeURLDescriptor protoreflect.FieldDescriptor // In specifies which type URLs the value may possess @@ -63,12 +67,13 @@ func (a anyPB) Evaluate(val protoreflect.Value, failFast bool) error { if _, ok := a.In[typeURL]; !ok { err.Violations = append(err.Violations, &errors.Violation{ Proto: &validate.Violation{ - Rule: &validate.FieldPath{Elements: anyInRulePath}, + Field: a.base.fieldPath(), + Rule: a.base.rulePath(anyInRulePath), ConstraintId: proto.String("any.in"), Message: proto.String("type URL must be in the allow list"), }, FieldValue: val, - FieldDescriptor: a.Descriptor, + FieldDescriptor: a.base.Descriptor, RuleValue: a.InValue, RuleDescriptor: anyInRuleDescriptor, }) @@ -82,12 +87,13 @@ func (a anyPB) Evaluate(val protoreflect.Value, failFast bool) error { if _, ok := a.NotIn[typeURL]; ok { err.Violations = append(err.Violations, &errors.Violation{ Proto: &validate.Violation{ - Rule: &validate.FieldPath{Elements: anyNotInRulePath}, + Field: a.base.fieldPath(), + Rule: a.base.rulePath(anyNotInRulePath), ConstraintId: proto.String("any.not_in"), Message: proto.String("type URL must not be in the block list"), }, FieldValue: val, - FieldDescriptor: a.Descriptor, + FieldDescriptor: a.base.Descriptor, RuleValue: a.NotInValue, RuleDescriptor: anyNotInDescriptor, }) diff --git a/internal/evaluator/base.go b/internal/evaluator/base.go new file mode 100644 index 0000000..57be3b6 --- /dev/null +++ b/internal/evaluator/base.go @@ -0,0 +1,90 @@ +// Copyright 2023-2024 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package evaluator + +import ( + "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" + "github.com/bufbuild/protovalidate-go/internal/errors" + "google.golang.org/protobuf/reflect/protoreflect" +) + +// base is a common struct used by all field evaluators. It holds +// some common information used across all field evaluators. +type base struct { + // Descriptor is the FieldDescriptor targeted by this evaluator, nor nil if + // there is none. + Descriptor protoreflect.FieldDescriptor + + // FieldPatht is the field path element that pertains to this evaluator, or + // nil if there is none. + FieldPathElement *validate.FieldPathElement + + // RulePrefix is a static prefix this evaluator should add to the rule path + // of violations. + RulePrefix *validate.FieldPath +} + +func newBase(valEval *value) base { + return base{ + Descriptor: valEval.Descriptor, + FieldPathElement: errors.FieldPathElement(valEval.Descriptor), + RulePrefix: rulePrefixForNesting(valEval.Nested), + } +} + +func (b *base) fieldPath() *validate.FieldPath { + if b.FieldPathElement == nil { + return nil + } + return &validate.FieldPath{ + Elements: []*validate.FieldPathElement{ + b.FieldPathElement, + }, + } +} + +func (b *base) rulePath(suffix *validate.FieldPath) *validate.FieldPath { + return prefixRulePath(b.RulePrefix, suffix) +} + +func rulePrefixForNesting(typ nestedType) *validate.FieldPath { + switch typ { + case nestedNone: + return nil + case nestedRepeatedItem: + return repeatedItemsRulePath + case nestedMapKey: + return mapKeysRulePath + case nestedMapValue: + return mapValuesRulePath + default: + return nil + } +} + +func prefixRulePath(prefix *validate.FieldPath, suffix *validate.FieldPath) *validate.FieldPath { + if len(prefix.GetElements()) > 0 { + return &validate.FieldPath{ + Elements: append( + append( + []*validate.FieldPathElement{}, + prefix.GetElements()..., + ), + suffix.GetElements()..., + ), + } + } + return suffix +} diff --git a/internal/evaluator/builder.go b/internal/evaluator/builder.go index 998e683..a7e2b12 100644 --- a/internal/evaluator/builder.go +++ b/internal/evaluator/builder.go @@ -175,7 +175,9 @@ func (bldr *Builder) processMessageExpressions( return } - msgEval.Append(celPrograms(compiledExprs)) + msgEval.Append(celPrograms{ + ProgramSet: compiledExprs, + }) } func (bldr *Builder) processOneofConstraints( @@ -221,8 +223,10 @@ func (bldr *Builder) buildField( cache MessageCache, ) (field, error) { fld := field{ - Descriptor: fieldDescriptor, - Required: fieldConstraints.GetRequired(), + Value: value{ + Descriptor: fieldDescriptor, + }, + Required: fieldConstraints.GetRequired(), IgnoreEmpty: fieldDescriptor.HasPresence() || bldr.shouldIgnoreEmpty(fieldConstraints), IgnoreDefault: fieldDescriptor.HasPresence() && @@ -231,21 +235,19 @@ func (bldr *Builder) buildField( if fld.IgnoreDefault { fld.Zero = bldr.zeroValue(fieldDescriptor, false) } - err := bldr.buildValue(fieldDescriptor, fieldConstraints, nil, &fld.Value, cache) + err := bldr.buildValue(fieldDescriptor, fieldConstraints, &fld.Value, cache) return fld, err } func (bldr *Builder) buildValue( fdesc protoreflect.FieldDescriptor, constraints *validate.FieldConstraints, - itemsWrapper wrapper, valEval *value, cache MessageCache, ) (err error) { steps := []func( fdesc protoreflect.FieldDescriptor, fieldConstraints *validate.FieldConstraints, - itemsWrapper wrapper, valEval *value, cache MessageCache, ) error{ @@ -261,7 +263,7 @@ func (bldr *Builder) buildValue( } for _, step := range steps { - if err = step(fdesc, constraints, itemsWrapper, valEval, cache); err != nil { + if err = step(fdesc, constraints, valEval, cache); err != nil { return err } } @@ -271,15 +273,14 @@ func (bldr *Builder) buildValue( func (bldr *Builder) processIgnoreEmpty( fdesc protoreflect.FieldDescriptor, constraints *validate.FieldConstraints, - itemsWrapper wrapper, val *value, _ MessageCache, ) error { // the only time we need to ignore empty on a value is if it's evaluating a // field item (repeated element or map key/value). - val.IgnoreEmpty = itemsWrapper != nil && bldr.shouldIgnoreEmpty(constraints) + val.IgnoreEmpty = val.Nested != nestedNone && bldr.shouldIgnoreEmpty(constraints) if val.IgnoreEmpty { - val.Zero = bldr.zeroValue(fdesc, itemsWrapper != nil) + val.Zero = bldr.zeroValue(fdesc, val.Nested != nestedNone) } return nil } @@ -287,7 +288,6 @@ func (bldr *Builder) processIgnoreEmpty( func (bldr *Builder) processFieldExpressions( fieldDesc protoreflect.FieldDescriptor, fieldConstraints *validate.FieldConstraints, - itemsWrapper wrapper, eval *value, _ MessageCache, ) error { @@ -295,7 +295,7 @@ func (bldr *Builder) processFieldExpressions( Constraints: fieldConstraints.GetCel(), } - celTyp := celext.ProtoFieldToCELType(fieldDesc, false, itemsWrapper != nil) + celTyp := celext.ProtoFieldToCELType(fieldDesc, false, eval.Nested != nestedNone) opts := append( celext.RequiredCELEnvOptions(fieldDesc), cel.Variable("this", celTyp), @@ -318,7 +318,12 @@ func (bldr *Builder) processFieldExpressions( compiledExpressions[i].Descriptor = celRuleDescriptor } if len(compiledExpressions) > 0 { - eval.Constraints = append(eval.Constraints, celPrograms(compiledExpressions)) + eval.Constraints = append(eval.Constraints, + celPrograms{ + base: newBase(eval), + ProgramSet: compiledExpressions, + }, + ) } return nil } @@ -326,14 +331,13 @@ func (bldr *Builder) processFieldExpressions( func (bldr *Builder) processEmbeddedMessage( fdesc protoreflect.FieldDescriptor, rules *validate.FieldConstraints, - itemsWrapper wrapper, valEval *value, cache MessageCache, ) error { if !isMessageField(fdesc) || bldr.shouldSkip(rules) || fdesc.IsMap() || - (fdesc.IsList() && itemsWrapper == nil) { + (fdesc.IsList() && valEval.Nested == nestedNone) { return nil } @@ -343,7 +347,10 @@ func (bldr *Builder) processEmbeddedMessage( "failed to compile embedded type %s for %s: %w", fdesc.Message().FullName(), fdesc.FullName(), err) } - appendEvaluator(valEval, embedEval, nil) + valEval.Append(&embeddedMessage{ + base: newBase(valEval), + message: embedEval, + }) return nil } @@ -351,14 +358,13 @@ func (bldr *Builder) processEmbeddedMessage( func (bldr *Builder) processWrapperConstraints( fdesc protoreflect.FieldDescriptor, rules *validate.FieldConstraints, - itemsWrapper wrapper, valEval *value, cache MessageCache, ) error { if !isMessageField(fdesc) || bldr.shouldSkip(rules) || fdesc.IsMap() || - (fdesc.IsList() && itemsWrapper == nil) { + (fdesc.IsList() && valEval.Nested == nestedNone) { return nil } @@ -366,19 +372,21 @@ func (bldr *Builder) processWrapperConstraints( if !ok || !rules.ProtoReflect().Has(expectedWrapperDescriptor) { return nil } - var unwrapped value - err := bldr.buildValue(fdesc.Message().Fields().ByName("value"), rules, nil, &unwrapped, cache) + unwrapped := value{ + Descriptor: valEval.Descriptor, + Nested: valEval.Nested, + } + err := bldr.buildValue(fdesc.Message().Fields().ByName("value"), rules, &unwrapped, cache) if err != nil { return err } - appendEvaluator(valEval, unwrapped.Constraints, itemsWrapper) + valEval.Append(unwrapped.Constraints) return nil } func (bldr *Builder) processStandardConstraints( fdesc protoreflect.FieldDescriptor, constraints *validate.FieldConstraints, - itemsWrapper wrapper, valEval *value, _ MessageCache, ) error { @@ -388,23 +396,25 @@ func (bldr *Builder) processStandardConstraints( constraints, bldr.extensionTypeResolver, bldr.allowUnknownFields, - itemsWrapper != nil, + valEval.Nested != nestedNone, ) if err != nil { return err } - appendEvaluator(valEval, celPrograms(stdConstraints), itemsWrapper) + valEval.Append(celPrograms{ + base: newBase(valEval), + ProgramSet: stdConstraints, + }) return nil } func (bldr *Builder) processAnyConstraints( fdesc protoreflect.FieldDescriptor, fieldConstraints *validate.FieldConstraints, - itemsWrapper wrapper, valEval *value, _ MessageCache, ) error { - if (fdesc.IsList() && itemsWrapper == nil) || + if (fdesc.IsList() && valEval.Nested == nestedNone) || !isMessageField(fdesc) || fdesc.Message().FullName() != "google.protobuf.Any" { return nil @@ -415,21 +425,20 @@ func (bldr *Builder) processAnyConstraints( inField := anyPbDesc.Fields().ByName("in") notInField := anyPbDesc.Fields().ByName("not_in") anyEval := anyPB{ - Descriptor: fdesc, + base: newBase(valEval), TypeURLDescriptor: typeURLDesc, In: stringsToSet(fieldConstraints.GetAny().GetIn()), NotIn: stringsToSet(fieldConstraints.GetAny().GetNotIn()), InValue: fieldConstraints.GetAny().ProtoReflect().Get(inField), NotInValue: fieldConstraints.GetAny().ProtoReflect().Get(notInField), } - appendEvaluator(valEval, anyEval, itemsWrapper) + valEval.Append(anyEval) return nil } func (bldr *Builder) processEnumConstraints( fdesc protoreflect.FieldDescriptor, fieldConstraints *validate.FieldConstraints, - itemsWrapper wrapper, valEval *value, _ MessageCache, ) error { @@ -437,10 +446,10 @@ func (bldr *Builder) processEnumConstraints( return nil } if fieldConstraints.GetEnum().GetDefinedOnly() { - appendEvaluator(valEval, definedEnum{ - Descriptor: fdesc, + valEval.Append(definedEnum{ + base: newBase(valEval), ValueDescriptors: fdesc.Enum().Values(), - }, itemsWrapper) + }) } return nil } @@ -448,7 +457,6 @@ func (bldr *Builder) processEnumConstraints( func (bldr *Builder) processMapConstraints( fieldDesc protoreflect.FieldDescriptor, constraints *validate.FieldConstraints, - itemsWrapper wrapper, valEval *value, cache MessageCache, ) error { @@ -457,13 +465,18 @@ func (bldr *Builder) processMapConstraints( } mapEval := kvPairs{ - Descriptor: fieldDesc, + base: newBase(valEval), + KeyConstraints: value{ + Nested: nestedMapKey, + }, + ValueConstraints: value{ + Nested: nestedMapValue, + }, } err := bldr.buildValue( fieldDesc.MapKey(), constraints.GetMap().GetKeys(), - newKeysWrapper, &mapEval.KeyConstraints, cache) if err != nil { @@ -475,7 +488,6 @@ func (bldr *Builder) processMapConstraints( err = bldr.buildValue( fieldDesc.MapValue(), constraints.GetMap().GetValues(), - newValuesWrapper, &mapEval.ValueConstraints, cache) if err != nil { @@ -484,26 +496,28 @@ func (bldr *Builder) processMapConstraints( fieldDesc.FullName(), err) } - appendEvaluator(valEval, mapEval, itemsWrapper) + valEval.Append(mapEval) return nil } func (bldr *Builder) processRepeatedConstraints( fdesc protoreflect.FieldDescriptor, fieldConstraints *validate.FieldConstraints, - itemsWrapper wrapper, valEval *value, cache MessageCache, ) error { - if !fdesc.IsList() || itemsWrapper != nil { + if !fdesc.IsList() || valEval.Nested != nestedNone { return nil } listEval := listItems{ - Descriptor: fdesc, + base: newBase(valEval), + ItemConstraints: value{ + Nested: nestedRepeatedItem, + }, } - err := bldr.buildValue(fdesc, fieldConstraints.GetRepeated().GetItems(), newItemsWrapper, &listEval.ItemConstraints, cache) + err := bldr.buildValue(fdesc, fieldConstraints.GetRepeated().GetItems(), &listEval.ItemConstraints, cache) if err != nil { return errors.NewCompilationErrorf( "failed to compile items constraints for repeated %v: %w", fdesc.FullName(), err) @@ -555,13 +569,6 @@ func (c MessageCache) SyncTo(other MessageCache) { } } -func appendEvaluator(value *value, evaluator evaluator, wrapper wrapper) { - if wrapper != nil { - evaluator = wrapper(evaluator) - } - value.Append(evaluator) -} - // isMessageField returns true if the field descriptor fdesc describes a field // containing a submessage. Although they are represented differently on the // wire, group fields are treated like message fields in protoreflect and have diff --git a/internal/evaluator/cel.go b/internal/evaluator/cel.go index cc6dd5a..d2fea76 100644 --- a/internal/evaluator/cel.go +++ b/internal/evaluator/cel.go @@ -15,26 +15,44 @@ package evaluator import ( + "errors" + + pverr "github.com/bufbuild/protovalidate-go/internal/errors" "github.com/bufbuild/protovalidate-go/internal/expression" "google.golang.org/protobuf/reflect/protoreflect" ) // celPrograms is an evaluator that executes an expression.ProgramSet. -type celPrograms expression.ProgramSet +type celPrograms struct { + base + expression.ProgramSet +} func (c celPrograms) Evaluate(val protoreflect.Value, failFast bool) error { - return expression.ProgramSet(c).Eval(val, failFast) + err := c.ProgramSet.Eval(val, failFast) + if err != nil { + var valErr *pverr.ValidationError + if errors.As(err, &valErr) { + for _, violation := range valErr.Violations { + violation.Proto.Field = c.base.fieldPath() + violation.Proto.Rule = c.base.rulePath(violation.Proto.GetRule()) + violation.FieldValue = val + violation.FieldDescriptor = c.base.Descriptor + } + } + } + return err } func (c celPrograms) EvaluateMessage(msg protoreflect.Message, failFast bool) error { - return expression.ProgramSet(c).Eval(protoreflect.ValueOfMessage(msg), failFast) + return c.ProgramSet.Eval(protoreflect.ValueOfMessage(msg), failFast) } func (c celPrograms) Tautology() bool { - return len(c) == 0 + return len(c.ProgramSet) == 0 } var ( - _ evaluator = (celPrograms)(nil) - _ MessageEvaluator = (celPrograms)(nil) + _ evaluator = celPrograms{} + _ MessageEvaluator = celPrograms{} ) diff --git a/internal/evaluator/enum.go b/internal/evaluator/enum.go index 5ce1156..4379232 100644 --- a/internal/evaluator/enum.go +++ b/internal/evaluator/enum.go @@ -25,9 +25,11 @@ import ( var ( enumRuleDescriptor = (&validate.FieldConstraints{}).ProtoReflect().Descriptor().Fields().ByName("enum") enumDefinedOnlyRuleDescriptor = (&validate.EnumRules{}).ProtoReflect().Descriptor().Fields().ByName("defined_only") - enumDefinedOnlyRulePath = []*validate.FieldPathElement{ - errors.FieldPathElement(enumRuleDescriptor), - errors.FieldPathElement(enumDefinedOnlyRuleDescriptor), + enumDefinedOnlyRulePath = &validate.FieldPath{ + Elements: []*validate.FieldPathElement{ + errors.FieldPathElement(enumRuleDescriptor), + errors.FieldPathElement(enumDefinedOnlyRuleDescriptor), + }, } ) @@ -35,8 +37,8 @@ var ( // the defined values exclusively. This check is handled outside CEL as enums // are completely type erased to integers. type definedEnum struct { - // Descriptor is the FieldDescriptor targeted by this evaluator - Descriptor protoreflect.FieldDescriptor + base + // ValueDescriptors captures all the defined values for this enum ValueDescriptors protoreflect.EnumValueDescriptors } @@ -45,12 +47,13 @@ func (d definedEnum) Evaluate(val protoreflect.Value, _ bool) error { if d.ValueDescriptors.ByNumber(val.Enum()) == nil { return &errors.ValidationError{Violations: []*errors.Violation{{ Proto: &validate.Violation{ - Rule: &validate.FieldPath{Elements: enumDefinedOnlyRulePath}, + Field: d.base.fieldPath(), + Rule: d.base.rulePath(enumDefinedOnlyRulePath), ConstraintId: proto.String("enum.defined_only"), Message: proto.String("value must be one of the defined enum values"), }, FieldValue: val, - FieldDescriptor: d.Descriptor, + FieldDescriptor: d.base.Descriptor, RuleValue: protoreflect.ValueOfBool(true), RuleDescriptor: enumDefinedOnlyRuleDescriptor, }}} diff --git a/internal/evaluator/evaluator.go b/internal/evaluator/evaluator.go index 359523d..5cee85c 100644 --- a/internal/evaluator/evaluator.go +++ b/internal/evaluator/evaluator.go @@ -101,12 +101,3 @@ func (m messageEvaluators) Tautology() bool { } return true } - -// wrapper wraps an evaluator, used to handle some logic that applies -// specifically to recursive constraint rules. -type wrapper func(evaluator) evaluator - -var ( - _ evaluator = evaluators(nil) - _ MessageEvaluator = messageEvaluators(nil) -) diff --git a/internal/evaluator/field.go b/internal/evaluator/field.go index 865ed59..18a8830 100644 --- a/internal/evaluator/field.go +++ b/internal/evaluator/field.go @@ -24,8 +24,10 @@ import ( //nolint:gochecknoglobals var ( requiredRuleDescriptor = (&validate.FieldConstraints{}).ProtoReflect().Descriptor().Fields().ByName("required") - requiredRulePath = []*validate.FieldPathElement{ - errors.FieldPathElement(requiredRuleDescriptor), + requiredRulePath = &validate.FieldPath{ + Elements: []*validate.FieldPathElement{ + errors.FieldPathElement(requiredRuleDescriptor), + }, } ) @@ -34,8 +36,6 @@ var ( type field struct { // Value is the evaluator to apply to the field's value Value value - // Descriptor is the FieldDescriptor targeted by this evaluator - Descriptor protoreflect.FieldDescriptor // Required indicates that the field must have a set value. Required bool // IgnoreEmpty indicates if a field should skip validation on its zero value. @@ -54,39 +54,30 @@ func (f field) Evaluate(val protoreflect.Value, failFast bool) error { } func (f field) EvaluateMessage(msg protoreflect.Message, failFast bool) (err error) { - if f.Required && !msg.Has(f.Descriptor) { + if f.Required && !msg.Has(f.Value.Descriptor) { return &errors.ValidationError{Violations: []*errors.Violation{{ Proto: &validate.Violation{ - Field: &validate.FieldPath{Elements: []*validate.FieldPathElement{ - errors.FieldPathElement(f.Descriptor), - }}, - Rule: &validate.FieldPath{Elements: requiredRulePath}, + Field: errors.FieldPath(f.Value.Descriptor), + Rule: prefixRulePath(rulePrefixForNesting(f.Value.Nested), requiredRulePath), ConstraintId: proto.String("required"), Message: proto.String("value is required"), }, FieldValue: protoreflect.Value{}, - FieldDescriptor: f.Descriptor, + FieldDescriptor: f.Value.Descriptor, RuleValue: protoreflect.ValueOfBool(true), RuleDescriptor: requiredRuleDescriptor, }}} } - if f.IgnoreEmpty && !msg.Has(f.Descriptor) { + if f.IgnoreEmpty && !msg.Has(f.Value.Descriptor) { return nil } - val := msg.Get(f.Descriptor) + val := msg.Get(f.Value.Descriptor) if f.IgnoreDefault && val.Equal(f.Zero) { return nil } - if err = f.Value.Evaluate(val, failFast); err != nil { - errors.AppendFieldPath( - err, - errors.FieldPathElement(f.Descriptor), - f.Descriptor.Cardinality() == protoreflect.Repeated, - ) - } - return err + return f.Value.Evaluate(val, failFast) } func (f field) Tautology() bool { diff --git a/internal/evaluator/map.go b/internal/evaluator/map.go index c1c708e..8b7aa7e 100644 --- a/internal/evaluator/map.go +++ b/internal/evaluator/map.go @@ -20,6 +20,7 @@ import ( "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" "github.com/bufbuild/protovalidate-go/internal/errors" + "google.golang.org/protobuf/proto" "google.golang.org/protobuf/reflect/protoreflect" "google.golang.org/protobuf/types/descriptorpb" ) @@ -28,21 +29,25 @@ import ( var ( mapRuleDescriptor = (&validate.FieldConstraints{}).ProtoReflect().Descriptor().Fields().ByName("map") mapKeysRuleDescriptor = (&validate.MapRules{}).ProtoReflect().Descriptor().Fields().ByName("keys") - mapKeysRulePath = []*validate.FieldPathElement{ - errors.FieldPathElement(mapRuleDescriptor), - errors.FieldPathElement(mapKeysRuleDescriptor), + mapKeysRulePath = &validate.FieldPath{ + Elements: []*validate.FieldPathElement{ + errors.FieldPathElement(mapRuleDescriptor), + errors.FieldPathElement(mapKeysRuleDescriptor), + }, } mapValuesDescriptor = (&validate.MapRules{}).ProtoReflect().Descriptor().Fields().ByName("values") - mapValuesRulePath = []*validate.FieldPathElement{ - errors.FieldPathElement(mapRuleDescriptor), - errors.FieldPathElement(mapValuesDescriptor), + mapValuesRulePath = &validate.FieldPath{ + Elements: []*validate.FieldPathElement{ + errors.FieldPathElement(mapRuleDescriptor), + errors.FieldPathElement(mapValuesDescriptor), + }, } ) // kvPairs performs validation on a map field's KV Pairs. type kvPairs struct { - // Descriptor is the FieldDescriptor targeted by this evaluator - Descriptor protoreflect.FieldDescriptor + base + // KeyConstraints are checked on the map keys KeyConstraints value // ValueConstraints are checked on the map values @@ -54,10 +59,14 @@ func (m kvPairs) Evaluate(val protoreflect.Value, failFast bool) (err error) { val.Map().Range(func(key protoreflect.MapKey, value protoreflect.Value) bool { evalErr := m.evalPairs(key, value, failFast) if evalErr != nil { - element := errors.FieldPathElement(m.Descriptor) - element.KeyType = descriptorpb.FieldDescriptorProto_Type(m.Descriptor.MapKey().Kind()).Enum() - element.ValueType = descriptorpb.FieldDescriptorProto_Type(m.Descriptor.MapValue().Kind()).Enum() - switch m.Descriptor.MapKey().Kind() { + element := &validate.FieldPathElement{ + FieldNumber: proto.Int32(m.base.FieldPathElement.GetFieldNumber()), + FieldType: m.base.FieldPathElement.GetFieldType().Enum(), + FieldName: proto.String(m.base.FieldPathElement.GetFieldName()), + } + element.KeyType = descriptorpb.FieldDescriptorProto_Type(m.base.Descriptor.MapKey().Kind()).Enum() + element.ValueType = descriptorpb.FieldDescriptorProto_Type(m.base.Descriptor.MapValue().Kind()).Enum() + switch m.base.Descriptor.MapKey().Kind() { case protoreflect.BoolKind: element.Subscript = &validate.FieldPathElement_BoolKey{BoolKey: key.Bool()} case protoreflect.Int32Kind, protoreflect.Int64Kind, @@ -73,11 +82,12 @@ func (m kvPairs) Evaluate(val protoreflect.Value, failFast bool) (err error) { protoreflect.BytesKind, protoreflect.MessageKind, protoreflect.GroupKind: err = errors.NewCompilationErrorf( "unexpected map key type %s", - m.Descriptor.MapKey().Kind(), + m.base.Descriptor.MapKey().Kind(), ) return false } - errors.AppendFieldPath(evalErr, element, false) + errors.AppendFieldPath(evalErr, element) + errors.PrependRulePath(evalErr, m.base.RulePrefix.GetElements()) } ok, err = errors.Merge(err, evalErr, failFast) return ok @@ -112,36 +122,6 @@ func (m kvPairs) formatKey(key any) string { } } -// keysWrapper wraps the evaluation of nested map key rules. -type keysWrapper struct { - evaluator -} - -func newKeysWrapper(evaluator evaluator) evaluator { return keysWrapper{evaluator} } - -func (e keysWrapper) Evaluate(val protoreflect.Value, failFast bool) error { - err := e.evaluator.Evaluate(val, failFast) - errors.PrependRulePath(err, mapKeysRulePath) - return err -} - -// valuesWrapper wraps the evaluation of nested map value rules. -type valuesWrapper struct { - evaluator -} - -func newValuesWrapper(evaluator evaluator) evaluator { return valuesWrapper{evaluator} } - -func (e valuesWrapper) Evaluate(val protoreflect.Value, failFast bool) error { - err := e.evaluator.Evaluate(val, failFast) - errors.PrependRulePath(err, mapValuesRulePath) - return err -} - var ( _ evaluator = kvPairs{} - _ evaluator = keysWrapper{} - _ wrapper = newKeysWrapper - _ evaluator = valuesWrapper{} - _ wrapper = newValuesWrapper ) diff --git a/internal/evaluator/message.go b/internal/evaluator/message.go index 94ca37d..11f8511 100644 --- a/internal/evaluator/message.go +++ b/internal/evaluator/message.go @@ -78,4 +78,29 @@ func (u unknownMessage) EvaluateMessage(_ protoreflect.Message, _ bool) error { return u.Err() } -var _ MessageEvaluator = (*message)(nil) +// embeddedMessage is a wrapper for fields containing messages. It contains data that +// may differ per embeddedMessage message so that it is not cached. +type embeddedMessage struct { + base + + message *message +} + +func (m *embeddedMessage) Evaluate(val protoreflect.Value, failFast bool) error { + err := m.message.EvaluateMessage(val.Message(), failFast) + if err != nil && m.base.FieldPathElement != nil { + errors.AppendFieldPath(err, m.base.FieldPathElement) + errors.PrependRulePath(err, m.base.RulePrefix.GetElements()) + } + return err +} + +func (m *embeddedMessage) Tautology() bool { + return m.message.Tautology() +} + +var ( + _ MessageEvaluator = (*message)(nil) + _ MessageEvaluator = (*unknownMessage)(nil) + _ evaluator = (*embeddedMessage)(nil) +) diff --git a/internal/evaluator/repeated.go b/internal/evaluator/repeated.go index 7d942cd..0915ede 100644 --- a/internal/evaluator/repeated.go +++ b/internal/evaluator/repeated.go @@ -17,6 +17,7 @@ package evaluator import ( "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" "github.com/bufbuild/protovalidate-go/internal/errors" + "google.golang.org/protobuf/proto" "google.golang.org/protobuf/reflect/protoreflect" ) @@ -24,16 +25,18 @@ import ( var ( repeatedRuleDescriptor = (&validate.FieldConstraints{}).ProtoReflect().Descriptor().Fields().ByName("repeated") repeatedItemsRuleDescriptor = (&validate.RepeatedRules{}).ProtoReflect().Descriptor().Fields().ByName("items") - repeatedItemsFieldPath = []*validate.FieldPathElement{ - errors.FieldPathElement(repeatedRuleDescriptor), - errors.FieldPathElement(repeatedItemsRuleDescriptor), + repeatedItemsRulePath = &validate.FieldPath{ + Elements: []*validate.FieldPathElement{ + errors.FieldPathElement(repeatedRuleDescriptor), + errors.FieldPathElement(repeatedItemsRuleDescriptor), + }, } ) // listItems performs validation on the elements of a repeated field. type listItems struct { - // Descriptor is the FieldDescriptor targeted by this evaluator - Descriptor protoreflect.FieldDescriptor + base + // ItemConstraints are checked on every item of the list ItemConstraints value } @@ -45,9 +48,13 @@ func (r listItems) Evaluate(val protoreflect.Value, failFast bool) error { for i := 0; i < list.Len(); i++ { itemErr := r.ItemConstraints.Evaluate(list.Get(i), failFast) if itemErr != nil { - element := errors.FieldPathElement(r.Descriptor) - element.Subscript = &validate.FieldPathElement_Index{Index: uint64(i)} - errors.AppendFieldPath(itemErr, element, false) + errors.AppendFieldPath(itemErr, &validate.FieldPathElement{ + FieldNumber: proto.Int32(r.base.FieldPathElement.GetFieldNumber()), + FieldType: r.base.FieldPathElement.GetFieldType().Enum(), + FieldName: proto.String(r.base.FieldPathElement.GetFieldName()), + Subscript: &validate.FieldPathElement_Index{Index: uint64(i)}, + }) + errors.PrependRulePath(itemErr, r.base.RulePrefix.GetElements()) } if ok, err = errors.Merge(err, itemErr, failFast); !ok { return err @@ -60,23 +67,6 @@ func (r listItems) Tautology() bool { return r.ItemConstraints.Tautology() } -// itemsWrapper wraps the evaluation of nested repeated field rules. -type itemsWrapper struct { - evaluator -} - -func newItemsWrapper(evaluator evaluator) evaluator { - return itemsWrapper{evaluator} -} - -func (e itemsWrapper) Evaluate(val protoreflect.Value, failFast bool) error { - err := e.evaluator.Evaluate(val, failFast) - errors.PrependRulePath(err, repeatedItemsFieldPath) - return err -} - var ( _ evaluator = listItems{} - _ evaluator = itemsWrapper{} - _ wrapper = newItemsWrapper ) diff --git a/internal/evaluator/value.go b/internal/evaluator/value.go index 7d500a4..e409f67 100644 --- a/internal/evaluator/value.go +++ b/internal/evaluator/value.go @@ -18,17 +18,36 @@ import ( "google.golang.org/protobuf/reflect/protoreflect" ) +// nestedType specifies a kind of nested value, if the value is being evaluated +// as a map key, map value, or repeated item. +type nestedType uint8 + +const ( + // nestedNone specifies that the value is not being evaluated as a nested value. + nestedNone nestedType = iota + // nestedRepeatedItem specifies that the value is being evaluated as a repeated field item. + nestedRepeatedItem + // nestedMapKey specifies that the value is being evaluated as a map key. + nestedMapKey + // nestedMapValue specifies that the value is being evaluated as a map value. + nestedMapValue +) + // value performs validation on any concrete value contained within a singular // field, repeated elements, or the keys/values of a map. type value struct { + // Descriptor is the FieldDescriptor targeted by this evaluator + Descriptor protoreflect.FieldDescriptor // Constraints are the individual evaluators applied to a value Constraints evaluators + // Zero is the default or zero-value for this value's type + Zero protoreflect.Value // IgnoreEmpty indicates that the Constraints should not be applied if the // value is unset or the default (typically zero) value. This only applies to // repeated elements or map keys/values with an ignore_empty rule. IgnoreEmpty bool - // Zero is the default or zero-value for this value's type - Zero protoreflect.Value + // Nested specifies the kind of nested field the value is for. + Nested nestedType } func (v *value) Evaluate(val protoreflect.Value, failFast bool) error { diff --git a/internal/expression/program.go b/internal/expression/program.go index 7843c33..2da631c 100644 --- a/internal/expression/program.go +++ b/internal/expression/program.go @@ -48,7 +48,6 @@ func (s ProgramSet) Eval(val protoreflect.Value, failFast bool) error { return err } if violation != nil { - violation.FieldValue = val violations = append(violations, violation) if failFast { break From b4b6e9b5be44054c1860578e1c035bff9c61c005 Mon Sep 17 00:00:00 2001 From: John Chadwick Date: Tue, 3 Dec 2024 13:37:30 -0500 Subject: [PATCH 15/24] Clean up unintentional diff --- internal/errors/utils.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/errors/utils.go b/internal/errors/utils.go index a86c8ac..c110f5f 100644 --- a/internal/errors/utils.go +++ b/internal/errors/utils.go @@ -26,9 +26,9 @@ import ( "google.golang.org/protobuf/types/descriptorpb" ) -// Merge is a utility to resolve and combine Errors resulting from +// Merge is a utility to resolve and combine errors resulting from // evaluation. If ok is false, execution of validation should stop (either due -// to failFast or the result is not a ValidationErrors). +// to failFast or the result is not a ValidationError). // //nolint:errorlint func Merge(dst, src error, failFast bool) (ok bool, err error) { From 9405cfdaee852f011d2dd603bd48028a5af6b38e Mon Sep 17 00:00:00 2001 From: John Chadwick Date: Tue, 3 Dec 2024 13:38:41 -0500 Subject: [PATCH 16/24] Coalesce helper functions --- internal/errors/utils.go | 62 +++++++++++----------------------- internal/evaluator/map.go | 3 +- internal/evaluator/message.go | 5 +-- internal/evaluator/repeated.go | 5 ++- validator.go | 3 +- 5 files changed, 25 insertions(+), 53 deletions(-) diff --git a/internal/errors/utils.go b/internal/errors/utils.go index c110f5f..b88b016 100644 --- a/internal/errors/utils.go +++ b/internal/errors/utils.go @@ -77,67 +77,45 @@ func FieldPath(field protoreflect.FieldDescriptor) *validate.FieldPath { } } -// AppendFieldPath appends an element to the end of each field path in err. +// UpdatePaths modifies the field and rule paths of an error, appending an +// element to the end of each field path (if provided) and prepending a list of +// elements to the beginning of each rule path (if provided.) // // Note that this function is ordinarily used to append field paths in reverse // order, as the stack bubbles up through the evaluators. Then, at the end, the -// path is reversed. -func AppendFieldPath(err error, suffix *validate.FieldPathElement) { - if suffix == nil { +// path is reversed. Rule paths are generally static, so this optimization isn't +// applied for rule paths. +func UpdatePaths(err error, fieldSuffix *validate.FieldPathElement, rulePrefix []*validate.FieldPathElement) { + if fieldSuffix == nil && len(rulePrefix) == 0 { return } var valErr *ValidationError if errors.As(err, &valErr) { for _, violation := range valErr.Violations { - if violation.Proto.GetField() == nil { - violation.Proto.Field = &validate.FieldPath{} + if fieldSuffix != nil { + if violation.Proto.GetField() == nil { + violation.Proto.Field = &validate.FieldPath{} + } + violation.Proto.Field.Elements = append(violation.Proto.Field.Elements, fieldSuffix) } - violation.Proto.Field.Elements = append(violation.Proto.Field.Elements, suffix) - } - } -} - -// PrependRulePath prepends some elements to the beginning of each rule path in -// err. Note that unlike field paths, the rule path is not appended in reverse -// order. This is because rule paths are very often fixed, simple paths, so it -// is better to avoid the copy instead if possible. This prepend is only used in -// the error case for nested rules (repeated.items, map.keys, map.values.) -func PrependRulePath(err error, prefix []*validate.FieldPathElement) { - if len(prefix) == 0 { - return - } - var valErr *ValidationError - if errors.As(err, &valErr) { - for _, violation := range valErr.Violations { - if violation.Proto.GetRule() == nil { - violation.Proto.Rule = &validate.FieldPath{} + if len(rulePrefix) != 0 { + violation.Proto.Rule.Elements = append( + append([]*validate.FieldPathElement{}, rulePrefix...), + violation.Proto.GetRule().GetElements()..., + ) } - violation.Proto.Rule.Elements = append( - append([]*validate.FieldPathElement{}, prefix...), - violation.Proto.GetRule().GetElements()..., - ) } } } -// ReverseFieldPaths reverses all field paths in the error. -func ReverseFieldPaths(err error) { +// FinalizePaths reverses all field paths in the error and populates the +// deprecated string-based field path. +func FinalizePaths(err error) { var valErr *ValidationError if errors.As(err, &valErr) { for _, violation := range valErr.Violations { if violation.Proto.GetField() != nil { slices.Reverse(violation.Proto.GetField().GetElements()) - } - } - } -} - -// PopulateFieldPathStrings populates the field path strings in the error. -func PopulateFieldPathStrings(err error) { - var valErr *ValidationError - if errors.As(err, &valErr) { - for _, violation := range valErr.Violations { - if violation.Proto.GetField() != nil { //nolint:staticcheck // Intentional use of deprecated field violation.Proto.FieldPath = proto.String(FieldPathString(violation.Proto.GetField().GetElements())) } diff --git a/internal/evaluator/map.go b/internal/evaluator/map.go index 8b7aa7e..90ae1fb 100644 --- a/internal/evaluator/map.go +++ b/internal/evaluator/map.go @@ -86,8 +86,7 @@ func (m kvPairs) Evaluate(val protoreflect.Value, failFast bool) (err error) { ) return false } - errors.AppendFieldPath(evalErr, element) - errors.PrependRulePath(evalErr, m.base.RulePrefix.GetElements()) + errors.UpdatePaths(evalErr, element, m.base.RulePrefix.GetElements()) } ok, err = errors.Merge(err, evalErr, failFast) return ok diff --git a/internal/evaluator/message.go b/internal/evaluator/message.go index 11f8511..dfb8330 100644 --- a/internal/evaluator/message.go +++ b/internal/evaluator/message.go @@ -88,10 +88,7 @@ type embeddedMessage struct { func (m *embeddedMessage) Evaluate(val protoreflect.Value, failFast bool) error { err := m.message.EvaluateMessage(val.Message(), failFast) - if err != nil && m.base.FieldPathElement != nil { - errors.AppendFieldPath(err, m.base.FieldPathElement) - errors.PrependRulePath(err, m.base.RulePrefix.GetElements()) - } + errors.UpdatePaths(err, m.base.FieldPathElement, nil) return err } diff --git a/internal/evaluator/repeated.go b/internal/evaluator/repeated.go index 0915ede..3e5314f 100644 --- a/internal/evaluator/repeated.go +++ b/internal/evaluator/repeated.go @@ -48,13 +48,12 @@ func (r listItems) Evaluate(val protoreflect.Value, failFast bool) error { for i := 0; i < list.Len(); i++ { itemErr := r.ItemConstraints.Evaluate(list.Get(i), failFast) if itemErr != nil { - errors.AppendFieldPath(itemErr, &validate.FieldPathElement{ + errors.UpdatePaths(itemErr, &validate.FieldPathElement{ FieldNumber: proto.Int32(r.base.FieldPathElement.GetFieldNumber()), FieldType: r.base.FieldPathElement.GetFieldType().Enum(), FieldName: proto.String(r.base.FieldPathElement.GetFieldName()), Subscript: &validate.FieldPathElement_Index{Index: uint64(i)}, - }) - errors.PrependRulePath(itemErr, r.base.RulePrefix.GetElements()) + }, r.base.RulePrefix.GetElements()) } if ok, err = errors.Merge(err, itemErr, failFast); !ok { return err diff --git a/validator.go b/validator.go index d8e1aa5..a791285 100644 --- a/validator.go +++ b/validator.go @@ -108,8 +108,7 @@ func (v *Validator) Validate(msg proto.Message) error { refl := msg.ProtoReflect() eval := v.builder.Load(refl.Descriptor()) err := eval.EvaluateMessage(refl, v.failFast) - errors.ReverseFieldPaths(err) - errors.PopulateFieldPathStrings(err) + errors.FinalizePaths(err) return err } From 012b3dc0be732a45f43ff22c66dd22c7456f7385 Mon Sep 17 00:00:00 2001 From: John Chadwick Date: Tue, 3 Dec 2024 13:41:01 -0500 Subject: [PATCH 17/24] Clean up unnecessary re-ordering in diff --- internal/errors/validation.go | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/internal/errors/validation.go b/internal/errors/validation.go index 22390fb..b498d32 100644 --- a/internal/errors/validation.go +++ b/internal/errors/validation.go @@ -55,17 +55,6 @@ type ValidationError struct { Violations []*Violation } -// ToProto converts this error into its proto.Message form. -func (err *ValidationError) ToProto() *validate.Violations { - violations := &validate.Violations{ - Violations: make([]*validate.Violation, len(err.Violations)), - } - for i, violation := range err.Violations { - violations.Violations[i] = violation.Proto - } - return violations -} - func (err *ValidationError) Error() string { bldr := &strings.Builder{} bldr.WriteString("validation error:") @@ -81,3 +70,14 @@ func (err *ValidationError) Error() string { } return bldr.String() } + +// ToProto converts this error into its proto.Message form. +func (err *ValidationError) ToProto() *validate.Violations { + violations := &validate.Violations{ + Violations: make([]*validate.Violation, len(err.Violations)), + } + for i, violation := range err.Violations { + violations.Violations[i] = violation.Proto + } + return violations +} From c7eb8524ce2d3a4b415c67d4c4d0edff3a43710b Mon Sep 17 00:00:00 2001 From: John Chadwick Date: Tue, 3 Dec 2024 14:01:57 -0500 Subject: [PATCH 18/24] Diff trimming, doc fix --- internal/evaluator/repeated.go | 4 +--- internal/expression/program.go | 8 ++++---- validator.go | 3 +-- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/internal/evaluator/repeated.go b/internal/evaluator/repeated.go index 3e5314f..75ff08a 100644 --- a/internal/evaluator/repeated.go +++ b/internal/evaluator/repeated.go @@ -66,6 +66,4 @@ func (r listItems) Tautology() bool { return r.ItemConstraints.Tautology() } -var ( - _ evaluator = listItems{} -) +var _ evaluator = listItems{} diff --git a/internal/expression/program.go b/internal/expression/program.go index 2da631c..55368d8 100644 --- a/internal/expression/program.go +++ b/internal/expression/program.go @@ -34,7 +34,7 @@ var nowPool = &NowPool{New: func() any { return &Now{} }} type ProgramSet []compiledProgram // Eval applies the contained expressions to the provided `this` val, returning -// either errors.ValidationErrors if the input is invalid or errors.RuntimeError +// either *errors.ValidationError if the input is invalid or errors.RuntimeError // if there is a type or range error. If failFast is true, execution stops at // the first failed expression. func (s ProgramSet) Eval(val protoreflect.Value, failFast bool) error { @@ -113,7 +113,7 @@ func (expr compiledProgram) eval(bindings *Variable) (*errors.Violation, error) } return &errors.Violation{ Proto: &validate.Violation{ - Rule: expr.rule(), + Rule: expr.rulePath(), ConstraintId: proto.String(expr.Source.GetId()), Message: proto.String(val), }, @@ -126,7 +126,7 @@ func (expr compiledProgram) eval(bindings *Variable) (*errors.Violation, error) } return &errors.Violation{ Proto: &validate.Violation{ - Rule: expr.rule(), + Rule: expr.rulePath(), ConstraintId: proto.String(expr.Source.GetId()), Message: proto.String(expr.Source.GetMessage()), }, @@ -139,7 +139,7 @@ func (expr compiledProgram) eval(bindings *Variable) (*errors.Violation, error) } } -func (expr compiledProgram) rule() *validate.FieldPath { +func (expr compiledProgram) rulePath() *validate.FieldPath { if len(expr.Path) > 0 { return &validate.FieldPath{Elements: expr.Path} } diff --git a/validator.go b/validator.go index a791285..69e4ee2 100644 --- a/validator.go +++ b/validator.go @@ -32,8 +32,7 @@ var getGlobalValidator = sync.OnceValues(func() (*Validator, error) { return New type ( // ValidationError is returned if one or more constraints on a message are - // violated. This error type is a composite of multiple ValidationError - // instances. + // violated. This error type is a composite of multiple Violation instances. // // err = validator.Validate(msg) // var valErr *ValidationError From ba578ec5af6adee20315088e50a9e2be4a5ff9d1 Mon Sep 17 00:00:00 2001 From: John Chadwick Date: Tue, 3 Dec 2024 14:03:39 -0500 Subject: [PATCH 19/24] Delete now-unused test file --- internal/errors/validation_test.go | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 internal/errors/validation_test.go diff --git a/internal/errors/validation_test.go b/internal/errors/validation_test.go deleted file mode 100644 index eb5ebd3..0000000 --- a/internal/errors/validation_test.go +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright 2023-2024 Buf Technologies, Inc. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -package errors From bbd370d3f99b6280f4072088fd243286f4f1bf99 Mon Sep 17 00:00:00 2001 From: John Chadwick Date: Tue, 3 Dec 2024 14:10:32 -0500 Subject: [PATCH 20/24] Even simpler handling of nesting --- internal/evaluator/base.go | 17 +---------------- internal/evaluator/builder.go | 35 +++++++++++----------------------- internal/evaluator/field.go | 2 +- internal/evaluator/map.go | 8 ++++++++ internal/evaluator/repeated.go | 7 +++++++ internal/evaluator/value.go | 20 +++---------------- 6 files changed, 31 insertions(+), 58 deletions(-) diff --git a/internal/evaluator/base.go b/internal/evaluator/base.go index 57be3b6..da65b95 100644 --- a/internal/evaluator/base.go +++ b/internal/evaluator/base.go @@ -40,7 +40,7 @@ func newBase(valEval *value) base { return base{ Descriptor: valEval.Descriptor, FieldPathElement: errors.FieldPathElement(valEval.Descriptor), - RulePrefix: rulePrefixForNesting(valEval.Nested), + RulePrefix: valEval.NestedRule, } } @@ -59,21 +59,6 @@ func (b *base) rulePath(suffix *validate.FieldPath) *validate.FieldPath { return prefixRulePath(b.RulePrefix, suffix) } -func rulePrefixForNesting(typ nestedType) *validate.FieldPath { - switch typ { - case nestedNone: - return nil - case nestedRepeatedItem: - return repeatedItemsRulePath - case nestedMapKey: - return mapKeysRulePath - case nestedMapValue: - return mapValuesRulePath - default: - return nil - } -} - func prefixRulePath(prefix *validate.FieldPath, suffix *validate.FieldPath) *validate.FieldPath { if len(prefix.GetElements()) > 0 { return &validate.FieldPath{ diff --git a/internal/evaluator/builder.go b/internal/evaluator/builder.go index a7e2b12..38b4ca3 100644 --- a/internal/evaluator/builder.go +++ b/internal/evaluator/builder.go @@ -278,9 +278,9 @@ func (bldr *Builder) processIgnoreEmpty( ) error { // the only time we need to ignore empty on a value is if it's evaluating a // field item (repeated element or map key/value). - val.IgnoreEmpty = val.Nested != nestedNone && bldr.shouldIgnoreEmpty(constraints) + val.IgnoreEmpty = val.NestedRule != nil && bldr.shouldIgnoreEmpty(constraints) if val.IgnoreEmpty { - val.Zero = bldr.zeroValue(fdesc, val.Nested != nestedNone) + val.Zero = bldr.zeroValue(fdesc, val.NestedRule != nil) } return nil } @@ -295,7 +295,7 @@ func (bldr *Builder) processFieldExpressions( Constraints: fieldConstraints.GetCel(), } - celTyp := celext.ProtoFieldToCELType(fieldDesc, false, eval.Nested != nestedNone) + celTyp := celext.ProtoFieldToCELType(fieldDesc, false, eval.NestedRule != nil) opts := append( celext.RequiredCELEnvOptions(fieldDesc), cel.Variable("this", celTyp), @@ -337,7 +337,7 @@ func (bldr *Builder) processEmbeddedMessage( if !isMessageField(fdesc) || bldr.shouldSkip(rules) || fdesc.IsMap() || - (fdesc.IsList() && valEval.Nested == nestedNone) { + (fdesc.IsList() && valEval.NestedRule == nil) { return nil } @@ -364,7 +364,7 @@ func (bldr *Builder) processWrapperConstraints( if !isMessageField(fdesc) || bldr.shouldSkip(rules) || fdesc.IsMap() || - (fdesc.IsList() && valEval.Nested == nestedNone) { + (fdesc.IsList() && valEval.NestedRule == nil) { return nil } @@ -374,7 +374,7 @@ func (bldr *Builder) processWrapperConstraints( } unwrapped := value{ Descriptor: valEval.Descriptor, - Nested: valEval.Nested, + NestedRule: valEval.NestedRule, } err := bldr.buildValue(fdesc.Message().Fields().ByName("value"), rules, &unwrapped, cache) if err != nil { @@ -396,7 +396,7 @@ func (bldr *Builder) processStandardConstraints( constraints, bldr.extensionTypeResolver, bldr.allowUnknownFields, - valEval.Nested != nestedNone, + valEval.NestedRule != nil, ) if err != nil { return err @@ -414,7 +414,7 @@ func (bldr *Builder) processAnyConstraints( valEval *value, _ MessageCache, ) error { - if (fdesc.IsList() && valEval.Nested == nestedNone) || + if (fdesc.IsList() && valEval.NestedRule == nil) || !isMessageField(fdesc) || fdesc.Message().FullName() != "google.protobuf.Any" { return nil @@ -464,15 +464,7 @@ func (bldr *Builder) processMapConstraints( return nil } - mapEval := kvPairs{ - base: newBase(valEval), - KeyConstraints: value{ - Nested: nestedMapKey, - }, - ValueConstraints: value{ - Nested: nestedMapValue, - }, - } + mapEval := newKVPairs(valEval) err := bldr.buildValue( fieldDesc.MapKey(), @@ -506,16 +498,11 @@ func (bldr *Builder) processRepeatedConstraints( valEval *value, cache MessageCache, ) error { - if !fdesc.IsList() || valEval.Nested != nestedNone { + if !fdesc.IsList() || valEval.NestedRule != nil { return nil } - listEval := listItems{ - base: newBase(valEval), - ItemConstraints: value{ - Nested: nestedRepeatedItem, - }, - } + listEval := newListItems(valEval) err := bldr.buildValue(fdesc, fieldConstraints.GetRepeated().GetItems(), &listEval.ItemConstraints, cache) if err != nil { diff --git a/internal/evaluator/field.go b/internal/evaluator/field.go index 18a8830..0b8388c 100644 --- a/internal/evaluator/field.go +++ b/internal/evaluator/field.go @@ -58,7 +58,7 @@ func (f field) EvaluateMessage(msg protoreflect.Message, failFast bool) (err err return &errors.ValidationError{Violations: []*errors.Violation{{ Proto: &validate.Violation{ Field: errors.FieldPath(f.Value.Descriptor), - Rule: prefixRulePath(rulePrefixForNesting(f.Value.Nested), requiredRulePath), + Rule: prefixRulePath(f.Value.NestedRule, requiredRulePath), ConstraintId: proto.String("required"), Message: proto.String("value is required"), }, diff --git a/internal/evaluator/map.go b/internal/evaluator/map.go index 90ae1fb..0fca1eb 100644 --- a/internal/evaluator/map.go +++ b/internal/evaluator/map.go @@ -54,6 +54,14 @@ type kvPairs struct { ValueConstraints value } +func newKVPairs(valEval *value) kvPairs { + return kvPairs{ + base: newBase(valEval), + KeyConstraints: value{NestedRule: mapKeysRulePath}, + ValueConstraints: value{NestedRule: mapValuesRulePath}, + } +} + func (m kvPairs) Evaluate(val protoreflect.Value, failFast bool) (err error) { var ok bool val.Map().Range(func(key protoreflect.MapKey, value protoreflect.Value) bool { diff --git a/internal/evaluator/repeated.go b/internal/evaluator/repeated.go index 75ff08a..032448e 100644 --- a/internal/evaluator/repeated.go +++ b/internal/evaluator/repeated.go @@ -41,6 +41,13 @@ type listItems struct { ItemConstraints value } +func newListItems(valEval *value) listItems { + return listItems{ + base: newBase(valEval), + ItemConstraints: value{NestedRule: repeatedItemsRulePath}, + } +} + func (r listItems) Evaluate(val protoreflect.Value, failFast bool) error { list := val.List() var ok bool diff --git a/internal/evaluator/value.go b/internal/evaluator/value.go index e409f67..753ba83 100644 --- a/internal/evaluator/value.go +++ b/internal/evaluator/value.go @@ -15,24 +15,10 @@ package evaluator import ( + "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" "google.golang.org/protobuf/reflect/protoreflect" ) -// nestedType specifies a kind of nested value, if the value is being evaluated -// as a map key, map value, or repeated item. -type nestedType uint8 - -const ( - // nestedNone specifies that the value is not being evaluated as a nested value. - nestedNone nestedType = iota - // nestedRepeatedItem specifies that the value is being evaluated as a repeated field item. - nestedRepeatedItem - // nestedMapKey specifies that the value is being evaluated as a map key. - nestedMapKey - // nestedMapValue specifies that the value is being evaluated as a map value. - nestedMapValue -) - // value performs validation on any concrete value contained within a singular // field, repeated elements, or the keys/values of a map. type value struct { @@ -42,12 +28,12 @@ type value struct { Constraints evaluators // Zero is the default or zero-value for this value's type Zero protoreflect.Value + // NestedRule specifies the nested rule type the value is for. + NestedRule *validate.FieldPath // IgnoreEmpty indicates that the Constraints should not be applied if the // value is unset or the default (typically zero) value. This only applies to // repeated elements or map keys/values with an ignore_empty rule. IgnoreEmpty bool - // Nested specifies the kind of nested field the value is for. - Nested nestedType } func (v *value) Evaluate(val protoreflect.Value, failFast bool) error { From 1dfc9df0955b7be0cc20a080760183a31faf6b10 Mon Sep 17 00:00:00 2001 From: John Chadwick Date: Tue, 3 Dec 2024 14:11:32 -0500 Subject: [PATCH 21/24] More diff pruning --- internal/evaluator/map.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/internal/evaluator/map.go b/internal/evaluator/map.go index 0fca1eb..ff9e63e 100644 --- a/internal/evaluator/map.go +++ b/internal/evaluator/map.go @@ -129,6 +129,4 @@ func (m kvPairs) formatKey(key any) string { } } -var ( - _ evaluator = kvPairs{} -) +var _ evaluator = kvPairs{} From 075795635e8c123640d98590c7fb47ccea8461e5 Mon Sep 17 00:00:00 2001 From: John Chadwick Date: Wed, 11 Dec 2024 22:40:15 -0500 Subject: [PATCH 22/24] Address review feedback --- internal/evaluator/map.go | 2 ++ internal/expression/ast.go | 2 -- internal/expression/compile.go | 5 +++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/internal/evaluator/map.go b/internal/evaluator/map.go index ff9e63e..6a088d6 100644 --- a/internal/evaluator/map.go +++ b/internal/evaluator/map.go @@ -88,6 +88,8 @@ func (m kvPairs) Evaluate(val protoreflect.Value, failFast bool) (err error) { element.Subscript = &validate.FieldPathElement_StringKey{StringKey: key.String()} case protoreflect.EnumKind, protoreflect.FloatKind, protoreflect.DoubleKind, protoreflect.BytesKind, protoreflect.MessageKind, protoreflect.GroupKind: + fallthrough + default: err = errors.NewCompilationErrorf( "unexpected map key type %s", m.base.Descriptor.MapKey().Kind(), diff --git a/internal/expression/ast.go b/internal/expression/ast.go index a6c2458..7d0868a 100644 --- a/internal/expression/ast.go +++ b/internal/expression/ast.go @@ -81,8 +81,6 @@ func (set ASTSet) ReduceResiduals(opts ...cel.ProgramOption) (ProgramSet, error) if err != nil { residuals = append(residuals, ast) } else { - x := residual.Source().Content() - _ = x residuals = append(residuals, compiledAST{ AST: residual, Source: ast.Source, diff --git a/internal/expression/compile.go b/internal/expression/compile.go index a418a64..9880bab 100644 --- a/internal/expression/compile.go +++ b/internal/expression/compile.go @@ -20,8 +20,9 @@ import ( "github.com/google/cel-go/cel" ) -// Expression is a container for the information needed to compile and evaluate -// a list of CEL-based expression, originating from a validate.Constraint. +// An Expressions instance is a container for the information needed to compile +// and evaluate a list of CEL-based expressions, originating from a +// validate.Constraint. type Expressions struct { Constraints []*validate.Constraint RulePath []*validate.FieldPathElement From 611775b9e6aeaec53e359c46ac4bf9b9860e990d Mon Sep 17 00:00:00 2001 From: John Chadwick Date: Wed, 11 Dec 2024 23:40:46 -0500 Subject: [PATCH 23/24] Add an example of how one might localize errors --- validator_example_test.go | 41 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/validator_example_test.go b/validator_example_test.go index 1847de2..f5a8d62 100644 --- a/validator_example_test.go +++ b/validator_example_test.go @@ -18,6 +18,8 @@ import ( "errors" "fmt" "log" + "os" + "text/template" pb "github.com/bufbuild/protovalidate-go/internal/gen/tests/example/v1" "google.golang.org/protobuf/reflect/protoregistry" @@ -164,3 +166,42 @@ func ExampleValidationError() { // output: lat double.gte_lte // -90 999.999 } + +func ExampleValidationError_localized() { + validator, err := New() + if err != nil { + log.Fatal(err) + } + + type ErrorInfo struct { + FieldName string + RuleValue any + FieldValue any + } + + var ruleMessages = map[string]string{ + "string.email_empty": "{{.FieldName}}: メールアドレスは空であってはなりません。\n", + "string.pattern": "{{.FieldName}}: 値はパターン「{{.RuleValue}}」一致する必要があります。\n", + "uint64.gt": "{{.FieldName}}: 値は{{.RuleValue}}を超える必要があります。(価値:{{.FieldValue}})\n", + } + + loc := &pb.Person{Id: 900} + err = validator.Validate(loc) + var valErr *ValidationError + if ok := errors.As(err, &valErr); ok { + for _, violation := range valErr.Violations { + _ = template. + Must(template.New("").Parse(ruleMessages[violation.Proto.GetConstraintId()])). + Execute(os.Stdout, ErrorInfo{ + FieldName: FieldPathString(violation.Proto.GetField()), + RuleValue: violation.RuleValue.Interface(), + FieldValue: violation.FieldValue.Interface(), + }) + } + } + + // output: + // id: 値は999を超える必要があります。(価値:900) + // email: メールアドレスは空であってはなりません。 + // name: 値はパターン「^[[:alpha:]]+( [[:alpha:]]+)*$」一致する必要があります。 +} From 2c4a54751328b3c13b82a606935e10b31f0744dc Mon Sep 17 00:00:00 2001 From: John Chadwick Date: Thu, 12 Dec 2024 10:12:40 -0500 Subject: [PATCH 24/24] Address collapsed review feedback --- internal/errors/utils.go | 7 +++++++ internal/errors/validation.go | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/internal/errors/utils.go b/internal/errors/utils.go index b88b016..5308e69 100644 --- a/internal/errors/utils.go +++ b/internal/errors/utils.go @@ -55,6 +55,9 @@ func Merge(dst, src error, failFast bool) (ok bool, err error) { return !(failFast && len(dstValErrs.Violations) > 0), dst } +// FieldPathElement returns a buf.validate.FieldPathElement that corresponds to +// a provided FieldDescriptor. If the provided FieldDescriptor is nil, nil is +// returned. func FieldPathElement(field protoreflect.FieldDescriptor) *validate.FieldPathElement { if field == nil { return nil @@ -66,6 +69,8 @@ func FieldPathElement(field protoreflect.FieldDescriptor) *validate.FieldPathEle } } +// FieldPath returns a single-element buf.validate.FieldPath corresponding to +// the provided FieldDescriptor, or nil if the provided FieldDescriptor is nil. func FieldPath(field protoreflect.FieldDescriptor) *validate.FieldPath { if field == nil { return nil @@ -154,6 +159,8 @@ func FieldPathString(path []*validate.FieldPathElement) string { return result.String() } +// MarkForKey marks the provided error as being for a map key, by setting the +// `for_key` flag on each violation within the validation error. func MarkForKey(err error) { var valErr *ValidationError if errors.As(err, &valErr) { diff --git a/internal/errors/validation.go b/internal/errors/validation.go index b498d32..337db60 100644 --- a/internal/errors/validation.go +++ b/internal/errors/validation.go @@ -34,7 +34,7 @@ type Violation struct { FieldValue protoreflect.Value // FieldDescriptor contains the field descriptor corresponding to the - // FieldValue, if there is a field value. + // field that failed validation. FieldDescriptor protoreflect.FieldDescriptor // RuleValue contains the value of the rule that specified the failed @@ -45,7 +45,7 @@ type Violation struct { RuleValue protoreflect.Value // RuleDescriptor contains the field descriptor corresponding to the - // Rulevalue, if there is a rule value. + // rule that failed validation. RuleDescriptor protoreflect.FieldDescriptor }