From a2088ebbecbdc76f081644b55914e762dca29646 Mon Sep 17 00:00:00 2001 From: jchadwick-buf <116005195+jchadwick-buf@users.noreply.github.com> Date: Thu, 12 Dec 2024 10:51:20 -0500 Subject: [PATCH] Implement structured field and rule paths, add field and rule values to ValidationErrors (#154) This implements the structured field and rule paths, passing the conformance test changes in https://github.com/bufbuild/protovalidate/pull/265. In addition, it also adds the ability to get the field and rule values. These are not present in the protobuf form of the violation. The API is still heavily centered around the protobuf form of violations, but the `ValidationError` type is no longer an alias to the protobuf `buf.validate.Violations` message. The intention is that users needing access to the error information will still use the proto violations message, only using the "native" interface methods to access in-memory data that is not feasible to transport over the wire. (e.g. `protoreflect.Value` pointers.) DO NOT MERGE until the following is done: - [x] https://github.com/bufbuild/protovalidate/pull/265 is merged - [x] Dependencies updated to stable version release --- Makefile | 2 +- go.mod | 2 +- go.sum | 4 +- internal/constraints/cache.go | 32 ++-- internal/constraints/cache_test.go | 6 +- internal/errors/utils.go | 113 +++++++++++++- internal/errors/utils_test.go | 18 +-- internal/errors/validation.go | 61 +++++--- internal/errors/validation_test.go | 74 --------- internal/evaluator/any.go | 55 ++++++- internal/evaluator/base.go | 75 +++++++++ internal/evaluator/builder.go | 115 +++++++++----- internal/evaluator/cel.go | 30 +++- internal/evaluator/enum.go | 28 +++- internal/evaluator/evaluator.go | 5 - internal/evaluator/field.go | 38 +++-- internal/evaluator/map.go | 63 +++++++- internal/evaluator/message.go | 24 ++- internal/evaluator/oneof.go | 14 +- internal/evaluator/repeated.go | 30 +++- internal/evaluator/value.go | 9 +- internal/expression/ast.go | 37 ++++- internal/expression/ast_test.go | 24 ++- internal/expression/compile.go | 51 ++++--- internal/expression/compile_test.go | 30 ++-- internal/expression/program.go | 44 ++++-- internal/expression/program_test.go | 31 ++-- .../custom_constraints.pb.go | 144 ++++++++++-------- validator.go | 20 ++- validator_example_test.go | 47 +++++- validator_test.go | 2 +- 31 files changed, 877 insertions(+), 351 deletions(-) delete mode 100644 internal/errors/validation_test.go create mode 100644 internal/evaluator/base.go diff --git a/Makefile b/Makefile index 1ab5f63..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 diff --git a/go.mod b/go.mod index ca9ddc7..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 diff --git a/go.sum b/go.sum index 597b8ae..e3a9b0d 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.35.2-20240920164238-5a7b106cbb87.1 h1:7QIeAuTdLp173vC/9JojRMDFcpmqtoYrxPmvdHAOynw= -buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.35.2-20240920164238-5a7b106cbb87.1/go.mod h1:mnHCFccv4HwuIAOHNGdiIc5ZYbBCvbTWZcodLN5wITI= +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= 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/constraints/cache.go b/internal/constraints/cache.go index 1c1768a..d4777e1 100644 --- a/internal/constraints/cache.go +++ b/internal/constraints/cache.go @@ -49,7 +49,7 @@ func (c *Cache) Build( allowUnknownFields bool, forItems bool, ) (set expression.ProgramSet, err error) { - constraints, done, err := c.resolveConstraints( + constraints, setOneof, done, err := c.resolveConstraints( fieldDesc, fieldConstraints, forItems, @@ -83,11 +83,12 @@ 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 } + precomputedASTs.SetRuleValue(rule, desc) asts = asts.Merge(precomputedASTs) return true }) @@ -108,15 +109,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(), @@ -124,10 +125,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 @@ -157,16 +158,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 0c13412..42bb447 100644 --- a/internal/constraints/cache_test.go +++ b/internal/constraints/cache_test.go @@ -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/utils.go b/internal/errors/utils.go index 35aa5c8..5308e69 100644 --- a/internal/errors/utils.go +++ b/internal/errors/utils.go @@ -16,8 +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 @@ -49,20 +55,117 @@ 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 -// ValidationError. -func PrefixErrorPaths(err error, format string, args ...any) { +// 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 + } + return &validate.FieldPathElement{ + FieldNumber: proto.Int32(int32(field.Number())), + FieldName: proto.String(field.TextName()), + FieldType: descriptorpb.FieldDescriptorProto_Type(field.Kind()).Enum(), + } +} + +// 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 + } + return &validate.FieldPath{ + Elements: []*validate.FieldPathElement{ + FieldPathElement(field), + }, + } +} + +// 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. 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) { - PrefixFieldPaths(valErr, format, args...) + for _, violation := range valErr.Violations { + if fieldSuffix != nil { + if violation.Proto.GetField() == nil { + violation.Proto.Field = &validate.FieldPath{} + } + violation.Proto.Field.Elements = append(violation.Proto.Field.Elements, fieldSuffix) + } + if len(rulePrefix) != 0 { + violation.Proto.Rule.Elements = append( + append([]*validate.FieldPathElement{}, rulePrefix...), + violation.Proto.GetRule().GetElements()..., + ) + } + } + } +} + +// 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()) + //nolint:staticcheck // Intentional use of deprecated field + violation.Proto.FieldPath = proto.String(FieldPathString(violation.Proto.GetField().GetElements())) + } + } + } +} + +// FieldPathString takes a FieldPath and encodes it to a string-based dotted +// field path. +func FieldPathString(path []*validate.FieldPathElement) string { + var result strings.Builder + 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_UintKey: + result.WriteString(strconv.FormatUint(value.UintKey, 10)) + case *validate.FieldPathElement_StringKey: + result.WriteString(strconv.Quote(value.StringKey)) + } + result.WriteByte(']') } + 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) { for _, violation := range valErr.Violations { - violation.ForKey = proto.Bool(true) + violation.Proto.ForKey = proto.Bool(true) } } } diff --git a/internal/errors/utils_test.go b/internal/errors/utils_test.go index 122f3b9..3963b2a 100644 --- a/internal/errors/utils_test.go +++ b/internal/errors/utils_test.go @@ -53,7 +53,7 @@ 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{{Proto: &validate.Violation{ConstraintId: proto.String("foo")}}}} ok, err := Merge(nil, exErr, true) var valErr *ValidationError require.ErrorAs(t, err, &valErr) @@ -72,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: []*validate.Violation{{ConstraintId: proto.String("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) @@ -83,7 +83,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{{Proto: &validate.Violation{ConstraintId: proto.String("foo")}}}} srcErr := errors.New("some error") ok, err := Merge(dstErr, srcErr, true) assert.Equal(t, srcErr, err) @@ -96,18 +96,18 @@ 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{{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: []*validate.Violation{{ConstraintId: proto.String("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 bd4712c..337db60 100644 --- a/internal/errors/validation.go +++ b/internal/errors/validation.go @@ -19,46 +19,65 @@ 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 struct { + // Proto contains the violation's proto.Message form. + Proto *validate.Violation + + // 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 + + // FieldDescriptor contains the field descriptor corresponding to the + // field that failed validation. + 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 contain an + // invalid value. + RuleValue protoreflect.Value + + // RuleDescriptor contains the field descriptor corresponding to the + // rule that failed validation. + RuleDescriptor protoreflect.FieldDescriptor +} + // 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 := 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() } // 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 { - 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()) - default: // any other field - violation.FieldPath = proto.String(fmt.Sprintf("%s.%s", prefix, violation.GetFieldPath())) - } + violations := &validate.Violations{ + Violations: make([]*validate.Violation, len(err.Violations)), + } + for i, violation := range err.Violations { + violations.Violations[i] = violation.Proto } + return violations } diff --git a/internal/errors/validation_test.go b/internal/errors/validation_test.go deleted file mode 100644 index e864188..0000000 --- a/internal/errors/validation_test.go +++ /dev/null @@ -1,74 +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" - - "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) { - 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: []*validate.Violation{ - {FieldPath: proto.String(test.fieldPath)}, - {FieldPath: proto.String(test.fieldPath)}, - }} - PrefixFieldPaths(err, test.format, test.args...) - for _, v := range err.Violations { - assert.Equal(t, test.expected, v.GetFieldPath()) - } - }) - } -} diff --git a/internal/evaluator/any.go b/internal/evaluator/any.go index 8b8c9c4..8a46c43 100644 --- a/internal/evaluator/any.go +++ b/internal/evaluator/any.go @@ -21,28 +21,61 @@ import ( "google.golang.org/protobuf/reflect/protoreflect" ) +//nolint:gochecknoglobals +var ( + anyRuleDescriptor = (&validate.FieldConstraints{}).ProtoReflect().Descriptor().Fields().ByName("any") + anyInRuleDescriptor = (&validate.AnyRules{}).ProtoReflect().Descriptor().Fields().ByName("in") + anyInRulePath = &validate.FieldPath{ + Elements: []*validate.FieldPathElement{ + errors.FieldPathElement(anyRuleDescriptor), + errors.FieldPathElement(anyInRuleDescriptor), + }, + } + anyNotInDescriptor = (&validate.AnyRules{}).ProtoReflect().Descriptor().Fields().ByName("not_in") + anyNotInRulePath = &validate.FieldPath{ + Elements: []*validate.FieldPathElement{ + errors.FieldPathElement(anyRuleDescriptor), + errors.FieldPathElement(anyNotInDescriptor), + }, + } +) + // anyPB is a specialized evaluator for applying validate.AnyRules to an // anypb.Any message. This is handled outside CEL which attempts to // hydrate anyPB's within an expression, breaking evaluation if the type is // unknown at runtime. type anyPB struct { + base base + // TypeURLDescriptor is the descriptor for the TypeURL field TypeURLDescriptor protoreflect.FieldDescriptor // In specifies which type URLs the value may possess 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 { 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{ + Proto: &validate.Violation{ + 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.base.Descriptor, + RuleValue: a.InValue, + RuleDescriptor: anyInRuleDescriptor, }) if failFast { return err @@ -52,9 +85,17 @@ 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{ + Proto: &validate.Violation{ + 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.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..da65b95 --- /dev/null +++ b/internal/evaluator/base.go @@ -0,0 +1,75 @@ +// 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: valEval.NestedRule, + } +} + +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 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 a795097..38b4ca3 100644 --- a/internal/evaluator/builder.go +++ b/internal/evaluator/builder.go @@ -24,11 +24,18 @@ 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/dynamicpb" ) +//nolint:gochecknoglobals +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. type Builder struct { @@ -154,8 +161,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()))), @@ -165,7 +175,9 @@ func (bldr *Builder) processMessageExpressions( return } - msgEval.Append(celPrograms(compiledExprs)) + msgEval.Append(celPrograms{ + ProgramSet: compiledExprs, + }) } func (bldr *Builder) processOneofConstraints( @@ -211,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() && @@ -221,21 +235,19 @@ 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, &fld.Value, cache) return fld, err } func (bldr *Builder) buildValue( fdesc protoreflect.FieldDescriptor, constraints *validate.FieldConstraints, - forItems bool, valEval *value, cache MessageCache, ) (err error) { steps := []func( fdesc protoreflect.FieldDescriptor, fieldConstraints *validate.FieldConstraints, - forItems bool, valEval *value, cache MessageCache, ) error{ @@ -251,7 +263,7 @@ func (bldr *Builder) buildValue( } for _, step := range steps { - if err = step(fdesc, constraints, forItems, valEval, cache); err != nil { + if err = step(fdesc, constraints, valEval, cache); err != nil { return err } } @@ -261,15 +273,14 @@ func (bldr *Builder) buildValue( func (bldr *Builder) processIgnoreEmpty( fdesc protoreflect.FieldDescriptor, constraints *validate.FieldConstraints, - 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 = val.NestedRule != nil && bldr.shouldIgnoreEmpty(constraints) if val.IgnoreEmpty { - val.Zero = bldr.zeroValue(fdesc, forItems) + val.Zero = bldr.zeroValue(fdesc, val.NestedRule != nil) } return nil } @@ -277,16 +288,14 @@ func (bldr *Builder) processIgnoreEmpty( func (bldr *Builder) processFieldExpressions( fieldDesc protoreflect.FieldDescriptor, fieldConstraints *validate.FieldConstraints, - forItems bool, 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, eval.NestedRule != nil) opts := append( celext.RequiredCELEnvOptions(fieldDesc), cel.Variable("this", celTyp), @@ -295,8 +304,26 @@ 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), + }, + }, + } + 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 } @@ -304,14 +331,13 @@ func (bldr *Builder) processFieldExpressions( func (bldr *Builder) processEmbeddedMessage( fdesc protoreflect.FieldDescriptor, rules *validate.FieldConstraints, - forItems bool, valEval *value, cache MessageCache, ) error { if !isMessageField(fdesc) || bldr.shouldSkip(rules) || fdesc.IsMap() || - (fdesc.IsList() && !forItems) { + (fdesc.IsList() && valEval.NestedRule == nil) { return nil } @@ -321,7 +347,10 @@ func (bldr *Builder) processEmbeddedMessage( "failed to compile embedded type %s for %s: %w", fdesc.Message().FullName(), fdesc.FullName(), err) } - valEval.Append(embedEval) + valEval.Append(&embeddedMessage{ + base: newBase(valEval), + message: embedEval, + }) return nil } @@ -329,14 +358,13 @@ func (bldr *Builder) processEmbeddedMessage( func (bldr *Builder) processWrapperConstraints( fdesc protoreflect.FieldDescriptor, rules *validate.FieldConstraints, - forItems bool, valEval *value, cache MessageCache, ) error { if !isMessageField(fdesc) || bldr.shouldSkip(rules) || fdesc.IsMap() || - (fdesc.IsList() && !forItems) { + (fdesc.IsList() && valEval.NestedRule == nil) { return nil } @@ -344,8 +372,11 @@ func (bldr *Builder) processWrapperConstraints( if !ok || !rules.ProtoReflect().Has(expectedWrapperDescriptor) { return nil } - var unwrapped value - err := bldr.buildValue(fdesc.Message().Fields().ByName("value"), rules, true, &unwrapped, cache) + unwrapped := value{ + Descriptor: valEval.Descriptor, + NestedRule: valEval.NestedRule, + } + err := bldr.buildValue(fdesc.Message().Fields().ByName("value"), rules, &unwrapped, cache) if err != nil { return err } @@ -356,7 +387,6 @@ func (bldr *Builder) processWrapperConstraints( func (bldr *Builder) processStandardConstraints( fdesc protoreflect.FieldDescriptor, constraints *validate.FieldConstraints, - forItems bool, valEval *value, _ MessageCache, ) error { @@ -366,33 +396,41 @@ func (bldr *Builder) processStandardConstraints( constraints, bldr.extensionTypeResolver, bldr.allowUnknownFields, - forItems, + valEval.NestedRule != nil, ) if err != nil { return err } - valEval.Append(celPrograms(stdConstraints)) + valEval.Append(celPrograms{ + base: newBase(valEval), + ProgramSet: stdConstraints, + }) return nil } func (bldr *Builder) processAnyConstraints( fdesc protoreflect.FieldDescriptor, fieldConstraints *validate.FieldConstraints, - forItems bool, valEval *value, _ MessageCache, ) error { - if (fdesc.IsList() && !forItems) || + if (fdesc.IsList() && valEval.NestedRule == nil) || !isMessageField(fdesc) || fdesc.Message().FullName() != "google.protobuf.Any" { return nil } 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{ + 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), } valEval.Append(anyEval) return nil @@ -401,7 +439,6 @@ func (bldr *Builder) processAnyConstraints( func (bldr *Builder) processEnumConstraints( fdesc protoreflect.FieldDescriptor, fieldConstraints *validate.FieldConstraints, - _ bool, valEval *value, _ MessageCache, ) error { @@ -409,7 +446,10 @@ func (bldr *Builder) processEnumConstraints( return nil } if fieldConstraints.GetEnum().GetDefinedOnly() { - valEval.Append(definedEnum{ValueDescriptors: fdesc.Enum().Values()}) + valEval.Append(definedEnum{ + base: newBase(valEval), + ValueDescriptors: fdesc.Enum().Values(), + }) } return nil } @@ -417,7 +457,6 @@ func (bldr *Builder) processEnumConstraints( func (bldr *Builder) processMapConstraints( fieldDesc protoreflect.FieldDescriptor, constraints *validate.FieldConstraints, - _ bool, valEval *value, cache MessageCache, ) error { @@ -425,12 +464,11 @@ func (bldr *Builder) processMapConstraints( return nil } - var mapEval kvPairs + mapEval := newKVPairs(valEval) err := bldr.buildValue( fieldDesc.MapKey(), constraints.GetMap().GetKeys(), - true, &mapEval.KeyConstraints, cache) if err != nil { @@ -442,7 +480,6 @@ func (bldr *Builder) processMapConstraints( err = bldr.buildValue( fieldDesc.MapValue(), constraints.GetMap().GetValues(), - true, &mapEval.ValueConstraints, cache) if err != nil { @@ -458,16 +495,16 @@ func (bldr *Builder) processMapConstraints( func (bldr *Builder) processRepeatedConstraints( fdesc protoreflect.FieldDescriptor, fieldConstraints *validate.FieldConstraints, - forItems bool, valEval *value, cache MessageCache, ) error { - if !fdesc.IsList() || forItems { + if !fdesc.IsList() || valEval.NestedRule != nil { return nil } - var listEval listItems - err := bldr.buildValue(fdesc, fieldConstraints.GetRepeated().GetItems(), true, &listEval.ItemConstraints, cache) + listEval := newListItems(valEval) + + 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) diff --git a/internal/evaluator/cel.go b/internal/evaluator/cel.go index 0461798..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.Interface(), 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(msg.Interface(), 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 e356cbe..4379232 100644 --- a/internal/evaluator/enum.go +++ b/internal/evaluator/enum.go @@ -21,19 +21,41 @@ import ( "google.golang.org/protobuf/reflect/protoreflect" ) +//nolint:gochecknoglobals +var ( + enumRuleDescriptor = (&validate.FieldConstraints{}).ProtoReflect().Descriptor().Fields().ByName("enum") + enumDefinedOnlyRuleDescriptor = (&validate.EnumRules{}).ProtoReflect().Descriptor().Fields().ByName("defined_only") + enumDefinedOnlyRulePath = &validate.FieldPath{ + Elements: []*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 { + base + // ValueDescriptors captures all the defined values for this enum ValueDescriptors protoreflect.EnumValueDescriptors } 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{{ + Proto: &validate.Violation{ + 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.base.Descriptor, + RuleValue: protoreflect.ValueOfBool(true), + RuleDescriptor: enumDefinedOnlyRuleDescriptor, }}} } return nil diff --git a/internal/evaluator/evaluator.go b/internal/evaluator/evaluator.go index 132e095..5cee85c 100644 --- a/internal/evaluator/evaluator.go +++ b/internal/evaluator/evaluator.go @@ -101,8 +101,3 @@ func (m messageEvaluators) Tautology() bool { } return true } - -var ( - _ evaluator = evaluators(nil) - _ MessageEvaluator = messageEvaluators(nil) -) diff --git a/internal/evaluator/field.go b/internal/evaluator/field.go index edfd053..0b8388c 100644 --- a/internal/evaluator/field.go +++ b/internal/evaluator/field.go @@ -21,13 +21,21 @@ import ( "google.golang.org/protobuf/reflect/protoreflect" ) +//nolint:gochecknoglobals +var ( + requiredRuleDescriptor = (&validate.FieldConstraints{}).ProtoReflect().Descriptor().Fields().ByName("required") + requiredRulePath = &validate.FieldPath{ + Elements: []*validate.FieldPathElement{ + errors.FieldPathElement(requiredRuleDescriptor), + }, + } +) + // field performs validation on a single message field, defined by its // descriptor. 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. @@ -46,26 +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) { - return &errors.ValidationError{Violations: []*validate.Violation{{ - FieldPath: proto.String(string(f.Descriptor.Name())), - ConstraintId: proto.String("required"), - Message: proto.String("value is required"), + if f.Required && !msg.Has(f.Value.Descriptor) { + return &errors.ValidationError{Violations: []*errors.Violation{{ + Proto: &validate.Violation{ + Field: errors.FieldPath(f.Value.Descriptor), + Rule: prefixRulePath(f.Value.NestedRule, requiredRulePath), + ConstraintId: proto.String("required"), + Message: proto.String("value is required"), + }, + FieldValue: protoreflect.Value{}, + 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.PrefixErrorPaths(err, "%s", f.Descriptor.Name()) - } - 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 5fc51cf..6a088d6 100644 --- a/internal/evaluator/map.go +++ b/internal/evaluator/map.go @@ -18,24 +18,85 @@ 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 ( + mapRuleDescriptor = (&validate.FieldConstraints{}).ProtoReflect().Descriptor().Fields().ByName("map") + mapKeysRuleDescriptor = (&validate.MapRules{}).ProtoReflect().Descriptor().Fields().ByName("keys") + mapKeysRulePath = &validate.FieldPath{ + Elements: []*validate.FieldPathElement{ + errors.FieldPathElement(mapRuleDescriptor), + errors.FieldPathElement(mapKeysRuleDescriptor), + }, + } + mapValuesDescriptor = (&validate.MapRules{}).ProtoReflect().Descriptor().Fields().ByName("values") + 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 { + base + // KeyConstraints are checked on the map keys KeyConstraints value // ValueConstraints are checked on the map values 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 { evalErr := m.evalPairs(key, value, failFast) if evalErr != nil { - errors.PrefixErrorPaths(evalErr, "[%s]", m.formatKey(key.Interface())) + 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, + protoreflect.Sfixed32Kind, protoreflect.Sfixed64Kind, + protoreflect.Sint32Kind, protoreflect.Sint64Kind: + element.Subscript = &validate.FieldPathElement_IntKey{IntKey: 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: + fallthrough + default: + err = errors.NewCompilationErrorf( + "unexpected map key type %s", + m.base.Descriptor.MapKey().Kind(), + ) + return false + } + 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 94ca37d..dfb8330 100644 --- a/internal/evaluator/message.go +++ b/internal/evaluator/message.go @@ -78,4 +78,26 @@ 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) + errors.UpdatePaths(err, m.base.FieldPathElement, nil) + 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/oneof.go b/internal/evaluator/oneof.go index 19e05da..355fbb2 100644 --- a/internal/evaluator/oneof.go +++ b/internal/evaluator/oneof.go @@ -35,10 +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: []*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{{ + 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/evaluator/repeated.go b/internal/evaluator/repeated.go index 14973bf..032448e 100644 --- a/internal/evaluator/repeated.go +++ b/internal/evaluator/repeated.go @@ -15,16 +15,39 @@ 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" ) +//nolint:gochecknoglobals +var ( + repeatedRuleDescriptor = (&validate.FieldConstraints{}).ProtoReflect().Descriptor().Fields().ByName("repeated") + repeatedItemsRuleDescriptor = (&validate.RepeatedRules{}).ProtoReflect().Descriptor().Fields().ByName("items") + 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 { + base + // ItemConstraints are checked on every item of the list 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 @@ -32,7 +55,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.PrefixErrorPaths(itemErr, "[%d]", i) + 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)}, + }, r.base.RulePrefix.GetElements()) } if ok, err = errors.Merge(err, itemErr, failFast); !ok { return err diff --git a/internal/evaluator/value.go b/internal/evaluator/value.go index 7d500a4..753ba83 100644 --- a/internal/evaluator/value.go +++ b/internal/evaluator/value.go @@ -15,20 +15,25 @@ package evaluator import ( + "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" "google.golang.org/protobuf/reflect/protoreflect" ) // 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 + // 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 - // Zero is the default or zero-value for this value's type - Zero protoreflect.Value } func (v *value) Evaluate(val protoreflect.Value, failFast bool) error { diff --git a/internal/expression/ast.go b/internal/expression/ast.go index 2f699df..7d0868a 100644 --- a/internal/expression/ast.go +++ b/internal/expression/ast.go @@ -15,9 +15,11 @@ 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. @@ -79,11 +81,12 @@ 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, + AST: residual, + Source: ast.Source, + Path: ast.Path, + Value: ast.Value, + Descriptor: ast.Descriptor, }) } } @@ -109,9 +112,24 @@ 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, + 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 Expression + 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) { @@ -121,7 +139,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, + Program: prog, + Source: ast.Source, + Path: ast.Path, + Value: ast.Value, + Descriptor: ast.Descriptor, }, 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..9880bab 100644 --- a/internal/expression/compile.go +++ b/internal/expression/compile.go @@ -15,27 +15,28 @@ 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 +// 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 } // 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 +48,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 +71,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 +89,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 +100,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 6f01781..55368d8 100644 --- a/internal/expression/program.go +++ b/internal/expression/program.go @@ -37,11 +37,11 @@ type ProgramSet []compiledProgram // 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 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 { @@ -88,12 +88,15 @@ 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 *validate.Constraint + Path []*validate.FieldPathElement + Value protoreflect.Value + Descriptor protoreflect.FieldDescriptor } //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,20 +111,37 @@ 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{ + Proto: &validate.Violation{ + Rule: expr.rulePath(), + ConstraintId: proto.String(expr.Source.GetId()), + Message: proto.String(val), + }, + RuleValue: expr.Value, + RuleDescriptor: expr.Descriptor, }, 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{ + Proto: &validate.Violation{ + Rule: expr.rulePath(), + ConstraintId: proto.String(expr.Source.GetId()), + Message: proto.String(expr.Source.GetMessage()), + }, + RuleValue: expr.Value, + RuleDescriptor: expr.Descriptor, }, nil default: return nil, errors.NewRuntimeErrorf( "resolved to an unexpected type %T", val) } } + +func (expr compiledProgram) rulePath() *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 6b40d92..54986c2 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" ) @@ -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, proto.Equal(test.exViol, violation.Proto)) + } } }) } @@ -98,7 +103,7 @@ func TestSet(t *testing.T) { name string set ProgramSet failFast bool - exViols *pverr.ValidationError + exViols *validate.Violations exErr bool }{ { @@ -143,10 +148,12 @@ 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: &validate.Violations{ + Violations: []*validate.Violation{ + {ConstraintId: proto.String("foo"), Message: proto.String("fizz")}, + {ConstraintId: proto.String("bar"), Message: proto.String("buzz")}, + }, + }, }, { name: "invalid fail fast", @@ -161,9 +168,11 @@ 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: &validate.Violations{ + Violations: []*validate.Violation{ + {ConstraintId: proto.String("foo"), Message: proto.String("fizz")}, + }, + }, }, } @@ -172,12 +181,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, proto.Equal(test.exViols, viols.ToProto())) case test.exErr: require.Error(t, err) default: 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 ( diff --git a/validator.go b/validator.go index 990f93e..69e4ee2 100644 --- a/validator.go +++ b/validator.go @@ -31,18 +31,20 @@ 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 Violation 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 provides 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 @@ -104,7 +106,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.FinalizePaths(err) + return err } // Validate uses a global instance of Validator constructed with no ValidatorOptions and @@ -119,6 +123,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 ce8ad89..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" @@ -156,9 +158,50 @@ 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()) + violation := valErr.Violations[0] + fmt.Println(FieldPathString(violation.Proto.GetField()), violation.Proto.GetConstraintId()) + fmt.Println(violation.RuleValue, violation.FieldValue) } // 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:]]+)*$」一致する必要があります。 } diff --git a/validator_test.go b/validator_test.go index c9aab01..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].GetConstraintId()) + assert.Equal(t, "paths.no_space", valErr.Violations[0].Proto.GetConstraintId()) } func TestValidator_Validate_Issue141(t *testing.T) {