Skip to content
Open
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
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,14 @@ if err = protovalidate.Validate(moneyTransfer); err != nil {
}
```

To bound validation with a `context.Context` (cancellation or deadline):

```go
if err = protovalidate.ValidateContext(ctx, moneyTransfer); err != nil {
// Handle failure; errors.Is(err, context.DeadlineExceeded) on timeout.
}
```

## Installation

> [!TIP]
Expand Down
8 changes: 7 additions & 1 deletion any.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
package protovalidate

import (
"context"

"buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protoreflect"
Expand Down Expand Up @@ -58,7 +60,11 @@ type anyPB struct {
NotInValue protoreflect.Value
}

func (a anyPB) Evaluate(_ protoreflect.Message, val protoreflect.Value, cfg *validationConfig) error {
func (a anyPB) Evaluate(msg protoreflect.Message, val protoreflect.Value, cfg *validationConfig) error {
return a.EvaluateContext(context.Background(), msg, val, cfg)
}

func (a anyPB) EvaluateContext(_ context.Context, _ protoreflect.Message, val protoreflect.Value, cfg *validationConfig) error {
typeURL := val.Message().Get(a.TypeURLDescriptor).String()

err := &ValidationError{}
Expand Down
15 changes: 12 additions & 3 deletions cel.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package protovalidate

import (
"context"
"errors"

"google.golang.org/protobuf/reflect/protoreflect"
Expand All @@ -26,8 +27,12 @@ type celPrograms struct {
programSet
}

func (c celPrograms) Evaluate(_ protoreflect.Message, val protoreflect.Value, cfg *validationConfig) error {
err := c.Eval(val, c.Descriptor, cfg)
func (c celPrograms) Evaluate(msg protoreflect.Message, val protoreflect.Value, cfg *validationConfig) error {
return c.EvaluateContext(context.Background(), msg, val, cfg)
}

func (c celPrograms) EvaluateContext(ctx context.Context, _ protoreflect.Message, val protoreflect.Value, cfg *validationConfig) error {
err := c.EvalContext(ctx, val, c.Descriptor, cfg)
if err != nil {
var valErr *ValidationError
if errors.As(err, &valErr) {
Expand All @@ -43,7 +48,11 @@ func (c celPrograms) Evaluate(_ protoreflect.Message, val protoreflect.Value, cf
}

func (c celPrograms) EvaluateMessage(msg protoreflect.Message, cfg *validationConfig) error {
return c.Eval(protoreflect.ValueOfMessage(msg), nil, cfg)
return c.EvaluateMessageContext(context.Background(), msg, cfg)
}

func (c celPrograms) EvaluateMessageContext(ctx context.Context, msg protoreflect.Message, cfg *validationConfig) error {
return c.EvalContext(ctx, protoreflect.ValueOfMessage(msg), nil, cfg)
}

func (c celPrograms) Tautology() bool {
Expand Down
48 changes: 43 additions & 5 deletions cel/library.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,48 @@ import (
//
// Using this function, you can create a CEL environment that is identical to
// the one used to evaluate protovalidate CEL expressions.
func NewLibrary() cel.Library {
return &library{
// Options may be passed to configure the library; see [LibraryOption].
func NewLibrary(options ...LibraryOption) cel.Library {
lib := &library{
interruptCheckFrequency: DefaultInterruptCheckFrequency,
uniqueScalarPool: sync.Pool{New: func() any {
return map[ref.Val]struct{}{}
}},
uniqueBytesPool: sync.Pool{New: func() any {
return map[string]struct{}{}
}},
}
for _, option := range options {
option(lib)
}
return lib
}

// DefaultInterruptCheckFrequency is the default value for
// [WithInterruptCheckFrequency].
//
// cel-go checks for context cancellation only inside comprehension loops
// (macros such as all, exists, map and filter), once per iteration. The
// frequency selects how many of those iterations pass between successive polls
// of ctx.Done(): the per-iteration bookkeeping happens regardless, and the
// frequency only gates the (very cheap) non-blocking channel poll. Raising it
// therefore buys no measurable throughput, while making cancellation latency
// proportionally worse. 1 is the responsive default.
const DefaultInterruptCheckFrequency = 1

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should remove this constant and interrupt checks should be disabled by default (matching CEL's defaults). To this point no users of protovalidate-go have requested this feature and shouldn't require this additional overhead by default. We can always re-evaluate the default later.


// LibraryOption configures the CEL library returned by [NewLibrary].
type LibraryOption func(*library)

// WithInterruptCheckFrequency sets how many comprehension iterations elapse
// between checks for context cancellation during CEL evaluation.
//
// A value of 0 disables interrupt checking entirely: CEL expressions then run
// to completion and cannot be cancelled mid-evaluation.
//
// Note that expressions containing no comprehension are never interruptible,
// whatever the frequency, because cel-go only checks inside comprehension loops.
func WithInterruptCheckFrequency(frequency uint) LibraryOption {
return func(lib *library) { lib.interruptCheckFrequency = frequency }
}

// library is the collection of functions and settings required by protovalidate
Expand All @@ -59,8 +92,9 @@ func NewLibrary() cel.Library {
// All implementations of protovalidate MUST implement these functions and
// should avoid exposing additional functions as they will not be portable.
type library struct {
uniqueScalarPool sync.Pool
uniqueBytesPool sync.Pool
uniqueScalarPool sync.Pool
uniqueBytesPool sync.Pool
interruptCheckFrequency uint
}

func (l *library) CompileOptions() []cel.EnvOption { //nolint:funlen,gocyclo
Expand Down Expand Up @@ -378,11 +412,15 @@ func (l *library) CompileOptions() []cel.EnvOption { //nolint:funlen,gocyclo
}

func (l *library) ProgramOptions() []cel.ProgramOption {
return []cel.ProgramOption{
opts := []cel.ProgramOption{
cel.EvalOptions(
cel.OptOptimize,
),
}
if l.interruptCheckFrequency > 0 {
opts = append(opts, cel.InterruptCheckFrequency(l.interruptCheckFrequency))
}
return opts
}

func (l *library) uniqueMemberOverload(itemType *cel.Type, overload func(lister traits.Lister) ref.Val) cel.FunctionOpt {
Expand Down
8 changes: 7 additions & 1 deletion enum.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
package protovalidate

import (
"context"

"buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protoreflect"
Expand Down Expand Up @@ -42,7 +44,11 @@ type definedEnum struct {
ValueDescriptors protoreflect.EnumValueDescriptors
}

func (d definedEnum) Evaluate(_ protoreflect.Message, val protoreflect.Value, _ *validationConfig) error {
func (d definedEnum) Evaluate(msg protoreflect.Message, val protoreflect.Value, cfg *validationConfig) error {
return d.EvaluateContext(context.Background(), msg, val, cfg)
}

func (d definedEnum) EvaluateContext(_ context.Context, _ protoreflect.Message, val protoreflect.Value, _ *validationConfig) error {
if d.ValueDescriptors.ByNumber(val.Enum()) == nil {
return &ValidationError{Violations: []*Violation{{
Proto: validate.Violation_builder{
Expand Down
25 changes: 21 additions & 4 deletions evaluator.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
package protovalidate

import (
"context"

"google.golang.org/protobuf/reflect/protoreflect"
)

Expand All @@ -36,6 +38,9 @@ type evaluator interface {
// build. This error is not recoverable.
//
Evaluate(msg protoreflect.Message, val protoreflect.Value, cfg *validationConfig) error

// EvaluateContext is like Evaluate, but honors context cancellation.
EvaluateContext(ctx context.Context, msg protoreflect.Message, val protoreflect.Value, cfg *validationConfig) error

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of overloads on the evaluator (internal interface) all the way down we should just update the signature for Evaluate to take a ctx context.Context.

}

// messageEvaluator is essentially the same as evaluator, but specialized for
Expand All @@ -46,17 +51,25 @@ type messageEvaluator interface {
// EvaluateMessage checks that the provided msg is valid. See
// evaluator.Evaluate for behavior
EvaluateMessage(msg protoreflect.Message, cfg *validationConfig) error

// EvaluateMessageContext is like EvaluateMessage, but honors context
// cancellation.
EvaluateMessageContext(ctx context.Context, msg protoreflect.Message, cfg *validationConfig) error

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here for EvaluateMessage.

}

// evaluators are a set of evaluator applied together to a value. Evaluation
// merges all errors.ValidationError violations or short-circuits if failFast is
// true or a different error is returned.
type evaluators []evaluator

func (e evaluators) Evaluate(msg protoreflect.Message, val protoreflect.Value, cfg *validationConfig) (err error) {
func (e evaluators) Evaluate(msg protoreflect.Message, val protoreflect.Value, cfg *validationConfig) error {
return e.EvaluateContext(context.Background(), msg, val, cfg)
}

func (e evaluators) EvaluateContext(ctx context.Context, msg protoreflect.Message, val protoreflect.Value, cfg *validationConfig) (err error) {
var ok bool
for _, eval := range e {
evalErr := eval.Evaluate(msg, val, cfg)
evalErr := eval.EvaluateContext(ctx, msg, val, cfg)
if ok, err = mergeViolations(err, evalErr, cfg); !ok {
return err
}
Expand All @@ -81,10 +94,14 @@ func (m messageEvaluators) Evaluate(val protoreflect.Value, cfg *validationConfi
return m.EvaluateMessage(val.Message(), cfg)
}

func (m messageEvaluators) EvaluateMessage(msg protoreflect.Message, cfg *validationConfig) (err error) {
func (m messageEvaluators) EvaluateMessage(msg protoreflect.Message, cfg *validationConfig) error {
return m.EvaluateMessageContext(context.Background(), msg, cfg)
}

func (m messageEvaluators) EvaluateMessageContext(ctx context.Context, msg protoreflect.Message, cfg *validationConfig) (err error) {
var ok bool
for _, eval := range m {
evalErr := eval.EvaluateMessage(msg, cfg)
evalErr := eval.EvaluateMessageContext(ctx, msg, cfg)
if ok, err = mergeViolations(err, evalErr, cfg); !ok {
return err
}
Expand Down
18 changes: 14 additions & 4 deletions field.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@
package protovalidate

import (
"context"

"buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protoreflect"
Expand Down Expand Up @@ -60,11 +62,19 @@ func (f field) shouldIgnoreEmpty() bool {
return f.HasPresence || f.Ignore == validate.Ignore_IGNORE_IF_ZERO_VALUE
}

func (f field) Evaluate(_ protoreflect.Message, val protoreflect.Value, cfg *validationConfig) error {
return f.EvaluateMessage(val.Message(), cfg)
func (f field) Evaluate(msg protoreflect.Message, val protoreflect.Value, cfg *validationConfig) error {
return f.EvaluateContext(context.Background(), msg, val, cfg)
}

func (f field) EvaluateContext(ctx context.Context, _ protoreflect.Message, val protoreflect.Value, cfg *validationConfig) error {
return f.EvaluateMessageContext(ctx, val.Message(), cfg)
}

func (f field) EvaluateMessage(msg protoreflect.Message, cfg *validationConfig) error {
return f.EvaluateMessageContext(context.Background(), msg, cfg)
}

func (f field) EvaluateMessage(msg protoreflect.Message, cfg *validationConfig) (err error) {
func (f field) EvaluateMessageContext(ctx context.Context, msg protoreflect.Message, cfg *validationConfig) (err error) {
if f.shouldIgnoreAlways() {
return nil
}
Expand Down Expand Up @@ -95,7 +105,7 @@ func (f field) EvaluateMessage(msg protoreflect.Message, cfg *validationConfig)
return nil
}

return f.Value.EvaluateField(msg, msg.Get(f.Value.Descriptor), cfg, true)
return f.Value.EvaluateFieldContext(ctx, msg, msg.Get(f.Value.Descriptor), cfg, true)
}

func (f field) Tautology() bool {
Expand Down
Loading