Skip to content

Commit 3e7c8cf

Browse files
authored
Merge pull request #4 from dumim/develop
Getting things ready for first stable release
2 parents 0001959 + 4b3f378 commit 3e7c8cf

File tree

5 files changed

+185
-34
lines changed

5 files changed

+185
-34
lines changed

.circleci/config.yml

Lines changed: 8 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
# Use the latest 2.1 version of CircleCI pipeline process engine. See: https://circleci.com/docs/2.0/configuration-reference
21
version: 2.1
32
jobs:
43
build:
@@ -12,19 +11,17 @@ jobs:
1211
- go-mod-v4-{{ checksum "go.sum" }}
1312
- run:
1413
name: Install Dependencies
15-
command: |
16-
go mod download
17-
go get github.com/mattn/goveralls
14+
command: go get ./...
1815
- save_cache:
1916
key: go-mod-v4-{{ checksum "go.sum" }}
2017
paths:
2118
- "/go/pkg/mod"
19+
- run:
20+
name: Get coverage libs
21+
command: go get github.com/mattn/goveralls
2222
- run:
2323
name: Run tests
24-
command: |
25-
mkdir -p /tmp/test-reports
26-
gotestsum --junitfile /tmp/test-reports/unit-tests.xml
27-
go test -v -cover -race -coverprofile=/home/ubuntu/coverage.out
28-
/home/ubuntu/.go_workspace/bin/goveralls -coverprofile=/home/ubuntu/coverage.out -service=circle-ci -repotoken=$COVERALLS_TOKEN
29-
- store_test_results:
30-
path: /tmp/test-reports
24+
command: go test -v -cover -race -coverprofile=coverage.out
25+
- run:
26+
name: Push coverage results
27+
command: $GOPATH/bin/goveralls -coverprofile=coverage.out -service=circle-ci -repotoken=$COVERALLS_TOKEN

README.md

