Browse Source

升级v3版本

fancl 2 years ago
commit
2e44e96d24
22 changed files with 3452 additions and 0 deletions
  1. 44 0
      .gitignore
  2. 0 0
      README.md
  3. 16 0
      cmd/main.go
  4. 329 0
      crud.go
  5. 117 0
      delegate.go
  6. 13 0
      error/error.go
  7. 196 0
      formatter.go
  8. 225 0
      generator.go
  9. 24 0
      generator_test.go
  10. 24 0
      go.mod
  11. 202 0
      go.sum
  12. 408 0
      inflector/inflector.go
  13. 54 0
      instance.go
  14. 124 0
      model.go
  15. 58 0
      options.go
  16. 54 0
      plugins/snowflake_id.go
  17. 271 0
      plugins/validate.go
  18. 31 0
      proto.go
  19. 369 0
      query.go
  20. 699 0
      rest.go
  21. 166 0
      schema.go
  22. 28 0
      utils/empty.go

+ 44 - 0
.gitignore

@@ -0,0 +1,44 @@
+bin/
+
+.svn/
+.godeps
+./build
+.cover/
+dist
+_site
+_posts
+*.dat
+.vscode
+vendor
+
+# Go.gitignore
+
+# Compiled Object files, Static and Dynamic libs (Shared Objects)
+*.o
+*.a
+*.so
+
+# Folders
+_obj
+_test
+storage
+.idea
+
+# Architecture specific extensions/prefixes
+*.[568vq]
+[568vq].out
+
+*.cgo1.go
+*.cgo2.c
+_cgo_defun.c
+_cgo_gotypes.go
+_cgo_export.*
+
+_testmain.go
+
+*.exe
+
+profile
+
+# vim stuff
+*.sw[op]

+ 0 - 0
README.md


+ 16 - 0
cmd/main.go

@@ -0,0 +1,16 @@
+package main
+
+import (
+	"fmt"
+	"git.nspix.com/golang/rest/v3"
+	"gorm.io/driver/mysql"
+)
+
+func main() {
+	if err := rest.Init(mysql.New(mysql.Config{
+		DriverName: "mysql",
+		DSN:        "root:root@tcp(192.168.9.199:3306)/cci?checkConnLiveness=false&maxAllowedPacket=0",
+	})); err != nil {
+		fmt.Println(err.Error())
+	}
+}

+ 329 - 0
crud.go

@@ -0,0 +1,329 @@
+package rest
+
+import (
+	"context"
+	"fmt"
+	"git.nspix.com/golang/micro/gateway/http"
+	"gorm.io/gorm"
+	"gorm.io/gorm/clause"
+	"strings"
+)
+
+const (
+	DefaultNamespace = "default"
+
+	NamespaceVariable  = "namespace"
+	UserVariable       = "@uid"
+	DepartmentVariable = "@department"
+
+	NamespaceField   = "namespace"
+	CreatedByField   = "CreatedBy"
+	CreatedDeptField = "CreatedDept"
+	UpdatedByField   = "UpdatedBy"
+	UpdatedDeptField = "UpdatedDept"
+)
+
+const (
+	TypeModule = "module"
+	TypeTable  = "table"
+)
+
+type (
+	CRUD struct {
+		db       *gorm.DB
+		modules  []*Restful
+		delegate *delegate
+	}
+
+	treeValue struct {
+		Label    string       `json:"label"`
+		Value    string       `json:"value"`
+		Type     string       `json:"type"`
+		Children []*treeValue `json:"children,omitempty"`
+	}
+)
+
+func (t *treeValue) Append(v *treeValue) {
+	if t.Children == nil {
+		t.Children = make([]*treeValue, 0)
+	}
+	t.Children = append(t.Children, v)
+}
+
+//createStatement create new statement
+func (r *CRUD) createStatement(db *gorm.DB) *gorm.Statement {
+	return &gorm.Statement{
+		DB:       db,
+		ConnPool: db.Statement.ConnPool,
+		Context:  db.Statement.Context,
+		Clauses:  map[string]clause.Clause{},
+	}
+}
+
+func (r *CRUD) RegisterDelegate(d Delegate) {
+	r.delegate.Register(d)
+}
+
+//Attach 注册一个表
+func (r *CRUD) Attach(ctx context.Context, model Model, cbs ...Option) (err error) {
+	var (
+		opts *Options
+	)
+	opts = newOptions()
+	for _, cb := range cbs {
+		cb(opts)
+	}
+	if opts.DB == nil {
+		opts.DB = r.db
+	}
+	if err = r.db.AutoMigrate(model); err != nil {
+		return
+	}
+	if err = r.migrateSchema(ctx, DefaultNamespace, model); err != nil {
+		return
+	}
+	opts.Delegate = r.delegate
+	opts.LookupFunc = r.VisibleSchemas
+	r.modules = append(r.modules, newRestful(model, opts))
+	return
+}
+
+//migrateSchema 合并表的结构数据
+func (r *CRUD) migrateSchema(ctx context.Context, namespace string, model Model) (err error) {
+	var (
+		pos            int
+		columnLabel    string
+		columnName     string
+		columnIsExists bool
+		stmt           *gorm.Statement
+		schemas        []*Schema
+		schemaModels   []*Schema
+	)
+	schemas, err = r.GetSchemas(ctx, namespace, model.ModuleName(), model.TableName())
+	stmt = r.createStatement(r.db)
+	if err = stmt.Parse(model); err != nil {
+		return
+	}
+	if len(schemas) > 0 {
+		pos = len(schemas)
+	}
+	for index, field := range stmt.Schema.Fields {
+		columnName = field.DBName
+		if columnName == "-" {
+			continue
+		}
+		if columnName == "" {
+			columnName = field.Name
+		}
+		columnIsExists = false
+		for _, sm := range schemas {
+			if sm.Column == columnName {
+				columnIsExists = true
+				break
+			}
+		}
+		if columnIsExists {
+			continue
+		}
+		columnLabel = field.Tag.Get("comment")
+		if columnLabel == "" {
+			columnLabel = generateFieldName(field.DBName)
+		}
+		isPrimaryKey := uint8(0)
+		if field.PrimaryKey {
+			isPrimaryKey = 1
+		}
+		schemaModel := &Schema{
+			Namespace:    namespace,
+			ModuleName:   model.ModuleName(),
+			TableName:    model.TableName(),
+			Enable:       1,
+			Column:       columnName,
+			Label:        columnLabel,
+			Type:         strings.ToLower(dataTypeOf(field)),
+			Format:       strings.ToLower(dataFormatOf(field)),
+			Native:       1,
+			IsPrimaryKey: isPrimaryKey,
+			Scenarios:    generateFieldScenario(index, field),
+			Rule:         generateFieldRule(field),
+			Attribute:    generateFieldAttribute(field),
+			Position:     pos,
+		}
+		schemaModels = append(schemaModels, schemaModel)
+		pos++
+	}
+	if len(schemaModels) > 0 {
+		err = r.db.Create(schemaModels).Error
+	}
+	return
+}
+
+//GetSchemas 获取表的结构数据
+func (r *CRUD) GetSchemas(ctx context.Context, namespace, moduleName, tableName string) (schemas []*Schema, err error) {
+	schemas = make([]*Schema, 0)
+	if len(namespace) == 0 {
+		namespace = DefaultNamespace
+	}
+	sess := r.db.Session(&gorm.Session{NewDB: true, Context: ctx})
+	err = sess.Where("`namespace`=? AND `module_name`=? AND `table_name`=?", namespace, moduleName, tableName).Order("position,id ASC").Find(&schemas).Error
+	return
+}
+
+//VisibleSchemas 获取指定场景表的数据
+func (r *CRUD) VisibleSchemas(ctx context.Context, ns, moduleName, tableName, scene string) (result []*Schema) {
+	var (
+		err     error
+		schemas []*Schema
+	)
+	if schemas, err = r.GetSchemas(ctx, ns, moduleName, tableName); err == nil {
+		result = make([]*Schema, 0, len(schemas))
+		for _, s := range schemas {
+			if s.Scenarios.Has(scene) || s.Attribute.PrimaryKey {
+				result = append(result, s)
+			}
+		}
+	}
+	return
+}
+
+func (r *CRUD) handleListSchema(httpCtx *http.Context) (err error) {
+	var (
+		schemas []*Schema
+	)
+	ns := httpCtx.FormValue(NamespaceVariable)
+	if ns == "" {
+		ns = DefaultNamespace
+	}
+	if schemas, err = r.GetSchemas(httpCtx.Request().Context(), ns, httpCtx.ParamValue("module"), httpCtx.ParamValue("table")); err == nil {
+		return httpCtx.Success(schemas)
+	} else {
+		return httpCtx.Error(HttpDatabaseQueryFailed, err.Error())
+	}
+}
+
+func (r *CRUD) handleSaveSchema(httpCtx *http.Context) (err error) {
+	var (
+		rest *Restful
+	)
+	ns := httpCtx.FormValue(NamespaceVariable)
+	if ns == "" {
+		ns = DefaultNamespace
+	}
+	moduleName := httpCtx.ParamValue("module")
+	tableName := httpCtx.ParamValue("table")
+	for _, row := range r.modules {
+		if row.model.ModuleName() == moduleName && row.model.TableName() == tableName {
+			rest = row
+			break
+		}
+	}
+	if rest == nil {
+		return httpCtx.Error(HTTPUnknownFailed, fmt.Sprintf("module %s table %s schema not found", moduleName, tableName))
+	}
+	schemas := make([]*Schema, 0)
+	if err = httpCtx.Bind(&schemas); err != nil {
+		return httpCtx.Error(HttpInvalidPayload, err.Error())
+	}
+	if err = r.db.Transaction(func(tx *gorm.DB) error {
+		var (
+			errTx error
+		)
+		for _, scm := range schemas {
+			if errTx = tx.Save(scm).Error; errTx != nil {
+				return errTx
+			}
+		}
+		return nil
+	}); err == nil {
+		return httpCtx.Success(map[string]interface{}{
+			"count": len(schemas),
+			"state": "success",
+		})
+	} else {
+		return httpCtx.Error(HTTPUnknownFailed, err.Error())
+	}
+}
+
+func (r *CRUD) handleListSchemas(httpCtx *http.Context) (err error) {
+	var (
+		moduleLabel string
+	)
+	ts := make([]*treeValue, 0)
+	isHandled := false
+	for _, e := range r.modules {
+		isHandled = false
+		for _, tv := range ts {
+			if tv.Value == e.model.ModuleName() {
+				tv.Append(&treeValue{Value: e.model.TableName(), Label: e.model.TableName(), Type: TypeTable})
+				isHandled = true
+				break
+			}
+		}
+		if isHandled {
+			continue
+		}
+		moduleLabel = e.model.ModuleName()
+		tv := &treeValue{Label: moduleLabel, Value: e.model.ModuleName(), Type: TypeModule}
+		tv.Append(&treeValue{Value: e.model.TableName(), Label: e.model.TableName(), Type: TypeTable})
+		ts = append(ts, tv)
+	}
+	return httpCtx.Success(ts)
+}
+
+func (r *CRUD) handleDeleteSchema(httpCtx *http.Context) (err error) {
+	id := httpCtx.ParamValue("id")
+	model := &Schema{}
+	if err = r.db.Where("id=?", id).First(model).Error; err == nil {
+		if err = r.db.Where("id=?", id).Delete(&Schema{}).Error; err == nil {
+			return httpCtx.Success(map[string]string{
+				"id":    id,
+				"state": "success",
+			})
+		} else {
+			return httpCtx.Error(HttpDatabaseDeleteFailed, err.Error())
+		}
+	} else {
+		return httpCtx.Error(HttpDatabaseFindFailed, err.Error())
+	}
+}
+
+func (r *CRUD) Router(svr *http.Server, ms ...http.Middleware) {
+	svr.Handle("GET", "/rest/schemas", r.handleListSchemas, ms...)
+	svr.Handle("GET", "/rest/schema/:module/:table", r.handleListSchema, ms...)
+	svr.Handle("PUT", "/rest/schema/:module/:table", r.handleSaveSchema, ms...)
+	svr.Handle("DELETE", "/rest/schema/:id", r.handleDeleteSchema, ms...)
+
+	for _, rest := range r.modules {
+		if rest.hasScenario(ScenarioList) {
+			svr.Handle("GET", rest.getScenarioUrl(ScenarioList), rest.getScenarioHandle(ScenarioList), ms...)
+		}
+		if rest.hasScenario(ScenarioCreate) {
+			svr.Handle("POST", rest.getScenarioUrl(ScenarioCreate), rest.getScenarioHandle(ScenarioCreate), ms...)
+		}
+		if rest.hasScenario(ScenarioUpdate) {
+			svr.Handle("PUT", rest.getScenarioUrl(ScenarioUpdate), rest.getScenarioHandle(ScenarioUpdate), ms...)
+		}
+		if rest.hasScenario(ScenarioDelete) {
+			svr.Handle("DELETE", rest.getScenarioUrl(ScenarioDelete), rest.getScenarioHandle(ScenarioDelete), ms...)
+		}
+		if rest.hasScenario(ScenarioExport) {
+			svr.Handle("GET", rest.getScenarioUrl(ScenarioExport), rest.getScenarioHandle(ScenarioExport), ms...)
+		}
+		if rest.hasScenario(ScenarioView) {
+			svr.Handle("GET", rest.getScenarioUrl(ScenarioView), rest.getScenarioHandle(ScenarioView), ms...)
+		}
+	}
+}
+
+//New  create new restful
+func New(dialer gorm.Dialector) (r *CRUD, err error) {
+	r = &CRUD{
+		delegate: &delegate{},
+	}
+	if r.db, err = gorm.Open(dialer); err != nil {
+		return
+	}
+	r.db = r.db.Debug()
+	err = r.db.AutoMigrate(&Schema{})
+	return
+}

+ 117 - 0
delegate.go

