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"` // // Field int `json:",string,formatter=2006-01-02"` // // 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 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 // valid field name if ss[0] != "-" { mp["name"] = ss[0] } // other settings: omitempty, string for _, tt := range ss[1:] { if tt == "" { continue } // kv if !strings.ContainsRune(tt, '=') { mp[tt] = "true" continue } key, val := nstr.TrimCut(tt, "=") mp[key] = val } 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 }