Skip to content

Commit 5b8ad84

Browse files
authored
Add all-validator support (#443)
* Add all-validator support * Use single argument instead of variadic Since we are working on v2, keeping the signature compatible with v1 is not actually necessary. Use a clearer single argument instead. Also add comments for the new argument.
1 parent dc87da6 commit 5b8ad84

File tree

3 files changed

+91
-19
lines changed

3 files changed

+91
-19
lines changed

interceptors/validator/validator.go

+48-10
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,12 @@ import (
1111
"google.golang.org/grpc/status"
1212
)
1313

14+
// The validateAller interface at protoc-gen-validate main branch.
15+
// See https://github.com/envoyproxy/protoc-gen-validate/pull/468.
16+
type validateAller interface {
17+
ValidateAll() error
18+
}
19+
1420
// The validate interface starting with protoc-gen-validate v0.6.0.
1521
// See https://github.com/envoyproxy/protoc-gen-validate/pull/455.
1622
type validator interface {
@@ -22,9 +28,25 @@ type validatorLegacy interface {
2228
Validate() error
2329
}
2430

25-
// Calls the Validate function on a proto message using either the current or legacy interface if the Validate function
26-
// is present. If validation fails, the error is wrapped with `InvalidArgument` and returned.
27-
func validate(req interface{}) error {
31+
func validate(req interface{}, all bool) error {
32+
if all {
33+
switch v := req.(type) {
34+
case validateAller:
35+
if err := v.ValidateAll(); err != nil {
36+
return status.Error(codes.InvalidArgument, err.Error())
37+
}
38+
case validator:
39+
if err := v.Validate(true); err != nil {
40+
return status.Error(codes.InvalidArgument, err.Error())
41+
}
42+
case validatorLegacy:
43+
// Fallback to legacy validator
44+
if err := v.Validate(); err != nil {
45+
return status.Error(codes.InvalidArgument, err.Error())
46+
}
47+
}
48+
return nil
49+
}
2850
switch v := req.(type) {
2951
case validatorLegacy:
3052
if err := v.Validate(); err != nil {
@@ -41,9 +63,13 @@ func validate(req interface{}) error {
4163
// UnaryServerInterceptor returns a new unary server interceptor that validates incoming messages.
4264
//
4365
// Invalid messages will be rejected with `InvalidArgument` before reaching any userspace handlers.
44-
func UnaryServerInterceptor() grpc.UnaryServerInterceptor {
66+
// If `all` is false, the interceptor returns first validation error. Otherwise the interceptor
67+
// returns ALL validation error as a wrapped multi-error.
68+
// Note that generated codes prior to protoc-gen-validate v0.6.0 do not provide an all-validation
69+
// interface. In this case the interceptor fallbacks to legacy validation and `all` is ignored.
70+
func UnaryServerInterceptor(all bool) grpc.UnaryServerInterceptor {
4571
return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
46-
if err := validate(req); err != nil {
72+
if err := validate(req, all); err != nil {
4773
return nil, err
4874
}
4975
return handler(ctx, req)
@@ -53,9 +79,13 @@ func UnaryServerInterceptor() grpc.UnaryServerInterceptor {
5379
// UnaryClientInterceptor returns a new unary client interceptor that validates outgoing messages.
5480
//
5581
// Invalid messages will be rejected with `InvalidArgument` before sending the request to server.
56-
func UnaryClientInterceptor() grpc.UnaryClientInterceptor {
82+
// If `all` is false, the interceptor returns first validation error. Otherwise the interceptor
83+
// returns ALL validation error as a wrapped multi-error.
84+
// Note that generated codes prior to protoc-gen-validate v0.6.0 do not provide an all-validation
85+
// interface. In this case the interceptor fallbacks to legacy validation and `all` is ignored.
86+
func UnaryClientInterceptor(all bool) grpc.UnaryClientInterceptor {
5787
return func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error {
58-
if err := validate(req); err != nil {
88+
if err := validate(req, all); err != nil {
5989
return err
6090
}
6191
return invoker(ctx, method, req, reply, cc, opts...)
@@ -64,26 +94,34 @@ func UnaryClientInterceptor() grpc.UnaryClientInterceptor {
6494

6595
// StreamServerInterceptor returns a new streaming server interceptor that validates incoming messages.
6696
//
97+
// If `all` is false, the interceptor returns first validation error. Otherwise the interceptor
98+
// returns ALL validation error as a wrapped multi-error.
99+
// Note that generated codes prior to protoc-gen-validate v0.6.0 do not provide an all-validation
100+
// interface. In this case the interceptor fallbacks to legacy validation and `all` is ignored.
67101
// The stage at which invalid messages will be rejected with `InvalidArgument` varies based on the
68102
// type of the RPC. For `ServerStream` (1:m) requests, it will happen before reaching any userspace
69103
// handlers. For `ClientStream` (n:1) or `BidiStream` (n:m) RPCs, the messages will be rejected on
70104
// calls to `stream.Recv()`.
71-
func StreamServerInterceptor() grpc.StreamServerInterceptor {
105+
func StreamServerInterceptor(all bool) grpc.StreamServerInterceptor {
72106
return func(srv interface{}, stream grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error {
73-
wrapper := &recvWrapper{stream}
107+
wrapper := &recvWrapper{
108+
all: all,
109+
ServerStream: stream,
110+
}
74111
return handler(srv, wrapper)
75112
}
76113
}
77114

78115
type recvWrapper struct {
116+
all bool
79117
grpc.ServerStream
80118
}
81119

82120
func (s *recvWrapper) RecvMsg(m interface{}) error {
83121
if err := s.ServerStream.RecvMsg(m); err != nil {
84122
return err
85123
}
86-
if err := validate(m); err != nil {
124+
if err := validate(m, s.all); err != nil {
87125
return err
88126
}
89127
return nil

interceptors/validator/validator_test.go

+34-8
Original file line numberDiff line numberDiff line change
@@ -18,32 +18,58 @@ import (
1818
)
1919

2020
func TestValidateWrapper(t *testing.T) {
21-
assert.NoError(t, validate(testpb.GoodPing))
22-
assert.Error(t, validate(testpb.BadPing))
23-
24-
assert.NoError(t, validate(testpb.GoodPingResponse))
25-
assert.Error(t, validate(testpb.BadPingResponse))
21+
assert.NoError(t, validate(testpb.GoodPing, false))
22+
assert.Error(t, validate(testpb.BadPing, false))
23+
assert.NoError(t, validate(testpb.GoodPing, true))
24+
assert.Error(t, validate(testpb.BadPing, true))
25+
26+
assert.NoError(t, validate(testpb.GoodPingError, false))
27+
assert.Error(t, validate(testpb.BadPingError, false))
28+
assert.NoError(t, validate(testpb.GoodPingError, true))
29+
assert.Error(t, validate(testpb.BadPingError, true))
30+
31+
assert.NoError(t, validate(testpb.GoodPingResponse, false))
32+
assert.NoError(t, validate(testpb.GoodPingResponse, true))
33+
assert.Error(t, validate(testpb.BadPingResponse, false))
34+
assert.Error(t, validate(testpb.BadPingResponse, true))
2635
}
2736

2837
func TestValidatorTestSuite(t *testing.T) {
2938
s := &ValidatorTestSuite{
3039
InterceptorTestSuite: &testpb.InterceptorTestSuite{
3140
ServerOpts: []grpc.ServerOption{
32-
grpc.StreamInterceptor(StreamServerInterceptor()),
33-
grpc.UnaryInterceptor(UnaryServerInterceptor()),
41+
grpc.StreamInterceptor(StreamServerInterceptor(false)),
42+
grpc.UnaryInterceptor(UnaryServerInterceptor(false)),
3443
},
3544
},
3645
}
3746
suite.Run(t, s)
47+
sAll := &ValidatorTestSuite{
48+
InterceptorTestSuite: &testpb.InterceptorTestSuite{
49+
ServerOpts: []grpc.ServerOption{
50+
grpc.StreamInterceptor(StreamServerInterceptor(true)),
51+
grpc.UnaryInterceptor(UnaryServerInterceptor(true)),
52+
},
53+
},
54+
}
55+
suite.Run(t, sAll)
3856

3957
cs := &ClientValidatorTestSuite{
4058
InterceptorTestSuite: &testpb.InterceptorTestSuite{
4159
ClientOpts: []grpc.DialOption{
42-
grpc.WithUnaryInterceptor(UnaryClientInterceptor()),
60+
grpc.WithUnaryInterceptor(UnaryClientInterceptor(false)),
4361
},
4462
},
4563
}
4664
suite.Run(t, cs)
65+
csAll := &ClientValidatorTestSuite{
66+
InterceptorTestSuite: &testpb.InterceptorTestSuite{
67+
ClientOpts: []grpc.DialOption{
68+
grpc.WithUnaryInterceptor(UnaryClientInterceptor(true)),
69+
},
70+
},
71+
}
72+
suite.Run(t, csAll)
4773
}
4874

4975
type ValidatorTestSuite struct {

testing/testpb/test.manual_validator.pb.go

+9-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ func (x *PingRequest) Validate(bool) error {
1515
return nil
1616
}
1717

18-
func (x *PingErrorRequest) Validate(bool) error {
18+
func (x *PingErrorRequest) Validate() error {
1919
if x.SleepTimeMs > 10000 {
2020
return errors.New("cannot sleep for more than 10s")
2121
}
@@ -44,6 +44,14 @@ func (x *PingResponse) Validate() error {
4444
return nil
4545
}
4646

47+
// Implements the new ValidateAll interface from protoc-gen-validate.
48+
func (x *PingResponse) ValidateAll() error {
49+
if x.Counter > math.MaxInt16 {
50+
return errors.New("ping allocation exceeded")
51+
}
52+
return nil
53+
}
54+
4755
var (
4856
GoodPing = &PingRequest{Value: "something", SleepTimeMs: 9999}
4957
GoodPingError = &PingErrorRequest{Value: "something", SleepTimeMs: 9999}

0 commit comments

Comments
 (0)