@@ -0,0 +1,117 @@
+package rest
+
+import "context"
+
+type Delegate interface {
+	BeforeQuery(ctx context.Context, query *Query) (err error)
+	AfterQuery(ctx context.Context, query *Query) (err error)
+	BeforeSave(ctx context.Context, model interface{}) (err error)
+	AfterSave(ctx context.Context, model interface{}, diff []*DiffAttr) (err error)
+	BeforeCreate(ctx context.Context, model interface{}) (err error)
+	AfterCreate(ctx context.Context, model interface{}, diff []*DiffAttr) (err error)
+	BeforeUpdate(ctx context.Context, model interface{}) (err error)
+	AfterUpdate(ctx context.Context, model interface{}, diff []*DiffAttr) (err error)
+	BeforeDelete(ctx context.Context, model interface{}) (err error)
+	AfterDelete(ctx context.Context, model interface{}) (err error)
+}
+
+type delegate struct {
+	delegates []Delegate
+}
+
+func (dm *delegate) Register(d Delegate) {
+	if dm.delegates == nil {
+		dm.delegates = make([]Delegate, 0)
+	}
+	dm.delegates = append(dm.delegates, d)
+}
+
+func (dm *delegate) BeforeQuery(ctx context.Context, query *Query) (err error) {
+	for i := len(dm.delegates) - 1; i >= 0; i-- {
+		if err = dm.delegates[i].BeforeQuery(ctx, query); err != nil {
+			return
+		}
+	}
+	return
+}
+
+func (dm *delegate) AfterQuery(ctx context.Context, query *Query) (err error) {
+	for i := len(dm.delegates) - 1; i >= 0; i-- {
+		if err = dm.delegates[i].AfterQuery(ctx, query); err != nil {
+			return
+		}
+	}
+	return
+}
+
+func (dm *delegate) BeforeSave(ctx context.Context, model interface{}) (err error) {
+	for i := len(dm.delegates) - 1; i >= 0; i-- {
+		if err = dm.delegates[i].BeforeSave(ctx, model); err != nil {
+			return
+		}
+	}
+	return
+}
+
+func (dm *delegate) AfterSave(ctx context.Context, model interface{}, diff []*DiffAttr) (err error) {
+	for i := len(dm.delegates) - 1; i >= 0; i-- {
+		if err = dm.delegates[i].AfterSave(ctx, model, diff); err != nil {
+			return
+		}
+	}
+	return
+}
+
+func (dm *delegate) BeforeCreate(ctx context.Context, model interface{}) (err error) {
+	for i := len(dm.delegates) - 1; i >= 0; i-- {
+		if err = dm.delegates[i].BeforeCreate(ctx, model); err != nil {
+			return
+		}
+	}
+	return
+}
+
+func (dm *delegate) AfterCreate(ctx context.Context, model interface{}, diff []*DiffAttr) (err error) {
+	for i := len(dm.delegates) - 1; i >= 0; i-- {
+		if err = dm.delegates[i].AfterCreate(ctx, model, diff); err != nil {
+			return
+		}
+	}
+	return
+}
+
+func (dm *delegate) BeforeUpdate(ctx context.Context, model interface{}) (err error) {
+	for i := len(dm.delegates) - 1; i >= 0; i-- {
+		if err = dm.delegates[i].BeforeUpdate(ctx, model); err != nil {
+			return
+		}
+	}
+	return
+}
+
+func (dm *delegate) AfterUpdate(ctx context.Context, model interface{}, diff []*DiffAttr) (err error) {
+	for i := len(dm.delegates) - 1; i >= 0; i-- {
+		if err = dm.delegates[i].AfterUpdate(ctx, model, diff); err != nil {
+			return
+		}
+	}
+	return
+}
+
+func (dm *delegate) BeforeDelete(ctx context.Context, model interface{}) (err error) {
+	for i := len(dm.delegates) - 1; i >= 0; i-- {
+		if err = dm.delegates[i].BeforeDelete(ctx, model); err != nil {
+			return
+		}
+	}
+	return
+}
+
+func (dm *delegate) AfterDelete(ctx context.Context, model interface{}) (err error) {
+	for i := len(dm.delegates) - 1; i >= 0; i-- {
+		if err = dm.delegates[i].AfterDelete(ctx, model); err != nil {
+			return
+		}
+	}
+	return
+}

+ 13 - 0
error/error.go

@@ -0,0 +1,13 @@
+package error
+
+type (
+	StructError struct {
+		Tag     string `json:"rule"`
+		Column  string `json:"column"`
+		Message string `json:"message"`
+	}
+)
+
+func (err *StructError) Error() string {
+	return err.Message
+}

+ 196 - 0
formatter.go

@@ -0,0 +1,196 @@
+package rest
+
+import (
+	"context"
+	"database/sql"
+	"fmt"
+	"gorm.io/gorm"
+	"reflect"
+	"strconv"
+	"sync"
+	"time"
+)
+
+var (
+	DefaultFormatter = NewFormatter()
+
+	DefaultNullDisplay = ""
+)
+
+func init() {
+	DefaultFormatter.Register("string", stringFormat)
+	DefaultFormatter.Register("integer", integerFormat)
+	DefaultFormatter.Register("decimal", decimalFormat)
+	DefaultFormatter.Register("date", dateFormat)
+	DefaultFormatter.Register("time", timeFormat)
+	DefaultFormatter.Register("datetime", datetimeFormat)
+	DefaultFormatter.Register("duration", durationFormat)
+	DefaultFormatter.Register("dropdown", dropdownFormat)
+}
+
+type FormatFunc func(ctx context.Context, value interface{}, model interface{}, scm *Schema) interface{}
+
+type Formatter struct {
+	callbacks sync.Map
+}
+
+func (formatter *Formatter) Register(f string, fun FormatFunc) {
+	formatter.callbacks.Store(f, fun)
+}
+
+func (formatter *Formatter) Format(ctx context.Context, format string, value interface{}, model interface{}, scm *Schema) interface{} {
+	v, ok := formatter.callbacks.Load(format)
+	if ok {
+		return v.(FormatFunc)(ctx, value, model, scm)
+	}
+	return value
+}
+
+func (formatter *Formatter) formatModel(ctx context.Context, val interface{}, schemas []*Schema, stmt *gorm.Statement) interface{} {
+	values := make(map[string]interface{})
+	refVal := reflect.Indirect(reflect.ValueOf(val))
+	for _, field := range schemas {
+		if stmt.Schema != nil {
+			f := stmt.Schema.LookUpField(field.Column)
+			values[field.Column] = formatter.Format(ctx, field.Format, refVal.FieldByName(f.Name).Interface(), val, field)
+		} else {
+			values[field.Column] = ""
+		}
+	}
+	return values
+}
+
+func (formatter *Formatter) formatModels(ctx context.Context, val interface{}, schemas []*Schema, stmt *gorm.Statement) interface{} {
+	reflectValue := reflect.Indirect(reflect.ValueOf(val))
+	if reflectValue.Type().Kind() != reflect.Slice {
+		return nil
+	}
+	length := reflectValue.Len()
+	values := make([]interface{}, length)
+	for i := 0; i < length; i++ {
+		values[i] = formatter.formatModel(ctx, reflectValue.Index(i).Interface(), schemas, stmt)
+	}
+	return values
+}
+
+func stringFormat(ctx context.Context, value interface{}, model interface{}, scm *Schema) interface{} {
+	return fmt.Sprint(value)
+}
+
+func integerFormat(ctx context.Context, value interface{}, model interface{}, scm *Schema) interface{} {
+	var (
+		n int
+	)
+	switch value.(type) {
+	case float32, float64:
+		n = int(reflect.ValueOf(value).Float())
+	case int, int8, int16, int32, int64:
+		n = int(reflect.ValueOf(value).Int())
+	case uint, uint8, uint16, uint32, uint64:
+		n = int(reflect.ValueOf(value).Uint())
+	case string:
+		n, _ = strconv.Atoi(reflect.ValueOf(value).String())
+	case []byte:
+		n, _ = strconv.Atoi(string(reflect.ValueOf(value).Bytes()))
+	}
+	return n
+}
+
+func decimalFormat(ctx context.Context, value interface{}, model interface{}, scm *Schema) interface{} {
+	var (
+		n float64
+	)
+	switch value.(type) {
+	case float32, float64:
+		n = float64(reflect.ValueOf(value).Float())
+	case int, int8, int16, int32, int64:
+		n = float64(reflect.ValueOf(value).Int())
+	case uint, uint8, uint16, uint32, uint64:
+		n = float64(reflect.ValueOf(value).Uint())
+	case string:
+		n, _ = strconv.ParseFloat(reflect.ValueOf(value).String(), 64)
+	case []byte:
+		n, _ = strconv.ParseFloat(string(reflect.ValueOf(value).Bytes()), 64)
+	}
+	return n
+}
+
+func dateFormat(ctx context.Context, value interface{}, model interface{}, scm *Schema) interface{} {
+	if t, ok := value.(time.Time); ok {
+		return t.Format("2006-01-02")
+	}
+	if t, ok := value.(*sql.NullTime); ok {
+		if t != nil && t.Valid {
+			return t.Time.Format("2006-01-02")
+		}
+	}
+	if t, ok := value.(int64); ok {
+		tm := time.Unix(t, 0)
+		return tm.Format("2006-01-02")
+	}
+	return DefaultNullDisplay
+}
+
+func timeFormat(ctx context.Context, value interface{}, model interface{}, scm *Schema) interface{} {
+	if t, ok := value.(time.Time); ok {
+		return t.Format("15:04:05")
+	}
+	if t, ok := value.(*sql.NullTime); ok {
+		if t != nil && t.Valid {
+			return t.Time.Format("15:04:05")
+		}
+	}
+	if t, ok := value.(int64); ok {
+		tm := time.Unix(t, 0)
+		return tm.Format("15:04:05")
+	}
+	return DefaultNullDisplay
+}
+
+func datetimeFormat(ctx context.Context, value interface{}, model interface{}, scm *Schema) interface{} {
+	if t, ok := value.(time.Time); ok {
+		return t.Format("2006-01-02 15:04:05")
+	}
+	if t, ok := value.(*sql.NullTime); ok {
+		if t != nil && t.Valid {
+			return t.Time.Format("2006-01-02 15:04:05")
+		}
+	}
+	if t, ok := value.(int64); ok {
+		if t > 0 {
+			tm := time.Unix(t, 0)
+			return tm.Format("2006-01-02 15:04:05")
+		}
+	}
+	return DefaultNullDisplay
+}
+
+func durationFormat(ctx context.Context, value interface{}, model interface{}, scm *Schema) interface{} {
+	var (
+		hour int
+		min  int
+		sec  int
+	)
+	n := integerFormat(ctx, value, model, scm).(int)
+	hour = n / 3600
+	min = (n - hour*3600) / 60
+	sec = n - hour*3600 - min*60
+	return fmt.Sprintf("%02d:%02d:%02d", hour, min, sec)
+}
+
+func dropdownFormat(ctx context.Context, value interface{}, model interface{}, scm *Schema) interface{} {
+	attributes := scm.Attribute
+	if attributes.Values != nil {
+		for _, v := range attributes.Values {
+			if v.Value == value {
+				return v.Label
+			}
+		}
+	}
+	return value
+}
+
+func NewFormatter() *Formatter {
+	formatter := &Formatter{}
+	return formatter
+}

+ 225 - 0
generator.go

@@ -0,0 +1,225 @@
+package rest
+
+import (
+	"gorm.io/gorm/schema"
+	"reflect"
+	"strings"
+	"time"
+)
+
+var (
+	timeKind    = reflect.TypeOf(time.Time{}).Kind()
+	timePtrKind = reflect.TypeOf(&time.Time{}).Kind()
+)
+
+//dataTypeOf 推断数据的类型
+func dataTypeOf(field *schema.Field) string {
+	var dataType string
+	reflectType := field.FieldType
+	for reflectType.Kind() == reflect.Ptr {
+		reflectType = reflectType.Elem()
+	}
+	dataValue := reflect.Indirect(reflect.New(reflectType))
+	switch dataValue.Kind() {
+	case reflect.Bool:
+		dataType = "boolean"
+	case reflect.Int8, reflect.Int, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint8, reflect.Uint, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
+		dataType = "integer"
+	case reflect.Float32, reflect.Float64:
+		dataType = "double"
+	case reflect.Struct:
+		if _, ok := dataValue.Interface().(time.Time); ok {
+			dataType = "string"
+		}
+	default:
+		dataType = "string"
+	}
+	return dataType
+}
+
+//dataFormatOf 推断数据的格式
+func dataFormatOf(field *schema.Field) string {
+	var format string
+	format = field.Tag.Get("format")
+	if format != "" {
+		return format
+	}
+	//如果有枚举值,直接设置为下拉类型
+	enum := field.Tag.Get("enum")
+	if enum != "" {
+		return "dropdown"
+	}
+	reflectType := field.FieldType
+	for reflectType.Kind() == reflect.Ptr {
+		reflectType = reflectType.Elem()
+	}
+	//时间处理
+	if field.Name == "CreatedAt" || field.Name == "UpdatedAt" || field.Name == "DeletedAt" {
+		return "datetime"
+	}
+	if strings.Contains(strings.ToLower(field.Name), "pass") {
+		return "password"
+	}
+	dataValue := reflect.Indirect(reflect.New(reflectType))
+	switch dataValue.Kind() {
+	case timeKind, timePtrKind:
+		format = "datetime"
+	case reflect.Bool:
+		format = "boolean"
+	case reflect.Int8, reflect.Int, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint8, reflect.Uint, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
+		format = "integer"
+	case reflect.Float32, reflect.Float64:
+		format = "decimal"
+	case reflect.Struct:
+		if _, ok := dataValue.Interface().(time.Time); ok {
+			format = "datetime"
+		}
+	default:
+		if field.Size >= 1024 {
+			format = "text"
+		} else {
+			format = "string"
+		}
+	}
+	return format
+}
+
+//generateFieldName 生成字段名称
+func generateFieldName(name string) string {
+	tokens := strings.Split(name, "_")
+	for i, s := range tokens {
+		tokens[i] = strings.Title(s)
+	}
+	return strings.Join(tokens, " ")
+}
+
+//generateFieldScenario 生成数据的显示场景
+func generateFieldScenario(index int, field *schema.Field) Scenarios {
+	var ss Scenarios
+	if v, ok := field.Tag.Lookup("scenarios"); ok {
+		v = strings.TrimSpace(v)
+		if v != "" {
+			ss = strings.Split(v, ";")
+		}
+	} else {
+		if field.PrimaryKey {
+			ss = []string{ScenarioList, ScenarioView, ScenarioExport}
+		} else if field.Name == "CreatedAt" || field.Name == "UpdatedAt" {
+			ss = []string{ScenarioList}
+		} else if field.Name == "DeletedAt" || field.Name == "Namespace" {
+			//不添加任何显示场景
+			ss = []string{}
+		} else {
+			if index < 10 {
+				//高级字段只配置一些简单的场景
+				ss = []string{ScenarioSearch, ScenarioList, ScenarioCreate, ScenarioUpdate, ScenarioView, ScenarioExport}
+			} else {
+				//高级字段只配置一些简单的场景
+				ss = []string{ScenarioCreate, ScenarioUpdate, ScenarioView, ScenarioExport}
+			}
+		}
+	}
+	return ss
+}
+
+//generateFieldRule 生成字段的规则
+func generateFieldRule(field *schema.Field) Rule {
+	r := Rule{
+		Required: []string{},
+	}
+	if field.GORMDataType == schema.String {
+		r.Max = field.Size
+	}
+	if field.GORMDataType == schema.Int || field.GORMDataType == schema.Float || field.GORMDataType == schema.Uint {
+		r.Max = field.Scale
+	}
+	if field.NotNull {
+		r.Required = []string{ScenarioCreate, ScenarioUpdate}
+	}
+	if field.PrimaryKey {
+		r.Unique = true
+	}
+	return r
+}
+
+// generateFieldAttribute 生成数据属性
+func generateFieldAttribute(field *schema.Field) Attribute {
+	attr := Attribute{
+		Match:        MatchFuzzy,
+		PrimaryKey:   field.PrimaryKey,
+		DefaultValue: field.DefaultValue,
+		Readonly:     []string{},
+		Disable:      []string{},
+		Visible:      make([]VisibleCondition, 0),
+		Values:       make([]EnumValue, 0),
+		Live:         LiveValue{},
+		Description:  "",
+	}
+	//赋值属性
+	props := field.Tag.Get("props")
+	if props != "" {
+		vs := strings.Split(props, ";")
+		for _, str := range vs {
+			kv := strings.SplitN(str, ":", 2)
+			if len(kv) != 2 {
+				continue
+			}
+			switch strings.ToLower(kv[0]) {
+			case "icon":
+				attr.Icon = kv[1]
+			case "suffix":
+				attr.Suffix = kv[1]
+			case "tooltip":
+				attr.Tooltip = kv[1]
+			case "description":
+				attr.Description = kv[1]
+			case "live":
+				//在线数据解析
+				ss := strings.Split(kv[1], ",")
+				for _, s := range ss {
+					if len(s) == 0 {
+						continue
+					}
+					attr.Live.Enable = true
+					if s == LiveTypeDropdown || s == LiveTypeCascader {
+						attr.Live.Type = s
+					} else if s[0] == '/' || strings.HasPrefix(s, "http") {
+						attr.Live.Url = s
+					} else {
+						if strings.IndexByte(s, '.') > -1 {
+							attr.Live.Columns = strings.Split(s, ".")
+						}
+					}
+				}
+			}
+		}
+	}
+	//赋值枚举值
+	enumns := field.Tag.Get("enum")
+	if enumns != "" {
+		vs := strings.Split(enumns, ";")
+		for _, str := range vs {
+			kv := strings.SplitN(str, ":", 2)
+			if len(kv) != 2 {
+				continue
+			}
+			fv := EnumValue{Value: kv[0]}
+			//颜色分隔符
+			if pos := strings.IndexByte(kv[1], '#'); pos > -1 {
+				fv.Label = kv[1][:pos]
+				fv.Color = kv[1][pos:]
+			} else {
+				fv.Label = kv[1]
+			}
+			attr.Values = append(attr.Values, fv)
+		}
+	}
+	if !field.Creatable {
+		attr.Disable = append(attr.Disable, ScenarioCreate)
+	}
+	if !field.Updatable {
+		attr.Disable = append(attr.Disable, ScenarioUpdate)
+	}
+	attr.Tooltip = field.Comment
+	return attr
+}

