├── go.mod ├── .gitignore ├── Makefile ├── go.sum ├── .github └── workflows │ ├── test.yml │ └── rebase.yml ├── doc.go ├── LICENSE ├── README.md ├── flat.go └── flat_test.go /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/nqd/flat 2 | 3 | go 1.16 4 | 5 | require github.com/imdario/mergo v0.3.12 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | .idea/ 8 | 9 | # Test binary, build with `go test -c` 10 | *.test 11 | 12 | # Output of the go coverage tool, specifically when used with LiteIDE 13 | *.out 14 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: lint-pkgs lint test 2 | 3 | lint-pkgs: 4 | GO111MODULE=off go get -u honnef.co/go/tools/cmd/staticcheck 5 | GO111MODULE=off go get -u github.com/client9/misspell/cmd/misspell 6 | 7 | lint: 8 | $(exit $(go fmt ./... | wc -l)) 9 | go vet ./... 10 | find . -type f -name "*.go" | xargs misspell -error -locale US 11 | staticcheck $(go list ./...) 12 | 13 | test: 14 | go test -race ./... 15 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= 2 | github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= 3 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 4 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 5 | gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= 6 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 7 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: [push, pull_request] 3 | 4 | jobs: 5 | test: 6 | strategy: 7 | matrix: 8 | go: [1.18, 1.19] 9 | 10 | name: Test 11 | runs-on: ubuntu-latest 12 | steps: 13 | 14 | - name: Set up Go 15 | uses: actions/setup-go@v2 16 | with: 17 | go-version: ${{matrix.go}} 18 | 19 | - name: Check out code 20 | uses: actions/checkout@v3 21 | 22 | - name: Lint 23 | run: | 24 | ls -al 25 | make lint-pkgs 26 | make lint 27 | 28 | - name: Test 29 | run: | 30 | make test -------------------------------------------------------------------------------- /.github/workflows/rebase.yml: -------------------------------------------------------------------------------- 1 | name: Automatic Rebase 2 | on: 3 | issue_comment: 4 | types: [created] 5 | 6 | jobs: 7 | rebase: 8 | name: Rebase 9 | runs-on: ubuntu-latest 10 | if: >- 11 | github.event.issue.pull_request != '' && 12 | ( 13 | contains(github.event.comment.body, '/rebase') || 14 | contains(github.event.comment.body, '/autosquash') 15 | ) 16 | steps: 17 | - name: Checkout the latest code 18 | uses: actions/checkout@v3 19 | with: 20 | token: ${{ secrets.GITHUB_TOKEN }} 21 | fetch-depth: 0 # otherwise, you will fail to push refs to dest repo 22 | - name: Automatic Rebase 23 | uses: cirrus-actions/rebase@1.7 24 | with: 25 | autosquash: ${{ contains(github.event.comment.body, '/autosquash') || contains(github.event.comment.body, '/rebase-autosquash') }} 26 | env: 27 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package flat flattens a nested Golang map into a one level deep map. Flat also supports unflatten, turn a one level map into nested one. 2 | // 3 | // You can flatten a Go map 4 | // 5 | // in = map[string]interface{}{ 6 | // "foo": map[string]interface{}{ 7 | // "bar": map[string]interface{}{ 8 | // "t": 123, 9 | // }, 10 | // "k": 456, 11 | // }, 12 | // } 13 | // 14 | // out, err := flat.Flatten(in, nil) 15 | // // out = map[string]interface{}{ 16 | // // "foo.bar.t": 123, 17 | // // "foo.k": 456, 18 | // // } 19 | // 20 | // and a reverse with unflatten 21 | // in = map[string]interface{}{ 22 | // "foo.bar.t": 123, 23 | // "foo.k": 456, 24 | // } 25 | // out, err := flat.Unflatten(in, nil) 26 | // // out = map[string]interface{}{ 27 | // // "foo": map[string]interface{}{ 28 | // // "bar": map[string]interface{}{ 29 | // // "t": 123, 30 | // // }, 31 | // // "k": 456, 32 | // // }, 33 | // // } 34 | // 35 | package flat 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Nguyễn Quốc Đính 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # flat [![Build Status](https://secure.travis-ci.org/nqd/flat.png?branch=master)](http://travis-ci.org/nqd/flat) 2 | 3 | Take a golang map and flatten it or unfatten a map with delimited key. 4 | 5 | This work inspired by the [nodejs flat package](https://github.com/hughsk/flat/) 6 | 7 | ## Method 8 | 9 | ### Flatten 10 | 11 | Flatten given map, returns a map one level deep. 12 | 13 | ```{go} 14 | in := map[string]interface{}{ 15 | "a": "b", 16 | "c": map[string]interface{}{ 17 | "d": "e", 18 | "f": "g", 19 | }, 20 | "z": [2, 1.4567], 21 | } 22 | 23 | out, err := flat.Flatten(in, nil) 24 | // out = map[string]interface{}{ 25 | // "a": "b", 26 | // "c.d": "e", 27 | // "c.f": "g", 28 | // "z.0": 2, 29 | // "z.1": 1.4567, 30 | // } 31 | ``` 32 | 33 | ### Unflatten 34 | 35 | Since there is flatten, flat should have unfatten. 36 | 37 | ```{go} 38 | in := map[string]interface{}{ 39 | "foo.bar": map[string]interface{}{"t": 123}, 40 | "foo": map[string]interface{}{"k": 456}, 41 | } 42 | 43 | out, err := flat.Unflatten(in, nil) 44 | // out = map[string]interface{}{ 45 | // "foo": map[string]interface{}{ 46 | // "bar": map[string]interface{}{ 47 | // "t": 123, 48 | // }, 49 | // "k": 456, 50 | // }, 51 | // } 52 | ``` 53 | 54 | ## Options 55 | 56 | ### Delimiter 57 | 58 | Use a custom delimiter for flattening/unflattening your objects. Default value is `.`. 59 | 60 | ```{go} 61 | in := map[string]interface{}{ 62 | "hello": map[string]interface{}{ 63 | "world": map[string]interface{}{ 64 | "again": "good morning", 65 | } 66 | }, 67 | } 68 | 69 | out, err := flat.Flatten(in, &flat.Options{ 70 | Delimiter: ":", 71 | }) 72 | // out = map[string]interface{}{ 73 | // "hello:world:again": "good morning", 74 | // } 75 | ``` 76 | 77 | ### Safe 78 | 79 | 80 | When Safe is true, fatten will preserve arrays and their contents. Default Safe value is `false`. 81 | 82 | ```{go} 83 | in := map[string]interface{}{ 84 | "hello": map[string]interface{}{ 85 | "world": []interface{}{ 86 | "one", 87 | "two", 88 | } 89 | }, 90 | } 91 | 92 | out, err := flat.Flatten(in, &flat.Options{ 93 | Delimiter: ".", 94 | Safe: true, 95 | }) 96 | // out = map[string]interface{}{ 97 | // "hello.world": []interface{}{"one", "two"}, 98 | // } 99 | ``` 100 | 101 | 102 | 103 | ### MaxDepth 104 | 105 | MaxDepth is the maximum number of nested objects to flatten. MaxDepth can be any integer number. MaxDepth = 0 means no limit. 106 | 107 | Default MaxDepth value is `0`. 108 | 109 | ```{go} 110 | in := map[string]interface{}{ 111 | "hello": map[string]interface{}{ 112 | "world": []interface{}{ 113 | "again": "good morning", 114 | } 115 | }, 116 | } 117 | 118 | out, err := flat.Flatten(in, &flat.Options{ 119 | Delimiter: ".", 120 | MaxDepth: 2, 121 | }) 122 | // out = map[string]interface{}{ 123 | // "hello.world": map[string]interface{}{"again": "good morning"}, 124 | // } 125 | ``` 126 | 127 | ## Todos 128 | 129 | - [ ] Safe option for Unflatten 130 | - [ ] Overwrite -------------------------------------------------------------------------------- /flat.go: -------------------------------------------------------------------------------- 1 | package flat 2 | 3 | import ( 4 | "reflect" 5 | "strconv" 6 | "strings" 7 | 8 | "github.com/imdario/mergo" 9 | ) 10 | 11 | // Options the flatten options. 12 | // By default: Delimiter = "." 13 | type Options struct { 14 | Prefix string 15 | Delimiter string 16 | Safe bool 17 | MaxDepth int 18 | } 19 | 20 | // Flatten the map, it returns a map one level deep 21 | // regardless of how nested the original map was. 22 | // By default, the flatten has Delimiter = ".", and 23 | // no limitation of MaxDepth 24 | func Flatten(nested map[string]interface{}, opts *Options) (m map[string]interface{}, err error) { 25 | if opts == nil { 26 | opts = &Options{ 27 | Delimiter: ".", 28 | } 29 | } 30 | 31 | m, err = flatten(opts.Prefix, 0, nested, opts) 32 | 33 | return 34 | } 35 | 36 | func flatten(prefix string, depth int, nested interface{}, opts *Options) (flatmap map[string]interface{}, err error) { 37 | flatmap = make(map[string]interface{}) 38 | 39 | switch nested := nested.(type) { 40 | case map[string]interface{}: 41 | if opts.MaxDepth != 0 && depth >= opts.MaxDepth { 42 | flatmap[prefix] = nested 43 | return 44 | } 45 | if reflect.DeepEqual(nested, map[string]interface{}{}) { 46 | flatmap[prefix] = nested 47 | return 48 | } 49 | for k, v := range nested { 50 | // create new key 51 | newKey := k 52 | if prefix != "" { 53 | newKey = prefix + opts.Delimiter + newKey 54 | } 55 | fm1, fe := flatten(newKey, depth+1, v, opts) 56 | if fe != nil { 57 | err = fe 58 | return 59 | } 60 | update(flatmap, fm1) 61 | } 62 | case []interface{}: 63 | if opts.Safe { 64 | flatmap[prefix] = nested 65 | return 66 | } 67 | if reflect.DeepEqual(nested, []interface{}{}) { 68 | flatmap[prefix] = nested 69 | return 70 | } 71 | for i, v := range nested { 72 | newKey := strconv.Itoa(i) 73 | if prefix != "" { 74 | newKey = prefix + opts.Delimiter + newKey 75 | } 76 | fm1, fe := flatten(newKey, depth+1, v, opts) 77 | if fe != nil { 78 | err = fe 79 | return 80 | } 81 | update(flatmap, fm1) 82 | } 83 | default: 84 | flatmap[prefix] = nested 85 | } 86 | return 87 | } 88 | 89 | // update is the function that update to map with from 90 | // example: 91 | // to = {"hi": "there"} 92 | // from = {"foo": "bar"} 93 | // then, to = {"hi": "there", "foo": "bar"} 94 | func update(to map[string]interface{}, from map[string]interface{}) { 95 | for kt, vt := range from { 96 | to[kt] = vt 97 | } 98 | } 99 | 100 | // Unflatten the map, it returns a nested map of a map 101 | // By default, the flatten has Delimiter = "." 102 | func Unflatten(flat map[string]interface{}, opts *Options) (nested map[string]interface{}, err error) { 103 | if opts == nil { 104 | opts = &Options{ 105 | Delimiter: ".", 106 | } 107 | } 108 | nested, err = unflatten(flat, opts) 109 | return 110 | } 111 | 112 | func unflatten(flat map[string]interface{}, opts *Options) (nested map[string]interface{}, err error) { 113 | nested = make(map[string]interface{}) 114 | 115 | for k, v := range flat { 116 | temp := uf(k, v, opts).(map[string]interface{}) 117 | err = mergo.Merge(&nested, temp, func(c *mergo.Config) { c.Overwrite = true }) 118 | if err != nil { 119 | return 120 | } 121 | } 122 | 123 | return 124 | } 125 | 126 | func uf(k string, v interface{}, opts *Options) (n interface{}) { 127 | n = v 128 | 129 | if opts.Prefix != "" { 130 | k = strings.TrimPrefix(k, opts.Prefix+opts.Delimiter) 131 | } 132 | keys := strings.Split(k, opts.Delimiter) 133 | 134 | for i := len(keys) - 1; i >= 0; i-- { 135 | temp := make(map[string]interface{}) 136 | temp[keys[i]] = n 137 | n = temp 138 | } 139 | 140 | return 141 | } 142 | -------------------------------------------------------------------------------- /flat_test.go: -------------------------------------------------------------------------------- 1 | package flat 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "testing" 7 | ) 8 | 9 | func TestFlatten(t *testing.T) { 10 | tests := []struct { 11 | given string 12 | options *Options 13 | want map[string]interface{} 14 | }{ 15 | // test with different primitives and upper/lower case 16 | // String: 'world', 17 | // Number: 1234.99, 18 | // Boolean: true, 19 | // null: null, 20 | { 21 | `{"hello": "world"}`, 22 | nil, 23 | map[string]interface{}{"hello": "world"}, 24 | }, 25 | { 26 | `{"Hello": "world"}`, 27 | nil, 28 | map[string]interface{}{"Hello": "world"}, 29 | }, 30 | { 31 | `{"hello": 1234.99}`, 32 | nil, 33 | map[string]interface{}{"hello": 1234.99}, 34 | }, 35 | { 36 | `{"hello": true}`, 37 | nil, 38 | map[string]interface{}{"hello": true}, 39 | }, 40 | { 41 | `{"hello": null}`, 42 | nil, 43 | map[string]interface{}{"hello": nil}, 44 | }, 45 | // nested once 46 | { 47 | `{"hello":{}}`, 48 | nil, 49 | map[string]interface{}{"hello": map[string]interface{}{}}, 50 | }, 51 | { 52 | `{"hello":{"world":"good morning"}}`, 53 | nil, 54 | map[string]interface{}{"hello.world": "good morning"}, 55 | }, 56 | { 57 | `{"Hello":{"world":"good morning"}}`, 58 | nil, 59 | map[string]interface{}{"Hello.world": "good morning"}, 60 | }, 61 | { 62 | `{"hello":{"World":"good morning"}}`, 63 | nil, 64 | map[string]interface{}{"hello.World": "good morning"}, 65 | }, 66 | { 67 | `{"Hello":{"World":"good morning"}}`, 68 | nil, 69 | map[string]interface{}{"Hello.World": "good morning"}, 70 | }, 71 | { 72 | `{"hello":{"world":1234.99}}`, 73 | nil, 74 | map[string]interface{}{"hello.world": 1234.99}, 75 | }, 76 | { 77 | `{"hello":{"world":true}}`, 78 | nil, 79 | map[string]interface{}{"hello.world": true}, 80 | }, 81 | { 82 | `{"hello":{"world":null}}`, 83 | nil, 84 | map[string]interface{}{"hello.world": nil}, 85 | }, 86 | // empty slice 87 | { 88 | `{"hello":{"world":[]}}`, 89 | nil, 90 | map[string]interface{}{"hello.world": []interface{}{}}, 91 | }, 92 | // slice 93 | { 94 | `{"hello":{"world":["one","two"]}}`, 95 | nil, 96 | map[string]interface{}{ 97 | "hello.world.0": "one", 98 | "hello.world.1": "two", 99 | }, 100 | }, 101 | // nested twice 102 | { 103 | `{"hello":{"world":{"again":"good morning"}}}`, 104 | nil, 105 | map[string]interface{}{"hello.world.again": "good morning"}, 106 | }, 107 | // multiple keys 108 | { 109 | `{ 110 | "hello": { 111 | "lorem": { 112 | "ipsum":"again", 113 | "dolor":"sit" 114 | } 115 | }, 116 | "world": { 117 | "lorem": { 118 | "ipsum":"again", 119 | "dolor":"sit" 120 | } 121 | } 122 | }`, 123 | nil, 124 | map[string]interface{}{ 125 | "hello.lorem.ipsum": "again", 126 | "hello.lorem.dolor": "sit", 127 | "world.lorem.ipsum": "again", 128 | "world.lorem.dolor": "sit"}, 129 | }, 130 | // empty object 131 | { 132 | `{"hello":{"empty":{"nested":{}}}}`, 133 | nil, 134 | map[string]interface{}{"hello.empty.nested": map[string]interface{}{}}, 135 | }, 136 | // custom delimiter 137 | { 138 | `{"hello":{"world":{"again":"good morning"}}}`, 139 | &Options{ 140 | Delimiter: ":", 141 | MaxDepth: 20, 142 | }, 143 | map[string]interface{}{"hello:world:again": "good morning"}, 144 | }, 145 | // custom depth 146 | { 147 | `{ 148 | "hello": { 149 | "world": { 150 | "again": "good morning" 151 | } 152 | }, 153 | "lorem": { 154 | "ipsum": { 155 | "dolor": "good evening" 156 | } 157 | } 158 | } 159 | `, 160 | &Options{ 161 | MaxDepth: 2, 162 | Delimiter: ".", 163 | }, 164 | map[string]interface{}{ 165 | "hello.world": map[string]interface{}{"again": "good morning"}, 166 | "lorem.ipsum": map[string]interface{}{"dolor": "good evening"}, 167 | }, 168 | }, 169 | // custom safe = true 170 | { 171 | `{"hello":{"world":["one","two"]}}`, 172 | &Options{ 173 | Safe: true, 174 | Delimiter: ".", 175 | }, 176 | map[string]interface{}{ 177 | "hello.world": []interface{}{"one", "two"}, 178 | }, 179 | }, 180 | } 181 | for i, test := range tests { 182 | var given interface{} 183 | err := json.Unmarshal([]byte(test.given), &given) 184 | if err != nil { 185 | t.Errorf("%d: failed to unmarshal test: %v", i+1, err) 186 | } 187 | got, err := Flatten(given.(map[string]interface{}), test.options) 188 | if err != nil { 189 | t.Errorf("%d: failed to flatten: %v", i+1, err) 190 | } 191 | if !reflect.DeepEqual(got, test.want) { 192 | t.Errorf("%d: mismatch, got: %v want: %v", i+1, got, test.want) 193 | } 194 | } 195 | } 196 | 197 | func TestUnflatten(t *testing.T) { 198 | tests := []struct { 199 | flat map[string]interface{} 200 | options *Options 201 | want map[string]interface{} 202 | }{ 203 | { 204 | map[string]interface{}{"hello": "world"}, 205 | nil, 206 | map[string]interface{}{"hello": "world"}, 207 | }, 208 | // Key starts with upper case 209 | { 210 | map[string]interface{}{"Hello": "world"}, 211 | nil, 212 | map[string]interface{}{"Hello": "world"}, 213 | }, 214 | { 215 | map[string]interface{}{"hello": 1234.56}, 216 | nil, 217 | map[string]interface{}{"hello": 1234.56}, 218 | }, 219 | { 220 | map[string]interface{}{"hello": true}, 221 | nil, 222 | map[string]interface{}{"hello": true}, 223 | }, 224 | // nested twice 225 | { 226 | map[string]interface{}{"hello.world.again": "good morning"}, 227 | nil, 228 | map[string]interface{}{ 229 | "hello": map[string]interface{}{ 230 | "world": map[string]interface{}{ 231 | "again": "good morning", 232 | }, 233 | }, 234 | }, 235 | }, 236 | // multiple keys 237 | { 238 | map[string]interface{}{ 239 | "hello.lorem.ipsum": "again", 240 | "hello.lorem.dolor": "sit", 241 | "world.lorem.ipsum": "again", 242 | "world.lorem.dolor": "sit", 243 | "world": map[string]interface{}{"greet": "hello"}, 244 | }, 245 | nil, 246 | map[string]interface{}{ 247 | "hello": map[string]interface{}{ 248 | "lorem": map[string]interface{}{ 249 | "ipsum": "again", 250 | "dolor": "sit", 251 | }, 252 | }, 253 | "world": map[string]interface{}{ 254 | "greet": "hello", 255 | "lorem": map[string]interface{}{ 256 | "ipsum": "again", 257 | "dolor": "sit", 258 | }, 259 | }, 260 | }, 261 | }, 262 | // multiple keys - key starts with upper case 263 | { 264 | map[string]interface{}{ 265 | "Hello.lorem.ipsum": "L1 upper", 266 | "hello.lorem.ipsum": "L1 lower", 267 | "hello.Lorem.dolor": "L2 upper", 268 | "hello.lorem.dolor": "L2 lower", 269 | "world.lorem.Ipsum": "L3 upper", 270 | "world.lorem.ipsum": "L3 lower", 271 | "world.lorem.dolor": "sit", 272 | "world": map[string]interface{}{ 273 | "greet": "hello", 274 | "From": "alice", 275 | }, 276 | }, 277 | nil, 278 | map[string]interface{}{ 279 | "hello": map[string]interface{}{ 280 | "lorem": map[string]interface{}{ 281 | "ipsum": "L1 lower", 282 | "dolor": "L2 lower", 283 | }, 284 | "Lorem": map[string]interface{}{"dolor": "L2 upper"}, 285 | }, 286 | "Hello": map[string]interface{}{ 287 | "lorem": map[string]interface{}{"ipsum": "L1 upper"}, 288 | }, 289 | "world": map[string]interface{}{ 290 | "greet": "hello", 291 | "From": "alice", 292 | "lorem": map[string]interface{}{ 293 | "ipsum": "L3 lower", 294 | "Ipsum": "L3 upper", 295 | "dolor": "sit", 296 | }, 297 | }, 298 | }, 299 | }, 300 | // nested objects do not clobber each other 301 | { 302 | map[string]interface{}{ 303 | "foo.bar": map[string]interface{}{"t": 123}, 304 | "foo": map[string]interface{}{"k": 456}, 305 | }, 306 | nil, 307 | map[string]interface{}{ 308 | "foo": map[string]interface{}{ 309 | "bar": map[string]interface{}{ 310 | "t": 123, 311 | }, 312 | "k": 456, 313 | }, 314 | }, 315 | }, 316 | // custom delimiter 317 | { 318 | map[string]interface{}{ 319 | "hello world again": "good morning", 320 | }, 321 | &Options{ 322 | Delimiter: " ", 323 | }, 324 | map[string]interface{}{ 325 | "hello": map[string]interface{}{ 326 | "world": map[string]interface{}{ 327 | "again": "good morning", 328 | }, 329 | }, 330 | }, 331 | }, 332 | // do not overwrite 333 | { 334 | map[string]interface{}{ 335 | "travis": "true", 336 | "travis_build_dir": "/home/foo", 337 | }, 338 | &Options{ 339 | Delimiter: "_", 340 | }, 341 | map[string]interface{}{ 342 | "travis": "true", 343 | }, 344 | }, 345 | // keys with nil values 346 | { 347 | map[string]interface{}{ 348 | "foo.bar": map[string]interface{}{"t": nil}, 349 | "foo": map[string]interface{}{"k": nil}, 350 | }, 351 | nil, 352 | map[string]interface{}{ 353 | "foo": map[string]interface{}{ 354 | "bar": map[string]interface{}{ 355 | "t": nil, 356 | }, 357 | "k": nil, 358 | }, 359 | }, 360 | }, 361 | // todo 362 | // overwrite true 363 | // { 364 | // map[string]interface{}{ 365 | // "travis": "true", 366 | // "travis_build_dir": "/home/foo", 367 | // }, 368 | // Options{ 369 | // Delimiter: "_", 370 | // Overwrite: true, 371 | // }, 372 | // map[string]interface{}{ 373 | // "travis": map[string]interface{}{ 374 | // "build": map[string]interface{}{ 375 | // "dir": "/home/foo", 376 | // }, 377 | // }, 378 | // }, 379 | // }, 380 | } 381 | for i, test := range tests { 382 | got, err := Unflatten(test.flat, test.options) 383 | if err != nil { 384 | t.Errorf("%d: failed to unflatten: %v", i+1, err) 385 | } 386 | if !reflect.DeepEqual(got, test.want) { 387 | t.Errorf("%d: mismatch, got: %v want: %v", i+1, got, test.want) 388 | } 389 | } 390 | } 391 | 392 | func TestFlattenPrefix(t *testing.T) { 393 | tests := []struct { 394 | given string 395 | options *Options 396 | want map[string]interface{} 397 | }{ 398 | // test with different primitives 399 | // String: 'world', 400 | // Number: 1234.99, 401 | // Boolean: true, 402 | // null: null, 403 | { 404 | `{"hello": "world"}`, 405 | &Options{Prefix: "test", Delimiter: "."}, 406 | map[string]interface{}{"test.hello": "world"}, 407 | }, 408 | { 409 | `{"hello": 1234.99}`, 410 | &Options{Prefix: "test", Delimiter: "_"}, 411 | map[string]interface{}{"test_hello": 1234.99}, 412 | }, 413 | { 414 | `{"hello": true}`, 415 | &Options{Prefix: "test", Delimiter: "-"}, 416 | map[string]interface{}{"test-hello": true}, 417 | }, 418 | { 419 | `{"hello":{"world":"good morning"}}`, 420 | &Options{Prefix: "test", Delimiter: "."}, 421 | map[string]interface{}{"test.hello.world": "good morning"}, 422 | }, 423 | { 424 | `{"hello":{"world":1234.99}}`, 425 | &Options{Prefix: "test", Delimiter: "_"}, 426 | map[string]interface{}{"test_hello_world": 1234.99}, 427 | }, 428 | { 429 | `{"hello":{"world":true}}`, 430 | &Options{Prefix: "test", Delimiter: "-"}, 431 | map[string]interface{}{"test-hello-world": true}, 432 | }, 433 | } 434 | for i, test := range tests { 435 | var given interface{} 436 | err := json.Unmarshal([]byte(test.given), &given) 437 | if err != nil { 438 | t.Errorf("%d: failed to unmarshal test: %v", i+1, err) 439 | } 440 | got, err := Flatten(given.(map[string]interface{}), test.options) 441 | if err != nil { 442 | t.Errorf("%d: failed to flatten: %v", i+1, err) 443 | } 444 | if !reflect.DeepEqual(got, test.want) { 445 | t.Errorf("%d: mismatch, got: %v want: %v", i+1, got, test.want) 446 | } 447 | } 448 | } 449 | 450 | func TestUnflattenPrefix(t *testing.T) { 451 | tests := []struct { 452 | flat map[string]interface{} 453 | options *Options 454 | want map[string]interface{} 455 | }{ 456 | { 457 | map[string]interface{}{"test.hello": "world"}, 458 | &Options{Prefix: "test", Delimiter: "."}, 459 | map[string]interface{}{"hello": "world"}, 460 | }, 461 | { 462 | map[string]interface{}{"test_hello": 1234.56}, 463 | &Options{Prefix: "test", Delimiter: "_"}, 464 | map[string]interface{}{"hello": 1234.56}, 465 | }, 466 | { 467 | map[string]interface{}{"test-hello": true}, 468 | &Options{Prefix: "test", Delimiter: "-"}, 469 | map[string]interface{}{"hello": true}, 470 | }, 471 | // nested twice 472 | { 473 | map[string]interface{}{"test.hello.world.again": "good morning"}, 474 | &Options{Prefix: "test", Delimiter: "."}, 475 | map[string]interface{}{ 476 | "hello": map[string]interface{}{ 477 | "world": map[string]interface{}{ 478 | "again": "good morning", 479 | }, 480 | }, 481 | }, 482 | }, 483 | // custom delimiter 484 | { 485 | map[string]interface{}{ 486 | "test hello world again": "good morning", 487 | }, 488 | &Options{ 489 | Prefix: "test", 490 | Delimiter: " ", 491 | }, 492 | map[string]interface{}{ 493 | "hello": map[string]interface{}{ 494 | "world": map[string]interface{}{ 495 | "again": "good morning", 496 | }, 497 | }, 498 | }, 499 | }, 500 | } 501 | for i, test := range tests { 502 | got, err := Unflatten(test.flat, test.options) 503 | if err != nil { 504 | t.Errorf("%d: failed to unflatten: %v", i+1, err) 505 | } 506 | if !reflect.DeepEqual(got, test.want) { 507 | t.Errorf("%d: mismatch, got: %v want: %v", i+1, got, test.want) 508 | } 509 | } 510 | } 511 | --------------------------------------------------------------------------------