rename internal/structtag to internal/structs; add more feature for internal/structs; improve gvalid to support recursive validation for struct; improve gconv.Struct/Map functions

This commit is contained in:
John
2019-07-04 11:11:41 +08:00
parent 8d01e565c5
commit b29c6add47
13 changed files with 207 additions and 77 deletions

View File

@ -0,0 +1,15 @@
// Copyright 2019 gf Author(https://github.com/gogf/gf). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
// Package structs provides functions for struct conversion.
package structs
import (
"github.com/gogf/gf/third/github.com/fatih/structs"
)
// Field is alias of structs.Field.
type Field = structs.Field

View File

@ -4,19 +4,21 @@
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
// Package util provides util functions for internal usage.
package structtag
package structs
import (
"reflect"
"strings"
"github.com/gogf/gf/third/github.com/fatih/structs"
)
// Map recursively retrieves struct tags as map[tag]attribute from <pointer>, and returns it.
func Map(pointer interface{}, priority []string) map[string]string {
tagMap := make(map[string]string)
// MapField retrieves struct field as map[name/tag]*Field from <pointer>, and returns it.
//
// The parameter <recursive> specifies whether retrieving the struct field recursively.
//
// Note that it only retrieves the exported attributes with first letter up-case from struct.
func MapField(pointer interface{}, priority []string, recursive bool) map[string]*Field {
fieldMap := make(map[string]*Field)
fields := ([]*structs.Field)(nil)
if v, ok := pointer.(reflect.Value); ok {
fields = structs.Fields(v.Interface())
@ -26,6 +28,12 @@ func Map(pointer interface{}, priority []string) map[string]string {
tag := ""
name := ""
for _, field := range fields {
name = field.Name()
// Only retrieve exported attributes.
if name[0] < byte('A') || name[0] > byte('Z') {
continue
}
fieldMap[name] = field
tag = ""
for _, p := range priority {
tag = field.Tag(p)
@ -33,16 +41,10 @@ func Map(pointer interface{}, priority []string) map[string]string {
break
}
}
name = field.Name()
// Only retrieve exported attributes.
if name[0] < byte('A') || name[0] > byte('Z') {
continue
}
if tag != "" {
for _, v := range strings.Split(tag, ",") {
tagMap[strings.TrimSpace(v)] = name
}
} else {
fieldMap[tag] = field
}
if recursive {
rv := reflect.ValueOf(field.Value())
kind := rv.Kind()
if kind == reflect.Ptr {
@ -50,13 +52,13 @@ func Map(pointer interface{}, priority []string) map[string]string {
kind = rv.Kind()
}
if kind == reflect.Struct {
for k, v := range Map(rv, priority) {
if _, ok := tagMap[k]; !ok {
tagMap[k] = v
for k, v := range MapField(rv, priority, true) {
if _, ok := fieldMap[k]; !ok {
fieldMap[k] = v
}
}
}
}
}
return tagMap
return fieldMap
}

View File

@ -0,0 +1,81 @@
// Copyright 2019 gf Author(https://github.com/gogf/gf). All Rights Reserved.
//
// This Source Code Form is subject to the terms of the MIT License.
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package structs
import (
"reflect"
"github.com/gogf/gf/third/github.com/fatih/structs"
)
// TagMapName retrieves struct tags as map[tag]attribute from <pointer>, and returns it.
//
// The parameter <recursive> specifies whether retrieving the struct field recursively.
//
// Note that it only retrieves the exported attributes with first letter up-case from struct.
func TagMapName(pointer interface{}, priority []string, recursive bool) map[string]string {
tagMap := TagMapField(pointer, priority, recursive)
if len(tagMap) > 0 {
m := make(map[string]string, len(tagMap))
for k, v := range tagMap {
m[k] = v.Name()
}
return m
}
return nil
}
// TagMapField retrieves struct tags as map[tag]*Field from <pointer>, and returns it.
//
// The parameter <recursive> specifies whether retrieving the struct field recursively.
//
// Note that it only retrieves the exported attributes with first letter up-case from struct.
func TagMapField(pointer interface{}, priority []string, recursive bool) map[string]*Field {
tagMap := make(map[string]*Field)
fields := ([]*structs.Field)(nil)
if v, ok := pointer.(reflect.Value); ok {
fields = structs.Fields(v.Interface())
} else {
fields = structs.Fields(pointer)
}
tag := ""
name := ""
for _, field := range fields {
name = field.Name()
// Only retrieve exported attributes.
if name[0] < byte('A') || name[0] > byte('Z') {
continue
}
tag = ""
for _, p := range priority {
tag = field.Tag(p)
if tag != "" {
break
}
}
if tag != "" {
tagMap[tag] = field
}
if recursive {
rv := reflect.ValueOf(field.Value())
kind := rv.Kind()
if kind == reflect.Ptr {
rv = rv.Elem()
kind = rv.Kind()
}
if kind == reflect.Struct {
for k, v := range TagMapField(rv, priority, true) {
if _, ok := tagMap[k]; !ok {
tagMap[k] = v
}
}
}
}
}
return tagMap
}

View File

@ -4,14 +4,14 @@
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
package structtag_test
package structs_test
import (
"testing"
"github.com/gogf/gf/g"
"github.com/gogf/gf/g/internal/structs"
"github.com/gogf/gf/g/internal/structtag"
"github.com/gogf/gf/g"
"github.com/gogf/gf/g/test/gtest"
)
@ -24,12 +24,12 @@ func Test_Basic(t *testing.T) {
Pass string `my-tag1:"pass1" my-tag2:"pass2" params:"pass"`
}
var user User
gtest.Assert(structtag.Map(user, []string{"params"}), g.Map{"name": "Name", "pass": "Pass"})
gtest.Assert(structtag.Map(&user, []string{"params"}), g.Map{"name": "Name", "pass": "Pass"})
gtest.Assert(structs.TagMapName(user, []string{"params"}, true), g.Map{"name": "Name", "pass": "Pass"})
gtest.Assert(structs.TagMapName(&user, []string{"params"}, true), g.Map{"name": "Name", "pass": "Pass"})
gtest.Assert(structtag.Map(&user, []string{"params", "my-tag1"}), g.Map{"name": "Name", "pass": "Pass"})
gtest.Assert(structtag.Map(&user, []string{"my-tag1", "params"}), g.Map{"name": "Name", "pass1": "Pass"})
gtest.Assert(structtag.Map(&user, []string{"my-tag2", "params"}), g.Map{"name": "Name", "pass2": "Pass"})
gtest.Assert(structs.TagMapName(&user, []string{"params", "my-tag1"}, true), g.Map{"name": "Name", "pass": "Pass"})
gtest.Assert(structs.TagMapName(&user, []string{"my-tag1", "params"}, true), g.Map{"name": "Name", "pass1": "Pass"})
gtest.Assert(structs.TagMapName(&user, []string{"my-tag2", "params"}, true), g.Map{"name": "Name", "pass2": "Pass"})
})
gtest.Case(t, func() {
@ -43,7 +43,11 @@ func Test_Basic(t *testing.T) {
Base `params:"base"`
}
user := new(UserWithBase)
gtest.Assert(structtag.Map(user, []string{"params"}), g.Map{"base": "Base"})
gtest.Assert(structs.TagMapName(user, []string{"params"}, true), g.Map{
"base": "Base",
"password1": "Pass1",
"password2": "Pass2",
})
})
gtest.Case(t, func() {
@ -63,7 +67,7 @@ func Test_Basic(t *testing.T) {
}
user1 := new(UserWithBase1)
user2 := new(UserWithBase2)
gtest.Assert(structtag.Map(user1, []string{"params"}), g.Map{"password1": "Pass1", "password2": "Pass2"})
gtest.Assert(structtag.Map(user2, []string{"params"}), g.Map{"password1": "Pass1", "password2": "Pass2"})
gtest.Assert(structs.TagMapName(user1, []string{"params"}, true), g.Map{"password1": "Pass1", "password2": "Pass2"})
gtest.Assert(structs.TagMapName(user2, []string{"params"}, true), g.Map{"password1": "Pass1", "password2": "Pass2"})
})
}

View File

@ -7,7 +7,7 @@
package ghttp
import (
"github.com/gogf/gf/g/internal/structtag"
"github.com/gogf/gf/g/internal/structs"
"github.com/gogf/gf/g/util/gconv"
)
@ -144,7 +144,7 @@ func (r *Request) GetPostMap(def ...map[string]string) map[string]string {
// 将所有的request参数映射到struct属性上参数object应当为一个struct对象的指针, mapping为非必需参数自定义参数与属性的映射关系
func (r *Request) GetPostToStruct(pointer interface{}, mapping ...map[string]string) error {
tagMap := structtag.Map(pointer, paramTagPriority)
tagMap := structs.TagMapName(pointer, paramTagPriority, true)
if len(mapping) > 0 {
for k, v := range mapping[0] {
tagMap[k] = v

View File

@ -9,8 +9,7 @@ package ghttp
import (
"strings"
"github.com/gogf/gf/g/internal/structtag"
"github.com/gogf/gf/g/internal/structs"
"github.com/gogf/gf/g/util/gconv"
)
@ -154,7 +153,7 @@ func (r *Request) GetQueryMap(def ...map[string]string) map[string]string {
// 将所有的get参数映射到struct属性上参数object应当为一个struct对象的指针, mapping为非必需参数自定义参数与属性的映射关系
func (r *Request) GetQueryToStruct(pointer interface{}, mapping ...map[string]string) error {
tagmap := structtag.Map(pointer, paramTagPriority)
tagmap := structs.TagMapName(pointer, paramTagPriority, true)
if len(mapping) > 0 {
for k, v := range mapping[0] {
tagmap[k] = v

View File

@ -8,7 +8,7 @@ package ghttp
import (
"github.com/gogf/gf/g/container/gvar"
"github.com/gogf/gf/g/internal/structtag"
"github.com/gogf/gf/g/internal/structs"
"github.com/gogf/gf/g/util/gconv"
)
@ -134,7 +134,7 @@ func (r *Request) GetRequestMap(def ...map[string]string) map[string]string {
// 将所有的request参数映射到struct属性上参数object应当为一个struct对象的指针, mapping为非必需参数自定义参数与属性的映射关系
func (r *Request) GetRequestToStruct(pointer interface{}, mapping ...map[string]string) error {
tagMap := structtag.Map(pointer, paramTagPriority)
tagMap := structs.TagMapName(pointer, paramTagPriority, true)
if len(mapping) > 0 {
for k, v := range mapping[0] {
tagMap[k] = v

View File

@ -9,10 +9,11 @@ package gconv
import (
"encoding/json"
"github.com/gogf/gf/g/encoding/gbinary"
"reflect"
"strconv"
"strings"
"github.com/gogf/gf/g/encoding/gbinary"
)
// Type assert api for String().
@ -25,6 +26,10 @@ type apiError interface {
Error() string
}
const (
gGCONV_TAG = "gconv"
)
var (
// Empty strings.
emptyStringMap = map[string]struct{}{
@ -33,6 +38,9 @@ var (
"off": struct{}{},
"false": struct{}{},
}
// Priority tags for Map*/Struct* functions.
structTagPriority = []string{gGCONV_TAG, "json"}
)
// Convert converts the variable <i> to the type <t>, the type <t> is specified by string.

View File

@ -7,20 +7,19 @@
package gconv
import (
"github.com/gogf/gf/g/internal/empty"
"github.com/gogf/gf/g/text/gstr"
"reflect"
"strings"
)
const (
gGCONV_TAG = "gconv"
"github.com/gogf/gf/g/internal/empty"
"github.com/gogf/gf/g/text/gstr"
)
// Map converts any variable <value> to map[string]interface{}.
// If the parameter <value> is not a map type, then the conversion will fail and returns nil.
// If <value> is a struct object, the second parameter <tags> specifies the most priority
// tags that will be detected, otherwise it detects the tags in order of: gconv, json.
//
// If the parameter <value> is not a map/struct/*struct type, then the conversion will fail and returns nil.
//
// If <value> is a struct/*struct object, the second parameter <tags> specifies the most priority
// tags that will be detected, otherwise it detects the tags in order of: gconv, json, and then the field name.
func Map(value interface{}, tags ...string) map[string]interface{} {
if value == nil {
return nil
@ -105,7 +104,7 @@ func Map(value interface{}, tags ...string) map[string]interface{} {
case reflect.Struct:
rt := rv.Type()
name := ""
tagArray := []string{gGCONV_TAG, "json"}
tagArray := structTagPriority
switch len(tags) {
case 0:
// No need handle.
@ -114,9 +113,6 @@ func Map(value interface{}, tags ...string) map[string]interface{} {
default:
tagArray = tags
}
if gstr.SearchArray(tagArray, gGCONV_TAG) < 0 {
tagArray = append(tagArray, gGCONV_TAG)
}
for i := 0; i < rv.NumField(); i++ {
// Only convert the public attributes.
fieldName := rt.Field(i).Name

View File

@ -12,14 +12,10 @@ import (
"reflect"
"strings"
"github.com/gogf/gf/g/internal/structtag"
"github.com/gogf/gf/g/internal/structs"
"github.com/gogf/gf/g/text/gstr"
)
var (
structTagPriority = []string{"gconv", "json"}
)
// Struct maps the params key-value pairs to the corresponding struct object's properties.
// The third parameter <mapping> is unnecessary, indicating the mapping rules between the custom key name
// and the attribute name(case sensitive).
@ -72,7 +68,7 @@ func Struct(params interface{}, pointer interface{}, mapping ...map[string]strin
}
}
// It secondly checks the tags of attributes.
tagMap := structtag.Map(pointer, structTagPriority)
tagMap := structs.TagMapName(pointer, structTagPriority, true)
for tagK, tagV := range tagMap {
if _, ok := doneMap[tagV]; ok {
continue

View File

@ -9,15 +9,19 @@ package gvalid
import (
"strings"
"github.com/gogf/gf/g/text/gstr"
"github.com/gogf/gf/g/internal/structs"
"github.com/gogf/gf/g/util/gconv"
"github.com/gogf/gf/third/github.com/fatih/structs"
)
var (
// 同时支持valid和gvalid标签优先使用valid
structTagPriority = []string{"valid", "gvalid"}
)
// 校验struct对象属性object参数也可以是一个指向对象的指针返回值同CheckMap方法。
// struct的数据校验结果信息是顺序的。
func CheckStruct(object interface{}, rules interface{}, msgs ...CustomMsg) *Error {
fields := structs.Fields(object)
params := make(map[string]interface{})
checkRules := make(map[string]string)
customMsgs := make(CustomMsg)
@ -57,26 +61,25 @@ func CheckStruct(object interface{}, rules interface{}, msgs ...CustomMsg) *Erro
errorRules = append(errorRules, name+"@"+rule)
}
// 不支持校验错误顺序: map[键名]校验规则
// 不支持校验错误顺序: map[键名]校验规则
case map[string]string:
checkRules = v
}
// 首先, 按照属性循环一遍将struct的属性、数值、tag解析
for _, field := range fields {
tagValue := ""
for _, field := range structs.MapField(object, structTagPriority, true) {
fieldName := field.Name()
// 只检测公开属性
if !gstr.IsLetterUpper(fieldName[0]) {
continue
}
params[fieldName] = field.Value()
// 同时支持valid和gvalid标签优先使用valid
tag := field.Tag("valid")
if tag == "" {
tag = field.Tag("gvalid")
tagValue = ""
for _, v := range structTagPriority {
tagValue = field.Tag(v)
if tagValue != "" {
break
}
}
if tag != "" {
if tagValue != "" {
// sequence tag == struct tag, 这里的name为别名
name, rule, msg := parseSequenceTag(tag)
name, rule, msg := parseSequenceTag(tagValue)
if len(name) == 0 {
name = fieldName
}

View File

@ -4,18 +4,16 @@
// If a copy of the MIT was not distributed with this file,
// You can obtain one at https://github.com/gogf/gf.
// 返回错误对象。
package gvalid
import "strings"
// 校验错误对象
type Error struct {
rules []string // 校验结果顺序(可能为nil)
errors ErrorMap // 校验结果(map无序)
firstKey string // 第一条错误项键名(常用操作冗余数据)
firstItem map[string]string // 第一条错误项(常用操作冗余数据)
rules []string // 校验结果顺序(可能为nil),可保证返回校验错误的顺序性
errors ErrorMap // 完整的数据校验结果存储(map无序)
firstKey string // 第一条错误项键名(常用操作冗余数据),默认为空
firstItem map[string]string // 第一条错误项(常用操作冗余数据)默认为nil
}
// 校验错误信息: map[键名]map[规则名]错误信息

View File

@ -9,6 +9,8 @@ package gvalid_test
import (
"testing"
"github.com/gogf/gf/g"
"github.com/gogf/gf/g/test/gtest"
"github.com/gogf/gf/g/util/gvalid"
)
@ -90,3 +92,29 @@ func Test_CheckStruct(t *testing.T) {
gtest.Assert(err.Maps()["uid"]["min"], "ID不能为空")
})
}
func Test_CheckStruct_With_Inherit(t *testing.T) {
gtest.Case(t, func() {
type Pass struct {
Pass1 string `valid:"password1@required|same:password2#请输入您的密码|您两次输入的密码不一致"`
Pass2 string `valid:"password2@required|same:password1#请再次输入您的密码|您两次输入的密码不一致"`
}
type User struct {
Id int
Name string `valid:"name@required#请输入您的姓名"`
Pass Pass
}
user := &User{
Name: "",
Pass: Pass{
Pass1: "1",
Pass2: "2",
},
}
err := gvalid.CheckStruct(user, nil)
gtest.AssertNE(err, nil)
gtest.Assert(err.Maps()["name"], g.Map{"required": "请输入您的姓名"})
gtest.Assert(err.Maps()["password1"], g.Map{"same": "您两次输入的密码不一致"})
gtest.Assert(err.Maps()["password2"], g.Map{"same": "您两次输入的密码不一致"})
})
}