You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@apisix.apache.org by vi...@apache.org on 2020/09/18 14:04:53 UTC

[apisix-dashboard] 01/02: feat: add validator for generic store; add demo error

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 <27...@qq.com>
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"}
+)