From 01745620c7dca384bd4a8f4ad913c6db542fa8db Mon Sep 17 00:00:00 2001 From: David Sharnoff Date: Sat, 6 Nov 2021 22:19:09 -0700 Subject: [PATCH 1/3] add TagSet and Tag.Fill and an example --- example_parsetag_test.go | 37 ++++++++++ parsetag.go | 152 +++++++++++++++++++++++++++++++++++++-- 2 files changed, 183 insertions(+), 6 deletions(-) create mode 100644 example_parsetag_test.go diff --git a/example_parsetag_test.go b/example_parsetag_test.go new file mode 100644 index 0000000..f590b8b --- /dev/null +++ b/example_parsetag_test.go @@ -0,0 +1,37 @@ +package reflectutils_test + +import ( + "fmt" + "reflect" + + "github.com/muir/reflectutils" +) + +type ExampleStruct struct { + String string `foo:"something,N=9,square,!jump"` + Bar string `foo:"different,!square,jump"` +} + +type TagExtractorType struct { + Name string `pt:"0"` + Square bool `pt:"square"` + Jump bool `pt:"jump"` + N int +} + +// Fill is a helper for when you're working with tags. +func ExampleTag_Fill() { + var es ExampleStruct + t := reflect.TypeOf(es) + reflectutils.WalkStructElements(t, func(f reflect.StructField) bool { + var tet TagExtractorType + err := reflectutils.SplitTag(f.Tag).Set().Get("foo").Fill(&tet) + if err != nil { + fmt.Println(err) + } + fmt.Printf("%s: %+v\n", f.Name, tet) + return true + }) + // Output: String: {Name:something Square:true Jump:false N:9} + // Bar: {Name:different Square:false Jump:true N:0} +} diff --git a/parsetag.go b/parsetag.go index 7846177..9c7a89c 100644 --- a/parsetag.go +++ b/parsetag.go @@ -3,17 +3,41 @@ package reflectutils import ( "reflect" "regexp" + "strconv" + "strings" + + "github.com/pkg/errors" ) var aTagRE = regexp.MustCompile(`(\S+):"((?:[^"\\]|\\.)*)"(?:\s+|$)`) +// Tag represents a single element of a struct tag. For example for the +// field S in the struct below, there would be to Tags: one for json and +// one for xml. The value for the json one would be "s,omitempty". +// +// type Foo struct { +// S string `json:"s,omitempty" xml:"s_thing"` +// } +// type Tag struct { Tag string Value string } -// SplitTag breaks apart a reflect.StructTag into an array of key/value pairs. -func SplitTag(tag reflect.StructTag) []Tag { +type Tags []Tag + +// TagSet is a simple transformation of an array of Tag into an +// indexted structure so that lookup is efficient. +type TagSet struct { + tags []Tag + index map[string]int +} + +// SplitTag breaks apart a reflect.StructTag into an array of annotated key/value pairs. +// Tags are expected to be in the conventional format. What does "contentional" +// mean? `name:"values,value=value" name2:"value"`. See https://flaviocopes.com/go-tags/ +// a light introduction. +func SplitTag(tag reflect.StructTag) Tags { found := make([]Tag, 0, 5) s := string(tag) for len(s) > 0 { @@ -23,10 +47,7 @@ func SplitTag(tag reflect.StructTag) []Tag { } tag := s[f[2]:f[3]] value := s[f[4]:f[5]] - found = append(found, Tag{ - Tag: tag, - Value: value, - }) + found = append(found, mkTag(tag, value)) s = s[f[1]:] } if len(found) == 0 { @@ -34,3 +55,122 @@ func SplitTag(tag reflect.StructTag) []Tag { } return found } + +func mkTag(tag, value string) Tag { + return Tag{ + Tag: tag, + Value: value, + } +} + +func (s TagSet) Get(tag string) Tag { + t, _ := s.Lookup(tag) + return t +} + +func (s TagSet) Lookup(tag string) (Tag, bool) { + if i, ok := s.index[tag]; ok { + return s.tags[i], true + } + return mkTag("", ""), false +} + +func (t Tags) Set() TagSet { + index := make(map[string]int) + for i, tag := range t { + index[tag.Tag] = i + } + return TagSet{ + tags: t, + index: index, + } +} + +// Fill unpacks struct tags into a struct based on tags of the desitnation struct. +// +// type MyTags struct { +// Name string `pt:"0"` +// Flag bool `pt:"flag"` +// Int int `pt:"intValue"` +// } +// +// The above will fill the Name field by grabbing the first element of the tag. +// It will fill Flag by noticing if any of the following are present in the +// comma-separated list of tag elements: "flag", "!flag" (sets to false), "flag=true", +// "flag=false", "flag=0", "flag=1", "flag=t", "flag=f", "flag=T", "flag=F". +// It will set Int by looking for "intValue=280" (set to 280). +func (tag Tag) Fill(model interface{}, opts ...FillOptArg) error { + opt := fillOpt{ + tag: "pt", + } + for _, f := range opts { + f(&opt) + } + v := reflect.ValueOf(model) + if !v.IsValid() || v.IsNil() || v.Type().Kind() != reflect.Ptr || v.Type().Elem().Kind() != reflect.Struct { + return errors.Errorf("Fill target must be a pointer to a struct, not %T", model) + } + kv := make(map[string]string) + elements := strings.Split(tag.Value, ",") + for _, element := range elements { + if eq := strings.IndexByte(element, '='); eq != -1 { + kv[element[0:eq]] = element[eq+1:] + } else { + if strings.HasPrefix(element, "!") { + kv[element[1:]] = "f" + } else { + kv[element] = "t" + } + } + } + var count int + var walkErr error + WalkStructElements(v.Type(), func(f reflect.StructField) bool { + tag := f.Tag.Get(opt.tag) + name := f.Name + if tag == "-" { + return false + } + count++ + i, err := strconv.Atoi(tag) + var value string + if err == nil { + // positional! + if i >= len(elements) { + return true + } + value = elements[i] + } else { + if tag != "" { + name = tag + } + value = kv[name] + } + if value == "" { + return true + } + set, err := MakeStringSetter(f.Type) + if err != nil { + walkErr = errors.Wrapf(err, "Cannot set %s", f.Type) + return true + } + err = set(v.Elem().FieldByIndex(f.Index), value) + if err != nil { + walkErr = errors.Wrap(err, f.Name) + } + return true + }) + return walkErr +} + +type FillOptArg func(*fillOpt) + +type fillOpt struct { + tag string +} + +func WithTag(tag string) FillOptArg { + return func(o *fillOpt) { + o.tag = tag + } +} From 49e15c7b26899fc957a886555af006de6231f51e Mon Sep 17 00:00:00 2001 From: David Sharnoff Date: Sat, 6 Nov 2021 22:23:40 -0700 Subject: [PATCH 2/3] also demonstrage ignoring tags --- example_parsetag_test.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/example_parsetag_test.go b/example_parsetag_test.go index f590b8b..b597ea4 100644 --- a/example_parsetag_test.go +++ b/example_parsetag_test.go @@ -9,13 +9,14 @@ import ( type ExampleStruct struct { String string `foo:"something,N=9,square,!jump"` - Bar string `foo:"different,!square,jump"` + Bar string `foo:"different,!square,jump,ignore=xyz"` } type TagExtractorType struct { Name string `pt:"0"` Square bool `pt:"square"` Jump bool `pt:"jump"` + Ignore string `pt:"-"` N int } @@ -32,6 +33,6 @@ func ExampleTag_Fill() { fmt.Printf("%s: %+v\n", f.Name, tet) return true }) - // Output: String: {Name:something Square:true Jump:false N:9} - // Bar: {Name:different Square:false Jump:true N:0} + // Output: String: {Name:something Square:true Jump:false Ignore: N:9} + // Bar: {Name:different Square:false Jump:true Ignore: N:0} } From f9d5468f2f120b3e548884b38a36912db1a4475d Mon Sep 17 00:00:00 2001 From: David Sharnoff Date: Sat, 6 Nov 2021 22:30:52 -0700 Subject: [PATCH 3/3] add godoc --- parsetag.go | 1 + 1 file changed, 1 insertion(+) diff --git a/parsetag.go b/parsetag.go index 9c7a89c..f4006fc 100644 --- a/parsetag.go +++ b/parsetag.go @@ -169,6 +169,7 @@ type fillOpt struct { tag string } +// WithTag overrides the tag used by Tag.Fill. The default is "pt". func WithTag(tag string) FillOptArg { return func(o *fillOpt) { o.tag = tag