From 1ca4047bb8936bc3e795fee7181223c4b86cc005 Mon Sep 17 00:00:00 2001 From: bufdev <4228796+bufdev@users.noreply.github.com> Date: Wed, 2 Oct 2024 15:23:45 -0400 Subject: [PATCH] Add a global validate function (#152) Having used this library for a while, I think it's finally time to add this function. It's what 99.99% of users want to do, and having to pass around a `protovalidate.Validator` really messes with a ton of functions downstream. There's no actual state that we care about being shared, other than a cache, and I think this is an exception to our no-global rule. If there's major downsides, let's hear them, but I think this is an extremely user-favorable move. --- .golangci.yml | 4 ++++ README.md | 32 +++++++++++++------------------- validator.go | 15 +++++++++++++++ validator_example_test.go | 9 ++------- validator_test.go | 31 +++++++++++++++++++++++++++++++ 5 files changed, 65 insertions(+), 26 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index ab11364..23f48e7 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -79,3 +79,7 @@ issues: linters: # uses deprecated fields on protoimpl.ExtensionInfo but its the only way - staticcheck + # We allow a global validator. + - path: validator.go + linters: + - gochecknoglobals diff --git a/README.md b/README.md index 96c6e33..314c254 100644 --- a/README.md +++ b/README.md @@ -74,7 +74,7 @@ message Transaction { uint64 id = 1 [(buf.validate.field).uint64.gt = 999]; google.protobuf.Timestamp purchase_date = 2; google.protobuf.Timestamp delivery_date = 3; - + string price = 4 [(buf.validate.field).cel = { id: "transaction.price", message: "price must be positive and include a valid currency symbol ($ or £)", @@ -94,7 +94,7 @@ message Transaction { `protovalidate-go` assumes the constraint extensions are imported into the generated code via `buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go`. -If you are using Buf [managed mode](https://buf.build/docs/generate/managed-mode/) to augment Go code generation, ensure +If you are using Buf [managed mode](https://buf.build/docs/generate/managed-mode/) to augment Go code generation, ensure that the `protovalidate` module is excluded in your [`buf.gen.yaml`](https://buf.build/docs/configuration/v1/buf-gen-yaml#except): **`buf.gen.yaml` v1** @@ -129,7 +129,7 @@ package main import ( "fmt" "time" - + pb "github.com/path/to/generated/protos" "github.com/bufbuild/protovalidate-go" "google.golang.org/protobuf/types/known/timestamppb" @@ -142,13 +142,7 @@ func main() { PurchaseDate: timestamppb.New(time.Now()), DeliveryDate: timestamppb.New(time.Now().Add(time.Hour)), } - - v, err := protovalidate.New() - if err != nil { - fmt.Println("failed to initialize validator:", err) - } - - if err = v.Validate(msg); err != nil { + if err = protovalidate.Validate(msg); err != nil { fmt.Println("validation failed:", err) } else { fmt.Println("validation succeeded") @@ -158,16 +152,16 @@ func main() { ### Lazy mode -`protovalidate-go` defaults to lazily construct validation logic for Protobuf -message types the first time they are encountered. A validator's internal -cache can be pre-warmed with the `WithMessages` or `WithDescriptors` options +`protovalidate-go` defaults to lazily construct validation logic for Protobuf +message types the first time they are encountered. A validator's internal +cache can be pre-warmed with the `WithMessages` or `WithDescriptors` options during initialization: ```go validator, err := protovalidate.New( protovalidate.WithMessages( - &pb.MyFoo{}, - &pb.MyBar{}, + &pb.MyFoo{}, + &pb.MyBar{}, ), ) ``` @@ -191,7 +185,7 @@ validator, err := protovalidate.New( ### Support legacy `protoc-gen-validate` constraints The `protovalidate-go` module comes with a `legacy` package which adds opt-in support -for existing `protoc-gen-validate` constraints. Provide the`legacy.WithLegacySupport` +for existing `protoc-gen-validate` constraints. Provide the`legacy.WithLegacySupport` option when initializing the validator: ```go @@ -200,7 +194,7 @@ validator, err := protovalidate.New( ) ``` -`protoc-gen-validate` code generation is **not** used by `protovalidate-go`. The +`protoc-gen-validate` code generation is **not** used by `protovalidate-go`. The `legacy` package assumes the `protoc-gen-validate` extensions are imported into the generated code via `github.com/envoyproxy/protoc-gen-validate/validate`. @@ -208,8 +202,8 @@ A [migration tool](https://github.com/bufbuild/protovalidate/tree/main/tools/pro ## Performance -[Benchmarks](validator_bench_test.go) are provided to test a variety of use-cases. Generally, after the -initial cold start, validation on a message is sub-microsecond +[Benchmarks](validator_bench_test.go) are provided to test a variety of use-cases. Generally, after the +initial cold start, validation on a message is sub-microsecond and only allocates in the event of a validation error. ``` diff --git a/validator.go b/validator.go index 0a74ba0..990f93e 100644 --- a/validator.go +++ b/validator.go @@ -16,6 +16,7 @@ package protovalidate import ( "fmt" + "sync" "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" "github.com/bufbuild/protovalidate-go/celext" @@ -27,6 +28,8 @@ import ( "google.golang.org/protobuf/reflect/protoregistry" ) +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 @@ -104,6 +107,18 @@ func (v *Validator) Validate(msg proto.Message) error { return eval.EvaluateMessage(refl, v.failFast) } +// Validate uses a global instance of Validator constructed with no ValidatorOptions and +// calls its Validate function. For the vast majority of validation cases, using this global +// function is safe and acceptable. If you need to provide i.e. a custom +// ExtensionTypeResolver, you'll need to construct a Validator. +func Validate(msg proto.Message) error { + globalValidator, err := getGlobalValidator() + if err != nil { + return err + } + return globalValidator.Validate(msg) +} + type config struct { failFast bool useUTC bool diff --git a/validator_example_test.go b/validator_example_test.go index 1b9973e..ce8ad89 100644 --- a/validator_example_test.go +++ b/validator_example_test.go @@ -24,11 +24,6 @@ import ( ) func Example() { - validator, err := New() - if err != nil { - log.Fatal(err) - } - person := &pb.Person{ Id: 1234, Email: "protovalidate@buf.build", @@ -39,11 +34,11 @@ func Example() { }, } - err = validator.Validate(person) + err := Validate(person) fmt.Println("valid:", err) person.Email = "not an email" - err = validator.Validate(person) + err = Validate(person) fmt.Println("invalid:", err) // output: diff --git a/validator_test.go b/validator_test.go index 251b078..c9aab01 100644 --- a/validator_test.go +++ b/validator_test.go @@ -60,6 +60,37 @@ func TestValidator_Validate(t *testing.T) { }) } +func TestValidator_ValidateGlobal(t *testing.T) { + t.Parallel() + + t.Run("HasMsgExprs", func(t *testing.T) { + t.Parallel() + + tests := []struct { + msg *pb.HasMsgExprs + exErr bool + }{ + { + &pb.HasMsgExprs{X: 2, Y: 43}, + false, + }, + { + &pb.HasMsgExprs{X: 9, Y: 8}, + true, + }, + } + + for _, test := range tests { + err := Validate(test.msg) + if test.exErr { + assert.Error(t, err) + } else { + require.NoError(t, err) + } + } + }) +} + func TestRecursive(t *testing.T) { t.Parallel() val, err := New()