├── .gitignore ├── LICENSE ├── README.md ├── TODO ├── doc.go ├── flatten.go ├── flatten_test.go ├── go.mod └── v2 ├── README.md ├── doc.go ├── flatten.go ├── flatten_test.go └── go.mod /.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 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Jeremy Wohl 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 | flatten 2 | ======= 3 | 4 | [![Go Reference](https://pkg.go.dev/badge/github.com/jeremywohl/flatten.svg)](https://pkg.go.dev/github.com/jeremywohl/flatten) 5 | 6 | **Note: development continues in [v2](/v2)** 7 | 8 | Flatten makes flat, one-dimensional maps from arbitrarily nested ones. 9 | 10 | It turns map keys into compound 11 | names, in four default styles: dotted (`a.b.1.c`), path-like (`a/b/1/c`), Rails (`a[b][1][c]`), or with underscores (`a_b_1_c`). Alternatively, you can pass a custom style. 12 | 13 | It takes input as either JSON strings or 14 | Go structures. It knows how to traverse these JSON types: objects/maps, arrays and scalars. 15 | 16 | You can flatten JSON strings. 17 | 18 | ```go 19 | nested := `{ 20 | "one": { 21 | "two": [ 22 | "2a", 23 | "2b" 24 | ] 25 | }, 26 | "side": "value" 27 | }` 28 | 29 | flat, err := flatten.FlattenString(nested, "", flatten.DotStyle) 30 | 31 | // output: `{ "one.two.0": "2a", "one.two.1": "2b", "side": "value" }` 32 | ``` 33 | 34 | Or Go maps directly. 35 | 36 | ```go 37 | nested := map[string]interface{}{ 38 | "a": "b", 39 | "c": map[string]interface{}{ 40 | "d": "e", 41 | "f": "g", 42 | }, 43 | "z": 1.4567, 44 | } 45 | 46 | flat, err := flatten.Flatten(nested, "", flatten.RailsStyle) 47 | 48 | // output: 49 | // map[string]interface{}{ 50 | // "a": "b", 51 | // "c[d]": "e", 52 | // "c[f]": "g", 53 | // "z": 1.4567, 54 | // } 55 | ``` 56 | 57 | Let's try a custom style, with the first example above. 58 | 59 | ```go 60 | emdash := flatten.SeparatorStyle{Middle: "--"} 61 | flat, err := flatten.FlattenString(nested, "", emdash) 62 | 63 | // output: `{ "one--two--0": "2a", "one--two--1": "2b", "side": "value" }` 64 | ``` 65 | 66 | See [godoc](https://godoc.org/github.com/jeremywohl/flatten) for API. 67 | -------------------------------------------------------------------------------- /TODO: -------------------------------------------------------------------------------- 1 | test: all Go scalar types 2 | todo: initial list vs map 3 | todo: fail properly with alternate types 4 | todo: support structs and pointers? 5 | -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Flatten makes flat, one-dimensional maps from arbitrarily nested ones. 2 | // 3 | // It turns map keys into compound 4 | // names, in four default styles: dotted (`a.b.1.c`), path-like (`a/b/1/c`), Rails (`a[b][1][c]`), 5 | // or with underscores (`a_b_1_c`). Alternatively, you can pass a custom style. 6 | // 7 | // It takes input as either JSON strings or 8 | // Go structures. It knows how to traverse these JSON types: objects/maps, arrays and scalars. 9 | // 10 | // You can flatten JSON strings. 11 | // 12 | // nested := `{ 13 | // "one": { 14 | // "two": [ 15 | // "2a", 16 | // "2b" 17 | // ] 18 | // }, 19 | // "side": "value" 20 | // }` 21 | // 22 | // flat, err := flatten.FlattenString(nested, "", flatten.DotStyle) 23 | // 24 | // // output: `{ "one.two.0": "2a", "one.two.1": "2b", "side": "value" }` 25 | // 26 | // Or Go maps directly. 27 | // 28 | // nested := map[string]interface{}{ 29 | // "a": "b", 30 | // "c": map[string]interface{}{ 31 | // "d": "e", 32 | // "f": "g", 33 | // }, 34 | // "z": 1.4567, 35 | // } 36 | // 37 | // flat, err := flatten.Flatten(nested, "", flatten.RailsStyle) 38 | // 39 | // // output: 40 | // // map[string]interface{}{ 41 | // // "a": "b", 42 | // // "c[d]": "e", 43 | // // "c[f]": "g", 44 | // // "z": 1.4567, 45 | // // } 46 | // 47 | // Let's try a custom style, with the first example above. 48 | // 49 | // emdash := flatten.SeparatorStyle{Middle: "--"} 50 | // flat, err := flatten.FlattenString(nested, "", emdash) 51 | // 52 | // // output: `{ "one--two--0": "2a", "one--two--1": "2b", "side": "value" }` 53 | // 54 | package flatten 55 | -------------------------------------------------------------------------------- /flatten.go: -------------------------------------------------------------------------------- 1 | package flatten 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "regexp" 7 | "strconv" 8 | ) 9 | 10 | // The style of keys. If there is an input with two 11 | // nested keys "f" and "g", with "f" at the root, 12 | // { "f": { "g": ... } } 13 | // the output will be the concatenation 14 | // f{Middle}{Before}g{After}... 15 | // Any struct element may be blank. 16 | // If you use Middle, you will probably leave Before & After blank, and vice-versa. 17 | // See examples in flatten_test.go and the "Default styles" here. 18 | type SeparatorStyle struct { 19 | Before string // Prepend to key 20 | Middle string // Add between keys 21 | After string // Append to key 22 | } 23 | 24 | // Default styles 25 | var ( 26 | // Separate nested key components with dots, e.g. "a.b.1.c.d" 27 | DotStyle = SeparatorStyle{Middle: "."} 28 | 29 | // Separate with path-like slashes, e.g. a/b/1/c/d 30 | PathStyle = SeparatorStyle{Middle: "/"} 31 | 32 | // Separate ala Rails, e.g. "a[b][c][1][d]" 33 | RailsStyle = SeparatorStyle{Before: "[", After: "]"} 34 | 35 | // Separate with underscores, e.g. "a_b_1_c_d" 36 | UnderscoreStyle = SeparatorStyle{Middle: "_"} 37 | ) 38 | 39 | // Nested input must be a map or slice 40 | var NotValidInputError = errors.New("Not a valid input: map or slice") 41 | 42 | // Flatten generates a flat map from a nested one. The original may include values of type map, slice and scalar, 43 | // but not struct. Keys in the flat map will be a compound of descending map keys and slice iterations. 44 | // The presentation of keys is set by style. A prefix is joined to each key. 45 | func Flatten(nested map[string]interface{}, prefix string, style SeparatorStyle) (map[string]interface{}, error) { 46 | flatmap := make(map[string]interface{}) 47 | 48 | err := flatten(true, flatmap, nested, prefix, style) 49 | if err != nil { 50 | return nil, err 51 | } 52 | 53 | return flatmap, nil 54 | } 55 | 56 | // JSON nested input must be a map 57 | var NotValidJsonInputError = errors.New("Not a valid input, must be a map") 58 | 59 | var isJsonMap = regexp.MustCompile(`^\s*\{`) 60 | 61 | // FlattenString generates a flat JSON map from a nested one. Keys in the flat map will be a compound of 62 | // descending map keys and slice iterations. The presentation of keys is set by style. A prefix is joined 63 | // to each key. 64 | func FlattenString(nestedstr, prefix string, style SeparatorStyle) (string, error) { 65 | if !isJsonMap.MatchString(nestedstr) { 66 | return "", NotValidJsonInputError 67 | } 68 | 69 | var nested map[string]interface{} 70 | err := json.Unmarshal([]byte(nestedstr), &nested) 71 | if err != nil { 72 | return "", err 73 | } 74 | 75 | flatmap, err := Flatten(nested, prefix, style) 76 | if err != nil { 77 | return "", err 78 | } 79 | 80 | flatb, err := json.Marshal(&flatmap) 81 | if err != nil { 82 | return "", err 83 | } 84 | 85 | return string(flatb), nil 86 | } 87 | 88 | func flatten(top bool, flatMap map[string]interface{}, nested interface{}, prefix string, style SeparatorStyle) error { 89 | assign := func(newKey string, v interface{}) error { 90 | switch v.(type) { 91 | case map[string]interface{}, []interface{}: 92 | if err := flatten(false, flatMap, v, newKey, style); err != nil { 93 | return err 94 | } 95 | default: 96 | flatMap[newKey] = v 97 | } 98 | 99 | return nil 100 | } 101 | 102 | switch nested.(type) { 103 | case map[string]interface{}: 104 | for k, v := range nested.(map[string]interface{}) { 105 | newKey := enkey(top, prefix, k, style) 106 | assign(newKey, v) 107 | } 108 | case []interface{}: 109 | for i, v := range nested.([]interface{}) { 110 | newKey := enkey(top, prefix, strconv.Itoa(i), style) 111 | assign(newKey, v) 112 | } 113 | default: 114 | return NotValidInputError 115 | } 116 | 117 | return nil 118 | } 119 | 120 | func enkey(top bool, prefix, subkey string, style SeparatorStyle) string { 121 | key := prefix 122 | 123 | if top { 124 | key += subkey 125 | } else { 126 | key += style.Before + style.Middle + subkey + style.After 127 | } 128 | 129 | return key 130 | } 131 | -------------------------------------------------------------------------------- /flatten_test.go: -------------------------------------------------------------------------------- 1 | package flatten 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "strings" 7 | "testing" 8 | "unicode" 9 | ) 10 | 11 | func TestFlatten(t *testing.T) { 12 | cases := []struct { 13 | test string 14 | want map[string]interface{} 15 | prefix string 16 | style SeparatorStyle 17 | }{ 18 | // 1 19 | { 20 | `{ 21 | "foo": { 22 | "jim":"bean" 23 | }, 24 | "fee": "bar", 25 | "n1": { 26 | "alist": [ 27 | "a", 28 | "b", 29 | "c", 30 | { 31 | "d": "other", 32 | "e": "another" 33 | } 34 | ] 35 | }, 36 | "number": 1.4567, 37 | "bool": true 38 | }`, 39 | map[string]interface{}{ 40 | "foo.jim": "bean", 41 | "fee": "bar", 42 | "n1.alist.0": "a", 43 | "n1.alist.1": "b", 44 | "n1.alist.2": "c", 45 | "n1.alist.3.d": "other", 46 | "n1.alist.3.e": "another", 47 | "number": 1.4567, 48 | "bool": true, 49 | }, 50 | "", 51 | DotStyle, 52 | }, 53 | // 2 54 | { 55 | `{ 56 | "foo": { 57 | "jim":"bean" 58 | }, 59 | "fee": "bar", 60 | "n1": { 61 | "alist": [ 62 | "a", 63 | "b", 64 | "c", 65 | { 66 | "d": "other", 67 | "e": "another" 68 | } 69 | ] 70 | } 71 | }`, 72 | map[string]interface{}{ 73 | "foo[jim]": "bean", 74 | "fee": "bar", 75 | "n1[alist][0]": "a", 76 | "n1[alist][1]": "b", 77 | "n1[alist][2]": "c", 78 | "n1[alist][3][d]": "other", 79 | "n1[alist][3][e]": "another", 80 | }, 81 | "", 82 | RailsStyle, 83 | }, 84 | // 3 85 | { 86 | `{ 87 | "foo": { 88 | "jim":"bean" 89 | }, 90 | "fee": "bar", 91 | "n1": { 92 | "alist": [ 93 | "a", 94 | "b", 95 | "c", 96 | { 97 | "d": "other", 98 | "e": "another" 99 | } 100 | ] 101 | }, 102 | "number": 1.4567, 103 | "bool": true 104 | }`, 105 | map[string]interface{}{ 106 | "foo/jim": "bean", 107 | "fee": "bar", 108 | "n1/alist/0": "a", 109 | "n1/alist/1": "b", 110 | "n1/alist/2": "c", 111 | "n1/alist/3/d": "other", 112 | "n1/alist/3/e": "another", 113 | "number": 1.4567, 114 | "bool": true, 115 | }, 116 | "", 117 | PathStyle, 118 | }, 119 | // 4 120 | { 121 | `{ "a": { "b": "c" }, "e": "f" }`, 122 | map[string]interface{}{ 123 | "p:a.b": "c", 124 | "p:e": "f", 125 | }, 126 | "p:", 127 | DotStyle, 128 | }, 129 | // 5 130 | { 131 | `{ 132 | "foo": { 133 | "jim":"bean" 134 | }, 135 | "fee": "bar", 136 | "n1": { 137 | "alist": [ 138 | "a", 139 | "b", 140 | "c", 141 | { 142 | "d": "other", 143 | "e": "another" 144 | } 145 | ] 146 | }, 147 | "number": 1.4567, 148 | "bool": true 149 | }`, 150 | map[string]interface{}{ 151 | "foo_jim": "bean", 152 | "fee": "bar", 153 | "n1_alist_0": "a", 154 | "n1_alist_1": "b", 155 | "n1_alist_2": "c", 156 | "n1_alist_3_d": "other", 157 | "n1_alist_3_e": "another", 158 | "number": 1.4567, 159 | "bool": true, 160 | }, 161 | "", 162 | UnderscoreStyle, 163 | }, 164 | // 6 -- try a prefix 165 | { 166 | `{ 167 | "foo": { 168 | "jim":"bean" 169 | }, 170 | "fee": "bar", 171 | "n1": { 172 | "alist": [ 173 | "a", 174 | "b", 175 | "c", 176 | { 177 | "d": "other", 178 | "e": "another" 179 | } 180 | ] 181 | }, 182 | "number": 1.4567, 183 | "bool": true 184 | }`, 185 | map[string]interface{}{ 186 | "flag-foo_jim": "bean", 187 | "flag-fee": "bar", 188 | "flag-n1_alist_0": "a", 189 | "flag-n1_alist_1": "b", 190 | "flag-n1_alist_2": "c", 191 | "flag-n1_alist_3_d": "other", 192 | "flag-n1_alist_3_e": "another", 193 | "flag-number": 1.4567, 194 | "flag-bool": true, 195 | }, 196 | "flag-", 197 | UnderscoreStyle, 198 | }, 199 | } 200 | 201 | for i, test := range cases { 202 | var m interface{} 203 | err := json.Unmarshal([]byte(test.test), &m) 204 | if err != nil { 205 | t.Errorf("%d: failed to unmarshal test: %v", i+1, err) 206 | continue 207 | } 208 | got, err := Flatten(m.(map[string]interface{}), test.prefix, test.style) 209 | if err != nil { 210 | t.Errorf("%d: failed to flatten: %v", i+1, err) 211 | continue 212 | } 213 | if !reflect.DeepEqual(got, test.want) { 214 | t.Errorf("%d: mismatch, got: %v wanted: %v", i+1, got, test.want) 215 | } 216 | } 217 | } 218 | 219 | func TestFlattenString(t *testing.T) { 220 | cases := []struct { 221 | test string 222 | want string 223 | prefix string 224 | style SeparatorStyle 225 | err error 226 | }{ 227 | // 1 228 | { 229 | `{ "a": "b" }`, 230 | `{ "a": "b" }`, 231 | "", 232 | DotStyle, 233 | nil, 234 | }, 235 | // 2 236 | { 237 | `{ "a": { "b" : { "c" : { "d" : "e" } } }, "number": 1.4567, "bool": true }`, 238 | `{ "a.b.c.d": "e", "bool": true, "number": 1.4567 }`, 239 | "", 240 | DotStyle, 241 | nil, 242 | }, 243 | // 3 244 | { 245 | `{ "a": { "b" : { "c" : { "d" : "e" } } }, "number": 1.4567, "bool": true }`, 246 | `{ "a/b/c/d": "e", "bool": true, "number": 1.4567 }`, 247 | "", 248 | PathStyle, 249 | nil, 250 | }, 251 | // 4 252 | { 253 | `{ "a": { "b" : { "c" : { "d" : "e" } } } }`, 254 | `{ "a--b--c--d": "e" }`, 255 | "", 256 | SeparatorStyle{Middle: "--"}, // emdash 257 | nil, 258 | }, 259 | // 5 260 | { 261 | `{ "a": { "b" : { "c" : { "d" : "e" } } } }`, 262 | `{ "a(b)(c)(d)": "e" }`, 263 | "", 264 | SeparatorStyle{Before: "(", After: ")"}, // paren groupings 265 | nil, 266 | }, 267 | // 6 -- with leading whitespace 268 | { 269 | ` 270 | { "a": { "b" : { "c" : { "d" : "e" } } } }`, 271 | `{ "a(b)(c)(d)": "e" }`, 272 | "", 273 | SeparatorStyle{Before: "(", After: ")"}, // paren groupings 274 | nil, 275 | }, 276 | 277 | // 278 | // Valid JSON text, but invalid for FlattenString 279 | // 280 | 281 | // 7 282 | { 283 | `[ "a": { "b": "c" }, "d" ]`, 284 | `bogus`, 285 | "", 286 | PathStyle, 287 | NotValidJsonInputError, 288 | }, 289 | // 8 290 | { 291 | ``, 292 | `bogus`, 293 | "", 294 | PathStyle, 295 | NotValidJsonInputError, 296 | }, 297 | // 9 298 | { 299 | `astring`, 300 | `bogus`, 301 | "", 302 | PathStyle, 303 | NotValidJsonInputError, 304 | }, 305 | // 10 306 | { 307 | `false`, 308 | `bogus`, 309 | "", 310 | PathStyle, 311 | NotValidJsonInputError, 312 | }, 313 | // 11 314 | { 315 | `42`, 316 | `bogus`, 317 | "", 318 | PathStyle, 319 | NotValidJsonInputError, 320 | }, 321 | // 12 -- prior to version 1.0.1, this was accepted & unmarshalled as an empty map, finally returning `{}`. 322 | { 323 | `null`, 324 | `{}`, 325 | "", 326 | PathStyle, 327 | NotValidJsonInputError, 328 | }, 329 | // 13 -- try a prefix 330 | { 331 | `{ "a": { "b" : { "c" : { "d" : "e" } } }, "number": 1.4567, "bool": true }`, 332 | `{ "flag-a.b.c.d": "e", "flag-bool": true, "flag-number": 1.4567 }`, 333 | "flag-", 334 | DotStyle, 335 | nil, 336 | }, 337 | } 338 | 339 | for i, test := range cases { 340 | got, err := FlattenString(test.test, test.prefix, test.style) 341 | if err != test.err { 342 | t.Errorf("%d: error mismatch, got: [%v], wanted: [%v]", i+1, err, test.err) 343 | continue 344 | } 345 | if err != nil { 346 | continue 347 | } 348 | 349 | nixws := func(r rune) rune { 350 | if unicode.IsSpace(r) { 351 | return -1 352 | } 353 | return r 354 | } 355 | 356 | if got != strings.Map(nixws, test.want) { 357 | t.Errorf("%d: mismatch, got: %v wanted: %v", i+1, got, test.want) 358 | } 359 | } 360 | } 361 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jeremywohl/flatten 2 | -------------------------------------------------------------------------------- /v2/README.md: -------------------------------------------------------------------------------- 1 | flatten 2 | ======= 3 | 4 | [![Go Reference](https://pkg.go.dev/badge/pkg.go.dev/github.com/jeremywohl/flatten/v2.svg)](https://pkg.go.dev/pkg.go.dev/github.com/jeremywohl/flatten/v2) 5 | 6 | Flatten makes flat, one-dimensional maps from arbitrarily nested ones. 7 | 8 | It turns map keys into compound 9 | names, in four default styles: dotted (`a.b.1.c`), path-like (`a/b/1/c`), Rails (`a[b][1][c]`), or with underscores (`a_b_1_c`). Alternatively, you can pass a custom style. 10 | 11 | It takes input as either JSON strings or 12 | Go structures. It knows how to traverse these JSON types: objects/maps, arrays and scalars. 13 | 14 | You can flatten JSON strings. 15 | 16 | ```go 17 | nested := `{ 18 | "one": { 19 | "two": [ 20 | "2a", 21 | "2b" 22 | ] 23 | }, 24 | "side": "value" 25 | }` 26 | 27 | flat, err := flatten.FlattenString(nested, "", flatten.DotStyle) 28 | 29 | // output: `{ "one.two.0": "2a", "one.two.1": "2b", "side": "value" }` 30 | ``` 31 | 32 | Or Go maps directly. 33 | 34 | ```go 35 | nested := map[string]interface{}{ 36 | "a": "b", 37 | "c": map[string]interface{}{ 38 | "d": "e", 39 | "f": "g", 40 | }, 41 | "z": 1.4567, 42 | } 43 | 44 | flat, err := flatten.Flatten(nested, "", flatten.RailsStyle) 45 | 46 | // output: 47 | // map[string]interface{}{ 48 | // "a": "b", 49 | // "c[d]": "e", 50 | // "c[f]": "g", 51 | // "z": 1.4567, 52 | // } 53 | ``` 54 | 55 | Let's try a custom style, with the first example above. 56 | 57 | ```go 58 | emdash := flatten.SeparatorStyle{Middle: "--"} 59 | flat, err := flatten.FlattenString(nested, "", emdash) 60 | 61 | // output: `{ "one--two--0": "2a", "one--two--1": "2b", "side": "value" }` 62 | ``` 63 | 64 | See [godoc](https://godoc.org/github.com/jeremywohl/flatten) for API. 65 | -------------------------------------------------------------------------------- /v2/doc.go: -------------------------------------------------------------------------------- 1 | // Flatten makes flat, one-dimensional maps from arbitrarily nested ones. 2 | // 3 | // It turns map keys into compound 4 | // names, in four default styles: dotted (`a.b.1.c`), path-like (`a/b/1/c`), Rails (`a[b][1][c]`), 5 | // or with underscores (`a_b_1_c`). Alternatively, you can pass a custom style. 6 | // 7 | // It takes input as either JSON strings or 8 | // Go structures. It knows how to traverse these JSON types: objects/maps, arrays and scalars. 9 | // 10 | // You can flatten JSON strings. 11 | // 12 | // nested := `{ 13 | // "one": { 14 | // "two": [ 15 | // "2a", 16 | // "2b" 17 | // ] 18 | // }, 19 | // "side": "value" 20 | // }` 21 | // 22 | // flat, err := flatten.FlattenString(nested, "", flatten.DotStyle) 23 | // 24 | // // output: `{ "one.two.0": "2a", "one.two.1": "2b", "side": "value" }` 25 | // 26 | // Or Go maps directly. 27 | // 28 | // nested := map[string]interface{}{ 29 | // "a": "b", 30 | // "c": map[string]interface{}{ 31 | // "d": "e", 32 | // "f": "g", 33 | // }, 34 | // "z": 1.4567, 35 | // } 36 | // 37 | // flat, err := flatten.Flatten(nested, "", flatten.RailsStyle) 38 | // 39 | // // output: 40 | // // map[string]interface{}{ 41 | // // "a": "b", 42 | // // "c[d]": "e", 43 | // // "c[f]": "g", 44 | // // "z": 1.4567, 45 | // // } 46 | // 47 | // Let's try a custom style, with the first example above. 48 | // 49 | // emdash := flatten.SeparatorStyle{Middle: "--"} 50 | // flat, err := flatten.FlattenString(nested, "", emdash) 51 | // 52 | // // output: `{ "one--two--0": "2a", "one--two--1": "2b", "side": "value" }` 53 | // 54 | package flatten 55 | -------------------------------------------------------------------------------- /v2/flatten.go: -------------------------------------------------------------------------------- 1 | package flatten 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "regexp" 7 | "strconv" 8 | ) 9 | 10 | // The style of keys. If there is an input with two 11 | // nested keys "f" and "g", with "f" at the root, 12 | // { "f": { "g": ... } } 13 | // the output will be the concatenation 14 | // f{Middle}{Before}g{After}... 15 | // Any struct element may be blank. 16 | // If you use Middle, you will probably leave Before & After blank, and vice-versa. 17 | // See examples in flatten_test.go and the "Default styles" here. 18 | type SeparatorStyle struct { 19 | Before string // Prepend to key 20 | Middle string // Add between keys 21 | After string // Append to key 22 | } 23 | 24 | // Default styles 25 | var ( 26 | // Separate nested key components with dots, e.g. "a.b.1.c.d" 27 | DotStyle = SeparatorStyle{Middle: "."} 28 | 29 | // Separate with path-like slashes, e.g. a/b/1/c/d 30 | PathStyle = SeparatorStyle{Middle: "/"} 31 | 32 | // Separate ala Rails, e.g. "a[b][c][1][d]" 33 | RailsStyle = SeparatorStyle{Before: "[", After: "]"} 34 | 35 | // Separate with underscores, e.g. "a_b_1_c_d" 36 | UnderscoreStyle = SeparatorStyle{Middle: "_"} 37 | ) 38 | 39 | // Nested input must be a map or slice 40 | var NotValidInputError = errors.New("Not a valid input: map or slice") 41 | 42 | // Flatten generates a flat map from a nested one. The original may include values of type map, slice and scalar, 43 | // but not struct. Keys in the flat map will be a compound of descending map keys and slice iterations. 44 | // The presentation of keys is set by style. A prefix is joined to each key. 45 | func Flatten(nested map[string]interface{}, prefix string, style SeparatorStyle) (map[string]interface{}, error) { 46 | flatmap := make(map[string]interface{}) 47 | 48 | err := flatten(true, flatmap, nested, prefix, style) 49 | if err != nil { 50 | return nil, err 51 | } 52 | 53 | return flatmap, nil 54 | } 55 | 56 | // JSON nested input must be a map 57 | var NotValidJsonInputError = errors.New("Not a valid input, must be a map") 58 | 59 | var isJsonMap = regexp.MustCompile(`^\s*\{`) 60 | 61 | // FlattenString generates a flat JSON map from a nested one. Keys in the flat map will be a compound of 62 | // descending map keys and slice iterations. The presentation of keys is set by style. A prefix is joined 63 | // to each key. 64 | func FlattenString(nestedstr, prefix string, style SeparatorStyle) (string, error) { 65 | if !isJsonMap.MatchString(nestedstr) { 66 | return "", NotValidJsonInputError 67 | } 68 | 69 | var nested map[string]interface{} 70 | err := json.Unmarshal([]byte(nestedstr), &nested) 71 | if err != nil { 72 | return "", err 73 | } 74 | 75 | flatmap, err := Flatten(nested, prefix, style) 76 | if err != nil { 77 | return "", err 78 | } 79 | 80 | flatb, err := json.Marshal(&flatmap) 81 | if err != nil { 82 | return "", err 83 | } 84 | 85 | return string(flatb), nil 86 | } 87 | 88 | func flatten(top bool, flatMap map[string]interface{}, nested interface{}, prefix string, style SeparatorStyle) error { 89 | assign := func(newKey string, v interface{}) error { 90 | switch v.(type) { 91 | case map[string]interface{}, []interface{}: 92 | if err := flatten(false, flatMap, v, newKey, style); err != nil { 93 | return err 94 | } 95 | default: 96 | flatMap[newKey] = v 97 | } 98 | 99 | return nil 100 | } 101 | 102 | switch nested.(type) { 103 | case map[string]interface{}: 104 | for k, v := range nested.(map[string]interface{}) { 105 | newKey := enkey(top, prefix, k, style) 106 | assign(newKey, v) 107 | } 108 | case []interface{}: 109 | for i, v := range nested.([]interface{}) { 110 | newKey := enkey(top, prefix, strconv.Itoa(i), style) 111 | assign(newKey, v) 112 | } 113 | default: 114 | return NotValidInputError 115 | } 116 | 117 | return nil 118 | } 119 | 120 | func enkey(top bool, prefix, subkey string, style SeparatorStyle) string { 121 | key := prefix 122 | 123 | if top { 124 | key += subkey 125 | } else { 126 | key += style.Before + style.Middle + subkey + style.After 127 | } 128 | 129 | return key 130 | } 131 | -------------------------------------------------------------------------------- /v2/flatten_test.go: -------------------------------------------------------------------------------- 1 | package flatten 2 | 3 | import ( 4 | "encoding/json" 5 | "reflect" 6 | "strings" 7 | "testing" 8 | "unicode" 9 | ) 10 | 11 | func TestFlatten(t *testing.T) { 12 | cases := []struct { 13 | test string 14 | want map[string]interface{} 15 | prefix string 16 | style SeparatorStyle 17 | }{ 18 | // 1 19 | { 20 | `{ 21 | "foo": { 22 | "jim":"bean" 23 | }, 24 | "fee": "bar", 25 | "n1": { 26 | "alist": [ 27 | "a", 28 | "b", 29 | "c", 30 | { 31 | "d": "other", 32 | "e": "another" 33 | } 34 | ] 35 | }, 36 | "number": 1.4567, 37 | "bool": true 38 | }`, 39 | map[string]interface{}{ 40 | "foo.jim": "bean", 41 | "fee": "bar", 42 | "n1.alist.0": "a", 43 | "n1.alist.1": "b", 44 | "n1.alist.2": "c", 45 | "n1.alist.3.d": "other", 46 | "n1.alist.3.e": "another", 47 | "number": 1.4567, 48 | "bool": true, 49 | }, 50 | "", 51 | DotStyle, 52 | }, 53 | // 2 54 | { 55 | `{ 56 | "foo": { 57 | "jim":"bean" 58 | }, 59 | "fee": "bar", 60 | "n1": { 61 | "alist": [ 62 | "a", 63 | "b", 64 | "c", 65 | { 66 | "d": "other", 67 | "e": "another" 68 | } 69 | ] 70 | } 71 | }`, 72 | map[string]interface{}{ 73 | "foo[jim]": "bean", 74 | "fee": "bar", 75 | "n1[alist][0]": "a", 76 | "n1[alist][1]": "b", 77 | "n1[alist][2]": "c", 78 | "n1[alist][3][d]": "other", 79 | "n1[alist][3][e]": "another", 80 | }, 81 | "", 82 | RailsStyle, 83 | }, 84 | // 3 85 | { 86 | `{ 87 | "foo": { 88 | "jim":"bean" 89 | }, 90 | "fee": "bar", 91 | "n1": { 92 | "alist": [ 93 | "a", 94 | "b", 95 | "c", 96 | { 97 | "d": "other", 98 | "e": "another" 99 | } 100 | ] 101 | }, 102 | "number": 1.4567, 103 | "bool": true 104 | }`, 105 | map[string]interface{}{ 106 | "foo/jim": "bean", 107 | "fee": "bar", 108 | "n1/alist/0": "a", 109 | "n1/alist/1": "b", 110 | "n1/alist/2": "c", 111 | "n1/alist/3/d": "other", 112 | "n1/alist/3/e": "another", 113 | "number": 1.4567, 114 | "bool": true, 115 | }, 116 | "", 117 | PathStyle, 118 | }, 119 | // 4 120 | { 121 | `{ "a": { "b": "c" }, "e": "f" }`, 122 | map[string]interface{}{ 123 | "p:a.b": "c", 124 | "p:e": "f", 125 | }, 126 | "p:", 127 | DotStyle, 128 | }, 129 | // 5 130 | { 131 | `{ 132 | "foo": { 133 | "jim":"bean" 134 | }, 135 | "fee": "bar", 136 | "n1": { 137 | "alist": [ 138 | "a", 139 | "b", 140 | "c", 141 | { 142 | "d": "other", 143 | "e": "another" 144 | } 145 | ] 146 | }, 147 | "number": 1.4567, 148 | "bool": true 149 | }`, 150 | map[string]interface{}{ 151 | "foo_jim": "bean", 152 | "fee": "bar", 153 | "n1_alist_0": "a", 154 | "n1_alist_1": "b", 155 | "n1_alist_2": "c", 156 | "n1_alist_3_d": "other", 157 | "n1_alist_3_e": "another", 158 | "number": 1.4567, 159 | "bool": true, 160 | }, 161 | "", 162 | UnderscoreStyle, 163 | }, 164 | // 6 -- try a prefix 165 | { 166 | `{ 167 | "foo": { 168 | "jim":"bean" 169 | }, 170 | "fee": "bar", 171 | "n1": { 172 | "alist": [ 173 | "a", 174 | "b", 175 | "c", 176 | { 177 | "d": "other", 178 | "e": "another" 179 | } 180 | ] 181 | }, 182 | "number": 1.4567, 183 | "bool": true 184 | }`, 185 | map[string]interface{}{ 186 | "flag-foo_jim": "bean", 187 | "flag-fee": "bar", 188 | "flag-n1_alist_0": "a", 189 | "flag-n1_alist_1": "b", 190 | "flag-n1_alist_2": "c", 191 | "flag-n1_alist_3_d": "other", 192 | "flag-n1_alist_3_e": "another", 193 | "flag-number": 1.4567, 194 | "flag-bool": true, 195 | }, 196 | "flag-", 197 | UnderscoreStyle, 198 | }, 199 | } 200 | 201 | for i, test := range cases { 202 | var m interface{} 203 | err := json.Unmarshal([]byte(test.test), &m) 204 | if err != nil { 205 | t.Errorf("%d: failed to unmarshal test: %v", i+1, err) 206 | continue 207 | } 208 | got, err := Flatten(m.(map[string]interface{}), test.prefix, test.style) 209 | if err != nil { 210 | t.Errorf("%d: failed to flatten: %v", i+1, err) 211 | continue 212 | } 213 | if !reflect.DeepEqual(got, test.want) { 214 | t.Errorf("%d: mismatch, got: %v wanted: %v", i+1, got, test.want) 215 | } 216 | } 217 | } 218 | 219 | func TestFlattenString(t *testing.T) { 220 | cases := []struct { 221 | test string 222 | want string 223 | prefix string 224 | style SeparatorStyle 225 | err error 226 | }{ 227 | // 1 228 | { 229 | `{ "a": "b" }`, 230 | `{ "a": "b" }`, 231 | "", 232 | DotStyle, 233 | nil, 234 | }, 235 | // 2 236 | { 237 | `{ "a": { "b" : { "c" : { "d" : "e" } } }, "number": 1.4567, "bool": true }`, 238 | `{ "a.b.c.d": "e", "bool": true, "number": 1.4567 }`, 239 | "", 240 | DotStyle, 241 | nil, 242 | }, 243 | // 3 244 | { 245 | `{ "a": { "b" : { "c" : { "d" : "e" } } }, "number": 1.4567, "bool": true }`, 246 | `{ "a/b/c/d": "e", "bool": true, "number": 1.4567 }`, 247 | "", 248 | PathStyle, 249 | nil, 250 | }, 251 | // 4 252 | { 253 | `{ "a": { "b" : { "c" : { "d" : "e" } } } }`, 254 | `{ "a--b--c--d": "e" }`, 255 | "", 256 | SeparatorStyle{Middle: "--"}, // emdash 257 | nil, 258 | }, 259 | // 5 260 | { 261 | `{ "a": { "b" : { "c" : { "d" : "e" } } } }`, 262 | `{ "a(b)(c)(d)": "e" }`, 263 | "", 264 | SeparatorStyle{Before: "(", After: ")"}, // paren groupings 265 | nil, 266 | }, 267 | // 6 -- with leading whitespace 268 | { 269 | ` 270 | { "a": { "b" : { "c" : { "d" : "e" } } } }`, 271 | `{ "a(b)(c)(d)": "e" }`, 272 | "", 273 | SeparatorStyle{Before: "(", After: ")"}, // paren groupings 274 | nil, 275 | }, 276 | 277 | // 278 | // Valid JSON text, but invalid for FlattenString 279 | // 280 | 281 | // 7 282 | { 283 | `[ "a": { "b": "c" }, "d" ]`, 284 | `bogus`, 285 | "", 286 | PathStyle, 287 | NotValidJsonInputError, 288 | }, 289 | // 8 290 | { 291 | ``, 292 | `bogus`, 293 | "", 294 | PathStyle, 295 | NotValidJsonInputError, 296 | }, 297 | // 9 298 | { 299 | `astring`, 300 | `bogus`, 301 | "", 302 | PathStyle, 303 | NotValidJsonInputError, 304 | }, 305 | // 10 306 | { 307 | `false`, 308 | `bogus`, 309 | "", 310 | PathStyle, 311 | NotValidJsonInputError, 312 | }, 313 | // 11 314 | { 315 | `42`, 316 | `bogus`, 317 | "", 318 | PathStyle, 319 | NotValidJsonInputError, 320 | }, 321 | // 12 -- prior to version 1.0.1, this was accepted & unmarshalled as an empty map, finally returning `{}`. 322 | { 323 | `null`, 324 | `{}`, 325 | "", 326 | PathStyle, 327 | NotValidJsonInputError, 328 | }, 329 | // 13 -- try a prefix 330 | { 331 | `{ "a": { "b" : { "c" : { "d" : "e" } } }, "number": 1.4567, "bool": true }`, 332 | `{ "flag-a.b.c.d": "e", "flag-bool": true, "flag-number": 1.4567 }`, 333 | "flag-", 334 | DotStyle, 335 | nil, 336 | }, 337 | } 338 | 339 | for i, test := range cases { 340 | got, err := FlattenString(test.test, test.prefix, test.style) 341 | if err != test.err { 342 | t.Errorf("%d: error mismatch, got: [%v], wanted: [%v]", i+1, err, test.err) 343 | continue 344 | } 345 | if err != nil { 346 | continue 347 | } 348 | 349 | nixws := func(r rune) rune { 350 | if unicode.IsSpace(r) { 351 | return -1 352 | } 353 | return r 354 | } 355 | 356 | if got != strings.Map(nixws, test.want) { 357 | t.Errorf("%d: mismatch, got: %v wanted: %v", i+1, got, test.want) 358 | } 359 | } 360 | } 361 | -------------------------------------------------------------------------------- /v2/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/jeremywohl/flatten/v2 2 | --------------------------------------------------------------------------------