Skip to content

Commit

Permalink
Merge pull request #3 from dumim/develop
Browse files Browse the repository at this point in the history
Fixed panics on unexported fields
  • Loading branch information
dumim authored Jul 6, 2021
2 parents 8f02269 + 49c562f commit 0001959
Show file tree
Hide file tree
Showing 4 changed files with 180 additions and 16 deletions.
30 changes: 30 additions & 0 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Use the latest 2.1 version of CircleCI pipeline process engine. See: https://circleci.com/docs/2.0/configuration-reference
version: 2.1
jobs:
build:
working_directory: ~/repo
docker:
- image: circleci/golang:1.15.8
steps:
- checkout
- restore_cache:
keys:
- go-mod-v4-{{ checksum "go.sum" }}
- run:
name: Install Dependencies
command: |
go mod download
go get github.com/mattn/goveralls
- save_cache:
key: go-mod-v4-{{ checksum "go.sum" }}
paths:
- "/go/pkg/mod"
- run:
name: Run tests
command: |
mkdir -p /tmp/test-reports
gotestsum --junitfile /tmp/test-reports/unit-tests.xml
go test -v -cover -race -coverprofile=/home/ubuntu/coverage.out
/home/ubuntu/.go_workspace/bin/goveralls -coverprofile=/home/ubuntu/coverage.out -service=circle-ci -repotoken=$COVERALLS_TOKEN
- store_test_results:
path: /tmp/test-reports
132 changes: 130 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,132 @@
[![Documentation](https://godoc.org/github.com/dumim/tagconv?status.svg)](http://godoc.org/github.com/dumim/tagconv)
[![Go Report Card](https://goreportcard.com/badge/github.com/dumim/tagconv)](https://goreportcard.com/report/github.com/dumim/tagconv)
[![MIT License](https://img.shields.io/apm/l/atomic-design-ui.svg?)](https://github.com/tterb/atomic-design-ui/blob/master/LICENSEs)

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

## Background
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.
(eg: mapping data from the db to a presentable API output)

This package allows you to use custom struct tags (which could be any string) to define the mapping.
This mapping follows the dot-notation convention. Example:
```go
Hello string `mytag:"hello.world"`
```
The above will result in a map with the JSON equivalent of:
```json
{
"hello": {
"world": "hello world"
}
}
```
## Usage/Examples

Import the package
```go
import "github.com/dumim/tagconv"
```

Given a deeply-nested complex struct with custom tags like below:
```go
type Obj struct {
Name string `custom:"name"`
Text string `custom:"text"`
World string `custom:"data.world"` // dot notation inside nested struct
}
type ObjTwo struct {
Hello string `custom:"hello"`
Text string `custom:"data.text"`
}
type ObjThree struct {
Name string `custom:"name"`
Value int `custom:"value"`
}
type Example struct {
Name string `custom:"name"`
Email string `custom:"email"`
Obj Obj `custom:"object"`
ObjTwo ObjTwo // no tag
ObjThree ObjTwo `custom:"-"` // explicitly ignored
Id int `custom:"id"`
Call int `custom:"data.call"` // top-level dot notation
ArrayObj []ObjThree `custom:"list"`
}
```
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
obj := Example{
Name: "2",
Email: "3",
Obj: Obj{
Name: "4",
Text: "5",
World: "6",
},
ObjTwo: ObjTwo{
Hello: "1",
Text: "2",
},
Id: 01,
Call: 02,
ArrayObj: []ObjThree{
{"hi", 1},
{"world", 2},
},
}

// get the map from custom tags
tagName = "custom"
myMap, err := ToMap(obj, tagName)
if err != nil {
panic()
}

myMapJSON, err := json.MarshalIndent(myMap, "", " ")
if err != nil {
panic()
}

fmt.Print(myMapJSON)

```
This will produce a result similar to:
```json
{
"name": "2",
"email": "3",
"object": {
"name": "4",
"text": "5",
"data": {
"world": "6"
}
},
"hello": "1",
"data": {
"text": "2",
"call": 2
},
"id": 1,
"list": [
{
"name": "hi",
"value": 1
},
{
"name": "world",
"value": 2
}
]
}
```

## Acknowledgements

- [Helpful Stackoverflow answer](https://stackoverflow.com/a/7794127/10340220)
- [Mergo](https://github.com/imdario/mergo)
## Contributing

TODO: installation, examples, how-to, tests
Contributions are always welcome!
28 changes: 17 additions & 11 deletions map.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,6 @@ var tagName = ""
func getMapOfAllKeyValues(s interface{}) (*map[string]interface{}, error) {
var vars = make(map[string]interface{}) // this will hold the variables as a map (JSON)

// TODO: catch panics when reflecting unexported fields

// get value of object
t := reflect.ValueOf(s)
if t.IsZero() {
Expand All @@ -55,9 +53,11 @@ func getMapOfAllKeyValues(s interface{}) (*map[string]interface{}, error) {
if tag == "" {
if t.Field(i).Kind() == reflect.Struct {
// TODO: check for error
qVars, _ := getMapOfAllKeyValues(t.Field(i).Interface()) //recursive call
for k, v := range *qVars {
vars[k] = v
if t.Field(i).CanInterface() {
qVars, _ := getMapOfAllKeyValues(t.Field(i).Interface()) //recursive call
for k, v := range *qVars {
vars[k] = v
}
}
} else {
continue
Expand All @@ -66,12 +66,16 @@ func getMapOfAllKeyValues(s interface{}) (*map[string]interface{}, error) {
// recursive check nested fields in case this is a struct
if t.Field(i).Kind() == reflect.Struct {
// TODO: check for error
qVars, _ := getMapOfAllKeyValues(t.Field(i).Interface())
for k, v := range *qVars {
vars[fmt.Sprintf("%s.%s", tag, k)] = v // prepend the parent tag name
if t.Field(i).CanInterface() {
qVars, _ := getMapOfAllKeyValues(t.Field(i).Interface())
for k, v := range *qVars {
vars[fmt.Sprintf("%s.%s", tag, k)] = v // prepend the parent tag name
}
}
} else {
vars[tag] = t.Field(i).Interface()
if t.Field(i).CanInterface() {
vars[tag] = t.Field(i).Interface()
}
}
}
}
Expand All @@ -88,8 +92,10 @@ func getMapOfAllKeyValues(s interface{}) (*map[string]interface{}, error) {
s := reflect.ValueOf(v)
// iterate through the slice
for i := 0; i < s.Len(); i++ {
m, _ := getMapOfAllKeyValues(s.Index(i).Interface()) // get the map value of the object, recursively
sliceOfMap = append(sliceOfMap, *m) // append to the slice
if t.Field(i).CanInterface() {
m, _ := getMapOfAllKeyValues(s.Index(i).Interface()) // get the map value of the object, recursively
sliceOfMap = append(sliceOfMap, *m) // append to the slice
}
}
finalMap[k] = sliceOfMap
default:
Expand Down
6 changes: 3 additions & 3 deletions map_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,9 @@ type Example struct {
//three int `custom:"three"` // unexported, TODO: handle panic
}

// TestHelloName calls greetings.Hello with a name, checking
// for a valid return value.
func TestHelloName(t *testing.T) {
// TestStructToMap calls ToMap function and checks against the expcted result
// The struct used tries to cover all the scenarios
func TestStructToMap(t *testing.T) {

// the initial object
initial := Example{
Expand Down

0 comments on commit 0001959

Please sign in to comment.