diff --git a/celext/lib.go b/celext/lib.go index ae027c1..eb53e6b 100644 --- a/celext/lib.go +++ b/celext/lib.go @@ -29,6 +29,7 @@ import ( "github.com/google/cel-go/common/types/ref" "github.com/google/cel-go/common/types/traits" "github.com/google/cel-go/ext" + "google.golang.org/protobuf/reflect/protoregistry" ) // DefaultEnv produces a cel.Env with the necessary cel.EnvOption and @@ -37,6 +38,11 @@ import ( // of the local timezone. func DefaultEnv(useUTC bool) (*cel.Env, error) { return cel.NewEnv( + // we bind in the global type registry optimistically to ensure expressions + // operating against Any WKTs can resolve their underlying type if it's + // known to the application. They will otherwise fail with a runtime error + // if the type is unknown. + cel.TypeDescs(protoregistry.GlobalFiles), cel.Lib(lib{ useUTC: useUTC, }), diff --git a/internal/constraints/cache.go b/internal/constraints/cache.go index bb2edf7..02a3b9e 100644 --- a/internal/constraints/cache.go +++ b/internal/constraints/cache.go @@ -114,7 +114,7 @@ func (c *Cache) prepareEnvironment( ) (*cel.Env, error) { env, err := env.Extend( cel.Types(rules.Interface()), - cel.Variable("this", c.getCELType(fieldDesc, forItems)), + cel.Variable("this", expression.ProtoFieldToCELType(fieldDesc, true, forItems)), cel.Variable("rules", cel.ObjectType(string(rules.Descriptor().FullName()))), ) @@ -168,31 +168,3 @@ func (c *Cache) getExpectedConstraintDescriptor( return expected, ok } } - -// getCELType resolves the CEL value type for the provided FieldDescriptor. If -// forItems is true, the type for the repeated list items is returned instead of -// the list type itself. -func (c *Cache) getCELType(fieldDesc protoreflect.FieldDescriptor, forItems bool) *cel.Type { - if !forItems { - switch { - case fieldDesc.IsMap(): - return cel.MapType(cel.DynType, cel.DynType) - case fieldDesc.IsList(): - return cel.ListType(cel.DynType) - } - } - - if fieldDesc.Kind() == protoreflect.MessageKind { - switch fqn := fieldDesc.Message().FullName(); fqn { - case "google.protobuf.Any": - return cel.AnyType - case "google.protobuf.Duration": - return cel.DurationType - case "google.protobuf.Timestamp": - return cel.TimestampType - default: - return cel.ObjectType(string(fqn)) - } - } - return ProtoKindToCELType(fieldDesc.Kind()) -} diff --git a/internal/constraints/cache_test.go b/internal/constraints/cache_test.go index e36839a..5d16f17 100644 --- a/internal/constraints/cache_test.go +++ b/internal/constraints/cache_test.go @@ -20,7 +20,6 @@ import ( "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" "github.com/bufbuild/protovalidate-go/celext" "github.com/bufbuild/protovalidate-go/internal/gen/buf/validate/conformance/cases" - "github.com/google/cel-go/cel" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "google.golang.org/protobuf/proto" @@ -197,57 +196,3 @@ func TestCache_GetExpectedConstraintDescriptor(t *testing.T) { }) } } - -func TestCache_GetCELType(t *testing.T) { - t.Parallel() - - tests := []struct { - desc protoreflect.FieldDescriptor - forItems bool - ex *cel.Type - }{ - { - desc: getFieldDesc(t, &cases.MapNone{}, "val"), - ex: cel.MapType(cel.DynType, cel.DynType), - }, - { - desc: getFieldDesc(t, &cases.RepeatedNone{}, "val"), - ex: cel.ListType(cel.DynType), - }, - { - desc: getFieldDesc(t, &cases.RepeatedNone{}, "val"), - forItems: true, - ex: cel.IntType, - }, - { - desc: getFieldDesc(t, &cases.AnyNone{}, "val"), - ex: cel.AnyType, - }, - { - desc: getFieldDesc(t, &cases.DurationNone{}, "val"), - ex: cel.DurationType, - }, - { - desc: getFieldDesc(t, &cases.TimestampNone{}, "val"), - ex: cel.TimestampType, - }, - { - desc: getFieldDesc(t, &cases.MessageNone{}, "val"), - ex: cel.ObjectType(string(((&cases.MessageNone{}).GetVal()).ProtoReflect().Descriptor().FullName())), - }, - { - desc: getFieldDesc(t, &cases.Int32None{}, "val"), - ex: cel.IntType, - }, - } - - c := NewCache() - for _, tc := range tests { - test := tc - t.Run(string(test.desc.FullName()), func(t *testing.T) { - t.Parallel() - typ := c.getCELType(test.desc, test.forItems) - assert.Equal(t, test.ex.String(), typ.String()) - }) - } -} diff --git a/internal/constraints/lookups.go b/internal/constraints/lookups.go index 504ca2d..fa54c2a 100644 --- a/internal/constraints/lookups.go +++ b/internal/constraints/lookups.go @@ -16,7 +16,6 @@ package constraints import ( "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" - "github.com/google/cel-go/cel" "google.golang.org/protobuf/reflect/protoreflect" ) @@ -91,40 +90,3 @@ func ExpectedWrapperConstraints(fqn protoreflect.FullName) (desc protoreflect.Fi return nil, false } } - -// ProtoKindToCELType maps a protoreflect.Kind to a compatible cel.Type. -func ProtoKindToCELType(kind protoreflect.Kind) *cel.Type { - switch kind { - case - protoreflect.FloatKind, - protoreflect.DoubleKind: - return cel.DoubleType - case - protoreflect.Int32Kind, - protoreflect.Int64Kind, - protoreflect.Sint32Kind, - protoreflect.Sint64Kind, - protoreflect.Sfixed32Kind, - protoreflect.Sfixed64Kind, - protoreflect.EnumKind: - return cel.IntType - case - protoreflect.Uint32Kind, - protoreflect.Uint64Kind, - protoreflect.Fixed32Kind, - protoreflect.Fixed64Kind: - return cel.UintType - case protoreflect.BoolKind: - return cel.BoolType - case protoreflect.StringKind: - return cel.StringType - case protoreflect.BytesKind: - return cel.BytesType - case - protoreflect.MessageKind, - protoreflect.GroupKind: - return cel.DynType - default: - return cel.DynType - } -} diff --git a/internal/constraints/lookups_test.go b/internal/constraints/lookups_test.go index f240870..152201d 100644 --- a/internal/constraints/lookups_test.go +++ b/internal/constraints/lookups_test.go @@ -17,6 +17,7 @@ package constraints import ( "testing" + "github.com/bufbuild/protovalidate-go/internal/expression" "github.com/google/cel-go/cel" "github.com/stretchr/testify/assert" "google.golang.org/protobuf/proto" @@ -83,7 +84,7 @@ func TestProtoKindToCELType(t *testing.T) { kind, typ := k, ty t.Run(kind.String(), func(t *testing.T) { t.Parallel() - assert.Equal(t, typ, ProtoKindToCELType(kind)) + assert.Equal(t, typ, expression.ProtoKindToCELType(kind)) }) } } diff --git a/internal/evaluator/builder.go b/internal/evaluator/builder.go index 073547e..b525711 100644 --- a/internal/evaluator/builder.go +++ b/internal/evaluator/builder.go @@ -277,18 +277,12 @@ func (bldr *Builder) processFieldExpressions( if len(exprs) == 0 { return nil } - var opts []cel.EnvOption - if fieldDesc.Kind() == protoreflect.MessageKind { - opts = []cel.EnvOption{ - cel.Types(dynamicpb.NewMessage(fieldDesc.ContainingMessage())), - cel.Types(dynamicpb.NewMessage(fieldDesc.Message())), - cel.Variable("this", cel.ObjectType(string(fieldDesc.Message().FullName()))), - } - } else { - opts = []cel.EnvOption{ - cel.Variable("this", constraints.ProtoKindToCELType(fieldDesc.Kind())), - } - } + + celTyp := expression.ProtoFieldToCELType(fieldDesc, false, false) + opts := append( + expression.RequiredCELEnvOptions(fieldDesc), + cel.Variable("this", celTyp), + ) compiledExpressions, err := expression.Compile(exprs, bldr.env, opts...) if err != nil { return err diff --git a/internal/expression/lookups.go b/internal/expression/lookups.go new file mode 100644 index 0000000..5c26695 --- /dev/null +++ b/internal/expression/lookups.go @@ -0,0 +1,113 @@ +// Copyright 2023 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 expression + +import ( + "github.com/google/cel-go/cel" + "google.golang.org/protobuf/reflect/protoreflect" + "google.golang.org/protobuf/types/dynamicpb" +) + +// ProtoKindToCELType maps a protoreflect.Kind to a compatible cel.Type. +func ProtoKindToCELType(kind protoreflect.Kind) *cel.Type { + switch kind { + case + protoreflect.FloatKind, + protoreflect.DoubleKind: + return cel.DoubleType + case + protoreflect.Int32Kind, + protoreflect.Int64Kind, + protoreflect.Sint32Kind, + protoreflect.Sint64Kind, + protoreflect.Sfixed32Kind, + protoreflect.Sfixed64Kind, + protoreflect.EnumKind: + return cel.IntType + case + protoreflect.Uint32Kind, + protoreflect.Uint64Kind, + protoreflect.Fixed32Kind, + protoreflect.Fixed64Kind: + return cel.UintType + case protoreflect.BoolKind: + return cel.BoolType + case protoreflect.StringKind: + return cel.StringType + case protoreflect.BytesKind: + return cel.BytesType + case + protoreflect.MessageKind, + protoreflect.GroupKind: + return cel.DynType + default: + return cel.DynType + } +} + +// ProtoFieldToCELType resolves the CEL value type for the provided +// FieldDescriptor. If generic is true, the specific subtypes of map and +// repeated fields will be replaced with cel.DynType. If forItems is true, the +// type for the repeated list items is returned instead of the list type itself. +func ProtoFieldToCELType(fieldDesc protoreflect.FieldDescriptor, generic, forItems bool) *cel.Type { + if !forItems { + switch { + case fieldDesc.IsMap(): + if generic { + return cel.MapType(cel.DynType, cel.DynType) + } + keyType := ProtoFieldToCELType(fieldDesc.MapKey(), false, true) + valType := ProtoFieldToCELType(fieldDesc.MapValue(), false, true) + return cel.MapType(keyType, valType) + case fieldDesc.IsList(): + if generic { + return cel.ListType(cel.DynType) + } + itemType := ProtoFieldToCELType(fieldDesc, false, true) + return cel.ListType(itemType) + } + } + + if fieldDesc.Kind() == protoreflect.MessageKind { + switch fqn := fieldDesc.Message().FullName(); fqn { + case "google.protobuf.Any": + return cel.AnyType + case "google.protobuf.Duration": + return cel.DurationType + case "google.protobuf.Timestamp": + return cel.TimestampType + default: + return cel.ObjectType(string(fqn)) + } + } + return ProtoKindToCELType(fieldDesc.Kind()) +} + +// RequiredCELEnvOptions returns the options required to have expressions which +// rely on the provided descriptor. +func RequiredCELEnvOptions(fieldDesc protoreflect.FieldDescriptor) []cel.EnvOption { + if fieldDesc.IsMap() { + return append( + RequiredCELEnvOptions(fieldDesc.MapKey()), + RequiredCELEnvOptions(fieldDesc.MapValue())..., + ) + } + if fieldDesc.Kind() == protoreflect.MessageKind { + return []cel.EnvOption{ + cel.Types(dynamicpb.NewMessage(fieldDesc.Message())), + } + } + return nil +} diff --git a/internal/expression/lookups_test.go b/internal/expression/lookups_test.go new file mode 100644 index 0000000..f9fe452 --- /dev/null +++ b/internal/expression/lookups_test.go @@ -0,0 +1,97 @@ +// Copyright 2023 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 expression + +import ( + "testing" + + "github.com/bufbuild/protovalidate-go/internal/gen/buf/validate/conformance/cases" + "github.com/google/cel-go/cel" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/reflect/protoreflect" +) + +func TestCache_GetCELType(t *testing.T) { + t.Parallel() + + tests := []struct { + desc protoreflect.FieldDescriptor + generic bool + forItems bool + ex *cel.Type + }{ + { + desc: getFieldDesc(t, &cases.MapNone{}, "val"), + ex: cel.MapType(cel.UintType, cel.BoolType), + }, + { + desc: getFieldDesc(t, &cases.MapNone{}, "val"), + generic: true, + ex: cel.MapType(cel.DynType, cel.DynType), + }, + { + desc: getFieldDesc(t, &cases.RepeatedNone{}, "val"), + ex: cel.ListType(cel.IntType), + }, + { + desc: getFieldDesc(t, &cases.RepeatedNone{}, "val"), + generic: true, + ex: cel.ListType(cel.DynType), + }, + { + desc: getFieldDesc(t, &cases.RepeatedNone{}, "val"), + forItems: true, + ex: cel.IntType, + }, + { + desc: getFieldDesc(t, &cases.AnyNone{}, "val"), + ex: cel.AnyType, + }, + { + desc: getFieldDesc(t, &cases.DurationNone{}, "val"), + ex: cel.DurationType, + }, + { + desc: getFieldDesc(t, &cases.TimestampNone{}, "val"), + ex: cel.TimestampType, + }, + { + desc: getFieldDesc(t, &cases.MessageNone{}, "val"), + ex: cel.ObjectType(string(((&cases.MessageNone{}).GetVal()).ProtoReflect().Descriptor().FullName())), + }, + { + desc: getFieldDesc(t, &cases.Int32None{}, "val"), + ex: cel.IntType, + }, + } + + for _, tc := range tests { + test := tc + t.Run(string(test.desc.FullName()), func(t *testing.T) { + t.Parallel() + typ := ProtoFieldToCELType(test.desc, test.generic, test.forItems) + assert.Equal(t, test.ex.String(), typ.String()) + }) + } +} + +func getFieldDesc(t *testing.T, msg proto.Message, fld protoreflect.Name) protoreflect.FieldDescriptor { + t.Helper() + desc := msg.ProtoReflect().Descriptor().Fields().ByName(fld) + require.NotNil(t, desc) + return desc +} diff --git a/internal/gen/tests/example/v1/validations.pb.go b/internal/gen/tests/example/v1/validations.pb.go index 4917f40..0669b72 100644 --- a/internal/gen/tests/example/v1/validations.pb.go +++ b/internal/gen/tests/example/v1/validations.pb.go @@ -651,6 +651,101 @@ func (x *FieldOfTypeAny) GetAny() *anypb.Any { return nil } +// https://github.com/bufbuild/protovalidate/issues/92 +type CelMapOnARepeated struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Values []*CelMapOnARepeated_Value `protobuf:"bytes,1,rep,name=values,proto3" json:"values,omitempty"` +} + +func (x *CelMapOnARepeated) Reset() { + *x = CelMapOnARepeated{} + if protoimpl.UnsafeEnabled { + mi := &file_tests_example_v1_validations_proto_msgTypes[11] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CelMapOnARepeated) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CelMapOnARepeated) ProtoMessage() {} + +func (x *CelMapOnARepeated) ProtoReflect() protoreflect.Message { + mi := &file_tests_example_v1_validations_proto_msgTypes[11] + 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 CelMapOnARepeated.ProtoReflect.Descriptor instead. +func (*CelMapOnARepeated) Descriptor() ([]byte, []int) { + return file_tests_example_v1_validations_proto_rawDescGZIP(), []int{11} +} + +func (x *CelMapOnARepeated) GetValues() []*CelMapOnARepeated_Value { + if x != nil { + return x.Values + } + return nil +} + +type CelMapOnARepeated_Value struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` +} + +func (x *CelMapOnARepeated_Value) Reset() { + *x = CelMapOnARepeated_Value{} + if protoimpl.UnsafeEnabled { + mi := &file_tests_example_v1_validations_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CelMapOnARepeated_Value) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CelMapOnARepeated_Value) ProtoMessage() {} + +func (x *CelMapOnARepeated_Value) ProtoReflect() protoreflect.Message { + mi := &file_tests_example_v1_validations_proto_msgTypes[15] + 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 CelMapOnARepeated_Value.ProtoReflect.Descriptor instead. +func (*CelMapOnARepeated_Value) Descriptor() ([]byte, []int) { + return file_tests_example_v1_validations_proto_rawDescGZIP(), []int{11, 0} +} + +func (x *CelMapOnARepeated_Value) GetName() string { + if x != nil { + return x.Name + } + return "" +} + var File_tests_example_v1_validations_proto protoreflect.FileDescriptor var file_tests_example_v1_validations_proto_rawDesc = []byte{ @@ -794,21 +889,34 @@ var file_tests_example_v1_validations_proto_rawDesc = []byte{ 0x79, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x12, 0x16, 0x74, 0x68, 0x69, 0x73, 0x20, 0x73, 0x68, 0x6f, 0x75, 0x6c, 0x64, 0x20, 0x6e, 0x65, 0x76, 0x65, 0x72, 0x20, 0x66, 0x61, 0x69, 0x6c, 0x1a, 0x0c, 0x74, 0x68, 0x69, 0x73, 0x20, 0x3d, 0x3d, 0x20, 0x74, 0x68, 0x69, 0x73, 0x52, 0x03, 0x61, 0x6e, - 0x79, 0x42, 0xd8, 0x01, 0x0a, 0x14, 0x63, 0x6f, 0x6d, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x73, 0x2e, - 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x42, 0x10, 0x56, 0x61, 0x6c, 0x69, - 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 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, 0x62, 0x06, 0x70, 0x72, - 0x6f, 0x74, 0x6f, 0x33, + 0x79, 0x22, 0xcf, 0x01, 0x0a, 0x11, 0x43, 0x65, 0x6c, 0x4d, 0x61, 0x70, 0x4f, 0x6e, 0x41, 0x52, + 0x65, 0x70, 0x65, 0x61, 0x74, 0x65, 0x64, 0x12, 0x9c, 0x01, 0x0a, 0x06, 0x76, 0x61, 0x6c, 0x75, + 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x74, 0x65, 0x73, 0x74, 0x73, + 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x65, 0x6c, 0x4d, + 0x61, 0x70, 0x4f, 0x6e, 0x41, 0x52, 0x65, 0x70, 0x65, 0x61, 0x74, 0x65, 0x64, 0x2e, 0x56, 0x61, + 0x6c, 0x75, 0x65, 0x42, 0x59, 0xba, 0x48, 0x56, 0xba, 0x01, 0x53, 0x0a, 0x0f, 0x65, 0x6e, 0x76, + 0x2e, 0x76, 0x61, 0x72, 0x73, 0x2e, 0x75, 0x6e, 0x69, 0x71, 0x75, 0x65, 0x1a, 0x40, 0x74, 0x68, + 0x69, 0x73, 0x2e, 0x6d, 0x61, 0x70, 0x28, 0x76, 0x2c, 0x20, 0x76, 0x2e, 0x6e, 0x61, 0x6d, 0x65, + 0x29, 0x2e, 0x75, 0x6e, 0x69, 0x71, 0x75, 0x65, 0x28, 0x29, 0x20, 0x3f, 0x20, 0x27, 0x27, 0x20, + 0x3a, 0x20, 0x27, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x20, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x20, 0x6d, + 0x75, 0x73, 0x74, 0x20, 0x62, 0x65, 0x20, 0x75, 0x6e, 0x69, 0x71, 0x75, 0x65, 0x27, 0x52, 0x06, + 0x76, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x1a, 0x1b, 0x0a, 0x05, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x12, + 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, + 0x61, 0x6d, 0x65, 0x42, 0xd8, 0x01, 0x0a, 0x14, 0x63, 0x6f, 0x6d, 0x2e, 0x74, 0x65, 0x73, 0x74, + 0x73, 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x76, 0x31, 0x42, 0x10, 0x56, 0x61, + 0x6c, 0x69, 0x64, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 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, 0x62, 0x06, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -823,7 +931,7 @@ func file_tests_example_v1_validations_proto_rawDescGZIP() []byte { return file_tests_example_v1_validations_proto_rawDescData } -var file_tests_example_v1_validations_proto_msgTypes = make([]protoimpl.MessageInfo, 14) +var file_tests_example_v1_validations_proto_msgTypes = make([]protoimpl.MessageInfo, 16) var file_tests_example_v1_validations_proto_goTypes = []interface{}{ (*HasMsgExprs)(nil), // 0: tests.example.v1.HasMsgExprs (*SelfRecursive)(nil), // 1: tests.example.v1.SelfRecursive @@ -836,12 +944,14 @@ var file_tests_example_v1_validations_proto_goTypes = []interface{}{ (*MultipleStepsTransitiveFieldConstraints)(nil), // 8: tests.example.v1.MultipleStepsTransitiveFieldConstraints (*Simple)(nil), // 9: tests.example.v1.Simple (*FieldOfTypeAny)(nil), // 10: tests.example.v1.FieldOfTypeAny - nil, // 11: tests.example.v1.MsgHasMap.Int32mapEntry - nil, // 12: tests.example.v1.MsgHasMap.StringMapEntry - nil, // 13: tests.example.v1.MsgHasMap.MessageMapEntry - (*fieldmaskpb.FieldMask)(nil), // 14: google.protobuf.FieldMask - (*apipb.Api)(nil), // 15: google.protobuf.Api - (*anypb.Any)(nil), // 16: google.protobuf.Any + (*CelMapOnARepeated)(nil), // 11: tests.example.v1.CelMapOnARepeated + nil, // 12: tests.example.v1.MsgHasMap.Int32mapEntry + nil, // 13: tests.example.v1.MsgHasMap.StringMapEntry + nil, // 14: tests.example.v1.MsgHasMap.MessageMapEntry + (*CelMapOnARepeated_Value)(nil), // 15: tests.example.v1.CelMapOnARepeated.Value + (*fieldmaskpb.FieldMask)(nil), // 16: google.protobuf.FieldMask + (*apipb.Api)(nil), // 17: google.protobuf.Api + (*anypb.Any)(nil), // 18: google.protobuf.Any } var file_tests_example_v1_validations_proto_depIdxs = []int32{ 1, // 0: tests.example.v1.SelfRecursive.turtle:type_name -> tests.example.v1.SelfRecursive @@ -849,18 +959,19 @@ var file_tests_example_v1_validations_proto_depIdxs = []int32{ 2, // 2: tests.example.v1.LoopRecursiveB.a:type_name -> tests.example.v1.LoopRecursiveA 0, // 3: tests.example.v1.MsgHasOneof.msg:type_name -> tests.example.v1.HasMsgExprs 0, // 4: tests.example.v1.MsgHasRepeated.z:type_name -> tests.example.v1.HasMsgExprs - 11, // 5: tests.example.v1.MsgHasMap.int32map:type_name -> tests.example.v1.MsgHasMap.Int32mapEntry - 12, // 6: tests.example.v1.MsgHasMap.string_map:type_name -> tests.example.v1.MsgHasMap.StringMapEntry - 13, // 7: tests.example.v1.MsgHasMap.message_map:type_name -> tests.example.v1.MsgHasMap.MessageMapEntry - 14, // 8: tests.example.v1.TransitiveFieldConstraint.mask:type_name -> google.protobuf.FieldMask - 15, // 9: tests.example.v1.MultipleStepsTransitiveFieldConstraints.api:type_name -> google.protobuf.Api - 16, // 10: tests.example.v1.FieldOfTypeAny.any:type_name -> google.protobuf.Any - 2, // 11: tests.example.v1.MsgHasMap.MessageMapEntry.value:type_name -> tests.example.v1.LoopRecursiveA - 12, // [12:12] is the sub-list for method output_type - 12, // [12:12] is the sub-list for method input_type - 12, // [12:12] is the sub-list for extension type_name - 12, // [12:12] is the sub-list for extension extendee - 0, // [0:12] is the sub-list for field type_name + 12, // 5: tests.example.v1.MsgHasMap.int32map:type_name -> tests.example.v1.MsgHasMap.Int32mapEntry + 13, // 6: tests.example.v1.MsgHasMap.string_map:type_name -> tests.example.v1.MsgHasMap.StringMapEntry + 14, // 7: tests.example.v1.MsgHasMap.message_map:type_name -> tests.example.v1.MsgHasMap.MessageMapEntry + 16, // 8: tests.example.v1.TransitiveFieldConstraint.mask:type_name -> google.protobuf.FieldMask + 17, // 9: tests.example.v1.MultipleStepsTransitiveFieldConstraints.api:type_name -> google.protobuf.Api + 18, // 10: tests.example.v1.FieldOfTypeAny.any:type_name -> google.protobuf.Any + 15, // 11: tests.example.v1.CelMapOnARepeated.values:type_name -> tests.example.v1.CelMapOnARepeated.Value + 2, // 12: tests.example.v1.MsgHasMap.MessageMapEntry.value:type_name -> tests.example.v1.LoopRecursiveA + 13, // [13:13] is the sub-list for method output_type + 13, // [13:13] is the sub-list for method input_type + 13, // [13:13] is the sub-list for extension type_name + 13, // [13:13] is the sub-list for extension extendee + 0, // [0:13] is the sub-list for field type_name } func init() { file_tests_example_v1_validations_proto_init() } @@ -1001,6 +1112,30 @@ func file_tests_example_v1_validations_proto_init() { return nil } } + file_tests_example_v1_validations_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CelMapOnARepeated); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_tests_example_v1_validations_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CelMapOnARepeated_Value); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } } file_tests_example_v1_validations_proto_msgTypes[4].OneofWrappers = []interface{}{ (*MsgHasOneof_X)(nil), @@ -1013,7 +1148,7 @@ func file_tests_example_v1_validations_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_tests_example_v1_validations_proto_rawDesc, NumEnums: 0, - NumMessages: 14, + NumMessages: 16, NumExtensions: 0, NumServices: 0, }, diff --git a/proto/tests/example/v1/validations.proto b/proto/tests/example/v1/validations.proto index f5b7307..b6f0507 100644 --- a/proto/tests/example/v1/validations.proto +++ b/proto/tests/example/v1/validations.proto @@ -153,3 +153,15 @@ message FieldOfTypeAny { expression: "this == this" }]; } + +// https://github.com/bufbuild/protovalidate/issues/92 +message CelMapOnARepeated { + repeated Value values = 1 [(buf.validate.field).cel = { + id: "env.vars.unique", + expression: "this.map(v, v.name).unique() ? '' : 'value names must be unique'" + }]; + + message Value { + string name = 1; + } +} diff --git a/validator_test.go b/validator_test.go index d2aeff9..5944843 100644 --- a/validator_test.go +++ b/validator_test.go @@ -167,3 +167,20 @@ func TestValidator_Validate_FieldOfTypeAny(t *testing.T) { err = val.Validate(msg) require.NoError(t, err) } + +func TestValidator_Validate_CelMapOnARepeated(t *testing.T) { + t.Parallel() + val, err := New() + require.NoError(t, err) + msg := &pb.CelMapOnARepeated{Values: []*pb.CelMapOnARepeated_Value{ + {Name: "foo"}, + {Name: "bar"}, + {Name: "baz"}, + }} + err = val.Validate(msg) + assert.NoError(t, err) + msg.Values = append(msg.Values, &pb.CelMapOnARepeated_Value{Name: "foo"}) + err = val.Validate(msg) + valErr := &ValidationError{} + assert.ErrorAs(t, err, &valErr) +}