Skip to content

Commit

Permalink
up: structs - update the struct tags collect and parse logic, support…
Browse files Browse the repository at this point in the history
… handle sub-struct
  • Loading branch information
inhere committed Aug 27, 2022
1 parent a83c4b2 commit 10265c9
Show file tree
Hide file tree
Showing 2 changed files with 281 additions and 34 deletions.
224 changes: 192 additions & 32 deletions structs/tags.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,38 +6,88 @@ import (
"reflect"
"strings"

"github.com/gookit/goutil/errorx"
"github.com/gookit/goutil/maputil"
"github.com/gookit/goutil/strutil"
)

var errNotAnStruct = errors.New("must input an struct")
// var emptyStringMap = make(maputil.SMap)
var errNotAnStruct = errors.New("must input an struct value")

// ParseTags for parse struct tags.
func ParseTags(st interface{}, tagNames []string) (map[string]maputil.SMap, error) {
p := NewTagParser(tagNames...)

err := p.Parse(st)
if err != nil {
return nil, err
}

return p.Tags(), nil
}

// ParseReflectTags parse struct tags info.
func ParseReflectTags(rt reflect.Type, tagNames []string) (map[string]maputil.SMap, error) {
p := NewTagParser(tagNames...)

err := p.ParseType(rt)
if err != nil {
return nil, err
}

return p.Tags(), nil
}

// TagValFunc handle func
type TagValFunc func(field, tagVal string) (maputil.SMap, error)

// TagParser struct
type TagParser struct {
TagNames string
// TagNames want parsed tag names.
TagNames []string
// ValueFunc tag value parse func.
ValueFunc TagValFunc

Func func(tagVal string) map[string]string
// key: field name
// value: tag map {tag-name: value string.}
tags map[string]maputil.SMap
}

