Skip to content

Commit

Permalink
Add a global validate function (#152)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
bufdev authored Oct 2, 2024
1 parent b477f4c commit 1ca4047
Show file tree
Hide file tree
Showing 5 changed files with 65 additions and 26 deletions.
4 changes: 4 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
32 changes: 13 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 £)",
Expand All @@ -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**
Expand Down Expand Up @@ -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"
Expand All @@ -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")
Expand All @@ -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{},
),
)
```
Expand All @@ -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
Expand All @@ -200,16 +194,16 @@ 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`.

A [migration tool](https://github.com/bufbuild/protovalidate/tree/main/tools/protovalidate-migrate) is also available to incrementally upgrade legacy constraints in `.proto` files.

## 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.

```
Expand Down
15 changes: 15 additions & 0 deletions validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
9 changes: 2 additions & 7 deletions validator_example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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:
Expand Down
31 changes: 31 additions & 0 deletions validator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down

0 comments on commit 1ca4047

Please sign in to comment.