├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── exml.go
└── exml_test.go
/.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 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: go
2 |
3 | install:
4 | - go get gopkg.in/check.v1
5 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright (c) 2014, Luc Heinrich
2 | All rights reserved.
3 |
4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
5 |
6 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
7 |
8 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
9 |
10 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # exml [](https://drone.io/github.com/lucsky/go-exml/latest)
2 |
3 | The **exml** package provides an intuitive event based XML parsing API which sits on top of a standard Go ```encoding/xml/Decoder```, greatly simplifying the parsing code while retaining the raw speed and low memory overhead of the underlying stream engine, regardless of the size of the input. The module takes care of the complex tasks of maintaining contexts between event handlers allowing you to concentrate on dealing with the actual structure of the XML document.
4 |
5 | # Installation
6 |
7 | **HEAD:**
8 |
9 | ```go get github.com/lucsky/go-exml```
10 |
11 | **v3.1.1:**
12 |
13 | ```go get gopkg.in/lucsky/go-exml.v3```
14 |
15 | The third version of **exml** provides compile time callback safety at the cost of an **API CHANGE**. Ad hoc ```$text``` events have been replaced by the specific ```OnText``` and ```OnTextOf``` event registration methods. Also new in v3.1: custom xml.Decoder support, type attribute readers, typed assignment/appending shortcuts (AssignT and AppendT) and full API documentation. v3.1.1 fixes a major bug causing the handlers stack to become inconsistent when ignoring tags.
16 |
17 | **v2:**
18 |
19 | ```go get gopkg.in/lucsky/go-exml.v2```
20 |
21 | The second version of **exml** has a better implementation based on a dynamic handler tree, allowing global events (see example below), having lower memory usage and also being faster.
22 |
23 | **v1:**
24 |
25 | ```go get gopkg.in/lucsky/go-exml.v1```
26 |
27 | Initial (and naive) implementation based on a flat list of absolute event paths.
28 |
29 | # Usage
30 |
31 | The best way to illustrate how **exml** makes parsing very easy is to look at actual examples. Consider the following contrived sample document:
32 |
33 | ```xml
34 |
35 |
36 |
37 | Tim
38 | Cook
39 | Cupertino
40 |
41 |
42 | Steve
43 | Ballmer
44 | Redmond
45 |
46 |
47 | Mark
48 | Zuckerberg
49 | Menlo Park
50 |
51 |
52 | ```
53 |
54 | Here is a way to parse it into an array of contact objects using **exml**:
55 |
56 | ```go
57 | package main
58 |
59 | import (
60 | "fmt"
61 | "os"
62 |
63 | "gopkg.in/lucsky/go-exml.v3"
64 | )
65 |
66 | type AddressBook struct {
67 | Name string
68 | Contacts []*Contact
69 | }
70 |
71 | type Contact struct {
72 | FirstName string
73 | LastName string
74 | Address string
75 | }
76 |
77 | func main() {
78 | reader, _ := os.Open("example.xml")
79 | defer reader.Close()
80 |
81 | addressBook := AddressBook{}
82 | decoder := exml.NewDecoder(reader)
83 |
84 | decoder.On("address-book", func(attrs exml.Attrs) {
85 | addressBook.Name, _ = attrs.Get("name")
86 |
87 | decoder.On("contact", func(attrs exml.Attrs) {
88 | contact := &Contact{}
89 | addressBook.Contacts = append(addressBook.Contacts, contact)
90 |
91 | decoder.On("first-name", func(attrs exml.Attrs) {
92 | decoder.OnText(func(text exml.CharData) {
93 | contact.FirstName = string(text)
94 | })
95 | })
96 |
97 | decoder.On("last-name", func(attrs exml.Attrs) {
98 | decoder.OnText(func(text exml.CharData) {
99 | contact.LastName = string(text)
100 | })
101 | })
102 |
103 | decoder.On("address", func(attrs exml.Attrs) {
104 | decoder.OnText(func(text exml.CharData) {
105 | contact.Address = string(text)
106 | })
107 | })
108 | })
109 | })
110 |
111 | decoder.Run()
112 |
113 | fmt.Printf("Address book: %s\n", addressBook.Name)
114 | for _, c := range addressBook.Contacts {
115 | fmt.Printf("- %s %s @ %s\n", c.FirstName, c.LastName, c.Address)
116 | }
117 | }
118 | ```
119 |
120 | To reduce the amount and depth of event callbacks that you have to write, **exml** allows you to register handlers on **events paths**:
121 |
122 | ```go
123 | decoder.OnTextOf("address-book/contact/first-name", func(text exml.CharData) {
124 | fmt.Println("First name: ", string(text))
125 | })
126 |
127 | // This works too:
128 | decoder.On("address-book/contact", func(attrs exml.Attrs) {
129 | decoder.OnTextOf("last-name", func(text exml.CharData) {
130 | fmt.Println("Last name: ", string(text))
131 | })
132 | })
133 |
134 | // And this as well:
135 | decoder.On("address-book/contact/address", func(attrs exml.Attrs) {
136 | decoder.OnText(func(text exml.CharData) {
137 | fmt.Println("Address: ", string(text))
138 | })
139 | })
140 | ```
141 |
142 | Finally, since using nodes text content to initialize struct fields is a pretty frequent task, **exml** provides shortcuts to make it shorter to write. Let's revisit our address book example and use this shortcut:
143 |
144 | ```go
145 | contact := &Contact{}
146 | decoder.OnTextOf("first-name", exml.Assign(&contact.FirstName))
147 | decoder.OnTextOf("last-name", exml.Assign(&contact.LastName))
148 | decoder.OnTextOf("address", exml.Assign(&contact.Address))
149 | ```
150 |
151 | Other assignment shortcuts (AssignBool, AssignFloat, AssignInt and AssignUInt) allow to pick typed values. Another type of shortcuts allow to accumulate text content from various nodes to a single slice:
152 |
153 | ```go
154 | info := []string{}
155 | decoder.OnTextOf("first-name", exml.Append(&info))
156 | decoder.OnTextOf("last-name", exml.Append(&info))
157 | decoder.OnTextOf("address", exml.Append(&info))
158 | ```
159 |
160 | In the same way, there are typed versions of the appending shortcuts (AppendBool, AppendFloat, AppendInt and AppendUInt) which allow to append typed parsed values.
161 |
162 | The second version (aka v2) of **exml** introduced global events which allow to register a top level handler that would be picked up at any level whenever a corresponding XML node is encountered. For example, this snippet would allow to print all text nodes regardless of their depth and parent tag:
163 |
164 | ```go
165 | decoder := exml.NewDecoder(reader)
166 | decoder.OnText(func(text CharData) {
167 | fmt.Println(string(text))
168 | })
169 | ```
170 |
171 | # API
172 |
173 | The full API is visible at the **exml** [gopkg.in][gopkg] page.
174 |
175 | # Benchmarks
176 |
177 | The included benchmarks show that **exml** can be *massively* faster than standard unmarshaling and the difference would most likely be even greater for bigger inputs.
178 |
179 | ```shell
180 | % go test -bench . -benchmem
181 | OK: 23 passed
182 | PASS
183 | Benchmark_UnmarshalSimple 50000 57156 ns/op 6138 B/op 128 allocs/op
184 | Benchmark_UnmarshalText 100000 22423 ns/op 3452 B/op 61 allocs/op
185 | Benchmark_UnmarshalCDATA 100000 23460 ns/op 3483 B/op 61 allocs/op
186 | Benchmark_UnmarshalMixed 100000 28807 ns/op 4034 B/op 67 allocs/op
187 | Benchmark_DecodeSimple 5000000 388 ns/op 99 B/op 3 allocs/op
188 | Benchmark_DecodeText 5000000 485 ns/op 114 B/op 3 allocs/op
189 | Benchmark_DecodeCDATA 5000000 485 ns/op 114 B/op 3 allocs/op
190 | Benchmark_DecodeMixed 5000000 487 ns/op 114 B/op 3 allocs/op
191 | ok github.com/lucsky/go-exml 11.194s
192 | ```
193 |
194 | # Contributors
195 |
196 | * Luc Heinrich (author)
197 | * Hubert Figuière
198 |
199 | # License
200 |
201 | Code is under the [BSD 2 Clause (NetBSD) license][license].
202 |
203 | [license]:https://github.com/lucsky/go-exml/tree/master/LICENSE
204 | [gopkg]:http://gopkg.in/lucsky/go-exml.v3
205 |
--------------------------------------------------------------------------------
/exml.go:
--------------------------------------------------------------------------------
1 | /*
2 | The exml package provides an intuitive event based XML parsing API which sits
3 | on top of a standard Go xml.Decoder, greatly simplifying the parsing code
4 | while retaining the raw speed and low memory overhead of the underlying stream
5 | engine, regardless of the size of the input. The package takes care of the
6 | complex tasks of maintaining contexts between event handlers allowing you to
7 | concentrate on dealing with the actual structure of the XML document.
8 | */
9 | package exml
10 |
11 | import (
12 | "bytes"
13 | "encoding/xml"
14 | "io"
15 | "strconv"
16 | "strings"
17 | )
18 |
19 | type TagCallback func(Attrs)
20 | type TextCallback func(CharData)
21 | type ErrorCallback func(error)
22 |
23 | type handler struct {
24 | tagCallback TagCallback
25 | textCallback TextCallback
26 | subHandlers map[string]*handler
27 | parentHandler *handler
28 | text []byte
29 | }
30 |
31 | // A Decoder wraps an xml.Decoder and maintains the various states
32 | // between the encountered XML nodes during parsing.
33 | type Decoder struct {
34 | decoder *xml.Decoder
35 | topHandler *handler
36 | currentHandler *handler
37 | errorCallback ErrorCallback
38 | }
39 |
40 | // NewDecoder creates a new exml parser reading from r.
41 | func NewDecoder(r io.Reader) *Decoder {
42 | return NewCustomDecoder(xml.NewDecoder(r))
43 | }
44 |
45 | // NewCustomDecoder creates a new exml parser reading from the passed
46 | // xml.Decoder which is useful when you need to configure the underlying
47 | // decoder, when you need to handle non-UTF8 xml documents for example.
48 | func NewCustomDecoder(d *xml.Decoder) *Decoder {
49 | topHandler := &handler{}
50 | return &Decoder{
51 | decoder: d,
52 | topHandler: topHandler,
53 | currentHandler: topHandler,
54 | }
55 | }
56 |
57 | // On registers a handler for a single tag or for a path.
58 | func (d *Decoder) On(path string, callback TagCallback) {
59 | h := d.installHandlers(path)
60 | h.tagCallback = callback
61 | }
62 |
63 | // OnTextOf registers a handler for the text content of a single tag or
64 | // for the text content at a certain path.
65 | func (d *Decoder) OnTextOf(path string, callback TextCallback) {
66 | h := d.installHandlers(path)
67 | h.textCallback = callback
68 | }
69 |
70 | // OnText registers a handler for the text content of the current tag.
71 | func (d *Decoder) OnText(callback TextCallback) {
72 | d.currentHandler.textCallback = callback
73 | }
74 |
75 | func (d *Decoder) installHandlers(path string) *handler {
76 | events := strings.Split(path, "/")
77 | depth := len(events) - 1
78 | h := d.currentHandler
79 |
80 | var sub *handler
81 | for i, ev := range events {
82 | if i < depth {
83 | sub = h.subHandlers[ev]
84 | if sub == nil {
85 | sub = &handler{parentHandler: h}
86 | }
87 | } else {
88 | sub = &handler{parentHandler: h}
89 | }
90 |
91 | if h.subHandlers == nil {
92 | h.subHandlers = make(map[string]*handler)
93 | }
94 |
95 | h.subHandlers[ev] = sub
96 | h = sub
97 | }
98 |
99 | return sub
100 | }
101 |
102 | // OnError registers a global error handler which will be called whenever
103 | // the underlying xml.Decoder reports an error.
104 | func (d *Decoder) OnError(handler ErrorCallback) {
105 | d.errorCallback = handler
106 | }
107 |
108 | // Run starts the parsing process.
109 | func (d *Decoder) Run() {
110 | for {
111 | token, err := d.decoder.Token()
112 | if token == nil {
113 | if err != io.EOF && d.errorCallback != nil {
114 | d.errorCallback(err)
115 | }
116 | break
117 | }
118 |
119 | switch t := token.(type) {
120 | case xml.StartElement:
121 | d.handleText()
122 | d.handleTag(t)
123 | case xml.CharData:
124 | d.currentHandler.text = append(d.currentHandler.text, t...)
125 | case xml.EndElement:
126 | d.handleText()
127 | if d.currentHandler != d.topHandler {
128 | d.currentHandler = d.currentHandler.parentHandler
129 | }
130 | }
131 | }
132 | }
133 |
134 | func (d *Decoder) handleTag(t xml.StartElement) {
135 | h := d.topHandler.subHandlers[t.Name.Local]
136 | if h == nil && d.currentHandler != d.topHandler {
137 | h = d.currentHandler.subHandlers[t.Name.Local]
138 | if h == nil {
139 | h = &handler{}
140 | }
141 | }
142 |
143 | if h != nil {
144 | h.parentHandler = d.currentHandler
145 | d.currentHandler = h
146 | if h.tagCallback != nil {
147 | h.tagCallback(t.Attr)
148 | }
149 | }
150 | }
151 |
152 | func (d *Decoder) handleText() {
153 | text := bytes.TrimSpace(d.currentHandler.text)
154 | d.currentHandler.text = d.currentHandler.text[:0]
155 | if d.currentHandler.textCallback != nil && len(text) > 0 {
156 | d.currentHandler.textCallback(text)
157 | }
158 | }
159 |
160 | // Assign is a helper function which returns a text callback that assigns
161 | // the text content of the current tag to the passed variable pointer.
162 | func Assign(v *string) TextCallback {
163 | return func(c CharData) {
164 | *v = string(c)
165 | }
166 | }
167 |
168 | // AssignBool is a helper function which returns a text callback that
169 | // assigns the text content of the current tag parsed as a bool to the
170 | // passed variable pointer. The accepted text values correspond to the
171 | // one accepted by the strconv.ParseBool() function. The fallback parameter
172 | // value is used when the parsing of the text content fails.
173 | func AssignBool(v *bool, fallback bool) TextCallback {
174 | return func(c CharData) {
175 | val, err := strconv.ParseBool(string(c))
176 | if err == nil {
177 | *v = val
178 | } else {
179 | *v = fallback
180 | }
181 | }
182 | }
183 |
184 | // AssignFloat is a helper function which returns a text callback that
185 | // assigns the text content of the current tag parsed as a float to the
186 | // passed variable pointer. The accepted text values correspond to the
187 | // one accepted by the strconv.ParseFloat() function. The fallback parameter
188 | // value is used when the parsing of the text content fails.
189 | func AssignFloat(v *float64, bitsize int, fallback float64) TextCallback {
190 | return func(c CharData) {
191 | val, err := strconv.ParseFloat(string(c), bitsize)
192 | if err == nil {
193 | *v = val
194 | } else {
195 | *v = fallback
196 | }
197 | }
198 | }
199 |
200 | // AssignInt is a helper function which returns a text callback that
201 | // assigns the text content of the current tag parsed as an int to the
202 | // passed variable pointer. The accepted text values correspond to the
203 | // one accepted by the strconv.ParseInt() function. The fallback parameter
204 | // value is used when the parsing of the text content fails.
205 | func AssignInt(v *int64, base int, bitsize int, fallback int64) TextCallback {
206 | return func(c CharData) {
207 | val, err := strconv.ParseInt(string(c), base, bitsize)
208 | if err == nil {
209 | *v = val
210 | } else {
211 | *v = fallback
212 | }
213 | }
214 | }
215 |
216 | // AssignUInt is a helper function which returns a text callback that
217 | // assigns the text content of the current tag parsed as an uint to the
218 | // passed variable pointer. The accepted text values correspond to the
219 | // one accepted by the strconv.ParseUint() function. The fallback parameter
220 | // value is used when the parsing of the text content fails.
221 | func AssignUInt(v *uint64, base int, bitsize int, fallback uint64) TextCallback {
222 | return func(c CharData) {
223 | val, err := strconv.ParseUint(string(c), base, bitsize)
224 | if err == nil {
225 | *v = val
226 | } else {
227 | *v = fallback
228 | }
229 | }
230 | }
231 |
232 | // Append is a helper function which returns a text callback that appends
233 | // the text content of the current tag to the passed strings slice pointer.
234 | func Append(a *[]string) TextCallback {
235 | return func(c CharData) {
236 | *a = append(*a, string(c))
237 | }
238 | }
239 |
240 | // AppendBool is a helper function which returns a text callback that appends
241 | // the text content of the current tag parsed as a bool to the passed slice
242 | // pointer. The accepted text values correspond to the one accepted by the
243 | // strconv.ParseBool() function. The fallback parameter value is used when
244 | // the parsing of the text content fails.
245 | func AppendBool(a *[]bool, fallback bool) TextCallback {
246 | return func(c CharData) {
247 | var val bool
248 | AssignBool(&val, fallback)(c)
249 | *a = append(*a, val)
250 | }
251 | }
252 |
253 | // AppendFloat is a helper function which returns a text callback that appends
254 | // the text content of the current tag parsed as a float to the passed slice
255 | // pointer. The accepted text values correspond to the one accepted by the
256 | // strconv.ParseFloat() function. The fallback parameter value is used when
257 | // the parsing of the text content fails.
258 | func AppendFloat(a *[]float64, bitsize int, fallback float64) TextCallback {
259 | return func(c CharData) {
260 | var val float64
261 | AssignFloat(&val, bitsize, fallback)(c)
262 | *a = append(*a, val)
263 | }
264 | }
265 |
266 | // AppendInt is a helper function which returns a text callback that appends
267 | // the text content of the current tag parsed as an int to the passed slice
268 | // pointer. The accepted text values correspond to the one accepted by the
269 | // strconv.ParseInt() function. The fallback parameter value is used when
270 | // the parsing of the text content fails.
271 | func AppendInt(a *[]int64, base int, bitsize int, fallback int64) TextCallback {
272 | return func(c CharData) {
273 | var val int64
274 | AssignInt(&val, base, bitsize, fallback)(c)
275 | *a = append(*a, val)
276 | }
277 | }
278 |
279 | // AppendUInt is a helper function which returns a text callback that appends
280 | // the text content of the current tag parsed as an uint to the passed slice
281 | // pointer. The accepted text values correspond to the one accepted by the
282 | // strconv.ParseUint() function. The fallback parameter value is used when
283 | // the parsing of the text content fails.
284 | func AppendUInt(a *[]uint64, base int, bitsize int, fallback uint64) TextCallback {
285 | return func(c CharData) {
286 | var val uint64
287 | AssignUInt(&val, base, bitsize, fallback)(c)
288 | *a = append(*a, val)
289 | }
290 | }
291 |
292 | type CharData xml.CharData
293 | type Attrs []xml.Attr
294 |
295 | // Get returns the value of the requested attribute and true when the
296 | // attributes exists, or an empty string and false when it doesn't.
297 | func (a Attrs) Get(name string) (string, bool) {
298 | for _, attr := range a {
299 | if attr.Name.Local == name {
300 | return attr.Value, true
301 | }
302 | }
303 |
304 | return "", false
305 | }
306 |
307 | // GetString returns the value of the requested attribute when it exists
308 | // or the passed fallback value when it doesn't.
309 | func (a Attrs) GetString(name string, fallback string) string {
310 | val, ok := a.Get(name)
311 | if !ok {
312 | return fallback
313 | }
314 |
315 | return val
316 | }
317 |
318 | // GetBool returns the value of the requested attribute as a bool when it
319 | // exists or the passed fallback value when it doesn't. The accepted values
320 | // corresponds to the one accepted by the strconv.ParseBool() function.
321 | func (a Attrs) GetBool(name string, fallback bool) bool {
322 | strVal, ok := a.Get(name)
323 | if !ok {
324 | return fallback
325 | }
326 |
327 | val, err := strconv.ParseBool(strVal)
328 | if err != nil {
329 | return fallback
330 | }
331 |
332 | return val
333 | }
334 |
335 | // GetFloat returns the value of the requested attribute as a float when it
336 | // exists or the passed fallback value when it doesn't. The additional
337 | // parameters and the accepted values corresponds to the one accepted by
338 | // the strconv.ParseFloat() function.
339 | func (a Attrs) GetFloat(name string, bitsize int, fallback float64) float64 {
340 | strVal, ok := a.Get(name)
341 | if !ok {
342 | return fallback
343 | }
344 |
345 | val, err := strconv.ParseFloat(strVal, bitsize)
346 | if err != nil {
347 | return fallback
348 | }
349 |
350 | return val
351 | }
352 |
353 | // GetInt returns the value of the requested attribute as an int when it
354 | // exists or the passed fallback value when it doesn't. The additional
355 | // parameters and the accepted values corresponds to the one accepted by
356 | // the strconv.ParseInt() function.
357 | func (a Attrs) GetInt(name string, base int, bitsize int, fallback int64) int64 {
358 | strVal, ok := a.Get(name)
359 | if !ok {
360 | return fallback
361 | }
362 |
363 | val, err := strconv.ParseInt(strVal, base, bitsize)
364 | if err != nil {
365 | return fallback
366 | }
367 |
368 | return val
369 | }
370 |
371 | // GetUInt returns the value of the requested attribute as an uint when it
372 | // exists or the passed fallback value when it doesn't. The additional
373 | // parameters and the accepted values corresponds to the one accepted by
374 | // the strconv.ParseUint() function.
375 | func (a Attrs) GetUInt(name string, base int, bitsize int, fallback uint64) uint64 {
376 | strVal, ok := a.Get(name)
377 | if !ok {
378 | return fallback
379 | }
380 |
381 | val, err := strconv.ParseUint(strVal, base, bitsize)
382 | if err != nil {
383 | return fallback
384 | }
385 |
386 | return val
387 | }
388 |
--------------------------------------------------------------------------------
/exml_test.go:
--------------------------------------------------------------------------------
1 | package exml
2 |
3 | import (
4 | "encoding/xml"
5 | "fmt"
6 | "strings"
7 | "testing"
8 |
9 | "gopkg.in/check.v1"
10 | )
11 |
12 | // ============================================================================
13 | // Example
14 |
15 | const EXAMPLE = `
16 |
17 |
18 | Tim
19 | Cook
20 | Cupertino
21 | 90210
22 |
23 |
24 | Steve
25 | Ballmer
26 | Redmond
27 |
28 |
29 | Mark
30 | Zuckerberg
31 | Menlo Park
32 |
33 | `
34 |
35 | type AddressBook struct {
36 | Name string
37 | Contacts []*Contact
38 | }
39 |
40 | type Contact struct {
41 | FirstName string
42 | LastName string
43 | Address string
44 | }
45 |
46 | func Example() {
47 | reader := strings.NewReader(EXAMPLE)
48 | decoder := NewDecoder(reader)
49 |
50 | addressBook := AddressBook{}
51 |
52 | decoder.On("address-book", func(attrs Attrs) {
53 | addressBook.Name, _ = attrs.Get("name")
54 |
55 | decoder.On("contact", func(attrs Attrs) {
56 | contact := &Contact{}
57 | addressBook.Contacts = append(addressBook.Contacts, contact)
58 |
59 | decoder.OnTextOf("first-name", func(text CharData) {
60 | contact.FirstName = string(text)
61 | })
62 |
63 | decoder.OnTextOf("last-name", func(text CharData) {
64 | contact.LastName = string(text)
65 | })
66 |
67 | decoder.OnTextOf("address", func(text CharData) {
68 | contact.Address = string(text)
69 | })
70 | })
71 | })
72 |
73 | decoder.Run()
74 |
75 | fmt.Printf("Address book: %s\n", addressBook.Name)
76 | for _, c := range addressBook.Contacts {
77 | fmt.Println("-", c.FirstName, c.LastName, "@", c.Address)
78 | }
79 |
80 | // Output:
81 | // Address book: homies
82 | // - Tim Cook @ Cupertino
83 | // - Steve Ballmer @ Redmond
84 | // - Mark Zuckerberg @ Menlo Park
85 | }
86 |
87 | // ============================================================================
88 | // Tests
89 |
90 | // Hook up gocheck into the "go test" runner.
91 | func Test(t *testing.T) { check.TestingT(t) }
92 |
93 | type EXMLSuite struct{}
94 |
95 | var _ = check.Suite(&EXMLSuite{})
96 |
97 | const ATTRIBUTE = `
98 | `
99 |
100 | func (s *EXMLSuite) Test_AttributeReaders(c *check.C) {
101 | decoder := NewDecoder(strings.NewReader(ATTRIBUTE))
102 | handlerWasCalled := false
103 |
104 | decoder.On("node", func(attrs Attrs) {
105 | handlerWasCalled = true
106 |
107 | var attr string
108 | var ok bool
109 |
110 | attr, ok = attrs.Get("attr")
111 | c.Assert(attr, check.Equals, "node.attr")
112 | c.Assert(ok, check.Equals, true)
113 |
114 | attr, ok = attrs.Get("omfglol")
115 | c.Assert(attr, check.Equals, "")
116 | c.Assert(ok, check.Equals, false)
117 |
118 | attr = attrs.GetString("attr", "default")
119 | c.Assert(attr, check.Equals, "node.attr")
120 |
121 | attr = attrs.GetString("omfglol", "default")
122 | c.Assert(attr, check.Equals, "default")
123 |
124 | c.Assert(attrs.GetBool("b1", false), check.Equals, true)
125 | c.Assert(attrs.GetBool("b2", true), check.Equals, false)
126 | c.Assert(attrs.GetBool("b3", true), check.Equals, true)
127 | c.Assert(attrs.GetBool("b4", true), check.Equals, true)
128 |
129 | c.Assert(attrs.GetFloat("f1", 64, 0.0), check.Equals, 2.5)
130 | c.Assert(attrs.GetFloat("f2", 64, 2.5), check.Equals, 2.5)
131 | c.Assert(attrs.GetFloat("f3", 64, 2.5), check.Equals, 2.5)
132 |
133 | c.Assert(attrs.GetInt("i1", 10, 64, 0), check.Equals, int64(-42))
134 | c.Assert(attrs.GetInt("i2", 10, 64, -42), check.Equals, int64(-42))
135 | c.Assert(attrs.GetInt("i3", 10, 64, -42), check.Equals, int64(-42))
136 |
137 | c.Assert(attrs.GetUInt("ui1", 10, 64, 0), check.Equals, uint64(42))
138 | c.Assert(attrs.GetUInt("ui2", 10, 64, 42), check.Equals, uint64(42))
139 | c.Assert(attrs.GetUInt("ui3", 10, 64, 42), check.Equals, uint64(42))
140 | })
141 |
142 | decoder.Run()
143 | c.Assert(handlerWasCalled, check.Equals, true)
144 | }
145 |
146 | const SIMPLE = `
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 | `
155 |
156 | func (s *EXMLSuite) Test_Empty(c *check.C) {
157 | decoder := NewDecoder(strings.NewReader(SIMPLE))
158 | decoder.Run()
159 | c.Succeed()
160 | }
161 |
162 | func (s *EXMLSuite) Test_Nested(c *check.C) {
163 | decoder := NewDecoder(strings.NewReader(SIMPLE))
164 |
165 | handler1WasCalled := false
166 | handler2WasCalled := false
167 | handler3WasCalled := false
168 |
169 | decoder.On("root", func(attrs Attrs) {
170 | handler1WasCalled = true
171 | attr1, _ := attrs.Get("attr1")
172 | c.Assert(attr1, check.Equals, "root.attr1")
173 | attr2, _ := attrs.Get("attr2")
174 | c.Assert(attr2, check.Equals, "root.attr2")
175 |
176 | nodeNum := 0
177 | decoder.On("node", func(attrs Attrs) {
178 | handler2WasCalled = true
179 | nodeNum = nodeNum + 1
180 | attr1, _ := attrs.Get("attr1")
181 | c.Assert(attr1, check.Equals, fmt.Sprintf("node%d.attr1", nodeNum))
182 | attr2, _ := attrs.Get("attr2")
183 | c.Assert(attr2, check.Equals, fmt.Sprintf("node%d.attr2", nodeNum))
184 |
185 | decoder.On("subnode", func(attrs Attrs) {
186 | handler3WasCalled = true
187 | attr1, _ := attrs.Get("attr1")
188 | c.Assert(attr1, check.Equals, "subnode.attr1")
189 | attr2, _ := attrs.Get("attr2")
190 | c.Assert(attr2, check.Equals, "subnode.attr2")
191 | })
192 | })
193 | })
194 |
195 | decoder.Run()
196 |
197 | c.Assert(handler1WasCalled, check.Equals, true)
198 | c.Assert(handler2WasCalled, check.Equals, true)
199 | c.Assert(handler3WasCalled, check.Equals, true)
200 | }
201 |
202 | func (s *EXMLSuite) Test_Flat(c *check.C) {
203 | decoder := NewDecoder(strings.NewReader(SIMPLE))
204 |
205 | handler1WasCalled := false
206 | handler2WasCalled := false
207 | handler3WasCalled := false
208 |
209 | decoder.On("root", func(attrs Attrs) {
210 | handler1WasCalled = true
211 | attr1, _ := attrs.Get("attr1")
212 | c.Assert(attr1, check.Equals, "root.attr1")
213 | attr2, _ := attrs.Get("attr2")
214 | c.Assert(attr2, check.Equals, "root.attr2")
215 | })
216 |
217 | nodeNum := 0
218 | decoder.On("root/node", func(attrs Attrs) {
219 | handler2WasCalled = true
220 | nodeNum = nodeNum + 1
221 | attr1, _ := attrs.Get("attr1")
222 | c.Assert(attr1, check.Equals, fmt.Sprintf("node%d.attr1", nodeNum))
223 | attr2, _ := attrs.Get("attr2")
224 | c.Assert(attr2, check.Equals, fmt.Sprintf("node%d.attr2", nodeNum))
225 | })
226 |
227 | decoder.On("root/node/subnode", func(attrs Attrs) {
228 | handler3WasCalled = true
229 | attr1, _ := attrs.Get("attr1")
230 | c.Assert(attr1, check.Equals, "subnode.attr1")
231 | attr2, _ := attrs.Get("attr2")
232 | c.Assert(attr2, check.Equals, "subnode.attr2")
233 | })
234 |
235 | decoder.Run()
236 |
237 | c.Assert(nodeNum, check.Equals, 4)
238 | c.Assert(handler1WasCalled, check.Equals, true)
239 | c.Assert(handler2WasCalled, check.Equals, true)
240 | c.Assert(handler3WasCalled, check.Equals, true)
241 | }
242 |
243 | func (s *EXMLSuite) Test_Mixed1(c *check.C) {
244 | decoder := NewDecoder(strings.NewReader(SIMPLE))
245 |
246 | handler1WasCalled := false
247 | handler2WasCalled := false
248 |
249 | nodeNum := 0
250 | decoder.On("root/node", func(attrs Attrs) {
251 | handler1WasCalled = true
252 | nodeNum = nodeNum + 1
253 | attr1, _ := attrs.Get("attr1")
254 | c.Assert(attr1, check.Equals, fmt.Sprintf("node%d.attr1", nodeNum))
255 | attr2, _ := attrs.Get("attr2")
256 | c.Assert(attr2, check.Equals, fmt.Sprintf("node%d.attr2", nodeNum))
257 |
258 | decoder.On("subnode", func(attrs Attrs) {
259 | handler2WasCalled = true
260 | attr1, _ := attrs.Get("attr1")
261 | c.Assert(attr1, check.Equals, "subnode.attr1")
262 | attr2, _ := attrs.Get("attr2")
263 | c.Assert(attr2, check.Equals, "subnode.attr2")
264 | })
265 | })
266 |
267 | decoder.Run()
268 |
269 | c.Assert(nodeNum, check.Equals, 4)
270 | c.Assert(handler1WasCalled, check.Equals, true)
271 | c.Assert(handler2WasCalled, check.Equals, true)
272 | }
273 |
274 | func (s *EXMLSuite) Test_Mixed2(c *check.C) {
275 | decoder := NewDecoder(strings.NewReader(SIMPLE))
276 |
277 | handler1WasCalled := false
278 | handler2WasCalled := false
279 |
280 | decoder.On("root", func(attrs Attrs) {
281 | handler1WasCalled = true
282 | attr1, _ := attrs.Get("attr1")
283 | c.Assert(attr1, check.Equals, "root.attr1")
284 | attr2, _ := attrs.Get("attr2")
285 | c.Assert(attr2, check.Equals, "root.attr2")
286 |
287 | decoder.On("node/subnode", func(attrs Attrs) {
288 | handler2WasCalled = true
289 | attr1, _ := attrs.Get("attr1")
290 | c.Assert(attr1, check.Equals, "subnode.attr1")
291 | attr2, _ := attrs.Get("attr2")
292 | c.Assert(attr2, check.Equals, "subnode.attr2")
293 | })
294 | })
295 |
296 | decoder.Run()
297 |
298 | c.Assert(handler1WasCalled, check.Equals, true)
299 | c.Assert(handler2WasCalled, check.Equals, true)
300 | }
301 |
302 | func (s *EXMLSuite) Test_Global(c *check.C) {
303 | decoder := NewDecoder(strings.NewReader(SIMPLE))
304 |
305 | handlerWasCalled := false
306 | decoder.On("subnode", func(attrs Attrs) {
307 | handlerWasCalled = true
308 | attr1, _ := attrs.Get("attr1")
309 | c.Assert(attr1, check.Equals, "subnode.attr1")
310 | attr2, _ := attrs.Get("attr2")
311 | c.Assert(attr2, check.Equals, "subnode.attr2")
312 | })
313 |
314 | decoder.Run()
315 |
316 | c.Assert(handlerWasCalled, check.Equals, true)
317 | }
318 |
319 | const TEXT = `
320 |
321 | text content 1
322 | text content 2
323 | text content 3
324 | `
325 |
326 | func (s *EXMLSuite) Test_Text1(c *check.C) {
327 | runTextTest1(c, TEXT, "text content %d")
328 | }
329 |
330 | func (s *EXMLSuite) Test_Text2(c *check.C) {
331 | runTextTest2(c, TEXT, "text content %d")
332 | }
333 |
334 | func (s *EXMLSuite) Test_Text3(c *check.C) {
335 | runTextTest3(c, TEXT, "text content %d")
336 | }
337 |
338 | func (s *EXMLSuite) Test_Text4(c *check.C) {
339 | runTextTest4(c, TEXT, "text content %d")
340 | }
341 |
342 | const CDATA = `
343 |
344 |
345 |
346 |
347 | `
348 |
349 | func (s *EXMLSuite) Test_CDATA1(c *check.C) {
350 | runTextTest1(c, CDATA, "CDATA content %d")
351 | }
352 |
353 | func (s *EXMLSuite) Test_CDATA2(c *check.C) {
354 | runTextTest2(c, CDATA, "CDATA content %d")
355 | }
356 |
357 | func (s *EXMLSuite) Test_CDATA3(c *check.C) {
358 | runTextTest3(c, CDATA, "CDATA content %d")
359 | }
360 |
361 | func (s *EXMLSuite) Test_CDATA4(c *check.C) {
362 | runTextTest4(c, CDATA, "CDATA content %d")
363 | }
364 |
365 | const MIXED = `
366 |
367 | Text content followed by some
368 | Text content followed by some
369 | Text content followed by some
370 | `
371 |
372 | func (s *EXMLSuite) Test_MixedContent1(c *check.C) {
373 | runTextTest1(c, MIXED, "Text content followed by some CDATA content %d")
374 | }
375 |
376 | func (s *EXMLSuite) Test_MixedContent2(c *check.C) {
377 | runTextTest2(c, MIXED, "Text content followed by some CDATA content %d")
378 | }
379 |
380 | func (s *EXMLSuite) Test_MixedContent3(c *check.C) {
381 | runTextTest3(c, MIXED, "Text content followed by some CDATA content %d")
382 | }
383 |
384 | func (s *EXMLSuite) Test_MixedContent4(c *check.C) {
385 | runTextTest4(c, MIXED, "Text content followed by some CDATA content %d")
386 | }
387 |
388 | func runTextTest1(c *check.C, data string, expectedFmt string) {
389 | decoder := NewDecoder(strings.NewReader(data))
390 |
391 | nodeNum := 0
392 | handlerWasCalled := []bool{false, false, false}
393 |
394 | decoder.On("root", func(attrs Attrs) {
395 | decoder.On("node", func(attrs Attrs) {
396 | handlerWasCalled[nodeNum] = true
397 | nodeNum = nodeNum + 1
398 | decoder.OnText(func(text CharData) {
399 | c.Assert(string(text), check.Equals, fmt.Sprintf(expectedFmt, nodeNum))
400 | })
401 | })
402 | })
403 |
404 | decoder.Run()
405 |
406 | c.Assert(handlerWasCalled[0], check.Equals, true)
407 | c.Assert(handlerWasCalled[1], check.Equals, true)
408 | c.Assert(handlerWasCalled[2], check.Equals, true)
409 | }
410 |
411 | func runTextTest2(c *check.C, data string, expectedFmt string) {
412 | decoder := NewDecoder(strings.NewReader(data))
413 |
414 | nodeNum := 0
415 | handlerWasCalled := []bool{false, false, false}
416 |
417 | decoder.On("root/node", func(attrs Attrs) {
418 | handlerWasCalled[nodeNum] = true
419 | nodeNum = nodeNum + 1
420 | decoder.OnText(func(text CharData) {
421 | c.Assert(string(text), check.Equals, fmt.Sprintf(expectedFmt, nodeNum))
422 | })
423 | })
424 |
425 | decoder.Run()
426 |
427 | c.Assert(handlerWasCalled[0], check.Equals, true)
428 | c.Assert(handlerWasCalled[1], check.Equals, true)
429 | c.Assert(handlerWasCalled[2], check.Equals, true)
430 |
431 | }
432 |
433 | func runTextTest3(c *check.C, data string, expectedFmt string) {
434 | decoder := NewDecoder(strings.NewReader(data))
435 |
436 | nodeNum := 0
437 | handlerWasCalled := []bool{false, false, false}
438 |
439 | decoder.OnTextOf("root/node", func(text CharData) {
440 | handlerWasCalled[nodeNum] = true
441 | nodeNum = nodeNum + 1
442 | c.Assert(string(text), check.Equals, fmt.Sprintf(expectedFmt, nodeNum))
443 | })
444 |
445 | decoder.Run()
446 |
447 | c.Assert(handlerWasCalled[0], check.Equals, true)
448 | c.Assert(handlerWasCalled[1], check.Equals, true)
449 | c.Assert(handlerWasCalled[2], check.Equals, true)
450 | }
451 |
452 | func runTextTest4(c *check.C, data string, expectedFmt string) {
453 | decoder := NewDecoder(strings.NewReader(data))
454 |
455 | nodeNum := 0
456 | handlerWasCalled := []bool{false, false, false}
457 |
458 | decoder.OnText(func(text CharData) {
459 | handlerWasCalled[nodeNum] = true
460 | nodeNum = nodeNum + 1
461 | c.Assert(string(text), check.Equals, fmt.Sprintf(expectedFmt, nodeNum))
462 | })
463 |
464 | decoder.Run()
465 |
466 | c.Assert(handlerWasCalled[0], check.Equals, true)
467 | c.Assert(handlerWasCalled[1], check.Equals, true)
468 | c.Assert(handlerWasCalled[2], check.Equals, true)
469 | }
470 |
471 | const ASSIGN = `
472 |
473 | Text content
474 | true
475 | foo
476 | 3.14
477 | foo
478 | -42
479 | foo
480 | 42
481 | foo
482 | `
483 |
484 | func (s *EXMLSuite) Test_Assign(c *check.C) {
485 | var text string
486 | var boolVal1 bool
487 | var boolVal2 bool
488 | var floatVal1 float64
489 | var floatVal2 float64
490 | var intVal1 int64
491 | var intVal2 int64
492 | var uintVal1 uint64
493 | var uintVal2 uint64
494 |
495 | decoder := NewDecoder(strings.NewReader(ASSIGN))
496 | decoder.OnTextOf("root/node", Assign(&text))
497 | decoder.OnTextOf("root/bool", AssignBool(&boolVal1, false))
498 | decoder.OnTextOf("root/bad-bool", AssignBool(&boolVal2, true))
499 | decoder.OnTextOf("root/float", AssignFloat(&floatVal1, 64, 0))
500 | decoder.OnTextOf("root/bad-float", AssignFloat(&floatVal2, 64, 3.14))
501 | decoder.OnTextOf("root/int", AssignInt(&intVal1, 10, 64, 0))
502 | decoder.OnTextOf("root/bad-int", AssignInt(&intVal2, 10, 64, 42))
503 | decoder.OnTextOf("root/uint", AssignUInt(&uintVal1, 10, 64, 0))
504 | decoder.OnTextOf("root/bad-uint", AssignUInt(&uintVal2, 10, 64, 42))
505 | decoder.Run()
506 |
507 | c.Assert(text, check.Equals, "Text content")
508 | c.Assert(boolVal1, check.Equals, true)
509 | c.Assert(boolVal2, check.Equals, true)
510 | c.Assert(floatVal1, check.Equals, 3.14)
511 | c.Assert(floatVal2, check.Equals, 3.14)
512 | c.Assert(intVal1, check.Equals, int64(-42))
513 | c.Assert(intVal2, check.Equals, int64(42))
514 | c.Assert(uintVal1, check.Equals, uint64(42))
515 | c.Assert(uintVal2, check.Equals, uint64(42))
516 | }
517 |
518 | const APPEND = `
519 |
520 | Text content 1Text content 2Text content 3
521 | true1foo
522 | 3.14-3.14foo
523 | -4242foo
524 | 4221foo
525 | `
526 |
527 | func (s *EXMLSuite) Test_Append(c *check.C) {
528 | texts := []string{}
529 | bools := []bool{}
530 | floats := []float64{}
531 | ints := []int64{}
532 | uints := []uint64{}
533 |
534 | decoder := NewDecoder(strings.NewReader(APPEND))
535 | decoder.OnTextOf("root/node", Append(&texts))
536 | decoder.OnTextOf("root/bool", AppendBool(&bools, false))
537 | decoder.OnTextOf("root/float", AppendFloat(&floats, 64, 1.5))
538 | decoder.OnTextOf("root/int", AppendInt(&ints, 10, 64, 21))
539 | decoder.OnTextOf("root/uint", AppendUInt(&uints, 10, 64, 2))
540 | decoder.Run()
541 |
542 | c.Assert(texts[0], check.Equals, "Text content 1")
543 | c.Assert(texts[1], check.Equals, "Text content 2")
544 | c.Assert(texts[2], check.Equals, "Text content 3")
545 | c.Assert(bools[0], check.Equals, true)
546 | c.Assert(bools[1], check.Equals, true)
547 | c.Assert(bools[2], check.Equals, false)
548 | c.Assert(floats[0], check.Equals, 3.14)
549 | c.Assert(floats[1], check.Equals, -3.14)
550 | c.Assert(floats[2], check.Equals, 1.5)
551 | c.Assert(ints[0], check.Equals, int64(-42))
552 | c.Assert(ints[1], check.Equals, int64(42))
553 | c.Assert(ints[2], check.Equals, int64(21))
554 | c.Assert(uints[0], check.Equals, uint64(42))
555 | c.Assert(uints[1], check.Equals, uint64(21))
556 | c.Assert(uints[2], check.Equals, uint64(2))
557 | }
558 |
559 | const NESTED_TEXT = `
560 | Root text 1Node textRoot text 2`
561 |
562 | func (s *EXMLSuite) Test_NestedText(c *check.C) {
563 | texts := []string{}
564 |
565 | decoder := NewDecoder(strings.NewReader(NESTED_TEXT))
566 | decoder.OnText(Append(&texts))
567 | decoder.Run()
568 |
569 | c.Assert(texts[0], check.Equals, "Root text 1")
570 | c.Assert(texts[1], check.Equals, "Node text")
571 | c.Assert(texts[2], check.Equals, "Root text 2")
572 | }
573 |
574 | const MALFORMED = ""
575 |
576 | func (s *EXMLSuite) Test_Error(c *check.C) {
577 | decoder := NewDecoder(strings.NewReader(MALFORMED))
578 |
579 | handlerWasCalled := false
580 |
581 | decoder.OnError(func(err error) {
582 | handlerWasCalled = true
583 | })
584 |
585 | decoder.Run()
586 |
587 | c.Assert(handlerWasCalled, check.Equals, true)
588 | }
589 |
590 | func (s *EXMLSuite) Test_EmptyInput(c *check.C) {
591 | decoder := NewDecoder(strings.NewReader(""))
592 |
593 | handlerWasCalled := false
594 |
595 | decoder.OnError(func(err error) {
596 | handlerWasCalled = true
597 | })
598 |
599 | decoder.Run()
600 |
601 | c.Assert(handlerWasCalled, check.Equals, false)
602 | }
603 |
604 | // ============================================================================
605 | // Benchmarks
606 |
607 | type SimpleTreeNode struct {
608 | Attr1 string `xml:"attr1,attr"`
609 | Attr2 string `xml:"attr2,attr"`
610 | Sub *SimpleTreeNode `xml:"subnode"`
611 | }
612 |
613 | type SimpleTree struct {
614 | XMLName xml.Name `xml:"root"`
615 | Attr1 string `xml:"attr1,attr"`
616 | Attr2 string `xml:"attr2,attr"`
617 | Nodes []*SimpleTreeNode `xml:"node"`
618 | }
619 |
620 | func Benchmark_UnmarshalSimple(b *testing.B) {
621 | for i := 0; i < b.N; i++ {
622 | tree := &SimpleTree{}
623 | var _ = xml.Unmarshal([]byte(SIMPLE), tree)
624 | }
625 | }
626 |
627 | func Benchmark_UnmarshalText(b *testing.B) {
628 | runUnmarshalTextBenchmark(b, TEXT)
629 | }
630 |
631 | func Benchmark_UnmarshalCDATA(b *testing.B) {
632 | runUnmarshalTextBenchmark(b, CDATA)
633 | }
634 |
635 | func Benchmark_UnmarshalMixed(b *testing.B) {
636 | runUnmarshalTextBenchmark(b, MIXED)
637 | }
638 |
639 | type TextList struct {
640 | Texts []string `xml:"node"`
641 | }
642 |
643 | func runUnmarshalTextBenchmark(b *testing.B, data string) {
644 | l := &TextList{}
645 | for i := 0; i < b.N; i++ {
646 | var _ = xml.Unmarshal([]byte(data), l)
647 | l.Texts = l.Texts[:0]
648 | }
649 | }
650 |
651 | func Benchmark_DecodeSimple(b *testing.B) {
652 | reader := strings.NewReader(SIMPLE)
653 | decoder := NewDecoder(reader)
654 |
655 | for i := 0; i < b.N; i++ {
656 | decoder.On("root", func(attrs Attrs) {
657 | tree := &SimpleTree{}
658 | tree.XMLName = xml.Name{Space: "", Local: "root"}
659 | tree.Attr1, _ = attrs.Get("attr1")
660 | tree.Attr2, _ = attrs.Get("attr2")
661 |
662 | decoder.On("node", func(attrs Attrs) {
663 | node := &SimpleTreeNode{}
664 | node.Attr1, _ = attrs.Get("attr1")
665 | node.Attr2, _ = attrs.Get("attr2")
666 | tree.Nodes = append(tree.Nodes, node)
667 |
668 | decoder.On("subnode", func(attrs Attrs) {
669 | node.Sub = &SimpleTreeNode{}
670 | node.Sub.Attr1, _ = attrs.Get("attr1")
671 | node.Sub.Attr2, _ = attrs.Get("attr2")
672 | })
673 | })
674 | })
675 |
676 | decoder.Run()
677 | reader.Seek(0, 0)
678 | }
679 | }
680 |
681 | func Benchmark_DecodeText(b *testing.B) {
682 | runDecodeTextBenchmark(b, TEXT)
683 | }
684 |
685 | func Benchmark_DecodeCDATA(b *testing.B) {
686 | runDecodeTextBenchmark(b, CDATA)
687 | }
688 |
689 | func Benchmark_DecodeMixed(b *testing.B) {
690 | runDecodeTextBenchmark(b, MIXED)
691 | }
692 |
693 | func runDecodeTextBenchmark(b *testing.B, data string) {
694 | reader := strings.NewReader(data)
695 | decoder := NewDecoder(reader)
696 | l := &TextList{}
697 |
698 | for i := 0; i < b.N; i++ {
699 | decoder.OnTextOf("root/node", func(text CharData) {
700 | l.Texts = append(l.Texts, string(text))
701 | })
702 |
703 | decoder.Run()
704 |
705 | reader.Seek(0, 0)
706 | l.Texts = l.Texts[:0]
707 | }
708 | }
709 |
--------------------------------------------------------------------------------