From 0686058c278d9909b3693579b408c41104875e39 Mon Sep 17 00:00:00 2001 From: Tim Foerster Date: Wed, 1 Jul 2026 13:12:48 +0200 Subject: [PATCH] Add ValidateContext for cancellation and timeout support Introduce a context-aware ValidateContext(ctx, msg, options...) alongside the existing Validate(), so callers can bound long or expensive validation with a context deadline or cancellation. The motivating case is complex validation work such as CEL rules over large collections. Content: - Add a ContextValidator interface (Validator plus ValidateContext). New now returns ContextValidator, so New() callers get ValidateContext directly while still satisfying Validator. The change is backward compatible: the Validator interface is unchanged, so external implementations of it keep compiling. - Add the package-level ValidateContext helper and implement ValidateContext on the global validator. - Thread context through every evaluator via parallel *Context methods. - Evaluate CEL through cel-go's ContextEval and enable InterruptCheckFrequency so a long-running expression can be interrupted mid-evaluation. - Check ctx.Err() at message entry and in the repeated/map iteration loops so native validation of large messages also honors cancellation. - Return the raw context error (context.Canceled / DeadlineExceeded), never wrapped, so callers can match it with errors.Is. Keeping Validate() free of overhead: A context that can never be cancelled (context.Background) has a nil Done channel. ValidateContext detects this once per call and selects one of two validation configs built at New(); evaluation then skips every per-node ctx.Err() check and calls cel-go's Eval rather than ContextEval, whose activation wrapping would otherwise be paid on every expression. Validate() therefore takes the same path it did before context support existed, and neither entry point allocates to carry the flag. Choosing the interrupt check frequency: cel-go only checks for cancellation inside comprehension loops (all, exists, map, filter), once per iteration, and never for expressions without a comprehension. The frequency does not skip that per-iteration bookkeeping; it only gates a cheap non-blocking poll of ctx.Done(). Raising it therefore buys no measurable throughput while making cancellation latency proportionally worse: on the BenchCrossReference fixture a 50ms deadline is honored in 50ms at frequency 1, but takes 2.2s at frequency 100. The default is therefore 1, and WithCELInterruptCheckFrequency lets callers tune or disable it (0). Benchmarks: - BenchmarkContextOverhead compares Validate, ValidateContext with a background context, and ValidateContext with a cancellable context. - BenchmarkInterruptCheckFrequency shows throughput is flat across frequencies. - BenchCrossReference is a new message whose CEL rule costs O(n^3), used to test that an expensive expression is actually interrupted by a deadline. --- README.md | 8 + any.go | 8 +- cel.go | 15 +- cel/library.go | 48 ++- enum.go | 8 +- evaluator.go | 25 +- field.go | 18 +- internal/gen/tests/example/v1/bench.pb.go | 341 +++++++++++++++-- .../tests/example/v1/bench_protoopaque.pb.go | 347 ++++++++++++++++-- map.go | 21 +- message.go | 42 ++- message_oneof.go | 13 +- native_bool.go | 7 +- native_bytes.go | 7 +- native_enum.go | 7 +- native_map.go | 12 +- native_numeric.go | 7 +- native_repeated.go | 12 +- native_string.go | 7 +- oneof.go | 14 +- option.go | 24 ++ program.go | 49 ++- program_test.go | 56 +++ proto/tests/example/v1/bench.proto | 33 ++ repeated.go | 13 +- validator.go | 136 +++++-- validator_bench_test.go | 110 ++++++ validator_test.go | 141 +++++++ value.go | 19 +- wrapper.go | 12 +- 30 files changed, 1435 insertions(+), 125 deletions(-) diff --git a/README.md b/README.md index d014445..8dd32ec 100644 --- a/README.md +++ b/README.md @@ -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] diff --git a/any.go b/any.go index ead36b7..7d93982 100644 --- a/any.go +++ b/any.go @@ -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" @@ -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{} diff --git a/cel.go b/cel.go index f643a82..7f12429 100644 --- a/cel.go +++ b/cel.go @@ -15,6 +15,7 @@ package protovalidate import ( + "context" "errors" "google.golang.org/protobuf/reflect/protoreflect" @@ -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) { @@ -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 { diff --git a/cel/library.go b/cel/library.go index b9f262e..4f19b1b 100644 --- a/cel/library.go +++ b/cel/library.go @@ -40,8 +40,10 @@ 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{}{} }}, @@ -49,6 +51,37 @@ func NewLibrary() cel.Library { 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 + +// 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 @@ -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 @@ -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 { diff --git a/enum.go b/enum.go index e29f931..01cbfc9 100644 --- a/enum.go +++ b/enum.go @@ -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" @@ -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{ diff --git a/evaluator.go b/evaluator.go index e6bbe6d..df52511 100644 --- a/evaluator.go +++ b/evaluator.go @@ -15,6 +15,8 @@ package protovalidate import ( + "context" + "google.golang.org/protobuf/reflect/protoreflect" ) @@ -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 } // messageEvaluator is essentially the same as evaluator, but specialized for @@ -46,6 +51,10 @@ 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 } // evaluators are a set of evaluator applied together to a value. Evaluation @@ -53,10 +62,14 @@ type messageEvaluator interface { // 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 } @@ -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 } diff --git a/field.go b/field.go index 9c2c5b7..c15bcc5 100644 --- a/field.go +++ b/field.go @@ -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" @@ -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 } @@ -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 { diff --git a/internal/gen/tests/example/v1/bench.pb.go b/internal/gen/tests/example/v1/bench.pb.go index 28bbb56..6fdf5c5 100644 --- a/internal/gen/tests/example/v1/bench.pb.go +++ b/internal/gen/tests/example/v1/bench.pb.go @@ -1096,6 +1096,280 @@ func (*BenchComplexSchema_OneofI32) isBenchComplexSchema_Choice() {} func (*BenchComplexSchema_OneofMsg) isBenchComplexSchema_Choice() {} +// BenchCrossReference models a message whose CEL rule enforces referential +// integrity: every reference held by every rule must resolve to a declared +// entry. Evaluating it costs O(rules * refs_per_rule * entries), so its +// validation time grows multiplicatively with the size of the message. +// +// It exists to exercise cancellation of a single long-running CEL expression. +// It is not part of the faked benchmark corpus; tests build it explicitly. +type BenchCrossReference struct { + state protoimpl.MessageState `protogen:"hybrid.v1"` + Entries []*BenchCrossReference_Entry `protobuf:"bytes,1,rep,name=entries,proto3" json:"entries,omitempty"` + Rules []*BenchCrossReference_Rule `protobuf:"bytes,2,rep,name=rules,proto3" json:"rules,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BenchCrossReference) Reset() { + *x = BenchCrossReference{} + mi := &file_tests_example_v1_bench_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BenchCrossReference) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BenchCrossReference) ProtoMessage() {} + +func (x *BenchCrossReference) ProtoReflect() protoreflect.Message { + mi := &file_tests_example_v1_bench_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +func (x *BenchCrossReference) GetEntries() []*BenchCrossReference_Entry { + if x != nil { + return x.Entries + } + return nil +} + +func (x *BenchCrossReference) GetRules() []*BenchCrossReference_Rule { + if x != nil { + return x.Rules + } + return nil +} + +func (x *BenchCrossReference) SetEntries(v []*BenchCrossReference_Entry) { + x.Entries = v +} + +func (x *BenchCrossReference) SetRules(v []*BenchCrossReference_Rule) { + x.Rules = v +} + +type BenchCrossReference_builder struct { + _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. + + Entries []*BenchCrossReference_Entry + Rules []*BenchCrossReference_Rule +} + +func (b0 BenchCrossReference_builder) Build() *BenchCrossReference { + m0 := &BenchCrossReference{} + b, x := &b0, m0 + _, _ = b, x + x.Entries = b.Entries + x.Rules = b.Rules + return m0 +} + +type BenchCrossReference_Key struct { + state protoimpl.MessageState `protogen:"hybrid.v1"` + Kind string `protobuf:"bytes,1,opt,name=kind,proto3" json:"kind,omitempty"` + Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BenchCrossReference_Key) Reset() { + *x = BenchCrossReference_Key{} + mi := &file_tests_example_v1_bench_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BenchCrossReference_Key) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BenchCrossReference_Key) ProtoMessage() {} + +func (x *BenchCrossReference_Key) ProtoReflect() protoreflect.Message { + mi := &file_tests_example_v1_bench_proto_msgTypes[15] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +func (x *BenchCrossReference_Key) GetKind() string { + if x != nil { + return x.Kind + } + return "" +} + +func (x *BenchCrossReference_Key) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *BenchCrossReference_Key) SetKind(v string) { + x.Kind = v +} + +func (x *BenchCrossReference_Key) SetName(v string) { + x.Name = v +} + +type BenchCrossReference_Key_builder struct { + _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. + + Kind string + Name string +} + +func (b0 BenchCrossReference_Key_builder) Build() *BenchCrossReference_Key { + m0 := &BenchCrossReference_Key{} + b, x := &b0, m0 + _, _ = b, x + x.Kind = b.Kind + x.Name = b.Name + return m0 +} + +type BenchCrossReference_Entry struct { + state protoimpl.MessageState `protogen:"hybrid.v1"` + Key *BenchCrossReference_Key `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BenchCrossReference_Entry) Reset() { + *x = BenchCrossReference_Entry{} + mi := &file_tests_example_v1_bench_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BenchCrossReference_Entry) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BenchCrossReference_Entry) ProtoMessage() {} + +func (x *BenchCrossReference_Entry) ProtoReflect() protoreflect.Message { + mi := &file_tests_example_v1_bench_proto_msgTypes[16] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +func (x *BenchCrossReference_Entry) GetKey() *BenchCrossReference_Key { + if x != nil { + return x.Key + } + return nil +} + +func (x *BenchCrossReference_Entry) SetKey(v *BenchCrossReference_Key) { + x.Key = v +} + +func (x *BenchCrossReference_Entry) HasKey() bool { + if x == nil { + return false + } + return x.Key != nil +} + +func (x *BenchCrossReference_Entry) ClearKey() { + x.Key = nil +} + +type BenchCrossReference_Entry_builder struct { + _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. + + Key *BenchCrossReference_Key +} + +func (b0 BenchCrossReference_Entry_builder) Build() *BenchCrossReference_Entry { + m0 := &BenchCrossReference_Entry{} + b, x := &b0, m0 + _, _ = b, x + x.Key = b.Key + return m0 +} + +type BenchCrossReference_Rule struct { + state protoimpl.MessageState `protogen:"hybrid.v1"` + Refs []*BenchCrossReference_Key `protobuf:"bytes,1,rep,name=refs,proto3" json:"refs,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BenchCrossReference_Rule) Reset() { + *x = BenchCrossReference_Rule{} + mi := &file_tests_example_v1_bench_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BenchCrossReference_Rule) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BenchCrossReference_Rule) ProtoMessage() {} + +func (x *BenchCrossReference_Rule) ProtoReflect() protoreflect.Message { + mi := &file_tests_example_v1_bench_proto_msgTypes[17] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +func (x *BenchCrossReference_Rule) GetRefs() []*BenchCrossReference_Key { + if x != nil { + return x.Refs + } + return nil +} + +func (x *BenchCrossReference_Rule) SetRefs(v []*BenchCrossReference_Key) { + x.Refs = v +} + +type BenchCrossReference_Rule_builder struct { + _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. + + Refs []*BenchCrossReference_Key +} + +func (b0 BenchCrossReference_Rule_builder) Build() *BenchCrossReference_Rule { + m0 := &BenchCrossReference_Rule{} + b, x := &b0, m0 + _, _ = b, x + x.Refs = b.Refs + return m0 +} + var File_tests_example_v1_bench_proto protoreflect.FileDescriptor const file_tests_example_v1_bench_proto_rawDesc = "" + @@ -1180,7 +1454,18 @@ const file_tests_example_v1_bench_proto_rawDesc = "" + "\x0eMapI64MsgEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\x03R\x03key\x123\n" + "\x05value\x18\x02 \x01(\v2\x1d.tests.example.v1.BenchScalarR\x05value:\x028\x01B\b\n" + - "\x06choice*O\n" + + "\x06choice\"\xa8\x04\n" + + "\x13BenchCrossReference\x12E\n" + + "\aentries\x18\x01 \x03(\v2+.tests.example.v1.BenchCrossReference.EntryR\aentries\x12@\n" + + "\x05rules\x18\x02 \x03(\v2*.tests.example.v1.BenchCrossReference.RuleR\x05rules\x1a-\n" + + "\x03Key\x12\x12\n" + + "\x04kind\x18\x01 \x01(\tR\x04kind\x12\x12\n" + + "\x04name\x18\x02 \x01(\tR\x04name\x1aD\n" + + "\x05Entry\x12;\n" + + "\x03key\x18\x01 \x01(\v2).tests.example.v1.BenchCrossReference.KeyR\x03key\x1aE\n" + + "\x04Rule\x12=\n" + + "\x04refs\x18\x01 \x03(\v2).tests.example.v1.BenchCrossReference.KeyR\x04refs:\xcb\x01\xbaH\xc7\x01\x1a\xc4\x01\n" + + "\frefs_resolve\x125every rule reference must resolve to a declared entry\x1a}this.rules.all(rule, rule.refs.all(ref,this.entries.exists(entry, entry.key.kind == ref.kind && entry.key.name == ref.name)))*O\n" + "\tBenchEnum\x12\x1a\n" + "\x16BENCH_ENUM_UNSPECIFIED\x10\x00\x12\x12\n" + "\x0eBENCH_ENUM_ONE\x10\x01\x12\x12\n" + @@ -1189,7 +1474,7 @@ const file_tests_example_v1_bench_proto_rawDesc = "" + "BenchProtoP\x01ZBbuf.build/go/protovalidate/internal/gen/tests/example/v1;examplev1\xa2\x02\x03TEX\xaa\x02\x10Tests.Example.V1\xca\x02\x10Tests\\Example\\V1\xe2\x02\x1cTests\\Example\\V1\\GPBMetadata\xea\x02\x12Tests::Example::V1b\x06proto3" var file_tests_example_v1_bench_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_tests_example_v1_bench_proto_msgTypes = make([]protoimpl.MessageInfo, 14) +var file_tests_example_v1_bench_proto_msgTypes = make([]protoimpl.MessageInfo, 18) var file_tests_example_v1_bench_proto_goTypes = []any{ (BenchEnum)(0), // 0: tests.example.v1.BenchEnum (*BenchScalar)(nil), // 1: tests.example.v1.BenchScalar @@ -1199,35 +1484,43 @@ var file_tests_example_v1_bench_proto_goTypes = []any{ (*BenchRepeatedBytesUnique)(nil), // 5: tests.example.v1.BenchRepeatedBytesUnique (*BenchMap)(nil), // 6: tests.example.v1.BenchMap (*BenchComplexSchema)(nil), // 7: tests.example.v1.BenchComplexSchema - nil, // 8: tests.example.v1.BenchMap.EntriesEntry - nil, // 9: tests.example.v1.BenchComplexSchema.MapStrStrEntry - nil, // 10: tests.example.v1.BenchComplexSchema.MapI32I64Entry - nil, // 11: tests.example.v1.BenchComplexSchema.MapU64BoolEntry - nil, // 12: tests.example.v1.BenchComplexSchema.MapStrBytesEntry - nil, // 13: tests.example.v1.BenchComplexSchema.MapStrMsgEntry - nil, // 14: tests.example.v1.BenchComplexSchema.MapI64MsgEntry + (*BenchCrossReference)(nil), // 8: tests.example.v1.BenchCrossReference + nil, // 9: tests.example.v1.BenchMap.EntriesEntry + nil, // 10: tests.example.v1.BenchComplexSchema.MapStrStrEntry + nil, // 11: tests.example.v1.BenchComplexSchema.MapI32I64Entry + nil, // 12: tests.example.v1.BenchComplexSchema.MapU64BoolEntry + nil, // 13: tests.example.v1.BenchComplexSchema.MapStrBytesEntry + nil, // 14: tests.example.v1.BenchComplexSchema.MapStrMsgEntry + nil, // 15: tests.example.v1.BenchComplexSchema.MapI64MsgEntry + (*BenchCrossReference_Key)(nil), // 16: tests.example.v1.BenchCrossReference.Key + (*BenchCrossReference_Entry)(nil), // 17: tests.example.v1.BenchCrossReference.Entry + (*BenchCrossReference_Rule)(nil), // 18: tests.example.v1.BenchCrossReference.Rule } var file_tests_example_v1_bench_proto_depIdxs = []int32{ 1, // 0: tests.example.v1.BenchRepeatedMessage.x:type_name -> tests.example.v1.BenchScalar - 8, // 1: tests.example.v1.BenchMap.entries:type_name -> tests.example.v1.BenchMap.EntriesEntry + 9, // 1: tests.example.v1.BenchMap.entries:type_name -> tests.example.v1.BenchMap.EntriesEntry 1, // 2: tests.example.v1.BenchComplexSchema.nested:type_name -> tests.example.v1.BenchScalar 7, // 3: tests.example.v1.BenchComplexSchema.self_ref:type_name -> tests.example.v1.BenchComplexSchema 1, // 4: tests.example.v1.BenchComplexSchema.rep_msg:type_name -> tests.example.v1.BenchScalar - 9, // 5: tests.example.v1.BenchComplexSchema.map_str_str:type_name -> tests.example.v1.BenchComplexSchema.MapStrStrEntry - 10, // 6: tests.example.v1.BenchComplexSchema.map_i32_i64:type_name -> tests.example.v1.BenchComplexSchema.MapI32I64Entry - 11, // 7: tests.example.v1.BenchComplexSchema.map_u64_bool:type_name -> tests.example.v1.BenchComplexSchema.MapU64BoolEntry - 12, // 8: tests.example.v1.BenchComplexSchema.map_str_bytes:type_name -> tests.example.v1.BenchComplexSchema.MapStrBytesEntry - 13, // 9: tests.example.v1.BenchComplexSchema.map_str_msg:type_name -> tests.example.v1.BenchComplexSchema.MapStrMsgEntry - 14, // 10: tests.example.v1.BenchComplexSchema.map_i64_msg:type_name -> tests.example.v1.BenchComplexSchema.MapI64MsgEntry + 10, // 5: tests.example.v1.BenchComplexSchema.map_str_str:type_name -> tests.example.v1.BenchComplexSchema.MapStrStrEntry + 11, // 6: tests.example.v1.BenchComplexSchema.map_i32_i64:type_name -> tests.example.v1.BenchComplexSchema.MapI32I64Entry + 12, // 7: tests.example.v1.BenchComplexSchema.map_u64_bool:type_name -> tests.example.v1.BenchComplexSchema.MapU64BoolEntry + 13, // 8: tests.example.v1.BenchComplexSchema.map_str_bytes:type_name -> tests.example.v1.BenchComplexSchema.MapStrBytesEntry + 14, // 9: tests.example.v1.BenchComplexSchema.map_str_msg:type_name -> tests.example.v1.BenchComplexSchema.MapStrMsgEntry + 15, // 10: tests.example.v1.BenchComplexSchema.map_i64_msg:type_name -> tests.example.v1.BenchComplexSchema.MapI64MsgEntry 0, // 11: tests.example.v1.BenchComplexSchema.enum_field:type_name -> tests.example.v1.BenchEnum 1, // 12: tests.example.v1.BenchComplexSchema.oneof_msg:type_name -> tests.example.v1.BenchScalar - 1, // 13: tests.example.v1.BenchComplexSchema.MapStrMsgEntry.value:type_name -> tests.example.v1.BenchScalar - 1, // 14: tests.example.v1.BenchComplexSchema.MapI64MsgEntry.value:type_name -> tests.example.v1.BenchScalar - 15, // [15:15] is the sub-list for method output_type - 15, // [15:15] is the sub-list for method input_type - 15, // [15:15] is the sub-list for extension type_name - 15, // [15:15] is the sub-list for extension extendee - 0, // [0:15] is the sub-list for field type_name + 17, // 13: tests.example.v1.BenchCrossReference.entries:type_name -> tests.example.v1.BenchCrossReference.Entry + 18, // 14: tests.example.v1.BenchCrossReference.rules:type_name -> tests.example.v1.BenchCrossReference.Rule + 1, // 15: tests.example.v1.BenchComplexSchema.MapStrMsgEntry.value:type_name -> tests.example.v1.BenchScalar + 1, // 16: tests.example.v1.BenchComplexSchema.MapI64MsgEntry.value:type_name -> tests.example.v1.BenchScalar + 16, // 17: tests.example.v1.BenchCrossReference.Entry.key:type_name -> tests.example.v1.BenchCrossReference.Key + 16, // 18: tests.example.v1.BenchCrossReference.Rule.refs:type_name -> tests.example.v1.BenchCrossReference.Key + 19, // [19:19] is the sub-list for method output_type + 19, // [19:19] is the sub-list for method input_type + 19, // [19:19] is the sub-list for extension type_name + 19, // [19:19] is the sub-list for extension extendee + 0, // [0:19] is the sub-list for field type_name } func init() { file_tests_example_v1_bench_proto_init() } @@ -1246,7 +1539,7 @@ func file_tests_example_v1_bench_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_tests_example_v1_bench_proto_rawDesc), len(file_tests_example_v1_bench_proto_rawDesc)), NumEnums: 1, - NumMessages: 14, + NumMessages: 18, NumExtensions: 0, NumServices: 0, }, diff --git a/internal/gen/tests/example/v1/bench_protoopaque.pb.go b/internal/gen/tests/example/v1/bench_protoopaque.pb.go index f90bc1f..116c31f 100644 --- a/internal/gen/tests/example/v1/bench_protoopaque.pb.go +++ b/internal/gen/tests/example/v1/bench_protoopaque.pb.go @@ -1078,6 +1078,286 @@ func (*benchComplexSchema_OneofI32) isBenchComplexSchema_Choice() {} func (*benchComplexSchema_OneofMsg) isBenchComplexSchema_Choice() {} +// BenchCrossReference models a message whose CEL rule enforces referential +// integrity: every reference held by every rule must resolve to a declared +// entry. Evaluating it costs O(rules * refs_per_rule * entries), so its +// validation time grows multiplicatively with the size of the message. +// +// It exists to exercise cancellation of a single long-running CEL expression. +// It is not part of the faked benchmark corpus; tests build it explicitly. +type BenchCrossReference struct { + state protoimpl.MessageState `protogen:"opaque.v1"` + xxx_hidden_Entries *[]*BenchCrossReference_Entry `protobuf:"bytes,1,rep,name=entries,proto3"` + xxx_hidden_Rules *[]*BenchCrossReference_Rule `protobuf:"bytes,2,rep,name=rules,proto3"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BenchCrossReference) Reset() { + *x = BenchCrossReference{} + mi := &file_tests_example_v1_bench_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BenchCrossReference) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BenchCrossReference) ProtoMessage() {} + +func (x *BenchCrossReference) ProtoReflect() protoreflect.Message { + mi := &file_tests_example_v1_bench_proto_msgTypes[7] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +func (x *BenchCrossReference) GetEntries() []*BenchCrossReference_Entry { + if x != nil { + if x.xxx_hidden_Entries != nil { + return *x.xxx_hidden_Entries + } + } + return nil +} + +func (x *BenchCrossReference) GetRules() []*BenchCrossReference_Rule { + if x != nil { + if x.xxx_hidden_Rules != nil { + return *x.xxx_hidden_Rules + } + } + return nil +} + +func (x *BenchCrossReference) SetEntries(v []*BenchCrossReference_Entry) { + x.xxx_hidden_Entries = &v +} + +func (x *BenchCrossReference) SetRules(v []*BenchCrossReference_Rule) { + x.xxx_hidden_Rules = &v +} + +type BenchCrossReference_builder struct { + _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. + + Entries []*BenchCrossReference_Entry + Rules []*BenchCrossReference_Rule +} + +func (b0 BenchCrossReference_builder) Build() *BenchCrossReference { + m0 := &BenchCrossReference{} + b, x := &b0, m0 + _, _ = b, x + x.xxx_hidden_Entries = &b.Entries + x.xxx_hidden_Rules = &b.Rules + return m0 +} + +type BenchCrossReference_Key struct { + state protoimpl.MessageState `protogen:"opaque.v1"` + xxx_hidden_Kind string `protobuf:"bytes,1,opt,name=kind,proto3"` + xxx_hidden_Name string `protobuf:"bytes,2,opt,name=name,proto3"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BenchCrossReference_Key) Reset() { + *x = BenchCrossReference_Key{} + mi := &file_tests_example_v1_bench_proto_msgTypes[15] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BenchCrossReference_Key) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BenchCrossReference_Key) ProtoMessage() {} + +func (x *BenchCrossReference_Key) ProtoReflect() protoreflect.Message { + mi := &file_tests_example_v1_bench_proto_msgTypes[15] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +func (x *BenchCrossReference_Key) GetKind() string { + if x != nil { + return x.xxx_hidden_Kind + } + return "" +} + +func (x *BenchCrossReference_Key) GetName() string { + if x != nil { + return x.xxx_hidden_Name + } + return "" +} + +func (x *BenchCrossReference_Key) SetKind(v string) { + x.xxx_hidden_Kind = v +} + +func (x *BenchCrossReference_Key) SetName(v string) { + x.xxx_hidden_Name = v +} + +type BenchCrossReference_Key_builder struct { + _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. + + Kind string + Name string +} + +func (b0 BenchCrossReference_Key_builder) Build() *BenchCrossReference_Key { + m0 := &BenchCrossReference_Key{} + b, x := &b0, m0 + _, _ = b, x + x.xxx_hidden_Kind = b.Kind + x.xxx_hidden_Name = b.Name + return m0 +} + +type BenchCrossReference_Entry struct { + state protoimpl.MessageState `protogen:"opaque.v1"` + xxx_hidden_Key *BenchCrossReference_Key `protobuf:"bytes,1,opt,name=key,proto3"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BenchCrossReference_Entry) Reset() { + *x = BenchCrossReference_Entry{} + mi := &file_tests_example_v1_bench_proto_msgTypes[16] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BenchCrossReference_Entry) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BenchCrossReference_Entry) ProtoMessage() {} + +func (x *BenchCrossReference_Entry) ProtoReflect() protoreflect.Message { + mi := &file_tests_example_v1_bench_proto_msgTypes[16] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +func (x *BenchCrossReference_Entry) GetKey() *BenchCrossReference_Key { + if x != nil { + return x.xxx_hidden_Key + } + return nil +} + +func (x *BenchCrossReference_Entry) SetKey(v *BenchCrossReference_Key) { + x.xxx_hidden_Key = v +} + +func (x *BenchCrossReference_Entry) HasKey() bool { + if x == nil { + return false + } + return x.xxx_hidden_Key != nil +} + +func (x *BenchCrossReference_Entry) ClearKey() { + x.xxx_hidden_Key = nil +} + +type BenchCrossReference_Entry_builder struct { + _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. + + Key *BenchCrossReference_Key +} + +func (b0 BenchCrossReference_Entry_builder) Build() *BenchCrossReference_Entry { + m0 := &BenchCrossReference_Entry{} + b, x := &b0, m0 + _, _ = b, x + x.xxx_hidden_Key = b.Key + return m0 +} + +type BenchCrossReference_Rule struct { + state protoimpl.MessageState `protogen:"opaque.v1"` + xxx_hidden_Refs *[]*BenchCrossReference_Key `protobuf:"bytes,1,rep,name=refs,proto3"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *BenchCrossReference_Rule) Reset() { + *x = BenchCrossReference_Rule{} + mi := &file_tests_example_v1_bench_proto_msgTypes[17] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *BenchCrossReference_Rule) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BenchCrossReference_Rule) ProtoMessage() {} + +func (x *BenchCrossReference_Rule) ProtoReflect() protoreflect.Message { + mi := &file_tests_example_v1_bench_proto_msgTypes[17] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +func (x *BenchCrossReference_Rule) GetRefs() []*BenchCrossReference_Key { + if x != nil { + if x.xxx_hidden_Refs != nil { + return *x.xxx_hidden_Refs + } + } + return nil +} + +func (x *BenchCrossReference_Rule) SetRefs(v []*BenchCrossReference_Key) { + x.xxx_hidden_Refs = &v +} + +type BenchCrossReference_Rule_builder struct { + _ [0]func() // Prevents comparability and use of unkeyed literals for the builder. + + Refs []*BenchCrossReference_Key +} + +func (b0 BenchCrossReference_Rule_builder) Build() *BenchCrossReference_Rule { + m0 := &BenchCrossReference_Rule{} + b, x := &b0, m0 + _, _ = b, x + x.xxx_hidden_Refs = &b.Refs + return m0 +} + var File_tests_example_v1_bench_proto protoreflect.FileDescriptor const file_tests_example_v1_bench_proto_rawDesc = "" + @@ -1162,7 +1442,18 @@ const file_tests_example_v1_bench_proto_rawDesc = "" + "\x0eMapI64MsgEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\x03R\x03key\x123\n" + "\x05value\x18\x02 \x01(\v2\x1d.tests.example.v1.BenchScalarR\x05value:\x028\x01B\b\n" + - "\x06choice*O\n" + + "\x06choice\"\xa8\x04\n" + + "\x13BenchCrossReference\x12E\n" + + "\aentries\x18\x01 \x03(\v2+.tests.example.v1.BenchCrossReference.EntryR\aentries\x12@\n" + + "\x05rules\x18\x02 \x03(\v2*.tests.example.v1.BenchCrossReference.RuleR\x05rules\x1a-\n" + + "\x03Key\x12\x12\n" + + "\x04kind\x18\x01 \x01(\tR\x04kind\x12\x12\n" + + "\x04name\x18\x02 \x01(\tR\x04name\x1aD\n" + + "\x05Entry\x12;\n" + + "\x03key\x18\x01 \x01(\v2).tests.example.v1.BenchCrossReference.KeyR\x03key\x1aE\n" + + "\x04Rule\x12=\n" + + "\x04refs\x18\x01 \x03(\v2).tests.example.v1.BenchCrossReference.KeyR\x04refs:\xcb\x01\xbaH\xc7\x01\x1a\xc4\x01\n" + + "\frefs_resolve\x125every rule reference must resolve to a declared entry\x1a}this.rules.all(rule, rule.refs.all(ref,this.entries.exists(entry, entry.key.kind == ref.kind && entry.key.name == ref.name)))*O\n" + "\tBenchEnum\x12\x1a\n" + "\x16BENCH_ENUM_UNSPECIFIED\x10\x00\x12\x12\n" + "\x0eBENCH_ENUM_ONE\x10\x01\x12\x12\n" + @@ -1171,7 +1462,7 @@ const file_tests_example_v1_bench_proto_rawDesc = "" + "BenchProtoP\x01ZBbuf.build/go/protovalidate/internal/gen/tests/example/v1;examplev1\xa2\x02\x03TEX\xaa\x02\x10Tests.Example.V1\xca\x02\x10Tests\\Example\\V1\xe2\x02\x1cTests\\Example\\V1\\GPBMetadata\xea\x02\x12Tests::Example::V1b\x06proto3" var file_tests_example_v1_bench_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_tests_example_v1_bench_proto_msgTypes = make([]protoimpl.MessageInfo, 14) +var file_tests_example_v1_bench_proto_msgTypes = make([]protoimpl.MessageInfo, 18) var file_tests_example_v1_bench_proto_goTypes = []any{ (BenchEnum)(0), // 0: tests.example.v1.BenchEnum (*BenchScalar)(nil), // 1: tests.example.v1.BenchScalar @@ -1181,35 +1472,43 @@ var file_tests_example_v1_bench_proto_goTypes = []any{ (*BenchRepeatedBytesUnique)(nil), // 5: tests.example.v1.BenchRepeatedBytesUnique (*BenchMap)(nil), // 6: tests.example.v1.BenchMap (*BenchComplexSchema)(nil), // 7: tests.example.v1.BenchComplexSchema - nil, // 8: tests.example.v1.BenchMap.EntriesEntry - nil, // 9: tests.example.v1.BenchComplexSchema.MapStrStrEntry - nil, // 10: tests.example.v1.BenchComplexSchema.MapI32I64Entry - nil, // 11: tests.example.v1.BenchComplexSchema.MapU64BoolEntry - nil, // 12: tests.example.v1.BenchComplexSchema.MapStrBytesEntry - nil, // 13: tests.example.v1.BenchComplexSchema.MapStrMsgEntry - nil, // 14: tests.example.v1.BenchComplexSchema.MapI64MsgEntry + (*BenchCrossReference)(nil), // 8: tests.example.v1.BenchCrossReference + nil, // 9: tests.example.v1.BenchMap.EntriesEntry + nil, // 10: tests.example.v1.BenchComplexSchema.MapStrStrEntry + nil, // 11: tests.example.v1.BenchComplexSchema.MapI32I64Entry + nil, // 12: tests.example.v1.BenchComplexSchema.MapU64BoolEntry + nil, // 13: tests.example.v1.BenchComplexSchema.MapStrBytesEntry + nil, // 14: tests.example.v1.BenchComplexSchema.MapStrMsgEntry + nil, // 15: tests.example.v1.BenchComplexSchema.MapI64MsgEntry + (*BenchCrossReference_Key)(nil), // 16: tests.example.v1.BenchCrossReference.Key + (*BenchCrossReference_Entry)(nil), // 17: tests.example.v1.BenchCrossReference.Entry + (*BenchCrossReference_Rule)(nil), // 18: tests.example.v1.BenchCrossReference.Rule } var file_tests_example_v1_bench_proto_depIdxs = []int32{ 1, // 0: tests.example.v1.BenchRepeatedMessage.x:type_name -> tests.example.v1.BenchScalar - 8, // 1: tests.example.v1.BenchMap.entries:type_name -> tests.example.v1.BenchMap.EntriesEntry + 9, // 1: tests.example.v1.BenchMap.entries:type_name -> tests.example.v1.BenchMap.EntriesEntry 1, // 2: tests.example.v1.BenchComplexSchema.nested:type_name -> tests.example.v1.BenchScalar 7, // 3: tests.example.v1.BenchComplexSchema.self_ref:type_name -> tests.example.v1.BenchComplexSchema 1, // 4: tests.example.v1.BenchComplexSchema.rep_msg:type_name -> tests.example.v1.BenchScalar - 9, // 5: tests.example.v1.BenchComplexSchema.map_str_str:type_name -> tests.example.v1.BenchComplexSchema.MapStrStrEntry - 10, // 6: tests.example.v1.BenchComplexSchema.map_i32_i64:type_name -> tests.example.v1.BenchComplexSchema.MapI32I64Entry - 11, // 7: tests.example.v1.BenchComplexSchema.map_u64_bool:type_name -> tests.example.v1.BenchComplexSchema.MapU64BoolEntry - 12, // 8: tests.example.v1.BenchComplexSchema.map_str_bytes:type_name -> tests.example.v1.BenchComplexSchema.MapStrBytesEntry - 13, // 9: tests.example.v1.BenchComplexSchema.map_str_msg:type_name -> tests.example.v1.BenchComplexSchema.MapStrMsgEntry - 14, // 10: tests.example.v1.BenchComplexSchema.map_i64_msg:type_name -> tests.example.v1.BenchComplexSchema.MapI64MsgEntry + 10, // 5: tests.example.v1.BenchComplexSchema.map_str_str:type_name -> tests.example.v1.BenchComplexSchema.MapStrStrEntry + 11, // 6: tests.example.v1.BenchComplexSchema.map_i32_i64:type_name -> tests.example.v1.BenchComplexSchema.MapI32I64Entry + 12, // 7: tests.example.v1.BenchComplexSchema.map_u64_bool:type_name -> tests.example.v1.BenchComplexSchema.MapU64BoolEntry + 13, // 8: tests.example.v1.BenchComplexSchema.map_str_bytes:type_name -> tests.example.v1.BenchComplexSchema.MapStrBytesEntry + 14, // 9: tests.example.v1.BenchComplexSchema.map_str_msg:type_name -> tests.example.v1.BenchComplexSchema.MapStrMsgEntry + 15, // 10: tests.example.v1.BenchComplexSchema.map_i64_msg:type_name -> tests.example.v1.BenchComplexSchema.MapI64MsgEntry 0, // 11: tests.example.v1.BenchComplexSchema.enum_field:type_name -> tests.example.v1.BenchEnum 1, // 12: tests.example.v1.BenchComplexSchema.oneof_msg:type_name -> tests.example.v1.BenchScalar - 1, // 13: tests.example.v1.BenchComplexSchema.MapStrMsgEntry.value:type_name -> tests.example.v1.BenchScalar - 1, // 14: tests.example.v1.BenchComplexSchema.MapI64MsgEntry.value:type_name -> tests.example.v1.BenchScalar - 15, // [15:15] is the sub-list for method output_type - 15, // [15:15] is the sub-list for method input_type - 15, // [15:15] is the sub-list for extension type_name - 15, // [15:15] is the sub-list for extension extendee - 0, // [0:15] is the sub-list for field type_name + 17, // 13: tests.example.v1.BenchCrossReference.entries:type_name -> tests.example.v1.BenchCrossReference.Entry + 18, // 14: tests.example.v1.BenchCrossReference.rules:type_name -> tests.example.v1.BenchCrossReference.Rule + 1, // 15: tests.example.v1.BenchComplexSchema.MapStrMsgEntry.value:type_name -> tests.example.v1.BenchScalar + 1, // 16: tests.example.v1.BenchComplexSchema.MapI64MsgEntry.value:type_name -> tests.example.v1.BenchScalar + 16, // 17: tests.example.v1.BenchCrossReference.Entry.key:type_name -> tests.example.v1.BenchCrossReference.Key + 16, // 18: tests.example.v1.BenchCrossReference.Rule.refs:type_name -> tests.example.v1.BenchCrossReference.Key + 19, // [19:19] is the sub-list for method output_type + 19, // [19:19] is the sub-list for method input_type + 19, // [19:19] is the sub-list for extension type_name + 19, // [19:19] is the sub-list for extension extendee + 0, // [0:19] is the sub-list for field type_name } func init() { file_tests_example_v1_bench_proto_init() } @@ -1228,7 +1527,7 @@ func file_tests_example_v1_bench_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_tests_example_v1_bench_proto_rawDesc), len(file_tests_example_v1_bench_proto_rawDesc)), NumEnums: 1, - NumMessages: 14, + NumMessages: 18, NumExtensions: 0, NumServices: 0, }, diff --git a/map.go b/map.go index 70c4ec7..2e097d2 100644 --- a/map.go +++ b/map.go @@ -15,6 +15,7 @@ package protovalidate import ( + "context" "fmt" "strconv" @@ -66,10 +67,20 @@ func newKVPairs(valEval *value) *kvPairs { } } -func (m *kvPairs) Evaluate(msg protoreflect.Message, val protoreflect.Value, cfg *validationConfig) (err error) { +func (m *kvPairs) Evaluate(msg protoreflect.Message, val protoreflect.Value, cfg *validationConfig) error { + return m.EvaluateContext(context.Background(), msg, val, cfg) +} + +func (m *kvPairs) EvaluateContext(ctx context.Context, msg protoreflect.Message, val protoreflect.Value, cfg *validationConfig) (err error) { var ok bool val.Map().Range(func(key protoreflect.MapKey, value protoreflect.Value) bool { - evalErr := m.evalPairs(msg, key, value, cfg) + if cfg.cancellable { + if ctxErr := ctx.Err(); ctxErr != nil { + err = ctxErr + return false + } + } + evalErr := m.evalPairsContext(ctx, msg, key, value, cfg) if evalErr != nil { element := validate.FieldPathElement_builder{ FieldNumber: proto.Int32(m.FieldPathElement.GetFieldNumber()), @@ -107,15 +118,15 @@ func (m *kvPairs) Evaluate(msg protoreflect.Message, val protoreflect.Value, cfg return err } -func (m *kvPairs) evalPairs(msg protoreflect.Message, key protoreflect.MapKey, value protoreflect.Value, cfg *validationConfig) (err error) { - evalErr := m.KeyRules.EvaluateField(msg, key.Value(), cfg, true) +func (m *kvPairs) evalPairsContext(ctx context.Context, msg protoreflect.Message, key protoreflect.MapKey, value protoreflect.Value, cfg *validationConfig) (err error) { + evalErr := m.KeyRules.EvaluateFieldContext(ctx, msg, key.Value(), cfg, true) markViolationForKey(evalErr) ok, err := mergeViolations(err, evalErr, cfg) if !ok { return err } - evalErr = m.ValueRules.EvaluateField(msg, value, cfg, true) + evalErr = m.ValueRules.EvaluateFieldContext(ctx, msg, value, cfg, true) _, err = mergeViolations(err, evalErr, cfg) return err } diff --git a/message.go b/message.go index ba8741f..5bfc8ab 100644 --- a/message.go +++ b/message.go @@ -15,6 +15,7 @@ package protovalidate import ( + "context" "fmt" "google.golang.org/protobuf/reflect/protoreflect" @@ -34,11 +35,24 @@ type message struct { nestedEvaluators messageEvaluators } -func (m *message) Evaluate(_ protoreflect.Message, val protoreflect.Value, cfg *validationConfig) error { - return m.EvaluateMessage(val.Message(), cfg) +func (m *message) Evaluate(msg protoreflect.Message, val protoreflect.Value, cfg *validationConfig) error { + return m.EvaluateContext(context.Background(), msg, val, cfg) +} + +func (m *message) EvaluateContext(ctx context.Context, _ protoreflect.Message, val protoreflect.Value, cfg *validationConfig) error { + return m.EvaluateMessageContext(ctx, val.Message(), cfg) } func (m *message) EvaluateMessage(msg protoreflect.Message, cfg *validationConfig) error { + return m.EvaluateMessageContext(context.Background(), msg, cfg) +} + +func (m *message) EvaluateMessageContext(ctx context.Context, msg protoreflect.Message, cfg *validationConfig) error { + if cfg.cancellable { + if err := ctx.Err(); err != nil { + return err + } + } var ( err error ok bool @@ -47,11 +61,11 @@ func (m *message) EvaluateMessage(msg protoreflect.Message, cfg *validationConfi if m.Err != nil { return m.Err } - if ok, err = mergeViolations(err, m.evaluators.EvaluateMessage(msg, cfg), cfg); !ok { + if ok, err = mergeViolations(err, m.evaluators.EvaluateMessageContext(ctx, msg, cfg), cfg); !ok { return err } } - _, err = mergeViolations(err, m.nestedEvaluators.EvaluateMessage(msg, cfg), cfg) + _, err = mergeViolations(err, m.nestedEvaluators.EvaluateMessageContext(ctx, msg, cfg), cfg) return err } @@ -91,11 +105,19 @@ func (u unknownMessage) Err() error { func (u unknownMessage) Tautology() bool { return false } -func (u unknownMessage) Evaluate(_ protoreflect.Message, _ protoreflect.Value, _ *validationConfig) error { +func (u unknownMessage) Evaluate(msg protoreflect.Message, val protoreflect.Value, cfg *validationConfig) error { + return u.EvaluateContext(context.Background(), msg, val, cfg) +} + +func (u unknownMessage) EvaluateContext(_ context.Context, _ protoreflect.Message, _ protoreflect.Value, _ *validationConfig) error { return u.Err() } -func (u unknownMessage) EvaluateMessage(_ protoreflect.Message, _ *validationConfig) error { +func (u unknownMessage) EvaluateMessage(msg protoreflect.Message, cfg *validationConfig) error { + return u.EvaluateMessageContext(context.Background(), msg, cfg) +} + +func (u unknownMessage) EvaluateMessageContext(_ context.Context, _ protoreflect.Message, _ *validationConfig) error { return u.Err() } @@ -107,8 +129,12 @@ type embeddedMessage struct { message *message } -func (m *embeddedMessage) Evaluate(_ protoreflect.Message, val protoreflect.Value, cfg *validationConfig) error { - err := m.message.EvaluateMessage(val.Message(), cfg) +func (m *embeddedMessage) Evaluate(msg protoreflect.Message, val protoreflect.Value, cfg *validationConfig) error { + return m.EvaluateContext(context.Background(), msg, val, cfg) +} + +func (m *embeddedMessage) EvaluateContext(ctx context.Context, _ protoreflect.Message, val protoreflect.Value, cfg *validationConfig) error { + err := m.message.EvaluateMessageContext(ctx, val.Message(), cfg) updateViolationPaths(err, m.FieldPathElement, nil) return err } diff --git a/message_oneof.go b/message_oneof.go index ce7842d..3e98909 100644 --- a/message_oneof.go +++ b/message_oneof.go @@ -15,6 +15,7 @@ package protovalidate import ( + "context" "fmt" "strings" @@ -39,11 +40,19 @@ func (o messageOneof) formatFields() string { return strings.Join(names, ", ") } -func (o messageOneof) Evaluate(_ protoreflect.Message, val protoreflect.Value, cfg *validationConfig) error { - return o.EvaluateMessage(val.Message(), cfg) +func (o messageOneof) Evaluate(msg protoreflect.Message, val protoreflect.Value, cfg *validationConfig) error { + return o.EvaluateContext(context.Background(), msg, val, cfg) +} + +func (o messageOneof) EvaluateContext(ctx context.Context, _ protoreflect.Message, val protoreflect.Value, cfg *validationConfig) error { + return o.EvaluateMessageContext(ctx, val.Message(), cfg) } func (o messageOneof) EvaluateMessage(msg protoreflect.Message, cfg *validationConfig) error { + return o.EvaluateMessageContext(context.Background(), msg, cfg) +} + +func (o messageOneof) EvaluateMessageContext(_ context.Context, msg protoreflect.Message, cfg *validationConfig) error { if !cfg.filter.ShouldValidate(msg, msg.Descriptor()) { return nil } diff --git a/native_bool.go b/native_bool.go index d2c4e6b..9eb0a84 100644 --- a/native_bool.go +++ b/native_bool.go @@ -15,6 +15,7 @@ package protovalidate import ( + "context" "fmt" "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" @@ -58,7 +59,11 @@ type nativeBoolEval struct { constVal bool } -func (n nativeBoolEval) Evaluate(_ protoreflect.Message, val protoreflect.Value, _ *validationConfig) error { +func (n nativeBoolEval) Evaluate(msg protoreflect.Message, val protoreflect.Value, cfg *validationConfig) error { + return n.EvaluateContext(context.Background(), msg, val, cfg) +} + +func (n nativeBoolEval) EvaluateContext(_ context.Context, _ protoreflect.Message, val protoreflect.Value, _ *validationConfig) error { if val.Bool() != n.constVal { return &ValidationError{Violations: []*Violation{n.newViolation(boolConstSite, "bool.const", fmt.Sprintf("must equal %t", n.constVal), diff --git a/native_bytes.go b/native_bytes.go index 023117b..0e940cf 100644 --- a/native_bytes.go +++ b/native_bytes.go @@ -16,6 +16,7 @@ package protovalidate import ( "bytes" + "context" "errors" "fmt" "math" @@ -274,8 +275,12 @@ type nativeBytesEval struct { var errNotUTF8 = errors.New("must be valid UTF-8 to apply regexp") +func (n nativeBytesEval) Evaluate(msg protoreflect.Message, val protoreflect.Value, cfg *validationConfig) error { + return n.EvaluateContext(context.Background(), msg, val, cfg) +} + //nolint:gocyclo // this code has nested ifs but it's not hard to follow. -func (n nativeBytesEval) Evaluate(_ protoreflect.Message, val protoreflect.Value, cfg *validationConfig) error { +func (n nativeBytesEval) EvaluateContext(_ context.Context, _ protoreflect.Message, val protoreflect.Value, cfg *validationConfig) error { bytesVal := val.Bytes() byteLen := uint64(len(bytesVal)) var violations []*Violation diff --git a/native_enum.go b/native_enum.go index 01d3a60..28b37d2 100644 --- a/native_enum.go +++ b/native_enum.go @@ -15,6 +15,7 @@ package protovalidate import ( + "context" "fmt" "slices" @@ -116,7 +117,11 @@ var enumProcessors = []enumProcessor{ }, } -func (n nativeEnumEval) Evaluate(_ protoreflect.Message, val protoreflect.Value, cfg *validationConfig) error { +func (n nativeEnumEval) Evaluate(msg protoreflect.Message, val protoreflect.Value, cfg *validationConfig) error { + return n.EvaluateContext(context.Background(), msg, val, cfg) +} + +func (n nativeEnumEval) EvaluateContext(_ context.Context, _ protoreflect.Message, val protoreflect.Value, cfg *validationConfig) error { enumVal := int32(val.Enum()) var violations []*Violation diff --git a/native_map.go b/native_map.go index cb49d9d..58c35db 100644 --- a/native_map.go +++ b/native_map.go @@ -15,6 +15,7 @@ package protovalidate import ( + "context" "fmt" "math" @@ -85,7 +86,16 @@ type nativeMapEval struct { maxPairs uint64 } -func (n nativeMapEval) Evaluate(_ protoreflect.Message, val protoreflect.Value, _ *validationConfig) error { +func (n nativeMapEval) Evaluate(msg protoreflect.Message, val protoreflect.Value, cfg *validationConfig) error { + return n.EvaluateContext(context.Background(), msg, val, cfg) +} + +func (n nativeMapEval) EvaluateContext(ctx context.Context, _ protoreflect.Message, val protoreflect.Value, cfg *validationConfig) error { + if cfg.cancellable { + if err := ctx.Err(); err != nil { + return err + } + } size := uint64(val.Map().Len()) //nolint:gosec // int will never be negative or out of uint64 range // min_pairs diff --git a/native_numeric.go b/native_numeric.go index 2b51874..0e0870a 100644 --- a/native_numeric.go +++ b/native_numeric.go @@ -15,6 +15,7 @@ package protovalidate import ( + "context" "fmt" "math" "slices" @@ -518,7 +519,11 @@ func (n nativeNumericCompare[T]) conjunction() string { return "or" } -func (n nativeNumericCompare[T]) Evaluate(_ protoreflect.Message, val protoreflect.Value, cfg *validationConfig) error { +func (n nativeNumericCompare[T]) Evaluate(msg protoreflect.Message, val protoreflect.Value, cfg *validationConfig) error { + return n.EvaluateContext(context.Background(), msg, val, cfg) +} + +func (n nativeNumericCompare[T]) EvaluateContext(_ context.Context, _ protoreflect.Message, val protoreflect.Value, cfg *validationConfig) error { valT := n.config.extractVal(val) var violations []*Violation diff --git a/native_repeated.go b/native_repeated.go index 6abf61f..5c14061 100644 --- a/native_repeated.go +++ b/native_repeated.go @@ -16,6 +16,7 @@ package protovalidate import ( "bytes" + "context" "fmt" "math" @@ -148,7 +149,16 @@ type nativeRepeatedEval struct { uniqueFn uniqueChecker } -func (n nativeRepeatedEval) Evaluate(_ protoreflect.Message, val protoreflect.Value, cfg *validationConfig) error { +func (n nativeRepeatedEval) Evaluate(msg protoreflect.Message, val protoreflect.Value, cfg *validationConfig) error { + return n.EvaluateContext(context.Background(), msg, val, cfg) +} + +func (n nativeRepeatedEval) EvaluateContext(ctx context.Context, _ protoreflect.Message, val protoreflect.Value, cfg *validationConfig) error { + if cfg.cancellable { + if err := ctx.Err(); err != nil { + return err + } + } list := val.List() size := uint64(list.Len()) //nolint:gosec // len can't be < 0 and is always within uint64 range var violations []*Violation diff --git a/native_string.go b/native_string.go index 5aa828b..9b015c1 100644 --- a/native_string.go +++ b/native_string.go @@ -15,6 +15,7 @@ package protovalidate import ( + "context" "errors" "fmt" "regexp" @@ -378,8 +379,12 @@ type nativeStringEval struct { strict bool } +func (n nativeStringEval) Evaluate(msg protoreflect.Message, val protoreflect.Value, cfg *validationConfig) error { + return n.EvaluateContext(context.Background(), msg, val, cfg) +} + //nolint:gocyclo // this code has nested ifs but it's not hard to follow. -func (n nativeStringEval) Evaluate(_ protoreflect.Message, val protoreflect.Value, cfg *validationConfig) error { +func (n nativeStringEval) EvaluateContext(_ context.Context, _ protoreflect.Message, val protoreflect.Value, cfg *validationConfig) error { strVal := val.String() var violations []*Violation diff --git a/oneof.go b/oneof.go index 326fd23..5beb6d1 100644 --- a/oneof.go +++ b/oneof.go @@ -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" @@ -28,11 +30,19 @@ type oneof struct { Required bool } -func (o oneof) Evaluate(_ protoreflect.Message, val protoreflect.Value, cfg *validationConfig) error { - return o.EvaluateMessage(val.Message(), cfg) +func (o oneof) Evaluate(msg protoreflect.Message, val protoreflect.Value, cfg *validationConfig) error { + return o.EvaluateContext(context.Background(), msg, val, cfg) +} + +func (o oneof) EvaluateContext(ctx context.Context, _ protoreflect.Message, val protoreflect.Value, cfg *validationConfig) error { + return o.EvaluateMessageContext(ctx, val.Message(), cfg) } func (o oneof) EvaluateMessage(msg protoreflect.Message, cfg *validationConfig) error { + return o.EvaluateMessageContext(context.Background(), msg, cfg) +} + +func (o oneof) EvaluateMessageContext(_ context.Context, msg protoreflect.Message, cfg *validationConfig) error { if !cfg.filter.ShouldValidate(msg, o.Descriptor) || !o.Required || msg.WhichOneof(o.Descriptor) != nil { return nil diff --git a/option.go b/option.go index 7eafc76..8435772 100644 --- a/option.go +++ b/option.go @@ -117,6 +117,30 @@ func WithNowFunc(fn func() *timestamppb.Timestamp) Option { return nowFuncOption(fn) } +// WithCELInterruptCheckFrequency sets how many CEL comprehension iterations +// elapse between checks for context cancellation during [ContextValidator.ValidateContext]. +// +// cel-go only checks for cancellation inside comprehension loops (all, exists, +// map, filter), once per iteration, so cancellation latency is proportional to +// this value. The per-iteration bookkeeping happens regardless of the +// frequency, which merely gates a cheap non-blocking channel poll; raising it +// therefore buys no measurable throughput while making cancellation +// correspondingly less responsive. The default, +// [buf.build/go/protovalidate/cel.DefaultInterruptCheckFrequency], is 1. +// +// A value of 0 disables interrupt checking: CEL expressions run to completion +// and cannot be cancelled mid-evaluation. Expressions with no comprehension are +// never interruptible regardless of this setting. +func WithCELInterruptCheckFrequency(frequency uint) ValidatorOption { + return celInterruptCheckFrequencyOption{frequency} +} + +type celInterruptCheckFrequencyOption struct{ frequency uint } + +func (o celInterruptCheckFrequencyOption) applyToValidator(cfg *config) { + cfg.interruptCheckFrequency = o.frequency +} + type messageDescriptorsOption struct { descriptors []protoreflect.MessageDescriptor } diff --git a/program.go b/program.go index a79a681..1e65e22 100644 --- a/program.go +++ b/program.go @@ -15,11 +15,15 @@ package protovalidate import ( + "context" + "errors" "fmt" "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" "github.com/google/cel-go/cel" "github.com/google/cel-go/common/types" + "github.com/google/cel-go/common/types/ref" + "github.com/google/cel-go/interpreter" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/reflect/protoreflect" ) @@ -41,16 +45,31 @@ func (s programSet) Eval( val protoreflect.Value, fieldDesc protoreflect.FieldDescriptor, cfg *validationConfig, +) error { + return s.EvalContext(context.Background(), val, fieldDesc, cfg) +} + +// EvalContext is like Eval, but honors context cancellation. +func (s programSet) EvalContext( + ctx context.Context, + val protoreflect.Value, + fieldDesc protoreflect.FieldDescriptor, + cfg *validationConfig, ) error { if len(s.programs) == 0 { return nil } + if cfg.cancellable { + if err := ctx.Err(); err != nil { + return err + } + } activation := getBindings() defer putBindings(activation) activation.This = newOptional(thisToCel(val.Interface(), fieldDesc, s.env.CELTypeAdapter())) var violations []*Violation for _, expr := range s.programs { - violation, err := expr.eval(activation, cfg) + violation, err := expr.evalContext(ctx, activation, cfg) if err != nil { return err } @@ -91,8 +110,12 @@ type compiledProgram struct { Descriptor protoreflect.FieldDescriptor } -//nolint:nilnil // non-existence of violations is intentional func (expr compiledProgram) eval(activation *bindings, cfg *validationConfig) (*Violation, error) { + return expr.evalContext(context.Background(), activation, cfg) +} + +//nolint:nilnil // non-existence of violations is intentional +func (expr compiledProgram) evalContext(ctx context.Context, activation *bindings, cfg *validationConfig) (*Violation, error) { activation.NowFn = cfg.nowFn if expr.Rules != nil { activation.Rules = expr.Rules.Interface() @@ -100,8 +123,28 @@ func (expr compiledProgram) eval(activation *bindings, cfg *validationConfig) (* if expr.Value.IsValid() { activation.Rule = expr.Value.Interface() } - value, _, err := expr.Program.Eval(activation) + var ( + value ref.Val + err error + ) + if cfg.cancellable { + value, _, err = expr.Program.ContextEval(ctx, activation) + } else { + // The context can never be cancelled (e.g. context.Background), so there + // is nothing to interrupt. Skip ContextEval, which would otherwise wrap + // the activation on every evaluation just to poll a nil channel. This + // keeps the Validate path free of any context-related overhead. + value, _, err = expr.Program.Eval(activation) + } if err != nil { + // Only cel-go's interrupt error signals cancellation; any other failure + // is a genuine evaluation error and must keep its RuntimeError identity + // (with the rule id) even if the context has expired in the meantime. + if errors.Is(err, interpreter.InterruptError{}) { + if ctxErr := ctx.Err(); ctxErr != nil { + return nil, ctxErr + } + } return nil, &RuntimeError{cause: fmt.Errorf( "error evaluating %s: %w", expr.Source.GetId(), err)} } diff --git a/program_test.go b/program_test.go index 9dc899b..135fed1 100644 --- a/program_test.go +++ b/program_test.go @@ -287,6 +287,62 @@ func TestSet_BindThis(t *testing.T) { } } +func TestCompiled_ContextCancelled(t *testing.T) { + t.Parallel() + ctx, cancel := context.WithCancel(context.Background()) + cancel() + program := programSet{programs: []compiledProgram{{ + Program: mockProgram{Val: types.True}, + Source: validate.Rule_builder{Id: proto.String("x")}.Build(), + }}} + err := program.EvalContext(ctx, protoreflect.ValueOfString("a"), nil, &validationConfig{cancellable: true}) + assert.ErrorIs(t, err, context.Canceled) +} + +func TestCompiled_ContextInterrupted(t *testing.T) { + t.Parallel() + env, err := cel.NewEnv() + require.NoError(t, err) + ast, iss := env.Compile("[1, 2, 3].all(x, x > 0)") + require.NoError(t, iss.Err()) + prog, err := env.Program(ast, cel.InterruptCheckFrequency(1)) + require.NoError(t, err) + + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + expr := compiledProgram{ + Program: prog, + Source: validate.Rule_builder{Id: proto.String("x")}.Build(), + } + activation := getBindings() + defer putBindings(activation) + _, err = expr.evalContext(ctx, activation, &validationConfig{cancellable: true}) + require.ErrorIs(t, err, context.Canceled) + var re *RuntimeError + assert.NotErrorAs(t, err, &re, "context cancellation must not be wrapped in RuntimeError, got: %v", err) +} + +// TestCompiled_EvalErrorNotMaskedByExpiredContext guards against reclassifying +// a genuine evaluation error as a context error just because the context +// expired while the expression ran: only cel-go's interrupt error signals +// cancellation. +func TestCompiled_EvalErrorNotMaskedByExpiredContext(t *testing.T) { + t.Parallel() + ctx, cancel := context.WithCancel(context.Background()) + cancel() + expr := compiledProgram{ + Program: mockProgram{Err: errors.New("division by zero")}, + Source: validate.Rule_builder{Id: proto.String("x")}.Build(), + } + activation := getBindings() + defer putBindings(activation) + _, err := expr.evalContext(ctx, activation, &validationConfig{cancellable: true}) + var re *RuntimeError + require.ErrorAs(t, err, &re, "a real evaluation error must stay a RuntimeError, got: %v", err) + assert.NotErrorIs(t, err, context.Canceled) +} + type mockProgram struct { Val ref.Val Err error diff --git a/proto/tests/example/v1/bench.proto b/proto/tests/example/v1/bench.proto index 1e158ca..1a3156f 100644 --- a/proto/tests/example/v1/bench.proto +++ b/proto/tests/example/v1/bench.proto @@ -190,3 +190,36 @@ enum BenchEnum { BENCH_ENUM_ONE = 1; BENCH_ENUM_TWO = 2; } + +// BenchCrossReference models a message whose CEL rule enforces referential +// integrity: every reference held by every rule must resolve to a declared +// entry. Evaluating it costs O(rules * refs_per_rule * entries), so its +// validation time grows multiplicatively with the size of the message. +// +// It exists to exercise cancellation of a single long-running CEL expression. +// It is not part of the faked benchmark corpus; tests build it explicitly. +message BenchCrossReference { + option (buf.validate.message).cel = { + id: "refs_resolve" + message: "every rule reference must resolve to a declared entry" + expression: + "this.rules.all(rule, rule.refs.all(ref," + "this.entries.exists(entry, entry.key.kind == ref.kind && entry.key.name == ref.name)))" + }; + + message Key { + string kind = 1; + string name = 2; + } + + message Entry { + Key key = 1; + } + + message Rule { + repeated Key refs = 1; + } + + repeated Entry entries = 1; + repeated Rule rules = 2; +} diff --git a/repeated.go b/repeated.go index ec71b2c..b530d90 100644 --- a/repeated.go +++ b/repeated.go @@ -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" @@ -48,11 +50,20 @@ func newListItems(valEval *value) listItems { } func (r listItems) Evaluate(msg protoreflect.Message, val protoreflect.Value, cfg *validationConfig) error { + return r.EvaluateContext(context.Background(), msg, val, cfg) +} + +func (r listItems) EvaluateContext(ctx context.Context, msg protoreflect.Message, val protoreflect.Value, cfg *validationConfig) error { list := val.List() var ok bool var err error for i := range list.Len() { - itemErr := r.ItemRules.EvaluateField(msg, list.Get(i), cfg, true) + if cfg.cancellable { + if ctxErr := ctx.Err(); ctxErr != nil { + return ctxErr + } + } + itemErr := r.ItemRules.EvaluateFieldContext(ctx, msg, list.Get(i), cfg, true) if itemErr != nil { updateViolationPaths(itemErr, validate.FieldPathElement_builder{ FieldNumber: proto.Int32(r.FieldPathElement.GetFieldNumber()), diff --git a/validator.go b/validator.go index b9107a6..200af2e 100644 --- a/validator.go +++ b/validator.go @@ -15,6 +15,7 @@ package protovalidate import ( + "context" "fmt" "sync" @@ -27,7 +28,7 @@ import ( ) var ( - getGlobalValidator = sync.OnceValues(func() (Validator, error) { return New() }) + getGlobalValidator = sync.OnceValues(func() (ContextValidator, error) { return New() }) // GlobalValidator provides access to the global Validator instance that is // used by the [Validate] function. This is intended to be used by libraries @@ -37,7 +38,7 @@ var ( // Using the global Validator instance (either through [Validator] or via // GlobalValidator) will result in lower memory usage than using multiple // Validator instances, because each Validator instance has its own caches. - GlobalValidator Validator = globalValidator{} + GlobalValidator ContextValidator = globalValidator{} ) // Validator performs validation on any proto.Message values. The Validator is @@ -52,13 +53,43 @@ type Validator interface { Validate(msg proto.Message, options ...ValidationOption) error } -// New creates a Validator with the given options. An error may occur in setting -// up the CEL execution environment if the configuration is invalid. See the -// individual ValidatorOption for how they impact the fallibility of New. -func New(options ...ValidatorOption) (Validator, error) { +// ContextValidator is a [Validator] that additionally supports context-aware +// validation. The value returned by [New] and the [GlobalValidator] both +// implement it. Callers that hold only a [Validator] can type-assert it to +// ContextValidator, or use the package-level [ValidateContext] function. +// +// ContextValidator is a separate interface (rather than a method on Validator) +// to preserve backward compatibility for external implementations of Validator. +type ContextValidator interface { + Validator + + // ValidateContext behaves like Validate, but stops early if ctx is + // cancelled or its deadline is exceeded, returning the context error + // (context.Canceled or context.DeadlineExceeded), detectable with + // errors.Is. + // + // Cancellation is observed at message, repeated and map boundaries, before + // each CEL rule, and inside CEL comprehension macros (all, exists, map, + // filter): the loops in which an expensive expression spends its time. A + // CEL expression containing no comprehension is not interruptible once it + // begins; see [WithCELInterruptCheckFrequency]. + // + // Passing a context that can never be cancelled, such as context.Background, + // costs nothing: evaluation then takes exactly the same path as Validate. + ValidateContext(ctx context.Context, msg proto.Message, options ...ValidationOption) error +} + +// New creates a ContextValidator with the given options. An error may occur in +// setting up the CEL execution environment if the configuration is invalid. See +// the individual ValidatorOption for how they impact the fallibility of New. +// +// The returned value implements [ContextValidator], and therefore also +// [Validator]; assigning it to a Validator remains valid. +func New(options ...ValidatorOption) (ContextValidator, error) { cfg := config{ - extensionTypeResolver: protoregistry.GlobalTypes, - nowFn: timestamppb.Now, + extensionTypeResolver: protoregistry.GlobalTypes, + nowFn: timestamppb.Now, + interruptCheckFrequency: pvcel.DefaultInterruptCheckFrequency, } for _, opt := range options { opt.applyToValidator(&cfg) @@ -72,7 +103,9 @@ func New(options ...ValidatorOption) (Validator, error) { env, err := cel.NewEnv( cel.CustomTypeProvider(reg), cel.CustomTypeAdapter(reg), - cel.Lib(pvcel.NewLibrary()), + cel.Lib(pvcel.NewLibrary( + pvcel.WithInterruptCheckFrequency(cfg.interruptCheckFrequency), + )), ) if err != nil { return nil, fmt.Errorf( @@ -88,29 +121,66 @@ func New(options ...ValidatorOption) (Validator, error) { cfg.desc..., ) + baseCfg := &validationConfig{ + failFast: cfg.failFast, + filter: nopFilter{}, + nowFn: cfg.nowFn, + } + cancellableCfg := baseCfg.clone() + cancellableCfg.cancellable = true + return &validator{ - builder: bldr, - cfg: &validationConfig{ - failFast: cfg.failFast, - filter: nopFilter{}, - nowFn: cfg.nowFn, - }, + builder: bldr, + cfg: baseCfg, + cancellableCfg: cancellableCfg, }, nil } type validator struct { builder *builder - cfg *validationConfig + // cfg and cancellableCfg are identical but for the cancellable flag. Both + // are built once and treated as immutable, so a ValidateContext call with no + // ValidationOptions can select one without allocating a copy. + cfg *validationConfig + cancellableCfg *validationConfig } func (v *validator) Validate( msg proto.Message, options ...ValidationOption, +) error { + // Pass cancellable=false without consulting the context: a background + // context can never be cancelled, and probing it would add interface calls + // to the hot path. + return v.validate(context.Background(), false, msg, options...) +} + +func (v *validator) ValidateContext( + ctx context.Context, + msg proto.Message, + options ...ValidationOption, +) error { + // Check the context before anything else (including a nil msg) so that a + // cancelled context always yields its error, as documented. + if err := ctx.Err(); err != nil { + return err + } + return v.validate(ctx, ctx.Done() != nil, msg, options...) +} + +func (v *validator) validate( + ctx context.Context, + cancellable bool, + msg proto.Message, + options ...ValidationOption, ) error { if msg == nil { return nil } cfg := v.cfg + if cancellable { + cfg = v.cancellableCfg + } if len(options) > 0 { cfg = cfg.clone() for _, opt := range options { @@ -119,7 +189,7 @@ func (v *validator) Validate( } refl := msg.ProtoReflect() eval := v.builder.Load(refl.Descriptor()) - err := eval.EvaluateMessage(refl, cfg) + err := eval.EvaluateMessageContext(ctx, refl, cfg) finalizeViolationPaths(err) return err } @@ -129,27 +199,39 @@ func (v *validator) Validate( // 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, options ...ValidationOption) error { + return ValidateContext(context.Background(), msg, options...) +} + +// ValidateContext is the context-aware counterpart to Validate, using the +// global Validator instance. See [ContextValidator.ValidateContext]. +func ValidateContext(ctx context.Context, msg proto.Message, options ...ValidationOption) error { globalValidator, err := getGlobalValidator() if err != nil { return err } - return globalValidator.Validate(msg, options...) + return globalValidator.ValidateContext(ctx, msg, options...) } type config struct { - failFast bool - disableLazy bool - desc []protoreflect.MessageDescriptor - extensionTypeResolver protoregistry.ExtensionTypeResolver - allowUnknownFields bool - nowFn func() *timestamppb.Timestamp - disableNativeRules bool + failFast bool + disableLazy bool + desc []protoreflect.MessageDescriptor + extensionTypeResolver protoregistry.ExtensionTypeResolver + allowUnknownFields bool + nowFn func() *timestamppb.Timestamp + disableNativeRules bool + interruptCheckFrequency uint } type validationConfig struct { failFast bool filter Filter nowFn func() *timestamppb.Timestamp + // cancellable reports whether the context passed to ValidateContext can + // ever be cancelled (ctx.Done() != nil). When false, evaluation skips every + // per-node context check and cel-go's ContextEval, so the Validate path + // costs exactly what it did before context support existed. + cancellable bool } func (cfg *validationConfig) clone() *validationConfig { @@ -162,3 +244,7 @@ type globalValidator struct{} func (globalValidator) Validate(msg proto.Message, options ...ValidationOption) error { return Validate(msg, options...) } + +func (globalValidator) ValidateContext(ctx context.Context, msg proto.Message, options ...ValidationOption) error { + return ValidateContext(ctx, msg, options...) +} diff --git a/validator_bench_test.go b/validator_bench_test.go index 914811f..f98fa39 100644 --- a/validator_bench_test.go +++ b/validator_bench_test.go @@ -15,6 +15,8 @@ package protovalidate import ( + "context" + "fmt" "os" "strings" "testing" @@ -247,3 +249,111 @@ func benchSuccess(b *testing.B, msg proto.Message) { _ = val.Validate(msg) } } + +// newCrossReference builds a BenchCrossReference with n entries and n rules of +// n references each. Every reference resolves to the *last* entry, so the +// exists() macro scans the whole entry list and no short-circuit truncates the +// work: evaluating the message-level rule costs Theta(n^3) comprehension +// iterations. The message is valid, so validation runs to completion. +func newCrossReference(n int) *pb.BenchCrossReference { + entries := make([]*pb.BenchCrossReference_Entry, n) + for i := range n { + entries[i] = pb.BenchCrossReference_Entry_builder{ + Key: pb.BenchCrossReference_Key_builder{Kind: "k", Name: fmt.Sprintf("n-%d", i)}.Build(), + }.Build() + } + last := pb.BenchCrossReference_Key_builder{Kind: "k", Name: fmt.Sprintf("n-%d", n-1)}.Build() + rules := make([]*pb.BenchCrossReference_Rule, n) + for i := range n { + refs := make([]*pb.BenchCrossReference_Key, n) + for j := range n { + refs[j] = last + } + rules[i] = pb.BenchCrossReference_Rule_builder{Refs: refs}.Build() + } + return pb.BenchCrossReference_builder{Entries: entries, Rules: rules}.Build() +} + +// benchContextVariants compares the three evaluation paths on one message: +// +// - Validate: the legacy entry point, which delegates with context.Background() +// - Background: ValidateContext with a context that can never be cancelled +// - Cancellable: ValidateContext with a real cancellable context +// +// Validate and Background must match the pre-context baseline: because +// context.Background().Done() is nil, evalContext skips cel-go's ContextEval +// and its per-evaluation activation wrapping. Only Cancellable pays for +// cancellation support. +func benchContextVariants(b *testing.B, msg proto.Message) { + b.Helper() + options := []ValidatorOption{WithMessages(msg), WithDisableLazy()} + if strings.EqualFold(os.Getenv("DISABLE_NATIVE_RULES"), "true") { + options = append(options, WithDisableNativeRules()) + } + val, err := New(options...) + require.NoError(b, err) + + b.Run("Validate", func(b *testing.B) { + b.ReportAllocs() + for b.Loop() { + _ = val.Validate(msg) + } + }) + b.Run("Background", func(b *testing.B) { + ctx := context.Background() + b.ReportAllocs() + for b.Loop() { + _ = val.ValidateContext(ctx, msg) + } + }) + b.Run("Cancellable", func(b *testing.B) { + ctx := b.Context() // cancelled when the benchmark ends; Done() is non-nil + b.ReportAllocs() + for b.Loop() { + _ = val.ValidateContext(ctx, msg) + } + }) +} + +// BenchmarkContextOverhead quantifies what context support costs across message +// shapes exercising the different cancellation checkpoints. +func BenchmarkContextOverhead(b *testing.B) { + faked := func(msg proto.Message) proto.Message { + require.NoError(b, protogofakeit.New(gofakeit.New(1)).FakeProto(msg)) + return msg + } + b.Run("Scalar", func(b *testing.B) { + benchContextVariants(b, faked(&pb.BenchScalar{})) + }) + b.Run("RepeatedMessage", func(b *testing.B) { + benchContextVariants(b, faked(&pb.BenchRepeatedMessage{})) + }) + b.Run("Map", func(b *testing.B) { + benchContextVariants(b, faked(&pb.BenchMap{})) + }) + b.Run("ComplexSchema", func(b *testing.B) { + benchContextVariants(b, faked(&pb.BenchComplexSchema{})) + }) +} + +// BenchmarkInterruptCheckFrequency shows that the interrupt check frequency does +// not meaningfully affect throughput. cel-go calls checkInterrupt on every +// comprehension iteration regardless of the frequency; the frequency only gates +// a non-blocking channel poll. Raising it therefore trades cancellation latency +// away for no throughput gain, which is why the default is 1. +func BenchmarkInterruptCheckFrequency(b *testing.B) { + msg := newCrossReference(8) + for _, frequency := range []uint{0, 1, 10, 100} { + b.Run(fmt.Sprintf("freq=%d", frequency), func(b *testing.B) { + val, err := New(WithMessages(msg), WithDisableLazy(), + WithCELInterruptCheckFrequency(frequency)) + require.NoError(b, err) + ctx := b.Context() + b.ReportAllocs() + b.ResetTimer() + for b.Loop() { + _ = val.ValidateContext(ctx, msg) + } + }) + } +} diff --git a/validator_test.go b/validator_test.go index cf9b1d4..1ee10cd 100644 --- a/validator_test.go +++ b/validator_test.go @@ -15,9 +15,11 @@ package protovalidate import ( + "context" "testing" "time" + pvcel "buf.build/go/protovalidate/cel" pb "buf.build/go/protovalidate/internal/gen/tests/example/v1" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -632,3 +634,142 @@ func TestValidator_Validate_Issue296(t *testing.T) { err = val.Validate(msg) require.NoError(t, err) } + +// validateOnly implements only the Validator interface. It exists to guard the +// backward-compatibility contract: ValidateContext must stay off Validator, so +// external types implementing only Validate keep satisfying it. +type validateOnly struct{} + +func (validateOnly) Validate(proto.Message, ...ValidationOption) error { return nil } + +var ( + _ Validator = validateOnly{} + _ ContextValidator = (*validator)(nil) +) + +func TestValidator_ValidateContext_Cancelled(t *testing.T) { + t.Parallel() + val, err := New() + require.NoError(t, err) + ctx, cancel := context.WithCancel(context.Background()) + cancel() + err = val.ValidateContext(ctx, &pb.Person{}) + assert.ErrorIs(t, err, context.Canceled) +} + +// TestValidator_ValidateContext_CancelledNilMessage pins the check order: a +// cancelled context yields its error even when the message is nil, so callers +// using the error as a cancellation signal never miss it. +func TestValidator_ValidateContext_CancelledNilMessage(t *testing.T) { + t.Parallel() + val, err := New() + require.NoError(t, err) + ctx, cancel := context.WithCancel(context.Background()) + cancel() + err = val.ValidateContext(ctx, nil) + assert.ErrorIs(t, err, context.Canceled) +} + +func TestValidator_ValidateContext_DelegatesToValidate(t *testing.T) { + t.Parallel() + val, err := New() + require.NoError(t, err) + // A valid message returns nil through both entry points. + msg := &pb.Person{Id: 1, Email: "a@b.co", Name: "x", Home: &pb.Coordinates{}} + assert.Equal(t, + val.Validate(msg), + val.ValidateContext(context.Background(), msg), + ) +} + +func TestValidateContext_Global(t *testing.T) { + t.Parallel() + ctx, cancel := context.WithCancel(context.Background()) + cancel() + err := ValidateContext(ctx, &pb.Person{}) + assert.ErrorIs(t, err, context.Canceled) +} + +// TestValidateContext_InterruptsExpensiveCELExpression covers the motivating +// case: a single CEL rule whose cost grows multiplicatively with message size +// (see BenchCrossReference). Uncancelled it runs for seconds; a deadline must +// cut it short rather than waiting for it to finish. +func TestValidateContext_InterruptsExpensiveCELExpression(t *testing.T) { + t.Parallel() + msg := newCrossReference(150) // ~3s of CEL evaluation when uncancelled + val, err := New(WithMessages(msg), WithDisableLazy()) + require.NoError(t, err) + + ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond) + defer cancel() + + start := time.Now() + err = val.ValidateContext(ctx, msg) + elapsed := time.Since(start) + + require.ErrorIs(t, err, context.DeadlineExceeded) + assert.Less(t, elapsed, time.Second, + "the deadline must interrupt evaluation, not be noticed after it completes") +} + +// TestValidateContext_InterruptCheckFrequency documents why the default +// frequency is 1. cel-go polls ctx.Done() once every `frequency` comprehension +// iterations, so cancellation latency scales with the frequency, while the +// per-iteration bookkeeping happens either way. A coarse frequency therefore +// buys no throughput (see BenchmarkInterruptCheckFrequency) and only makes a +// deadline less meaningful. +func TestValidateContext_InterruptCheckFrequency(t *testing.T) { + t.Parallel() + if testing.Short() { + t.Skip("timing-sensitive; runs a multi-second CEL expression") + } + msg := newCrossReference(150) + + latency := func(frequency uint) time.Duration { + val, err := New(WithMessages(msg), WithDisableLazy(), + WithCELInterruptCheckFrequency(frequency)) + require.NoError(t, err) + ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond) + defer cancel() + start := time.Now() + err = val.ValidateContext(ctx, msg) + require.ErrorIs(t, err, context.DeadlineExceeded) + return time.Since(start) + } + + def := latency(pvcel.DefaultInterruptCheckFrequency) + coarse := latency(100) + + assert.Less(t, def, 500*time.Millisecond, + "the default frequency should honor the deadline promptly") + assert.Greater(t, coarse, 2*def, + "a coarser frequency should notice the deadline substantially later") +} + +// TestWithCELInterruptCheckFrequency_Disabled shows that a frequency of 0 turns +// interrupt checking off: validation still works, but CEL expressions run to +// completion and cannot be cancelled mid-evaluation. +func TestWithCELInterruptCheckFrequency_Disabled(t *testing.T) { + t.Parallel() + msg := newCrossReference(8) + val, err := New(WithMessages(msg), WithDisableLazy(), + WithCELInterruptCheckFrequency(0)) + require.NoError(t, err) + require.NoError(t, val.Validate(msg)) + require.NoError(t, val.ValidateContext(context.Background(), msg)) +} + +func TestValidator_ValidateContext_CancelMidTraversal(t *testing.T) { + t.Parallel() + val, err := New() + require.NoError(t, err) + ctx, cancel := context.WithCancel(context.Background()) + // Cancel as soon as traversal asks whether to validate anything. + filter := FilterFunc(func(protoreflect.Message, protoreflect.Descriptor) bool { + cancel() + return true + }) + msg := &pb.BenchRepeatedMessage{X: []*pb.BenchScalar{{}, {}}} + err = val.ValidateContext(ctx, msg, WithFilter(filter)) + assert.ErrorIs(t, err, context.Canceled) +} diff --git a/value.go b/value.go index 9a6b3fa..b94805c 100644 --- a/value.go +++ b/value.go @@ -15,6 +15,8 @@ package protovalidate import ( + "context" + "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate" "google.golang.org/protobuf/reflect/protoreflect" ) @@ -40,10 +42,19 @@ type value struct { } func (v *value) Evaluate(msg protoreflect.Message, val protoreflect.Value, cfg *validationConfig) error { - return v.EvaluateField(msg, val, cfg, cfg.filter.ShouldValidate(msg, v.Descriptor)) + return v.EvaluateContext(context.Background(), msg, val, cfg) +} + +func (v *value) EvaluateContext(ctx context.Context, msg protoreflect.Message, val protoreflect.Value, cfg *validationConfig) error { + return v.EvaluateFieldContext(ctx, msg, val, cfg, cfg.filter.ShouldValidate(msg, v.Descriptor)) +} + +func (v *value) EvaluateField(msg protoreflect.Message, val protoreflect.Value, cfg *validationConfig, shouldValidate bool) error { + return v.EvaluateFieldContext(context.Background(), msg, val, cfg, shouldValidate) } -func (v *value) EvaluateField( +func (v *value) EvaluateFieldContext( + ctx context.Context, msg protoreflect.Message, val protoreflect.Value, cfg *validationConfig, @@ -57,11 +68,11 @@ func (v *value) EvaluateField( if v.IgnoreEmpty && val.Equal(v.Zero) { return nil } - if ok, err = mergeViolations(err, v.Rules.Evaluate(msg, val, cfg), cfg); !ok { + if ok, err = mergeViolations(err, v.Rules.EvaluateContext(ctx, msg, val, cfg), cfg); !ok { return err } } - _, err = mergeViolations(err, v.NestedRules.Evaluate(msg, val, cfg), cfg) + _, err = mergeViolations(err, v.NestedRules.EvaluateContext(ctx, msg, val, cfg), cfg) return err } diff --git a/wrapper.go b/wrapper.go index f64db8c..ce4aec9 100644 --- a/wrapper.go +++ b/wrapper.go @@ -14,7 +14,11 @@ package protovalidate -import "google.golang.org/protobuf/reflect/protoreflect" +import ( + "context" + + "google.golang.org/protobuf/reflect/protoreflect" +) // wrappedValueEval adapts a native evaluator built against a wrapper WKT's // inner scalar field (e.g. google.protobuf.Int32Value.value) so it can run @@ -32,7 +36,11 @@ type wrappedValueEval struct { } func (w wrappedValueEval) Evaluate(msg protoreflect.Message, val protoreflect.Value, cfg *validationConfig) error { - return w.inner.Evaluate(msg, val.Message().Get(w.innerField), cfg) + return w.EvaluateContext(context.Background(), msg, val, cfg) +} + +func (w wrappedValueEval) EvaluateContext(ctx context.Context, msg protoreflect.Message, val protoreflect.Value, cfg *validationConfig) error { + return w.inner.EvaluateContext(ctx, msg, val.Message().Get(w.innerField), cfg) } func (w wrappedValueEval) Tautology() bool {