diff --git a/internal/cmd/protovalidate-conformance-go/main.go b/internal/cmd/protovalidate-conformance-go/main.go index b0b912c..3f21365 100644 --- a/internal/cmd/protovalidate-conformance-go/main.go +++ b/internal/cmd/protovalidate-conformance-go/main.go @@ -19,7 +19,6 @@ import ( "io" "log" "os" - "strings" "github.com/bufbuild/protovalidate-go" "github.com/bufbuild/protovalidate-go/internal/gen/buf/validate/conformance/harness" @@ -27,6 +26,7 @@ import ( "google.golang.org/protobuf/reflect/protodesc" "google.golang.org/protobuf/reflect/protoreflect" "google.golang.org/protobuf/reflect/protoregistry" + "google.golang.org/protobuf/types/descriptorpb" "google.golang.org/protobuf/types/dynamicpb" "google.golang.org/protobuf/types/known/anypb" ) @@ -52,10 +52,33 @@ func main() { } } +func buildResolver(fdset *descriptorpb.FileDescriptorSet) (*dynResolver, error) { + opts := protodesc.FileOptions{} + res := &protoregistry.Files{} + + for _, file := range fdset.GetFile() { + if fdesc, err := opts.New(file, res); err != nil { + return nil, fmt.Errorf("unable to create file descriptor for %v: %v", file.GetName(), err) + } else if err = res.RegisterFile(fdesc); err != nil { + return nil, fmt.Errorf("unable to register file descriptor for %v: %v", file.GetName(), err) + } + } + + resolver := dynamicpb.NewTypes(res) + return &dynResolver{ + seen: map[protoreflect.FullName]struct{}{}, + resolver: resolver, + opts: proto.UnmarshalOptions{ + Merge: true, + AllowPartial: true, + Resolver: resolver, + }, + }, nil +} + func TestConformance(req *harness.TestConformanceRequest) (*harness.TestConformanceResponse, error) { - files, err := protodesc.NewFiles(req.GetFdset()) + resolver, err := buildResolver(req.GetFdset()) if err != nil { - err = fmt.Errorf("failed to parse file descriptors: %w", err) return nil, err } val, err := protovalidate.New() @@ -65,25 +88,14 @@ func TestConformance(req *harness.TestConformanceRequest) (*harness.TestConforma } resp := &harness.TestConformanceResponse{Results: map[string]*harness.TestResult{}} for caseName, testCase := range req.GetCases() { - resp.Results[caseName] = TestCase(val, files, testCase) + resp.Results[caseName] = TestCase(val, resolver, testCase) } return resp, nil } -func TestCase(val *protovalidate.Validator, files *protoregistry.Files, testCase *anypb.Any) *harness.TestResult { - urlParts := strings.Split(testCase.GetTypeUrl(), "/") - fullName := protoreflect.FullName(urlParts[len(urlParts)-1]) - desc, err := files.FindDescriptorByName(fullName) +func TestCase(val *protovalidate.Validator, resolver *dynResolver, testCase *anypb.Any) *harness.TestResult { + dyn, err := resolver.UnmarshalNew(testCase) if err != nil { - return unexpectedErrorResult("unable to find descriptor: %v", err) - } - msgDesc, ok := desc.(protoreflect.MessageDescriptor) - if !ok { - return unexpectedErrorResult("expected message descriptor, got %T", desc) - } - - dyn := dynamicpb.NewMessage(msgDesc) - if err = anypb.UnmarshalTo(testCase, dyn, proto.UnmarshalOptions{}); err != nil { return unexpectedErrorResult("unable to unmarshal test case: %v", err) } @@ -126,3 +138,49 @@ func unexpectedErrorResult(format string, args ...any) *harness.TestResult { }, } } + +type dynResolver struct { + seen map[protoreflect.FullName]struct{} + resolver *dynamicpb.Types + opts proto.UnmarshalOptions +} + +func (dr *dynResolver) UnmarshalNew(msg *anypb.Any) (proto.Message, error) { + msgTyp, err := dr.resolver.FindMessageByURL(msg.GetTypeUrl()) + if err != nil { + return nil, fmt.Errorf("unable to find message type: %v", err) + } else if err = dr.mergeMsgOpts(msgTyp.Descriptor()); err != nil { + return nil, err + } + dyn := msgTyp.New().Interface() + return dyn, anypb.UnmarshalTo(msg, dyn, dr.opts) +} + +func (dr *dynResolver) mergeOpts(desc protoreflect.Descriptor) error { + return dr.opts.Unmarshal(desc.Options().ProtoReflect().GetUnknown(), desc.Options()) +} + +func (dr *dynResolver) mergeMsgOpts(mDesc protoreflect.MessageDescriptor) error { + if _, seen := dr.seen[mDesc.FullName()]; seen { + return nil + } + dr.seen[mDesc.FullName()] = struct{}{} + if err := dr.mergeOpts(mDesc); err != nil { + return fmt.Errorf("failed to merge options for %v: %w", mDesc.FullName(), err) + } + + fields := mDesc.Fields() + for i, n := 0, fields.Len(); i < n; i++ { + field := fields.Get(i) + if err := dr.mergeOpts(field); err != nil { + return fmt.Errorf("failed to merge options for %v: %w", field.FullName(), err) + } + if field.Kind() == protoreflect.MessageKind { + if err := dr.mergeMsgOpts(field.Message()); err != nil { + return err + } + } + } + + return nil +} diff --git a/internal/evaluator/builder.go b/internal/evaluator/builder.go index 7e4c7e4..e13a09e 100644 --- a/internal/evaluator/builder.go +++ b/internal/evaluator/builder.go @@ -15,6 +15,7 @@ package evaluator import ( + "log" "sync" "sync/atomic" @@ -24,6 +25,7 @@ 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/types/dynamicpb" ) @@ -241,6 +243,7 @@ func (bldr *Builder) buildValue( bldr.processEnumConstraints, bldr.processMapConstraints, bldr.processRepeatedConstraints, + bldr.processSharedConstraints, } for _, step := range steps { @@ -251,6 +254,24 @@ func (bldr *Builder) buildValue( return nil } +func (bldr *Builder) processSharedConstraints( + fdesc protoreflect.FieldDescriptor, + _ *validate.FieldConstraints, + forItems bool, + valEval *value, + _ MessageCache, +) error { + if forItems { + return nil + } + proto.RangeExtensions(fdesc.Options(), func(extTyp protoreflect.ExtensionType, rules interface{}) bool { + if extTyp.TypeDescriptor().Kind() + log.Println(extTyp.TypeDescriptor().FullName(), rules) + return true + }) + return nil +} + func (bldr *Builder) processIgnoreEmpty( fdesc protoreflect.FieldDescriptor, constraints *validate.FieldConstraints, diff --git a/proto/tests/example/v1/validations.proto b/proto/tests/example/v1/validations.proto index 8de169d..de4f543 100644 --- a/proto/tests/example/v1/validations.proto +++ b/proto/tests/example/v1/validations.proto @@ -171,4 +171,4 @@ message RepeatedItemCel { id: "paths.no_space", expression: "!this.startsWith(' ')" }]; -} +} \ No newline at end of file