+ 24 - 0
generator_test.go

@@ -0,0 +1,24 @@
+package rest
+
+import "testing"
+
+func Test_generateFieldName(t *testing.T) {
+	type args struct {
+		name string
+	}
+	tests := []struct {
+		name string
+		args args
+		want string
+	}{
+		{"1", args{name: "id"}, "Id"},
+		{"2", args{name: "queue_stamp"}, "Queue Stamp"},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := generateFieldName(tt.args.name); got != tt.want {
+				t.Errorf("generateFieldName() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}

+ 24 - 0
go.mod

@@ -0,0 +1,24 @@
+module git.nspix.com/golang/rest/v3
+
+go 1.17
+
+require (
+	git.nspix.com/golang/micro v1.3.20
+	github.com/bwmarrin/snowflake v0.3.0
+	github.com/go-playground/validator/v10 v10.11.2
+	gorm.io/driver/mysql v1.4.6
+	gorm.io/gorm v1.24.5
+)
+
+require (
+	github.com/go-playground/locales v0.14.1 // indirect
+	github.com/go-playground/universal-translator v0.18.1 // indirect
+	github.com/go-sql-driver/mysql v1.7.0 // indirect
+	github.com/jinzhu/inflection v1.0.0 // indirect
+	github.com/jinzhu/now v1.1.5 // indirect
+	github.com/leodido/go-urn v1.2.1 // indirect
+	golang.org/x/crypto v0.5.0 // indirect
+	golang.org/x/net v0.5.0 // indirect
+	golang.org/x/sys v0.4.0 // indirect
+	golang.org/x/text v0.6.0 // indirect
+)

+ 202 - 0
go.sum

@@ -0,0 +1,202 @@
+cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+git.nspix.com/golang/micro v1.3.20 h1:sLky1NI6LhukjV0Q8LHKfO1Vszr2CRwyi/qwl99oIlc=
+git.nspix.com/golang/micro v1.3.20/go.mod h1:2/QSgmRJwwxRuGh0SpVVFb2zvJ1M4f+BP+F799WfSDk=
+github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
+github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
+github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
+github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
+github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
+github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
+github.com/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgISZN0=
+github.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE=
+github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
+github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
+github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
+github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
+github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
+github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
+github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
+github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
+github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
+github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
+github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
+github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
+github.com/go-playground/validator/v10 v10.11.2 h1:q3SHpufmypg+erIExEKUmsgmhDTyhcJ38oeKGACXohU=
+github.com/go-playground/validator/v10 v10.11.2/go.mod h1:NieE624vt4SCTJtD87arVLvdmjPAeV8BQlHtMnw9D7s=
+github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc=
+github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
+github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
+github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
+github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
+github.com/jinzhu/now v1.1.4/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
+github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
+github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
+github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
+github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
+github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
+github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
+github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/leodido/go-urn v1.2.1 h1:BqpAaACuzVSgi/VLzGZIobT2z4v53pjosyNd9Yv6n/w=
+github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
+github.com/mattn/go-runewidth v0.0.3/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
+github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
+github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
+github.com/peterh/liner v1.2.1/go.mod h1:CRroGNssyjTd/qIG2FyxByd2S8JEAZXBl4qUrZf8GS0=
+github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
+github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
+github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
+github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
+github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
+github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
+github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
+github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
+github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
+github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
+github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
+github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
+github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
+github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
+github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
+github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
+github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
+github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE=
+golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw=
+golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
+golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18=
+golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
+golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
+golang.org/x/text v0.6.0 h1:3XmdazWV+ubf7QgHSTWeykHOci5oeekaGJBLkrkaw4k=
+golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
+gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gorm.io/driver/mysql v1.4.6 h1:5zS3vIKcyb46byXZNcYxaT9EWNIhXzu0gPuvvVrwZ8s=
+gorm.io/driver/mysql v1.4.6/go.mod h1:SxzItlnT1cb6e1e4ZRpgJN2VYtcqJgqnHxWr4wsP8oc=
+gorm.io/gorm v1.23.8/go.mod h1:l2lP/RyAtc1ynaTjFksBde/O8v9oOGIApu2/xRitmZk=
+gorm.io/gorm v1.24.5 h1:g6OPREKqqlWq4kh/3MCQbZKImeB9e6Xgc4zD+JgNZGE=
+gorm.io/gorm v1.24.5/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=

+ 408 - 0
inflector/inflector.go

@@ -0,0 +1,408 @@
+package inflector
+
+import (
+	"bytes"
+	"fmt"
+	"regexp"
+	"strings"
+	"sync"
+)
+
+// Rule represents name of the inflector rule, can be
+// Plural or Singular
+type Rule int
+
+const (
+	Plural = iota
+	Singular
+)
+
+// InflectorRule represents inflector rule
+type InflectorRule struct {
+	Rules               []*ruleItem
+	Irregular           []*irregularItem
+	Uninflected         []string
+	compiledIrregular   *regexp.Regexp
+	compiledUninflected *regexp.Regexp
+	compiledRules       []*compiledRule
+}
+
+type ruleItem struct {
+	pattern     string
+	replacement string
+}
+
+type irregularItem struct {
+	word        string
+	replacement string
+}
+
+// compiledRule represents compiled version of Inflector.Rules.
+type compiledRule struct {
+	replacement string
+	*regexp.Regexp
+}
+
+// threadsafe access to rules and caches
+var mutex sync.Mutex
+var rules = make(map[Rule]*InflectorRule)
+
+// Words that should not be inflected
+var uninflected = []string{
+	`Amoyese`, `bison`, `Borghese`, `bream`, `breeches`, `britches`, `buffalo`,
+	`cantus`, `carp`, `chassis`, `clippers`, `cod`, `coitus`, `Congoese`,
+	`contretemps`, `corps`, `debris`, `diabetes`, `djinn`, `eland`, `elk`,
+	`equipment`, `Faroese`, `flounder`, `Foochowese`, `gallows`, `Genevese`,
+	`Genoese`, `Gilbertese`, `graffiti`, `headquarters`, `herpes`, `hijinks`,
+	`Hottentotese`, `information`, `innings`, `jackanapes`, `Kiplingese`,
+	`Kongoese`, `Lucchese`, `mackerel`, `Maltese`, `.*?media`, `mews`, `moose`,
+	`mumps`, `Nankingese`, `news`, `nexus`, `Niasese`, `Pekingese`,
+	`Piedmontese`, `pincers`, `Pistoiese`, `pliers`, `Portuguese`, `proceedings`,
+	`rabies`, `rice`, `rhinoceros`, `salmon`, `Sarawakese`, `scissors`,
+	`sea[- ]bass`, `series`, `Shavese`, `shears`, `siemens`, `species`, `swine`,
+	`testes`, `trousers`, `trout`, `tuna`, `Vermontese`, `Wenchowese`, `whiting`,
+	`wildebeest`, `Yengeese`,
+}
+
+// Plural words that should not be inflected
+var uninflectedPlurals = []string{
+	`.*[nrlm]ese`, `.*deer`, `.*fish`, `.*measles`, `.*ois`, `.*pox`, `.*sheep`,
+	`people`,
+}
+
+// Singular words that should not be inflected
+var uninflectedSingulars = []string{
+	`.*[nrlm]ese`, `.*deer`, `.*fish`, `.*measles`, `.*ois`, `.*pox`, `.*sheep`,
+	`.*ss`,
+}
+
+type cache map[string]string
+
+// Inflected words that already cached for immediate retrieval from a given Rule
+var caches = make(map[Rule]cache)
+
+// map of irregular words where its key is a word and its value is the replacement
+var irregularMaps = make(map[Rule]cache)
+
+var (
+	// https://github.com/golang/lint/blob/master/lint.go#L770
+	commonInitialisms         = []string{"API", "ASCII", "CPU", "CSS", "DNS", "EOF", "GUID", "HTML", "HTTP", "HTTPS", "ID", "IP", "JSON", "LHS", "QPS", "RAM", "RHS", "RPC", "SLA", "SMTP", "SSH", "TLS", "TTL", "UID", "UI", "UUID", "URI", "URL", "UTF8", "VM", "XML", "XSRF", "XSS"}
+	commonInitialismsReplacer *strings.Replacer
+)
+
+func init() {
+	rules[Plural] = &InflectorRule{
+		Rules: []*ruleItem{
+			{`(?i)(s)tatus$`, `${1}${2}tatuses`},
+			{`(?i)(quiz)$`, `${1}zes`},
+			{`(?i)^(ox)$`, `${1}${2}en`},
+			{`(?i)([m|l])ouse$`, `${1}ice`},
+			{`(?i)(matr|vert|ind)(ix|ex)$`, `${1}ices`},
+			{`(?i)(x|ch|ss|sh)$`, `${1}es`},
+			{`(?i)([^aeiouy]|qu)y$`, `${1}ies`},
+			{`(?i)(hive)$`, `$1s`},
+			{`(?i)(?:([^f])fe|([lre])f)$`, `${1}${2}ves`},
+			{`(?i)sis$`, `ses`},
+			{`(?i)([ti])um$`, `${1}a`},
+			{`(?i)(p)erson$`, `${1}eople`},
+			{`(?i)(m)an$`, `${1}en`},
+			{`(?i)(c)hild$`, `${1}hildren`},
+			{`(?i)(buffal|tomat)o$`, `${1}${2}oes`},
+			{`(?i)(alumn|bacill|cact|foc|fung|nucle|radi|stimul|syllab|termin|vir)us$`, `${1}i`},
+			{`(?i)us$`, `uses`},
+			{`(?i)(alias)$`, `${1}es`},
+			{`(?i)(ax|cris|test)is$`, `${1}es`},
+			{`s$`, `s`},
+			{`^$`, ``},
+			{`$`, `s`},
+		},
+		Irregular: []*irregularItem{
+			{`atlas`, `atlases`},
+			{`beef`, `beefs`},
+			{`brother`, `brothers`},
+			{`cafe`, `cafes`},
+			{`child`, `children`},
+			{`cookie`, `cookies`},
+			{`corpus`, `corpuses`},
+			{`cow`, `cows`},
+			{`ganglion`, `ganglions`},
+			{`genie`, `genies`},
+			{`genus`, `genera`},
+			{`graffito`, `graffiti`},
+			{`hoof`, `hoofs`},
+			{`loaf`, `loaves`},
+			{`man`, `men`},
+			{`money`, `monies`},
+			{`mongoose`, `mongooses`},
+			{`move`, `moves`},
+			{`mythos`, `mythoi`},
+			{`niche`, `niches`},
+			{`numen`, `numina`},
+			{`occiput`, `occiputs`},
+			{`octopus`, `octopuses`},
+			{`opus`, `opuses`},
+			{`ox`, `oxen`},
+			{`penis`, `penises`},
+			{`person`, `people`},
+			{`sex`, `sexes`},
+			{`soliloquy`, `soliloquies`},
+			{`testis`, `testes`},
+			{`trilby`, `trilbys`},
+			{`turf`, `turfs`},
+			{`potato`, `potatoes`},
+			{`hero`, `heroes`},
+			{`tooth`, `teeth`},
+			{`goose`, `geese`},
+			{`foot`, `feet`},
+		},
+	}
+	prepare(Plural)
+
+	rules[Singular] = &InflectorRule{
+		Rules: []*ruleItem{
+			{`(?i)(s)tatuses$`, `${1}${2}tatus`},
+			{`(?i)^(.*)(menu)s$`, `${1}${2}`},
+			{`(?i)(quiz)zes$`, `$1`},
+			{`(?i)(matr)ices$`, `${1}ix`},
+			{`(?i)(vert|ind)ices$`, `${1}ex`},
+			{`(?i)^(ox)en`, `$1`},
+			{`(?i)(alias)(es)*$`, `$1`},
+			{`(?i)(alumn|bacill|cact|foc|fung|nucle|radi|stimul|syllab|termin|viri?)i$`, `${1}us`},
+			{`(?i)([ftw]ax)es`, `$1`},
+			{`(?i)(cris|ax|test)es$`, `${1}is`},
+			{`(?i)(shoe|slave)s$`, `$1`},
+			{`(?i)(o)es$`, `$1`},
+			{`ouses$`, `ouse`},
+			{`([^a])uses$`, `${1}us`},
+			{`(?i)([m|l])ice$`, `${1}ouse`},
+			{`(?i)(x|ch|ss|sh)es$`, `$1`},
+			{`(?i)(m)ovies$`, `${1}${2}ovie`},
+			{`(?i)(s)eries$`, `${1}${2}eries`},
+			{`(?i)([^aeiouy]|qu)ies$`, `${1}y`},
+			{`(?i)(tive)s$`, `$1`},
+			{`(?i)([lre])ves$`, `${1}f`},
+			{`(?i)([^fo])ves$`, `${1}fe`},
+			{`(?i)(hive)s$`, `$1`},
+			{`(?i)(drive)s$`, `$1`},
+			{`(?i)(^analy)ses$`, `${1}sis`},
+			{`(?i)(analy|diagno|^ba|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$`, `${1}${2}sis`},
+			{`(?i)([ti])a$`, `${1}um`},
+			{`(?i)(p)eople$`, `${1}${2}erson`},
+			{`(?i)(m)en$`, `${1}an`},
+			{`(?i)(c)hildren$`, `${1}${2}hild`},
+			{`(?i)(n)ews$`, `${1}${2}ews`},
+			{`eaus$`, `eau`},
+			{`^(.*us)$`, `$1`},
+			{`(?i)s$`, ``},
+		},
+		Irregular: []*irregularItem{
+			{`foes`, `foe`},
+			{`waves`, `wave`},
+			{`curves`, `curve`},
+			{`atlases`, `atlas`},
+			{`beefs`, `beef`},
+			{`brothers`, `brother`},
+			{`cafes`, `cafe`},
+			{`children`, `child`},
+			{`cookies`, `cookie`},
+			{`corpuses`, `corpus`},
+			{`cows`, `cow`},
+			{`ganglions`, `ganglion`},
+			{`genies`, `genie`},
+			{`genera`, `genus`},
+			{`graffiti`, `graffito`},
+			{`hoofs`, `hoof`},
+			{`loaves`, `loaf`},
+			{`men`, `man`},
+			{`monies`, `money`},
+			{`mongooses`, `mongoose`},
+			{`moves`, `move`},
+			{`mythoi`, `mythos`},
+			{`niches`, `niche`},
+			{`numina`, `numen`},
+			{`occiputs`, `occiput`},
+			{`octopuses`, `octopus`},
+			{`opuses`, `opus`},
+			{`oxen`, `ox`},
+			{`penises`, `penis`},
+			{`people`, `person`},
+			{`sexes`, `sex`},
+			{`soliloquies`, `soliloquy`},
+			{`testes`, `testis`},
+			{`trilbys`, `trilby`},
+			{`turfs`, `turf`},
+			{`potatoes`, `potato`},
+			{`heroes`, `hero`},
+			{`teeth`, `tooth`},
+			{`geese`, `goose`},
+			{`feet`, `foot`},
+		},
+	}
+	prepare(Singular)
+
+	commonInitialismsForReplacer := make([]string, 0, len(commonInitialisms))
+	for _, initialism := range commonInitialisms {
+		commonInitialismsForReplacer = append(commonInitialismsForReplacer, initialism, strings.Title(strings.ToLower(initialism)))
+	}
+	commonInitialismsReplacer = strings.NewReplacer(commonInitialismsForReplacer...)
+}
+
+// prepare rule, e.g., compile the pattern.
+func prepare(r Rule) error {
+	var reString string
+
+	switch r {
+	case Plural:
+		// Merge global uninflected with singularsUninflected
+		rules[r].Uninflected = merge(uninflected, uninflectedPlurals)
+	case Singular:
+		// Merge global uninflected with singularsUninflected
+		rules[r].Uninflected = merge(uninflected, uninflectedSingulars)
+	}
+
+	// Set InflectorRule.compiledUninflected by joining InflectorRule.Uninflected into
+	// a single string then compile it.
+	reString = fmt.Sprintf(`(?i)(^(?:%s))$`, strings.Join(rules[r].Uninflected, `|`))
+	rules[r].compiledUninflected = regexp.MustCompile(reString)
+
+	// Prepare irregularMaps
+	irregularMaps[r] = make(cache, len(rules[r].Irregular))
+
+	// Set InflectorRule.compiledIrregular by joining the irregularItem.word of Inflector.Irregular
+	// into a single string then compile it.
+	vIrregulars := make([]string, len(rules[r].Irregular))
+	for i, item := range rules[r].Irregular {
+		vIrregulars[i] = item.word
+		irregularMaps[r][item.word] = item.replacement
+	}
+	reString = fmt.Sprintf(`(?i)(.*)\b((?:%s))$`, strings.Join(vIrregulars, `|`))
+	rules[r].compiledIrregular = regexp.MustCompile(reString)
+
+	// Compile all patterns in InflectorRule.Rules
+	rules[r].compiledRules = make([]*compiledRule, len(rules[r].Rules))
+	for i, item := range rules[r].Rules {
+		rules[r].compiledRules[i] = &compiledRule{item.replacement, regexp.MustCompile(item.pattern)}
+	}
+
+	// Prepare caches
+	caches[r] = make(cache)
+
+	return nil
+}
+
+// merge slice a and slice b
+func merge(a []string, b []string) []string {
+	result := make([]string, len(a)+len(b))
+	copy(result, a)
+	copy(result[len(a):], b)
+
+	return result
+}
+
+func getInflected(r Rule, s string) string {
+	mutex.Lock()
+	defer mutex.Unlock()
+	if v, ok := caches[r][s]; ok {
+		return v
+	}
+
+	// Check for irregular words
+	if res := rules[r].compiledIrregular.FindStringSubmatch(s); len(res) >= 3 {
+		var buf bytes.Buffer
+
+		buf.WriteString(res[1])
+		buf.WriteString(s[0:1])
+		buf.WriteString(irregularMaps[r][strings.ToLower(res[2])][1:])
+
+		// Cache it then returns
+		caches[r][s] = buf.String()
+		return caches[r][s]
+	}
+
+	// Check for uninflected words
+	if rules[r].compiledUninflected.MatchString(s) {
+		caches[r][s] = s
+		return caches[r][s]
+	}
+
+	// Check each rule
+	for _, re := range rules[r].compiledRules {
+		if re.MatchString(s) {
+			caches[r][s] = re.ReplaceAllString(s, re.replacement)
+			return caches[r][s]
+		}
+	}
+
+	// Returns unaltered
+	caches[r][s] = s
+	return caches[r][s]
+}
+
+// Pluralize returns string s in plural form.
+func Pluralize(s string) string {
+	return getInflected(Plural, s)
+}
+
+// Singularize returns string s in singular form.
+func Singularize(s string) string {
+	return getInflected(Singular, s)
+}
+
+var (
+	camelizeReg = regexp.MustCompile(`[^A-Za-z0-9]+`)
+)
+
+// Camelize Converts a word like "send_email" to "SendEmail"
+func Camelize(s string) string {
+	s = camelizeReg.ReplaceAllString(s, " ")
+	return strings.Replace(strings.Title(s), " ", "", -1)
+}
+
+// Camel2id Converts a word like "SendEmail" to "send_email"
+func Camel2id(name string) string {
+	var (
+		value                          = commonInitialismsReplacer.Replace(name)
+		buf                            strings.Builder
+		lastCase, nextCase, nextNumber bool // upper case == true
+		curCase                        = value[0] <= 'Z' && value[0] >= 'A'
+	)
+
+	for i, v := range value[:len(value)-1] {
+		nextCase = value[i+1] <= 'Z' && value[i+1] >= 'A'
+		nextNumber = value[i+1] >= '0' && value[i+1] <= '9'
+
+		if curCase {
+			if lastCase && (nextCase || nextNumber) {
+				buf.WriteRune(v + 32)
+			} else {
+				if i > 0 && value[i-1] != '_' && value[i+1] != '_' {
+					buf.WriteByte('_')
+				}
+				buf.WriteRune(v + 32)
+			}
+		} else {
+			buf.WriteRune(v)
+		}
+
+		lastCase = curCase
+		curCase = nextCase
+	}
+
+	if curCase {
+		if !lastCase && len(value) > 1 {
+			buf.WriteByte('_')
+		}
+		buf.WriteByte(value[len(value)-1] + 32)
+	} else {
+		buf.WriteByte(value[len(value)-1])
+	}
+	ret := buf.String()
+	return ret
+}
+
+// Camel2words Converts a CamelCase name into space-separated words.
+// For example, 'send_email' will be converted to 'Send Email'.
+func Camel2words(s string) string {
+	s = camelizeReg.ReplaceAllString(s, " ")
+	return strings.Title(s)
+}

+ 54 - 0
instance.go

@@ -0,0 +1,54 @@
+package rest
+
+import (
+	"context"
+	"errors"
+	"git.nspix.com/golang/micro/gateway/http"
+	"gorm.io/gorm"
+)
+
+var (
+	std *CRUD
+)
+
+var (
+	ErrUninitializedComponent = errors.New("uninitialized component")
+)
+
+func Init(dialer gorm.Dialector) (err error) {
+	std, err = New(dialer)
+	return
+}
+
+func Instance() *CRUD {
+	return std
+}
+
+func DB() (db *gorm.DB, err error) {
+	if std == nil {
+		return nil, ErrUninitializedComponent
+	}
+	return std.db, nil
+}
+
+func Attach(ctx context.Context, model Model, cbs ...Option) (err error) {
+	if std == nil {
+		return ErrUninitializedComponent
+	}
+	return std.Attach(ctx, model, cbs...)
+}
+
+func GetSchemas(ctx context.Context, model Model) ([]*Schema, error) {
+	if std == nil {
+		return nil, ErrUninitializedComponent
+	}
+	return std.GetSchemas(ctx, DefaultNamespace, model.ModuleName(), model.TableName())
+}
+
+func Router(hs *http.Server, ms ...http.Middleware) (err error) {
+	if std == nil {
+		return ErrUninitializedComponent
+	}
+	std.Router(hs, ms...)
+	return
+}

+ 124 - 0
model.go

@@ -0,0 +1,124 @@
+package rest
+
+import (
+	"context"
+	"gorm.io/gorm"
+)
+
+type (
+	Model interface {
+		ModuleName() string
+		TableName() string
+	}
+
+	FlexibleModel interface {
+		Scenarios() []string
+	}
+
+	ActiveModel interface {
+		BeforeQuery(ctx context.Context, query *Query) (err error)
+		AfterQuery(ctx context.Context, query *Query) (err error)
+		BeforeSave(ctx context.Context, model interface{}) (err error)
+		AfterSave(ctx context.Context, model interface{}, diff []*DiffAttr) (err error)
+		BeforeCreate(ctx context.Context, model interface{}) (err error)
+		AfterCreate(ctx context.Context, model interface{}, diff []*DiffAttr) (err error)
+		BeforeUpdate(ctx context.Context, model interface{}) (err error)
+		AfterUpdate(ctx context.Context, model interface{}, diff []*DiffAttr) (err error)
+		BeforeDelete(ctx context.Context, model interface{}) (err error)
+		AfterDelete(ctx context.Context, model interface{}) (err error)
+	}
+
+	BaseModel struct {
+		ID        string         `json:"id" gorm:"primaryKey;size:20" comment:"ID"`
+		CreatedAt int64          `json:"created_at" gorm:"autoCreateTime" comment:"创建时间"`
+		UpdatedAt int64          `json:"updated_at" gorm:"autoUpdateTime" comment:"更新时间"`
+		DeletedAt gorm.DeletedAt `json:"deleted_at" gorm:"index" comment:"删除时间"`
+		Namespace string         `json:"namespace" gorm:"index;size:60;not null;default:'default'" comment:"域"`
+	}
+
+	ReadonlyModel struct {
+		ID        string `json:"id" gorm:"primaryKey;size:32" comment:"ID"`
+		CreatedAt int64  `json:"created_at" gorm:"autoCreateTime" comment:"创建时间"`
+		Namespace string `json:"namespace" gorm:"index;size:60;not null;default:'default'" comment:"域"`
+	}
+)
+
+func (rom *ReadonlyModel) BeforeQuery(ctx context.Context, query *Query) (err error) {
+	return
+}
+
+func (rom *ReadonlyModel) AfterQuery(ctx context.Context, query *Query) (err error) {
+	return
+}
+
+func (rom *ReadonlyModel) BeforeSave(ctx context.Context, model interface{}) (err error) {
+	return
+}
+
+func (rom *ReadonlyModel) AfterSave(ctx context.Context, model interface{}, diff []*DiffAttr) (err error) {
+	return
+}
+
+func (rom *ReadonlyModel) BeforeCreate(ctx context.Context, model interface{}) (err error) {
+	return
+}
+
+func (rom *ReadonlyModel) AfterCreate(ctx context.Context, model interface{}, diff []*DiffAttr) (err error) {
+	return
+}
+
+func (rom *ReadonlyModel) BeforeUpdate(ctx context.Context, model interface{}) (err error) {
+	return
+}
+
+func (rom *ReadonlyModel) AfterUpdate(ctx context.Context, model interface{}, diff []*DiffAttr) (err error) {
+	return
+}
+
+func (rom *ReadonlyModel) BeforeDelete(ctx context.Context, model interface{}) (err error) {
+	return
+}
+
+func (rom *ReadonlyModel) AfterDelete(ctx context.Context, model interface{}) (err error) {
+	return
+}
+
+func (bsm *BaseModel) BeforeQuery(ctx context.Context, query *Query) (err error) {
+	return
+}
+
+func (bsm *BaseModel) AfterQuery(ctx context.Context, query *Query) (err error) {
+	return
+}
+
+func (bsm *BaseModel) BeforeSave(ctx context.Context, model interface{}) (err error) {
+	return
+}
+
+func (bsm *BaseModel) AfterSave(ctx context.Context, model interface{}, diff []*DiffAttr) (err error) {
+	return
+}
+
+func (bsm *BaseModel) BeforeCreate(ctx context.Context, model interface{}) (err error) {
+	return
+}
+
+func (bsm *BaseModel) AfterCreate(ctx context.Context, model interface{}, diff []*DiffAttr) (err error) {
+	return
+}
+
+func (bsm *BaseModel) BeforeUpdate(ctx context.Context, model interface{}) (err error) {
+	return
+}
+
+func (bsm *BaseModel) AfterUpdate(ctx context.Context, model interface{}, diff []*DiffAttr) (err error) {
+	return
+}
+
+func (bsm *BaseModel) BeforeDelete(ctx context.Context, model interface{}) (err error) {
+	return
+}
+
+func (bsm *BaseModel) AfterDelete(ctx context.Context, model interface{}) (err error) {
+	return
+}

+ 58 - 0
options.go

@@ -0,0 +1,58 @@
+package rest
+
+import (
+	"context"
+	"gorm.io/gorm"
+)
+
+type Options struct {
+	EnableNamespace bool
+	Namespace       string
+	ApiPrefix       string
+	TablePrefix     string
+	DB              *gorm.DB
+	Formatter       *Formatter
+	Delegate        *delegate
+	Context         context.Context
+	LookupFunc      func(ctx context.Context, ns string, moduleName string, tableName string, scene string) []*Schema
+}
+
+type Option func(o *Options)
+
+func WithContext(ctx context.Context) Option {
+	return func(o *Options) {
+		o.Context = ctx
+	}
+}
+
+func WithDB(db *gorm.DB) Option {
+	return func(o *Options) {
+		o.DB = db
+	}
+}
+
+func WithNamespace(ns string) Option {
+	return func(o *Options) {
+		o.EnableNamespace = true
+		o.Namespace = ns
+	}
+}
+
+func WithApiPrefix(prefix string) Option {
+	return func(o *Options) {
+		o.ApiPrefix = prefix
+	}
+}
+
+func WithTablePrefix(prefix string) Option {
+	return func(o *Options) {
+		o.TablePrefix = prefix
+	}
+}
+
+func newOptions() *Options {
+	return &Options{
+		Namespace: DefaultNamespace,
+		Formatter: DefaultFormatter,
+	}
+}

+ 54 - 0
plugins/snowflake_id.go

@@ -0,0 +1,54 @@
+package plugins
+
+import (
+	"context"
+	"github.com/bwmarrin/snowflake"
+	"gorm.io/gorm"
+	"gorm.io/gorm/schema"
+	"os"
+	"reflect"
+	"strconv"
+)
+
+var (
+	sf *snowflake.Node
+)
+
+func init() {
+	var err error
+	no, _ := strconv.ParseInt(os.Getenv("CC_NODE"), 10, 64)
+	if no == 0 {
+		no = 1
+	}
+	if sf, err = snowflake.NewNode(no); err != nil {
+		panic(err)
+	}
+}
+
+//SnowflakeID 自动生成主键ID
+func SnowflakeID(db *gorm.DB) {
+	var err error
+	if db.Statement.Schema != nil {
+		if field := db.Statement.Schema.LookUpField("ID"); field != nil {
+			if field.DataType == schema.String {
+				if db.Statement.ReflectValue.Kind() == reflect.Array || db.Statement.ReflectValue.Kind() == reflect.Slice {
+					for i := 0; i < db.Statement.ReflectValue.Len(); i++ {
+						if _, zero := field.ValueOf(context.Background(), db.Statement.ReflectValue.Index(i)); zero {
+							if err = field.Set(context.Background(), db.Statement.ReflectValue.Index(i), sf.Generate().String()); err != nil {
+								_ = db.AddError(err)
+							}
+						}
+					}
+				} else {
+					if _, zero := field.ValueOf(context.Background(), db.Statement.ReflectValue); zero {
+						db.Statement.SetColumn("ID", sf.Generate().String())
+					}
+				}
+			}
+		}
+	}
+}
+
+func RegisterSnowflakeIDCallback(db *gorm.DB) (err error) {
+	return db.Callback().Create().Before("gorm:create").Register("snowflake_id", SnowflakeID)
+}

+ 271 - 0
plugins/validate.go

@@ -0,0 +1,271 @@
+package plugins
+
+import (
+	"context"
+	"fmt"
+	"git.nspix.com/golang/rest/v3"
+	errpkg "git.nspix.com/golang/rest/v3/error"
+	"git.nspix.com/golang/rest/v3/utils"
+	validator "github.com/go-playground/validator/v10"
+	"gorm.io/gorm"
+	"gorm.io/gorm/schema"
+	"reflect"
+	"regexp"
+	"strconv"
+	"strings"
+)
+
+const (
+	SkipValidations = "validations:skip_validations"
+)
+
+type (
+	validateRule struct {
+		Rule  string
+		Value string
+		Valid bool
+	}
+
+	validateScope struct{}
+
+	validScope struct {
+		DB     *gorm.DB
+		Column string
+		Model  interface{}
+	}
+
+	validation struct {
+		crud *rest.CRUD
+	}
+)
+
+var (
+	validate         = validator.New()
+	validateScopeKey = validateScope{}
+
+	telephoneRegex = regexp.MustCompile("^\\d{5,20}$")
+)
+
+func init() {
+
+	_ = validate.RegisterValidationCtx("telephone", func(ctx context.Context, fl validator.FieldLevel) bool {
+		val := fmt.Sprint(fl.Field().Interface())
+		return telephoneRegex.MatchString(val)
+	})
+
+	_ = validate.RegisterValidationCtx("db_unique", func(ctx context.Context, fl validator.FieldLevel) bool {
+		var (
+			scope           *validScope
+			ok              bool
+			count           int64
+			field           *schema.Field
+			primaryKeyValue reflect.Value
+		)
+		val := fl.Field().Interface()
+		if scope, ok = ctx.Value(validateScopeKey).(*validScope); !ok {
+			return true
+		}
+		if len(scope.DB.Statement.Schema.PrimaryFields) > 0 {
+			field = scope.DB.Statement.Schema.PrimaryFields[0]
+			primaryKeyValue = reflect.Indirect(reflect.ValueOf(scope.Model))
+			for _, n := range field.BindNames {
+				primaryKeyValue = primaryKeyValue.FieldByName(n)
+			}
+		}
+		sess := scope.DB.Session(&gorm.Session{NewDB: true})
+		if primaryKeyValue.IsValid() && !primaryKeyValue.IsZero() && field != nil {
+			sess.Model(scope.Model).Where(scope.Column+"=? AND "+field.Name+" != ?", val, primaryKeyValue.Interface()).Count(&count)
+		} else {
+			sess.Model(scope.Model).Where(scope.Column+"=?", val).Count(&count)
+		}
+		if count > 0 {
+			return false
+		}
+		return true
+	})
+}
+
+func newRule(ss ...string) *validateRule {
+	v := &validateRule{
+		Valid: true,
+	}
+	if len(ss) == 1 {
+		v.Rule = ss[0]
+	} else if len(ss) >= 2 {
+		v.Rule = ss[0]
+		v.Value = ss[1]
+	}
+	return v
+}
+
+func generateRules(scm *rest.Schema, scenario string, rule rest.Rule) []*validateRule {
+	rules := make([]*validateRule, 0, 5)
+	if rule.Min != 0 {
+		rules = append(rules, newRule("min", strconv.Itoa(rule.Min)))
+	}
+	if rule.Max != 0 {
+		rules = append(rules, newRule("max", strconv.Itoa(rule.Max)))
+	}
+	//主键不做唯一判断
+	if rule.Unique && !scm.Attribute.PrimaryKey {
+		rules = append(rules, newRule("db_unique"))
+	}
+	if rule.Type != "" {
+		rules = append(rules, newRule(rule.Type))
+	}
+	if rule.Required != nil && len(rule.Required) > 0 {
+		for _, v := range rule.Required {
+			if v == scenario {
+				rules = append(rules, newRule("required"))
+				break
+			}
+		}
+	}
+	return rules
+}
+
+func buildRules(rs []*validateRule) string {
+	var sb strings.Builder
+	for _, r := range rs {
+		if !r.Valid {
+			continue
+		}
+		if sb.Len() > 0 {
+			sb.WriteString(",")
+		}
+		if r.Value == "" {
+			sb.WriteString(r.Rule)
+		} else {
+			sb.WriteString(r.Rule + "=" + r.Value)
+		}
+	}
+	return sb.String()
+}
+
+func getRule(name string, rules []*validateRule) *validateRule {
+	for _, r := range rules {
+		if r.Rule == name {
+			return r
+		}
+	}
+	return nil
+}
+
+func formatError(rule rest.Rule, scm *rest.Schema, tag string) string {
+	var s string
+	switch tag {
+	case "db_unique":
+		s = scm.Label + "值已经存在."
+		break
+	case "required":
+		s = scm.Label + "值不能为空."
+	case "max":
+		if scm.Type == "string" {
+			s = scm.Label + "长度不能大于" + strconv.Itoa(rule.Max)
+		} else {
+			s = scm.Label + "值不能大于" + strconv.Itoa(rule.Max)
+		}
+	case "min":
+		if scm.Type == "string" {
+			s = scm.Label + "长度不能小于" + strconv.Itoa(rule.Max)
+		} else {
+			s = scm.Label + "值不能小于" + strconv.Itoa(rule.Max)
+		}
+	}
+	return s
+}
+
+func (vv *validation) validation(db *gorm.DB) {
+	if result, ok := db.Get(SkipValidations); ok && result.(bool) {
+		return
+	}
+	var (
+		ok           bool
+		err          error
+		rules        []*validateRule
+		stmt         *gorm.Statement
+		model        rest.Model
+		scenario     string
+		skipValidate bool
+		value        reflect.Value
+		schemas      []*rest.Schema
+	)
+	stmt = db.Statement
+	if stmt.Model == nil {
+		return
+	}
+	if model, ok = stmt.Model.(rest.Model); !ok {
+		return
+	}
+	scenario = rest.ScenarioUpdate
+	for _, pk := range stmt.Schema.PrimaryFields {
+		if utils.IsEmpty(stmt.ReflectValue.FieldByName(pk.Name).Interface()) {
+			scenario = rest.ScenarioCreate
+			break
+		}
+	}
+	schemas = vv.crud.VisibleSchemas(context.Background(), stmt.ReflectValue.FieldByName("Namespace").String(), model.ModuleName(), model.TableName(), scenario)
+	for _, scm := range schemas {
+		if rules = generateRules(scm, scenario, scm.Rule); len(rules) <= 0 {
+			continue
+		}
+		value = stmt.ReflectValue.FieldByName(stmt.Schema.LookUpField(scm.Column).Name)
+		if !value.IsValid() {
+			continue
+		}
+		skipValidate = false
+		if r := getRule("required", rules); r != nil {
+			if value.Interface() != nil {
+				vType := reflect.ValueOf(value.Interface())
+				switch vType.Kind() {
+				case reflect.Bool:
+					skipValidate = true
+				case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+					skipValidate = true
+				case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
+					skipValidate = true
+				case reflect.Float32, reflect.Float64:
+					skipValidate = true
+				}
+			}
+			if skipValidate {
+				r.Valid = false
+			}
+		} else {
+			if utils.IsEmpty(value.Interface()) {
+				continue
+			}
+		}
+		ctx := context.WithValue(context.Background(), validateScopeKey, &validScope{
+			DB:     db,
+			Column: scm.Column,
+			Model:  stmt.Model,
+		})
+		if err = validate.VarCtx(ctx, value.Interface(), buildRules(rules)); err != nil {
+			if errs, ok := err.(validator.ValidationErrors); ok {
+				for _, e := range errs {
+					_ = db.AddError(&errpkg.StructError{
+						Tag:     e.Tag(),
+						Column:  scm.Column,
+						Message: formatError(scm.Rule, scm, e.Tag()),
+					})
+				}
+			} else {
+				_ = db.AddError(err)
+			}
+			break
+		}
+	}
+}
+
+func RegisterValidationCallback(db *gorm.DB, crud *rest.CRUD) (err error) {
+	callback := db.Callback()
+	vv := &validation{crud: crud}
+	if callback.Create().Get("validations:validate") == nil {
+		err = callback.Create().Before("gorm:before_create").Register("validations:validate", vv.validation)
+	}
+	if callback.Update().Get("validations:validate") == nil {
+		err = callback.Update().Before("gorm:before_update").Register("validations:validate", vv.validation)
+	}
+	return
+}

+ 31 - 0
proto.go

@@ -0,0 +1,31 @@
+package rest
+
+type (
+	ListResponse struct {
+		Page       int         `json:"page"`
+		PageSize   int         `json:"pagesize"`
+		TotalCount int64       `json:"totalCount"`
+		Data       interface{} `json:"data"`
+	}
+
+	ValidateResponse struct {
+	}
+
+	CreateResponse struct {
+		ID    interface{} `json:"id"`
+		Topic string      `json:"topic"`
+		State string      `json:"state"`
+	}
+
+	UpdateResponse struct {
+		ID    interface{} `json:"id"`
+		Topic string      `json:"topic"`
+		State string      `json:"state"`
+	}
+
+	DeleteResponse struct {
+		ID    interface{} `json:"id"`
+		Topic string      `json:"topic"`
+		State string      `json:"state"`
+	}
+)

+ 369 - 0
query.go

@@ -0,0 +1,369 @@
+package rest
+
+import (
+	"fmt"
+	"git.nspix.com/golang/rest/v3/utils"
+	"gorm.io/gorm"
+	"reflect"
+	"strconv"
+	"strings"
+	"time"
+)
+
+type (
+	Query struct {
+		db        *gorm.DB
+		condition string
+		fields    []string
+		params    []interface{}
+		table     string
+		joins     []join
+		orderBy   []string
+		groupBy   []string
+		limit     int
+		offset    int
+	}
+
+	condition struct {
+		Field string      `json:"field"`
+		Value interface{} `json:"value"`
+		Expr  string      `json:"expr"`
+	}
+
+	join struct {
+		Table     string
+		Direction string
+		Conds     []*condition
+	}
+)
+
+func (cond *condition) WithExpr(v string) *condition {
+	cond.Expr = v
+	return cond
+}
+
+func (query *Query) compile() (*gorm.DB, error) {
+	db := query.db
+	if query.condition != "" {
+		db = db.Where(query.condition, query.params...)
+	}
+	if query.fields != nil {
+		db = db.Select(strings.Join(query.fields, ","))
+	}
+	if query.table != "" {
+		db = db.Table(query.table)
+	}
+	if query.joins != nil && len(query.joins) > 0 {
+		for _, joinEntity := range query.joins {
+			cs, ps := query.buildConditions("OR", false, joinEntity.Conds...)
+			db = db.Joins(joinEntity.Direction+" JOIN "+joinEntity.Table+" ON "+cs, ps...)
+		}
+	}
+	if query.orderBy != nil && len(query.orderBy) > 0 {
+		db = db.Order(strings.Join(query.orderBy, ","))
+	}
+	if query.groupBy != nil && len(query.groupBy) > 0 {
+		db = db.Group(strings.Join(query.groupBy, ","))
+	}
+	if query.offset > 0 {
+		db = db.Offset(query.offset)
+	}
+	if query.limit > 0 {
+		db = db.Limit(query.limit)
+	}
+	return db, nil
+}
+
+func (query *Query) decodeValue(v interface{}) string {
+	refVal := reflect.Indirect(reflect.ValueOf(v))
+	switch refVal.Kind() {
+	case reflect.Bool:
+		if refVal.Bool() {
+			return "1"
+		} else {
+			return "0"
+		}
+	case reflect.Int, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint32, reflect.Uint64:
+		return strconv.FormatInt(refVal.Int(), 10)
+	case reflect.Float32, reflect.Float64:
+		return strconv.FormatFloat(refVal.Float(), 'f', -1, 64)
+	case reflect.String:
+		return "'" + refVal.String() + "'"
+	case timeKind:
+		if tm, ok := refVal.Interface().(time.Time); ok {
+			return "'" + tm.Format("2006-01-02 15:04:05") + "'"
+		}
+		return fmt.Sprint(v)
+	default:
+		return fmt.Sprint(v)
+	}
+}
+
+func (query *Query) buildConditions(operator string, filter bool, conds ...*condition) (str string, params []interface{}) {
+	var (
+		sb strings.Builder
+	)
+	params = make([]interface{}, 0)
+	for _, cond := range conds {
+		if filter {
+			if utils.IsEmpty(cond.Value) {
+				continue
+			}
+		}
+		if cond.Expr == "" {
+			cond.Expr = "="
+		}
+		switch strings.ToUpper(cond.Expr) {
+		case "=", "<>", ">", "<", ">=", "<=", "!=":
+			if sb.Len() > 0 {
+				sb.WriteString(" " + operator + " ")
+			}
+			if cond.Expr == "=" && cond.Value == nil {
+				sb.WriteString("`" + cond.Field + "` IS NULL")
+			} else {
+				sb.WriteString("`" + cond.Field + "` " + cond.Expr + " ?")
+				params = append(params, cond.Value)
+			}
+		case "LIKE":
+			if sb.Len() > 0 {
+				sb.WriteString(" " + operator + " ")
+			}
+			cond.Value = fmt.Sprintf("%%%s%%", cond.Value)
+			sb.WriteString("`" + cond.Field + "` LIKE ?")
+			params = append(params, cond.Value)
+		case "IN":
+			if sb.Len() > 0 {
+				sb.WriteString(" " + operator + " ")
+			}
+			refVal := reflect.Indirect(reflect.ValueOf(cond.Value))
+			switch refVal.Kind() {
+			case reflect.Slice:
+				ss := make([]string, refVal.Len())
+				for i := 0; i < refVal.Len(); i++ {
+					ss[i] = query.decodeValue(refVal.Index(i))
+				}
+				sb.WriteString("`" + cond.Field + "` IN (" + strings.Join(ss, ",") + ")")
+			case reflect.String:
+				sb.WriteString("`" + cond.Field + "` IN (" + refVal.String() + ")")
+			}
+		case "BETWEEN":
+			refVal := reflect.ValueOf(cond.Value)
+			if refVal.Kind() == reflect.Slice && refVal.Len() == 2 {
+				sb.WriteString("`" + cond.Field + "` BETWEEN ? AND ?")
+				params = append(params, refVal.Index(0), refVal.Index(1))
+			}
+		}
+	}
+	str = sb.String()
+	return
+}
+
+func (query *Query) Select(fields ...string) *Query {
+	if query.fields == nil {
+		query.fields = fields
+	} else {
+		query.fields = append(query.fields, fields...)
+	}
+	return query
+}
+
+func (query *Query) From(table string) *Query {
+	query.table = table
+	return query
+}
+
+//// Joins specify Joins conditions
+////     db.Joins("JOIN emails ON emails.user_id = users.id AND emails.email = ?", "jinzhu@example.org").Find(&user)
+//func (s *DB) Joins(query string, args ...interface{}) *DB {
+//	return s.clone().search.Joins(query, args...).db
+//}
+func (query *Query) LeftJoin(table string, conds ...*condition) *Query {
+	query.joins = append(query.joins, join{
+		Table:     table,
+		Direction: "LEFT",
+		Conds:     conds,
+	})
+	return query
+}
+
+func (query *Query) RightJoin(table string, conds ...*condition) *Query {
+	query.joins = append(query.joins, join{
+		Table:     table,
+		Direction: "RIGHT",
+		Conds:     conds,
+	})
+	return query
+}
+
+func (query *Query) InnerJoin(table string, conds ...*condition) *Query {
+	query.joins = append(query.joins, join{
+		Table:     table,
+		Direction: "INNER",
+		Conds:     conds,
+	})
+	return query
+}
+
+func (query *Query) AndFilterWhere(conds ...*condition) *Query {
+	length := len(conds)
+	if length == 0 {
+		return query
+	}
+	cs, ps := query.buildConditions("AND", true, conds...)
+	if cs == "" {
+		return query
+	}
+	query.params = append(query.params, ps...)
+	if query.condition == "" {
+		query.condition = cs
+	} else {
+		query.condition += " AND " + cs
+	}
+	return query
+}
+
+func (query *Query) AndWhere(conds ...*condition) *Query {
+	length := len(conds)
+	if length == 0 {
+		return query
+	}
+	cs, ps := query.buildConditions("AND", false, conds...)
+	if cs == "" {
+		return query
+	}
+	query.params = append(query.params, ps...)
+	if query.condition == "" {
+		query.condition = cs
+	} else {
+		query.condition += " AND (" + cs + ")"
+	}
+	return query
+}
+
+func (query *Query) OrFilterWhere(conds ...*condition) *Query {
+	length := len(conds)
+	if length == 0 {
+		return query
+	}
+	cs, ps := query.buildConditions("OR", true, conds...)
+	if cs == "" {
+		return query
+	}
+	query.params = append(query.params, ps...)
+	if query.condition == "" {
+		query.condition = cs
+	} else {
+		query.condition += " AND (" + cs + ")"
+	}
+	return query
+}
+
+func (query *Query) OrWhere(conds ...*condition) *Query {
+	length := len(conds)
+	if length == 0 {
+		return query
+	}
+	cs, ps := query.buildConditions("OR", false, conds...)
+	if cs == "" {
+		return query
+	}
+	query.params = append(query.params, ps...)
+	if query.condition == "" {
+		query.condition = cs
+	} else {
+		query.condition += " AND (" + cs + ")"
+	}
+	return query
+}
+
+func (query *Query) GroupBy(cols ...string) *Query {
+	query.groupBy = append(query.groupBy, cols...)
+	return query
+}
+
+func (query *Query) OrderBy(col, direction string) *Query {
+	direction = strings.ToUpper(direction)
+	if direction == "" || !(direction == "ASC" || direction == "DESC") {
+		direction = "ASC"
+	}
+	query.orderBy = append(query.orderBy, col+" "+direction)
+	return query
+}
+
+func (query *Query) Offset(i int) *Query {
+	query.offset = i
+	return query
+}
+
+func (query *Query) Limit(i int) *Query {
+	query.limit = i
+	return query
+}
+
+func (query *Query) Count(v interface{}) (i int64) {
+	var (
+		db  *gorm.DB
+		err error
+	)
+	if db, err = query.compile(); err != nil {
+		return
+	} else {
+		err = db.Model(v).Count(&i).Error
+	}
+	return
+}
+
+func (query *Query) One(v interface{}) (err error) {
+	var (
+		db *gorm.DB
+	)
+	if db, err = query.compile(); err != nil {
+		return
+	} else {
+		err = db.First(v).Error
+	}
+	return
+}
+
+func (query *Query) All(v interface{}) (err error) {
+	var (
+		db *gorm.DB
+	)
+	if db, err = query.compile(); err != nil {
+		return
+	} else {
+		err = db.Find(v).Error
+	}
+	return
+}
+
+func NewQuery(db *gorm.DB) *Query {
+	return &Query{
+		db:      db,
+		params:  make([]interface{}, 0),
+		orderBy: make([]string, 0),
+		groupBy: make([]string, 0),
+		joins:   make([]join, 0),
+	}
+}
+
+func NewCond(field string, value interface{}) *condition {
+	return NewQueryCondition(field, value)
+}
+
+func NewQueryCondition(field string, value interface{}) *condition {
+	return &condition{
+		Field: field,
+		Value: value,
+		Expr:  "=",
+	}
+}
+
+func NewQueryConditionWithOperator(operator, field string, value interface{}) *condition {
+	cond := &condition{
+		Field: field,
+		Value: value,
+		Expr:  operator,
+	}
+	return cond
+}

+ 699 - 0
rest.go

@@ -0,0 +1,699 @@
+package rest
+
+import (
+	"context"
+	"encoding/csv"
+	"encoding/json"
+	"fmt"
+	"git.nspix.com/golang/micro/gateway/http"
+	errpkg "git.nspix.com/golang/rest/v3/error"
+	"git.nspix.com/golang/rest/v3/inflector"
+	"gorm.io/gorm"
+	"gorm.io/gorm/clause"
+	"path"
+	"reflect"
+	"strconv"
+	"strings"
+)
+
+const (
+	HttpAccessDenied          = 8004 //拒绝访问
+	HttpInvalidPayload        = 8002 //请求内容无效
+	HttpRequestCallbackFailed = 8003 //执行回调失败
+	HttpValidateFailed        = 8008 //数据校验失败
+	HttpDatabaseQueryFailed   = 8010 //查询失败
+	HttpDatabaseFindFailed    = 8011 //查找失败
+	HttpDatabaseCreateFailed  = 8012 //创建失败
+	HttpDatabaseUpdateFailed  = 8013 //更新失败
+	HttpDatabaseDeleteFailed  = 8014 //删除失败
+	HttpDatabaseExportFailed  = 8015 //数据导出失败
+	HTTPUnknownFailed         = 9001 //未知错误
+)
+
+const (
+	ErrorAccessDeniedMessage = "access denied"
+)
+
+type (
+	DiffAttr struct {
+		Column   string      `json:"column"`
+		Label    string      `json:"label"`
+		OldValue interface{} `json:"old_value"`
+		NewValue interface{} `json:"new_value"`
+	}
+
+	Restful struct {
+		model         Model
+		opts          *Options
+		reflectType   reflect.Type
+		reflectValue  reflect.Value
+		singularName  string
+		pluralizeName string
+		statement     *gorm.Statement
+		primaryKey    string
+	}
+)
+
+// getScenarioUrl 获取某个场景下HTTP请求的URL
+func (r *Restful) getScenarioUrl(scenario string) string {
+	var uri string
+	switch scenario {
+	case ScenarioList:
+		uri = r.opts.ApiPrefix + "/" + r.model.ModuleName() + "/" + r.pluralizeName
+	case ScenarioView:
+		uri = r.opts.ApiPrefix + "/" + r.model.ModuleName() + "/" + r.singularName + "/:id"
+	case ScenarioCreate:
+		uri = r.opts.ApiPrefix + "/" + r.model.ModuleName() + "/" + r.singularName
+	case ScenarioUpdate:
+		uri = r.opts.ApiPrefix + "/" + r.model.ModuleName() + "/" + r.singularName + "/:id"
+	case ScenarioDelete:
+		uri = r.opts.ApiPrefix + "/" + r.model.ModuleName() + "/" + r.singularName + "/:id"
+	case ScenarioExport:
+		uri = r.opts.ApiPrefix + "/" + r.model.ModuleName() + "/" + r.singularName + "-export"
+	}
+	return path.Clean(uri)
+}
+
+// getScenarioHandle 获取某个场景下HTTP请求的处理回调
+func (r *Restful) getScenarioHandle(scenario string) http.HandleFunc {
+	var handleFunc http.HandleFunc
+	switch scenario {
+	case ScenarioList:
+		handleFunc = r.actionIndex
+	case ScenarioView:
+		handleFunc = r.actionView
+	case ScenarioCreate:
+		handleFunc = r.actionCreate
+	case ScenarioUpdate:
+		handleFunc = r.actionUpdate
+	case ScenarioDelete:
+		handleFunc = r.actionDelete
+	case ScenarioExport:
+		handleFunc = r.actionExport
+	}
+	return handleFunc
+}
+
+func (r *Restful) setFieldValue(model reflect.Value, column string, value interface{}) {
+	var (
+		name string
+	)
+	refVal := reflect.Indirect(model)
+	for _, field := range r.statement.Schema.Fields {
+		if field.DBName == column {
+			name = field.Name
+			break
+		} else if field.Name == column {
+			name = column
+			break
+		}
+	}
+	if name == "" {
+		return
+	}
+	fieldVal := refVal.FieldByName(name)
+	if fieldVal.CanSet() {
+		fieldVal.Set(reflect.ValueOf(value))
+	}
+}
+
+//getFieldValue get field value from reflect value
+func (r *Restful) getFieldValue(model reflect.Value, column string) interface{} {
+	var (
+		name string
+	)
+	refVal := reflect.Indirect(model)
+	for _, field := range r.statement.Schema.Fields {
+		if field.DBName == column {
+			name = field.Name
+			break
+		} else if field.Name == column {
+			name = column
+			break
+		}
+	}
+	if name == "" {
+		return nil
+	}
+	fieldVal := refVal.FieldByName(name)
+	return fieldVal.Interface()
+}
+
+func (r *Restful) hasScenario(s string) bool {
+	if v, ok := r.model.(FlexibleModel); ok {
+		for _, n := range v.Scenarios() {
+			if s == n {
+				return true
+			}
+		}
+		return false
+	}
+	return true
+}
+
+func (r *Restful) prepareConditions(ctx context.Context, requestCtx *http.Context, query *Query, schemas []*Schema) (err error) {
+	var (
+		ok          bool
+		skip        bool
+		formValue   string
+		model       interface{}
+		activeModel ActiveModel
+	)
+	model = reflect.New(r.reflectType).Interface()
+	if r.opts.Delegate != nil {
+		if err = r.opts.Delegate.BeforeQuery(ctx, query); err != nil {
+			return
+		}
+	}
+	if activeModel, ok = model.(ActiveModel); ok {
+		if err = activeModel.BeforeQuery(ctx, query); err != nil {
+			return
+		}
+	}
+	//处理默认的搜索
+	for _, schema := range schemas {
+		skip = false
+		if skip {
+			continue
+		}
+		if schema.Native == 0 {
+			continue
+		}
+		formValue = requestCtx.FormValue(schema.Column)
+		switch schema.Format {
+		case "string", "text", "textarea":
+			if schema.Attribute.Match == MatchExactly {
+				query.AndFilterWhere(NewCond(schema.Column, formValue))
+			} else {
+				query.AndFilterWhere(NewCond(schema.Column, formValue).WithExpr("LIKE"))
+			}
+		case "date", "time", "datetime":
+			var sep string
+			seps := []byte{',', '/'}
+			for _, s := range seps {
+				if strings.IndexByte(formValue, s) > -1 {
+					sep = string(s)
+				}
+			}
+			if ss := strings.Split(formValue, sep); len(ss) == 2 {
+				query.AndFilterWhere(
+					NewCond(schema.Column, strings.TrimSpace(ss[0])).WithExpr(">="),
+					NewCond(schema.Column, strings.TrimSpace(ss[1])).WithExpr("<="),
+				)
+			} else {
+				query.AndFilterWhere(NewCond(schema.Column, formValue))
+			}
+		case "duration", "number", "integer", "decimal":
+			query.AndFilterWhere(NewCond(schema.Column, formValue))
+		default:
+			if schema.Type == "string" {
+				if schema.Attribute.Match == MatchExactly {
+					query.AndFilterWhere(NewCond(schema.Column, formValue))
+				} else {
+					query.AndFilterWhere(NewCond(schema.Column, formValue).WithExpr("LIKE"))
+				}
+			} else {
+				query.AndFilterWhere(NewCond(schema.Column, formValue))
+			}
+		}
+	}
+	//处理排序
+	sortPar := requestCtx.FormValue("sort")
+	if sortPar != "" {
+		sorts := strings.Split(sortPar, ",")
+		for _, s := range sorts {
+			if s[0] == '-' {
+				query.OrderBy(s[1:], "DESC")
+			} else {
+				if s[0] == '+' {
+					query.OrderBy(s[1:], "ASC")
+				} else {
+					query.OrderBy(s, "ASC")
+				}
+			}
+		}
+	}
+	if activeModel, ok = model.(ActiveModel); ok {
+		err = activeModel.AfterQuery(ctx, query)
+	}
+	if r.opts.Delegate != nil {
+		err = r.opts.Delegate.AfterQuery(ctx, query)
+	}
+	return
+}
+
+func (r *Restful) actionIndex(httpCtx *http.Context) (err error) {
+	var (
+		page      int
+		pageSize  int
+		pageIndex int
+		query     *Query
+		namespace string
+	)
+	if !r.hasScenario(ScenarioList) {
+		return httpCtx.Error(HttpAccessDenied, ErrorAccessDeniedMessage)
+	}
+	ctx := httpCtx.Request().Context()
+	if ctx == nil {
+		ctx = context.Background()
+	}
+	namespace = httpCtx.ParamValue(NamespaceVariable)
+	ctx = context.WithValue(ctx, NamespaceField, namespace)
+	ctx = context.WithValue(ctx, "request", httpCtx.Request())
+	page, _ = strconv.Atoi(httpCtx.FormValue("page"))
+	pageSize, _ = strconv.Atoi(httpCtx.FormValue("pagesize"))
+	if pageSize <= 0 {
+		pageSize = 15
+	}
+	pageIndex = page
+	if pageIndex > 0 {
+		pageIndex--
+	}
+	sliceValue := reflect.MakeSlice(reflect.SliceOf(r.reflectType), 0, 0)
+	models := reflect.New(sliceValue.Type())
+	models.Elem().Set(sliceValue)
+	query = NewQuery(r.opts.DB)
+	searchSchemas := r.opts.LookupFunc(ctx, namespace, r.model.ModuleName(), r.model.TableName(), ScenarioSearch)
+	indexSchemas := r.opts.LookupFunc(ctx, namespace, r.model.ModuleName(), r.model.TableName(), ScenarioList)
+	if err = r.prepareConditions(ctx, httpCtx, query, searchSchemas); err != nil {
+		return httpCtx.Error(HttpDatabaseQueryFailed, err.Error())
+	}
+	if r.opts.EnableNamespace {
+		query.AndFilterWhere(NewQueryCondition(NamespaceField, namespace))
+	}
+	query.Offset(pageIndex * pageSize).Limit(pageSize)
+	if err = query.All(models.Interface()); err != nil {
+		return httpCtx.Error(HttpDatabaseQueryFailed, err.Error())
+	}
+	resp := &ListResponse{
+		Page:       page,
+		PageSize:   pageSize,
+		TotalCount: query.Limit(0).Offset(0).Count(r.model),
+	}
+	if resp.TotalCount > 0 {
+		resp.Data = r.opts.Formatter.formatModels(ctx, models.Interface(), indexSchemas, r.statement)
+	} else {
+		resp.Data = make([]string, 0)
+	}
+	return httpCtx.Success(resp)
+}
+
+func (r *Restful) actionCreate(httpCtx *http.Context) (err error) {
+	var (
+		errTx     error
+		namespace string
+		model     interface{}
+		schemas   []*Schema
+		refModel  reflect.Value
+		diffAttrs []*DiffAttr
+	)
+	ctx := httpCtx.Request().Context()
+	if ctx == nil {
+		ctx = context.Background()
+	}
+	diffAttrs = make([]*DiffAttr, 0, 10)
+	if !r.hasScenario(ScenarioCreate) {
+		return httpCtx.Error(HttpAccessDenied, ErrorAccessDeniedMessage)
+	}
+	namespace = httpCtx.ParamValue(NamespaceVariable)
+	refModel = reflect.New(r.reflectType)
+	model = refModel.Interface()
+	if err = httpCtx.Bind(model); err != nil {
+		return httpCtx.Error(HttpInvalidPayload, err.Error())
+	}
+	schemas = r.opts.LookupFunc(ctx, namespace, r.model.ModuleName(), r.model.TableName(), ScenarioCreate)
+
+	//global set field value
+	r.setFieldValue(refModel, NamespaceField, namespace)
+	r.setFieldValue(refModel, CreatedByField, httpCtx.ParamValue(UserVariable))
+	r.setFieldValue(refModel, CreatedDeptField, httpCtx.ParamValue(DepartmentVariable))
+	r.setFieldValue(refModel, UpdatedByField, httpCtx.ParamValue(UserVariable))
+	r.setFieldValue(refModel, UpdatedDeptField, httpCtx.ParamValue(DepartmentVariable))
+
+	if err = r.opts.DB.Transaction(func(tx *gorm.DB) error {
+		if r.opts.Delegate != nil {
+			if errTx = r.opts.Delegate.BeforeCreate(ctx, model); errTx != nil {
+				return httpCtx.Error(HttpRequestCallbackFailed, errTx.Error())
+			}
+			if errTx = r.opts.Delegate.BeforeSave(ctx, model); errTx != nil {
+				return httpCtx.Error(HttpRequestCallbackFailed, errTx.Error())
+			}
+		}
+		if activeModel, ok := model.(ActiveModel); ok {
+			if errTx = activeModel.BeforeCreate(ctx, model); errTx != nil {
+				return httpCtx.Error(HttpRequestCallbackFailed, errTx.Error())
+			}
+			if errTx = activeModel.BeforeSave(ctx, model); errTx != nil {
+				return httpCtx.Error(HttpRequestCallbackFailed, errTx.Error())
+			}
+		}
+		//创建数据
+		if errTx = tx.Create(model).Error; errTx != nil {
+			return errTx
+		}
+		//对比差异数据
+		for _, scm := range schemas {
+			diffAttrs = append(diffAttrs, &DiffAttr{
+				Column:   scm.Column,
+				Label:    scm.Label,
+				OldValue: nil,
+				NewValue: r.getFieldValue(refModel, scm.Column),
+			})
+		}
+		if activeModel, ok := model.(ActiveModel); ok {
+			if errTx = activeModel.AfterCreate(ctx, model, diffAttrs); errTx != nil {
+				return httpCtx.Error(HttpRequestCallbackFailed, err.Error())
+			}
+			if errTx = activeModel.AfterSave(ctx, model, diffAttrs); errTx != nil {
+				return httpCtx.Error(HttpRequestCallbackFailed, err.Error())
+			}
+		}
+		if r.opts.Delegate != nil {
+			if errTx = r.opts.Delegate.AfterCreate(ctx, model, diffAttrs); errTx != nil {
+				return httpCtx.Error(HttpRequestCallbackFailed, err.Error())
+			}
+			if errTx = r.opts.Delegate.AfterSave(ctx, model, diffAttrs); errTx != nil {
+				return httpCtx.Error(HttpRequestCallbackFailed, err.Error())
+			}
+		}
+		return errTx
+	}); err == nil {
+		pkVal := r.getFieldValue(refModel, r.primaryKey)
+		return httpCtx.Success(&CreateResponse{
+			ID:    pkVal,
+			Topic: r.model.TableName(),
+			State: "created",
+		})
+	}
+	//form validation
+	if validateError, ok := err.(*errpkg.StructError); ok {
+		httpCtx.Response().Header().Set("Content-Type", "application/json")
+		return json.NewEncoder(httpCtx.Response()).Encode(map[string]interface{}{
+			"errno":  HttpValidateFailed,
+			"result": validateError,
+		})
+	}
+	return httpCtx.Error(HttpDatabaseCreateFailed, err.Error())
+}
+
+func (r *Restful) actionUpdate(httpCtx *http.Context) (err error) {
+	var (
+		errTx     error
+		namespace string
+		model     interface{}
+		schemas   []*Schema
+		refModel  reflect.Value
+		oldValues = make(map[string]interface{})
+		diffs     = make(map[string]interface{})
+		diffAttrs = make([]*DiffAttr, 0)
+	)
+	ctx := httpCtx.Request().Context()
+	if ctx == nil {
+		ctx = context.Background()
+	}
+	if !r.hasScenario(ScenarioUpdate) {
+		return httpCtx.Error(HttpAccessDenied, ErrorAccessDeniedMessage)
+	}
+	namespace = httpCtx.ParamValue(NamespaceVariable)
+	idStr := httpCtx.ParamValue("id")
+	refModel = reflect.New(r.reflectType)
+	model = refModel.Interface()
+	//默认设置更新用户
+	r.setFieldValue(refModel, UpdatedByField, httpCtx.ParamValue(UserVariable))
+	r.setFieldValue(refModel, UpdatedDeptField, httpCtx.ParamValue(DepartmentVariable))
+	conditions := map[string]interface{}{
+		r.primaryKey: idStr,
+	}
+	if r.opts.EnableNamespace {
+		conditions[NamespaceField] = namespace
+	}
+	if err = r.opts.DB.Where(conditions).First(model).Error; err != nil {
+		return httpCtx.Error(HttpDatabaseFindFailed, err.Error())
+	}
+	schemas = r.opts.LookupFunc(ctx, namespace, r.model.ModuleName(), r.model.TableName(), ScenarioUpdate)
+	for _, scm := range schemas {
+		oldValues[scm.Column] = r.getFieldValue(refModel, scm.Column)
+	}
+	if err = httpCtx.Bind(model); err != nil {
+		return httpCtx.Error(HttpInvalidPayload, err.Error())
+	}
+	if err = r.opts.DB.Transaction(func(tx *gorm.DB) error {
+		if r.opts.Delegate != nil {
+			if errTx = r.opts.Delegate.BeforeUpdate(ctx, model); errTx != nil {
+				return httpCtx.Error(HttpRequestCallbackFailed, errTx.Error())
+			}
+			if errTx = r.opts.Delegate.BeforeSave(ctx, model); errTx != nil {
+				return httpCtx.Error(HttpRequestCallbackFailed, errTx.Error())
+			}
+		}
+		if activeModel, ok := model.(ActiveModel); ok {
+			if errTx = activeModel.BeforeUpdate(ctx, model); errTx != nil {
+				return httpCtx.Error(HttpRequestCallbackFailed, errTx.Error())
+			}
+			if errTx = activeModel.BeforeSave(ctx, model); errTx != nil {
+				return httpCtx.Error(HttpRequestCallbackFailed, errTx.Error())
+			}
+		}
+		//对比差异数据
+		for _, scm := range schemas {
+			v := r.getFieldValue(refModel, scm.Column)
+			if oldValues[scm.Column] != v {
+				diffs[scm.Column] = v
+				diffAttrs = append(diffAttrs, &DiffAttr{
+					Column:   scm.Column,
+					Label:    scm.Label,
+					OldValue: oldValues[scm.Column],
+					NewValue: v,
+				})
+			}
+		}
+		//进行局部数据更新
+		if len(diffs) > 0 {
+			if errTx = tx.Model(model).Updates(diffs).Error; errTx != nil {
+				return errTx
+			}
+		}
+		if activeModel, ok := model.(ActiveModel); ok {
+			if errTx = activeModel.AfterUpdate(ctx, model, diffAttrs); errTx != nil {
+				return httpCtx.Error(HttpRequestCallbackFailed, errTx.Error())
+			}
+			if errTx = activeModel.AfterSave(ctx, model, diffAttrs); errTx != nil {
+				return httpCtx.Error(HttpRequestCallbackFailed, errTx.Error())
+			}
+		}
+		if r.opts.Delegate != nil {
+			if errTx = r.opts.Delegate.AfterUpdate(ctx, model, diffAttrs); errTx != nil {
+				return httpCtx.Error(HttpRequestCallbackFailed, errTx.Error())
+			}
+			if errTx = r.opts.Delegate.AfterSave(ctx, model, diffAttrs); errTx != nil {
+				return httpCtx.Error(HttpRequestCallbackFailed, errTx.Error())
+			}
+		}
+		return errTx
+	}); err == nil {
+		pkVal := r.getFieldValue(refModel, r.primaryKey)
+		return httpCtx.Success(&UpdateResponse{
+			ID:    pkVal,
+			Topic: r.model.TableName(),
+			State: "updated",
+		})
+	}
+	//form validation
+	if validateError, ok := err.(*errpkg.StructError); ok {
+		httpCtx.Response().Header().Set("Content-Type", "application/json")
+		return json.NewEncoder(httpCtx.Response()).Encode(map[string]interface{}{
+			"errno":  HttpValidateFailed,
+			"result": validateError,
+		})
+	}
+	return httpCtx.Error(HttpDatabaseUpdateFailed, err.Error())
+}
+
+func (r *Restful) actionDelete(httpCtx *http.Context) (err error) {
+	var (
+		errTx     error
+		model     interface{}
+		namespace string
+	)
+	if !r.hasScenario(ScenarioDelete) {
+		return httpCtx.Error(HttpAccessDenied, ErrorAccessDeniedMessage)
+	}
+	ctx := httpCtx.Request().Context()
+	if ctx == nil {
+		ctx = context.Background()
+	}
+	idStr := httpCtx.ParamValue("id")
+	namespace = httpCtx.ParamValue(NamespaceVariable)
+	model = reflect.New(r.reflectType).Interface()
+	conditions := map[string]interface{}{
+		r.primaryKey: idStr,
+	}
+	if r.opts.EnableNamespace {
+		conditions[NamespaceField] = namespace
+	}
+	if err = r.opts.DB.Where(conditions).First(model).Error; err != nil {
+		return httpCtx.Error(HttpDatabaseFindFailed, err.Error())
+	}
+	if err = r.opts.DB.Transaction(func(tx *gorm.DB) error {
+		if r.opts.Delegate != nil {
+			if errTx = r.opts.Delegate.BeforeDelete(ctx, model); errTx != nil {
+				return httpCtx.Error(HttpRequestCallbackFailed, errTx.Error())
+			}
+		}
+		if activeModel, ok := model.(ActiveModel); ok {
+			if errTx = activeModel.BeforeDelete(ctx, model); errTx != nil {
+				return httpCtx.Error(HttpRequestCallbackFailed, errTx.Error())
+			}
+		}
+		if errTx = tx.Delete(model).Error; errTx != nil {
+			return errTx
+		}
+		if activeModel, ok := model.(ActiveModel); ok {
+			if errTx = activeModel.AfterDelete(ctx, model); errTx != nil {
+				return httpCtx.Error(HttpRequestCallbackFailed, errTx.Error())
+			}
+		}
+		if r.opts.Delegate != nil {
+			if errTx = r.opts.Delegate.AfterDelete(ctx, model); errTx != nil {
+				return httpCtx.Error(HttpRequestCallbackFailed, errTx.Error())
+			}
+		}
+		return errTx
+	}); err == nil {
+		return httpCtx.Success(&DeleteResponse{
+			ID:    r.getFieldValue(reflect.ValueOf(model), r.primaryKey),
+			Topic: r.model.TableName(),
+			State: "updated",
+		})
+	} else {
+		return httpCtx.Error(HttpDatabaseDeleteFailed, err.Error())
+	}
+}
+
+func (r *Restful) actionExport(httpCtx *http.Context) (err error) {
+	var (
+		query     *Query
+		namespace string
+	)
+	if !r.hasScenario(ScenarioExport) {
+		return httpCtx.Error(HttpAccessDenied, ErrorAccessDeniedMessage)
+	}
+	ctx := httpCtx.Request().Context()
+	if ctx == nil {
+		ctx = context.Background()
+	}
+	ctx = context.WithValue(ctx, NamespaceVariable, namespace)
+	namespace = httpCtx.ParamValue(NamespaceVariable)
+	sliceValue := reflect.MakeSlice(reflect.SliceOf(r.reflectType), 0, 0)
+	models := reflect.New(sliceValue.Type())
+	models.Elem().Set(sliceValue)
+	query = NewQuery(r.opts.DB)
+	searchSchemas := r.opts.LookupFunc(ctx, namespace, r.model.ModuleName(), r.model.TableName(), ScenarioSearch)
+	exportSchemas := r.opts.LookupFunc(ctx, namespace, r.model.ModuleName(), r.model.TableName(), ScenarioList)
+	if err = r.prepareConditions(ctx, httpCtx, query, searchSchemas); err != nil {
+		return httpCtx.Error(HttpDatabaseQueryFailed, err.Error())
+	}
+	if r.opts.EnableNamespace {
+		query.AndFilterWhere(NewQueryCondition(NamespaceVariable, namespace))
+	}
+	if err = query.All(models.Interface()); err != nil {
+		return httpCtx.Error(HttpDatabaseExportFailed, err.Error())
+	}
+	httpCtx.Response().Header().Set("Content-Type", "text/csv")
+	httpCtx.Response().Header().Set("Access-Control-Expose-Headers", "Content-Disposition")
+	httpCtx.Response().Header().Set("Content-Disposition", fmt.Sprintf("attachment;filename=%s.csv", r.singularName))
+	value := r.opts.Formatter.formatModels(ctx, models.Interface(), exportSchemas, r.statement)
+	writer := csv.NewWriter(httpCtx.Response())
+	ss := make([]string, len(exportSchemas))
+	for i, field := range exportSchemas {
+		ss[i] = field.Label
+	}
+	_ = writer.Write(ss)
+	if values, ok := value.([]interface{}); ok {
+		for _, val := range values {
+			row, ok2 := val.(map[string]interface{})
+			if !ok2 {
+				continue
+			}
+			for i, field := range exportSchemas {
+				if v, ok := row[field.Column]; ok {
+					ss[i] = fmt.Sprint(v)
+				} else {
+					ss[i] = ""
+				}
+			}
+			_ = writer.Write(ss)
+		}
+	}
+	writer.Flush()
+	return
+}
+
+func (r *Restful) actionView(httpCtx *http.Context) (err error) {
+	var (
+		model     interface{}
+		namespace string
+	)
+	if !r.hasScenario(ScenarioView) {
+		return httpCtx.Error(HttpAccessDenied, ErrorAccessDeniedMessage)
+	}
+	namespace = httpCtx.ParamValue(NamespaceVariable)
+	scenario := httpCtx.FormValue("scenario")
+	idStr := httpCtx.ParamValue("id")
+	model = reflect.New(r.reflectType).Interface()
+	conditions := map[string]interface{}{
+		r.primaryKey: idStr,
+	}
+	if r.opts.EnableNamespace {
+		conditions[NamespaceField] = namespace
+	}
+	if err = r.opts.DB.Where(conditions).First(model).Error; err != nil {
+		return httpCtx.Error(HttpDatabaseFindFailed, err.Error())
+	}
+	if httpCtx.FormValue("format") != "" {
+		//获取指定场景下面的字段进行渲染显示
+		var schemas []*Schema
+		if scenario == "" {
+			schemas = r.opts.LookupFunc(httpCtx.Request().Context(), namespace, r.model.ModuleName(), r.model.TableName(), ScenarioView)
+		} else {
+			schemas = r.opts.LookupFunc(httpCtx.Request().Context(), namespace, r.model.ModuleName(), r.model.TableName(), scenario)
+		}
+		requestCtx := httpCtx.Request().Context()
+		if requestCtx == nil {
+			requestCtx = context.Background()
+		}
+		requestCtx = context.WithValue(requestCtx, NamespaceVariable, namespace)
+		return httpCtx.Success(r.opts.Formatter.formatModel(requestCtx, model, schemas, r.statement))
+	}
+	return httpCtx.Success(model)
+}
+
+func newRestful(model Model, opts *Options) *Restful {
+	var (
+		err       error
+		tableName string
+	)
+	r := &Restful{
+		opts:         opts,
+		model:        model,
+		reflectValue: reflect.Indirect(reflect.ValueOf(model)),
+	}
+	r.reflectType = r.reflectValue.Type()
+	tableName = model.TableName()
+	r.singularName = inflector.Singularize(tableName)
+	r.pluralizeName = inflector.Pluralize(tableName)
+
+	r.statement = &gorm.Statement{
+		DB:       r.opts.DB,
+		ConnPool: r.opts.DB.ConnPool,
+		Clauses:  map[string]clause.Clause{},
+	}
+	if err = r.statement.Parse(model); err == nil {
+		if r.statement.Schema != nil {
+			if r.statement.Schema.PrimaryFieldDBNames != nil && len(r.statement.Schema.PrimaryFieldDBNames) > 0 {
+				r.primaryKey = r.statement.Schema.PrimaryFieldDBNames[0]
+			}
+		}
+	}
+	return r
+}

+ 166 - 0
schema.go

@@ -0,0 +1,166 @@
+package rest
+
+import (
+	"database/sql/driver"
+	"encoding/json"
+	"errors"
+)
+
+var (
+	errDbTypeUnsupported = errors.New("database type unsupported")
+)
+
+const (
+	ScenarioCreate  = "create"
+	ScenarioUpdate  = "update"
+	ScenarioDelete  = "delete"
+	ScenarioSearch  = "search"
+	ScenarioExport  = "export"
+	ScenarioList    = "list"
+	ScenarioView    = "view"
+	ScenarioMapping = "mapping"
+)
+
+const (
+	MatchExactly = "exactly" //精确匹配
+	MatchFuzzy   = "fuzzy"   //模糊匹配
+)
+
+const (
+	LiveTypeDropdown = "dropdown"
+	LiveTypeCascader = "cascader"
+)
+
+type (
+	LiveValue struct {
+		Enable  bool     `json:"enable"`
+		Type    string   `json:"type"`
+		Url     string   `json:"url"`
+		Columns []string `json:"columns"`
+	}
+
+	EnumValue struct {
+		Label string `json:"label"`
+		Value string `json:"value"`
+		Color string `json:"color"`
+	}
+
+	VisibleCondition struct {
+		Column string        `json:"column"`
+		Values []interface{} `json:"values"`
+	}
+
+	Rule struct {
+		Min      int      `json:"min"`
+		Max      int      `json:"max"`
+		Type     string   `json:"type"`
+		Unique   bool     `json:"unique"`
+		Required []string `json:"required"`
+		Regular  string   `json:"regular"`
+	}
+
+	Attribute struct {
+		Match        string             `json:"match"`         //匹配模式
+		PrimaryKey   bool               `json:"primary_key"`   //是否为主键
+		DefaultValue string             `json:"default_value"` //默认值
+		Readonly     []string           `json:"readonly"`      //只读场景
+		Disable      []string           `json:"disable"`       //禁用场景
+		Visible      []VisibleCondition `json:"visible"`       //可见条件
+		Values       []EnumValue        `json:"values"`        //值
+		Live         LiveValue          `json:"live"`          //延时加载配置
+		Icon         string             `json:"icon"`          //显示图标
+		Sort         bool               `json:"sort"`          //是否允许排序
+		Suffix       string             `json:"suffix"`        //追加内容
+		Tooltip      string             `json:"tooltip"`       //字段提示信息
+		Description  string             `json:"description"`   //字段说明信息
+	}
+
+	Scenarios []string
+
+	Schema struct {
+		Id           uint64    `json:"id" gorm:"primary_key"`
+		CreatedAt    int64     `json:"created_at" gorm:"autoCreateTime"`                             //创建时间
+		UpdatedAt    int64     `json:"updated_at" gorm:"autoUpdateTime"`                             //更新时间
+		Namespace    string    `json:"namespace" gorm:"column:namespace;type:char(60);index"`        //域
+		ModuleName   string    `json:"module_name" gorm:"column:module_name;type:varchar(60);index"` //模块名称
+		TableName    string    `json:"table_name" gorm:"column:table_name;type:varchar(120);index"`  //表名称
+		Enable       uint8     `json:"enable" gorm:"column:enable;type:int(1)"`                      //是否启用
+		Column       string    `json:"column" gorm:"type:varchar(120)"`                              //字段名称
+		Label        string    `json:"label" gorm:"type:varchar(120)"`                               //显示名称
+		Type         string    `json:"type" gorm:"type:varchar(120)"`                                //字段类型
+		Format       string    `json:"format" gorm:"type:varchar(120)"`                              //字段格式
+		Native       uint8     `json:"native" gorm:"type:int(1)"`                                    //是否为原生字段
+		IsPrimaryKey uint8     `json:"is_primary_key" gorm:"type:int(1)"`                            //是否为主键
+		Expression   string    `json:"expression" gorm:"type:varchar(526)"`                          //计算规则
+		Scenarios    Scenarios `json:"scenarios" gorm:"type:varchar(120)"`                           //场景
+		Rule         Rule      `json:"rule" gorm:"type:varchar(2048)"`                               //字段规则
+		Attribute    Attribute `json:"attribute" gorm:"type:varchar(4096)"`                          //字段属性
+		Position     int       `json:"position"`                                                     //字段排序位置
+	}
+)
+
+func (n Scenarios) Has(str string) bool {
+	for _, v := range n {
+		if v == str {
+			return true
+		}
+	}
+	return false
+}
+
+// Scan implements the Scanner interface.
+func (n *Scenarios) Scan(value interface{}) error {
+	if value == nil {
+		return nil
+	}
+	switch s := value.(type) {
+	case string:
+		return json.Unmarshal([]byte(s), n)
+	case []byte:
+		return json.Unmarshal(s, n)
+	}
+	return errDbTypeUnsupported
+}
+
+// Value implements the driver Valuer interface.
+func (n Scenarios) Value() (driver.Value, error) {
+	return json.Marshal(n)
+}
+
+// Scan implements the Scanner interface.
+func (n *Attribute) Scan(value interface{}) error {
+	if value == nil {
+		return nil
+	}
+	switch s := value.(type) {
+	case string:
+		return json.Unmarshal([]byte(s), n)
+	case []byte:
+		return json.Unmarshal(s, n)
+	}
+	return errDbTypeUnsupported
+}
+
+// Value implements the driver Valuer interface.
+func (n Attribute) Value() (driver.Value, error) {
+	return json.Marshal(n)
+}
+
+// Scan implements the Scanner interface.
+func (n *Rule) Scan(value interface{}) error {
+	if value == nil {
+		return nil
+	}
+	switch s := value.(type) {
+	case string:
+		return json.Unmarshal([]byte(s), n)
+	case []byte:
+		return json.Unmarshal(s, n)
+	}
+	return errDbTypeUnsupported
+}
+
+// Value implements the driver Valuer interface.
+func (n Rule) Value() (driver.Value, error) {
+	return json.Marshal(n)
+}

+ 28 - 0
utils/empty.go

@@ -0,0 +1,28 @@
+package utils
+
+import "reflect"
+
+//IsEmpty Determine whether a variable is empty
+func IsEmpty(val interface{}) bool {
+	if val == nil {
+		return true
+	}
+	v := reflect.ValueOf(val)
+	switch v.Kind() {
+	case reflect.String, reflect.Array:
+		return v.Len() == 0
+	case reflect.Map, reflect.Slice:
+		return v.Len() == 0 || v.IsNil()
+	case reflect.Bool:
+		return !v.Bool()
+	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
+		return v.Int() == 0
+	case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
+		return v.Uint() == 0
+	case reflect.Float32, reflect.Float64:
+		return v.Float() == 0
+	case reflect.Interface, reflect.Ptr:
+		return v.IsNil()
+	}
+	return reflect.DeepEqual(val, reflect.Zero(v.Type()).Interface())
+}