You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
274 lines
5.9 KiB
Go
274 lines
5.9 KiB
Go
1 year ago
|
package nstruct
|
||
|
|
||
|
import (
|
||
|
"errors"
|
||
|
"fmt"
|
||
|
"git.noahlan.cn/noahlan/ntool/narr"
|
||
|
"git.noahlan.cn/noahlan/ntool/nmap"
|
||
|
"git.noahlan.cn/noahlan/ntool/nstr"
|
||
|
"reflect"
|
||
|
"strings"
|
||
|
)
|
||
|
|
||
|
// ErrNotAnStruct error
|
||
|
// var emptyStringMap = make(nmap.SMap)
|
||
|
var ErrNotAnStruct = errors.New("must input an struct value")
|
||
|
|
||
|
// ParseTags for parse struct tags.
|
||
|
func ParseTags(st any, tagNames []string) (map[string]nmap.SMap, error) {
|
||
|
p := NewTagParser(tagNames...)
|
||
|
|
||
|
if err := p.Parse(st); err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
return p.Tags(), nil
|
||
|
}
|
||
|
|
||
|
// ParseReflectTags parse struct tags info.
|
||
|
func ParseReflectTags(rt reflect.Type, tagNames []string) (map[string]nmap.SMap, error) {
|
||
|
p := NewTagParser(tagNames...)
|
||
|
|
||
|
if err := p.ParseType(rt); err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
return p.Tags(), nil
|
||
|
}
|
||
|
|
||
|
// TagValFunc handle func
|
||
|
type TagValFunc func(field, tagVal string) (nmap.SMap, error)
|
||
|
|
||
|
// TagParser struct
|
||
|
type TagParser struct {
|
||
|
// TagNames want parsed tag names.
|
||
|
TagNames []string
|
||
|
// ValueFunc tag value parse func.
|
||
|
ValueFunc TagValFunc
|
||
|
|
||
|
// key: field name
|
||
|
// value: tag map {tag-name: value string.}
|
||
|
tags map[string]nmap.SMap
|
||
|
}
|
||
|
|
||
|
// Tags map data for struct fields
|
||
|
func (p *TagParser) Tags() map[string]nmap.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 any) error {
|
||
|
rv := reflect.ValueOf(st)
|
||
|
if rv.Kind() == reflect.Ptr && !rv.IsNil() {
|
||
|
rv = rv.Elem()
|
||
|
}
|
||
|
|
||
|
return p.ParseType(rv.Type())
|
||
|
}
|
||
|
|
||
|
// ParseType parse a struct type value
|
||
|
func (p *TagParser) ParseType(rt reflect.Type) error {
|
||
|
if rt.Kind() != reflect.Struct {
|
||
|
return ErrNotAnStruct
|
||
|
}
|
||
|
|
||
|
// key is field name.
|
||
|
p.tags = make(map[string]nmap.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)
|
||
|
|
||
|
// skip don't exported field
|
||
|
name := sf.Name
|
||
|
if name[0] >= 'a' && name[0] <= 'z' {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
smp := make(nmap.SMap)
|
||
|
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 == "" {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
smp[tagName] = tagVal
|
||
|
}
|
||
|
|
||
|
pathKey := name
|
||
|
if parent != "" {
|
||
|
pathKey = parent + "." + name
|
||
|
}
|
||
|
|
||
|
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
|
||
|
}
|
||
|
|
||
|
// Info parse the give field, returns tag value info.
|
||
|
//
|
||
|
// info, err := p.Info("Name", "json")
|
||
|
// exportField := info.Get("name")
|
||
|
func (p *TagParser) Info(field, tag string) (nmap.SMap, error) {
|
||
|
field = nstr.UpperFirst(field)
|
||
|
fTags, ok := p.tags[field]
|
||
|
if !ok {
|
||
|
return nil, fmt.Errorf("field %q not found", field)
|
||
|
}
|
||
|
|
||
|
val, ok := fTags.Value(tag)
|
||
|
if !ok {
|
||
|
return make(nmap.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 nmap.SMap, err error) {
|
||
|
ss := nstr.SplitTrimmed(tagVal, ",")
|
||
|
ln := len(ss)
|
||
|
if ln == 0 || tagVal == "," {
|
||
|
return nmap.SMap{"name": field}, nil
|
||
|
}
|
||
|
|
||
|
mp = make(nmap.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
|
||
|
}
|
||
|
|
||
|
// ParseTagValueQuick quick parse tag value string by sep(;)
|
||
|
func ParseTagValueQuick(tagVal string, defines []string) nmap.SMap {
|
||
|
parseFn := ParseTagValueDefine(";", defines)
|
||
|
|
||
|
mp, _ := parseFn("", tagVal)
|
||
|
return mp
|
||
|
}
|
||
|
|
||
|
// ParseTagValueDefine parse tag value string by given defines.
|
||
|
//
|
||
|
// Examples:
|
||
|
//
|
||
|
// eg: "desc;required;default;shorts"
|
||
|
// type MyStruct {
|
||
|
// Age int `flag:"int option message;;a,b"`
|
||
|
// }
|
||
|
// sepStr := ";"
|
||
|
// defines := []string{"desc", "required", "default", "shorts"}
|
||
|
func ParseTagValueDefine(sep string, defines []string) TagValFunc {
|
||
|
defNum := len(defines)
|
||
|
|
||
|
return func(field, tagVal string) (nmap.SMap, error) {
|
||
|
ss := nstr.SplitNTrimmed(tagVal, sep, defNum)
|
||
|
ln := len(ss)
|
||
|
mp := make(nmap.SMap, ln)
|
||
|
if ln == 0 {
|
||
|
return mp, nil
|
||
|
}
|
||
|
|
||
|
for i, val := range ss {
|
||
|
key := defines[i]
|
||
|
mp[key] = val
|
||
|
}
|
||
|
return mp, nil
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// ParseTagValueNamed parse k-v tag value string. it's like INI format contents.
|
||
|
//
|
||
|
// Examples:
|
||
|
//
|
||
|
// eg: "name=val0;shorts=i;required=true;desc=a message"
|
||
|
// =>
|
||
|
// {name: val0, shorts: i, required: true, desc: a message}
|
||
|
func ParseTagValueNamed(field, tagVal string, keys ...string) (mp nmap.SMap, err error) {
|
||
|
ss := nstr.Split(tagVal, ";")
|
||
|
ln := len(ss)
|
||
|
if ln == 0 {
|
||
|
return
|
||
|
}
|
||
|
|
||
|
mp = make(nmap.SMap, ln)
|
||
|
for _, s := range ss {
|
||
|
if !strings.ContainsRune(s, '=') {
|
||
|
err = fmt.Errorf("parse tag error on field '%s': must match `KEY=VAL`", field)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
key, val := nstr.TrimCut(s, "=")
|
||
|
if len(keys) > 0 && !narr.StringsHas(keys, key) {
|
||
|
err = fmt.Errorf("parse tag error on field '%s': invalid key name '%s'", field, key)
|
||
|
return
|
||
|
}
|
||
|
|
||
|
mp[key] = val
|
||
|
}
|
||
|
return
|
||
|
}
|