Lines changed: 106 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
[![Documentation](https://godoc.org/github.com/dumim/tagconv?status.svg)](http://godoc.org/github.com/dumim/tagconv)
22
[![Go Report Card](https://goreportcard.com/badge/github.com/dumim/tagconv)](https://goreportcard.com/report/github.com/dumim/tagconv)
3+
[![Coverage Status](https://coveralls.io/repos/github/dumim/tagconv/badge.svg?branch=main)](https://coveralls.io/github/dumim/tagconv?branch=main)
34
[![MIT License](https://img.shields.io/apm/l/atomic-design-ui.svg?)](https://github.com/tterb/atomic-design-ui/blob/master/LICENSEs)
45

56
# TagConv
67
Convert any Go Struct to a Map based on custom struct tags with dot notation.
78

89
## Background
9-
This package tries to simplify certain use-cases where a struct needs to be mapped manually to a different struct, which holds the same data but is organised differently.
10-
(eg: mapping data from the db to a presentable API output)
10+
This package tries to simplify certain use-cases where the same struct needs to be organised/represented differently (eg: mapping data from the db to a presentable API JSON output).
11+
This would normally have to be done by having two different structs and manually mapping the data between each other.
1112

12-
This package allows you to use custom struct tags (which could be any string) to define the mapping.
13+
14+
This package allows you to use any custom struct tag to define the mapping.
1315
This mapping follows the dot-notation convention. Example:
1416
```go
1517
Hello string `mytag:"hello.world"`
@@ -22,13 +24,106 @@ The above will result in a map with the JSON equivalent of:
2224
}
2325
}
2426
```
25-
## Usage/Examples
27+
Any number of custom tags can be used to represent the same struct in unlimited number of different ways. For examples, see below.
28+
29+
30+
## Usage & Examples
2631

2732
Import the package
2833
```go
2934
import "github.com/dumim/tagconv"
3035
```
3136

37+
Define your struct with custom struct tags:
38+
39+
```go
40+
type MyStruct struct {
41+
Age string `foo:"age"`
42+
Year int `foo:"dob.year"`
43+
Month int `foo:"dob.month"`
44+
}
45+
46+
obj := MyStruct{
47+
Age: "22",
48+
Year: 1998,
49+
Month: 1,
50+
}
51+
52+
tagName = "foo"
53+
myMap, err := ToMap(obj, tagName)
54+
if err != nil {
55+
panic()
56+
}
57+
```
58+
This will result in a map that looks like:
59+
```go
60+
myMap = map[string]interface{}{
61+
"age": "22",
62+
"dob": map[string]interface{}{
63+
"year": 1998,
64+
"month": 1,
65+
}
66+
}
67+
```
68+
Converting to JSON ...
69+
```go
70+
myMapJSON, err := json.MarshalIndent(myMap, "", " ")
71+
if err != nil {
72+
panic()
73+
}
74+
fmt.Print(myMapJSON)
75+
```
76+
... will result in something similar to:
77+
```json
78+
{
79+
"age": "22",
80+
"dob": {
81+
"year": 1998,
82+
"month": 1
83+
}
84+
}
85+
```
86+
87+
---
88+
### Multiple struct tags
89+
90+
You can use multiple struct tags for different representation of the same struct:
91+
For example, similar to the previous example:
92+
```go
93+
type MyStructMultiple struct {
94+
Age string `foo:"age" bar:"details.my_age"`
95+
}
96+
97+
obj := MyStruct{
98+
Age: "22",
99+
}
100+
```
101+
Using `tagconv` for `obj` over the `foo` tag (`ToMap(obj, "foo")`) will result in:
102+
```json
103+
{
104+
"age": "22"
105+
}
106+
```
107+
whereas using `bar` (`ToMap(obj, "bar")`) on the same `obj` will result in:
108+
```json
109+
{
110+
"details": {
111+
"my_age": "22"
112+
}
113+
}
114+
```
115+
---
116+
### Tag options
117+
- If a nested struct has a tag, this will create a parent-child relationship with that tag and the tags of the fields within that struct.
118+
- Dot notation will create a parent-child relationship for every `.`.
119+
- 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
120+
- `-` will explicitly ignore that field. As opposed to above, it will not look inside even if the field is of struct type.
121+
122+
For an example that includes all the above scenarios see the code below:
123+
124+
125+
### More complex example
126+
32127
Given a deeply-nested complex struct with custom tags like below:
33128
```go
34129
type Obj struct {
@@ -88,9 +183,7 @@ myMapJSON, err := json.MarshalIndent(myMap, "", " ")
88183
if err != nil {
89184
panic()
90185
}
91-
92186
fmt.Print(myMapJSON)
93-
94187
```
95188
This will produce a result similar to:
96189
```json
@@ -122,6 +215,13 @@ This will produce a result similar to:
122215
]
123216
}
124217
```
218+
---
219+
220+
## Testing
221+
Run the go tests using `go test ./.. -v`
222+
223+
224+
---
125225

126226
## Acknowledgements
127227

go.mod

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ module github.com/dumim/tagconv
33
go 1.15
44

55
require (
6-
github.com/fatih/structs v1.1.0
76
github.com/imdario/mergo v0.3.12
87
github.com/stretchr/testify v1.7.0
98
)

map.go

Lines changed: 12 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import (
77
"strings"
88
)
99

10-
var tagName = ""
10+
var tagName = "" // initialise the struct tag value
1111

1212
// getMapOfAllKeyValues builds a map of the fully specified key and the value from the struct tag
1313
// the struct tags with the full dot notation will be used as the key, and the value as the value
@@ -28,13 +28,13 @@ var tagName = ""
2828
{"name":"world", "value":2}
2929
}
3030
*/
31-
func getMapOfAllKeyValues(s interface{}) (*map[string]interface{}, error) {
31+
func getMapOfAllKeyValues(s interface{}) *map[string]interface{} {
3232
var vars = make(map[string]interface{}) // this will hold the variables as a map (JSON)
3333

3434
// get value of object
3535
t := reflect.ValueOf(s)
3636
if t.IsZero() {
37-
return nil, fmt.Errorf("empty struct sent")
37+
return nil
3838
}
3939
// Iterate over all available fields and read the tag value
4040
for i := 0; i < t.NumField(); i++ {
@@ -52,9 +52,9 @@ func getMapOfAllKeyValues(s interface{}) (*map[string]interface{}, error) {
5252
// and check for its fields inside for tags
5353
if tag == "" {
5454
if t.Field(i).Kind() == reflect.Struct {
55-
// TODO: check for error
55+
// only check if the value can be obtained without panicking (eg: for unexported fields)
5656
if t.Field(i).CanInterface() {
57-
qVars, _ := getMapOfAllKeyValues(t.Field(i).Interface()) //recursive call
57+
qVars := getMapOfAllKeyValues(t.Field(i).Interface()) //recursive call
5858
for k, v := range *qVars {
5959
vars[k] = v
6060
}
@@ -65,14 +65,15 @@ func getMapOfAllKeyValues(s interface{}) (*map[string]interface{}, error) {
6565
} else {
6666
// recursive check nested fields in case this is a struct
6767
if t.Field(i).Kind() == reflect.Struct {
68-
// TODO: check for error
68+
// only check if the value can be obtained without panicking (eg: for unexported fields)
6969
if t.Field(i).CanInterface() {
70-
qVars, _ := getMapOfAllKeyValues(t.Field(i).Interface())
70+
qVars := getMapOfAllKeyValues(t.Field(i).Interface()) //recursive call
7171
for k, v := range *qVars {
7272
vars[fmt.Sprintf("%s.%s", tag, k)] = v // prepend the parent tag name
7373
}
7474
}
7575
} else {
76+
// only check if the value can be obtained without panicking (eg: for unexported fields)
7677
if t.Field(i).CanInterface() {
7778
vars[tag] = t.Field(i).Interface()
7879
}
@@ -93,8 +94,8 @@ func getMapOfAllKeyValues(s interface{}) (*map[string]interface{}, error) {
9394
// iterate through the slice
9495
for i := 0; i < s.Len(); i++ {
9596
if t.Field(i).CanInterface() {
96-
m, _ := getMapOfAllKeyValues(s.Index(i).Interface()) // get the map value of the object, recursively
97-
sliceOfMap = append(sliceOfMap, *m) // append to the slice
97+
m := getMapOfAllKeyValues(s.Index(i).Interface()) // get the map value of the object, recursively
98+
sliceOfMap = append(sliceOfMap, *m) // append to the slice
9899
}
99100
}
100101
finalMap[k] = sliceOfMap
@@ -103,7 +104,7 @@ func getMapOfAllKeyValues(s interface{}) (*map[string]interface{}, error) {
103104
}
104105
}
105106

106-
return &finalMap, nil
107+
return &finalMap
107108
}
108109

109110
// buildMap builds the parent map and calls buildNestedMap to create the child maps based on dot notation
@@ -124,10 +125,7 @@ func buildMap(s []string, value interface{}, parent *map[string]interface{}) err
124125
// for a more comprehensive example, please see the
125126
func ToMap(obj interface{}, tag string) (*map[string]interface{}, error) {
126127
tagName = tag
127-
s, err := getMapOfAllKeyValues(obj)
128-
if err != nil {
129-
return nil, err
130-
}
128+
s := getMapOfAllKeyValues(obj)
131129

132130
var parentMap = make(map[string]interface{})
133131
for k, v := range *s {

map_test.go

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,9 @@ type Example struct {
3232
//three int `custom:"three"` // unexported, TODO: handle panic
3333
}
3434

35-
// TestStructToMap calls ToMap function and checks against the expcted result
35+
// TestFullStructToMap calls ToMap function and checks against the expected result
3636
// The struct used tries to cover all the scenarios
37-
func TestStructToMap(t *testing.T) {
37+
func TestFullStructToMap(t *testing.T) {
3838

3939
// the initial object
4040
initial := Example{
@@ -103,3 +103,60 @@ func TestStructToMap(t *testing.T) {
103103
// compare
104104
require.JSONEqf(t, expectedJSON, string(actualJSON), "JSON mismatch")
105105
}
106+
107+
// TestMultipleTagsStructToMap calls ToMap function and checks against the expected result
108+
// The struct used tries to use multiple struct tags for different responses
109+
func TestMultipleTagsStructToMap(t *testing.T) {
110+
type MyStruct struct {
111+
Age string `foo:"age" bar:"details.myAge"`
112+
Year int `foo:"dob.year" bar:"details.birthYear"`
113+
Month int `foo:"dob.month" bar:"-"`
114+
}
115+
116+
obj := MyStruct{
117+
Age: "22",
118+
Year: 1998,
119+
Month: 1,
120+
}
121+
122+
// expected response
123+
expectedJSONOne := `{
124+
"age": "22",
125+
"dob": {
126+
"year": 1998,
127+
"month": 1
128+
}
129+
}
130+
`
131+
expectedJSONTwo := `{
132+
"details": {
133+
"myAge": "22",
134+
"birthYear": 1998
135+
}
136+
}
137+
`
138+
139+
// get the map from custom tags for tag "foo"
140+
actualOne, err := ToMap(obj, "foo")
141+
if err != nil {
142+
t.Fail()
143+
}
144+
actualJSONOne, err := json.Marshal(actualOne)
145+
if err != nil {
146+
t.Fail()
147+
}
148+
149+
// get the map from custom tags for tag "bar"
150+
actualTwo, err := ToMap(obj, "bar")
151+
if err != nil {
152+
t.Fail()
153+
}
154+
actualJSONTwo, err := json.Marshal(actualTwo)
155+
if err != nil {
156+
t.Fail()
157+
}
158+
159+
// compare
160+
require.JSONEqf(t, expectedJSONOne, string(actualJSONOne), "JSON mismatch for foo tags")
161+
require.JSONEqf(t, expectedJSONTwo, string(actualJSONTwo), "JSON mismatch for bar tags")
162+
}

0 commit comments

Comments
 (0)