diff --git a/binding/bind.go b/binding/bind.go index f22ca78..b3c00ea 100644 --- a/binding/bind.go +++ b/binding/bind.go @@ -418,3 +418,7 @@ func (b *Binding) bindJSON(pointer interface{}, bodyBytes []byte) error { func (b *Binding) ResetJSONUnmarshaler(fn JSONUnmarshaler) { b.jsonUnmarshalFunc = fn } + +func (b *Binding) ResetValidator(vd *validator.Validator) { + b.vd = vd +} \ No newline at end of file diff --git a/go.mod b/go.mod index 1c25523..ea150e3 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,8 @@ module github.com/bytedance/go-tagexpr/v2 -go 1.14 +go 1.22 + +toolchain go1.22.2 require ( github.com/andeya/ameda v1.5.3 @@ -10,5 +12,15 @@ require ( github.com/stretchr/testify v1.7.5 github.com/tidwall/match v1.1.1 github.com/tidwall/pretty v1.2.0 - google.golang.org/protobuf v1.27.1 + google.golang.org/protobuf v1.36.6 +) + +require ( + github.com/golang/protobuf v1.5.0 // indirect + github.com/google/go-cmp v0.5.5 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/stretchr/objx v0.4.0 // indirect + golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 // indirect + gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 79105e4..1aaa1e6 100644 --- a/go.sum +++ b/go.sum @@ -28,6 +28,8 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= +google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/tagexpr.go b/tagexpr.go index 288f351..1661d15 100644 --- a/tagexpr.go +++ b/tagexpr.go @@ -38,10 +38,25 @@ type ( // VM struct tag expression interpreter type VM struct { tagName string + checkPublicOnly bool structJar map[uintptr]*structVM rw sync.RWMutex } +type TagExprOption func(*VM) + +func WithTagName(tagName string) TagExprOption { + return func(vm *VM) { + vm.tagName = tagName + } +} + +func WithCheckPublicOnly() TagExprOption { + return func(vm *VM) { + vm.checkPublicOnly = true + } +} + // structVM tag expression set of struct type structVM struct { vm *VM @@ -291,6 +306,9 @@ func (vm *VM) registerStructLocked(structType reflect.Type) (*structVM, error) { var sub *structVM for i := 0; i < numField; i++ { structField = structType.Field(i) + if vm.checkPublicOnly && structField.PkgPath != "" { + continue // skip private fields + } field, ok, err := s.newFieldVM(structField) if err != nil { s.err = err diff --git a/test_data/demo.pb.go b/test_data/demo.pb.go new file mode 100644 index 0000000..bf6bd7d --- /dev/null +++ b/test_data/demo.pb.go @@ -0,0 +1,222 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.28.0 +// protoc v5.27.1 +// source: demo.proto + +package test_data + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +type EchoRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Msg string `protobuf:"bytes,1,opt,name=msg,proto3" form:"msg" json:"msg,omitempty" vd:"@:len($) >= 3"` + Subs []*EchoRequest_SubMessage `protobuf:"bytes,2,rep,name=subs,proto3" form:"subs" json:"subs,omitempty" query:"subs"` +} + +func (x *EchoRequest) Reset() { + *x = EchoRequest{} + if protoimpl.UnsafeEnabled { + mi := &file_demo_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *EchoRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EchoRequest) ProtoMessage() {} + +func (x *EchoRequest) ProtoReflect() protoreflect.Message { + mi := &file_demo_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EchoRequest.ProtoReflect.Descriptor instead. +func (*EchoRequest) Descriptor() ([]byte, []int) { + return file_demo_proto_rawDescGZIP(), []int{0} +} + +func (x *EchoRequest) GetMsg() string { + if x != nil { + return x.Msg + } + return "" +} + +func (x *EchoRequest) GetSubs() []*EchoRequest_SubMessage { + if x != nil { + return x.Subs + } + return nil +} + +type EchoRequest_SubMessage struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Msg string `protobuf:"bytes,1,opt,name=msg,proto3" form:"msg" json:"msg,omitempty" query:"msg" vd:"len($) >= 4"` +} + +func (x *EchoRequest_SubMessage) Reset() { + *x = EchoRequest_SubMessage{} + if protoimpl.UnsafeEnabled { + mi := &file_demo_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *EchoRequest_SubMessage) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*EchoRequest_SubMessage) ProtoMessage() {} + +func (x *EchoRequest_SubMessage) ProtoReflect() protoreflect.Message { + mi := &file_demo_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use EchoRequest_SubMessage.ProtoReflect.Descriptor instead. +func (*EchoRequest_SubMessage) Descriptor() ([]byte, []int) { + return file_demo_proto_rawDescGZIP(), []int{0, 0} +} + +func (x *EchoRequest_SubMessage) GetMsg() string { + if x != nil { + return x.Msg + } + return "" +} + +var File_demo_proto protoreflect.FileDescriptor + +var file_demo_proto_rawDesc = []byte{ + 0x0a, 0x0a, 0x64, 0x65, 0x6d, 0x6f, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x09, 0x74, 0x65, + 0x73, 0x74, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x1a, 0x09, 0x61, 0x70, 0x69, 0x2e, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x22, 0xa1, 0x01, 0x0a, 0x0b, 0x45, 0x63, 0x68, 0x6f, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x2a, 0x0a, 0x03, 0x6d, 0x73, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x42, + 0x18, 0xca, 0xbb, 0x18, 0x03, 0x6d, 0x73, 0x67, 0xda, 0xbb, 0x18, 0x0d, 0x40, 0x3a, 0x6c, 0x65, + 0x6e, 0x28, 0x24, 0x29, 0x20, 0x3e, 0x3d, 0x20, 0x33, 0x52, 0x03, 0x6d, 0x73, 0x67, 0x12, 0x35, + 0x0a, 0x04, 0x73, 0x75, 0x62, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x21, 0x2e, 0x74, + 0x65, 0x73, 0x74, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x2e, 0x45, 0x63, 0x68, 0x6f, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x2e, 0x53, 0x75, 0x62, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, + 0x04, 0x73, 0x75, 0x62, 0x73, 0x1a, 0x2f, 0x0a, 0x0a, 0x53, 0x75, 0x62, 0x4d, 0x65, 0x73, 0x73, + 0x61, 0x67, 0x65, 0x12, 0x21, 0x0a, 0x03, 0x6d, 0x73, 0x67, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x42, 0x0f, 0xda, 0xbb, 0x18, 0x0b, 0x6c, 0x65, 0x6e, 0x28, 0x24, 0x29, 0x20, 0x3e, 0x3d, 0x20, + 0x34, 0x52, 0x03, 0x6d, 0x73, 0x67, 0x42, 0x2e, 0x5a, 0x2c, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, + 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x62, 0x79, 0x74, 0x65, 0x64, 0x61, 0x6e, 0x63, 0x65, 0x2f, 0x67, + 0x6f, 0x2d, 0x74, 0x61, 0x67, 0x65, 0x78, 0x70, 0x72, 0x2f, 0x76, 0x32, 0x2f, 0x74, 0x65, 0x73, + 0x74, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_demo_proto_rawDescOnce sync.Once + file_demo_proto_rawDescData = file_demo_proto_rawDesc +) + +func file_demo_proto_rawDescGZIP() []byte { + file_demo_proto_rawDescOnce.Do(func() { + file_demo_proto_rawDescData = protoimpl.X.CompressGZIP(file_demo_proto_rawDescData) + }) + return file_demo_proto_rawDescData +} + +var file_demo_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_demo_proto_goTypes = []interface{}{ + (*EchoRequest)(nil), // 0: test_data.EchoRequest + (*EchoRequest_SubMessage)(nil), // 1: test_data.EchoRequest.SubMessage +} +var file_demo_proto_depIdxs = []int32{ + 1, // 0: test_data.EchoRequest.subs:type_name -> test_data.EchoRequest.SubMessage + 1, // [1:1] is the sub-list for method output_type + 1, // [1:1] is the sub-list for method input_type + 1, // [1:1] is the sub-list for extension type_name + 1, // [1:1] is the sub-list for extension extendee + 0, // [0:1] is the sub-list for field type_name +} + +func init() { file_demo_proto_init() } +func file_demo_proto_init() { + if File_demo_proto != nil { + return + } + file_api_proto_init() + if !protoimpl.UnsafeEnabled { + file_demo_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*EchoRequest); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_demo_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*EchoRequest_SubMessage); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_demo_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_demo_proto_goTypes, + DependencyIndexes: file_demo_proto_depIdxs, + MessageInfos: file_demo_proto_msgTypes, + }.Build() + File_demo_proto = out.File + file_demo_proto_rawDesc = nil + file_demo_proto_goTypes = nil + file_demo_proto_depIdxs = nil +} diff --git a/validator/validator.go b/validator/validator.go index 577bffc..faaab8e 100644 --- a/validator/validator.go +++ b/validator/validator.go @@ -48,6 +48,24 @@ func New(tagName string) *Validator { return v } +var ( + WithTagName = tagexpr.WithTagName + WithCheckPublicOnly = tagexpr.WithCheckPublicOnly +) + +func NewValidator(opts ...tagexpr.TagExprOption) *Validator { + vm := tagexpr.New("") + for _, opt := range opts { + opt(vm) + } + + v := &Validator{ + vm: vm, + errFactory: defaultErrorFactory, + } + return v +} + // VM returns the struct tag expression interpreter. func (v *Validator) VM() *tagexpr.VM { return v.vm diff --git a/validator/validator_pb_test.go b/validator/validator_pb_test.go new file mode 100644 index 0000000..bc251da --- /dev/null +++ b/validator/validator_pb_test.go @@ -0,0 +1,91 @@ +package validator_test + +import ( + "encoding/json" + "testing" + + "github.com/bytedance/go-tagexpr/v2/test_data" + vd "github.com/bytedance/go-tagexpr/v2/validator" + "github.com/stretchr/testify/assert" + "google.golang.org/protobuf/encoding/protojson" + "google.golang.org/protobuf/proto" +) + +func TestVdFailWithPbMsg(t *testing.T) { + // Protobuf message got from protojson does not work + td := &test_data.EchoRequest{} + assert.NoError(t, protojson.Unmarshal([]byte(`{"msg": "hello"}`), td)) + assert.Equal(t, "hello", td.Msg) + assert.Error(t, vd.Validate(td), "unsupport data: nil") + + td1 := proto.Clone(td).(*test_data.EchoRequest) + assert.Equal(t, "hello", td1.Msg) + assert.Error(t, vd.Validate(td1), "unsupport data: nil") + + td2 := &test_data.EchoRequest{} + proto.Merge(td2, td) + assert.Equal(t, "hello", td2.Msg) + assert.Error(t, vd.Validate(td2), "unsupport data: nil") +} + +func TestVdWithPbMsg(t *testing.T) { + // Direct assign value works + { + td := &test_data.EchoRequest{Msg: "hello"} + assert.NoError(t, vd.Validate(td)) + } + { + td := &test_data.EchoRequest{Msg: "hi"} + assert.ErrorContains(t, vd.Validate(td), "invalid parameter: Msg") + } + // json works + { + td := &test_data.EchoRequest{} + assert.NoError(t, json.Unmarshal([]byte(`{"msg": "hello"}`), td)) + assert.Equal(t, "hello", td.Msg) + assert.NoError(t, vd.Validate(td)) + } + { + td := &test_data.EchoRequest{} + assert.NoError(t, json.Unmarshal([]byte(`{"msg": "hi"}`), td)) + assert.Equal(t, "hi", td.Msg) + assert.ErrorContains(t, vd.Validate(td), "invalid parameter: Msg") + } + // protojson works sometime for failure check + { + td := &test_data.EchoRequest{} + assert.NoError(t, protojson.Unmarshal([]byte(`{"msg": "hi"}`), td)) + assert.Equal(t, "hi", td.Msg) + assert.ErrorContains(t, vd.Validate(td), "invalid parameter: Msg") + } +} + +func TestVdFixedWithPbMsg(t *testing.T) { + validator := vd.NewValidator(vd.WithTagName("vd"), vd.WithCheckPublicOnly()) + { + td := &test_data.EchoRequest{} + assert.NoError(t, protojson.Unmarshal([]byte(`{"msg": "hello"}`), td)) + assert.Equal(t, "hello", td.Msg) + assert.NoError(t, validator.Validate(td)) + } + { + td := &test_data.EchoRequest{} + assert.NoError(t, protojson.Unmarshal([]byte(`{"msg": "hi"}`), td)) + assert.Equal(t, "hi", td.Msg) + assert.ErrorContains(t, validator.Validate(td), "invalid parameter: Msg") + } + { + td := &test_data.EchoRequest{} + assert.NoError(t, protojson.Unmarshal([]byte(`{"msg": "hello", "subs": [{"msg": "world"}]}`), td)) + assert.Equal(t, "hello", td.Msg) + assert.Equal(t, "world", td.Subs[0].Msg) + assert.NoError(t, validator.Validate(td)) + } + { + td := &test_data.EchoRequest{} + assert.NoError(t, protojson.Unmarshal([]byte(`{"msg": "hello", "subs": [{"msg": "bob"}]}`), td)) + assert.Equal(t, "hello", td.Msg) + assert.Equal(t, "bob", td.Subs[0].Msg) + assert.ErrorContains(t, validator.Validate(td), "invalid parameter: Subs[0].Msg") + } +}