This is an automated email from the ASF dual-hosted git repository. vinci pushed a commit to branch refactor in repository https://gitbox.apache.org/repos/asf/apisix-dashboard.git
commit dda1cb9e14ac0eefe17af1d29e0677596752d061 Author: ShiningRush <[email protected]> AuthorDate: Fri Sep 18 21:51:52 2020 +0800 feat: add validator for generic store; add demo error --- api/go.mod | 6 ++- api/go.sum | 8 ++++ api/internal/core/store/store.go | 19 +++++++-- api/internal/core/store/store_test.go | 71 +++++++++++++++++++++++-------- api/internal/core/store/test_case.json | 19 +++++++++ api/internal/core/store/validate.go | 49 +++++++++++++++++++++ api/internal/core/store/validate_test.go | 51 ++++++++++++++++++++++ api/internal/core/store/validator_mock.go | 24 +++++++++++ api/internal/utils/consts/error.go | 12 ++++++ 9 files changed, 237 insertions(+), 22 deletions(-) diff --git a/api/go.mod b/api/go.mod index 9d230c1..af5cb74 100644 --- a/api/go.mod +++ b/api/go.mod @@ -15,16 +15,18 @@ require ( github.com/gogo/protobuf v1.3.1 // indirect github.com/google/uuid v1.1.2 // indirect github.com/jinzhu/gorm v1.9.12 + github.com/magiconair/properties v1.8.1 github.com/satori/go.uuid v1.2.0 - github.com/shiningrush/droplet v0.1.0 + github.com/shiningrush/droplet v0.1.1 github.com/shiningrush/droplet/wrapper/gin v0.1.0 github.com/sirupsen/logrus v1.6.0 github.com/spf13/viper v1.7.1 github.com/steinfletcher/apitest v1.4.10 // indirect github.com/stretchr/testify v1.6.1 github.com/tidwall/gjson v1.6.0 + github.com/xeipuuv/gojsonschema v1.2.0 go.etcd.io/etcd v3.3.25+incompatible - go.uber.org/zap v1.16.0 // indirect + go.uber.org/zap v1.16.0 golang.org/x/net v0.0.0-20200904194848-62affa334b73 // indirect golang.org/x/sys v0.0.0-20200915084602-288bc346aa39 // indirect golang.org/x/text v0.3.3 // indirect diff --git a/api/go.sum b/api/go.sum index e99f700..7f9aa90 100644 --- a/api/go.sum +++ b/api/go.sum @@ -276,6 +276,8 @@ github.com/shiningrush/droplet v0.0.0-20191118073048-00b06fe19ce4 h1:p2mP/ZZegqn github.com/shiningrush/droplet v0.0.0-20191118073048-00b06fe19ce4/go.mod h1:E/th13n/wtPi+Cj2f0hAAEFeT3gb5xsS6Ob4WRrdxdM= github.com/shiningrush/droplet v0.1.0 h1:Lk/nzfouI8Xqv9VtzNZZISwTVxWeXmEd9IlgIZwU9IA= github.com/shiningrush/droplet v0.1.0/go.mod h1:akW2vIeamvMD6zj6wIBfzYn6StGXBxwlW3gA+hcHu5M= +github.com/shiningrush/droplet v0.1.1 h1:x+69JP60jzq6ROmsDooNhVSX8jhwFEmjBnryVvCHgjc= +github.com/shiningrush/droplet v0.1.1/go.mod h1:akW2vIeamvMD6zj6wIBfzYn6StGXBxwlW3gA+hcHu5M= github.com/shiningrush/droplet/wrapper/gin v0.1.0 h1:eKUtuInaz8BH9dwjDnpdnP29iH7bhaB0NIOF9tL7nFM= github.com/shiningrush/droplet/wrapper/gin v0.1.0/go.mod h1:ZJu+sCRrVXn5Pg618c1KK3Ob2UiXGuPM1ROx5uMM9YQ= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= @@ -324,6 +326,12 @@ github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f h1:J9EGpcZtP0E/raorCMxlFGSTBrsSlaDGf3jU/qvAE2c= +github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= +github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= +github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= +github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= go.etcd.io/bbolt v1.3.2 h1:Z/90sZLPOeCy2PwprqkFa25PdkusRzaj9P8zm/KNyvk= diff --git a/api/internal/core/store/store.go b/api/internal/core/store/store.go index f9d6a47..2d199d7 100644 --- a/api/internal/core/store/store.go +++ b/api/internal/core/store/store.go @@ -20,9 +20,10 @@ type GenericStore struct { } type GenericStoreOption struct { - BasePath string - ObjType reflect.Type - KeyFunc func(obj interface{}) string + BasePath string + ObjType reflect.Type + KeyFunc func(obj interface{}) string + Validator Validator } func NewGenericStore(opt GenericStoreOption) (*GenericStore, error) { @@ -146,6 +147,12 @@ func (s *GenericStore) List(input ListInput) (*ListOutput, error) { } func (s *GenericStore) Create(ctx context.Context, obj interface{}) error { + if s.opt.Validator != nil { + if err := s.opt.Validator.Validate(obj); err != nil { + return err + } + } + key := s.opt.KeyFunc(obj) if key == "" { return fmt.Errorf("key is required") @@ -167,6 +174,12 @@ func (s *GenericStore) Create(ctx context.Context, obj interface{}) error { } func (s *GenericStore) Update(ctx context.Context, obj interface{}) error { + if s.opt.Validator != nil { + if err := s.opt.Validator.Validate(obj); err != nil { + return err + } + } + key := s.opt.KeyFunc(obj) if key == "" { return fmt.Errorf("key is required") diff --git a/api/internal/core/store/store_test.go b/api/internal/core/store/store_test.go index 5d295a1..4895258 100644 --- a/api/internal/core/store/store_test.go +++ b/api/internal/core/store/store_test.go @@ -399,13 +399,14 @@ func TestGenericStore_List(t *testing.T) { func TestGenericStore_Create(t *testing.T) { tests := []struct { - caseDesc string - giveStore *GenericStore - giveObj *TestStruct - giveErr error - wantKey string - wantStr string - wantErr error + caseDesc string + giveStore *GenericStore + giveObj *TestStruct + giveErr error + giveValidateErr error + wantKey string + wantStr string + wantErr error }{ { caseDesc: "sanity", @@ -462,10 +463,25 @@ func TestGenericStore_Create(t *testing.T) { }, wantErr: fmt.Errorf("key: test1 is conflicted"), }, + { + caseDesc: "validate failed", + giveObj: &TestStruct{ + Field1: "test1", + Field2: "test2", + }, + giveStore: &GenericStore{ + cache: map[string]interface{}{ + "test1": struct{}{}, + }, + opt: GenericStoreOption{}, + }, + giveValidateErr: fmt.Errorf("validate failed"), + wantErr: fmt.Errorf("validate failed"), + }, } for _, tc := range tests { - createCalled := false + createCalled, validateCalled := false, false mStorage := &storage.MockInterface{} mStorage.On("Create", mock.Anything, mock.Anything, mock.Anything).Run(func(args mock.Arguments) { createCalled = true @@ -473,22 +489,34 @@ func TestGenericStore_Create(t *testing.T) { assert.Equal(t, tc.wantStr, args[2], tc.caseDesc) }).Return(tc.giveErr) + mValidator := &MockValidator{} + mValidator.On("Validate", mock.Anything).Run(func(args mock.Arguments) { + validateCalled = true + assert.Equal(t, tc.giveObj, args.Get(0), tc.caseDesc) + }).Return(tc.giveValidateErr) + tc.giveStore.Stg = mStorage + tc.giveStore.opt.Validator = mValidator err := tc.giveStore.Create(context.TODO(), tc.giveObj) + assert.True(t, validateCalled, tc.caseDesc) + if err != nil { + assert.Equal(t, tc.wantErr, err, tc.caseDesc) + continue + } assert.True(t, createCalled, tc.caseDesc) - assert.Equal(t, tc.wantErr, err, tc.caseDesc) } } func TestGenericStore_Update(t *testing.T) { tests := []struct { - caseDesc string - giveStore *GenericStore - giveObj *TestStruct - giveErr error - wantKey string - wantStr string - wantErr error + caseDesc string + giveStore *GenericStore + giveObj *TestStruct + giveErr error + giveValidateErr error + wantKey string + wantStr string + wantErr error }{ { caseDesc: "sanity", @@ -554,7 +582,7 @@ func TestGenericStore_Update(t *testing.T) { } for _, tc := range tests { - createCalled := false + createCalled, validateCalled := false, false mStorage := &storage.MockInterface{} mStorage.On("Update", mock.Anything, mock.Anything, mock.Anything).Run(func(args mock.Arguments) { createCalled = true @@ -562,8 +590,17 @@ func TestGenericStore_Update(t *testing.T) { assert.Equal(t, tc.wantStr, args[2], tc.caseDesc) }).Return(tc.giveErr) + mValidator := &MockValidator{} + mValidator.On("Validate", mock.Anything).Run(func(args mock.Arguments) { + validateCalled = true + assert.Equal(t, tc.giveObj, args.Get(0), tc.caseDesc) + }).Return(tc.giveValidateErr) + tc.giveStore.Stg = mStorage + tc.giveStore.opt.Validator = mValidator + err := tc.giveStore.Update(context.TODO(), tc.giveObj) + assert.True(t, validateCalled, tc.caseDesc) if err != nil { assert.Equal(t, tc.wantErr, err, tc.caseDesc) continue diff --git a/api/internal/core/store/test_case.json b/api/internal/core/store/test_case.json new file mode 100644 index 0000000..77f524b --- /dev/null +++ b/api/internal/core/store/test_case.json @@ -0,0 +1,19 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "properties": { + "name": { + "type": "string", + "minLength": 10 + }, + "email": { + "type": "string", + "maxLength": 10 + }, + "age": { + "type": "integer", + "minimum": 0 + } + }, + "additionalProperties": false +} diff --git a/api/internal/core/store/validate.go b/api/internal/core/store/validate.go new file mode 100644 index 0000000..1ee0960 --- /dev/null +++ b/api/internal/core/store/validate.go @@ -0,0 +1,49 @@ +package store + +import ( + "errors" + "fmt" + "github.com/xeipuuv/gojsonschema" + "go.uber.org/zap/buffer" + "io/ioutil" +) + +type Validator interface { + Validate(obj interface{}) error +} +type JsonSchemaValidator struct { + schema *gojsonschema.Schema +} + +func NewJsonSchemaValidator(jsonPath string) (Validator, error) { + bs, err := ioutil.ReadFile(jsonPath) + if err != nil { + return nil, fmt.Errorf("get abs path failed: %w", err) + } + s, err := gojsonschema.NewSchema(gojsonschema.NewStringLoader(string(bs))) + if err != nil { + return nil, fmt.Errorf("new schema failed: %w", err) + } + return &JsonSchemaValidator{ + schema: s, + }, nil +} + +func (v *JsonSchemaValidator) Validate(obj interface{}) error { + ret, err := v.schema.Validate(gojsonschema.NewGoLoader(obj)) + if err != nil { + return fmt.Errorf("validate failed: %w", err) + } + + if !ret.Valid() { + errString := buffer.Buffer{} + for i, vErr := range ret.Errors() { + if i != 0 { + errString.AppendString("\n") + } + errString.AppendString(vErr.String()) + } + return errors.New(errString.String()) + } + return nil +} diff --git a/api/internal/core/store/validate_test.go b/api/internal/core/store/validate_test.go new file mode 100644 index 0000000..f7d1ac9 --- /dev/null +++ b/api/internal/core/store/validate_test.go @@ -0,0 +1,51 @@ +package store + +import ( + "fmt" + "github.com/stretchr/testify/assert" + "testing" +) + +type TestOjb struct { + Name string `json:"name"` + Email string `json:"email"` + Age int `json:"age"` +} + +func TestJsonSchemaValidator_Validate(t *testing.T) { + tests := []struct { + givePath string + giveObj interface{} + wantNewErr error + wantValidateErr []error + }{ + { + givePath: "./test_case.json", + giveObj: TestOjb{ + Name: "lessName", + Email: "too long name greater than 10", + Age: 12, + }, + wantValidateErr: []error{ + fmt.Errorf("name: String length must be greater than or equal to 10\nemail: String length must be less than or equal to 10"), + fmt.Errorf("email: String length must be less than or equal to 10\nname: String length must be greater than or equal to 10"), + }, + }, + } + + for _, tc := range tests { + v, err := NewJsonSchemaValidator(tc.givePath) + if err != nil { + assert.Equal(t, tc.wantNewErr, err) + continue + } + err = v.Validate(tc.giveObj) + ret := false + for _, wantErr := range tc.wantValidateErr { + if wantErr.Error() == err.Error() { + ret = true + } + } + assert.True(t, ret) + } +} diff --git a/api/internal/core/store/validator_mock.go b/api/internal/core/store/validator_mock.go new file mode 100644 index 0000000..643ba78 --- /dev/null +++ b/api/internal/core/store/validator_mock.go @@ -0,0 +1,24 @@ +// Code generated by mockery v1.0.0. DO NOT EDIT. + +package store + +import mock "github.com/stretchr/testify/mock" + +// MockValidator is an autogenerated mock type for the Validator type +type MockValidator struct { + mock.Mock +} + +// Validate provides a mock function with given fields: obj +func (_m *MockValidator) Validate(obj interface{}) error { + ret := _m.Called(obj) + + var r0 error + if rf, ok := ret.Get(0).(func(interface{}) error); ok { + r0 = rf(obj) + } else { + r0 = ret.Error(0) + } + + return r0 +} diff --git a/api/internal/utils/consts/error.go b/api/internal/utils/consts/error.go new file mode 100644 index 0000000..7abee58 --- /dev/null +++ b/api/internal/utils/consts/error.go @@ -0,0 +1,12 @@ +package consts + +import "github.com/shiningrush/droplet/data" + +const ( + ErrCodeDemoBiz = 20000 +) + +var ( + // base error please refer to github.com/shiningrush/droplet/data, such as data.ErrNotFound, data.ErrConflicted + ErrDemoBiz = data.BaseError{Code: ErrCodeDemoBiz, Message: "this is just a demo error"} +)
