├── go.mod ├── go.sum ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── decoder.go └── decoder_test.go /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/sbabiv/xml2map 2 | 3 | go 1.17 4 | 5 | require github.com/google/gofuzz v1.2.0 6 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= 2 | github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, build with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | .idea/ 14 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: go 2 | sudo: false 3 | go: 4 | - "1.11.x" 5 | - master 6 | - tip 7 | 8 | script: go test ./... 9 | 10 | before_install: 11 | - go get github.com/mattn/goveralls 12 | script: 13 | - $GOPATH/bin/goveralls -service=travis-ci 14 | 15 | 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Babiv Sergey 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 | [![Build Status](https://travis-ci.org/sbabiv/xml2map.svg?branch=master)](https://travis-ci.org/sbabiv/xml2map) 2 | [![Coverage Status](https://coveralls.io/repos/github/sbabiv/xml2map/badge.svg?branch=master)](https://coveralls.io/github/sbabiv/xml2map?branch=master) 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/sbabiv/xml2map)](https://goreportcard.com/report/github.com/sbabiv/xml2map) 4 | [![GoDoc](https://godoc.org/github.com/sbabiv/xml2map?status.svg)](https://godoc.org/github.com/sbabiv/xml2map) 5 | [![Awesome](https://cdn.rawgit.com/sindresorhus/awesome/d7305f38d29fed78fa85652e3a63e154dd8e8829/media/badge.svg)](https://github.com/avelino/awesome-go#xml) 6 | 7 | # xml2map 8 | XML to MAP converter written Golang 9 | 10 | Sometimes there is a need for the representation of previously unknown structures. Such a universal representation is usually a string in the form of JSON, XML, or the structure of data map. similar to the map[string]interface{} or map[interface{}]interface{}. 11 | 12 | This is a converter from the old XML format to map[string]interface{} Golang 13 | 14 | For example, the map[string]interface{} can be used as a universal type in template generation. Golang "text/template" and etc. 15 | 16 | ## Getting started 17 | 18 | #### 1. install 19 | 20 | ``` sh 21 | go get -u github.com/sbabiv/xml2map 22 | ``` 23 | 24 | Or, using dep: 25 | 26 | ``` sh 27 | dep ensure -add github.com/sbabiv/xml2map 28 | ``` 29 | 30 | 31 | #### 2. use it 32 | 33 | ```go 34 | 35 | func main() { 36 | data := ` 37 | 38 | 39 | CDA035B6-D453-4A17-B090-84295AE2DEC5 40 | moritz 41 | 7 42 | 43 | 1293 44 | 1255 45 | 1257 46 | 47 | 48 | 49 | 1634C644-975F-4302-8336-1EF1366EC6A4 50 | oliver 51 | 12 52 | 53 | hello 54 | 55 | white 56 | NY 57 | ` 58 | 59 | decoder := xml2map.NewDecoder(strings.NewReader(data)) 60 | result, err := decoder.Decode() 61 | 62 | if err != nil { 63 | fmt.Printf("%v\n", err) 64 | } else { 65 | fmt.Printf("%v\n", result) 66 | } 67 | 68 | v := result["container"]. 69 | (map[string]interface{})["cats"]. 70 | (map[string]interface{})["cat"]. 71 | ([]map[string]interface{})[0]["items"]. 72 | (map[string]interface{})["n"].([]string)[1] 73 | 74 | fmt.Printf("n[1]: %v\n", v) 75 | } 76 | 77 | ``` 78 | if you want to use your custom prefixes use the 79 | 80 | ``` 81 | NewDecoderWithPrefix(reader io.Reader, attrPrefix, textPrefix string) *Decoder 82 | ``` 83 | [Go Playground](https://play.golang.org/p/_n35DRTxTYF) 84 | 85 | ## Output 86 | 87 | ```go 88 | map[container:map[@uid:FA6666D9-EC9F-4DA3-9C3D-4B2460A4E1F6 @lifetime:2019-10-10T18:00:11 cats:map[cat:[map[id:CDA035B6-D453-4A17-B090-84295AE2DEC5 name:moritz age:7 items:map[n:[1293 1255 1257]]] map[id:1634C644-975F-4302-8336-1EF1366EC6A4 name:oliver age:12]] dog:map[@color:gray #text:hello]] color:white city:NY]] 89 | 90 | result: 1255 91 | ``` 92 | 93 | ## Benchmark 94 | 95 | 96 | ```go 97 | $ go test -bench=. -benchmem 98 | goos: darwin 99 | goarch: amd64 100 | pkg: github.com/sbabiv/xml2map 101 | BenchmarkDecoder-8 50000 29773 ns/op 15032 B/op 261 allocs/op 102 | PASS 103 | ok github.com/sbabiv/xml2map 1.805s 104 | ``` 105 | 106 | ## Licence 107 | [MIT](https://opensource.org/licenses/MIT) 108 | 109 | ## Author 110 | Babiv Sergey 111 | -------------------------------------------------------------------------------- /decoder.go: -------------------------------------------------------------------------------- 1 | package xml2map 2 | 3 | import ( 4 | "encoding/xml" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "path" 9 | "strings" 10 | ) 11 | 12 | const ( 13 | attrPrefix = "@" 14 | textPrefix = "#text" 15 | ) 16 | 17 | var ( 18 | //ErrInvalidDocument invalid document err 19 | ErrInvalidDocument = errors.New("invalid document") 20 | 21 | //ErrInvalidRoot data at the root level is invalid err 22 | ErrInvalidRoot = errors.New("data at the root level is invalid") 23 | ) 24 | 25 | type node struct { 26 | Parent *node 27 | Value map[string]interface{} 28 | Attrs []xml.Attr 29 | Label string 30 | Space string 31 | Text string 32 | HasMany bool 33 | } 34 | 35 | // Decoder instance 36 | type Decoder struct { 37 | r io.Reader 38 | attrPrefix string 39 | textPrefix string 40 | } 41 | 42 | // NewDecoder create new decoder instance 43 | func NewDecoder(reader io.Reader) *Decoder { 44 | return NewDecoderWithPrefix(reader, attrPrefix, textPrefix) 45 | } 46 | 47 | // NewDecoderWithPrefix create new decoder instance with custom attribute prefix and text prefix 48 | func NewDecoderWithPrefix(reader io.Reader, attrPrefix, textPrefix string) *Decoder { 49 | return &Decoder{r: reader, attrPrefix: attrPrefix, textPrefix: textPrefix} 50 | } 51 | 52 | //Decode xml string to map[string]interface{} 53 | func (d *Decoder) Decode() (map[string]interface{}, error) { 54 | decoder := xml.NewDecoder(d.r) 55 | n := &node{} 56 | stack := make([]*node, 0) 57 | 58 | for { 59 | token, err := decoder.Token() 60 | if err != nil && err != io.EOF { 61 | return nil, err 62 | } 63 | 64 | if token == nil { 65 | break 66 | } 67 | 68 | switch tok := token.(type) { 69 | case xml.StartElement: 70 | { 71 | label := tok.Name.Local 72 | if tok.Name.Space != "" { 73 | label = fmt.Sprintf("%s:%s", strings.ToLower(path.Base(tok.Name.Space)), tok.Name.Local) 74 | } 75 | n = &node{ 76 | Label: label, 77 | Space: tok.Name.Space, 78 | Parent: n, 79 | Value: map[string]interface{}{label: map[string]interface{}{}}, 80 | Attrs: tok.Attr, 81 | } 82 | 83 | setAttrs(n, &tok, d.attrPrefix) 84 | stack = append(stack, n) 85 | 86 | if n.Parent != nil { 87 | n.Parent.HasMany = true 88 | } 89 | } 90 | 91 | case xml.CharData: 92 | data := strings.TrimSpace(string(tok)) 93 | if len(stack) > 0 { 94 | stack[len(stack)-1].Text = data 95 | } else if len(data) > 0 { 96 | return nil, ErrInvalidRoot 97 | } 98 | 99 | case xml.EndElement: 100 | { 101 | length := len(stack) 102 | stack, n = stack[:length-1], stack[length-1] 103 | 104 | if !n.HasMany { 105 | if len(n.Attrs) > 0 { 106 | m := n.Value[n.Label].(map[string]interface{}) 107 | m[d.textPrefix] = n.Text 108 | } else { 109 | n.Value[n.Label] = n.Text 110 | } 111 | } 112 | 113 | if len(stack) == 0 { 114 | return n.Value, nil 115 | } 116 | 117 | setNodeValue(n) 118 | n = n.Parent 119 | } 120 | } 121 | } 122 | 123 | return nil, ErrInvalidDocument 124 | } 125 | 126 | func setAttrs(n *node, tok *xml.StartElement, attrPrefix string) { 127 | if len(tok.Attr) > 0 { 128 | m := make(map[string]interface{}) 129 | for _, attr := range tok.Attr { 130 | if len(attr.Name.Space) > 0 { 131 | m[attrPrefix+attr.Name.Space+":"+attr.Name.Local] = attr.Value 132 | } else { 133 | m[attrPrefix+attr.Name.Local] = attr.Value 134 | } 135 | } 136 | n.Value[tok.Name.Local] = m 137 | } 138 | } 139 | 140 | func setNodeValue(n *node) { 141 | if v, ok := n.Parent.Value[n.Parent.Label]; ok { 142 | m := v.(map[string]interface{}) 143 | if v, ok = m[n.Label]; ok { 144 | switch item := v.(type) { 145 | case string: 146 | m[n.Label] = []string{item, n.Value[n.Label].(string)} 147 | case []string: 148 | m[n.Label] = append(item, n.Value[n.Label].(string)) 149 | case map[string]interface{}: 150 | vm := getMap(n) 151 | if vm != nil { 152 | m[n.Label] = []map[string]interface{}{item, vm} 153 | } 154 | case []map[string]interface{}: 155 | vm := getMap(n) 156 | if vm != nil { 157 | m[n.Label] = append(item, vm) 158 | } 159 | } 160 | } else { 161 | m[n.Label] = n.Value[n.Label] 162 | } 163 | 164 | } else { 165 | n.Parent.Value[n.Parent.Label] = n.Value[n.Label] 166 | } 167 | } 168 | 169 | func getMap(node *node) map[string]interface{} { 170 | if v, ok := node.Value[node.Label]; ok { 171 | switch v.(type) { 172 | case string: 173 | return map[string]interface{}{node.Label: v} 174 | case map[string]interface{}: 175 | return node.Value[node.Label].(map[string]interface{}) 176 | } 177 | } 178 | 179 | return nil 180 | } 181 | -------------------------------------------------------------------------------- /decoder_test.go: -------------------------------------------------------------------------------- 1 | package xml2map 2 | 3 | import ( 4 | "fmt" 5 | "github.com/google/gofuzz" 6 | "strings" 7 | "testing" 8 | ) 9 | 10 | func BenchmarkDecoder(b *testing.B) { 11 | for n := 0; n < b.N; n++ { 12 | NewDecoder(strings.NewReader(` 13 | 14 | 15 | CDA035B6-D453-4A17-B090-84295AE2DEC5 16 | moritz 17 | 7 18 | 19 | 1293 20 | 1255 21 | 1257 22 | 23 | 24 | 25 | 1634C644-975F-4302-8336-1EF1366EC6A4 26 | oliver 27 | 12 28 | 29 | 30 | white 31 | NY 32 | `)).Decode() 33 | 34 | } 35 | } 36 | 37 | func TestStartAttrs(t *testing.T) { 38 | tests := []string{ 39 | ` 40 | white 41 | `, 42 | ` 43 | white 44 | `, 45 | ` 46 | white 47 | `, 48 | } 49 | 50 | for _, s := range tests { 51 | _, err := NewDecoder(strings.NewReader(s)).Decode() 52 | if err == nil { 53 | t.Fail() 54 | } 55 | } 56 | } 57 | 58 | func TestNs(t *testing.T) { 59 | m, err := NewDecoder(strings.NewReader( 60 | ``)).Decode() 62 | if err != nil { 63 | t.Errorf("m: %v, err: %v\n", m, err) 64 | } 65 | } 66 | 67 | func TestPars(t *testing.T) { 68 | m, err := NewDecoder(strings.NewReader( 69 | ` 70 | 71 | 1 72 | 2 73 | 3 74 | 75 | `)).Decode() 76 | 77 | if err != nil { 78 | t.Errorf("m: %v, err: %v\n", m, err) 79 | } 80 | } 81 | 82 | func TestFuzz1000(t *testing.T) { 83 | f := fuzz.New().NilChance(0).NumElements(1, 1000) 84 | var myMap map[string]int 85 | f.Fuzz(&myMap) 86 | 87 | for v := range myMap { 88 | m, err := NewDecoder(strings.NewReader(v)).Decode() 89 | if err == nil { 90 | fmt.Printf("m: %v", m) 91 | 92 | } 93 | } 94 | } 95 | 96 | func TestErrDecoder(t *testing.T) { 97 | m, err := NewDecoder(strings.NewReader( 98 | ` 99 | Smith 100 | John> 101 |
102 | 1310 Villa Street 103 | Mountain View 104 | CA 105 | 94041 106 |
107 | `)).Decode() 108 | 109 | if m == nil && err != nil { 110 | t.Logf("result: %v err: %v\n", m, err) 111 | } else { 112 | t.Errorf("err %v\n", err) 113 | } 114 | } 115 | 116 | func TestEmpty(t *testing.T) { 117 | tests := []string{"", " ", " ", ``, ` `, "\n"} 118 | 119 | for _, s := range tests { 120 | _, err := NewDecoder(strings.NewReader(s)).Decode() 121 | if err != ErrInvalidDocument { 122 | t.Fail() 123 | } 124 | } 125 | } 126 | 127 | func TestSpaces(t *testing.T) { 128 | m, err := NewDecoder(strings.NewReader(` 129 | data 130 | `)).Decode() 131 | 132 | if err != nil { 133 | t.Errorf("err %v\n", err) 134 | } else { 135 | if m["note"] != "data" { 136 | t.Errorf("data not found") 137 | } 138 | } 139 | } 140 | 141 | func TestInvalidStartIndex(t *testing.T) { 142 | _, err := NewDecoder(strings.NewReader(`d 143 | data 144 | `)).Decode() 145 | 146 | if err == nil || err.Error() != "data at the root level is invalid" { 147 | t.Fail() 148 | } 149 | } 150 | 151 | func TestDecode(t *testing.T) { 152 | m, err := NewDecoder(strings.NewReader( 153 | ` 154 | 155 | 156 | CDA035B6-D453-4A17-B090-84295AE2DEC5 157 | moritz 158 | 7 159 | 160 | 1293 161 | 1255 162 | 1257 163 | 164 | 165 | 166 | 1634C644-975F-4302-8336-1EF1366EC6A4 167 | oliver 168 | 12 169 | 170 | 1293 171 | 1255 172 | 1257 173 | 174 | 175 | hello 176 | 177 | white 178 | NY 179 | `)).Decode() 180 | 181 | if err != nil { 182 | t.Errorf("err: %v", err) 183 | } 184 | 185 | container := m["container"].(map[string]interface{}) 186 | if container["@uid"] != "FA6666D9-EC9F-4DA3-9C3D-4B2460A4E1F6" && container["lifetime"] != "2019-10-10T18:00:11" { 187 | t.Errorf("container attrs not exists") 188 | } else { 189 | cats := container["cats"].(map[string]interface{}) 190 | catsItems := cats["cat"].([]map[string]interface{}) 191 | if len(catsItems) != 2 { 192 | t.Errorf("cats slice != 2") 193 | } 194 | 195 | dog := cats["dog"].(map[string]interface{}) 196 | 197 | if dog["@color"] != "gray" || dog["#text"] != "hello" { 198 | t.Errorf("bad value or attr dog") 199 | } 200 | 201 | if container["color"] != "white" || container["city"] != "NY" { 202 | t.Errorf("bad value color") 203 | } 204 | 205 | cat := catsItems[0] 206 | if cat["id"] != "" && cat["name"] != "" && cat["age"] != "" { 207 | items := cat["items"].(map[string]interface{})["n"].([]string) 208 | if len(items) != 3 { 209 | t.Errorf("items len %v", len(items)) 210 | } 211 | } 212 | } 213 | } 214 | 215 | func TestWithPrefix(t *testing.T) { 216 | m, err := NewDecoderWithPrefix(strings.NewReader( 217 | ` 218 | 219 | 1 220 | 2 221 | 3 222 | 223 | `), "$", "#").Decode() 224 | 225 | if err != nil { 226 | t.Errorf("m: %v, err: %v\n", m, err) 227 | } 228 | 229 | customer := m["customer"].(map[string]interface{}) 230 | if customer["$id"] != "FA6666D9-EC9F-4DA3-9C3D-4B2460A4E1F6" && customer["$lifetime"] != "2019-10-10T18:00:11" { 231 | t.Errorf("customer tag attr not found") 232 | } else { 233 | items := customer["items"].(map[string]interface{}) 234 | if items["$id"] != "100" || items["$count"] != "3" { 235 | t.Errorf("items tag attr not found") 236 | } else { 237 | list := items["n"].([]map[string]interface{}) 238 | if len(list) != 3 { 239 | t.Errorf("list len %v", len(items)) 240 | } else { 241 | if list[1]["$id"] != "20" && list[1]["%"] != "2" { 242 | t.Errorf("invalid parse n element attr or text") 243 | } 244 | } 245 | } 246 | } 247 | } 248 | 249 | func TestWithNameSpace(t *testing.T) { 250 | m, err := NewDecoder(strings.NewReader( 251 | ` 252 | 253 | 254 | example.com RSS 255 | https://www.example.com/ 256 | A cool website 257 | 258 | Atom Title 259 | 260 | Cool Article 261 | https://www.example.com/cool-article 262 | https://www.example.com/cool-article 263 | Sun, 10 Dec 2017 05:00:00 GMT 264 | My cool article description 265 | 266 | 267 | `)).Decode() 268 | 269 | if err != nil { 270 | t.Errorf("m: %v, err: %v\n", m, err) 271 | } 272 | 273 | rss := m["rss"].(map[string]interface{})["channel"].(map[string]interface{}) 274 | if rss["atom:title"] != "Atom Title" { 275 | t.Errorf("invalid value for namespace node") 276 | } 277 | } 278 | --------------------------------------------------------------------------------