├── .gitmodules ├── History.md ├── Makefile ├── go.mod ├── .gitignore ├── .buildkite └── pipeline.yml ├── Readme.md ├── cmd └── tableize │ └── main.go ├── go.sum ├── tableize.go ├── benchmark_test.go └── tableize_test.go /.gitmodules: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | 2 | v2.1.0 / 2021-06-21 3 | =================== 4 | 5 | * Support arrays (#7) 6 | * buildkite-migration (#8) 7 | * minor perf improvement 8 | * improvement 1 9 | * code cleanup 10 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | test: 3 | @go test -v -cover ./... 4 | .PHONY: test 5 | 6 | bench: 7 | @go test -cpu 1,2,4 -bench=. -benchmem 8 | .PHONY: bench 9 | 10 | race: 11 | @go test -cover -cpu 1,4 -bench=. -race ./... 12 | .PHONY: race -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/segmentio/go-tableize 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/segmentio/encoding v0.2.19 7 | github.com/segmentio/go-snakecase v1.2.0 8 | github.com/stretchr/testify v1.7.0 9 | github.com/tj/docopt v1.0.0 10 | ) 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | 26 | # Emacs 27 | *~ 28 | *.txt -------------------------------------------------------------------------------- /.buildkite/pipeline.yml: -------------------------------------------------------------------------------- 1 | steps: 2 | - label: 'Test' 3 | env: 4 | SEGMENT_CONTEXTS: snyk, aws-credentials 5 | SEGMENT_BUILDKITE_IMAGE: 'buildkite-agent-golang1.16' 6 | agents: 7 | queue: v1 8 | 9 | commands: | 10 | echo '--- Snyk' 11 | bk-snyk 12 | echo '--- Downloading Dependencies' 13 | go mod vendor 14 | echo '--- Running tests' 15 | go vet ./... 16 | go test -race ./... 17 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | 2 | # tablelize 3 | 4 | > **Note** 5 | > Segment has paused maintenance on this project, but may return it to an active status in the future. Issues and pull requests from external contributors are not being considered, although internal contributions may appear from time to time. The project remains available under its open source license for anyone to use. 6 | 7 | Go tableize the given map by recursively walking the map and normalizing 8 | its keys to produce a flat SQL-friendly map. 9 | 10 | ## CLI 11 | 12 | ```bash 13 | $ go get github.com/segmentio/go-tableize/cmd/tableize 14 | $ echo '{"user": { "id": 1 }}' | tableize 15 | { "user_id": 1 } 16 | ``` 17 | 18 | ## Example 19 | 20 | ```go 21 | event := map[string]interface{}{ 22 | "name": map[string]interface{}{ 23 | "first name ": "tobi", 24 | "last-name": "holowaychuk", 25 | }, 26 | "species": "ferret", 27 | } 28 | 29 | flat := Tableize(event) 30 | assert(t, flat["name_first_name"] == "tobi") 31 | assert(t, flat["name_last_name"] == "holowaychuk") 32 | assert(t, flat["species"] == "ferret") 33 | ``` 34 | -------------------------------------------------------------------------------- /cmd/tableize/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | "log" 7 | "os" 8 | 9 | "github.com/segmentio/go-tableize" 10 | "github.com/tj/docopt" 11 | ) 12 | 13 | var version = "0.0.1" 14 | var usage = ` 15 | Usage: 16 | tableize 17 | tableize -h | --help 18 | tableize -v | --version 19 | 20 | Examples: 21 | 22 | $ echo '{"user": { "id": 1 }}' | tableize 23 | { "user_id": 1 } 24 | 25 | Options: 26 | -h, --help show help information 27 | -v, --version show version information 28 | 29 | ` 30 | 31 | func main() { 32 | _, err := docopt.Parse(usage, nil, true, version, false) 33 | if err != nil { 34 | log.Fatal(err) 35 | } 36 | 37 | dec := json.NewDecoder(os.Stdin) 38 | enc := json.NewEncoder(os.Stdout) 39 | 40 | for { 41 | var m map[string]interface{} 42 | 43 | err := dec.Decode(&m) 44 | if err != nil { 45 | if err == io.EOF { 46 | return 47 | } 48 | log.Fatal(err) 49 | } 50 | 51 | err = enc.Encode(tableize.Tableize(&tableize.Input{Value: m})) 52 | if err != nil { 53 | log.Fatal(err) 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 2 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 3 | github.com/klauspost/cpuid/v2 v2.0.5 h1:qnfhwbFriwDIX51QncuNU5mEMf+6KE3t7O8V2KQl3Dg= 4 | github.com/klauspost/cpuid/v2 v2.0.5/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 5 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 6 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 7 | github.com/segmentio/encoding v0.2.19 h1:Kshkmoz080qvUtdtakR8Bjk2sIlLS8wSvijFMEHRGow= 8 | github.com/segmentio/encoding v0.2.19/go.mod h1:7E68jTSWMnNoYhHi1JbLd7NBSB6XfE4vzqhR88hDBQc= 9 | github.com/segmentio/go-snakecase v1.2.0 h1:4cTmEjPGi03WmyAHWBjX53viTpBkn/z+4DO++fqYvpw= 10 | github.com/segmentio/go-snakecase v1.2.0/go.mod h1:jk1miR5MS7Na32PZUykG89Arm+1BUSYhuGR6b7+hJto= 11 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 12 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 13 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 14 | github.com/tj/docopt v1.0.0 h1:echGqiJYxqwI1PQAppd8PrBM2e6E4G46NQugrTpL/wU= 15 | github.com/tj/docopt v1.0.0/go.mod h1:UWdJekySvYOgmpTJtkPaWS4fvSKYba+U6+E2iKJCV/I= 16 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 17 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 18 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= 19 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 20 | -------------------------------------------------------------------------------- /tableize.go: -------------------------------------------------------------------------------- 1 | package tableize 2 | 3 | import ( 4 | "log" 5 | "sort" 6 | 7 | "github.com/segmentio/encoding/json" 8 | snakecase "github.com/segmentio/go-snakecase" 9 | ) 10 | 11 | // Input contains all information to be tableized 12 | type Input struct { 13 | Value map[string]interface{} 14 | 15 | // Optional 16 | HintSize int 17 | Substitutions map[string]string 18 | StringifyArrays bool 19 | } 20 | 21 | // Tableize the given map by flattening and normalizing all 22 | // of the key/value pairs recursively. 23 | func Tableize(in *Input) map[string]interface{} { 24 | if in.HintSize == 0 { 25 | in.HintSize = len(in.Value) 26 | } 27 | 28 | ret := make(map[string]interface{}, in.HintSize) 29 | visit(ret, in.Value, "", in.Substitutions, in.StringifyArrays) 30 | return ret 31 | } 32 | 33 | // Visit map recursively and populate `ret`. 34 | // We have to lowercase for now so that properties 35 | // are mapped correctly to the schema fetched from 36 | // redshift, as RS _always_ lowercases the column 37 | // name in information_schema.columns. 38 | func visit(ret map[string]interface{}, m map[string]interface{}, prefix string, substitutions map[string]string, stringifyArrays bool) { 39 | var val interface{} 40 | var renamed string 41 | var ok bool 42 | keys := getSortedKeys(m) 43 | 44 | for _, key := range keys { 45 | val = m[key] 46 | if len(substitutions) > 0 { 47 | if renamed, ok = substitutions[prefix+key]; ok { 48 | key = renamed 49 | } 50 | } 51 | key = prefix + snakecase.Snakecase(key) 52 | 53 | switch t := val.(type) { 54 | case map[string]interface{}: 55 | visit(ret, t, key+"_", substitutions, stringifyArrays) 56 | case []interface{}: 57 | if stringifyArrays { 58 | valByteArr, err := json.Marshal(val) 59 | if err != nil { 60 | log.Printf("go-tableize: dropping array value %+v that could not be converted to string: %s\n", val, err) 61 | } else { 62 | ret[key] = string(valByteArr) 63 | } 64 | } else { 65 | ret[key] = val 66 | } 67 | default: 68 | ret[key] = val 69 | } 70 | } 71 | } 72 | 73 | func getSortedKeys(m map[string]interface{}) []string { 74 | keys := make([]string, 0, len(m)) 75 | for key := range m { 76 | keys = append(keys, key) 77 | } 78 | sort.Strings(keys) 79 | return keys 80 | } 81 | -------------------------------------------------------------------------------- /benchmark_test.go: -------------------------------------------------------------------------------- 1 | package tableize_test 2 | 3 | import ( 4 | "encoding/json" 5 | "testing" 6 | 7 | . "github.com/segmentio/go-tableize" 8 | ) 9 | 10 | func BenchmarkSmall(b *testing.B) { 11 | str := `{ 12 | "anonymousId": "b2e9efda-4fc1-4bfa-8dc8-95ce56d8f53e", 13 | "projectId": "gnv5tty0m6", 14 | "properties": { 15 | "path": "/_generated_background_page.html", 16 | "referrer": "", 17 | "search": "", 18 | "title": "", 19 | "url": "chrome-extension://djkmaiagjpalbehljdggfaebgmgioobl/_generated_background_page.html" 20 | }, 21 | "receivedAt": "2014-05-13T20:28:52.803Z", 22 | "requestId": "e894944b-9afe-49c0-a782-1e0cfc68fe48", 23 | "timestamp": "2014-05-13T20:28:50.540Z", 24 | "type": "page", 25 | "userId": "6fac5180-b4d5-4305-a210-a1674bb3af4b", 26 | "version": 2 27 | }` 28 | 29 | event := make(map[string]interface{}) 30 | check(json.Unmarshal([]byte(str), &event)) 31 | 32 | for i := 0; i < b.N; i++ { 33 | Tableize(&Input{Value: event}) 34 | } 35 | } 36 | 37 | func BenchmarkMedium(b *testing.B) { 38 | str := `{ 39 | "anonymousId": "b2e9efda-4fc1-4bfa-8dc8-95ce56d8f53e", 40 | "channel": "client", 41 | "context": { 42 | "ip": "67.208.188.98", 43 | "library": { 44 | "name": "analytics.js", 45 | "version": "unknown" 46 | }, 47 | "userAgent": "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.131 Safari/537.36" 48 | }, 49 | "projectId": "gnv5tty0m6", 50 | "properties": { 51 | "path": "/_generated_background_page.html", 52 | "referrer": "", 53 | "search": "", 54 | "title": "", 55 | "url": "chrome-extension://djkmaiagjpalbehljdggfaebgmgioobl/_generated_background_page.html" 56 | }, 57 | "receivedAt": "2014-05-13T20:28:52.803Z", 58 | "requestId": "e894944b-9afe-49c0-a782-1e0cfc68fe48", 59 | "timestamp": "2014-05-13T20:28:50.540Z", 60 | "type": "page", 61 | "userId": "6fac5180-b4d5-4305-a210-a1674bb3af4b", 62 | "version": 2 63 | }` 64 | 65 | event := make(map[string]interface{}) 66 | check(json.Unmarshal([]byte(str), &event)) 67 | 68 | for i := 0; i < b.N; i++ { 69 | Tableize(&Input{Value: event}) 70 | } 71 | } 72 | 73 | func BenchmarkLarge(b *testing.B) { 74 | str := `{ 75 | "anonymousId": "b2e9efda-4fc1-4bfa-8dc8-95ce56d8f53e", 76 | "channel": "client", 77 | "context": { 78 | "ip": "67.208.188.98", 79 | "library": { 80 | "name": "analytics.js", 81 | "version": "unknown" 82 | }, 83 | "userAgent": "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.131 Safari/537.36" 84 | }, 85 | "projectId": "gnv5tty0m6", 86 | "properties": { 87 | "path": "/_generated_background_page.html", 88 | "referrer": "", 89 | "search": "", 90 | "title": "", 91 | "url": "chrome-extension://djkmaiagjpalbehljdggfaebgmgioobl/_generated_background_page.html", 92 | "anonymousId": "b2e9efda-4fc1-4bfa-8dc8-95ce56d8f53e", 93 | "channel": "client", 94 | "context": { 95 | "ip": "67.208.188.98", 96 | "library": { 97 | "name": "analytics.js", 98 | "version": "unknown" 99 | }, 100 | "userAgent": "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.131 Safari/537.36" 101 | }, 102 | "projectId": "gnv5tty0m6", 103 | "properties": { 104 | "path": "/_generated_background_page.html", 105 | "referrer": "", 106 | "search": "", 107 | "title": "", 108 | "url": "chrome-extension://djkmaiagjpalbehljdggfaebgmgioobl/_generated_background_page.html", 109 | "anonymousId": "b2e9efda-4fc1-4bfa-8dc8-95ce56d8f53e", 110 | "channel": "client", 111 | "context": { 112 | "ip": "67.208.188.98", 113 | "library": { 114 | "name": "analytics.js", 115 | "version": "unknown" 116 | }, 117 | "userAgent": "Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/34.0.1847.131 Safari/537.36" 118 | }, 119 | "projectId": "gnv5tty0m6", 120 | "properties": { 121 | "path": "/_generated_background_page.html", 122 | "referrer": "", 123 | "search": "", 124 | "title": "", 125 | "url": "chrome-extension://djkmaiagjpalbehljdggfaebgmgioobl/_generated_background_page.html" 126 | }, 127 | "receivedAt": "2014-05-13T20:28:52.803Z", 128 | "requestId": "e894944b-9afe-49c0-a782-1e0cfc68fe48", 129 | "timestamp": "2014-05-13T20:28:50.540Z", 130 | "type": "page", 131 | "userId": "6fac5180-b4d5-4305-a210-a1674bb3af4b", 132 | "version": 2 133 | }, 134 | "receivedAt": "2014-05-13T20:28:52.803Z", 135 | "requestId": "e894944b-9afe-49c0-a782-1e0cfc68fe48", 136 | "timestamp": "2014-05-13T20:28:50.540Z", 137 | "type": "page", 138 | "userId": "6fac5180-b4d5-4305-a210-a1674bb3af4b", 139 | "version": 2 140 | }, 141 | "receivedAt": "2014-05-13T20:28:52.803Z", 142 | "requestId": "e894944b-9afe-49c0-a782-1e0cfc68fe48", 143 | "timestamp": "2014-05-13T20:28:50.540Z", 144 | "type": "page", 145 | "userId": "6fac5180-b4d5-4305-a210-a1674bb3af4b", 146 | "version": 2 147 | }` 148 | 149 | event := make(map[string]interface{}) 150 | check(json.Unmarshal([]byte(str), &event)) 151 | 152 | for i := 0; i < b.N; i++ { 153 | Tableize(&Input{Value: event}) 154 | } 155 | } 156 | 157 | func check(err error) { 158 | if err != nil { 159 | panic(err) 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /tableize_test.go: -------------------------------------------------------------------------------- 1 | package tableize 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "testing" 7 | 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestTableize(t *testing.T) { 12 | spec := []struct { 13 | desc string 14 | input Input 15 | expected map[string]interface{} 16 | }{ 17 | { 18 | desc: "simple", 19 | input: Input{Value: map[string]interface{}{ 20 | "species": "ferret", 21 | }}, 22 | expected: map[string]interface{}{ 23 | "species": "ferret", 24 | }, 25 | }, 26 | { 27 | desc: "nested", 28 | input: Input{ 29 | Value: map[string]interface{}{ 30 | "name": map[string]interface{}{ 31 | "first": "tobi", 32 | }, 33 | }, 34 | }, 35 | expected: map[string]interface{}{"name_first": "tobi"}, 36 | }, 37 | { 38 | desc: "normalize keys", 39 | input: Input{ 40 | Value: map[string]interface{}{ 41 | "first name ": "tobi", 42 | "Last-Name": "holowaychuk", 43 | "NickName": "shupa", 44 | "$some_thing": "tobi", 45 | }, 46 | }, 47 | expected: map[string]interface{}{ 48 | "first_name": "tobi", 49 | "last_name": "holowaychuk", 50 | "nick_name": "shupa", 51 | "some_thing": "tobi", 52 | }, 53 | }, 54 | { 55 | desc: "conflicting keys", 56 | input: Input{ 57 | Value: map[string]interface{}{ 58 | "firstName": "first_1", 59 | "first_name": "first_2", 60 | "lastName": "last_1", 61 | "last_name": "last_2", 62 | "middleName": "middle_1", 63 | "middle_name": "middle_2", 64 | "first": map[string]interface{}{"name": "first_3"}, 65 | "last": map[string]interface{}{"name": "last_3"}, 66 | }, 67 | }, 68 | expected: map[string]interface{}{ 69 | "first_name": "first_2", 70 | "last_name": "last_2", 71 | "middle_name": "middle_2", 72 | }, 73 | }, 74 | { 75 | desc: "substitutions", 76 | input: Input{ 77 | Value: map[string]interface{}{ 78 | "name": map[string]interface{}{ 79 | "first name ": "tobi", 80 | "Last-Name": "holowaychuk", 81 | "NickName": "shupa", 82 | "$some_thing": "tobi", 83 | }, 84 | "_mid": "value", 85 | "species": "ferret", 86 | }, 87 | Substitutions: map[string]string{ 88 | "species": "r_species", 89 | "name_$some_thing": "just_some_thing", 90 | "_mid": "u_mid", 91 | }, 92 | }, 93 | expected: map[string]interface{}{ 94 | "name_first_name": "tobi", 95 | "name_last_name": "holowaychuk", 96 | "r_species": "ferret", 97 | "name_nick_name": "shupa", 98 | "name_just_some_thing": "tobi", 99 | "u_mid": "value", 100 | }, 101 | }, 102 | } 103 | 104 | for _, test := range spec { 105 | t.Run(test.desc, func(t *testing.T) { 106 | assert.Equal(t, test.expected, Tableize(&test.input)) 107 | }) 108 | } 109 | } 110 | 111 | func TestTableizeArray(t *testing.T) { 112 | jsonStr := "{ \"context_client\": null,\n \"context_latitude\": null,\n \"context_location\": null,\n \"context_longitude\": null,\n \"data\": \"{\\\"via_zendesk\\\":true}\",\n \"event_type\": \"FacebookComment\",\n \"graph_object_id\": \"null\",\n \"program\": \"source-runner\",\n \"ticket_event_id\": 17820988764,\n \"ticket_event_via\": \"Mail\",\n \"ticket_id\": \"357276\",\n \"timestamp\": \"2014-07-18T04:12:42.000Z\",\n \"trusted\": true,\n \"updater_id\": \"473752564\",\n \"version\": \"8bc54c3\",\n \"via\": {\n \"channel\": \"email\",\n \"source\": {\n \"from\": {\n \"address\": null,\n \"name\": \"a b\",\n \"original_recipients\": [\n \"a@b.com\",\n \"service@v.com.au\"\n ]\n },\n \"rel\": null,\n \"to\": {\n \"address\": null,\n \"name\": \"companyName\"\n }\n }\n }\n}" 113 | dec := json.NewDecoder(bytes.NewReader([]byte(jsonStr))) 114 | dec.UseNumber() 115 | prop := make(map[string]interface{}) 116 | dec.Decode(&prop) 117 | var arr []interface{} 118 | arr = append(arr, "a@b.com", "service@v.com.au") 119 | marshal, _ := json.Marshal(arr) 120 | arrStr := string(marshal) 121 | spec := []struct { 122 | desc string 123 | input Input 124 | expected map[string]interface{} 125 | }{ 126 | { 127 | desc: "array inside big JSON", 128 | input: Input{Value: prop, StringifyArrays: true}, 129 | expected: map[string]interface{}{ 130 | "context_latitude": nil, 131 | "context_location": nil, 132 | "event_type": "FacebookComment", 133 | "graph_object_id": "null", 134 | "program": "source-runner", 135 | "ticket_event_id": json.Number("17820988764"), 136 | "ticket_event_via": "Mail", 137 | "ticket_id": "357276", 138 | "trusted": true, 139 | "via_source_rel": nil, 140 | "via_source_to_address": nil, 141 | "context_client": nil, 142 | "context_longitude": nil, 143 | "data": "{\"via_zendesk\":true}", 144 | "via_source_from_address": nil, 145 | "via_source_from_name": "a b", 146 | "via_source_from_original_recipients": arrStr, 147 | "timestamp": "2014-07-18T04:12:42.000Z", 148 | "updater_id": "473752564", 149 | "version": "8bc54c3", 150 | "via_channel": "email", 151 | "via_source_to_name": "companyName", 152 | }, 153 | }, 154 | { 155 | desc: "simple arr StringifyArrays=true", 156 | input: Input{Value: map[string]interface{}{ 157 | "colors": []interface{}{"red", "blue"}, 158 | }, StringifyArrays: true, 159 | }, 160 | expected: map[string]interface{}{ 161 | "colors": "[\"red\",\"blue\"]", 162 | }, 163 | }, 164 | { 165 | desc: "simple arr StringifyArrays=false", 166 | input: Input{Value: map[string]interface{}{ 167 | "colors": []interface{}{"red", "blue"}, 168 | }, StringifyArrays: false, 169 | }, 170 | expected: map[string]interface{}{ 171 | "colors": []interface{}{"red", "blue"}, 172 | }, 173 | }, 174 | } 175 | 176 | for _, test := range spec { 177 | t.Run(test.desc, func(t *testing.T) { 178 | assert.Equal(t, test.expected, Tableize(&test.input)) 179 | }) 180 | } 181 | } 182 | --------------------------------------------------------------------------------