├── .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 [![Build Status](https://drone.io/github.com/lucsky/go-exml/status.png)](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 | --------------------------------------------------------------------------------