// ParseTags for parse struct tags.
func ParseTags(v interface{}, tagNames []string) (map[string]maputil.SMap, error) {
rv := reflect.ValueOf(v)
// Tags map data for struct fields
func (p *TagParser) Tags() map[string]maputil.SMap {
return p.tags
}

// NewTagParser instance
func NewTagParser(tagNames ...string) *TagParser {
return &TagParser{
TagNames: tagNames,
ValueFunc: ParseTagValueDefault,
}
}

// Parse an struct value
func (p *TagParser) Parse(st interface{}) error {
rv := reflect.ValueOf(st)
if rv.Kind() == reflect.Ptr && !rv.IsNil() {
rv = rv.Elem()
}

return ParseReflectTags(rv.Type(), tagNames)
return p.ParseType(rv.Type())
}

// ParseReflectTags parse struct tags info.
func ParseReflectTags(rt reflect.Type, tagNames []string) (map[string]maputil.SMap, error) {
// ParseType parse a struct type value
func (p *TagParser) ParseType(rt reflect.Type) error {
if rt.Kind() != reflect.Struct {
return nil, errNotAnStruct
return errNotAnStruct
}

// key is field name.
result := make(map[string]maputil.SMap)
p.tags = make(map[string]maputil.SMap)
return p.parseType(rt, "")
}

func (p *TagParser) parseType(rt reflect.Type, parent string) error {
for i := 0; i < rt.NumField(); i++ {
sf := rt.Field(i)

Expand All @@ -48,7 +98,8 @@ func ParseReflectTags(rt reflect.Type, tagNames []string) (map[string]maputil.SM
}

smp := make(maputil.SMap)
for _, tagName := range tagNames {
for _, tagName := range p.TagNames {
// eg: `json:"age"`
// eg: "name=int0;shorts=i;required=true;desc=int option message"
tagVal := sf.Tag.Get(tagName)
if tagVal == "" {
Expand All @@ -58,42 +109,151 @@ func ParseReflectTags(rt reflect.Type, tagNames []string) (map[string]maputil.SM
smp[tagName] = tagVal
}

result[name] = smp
pathKey := name
if parent != "" {
pathKey = parent + "." + name
}

if len(smp) > 0 {
p.tags[pathKey] = smp
}

ft := sf.Type
if ft.Kind() == reflect.Ptr {
ft = ft.Elem()
}

// field is struct.
if ft.Kind() == reflect.Struct {
err := p.parseType(ft, pathKey)
if err != nil {
return err
}
}
}
return nil
}

// TODO field is struct.
// fv := v.Field(i)
// ft := t.Field(i).Type
// if ft.Kind() == reflect.Ptr {
// // isPtr = true
// ft = ft.Elem()
// // fv = fv.Elem()
// }
// Info parse the give field, returns tag value info.
//
// info, err := p.Info("Name", "json")
func (p *TagParser) Info(field, tag string) (maputil.SMap, error) {
field = strutil.UpperFirst(field)
fTags, ok := p.tags[field]
if !ok {
return nil, errorx.Rawf("field %q not found", field)
}

val, ok := fTags.Value(tag)
if !ok {
return make(maputil.SMap), nil
}

// parse tag value
return p.ValueFunc(field, val)
}

/*************************************************************
* some built in tag value parse func
*************************************************************/

// ParseTagValueDefault parse like json tag value.
//
// see json.Marshal():
//
// // JSON as key "myName", skipped if empty.
// Field int `json:"myName,omitempty"`
//
// // Field appears in JSON as key "Field" (the default), but skipped if empty.
// Field int `json:",omitempty"`
//
// // Field is ignored by this package.
// Field int `json:"-"`
//
// // Field appears in JSON as key "-".
// Field int `json:"-,"`
//
// Int64String int64 `json:",string"`
//
// Returns:
//
// {
// "name": "myName", // maybe is empty, on tag value is "-"
// "omitempty": "true",
// "string": "true",
// // ... more custom bool settings.
// }
func ParseTagValueDefault(field, tagVal string) (mp maputil.SMap, err error) {
ss := strutil.SplitTrimmed(tagVal, ",")
ln := len(ss)
if ln == 0 {
return maputil.SMap{"name": field}, nil
}

mp = make(maputil.SMap, ln)
if ln == 1 {
// valid field name
if ss[0] != "-" {
mp["name"] = ss[0]
}
return
}

// ln > 1
mp["name"] = ss[0]
// other settings: omitempty, string
for _, key := range ss[1:] {
mp[key] = "true"
}
return
}

// ParseTagValueDefine parse tag value string by given defines.
//
// Examples:
//
// eg: "desc;required;default;shorts"
// type My
// sepStr := ";"
// defines := []string{"desc", "required", "default", "shorts"}
func ParseTagValueDefine(sep string, defines []string) TagValFunc {
defNum := len(defines)

return func(field, tagVal string) (maputil.SMap, error) {
ss := strutil.SplitNTrimmed(tagVal, sep, defNum)
ln := len(ss)
mp := make(maputil.SMap, ln)
if ln == 0 {
return mp, nil
}

for i, val := range ss {
key := defines[i]
mp[key] = val
}
return mp, nil
}
return result, nil
}

// ParseTagValueINI tag value string. is like INI format data
// ParseTagValueNamed parse k-v tag value string. it's like INI format contents.
//
// eg: "name=int0;shorts=i;required=true;desc=int option message"
func ParseTagValueINI(field, tagStr string) (mp maputil.SMap, err error) {
tagStr = strings.Trim(tagStr, "; ")
ss := strutil.Split(tagStr, ";")
if len(ss) == 0 {
func ParseTagValueNamed(field, tagVal string) (mp maputil.SMap, err error) {
ss := strutil.Split(tagVal, ";")
ln := len(ss)
if ln == 0 {
return
}

mp = make(maputil.SMap, len(ss))
mp = make(maputil.SMap, ln)
for _, s := range ss {
if !strings.ContainsRune(s, '=') {
err = fmt.Errorf("parse tag error on field '%s': item must match `KEY=VAL`", field)
err = fmt.Errorf("parse tag error on field '%s': must match `KEY=VAL`", field)
return
}

kvNodes := strings.SplitN(s, "=", 2)
key, val := kvNodes[0], strings.TrimSpace(kvNodes[1])
// if !flagTagKeys.Has(key) {
// panicf("parse tag error on field '%s': invalid key name '%s'", name, key)
// }

mp[key] = val
}
Expand Down
91 changes: 89 additions & 2 deletions structs/tags_test.go
Original file line number Diff line number Diff line change
@@ -1,18 +1,105 @@
package structs_test

import (
"fmt"
"testing"

"github.com/gookit/goutil"
"github.com/gookit/goutil/dump"
"github.com/gookit/goutil/structs"
"github.com/gookit/goutil/testutil/assert"
)

func ExampleTagParser_Parse() {
type User struct {
Age int `json:"age" yaml:"age" default:"23"`
Name string `json:"name,omitempty" yaml:"name" default:"inhere"`
inner string
}

u := &User{}
p := structs.NewTagParser("json", "yaml", "default")
goutil.MustOK(p.Parse(u))

tags := p.Tags()
dump.P(tags)
/*tags:
map[string]maputil.SMap { #len=2
"Age": maputil.SMap { #len=3
"json": string("age"), #len=3
"yaml": string("age"), #len=3
"default": string("23"), #len=2
},
"Name": maputil.SMap { #len=3
"default": string("inhere"), #len=6
"json": string("name,omitempty"), #len=14
"yaml": string("name"), #len=4
},
},
*/

dump.P(p.Info("name", "json"))
/*info:
maputil.SMap { #len=2
"name": string("name"), #len=4
"omitempty": string("true"), #len=4
},
*/

fmt.Println(
tags["Age"].Get("json"),
tags["Age"].Get("default"),
)

// Output:
// age 23
}

func ExampleTagParser_Parse_parseTagValueDefine() {
// eg: "desc;required;default;shorts"
type MyCmd struct {
Name string `flag:"set your name;false;INHERE;n"`
}

c := &MyCmd{}
p := structs.NewTagParser("flag")

sepStr := ";"
defines := []string{"desc", "required", "default", "shorts"}
p.ValueFunc = structs.ParseTagValueDefine(sepStr, defines)

goutil.MustOK(p.Parse(c))
// dump.P(p.Tags())
/*
map[string]maputil.SMap { #len=1
"Name": maputil.SMap { #len=1
"flag": string("set your name;false;INHERE;n"), #len=28
},
},
*/
fmt.Println("tags:", p.Tags())

info, _ := p.Info("Name", "flag")
dump.P(info)
/*
maputil.SMap { #len=4
"desc": string("set your name"), #len=13
"required": string("false"), #len=5
"default": string("INHERE"), #len=6
"shorts": string("n"), #len=1
},
*/

// Output:
// tags: map[Name:{flag:set your name;false;INHERE;n}]
}

func TestParseTagValueINI(t *testing.T) {
mp, err := structs.ParseTagValueINI("name", "")
mp, err := structs.ParseTagValueNamed("name", "")
assert.NoErr(t, err)
assert.Empty(t, mp)

mp, err = structs.ParseTagValueINI("name", "default=inhere")
mp, err = structs.ParseTagValueNamed("name", "default=inhere")
assert.NoErr(t, err)
assert.NotEmpty(t, mp)
assert.Eq(t, "inhere", mp.Str("default"))
Expand Down

0 comments on commit 10265c9

Please sign in to comment.