Skip to content

Commit

Permalink
Merge pull request #7 from muir/negative-bools
Browse files Browse the repository at this point in the history
for tag parsing of bools, can now specify antonyms
  • Loading branch information
muir committed Nov 18, 2021
2 parents 0340dca + def8646 commit 9edbedf
Show file tree
Hide file tree
Showing 2 changed files with 101 additions and 21 deletions.
45 changes: 44 additions & 1 deletion parsetag.go
Expand Up @@ -88,6 +88,9 @@ func (t Tags) Set() TagSet {
}

// Fill unpacks struct tags into a struct based on tags of the desitnation struct.
// This is very meta. It is using struct tags to control parsing of struct tags.
// The tag being parsed is the receiver (tag). The model that controls the parsing
// is the function parameter (model). The parsing may be adjusted based on the opts.
//
// type MyTags struct {
// Name string `pt:"0"`
Expand All @@ -104,6 +107,13 @@ func (t Tags) Set() TagSet {
// When filling an array value, the default character to split upon is
// comma, but other values can be set with "split=X" to split on X.
// Special values of X are "quote", "space", and "none"
//
// For bool values (and *bool, etc) an antonym can be specified:
//
// MyBool bool `pt:"mybool,!other"`
//
// So, then "mybool" maps to true, "!mybool" maps to false,
// "other" maps to false and "!other" maps to true.
func (tag Tag) Fill(model interface{}, opts ...FillOptArg) error {
opt := fillOpt{
tag: "pt",
Expand All @@ -115,6 +125,11 @@ func (tag Tag) Fill(model interface{}, opts ...FillOptArg) error {
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)
}
// Break apart the tag into a list of elements (split on ",") and
// key/values (kv) when the elements have values (split on "="). If
// an element doesn't have a value from =, then it gets a value of
// "t" (true) unless the element name starts with "!" in which case,
// the "!" is discarded and the value is "f" (false)
kv := make(map[string]string)
elements := strings.Split(tag.Value, ",")
for _, element := range elements {
Expand All @@ -128,6 +143,7 @@ func (tag Tag) Fill(model interface{}, opts ...FillOptArg) error {
}
}
}
// Now walk over the input model that controls the parsing.
var count int
var walkErr error
WalkStructElements(v.Type(), func(f reflect.StructField) bool {
Expand All @@ -138,6 +154,7 @@ func (tag Tag) Fill(model interface{}, opts ...FillOptArg) error {
count++
parts := strings.Split(tag, ",")
var value string
isBool := NonPointer(f.Type).Kind() == reflect.Bool
if len(parts) > 0 && parts[0] != "" {
i, err := strconv.Atoi(parts[0])
if err == nil {
Expand All @@ -147,7 +164,26 @@ func (tag Tag) Fill(model interface{}, opts ...FillOptArg) error {
}
value = elements[i]
} else {
value = kv[parts[0]]
if isBool {
for _, p := range parts {
if len(p) > 0 && p[0] == '!' {
if v, ok := kv[p[1:]]; ok {
value = v
switch value {
case "f":
value = "t"
case "t":
value = "f"
}
}
} else if v, ok := kv[p]; ok {
value = v
break
}
}
} else {
value = kv[parts[0]]
}
}
} else {
value = kv[f.Name]
Expand Down Expand Up @@ -196,3 +232,10 @@ func WithTag(tag string) FillOptArg {
o.tag = tag
}
}

func NonPointer(t reflect.Type) reflect.Type {
for t.Kind() == reflect.Ptr {
t = t.Elem()
}
return t
}
77 changes: 57 additions & 20 deletions parsetag_test.go
Expand Up @@ -32,26 +32,63 @@ func ts(t *testing.T, tag reflect.StructTag, want ...string) {
}

func TestFill(t *testing.T) {
type tagData struct {
P0 []string `tf:"0,split=space" json:",omitempty"`
cases := []struct {
tests interface{}
model interface{}
targetTag string
metaTag string
}{
{
tests: struct {
T1 string `xyz:"a b" want:"{\"P0\":[\"a\",\"b\"]}"`
}{},
model: struct {
P0 []string `tf:"0,split=space" json:",omitempty"`
}{},
targetTag: "xyz",
metaTag: "tf",
},
{
tests: struct {
T2 string `xyz:"a,first" want:"{\"Name\":\"a\",\"First\":true}"`
T3 string `xyz:"a,last" want:"{\"Name\":\"a\",\"First\":false}"`
T4 string `xyz:"a,!first" want:"{\"Name\":\"a\",\"First\":false}"`
T5 string `xyz:"a,!last" want:"{\"Name\":\"a\",\"First\":true}"`
}{},
model: struct {
Name string `pf:"0" json:",omitempty"`
First *bool `pf:"first,!last" json:",omitempty"`
}{},
targetTag: "xyz",
metaTag: "pf",
},
}
type testStruct struct {
T1 string `xyz:"a b" want:"{\"P0\":[\"a\",\"b\"]}"`
}
var x testStruct
reflectutils.WalkStructElements(reflect.TypeOf(x), func(f reflect.StructField) bool {
var got tagData
t.Logf("%s: %s", f.Name, f.Tag)
err := reflectutils.SplitTag(f.Tag).Set().Get("xyz").Fill(&got, reflectutils.WithTag("tf"))
if !assert.NoErrorf(t, err, "extract tag %s", f.Name) {
return true
}
var want tagData
err = json.Unmarshal([]byte(f.Tag.Get("want")), &want)
if !assert.NoErrorf(t, err, "extract want %s", f.Name) {

for _, tc := range cases {
tc := tc
reflectutils.WalkStructElements(reflect.TypeOf(tc.tests), func(f reflect.StructField) bool {
if tc.metaTag == "" {
tc.metaTag = "pt"
}
if tc.targetTag == "" {
tc.targetTag = "xyz"
}
t.Run(f.Name+"_"+tc.targetTag+"_"+tc.metaTag,
func(t *testing.T) {
t.Logf("%s: %s", f.Name, f.Tag)
got := reflect.New(reflect.TypeOf(tc.model)).Interface()
err := reflectutils.SplitTag(f.Tag).Set().Get(tc.targetTag).Fill(got, reflectutils.WithTag(tc.metaTag))
if !assert.NoErrorf(t, err, "extract tag %s", f.Name) {
return
}
want := reflect.New(reflect.TypeOf(tc.model)).Interface()
err = json.Unmarshal([]byte(f.Tag.Get("want")), want)
if !assert.NoErrorf(t, err, "extract want %s", f.Name) {
return
}
assert.Equal(t, want, got, f.Name)
})
return true
}
assert.Equal(t, want, got, f.Name)
return true
})
})
}
}

0 comments on commit 9edbedf

Please sign in to comment.