Skip to content

Commit

Permalink
Merge pull request #11 from dumim/develop
Browse files Browse the repository at this point in the history
Omitempty implemented with intelligent default value checks
  • Loading branch information
dumim authored Feb 9, 2022
2 parents dc779c1 + 00b6a39 commit 7b4b7e7
Show file tree
Hide file tree
Showing 3 changed files with 117 additions and 2 deletions.
34 changes: 33 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ whereas using `bar` (`ToMap(obj, "bar")`) on the same `obj` will result in:
- Dot notation will create a parent-child relationship for every `.`.
- Not setting any tag will ignore that field, unless if it's a struct; then it will go inside the struct to check its tags
- `-` will explicitly ignore that field. As opposed to above, it will not look inside even if the field is of struct type.
- `,omitempty` can be used similar to the `json` package to skip fields that have empty values
- this also handles default empty values (`""` for `string`, `0` for `int`, etc.). Note that for `bool` values, `false` wil be omitted if this option is used since this is the default empty value. To work around this, use `*bool` (see example)

For an example that includes all the above scenarios see the code below:

Expand All @@ -139,6 +141,23 @@ type ObjThree struct {
Name string `custom:"name"`
Value int `custom:"value"`
}
type ObjFour struct {
F1 string `custom:"f1,omitempty"`
F2 struct {
F21 string `custom:"f21,omitempty"`
} `custom:"f2"`
F3 *string `custom:"f3, omitempty"` // omitempty with space
F4 int `custom:"f4,omitempty"`
F5 bool `custom:"f5,omitempty"`
F6 interface{} `custom:"f6,omitempty"`
F7 struct {
F71 string `custom:"f71"`
} `custom:"f7,omitempty"`
F8 *bool `custom:"f8,omitempty"` // use pointer to keep false on omitempty
F9 struct {
F91 string `custom:"f91"`
} `custom:"f9,omitempty"`
}
type Example struct {
Name string `custom:"name"`
Email string `custom:"email"`
Expand All @@ -148,10 +167,12 @@ type Example struct {
Id int `custom:"id"`
Call int `custom:"data.call"` // top-level dot notation
ArrayObj []ObjThree `custom:"list"`
Omit ObjFour `custom:omit`
}
```
The `ToMap` function can be used to convert this into a JSON/Map based on the values defined in the given custom tag like so.
```go
f := false
obj := Example{
Name: "2",
Email: "3",
Expand All @@ -170,7 +191,12 @@ obj := Example{
{"hi", 1},
{"world", 2},
},
Omit: ObjFour{
F5: f,
F8: &f,
}
}
obj.Omit.F9.F91 = "123"

// get the map from custom tags
tagName = "custom"
Expand Down Expand Up @@ -212,7 +238,13 @@ This will produce a result similar to:
"name": "world",
"value": 2
}
]
],
"omit": {
"f8": false,
"f9": {
"f91": "123"
}
}
}
```
---
Expand Down
32 changes: 32 additions & 0 deletions map.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import (

var tagName = "" // initialise the struct tag value

const omitEmptyTagOption = "omitempty" // omit values with this tag option if empty

// getMapOfAllKeyValues builds a map of the fully specified key and the value from the struct tag
// the struct tags with the full dot notation will be used as the key, and the value as the value
// slices will be also be maps
Expand Down Expand Up @@ -65,10 +67,17 @@ func getMapOfAllKeyValues(s interface{}) *map[string]interface{} {
continue
}
} else {
// omitempty tag passed?
tag, shouldOmitEmpty := shouldOmitEmpty(tag) // overwrite tag
// recursive check nested fields in case this is a struct
if t.Field(i).Kind() == reflect.Struct {
// only check if the value can be obtained without panicking (eg: for unexported fields)
if t.Field(i).CanInterface() {
if shouldOmitEmpty {
if t.Field(i).IsZero() {
continue
}
}
qVars := getMapOfAllKeyValues(t.Field(i).Interface()) //recursive call
if qVars != nil {
for k, v := range *qVars {
Expand All @@ -79,6 +88,11 @@ func getMapOfAllKeyValues(s interface{}) *map[string]interface{} {
} else {
// only check if the value can be obtained without panicking (eg: for unexported fields)
if t.Field(i).CanInterface() {
if shouldOmitEmpty {
if t.Field(i).IsZero() {
continue
}
}
vars[tag] = t.Field(i).Interface()
}
}
Expand Down Expand Up @@ -119,6 +133,20 @@ func getMapOfAllKeyValues(s interface{}) *map[string]interface{} {
return &finalMap
}

// shouldOmitEmpty checks if the omitEmptyTagOption option is passed in the tag
// eg: `foo:"bar,omitempty"`
func shouldOmitEmpty(originalTag string) (string, bool) {
if ss := strings.Split(originalTag, ","); len(ss) > 1 {
// TODO: add more validation & error checking
if strings.TrimSpace(ss[1]) == omitEmptyTagOption {
return ss[0], true
}
return ss[0], false
} else {
return originalTag, false
}
}

// buildMap builds the parent map and calls buildNestedMap to create the child maps based on dot notation
func buildMap(s []string, value interface{}, parent *map[string]interface{}) error {
var obj = make(map[string]interface{})
Expand All @@ -143,6 +171,10 @@ func ToMap(obj interface{}, tag string) (*map[string]interface{}, error) {
tagName = tag
s := getMapOfAllKeyValues(obj)

if s == nil {
return nil, fmt.Errorf("no valid map could be formed")
}

var parentMap = make(map[string]interface{})
for k, v := range *s {
keys := strings.Split(k, ".")
Expand Down
53 changes: 52 additions & 1 deletion map_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ func TestNilAndUnexportedFields(t *testing.T) {
f1 string `custom:"f1"`
F2 struct {
F21 string `custom:"f21"`
} `custom:"age"`
} `custom:"f2"`
F3 *string `custom:"f3"`
F4 int `custom:"f4"`
F5 interface{} `custom:"f5"`
Expand Down Expand Up @@ -204,3 +204,54 @@ func TestNilAndUnexportedFields(t *testing.T) {
// compare
require.JSONEqf(t, expectedJSON, string(actualJSON), "JSON mismatch")
}

// TestOmitEmptyOptionFields calls ToMap function and checks for the expected behaviour
// of passing the omitempty tag option
func TestOmitEmptyOptionFields(t *testing.T) {
type MyStruct struct {
F1 string `custom:"f1,omitempty"`
F2 struct {
F21 string `custom:"f21,omitempty"`
} `custom:"f2"`
F3 *string `custom:"f3, omitempty"` // omitempty with space
F4 int `custom:"f4,omitempty"`
F5 bool `custom:"f5,omitempty"`
F6 interface{} `custom:"f6,omitempty"`
F7 struct {
F71 string `custom:"f71"`
} `custom:"f7,omitempty"`
F8 *bool `custom:"f8,omitempty"`
F9 struct {
F91 string `custom:"f91"`
} `custom:"f9,omitempty"`
}

f := false
obj := MyStruct{
F5: f,
F8: &f,
}
obj.F9.F91 = "123"

// expected response
expectedJSON := `{
"f8": false,
"f9": {
"f91": "123"
}
}
`

// get the map from custom tags
actual, err := ToMap(obj, "custom")
if err != nil {
t.Fail()
}
actualJSON, err := json.Marshal(actual)
if err != nil {
t.Fail()
}

// compare
require.JSONEqf(t, expectedJSON, string(actualJSON), "JSON mismatch")
}

0 comments on commit 7b4b7e7

Please # to comment.