├── 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 | [](https://travis-ci.org/sbabiv/xml2map)
2 | [](https://coveralls.io/github/sbabiv/xml2map?branch=master)
3 | [](https://goreportcard.com/report/github.com/sbabiv/xml2map)
4 | [](https://godoc.org/github.com/sbabiv/xml2map)
5 | [](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 |
--------------------------------------------------------------------------------