From b29c6add4757ce20bc2a67f6178cf835f8d2f5d7 Mon Sep 17 00:00:00 2001 From: John Date: Thu, 4 Jul 2019 11:11:41 +0800 Subject: [PATCH] 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 --- g/internal/structs/structs.go | 15 ++++ .../structtag.go => structs/structs_map.go} | 40 ++++----- g/internal/structs/structs_tag.go | 81 +++++++++++++++++++ .../structs_test.go} | 26 +++--- g/net/ghttp/ghttp_request_post.go | 4 +- g/net/ghttp/ghttp_request_query.go | 5 +- g/net/ghttp/ghttp_request_request.go | 4 +- g/util/gconv/gconv.go | 10 ++- g/util/gconv/gconv_map.go | 20 ++--- g/util/gconv/gconv_struct.go | 8 +- g/util/gvalid/gvalid_check_struct.go | 33 ++++---- g/util/gvalid/gvalid_error.go | 10 +-- g/util/gvalid/gvalid_unit_checkstruct_test.go | 28 +++++++ 13 files changed, 207 insertions(+), 77 deletions(-) create mode 100644 g/internal/structs/structs.go rename g/internal/{structtag/structtag.go => structs/structs_map.go} (61%) create mode 100644 g/internal/structs/structs_tag.go rename g/internal/{structtag/structtag_test.go => structs/structs_test.go} (52%) diff --git a/g/internal/structs/structs.go b/g/internal/structs/structs.go new file mode 100644 index 000000000..b3c2afdf8 --- /dev/null +++ b/g/internal/structs/structs.go @@ -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 diff --git a/g/internal/structtag/structtag.go b/g/internal/structs/structs_map.go similarity index 61% rename from g/internal/structtag/structtag.go rename to g/internal/structs/structs_map.go index 363fd414d..fd720051a 100644 --- a/g/internal/structtag/structtag.go +++ b/g/internal/structs/structs_map.go @@ -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 , 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 , and returns it. +// +// The parameter 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 } diff --git a/g/internal/structs/structs_tag.go b/g/internal/structs/structs_tag.go new file mode 100644 index 000000000..9040e9560 --- /dev/null +++ b/g/internal/structs/structs_tag.go @@ -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 , and returns it. +// +// The parameter 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 , and returns it. +// +// The parameter 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 +} diff --git a/g/internal/structtag/structtag_test.go b/g/internal/structs/structs_test.go similarity index 52% rename from g/internal/structtag/structtag_test.go rename to g/internal/structs/structs_test.go index 4cc348fc4..54d63be74 100644 --- a/g/internal/structtag/structtag_test.go +++ b/g/internal/structs/structs_test.go @@ -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"}) }) } diff --git a/g/net/ghttp/ghttp_request_post.go b/g/net/ghttp/ghttp_request_post.go index 9d21eacb5..6a594fd1b 100644 --- a/g/net/ghttp/ghttp_request_post.go +++ b/g/net/ghttp/ghttp_request_post.go @@ -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 diff --git a/g/net/ghttp/ghttp_request_query.go b/g/net/ghttp/ghttp_request_query.go index 5301d9d31..5b0eed70e 100644 --- a/g/net/ghttp/ghttp_request_query.go +++ b/g/net/ghttp/ghttp_request_query.go @@ -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 diff --git a/g/net/ghttp/ghttp_request_request.go b/g/net/ghttp/ghttp_request_request.go index c77e24b19..e8386e2ad 100644 --- a/g/net/ghttp/ghttp_request_request.go +++ b/g/net/ghttp/ghttp_request_request.go @@ -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 diff --git a/g/util/gconv/gconv.go b/g/util/gconv/gconv.go index 9e7dfb535..15007083e 100644 --- a/g/util/gconv/gconv.go +++ b/g/util/gconv/gconv.go @@ -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 to the type , the type is specified by string. diff --git a/g/util/gconv/gconv_map.go b/g/util/gconv/gconv_map.go index 817d6a691..f2146350d 100644 --- a/g/util/gconv/gconv_map.go +++ b/g/util/gconv/gconv_map.go @@ -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 to map[string]interface{}. -// If the parameter is not a map type, then the conversion will fail and returns nil. -// If is a struct object, the second parameter specifies the most priority -// tags that will be detected, otherwise it detects the tags in order of: gconv, json. +// +// If the parameter is not a map/struct/*struct type, then the conversion will fail and returns nil. +// +// If is a struct/*struct object, the second parameter 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 diff --git a/g/util/gconv/gconv_struct.go b/g/util/gconv/gconv_struct.go index 8d35abc33..a4df30132 100644 --- a/g/util/gconv/gconv_struct.go +++ b/g/util/gconv/gconv_struct.go @@ -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 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 diff --git a/g/util/gvalid/gvalid_check_struct.go b/g/util/gvalid/gvalid_check_struct.go index cc025d722..fba014f95 100644 --- a/g/util/gvalid/gvalid_check_struct.go +++ b/g/util/gvalid/gvalid_check_struct.go @@ -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 } diff --git a/g/util/gvalid/gvalid_error.go b/g/util/gvalid/gvalid_error.go index d25c2d4bb..da7bd34d9 100644 --- a/g/util/gvalid/gvalid_error.go +++ b/g/util/gvalid/gvalid_error.go @@ -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[规则名]错误信息 diff --git a/g/util/gvalid/gvalid_unit_checkstruct_test.go b/g/util/gvalid/gvalid_unit_checkstruct_test.go index 40e3178f0..d5461a62f 100644 --- a/g/util/gvalid/gvalid_unit_checkstruct_test.go +++ b/g/util/gvalid/gvalid_unit_checkstruct_test.go @@ -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": "您两次输入的密码不一致"}) + }) +}