Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a global validate function #152

Merged
merged 3 commits into from
Oct 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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: "[email protected]",
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