spex/lib.rs
1/*!
2Simple(ish) parser and extractor of XML.
3
4This package provides an `XmlReader` which can automatically determine the character encoding
5of UTF-8 and UTF-16 (big endian and little endian byte order) XML byte streams, and parse the
6XML into an immutable `Element` tree held within an `XmlDocument`. It's also possible to use a
7custom byte stream decoder to read XML in other character encodings.
8
9The aim of this package is to support as closely as possible the W3C specifications
10[Extensible Markup Language (XML) 1.0](http://www.w3.org/TR/xml/) and
11[Namespaces in XML 1.0](http://www.w3.org/TR/xml-names/) for **well-formed** XML. This package
12does **not** aim to support validation of XML, and consequently DTD (document type definition)
13is deliberately not supported.
14
15Namespace support is always enabled, so the colon character is not permitted within the names of
16elements nor attributes.
17
18# XML concepts already supported
19
20* Elements
21* Attributes
22* Default namespaces `xmlns="namespace.com"`
23* Prefixed namespaces `xmlns:prefix="namespace.com"`
24* Processing instructions
25* Comments (skipped and thus not retrievable)
26* CDATA sections
27* Element language `xml:lang` and filtering by language
28* White space indication `xml:space`
29* Automatic detection and decoding of UTF-8 and UTF-16 XML streams.
30* Support for custom encodings where the encoding is known **before** parsing, and where the
31client supplies a custom decoder to handle the byte-to-character conversion.
32
33# Examples
34
35## Reading an XML file
36
37Suppose you want to read and extract XML from a file you know to be either UTF-8 or UTF-16
38encoded. You can use `XmlReader::parse_auto` to read, parse, and extract the XML from the file
39and return either an `XmlDocument` or an `std::io::Error`.
40
41```
42# use std::fs::File;
43# use spex::parsing::XmlReader;
44# fn main() -> Result<(), std::io::Error> {
45let xml_file = File::open("test_resources/xml_utf8_BOM.xml")?;
46let xml_doc = XmlReader::parse_auto(xml_file)?;
47# Ok(())
48# }
49```
50
51## Traversing an `XmlDocument`
52
53Once you have an `XmlDocument` you can grab an immutable reference to the root `Element` and
54then traverse through the element tree using the `req` (required child element) and `opt`
55(optional child element) methods to target the **first** child element with the specified name.
56And once we're pointing at the desired target, we can use `element()` or `text()` to attempt to
57grab the element or text-only content of the target element.
58
59For example, let's define a simple XML structure where required elements have a name starting
60with "r_" and optional elements have a name starting with "o_".
61
62```
63# use std::fs::File;
64# use spex::{parsing::XmlReader, xml::XmlDocument, common::XmlError};
65# fn main() {
66# // Panic if either step generates an error.
67# let xml_doc = read_xml().unwrap();
68# extract_xml(xml_doc).unwrap();
69# }
70#
71# // The XML parsing methods might throw an std::io::Error, so they go into their own method.
72# fn read_xml() -> Result<XmlDocument, std::io::Error> {
73# let xml = "
74<root>
75 <r_Widget>
76 <r_Name>Helix</r_Name>
77 <o_AdditionalInfo>
78 <r_ReleaseDate>2021-05-12</r_ReleaseDate>
79 <r_CurrentVersion>23.10</r_CurrentVersion>
80 <o_TopContributors>
81 <r_Name>archseer</r_Name>
82 <r_Name>the-mikedavis</r_Name>
83 <r_Name>sudormrfbin</r_Name>
84 <r_Name>pascalkuthe</r_Name>
85 <r_Name>dsseng</r_Name>
86 <r_Name>pickfire</r_Name>
87 </o_TopContributors>
88 </o_AdditionalInfo>
89 </r_Widget>
90</root>
91# ";
92# let xml_doc = XmlReader::parse_auto(xml.as_bytes());
93# xml_doc
94# }
95
96// Once the above XML is turned into an XmlDocument, it gets passed to this method.
97fn extract_xml(xml_doc: XmlDocument) -> Result<(), XmlError> {
98 // Let's start by grabbing a reference to the widget element. Because we use req to indicate
99 // that it should be considered an error if this required element is missing, the element()
100 // method will return a Result<&Element, XmlError>. So we use the `?` operator to throw the
101 // XmlError if it occurs.
102 let widget = xml_doc.root().req("r_Widget").element()?;
103
104 // The name is required, so we just use req again. We also expect the name to contain only
105 // simple text content (not mixed with other elements or processing instructions) so we
106 // call text() followed by the `?` operator to throw the XmlError that will be generated if
107 // either the name element is not found, or if it contains non-simple content.
108 let widget_name = widget.req("r_Name").text()?;
109 # assert_eq!(widget_name, "Helix");
110
111 // The info and top contributor elements are optional (may or may not appear in this type of
112 // XML document) so we can use the opt method to indicate that it is not an error if either
113 // element is not found. Instead of a Result<&Element, XmlError> this entirely optional
114 // chain will cause element() to give us an Option<&Element> instead, so we use `if let`
115 // to take action only if the given optional chain elements all exist.
116 if let Some(top_contrib_list) = widget
117 .opt("o_AdditionalInfo")
118 .opt("o_TopContributors")
119 .element() {
120 println!("Found top {} contributors!",
121 top_contrib_list.elements().filter(|e| e.is_named("r_Name")).count());
122 # assert_eq!(top_contrib_list.elements().filter(|e| e.is_named("r_Name")).count(), 6);
123 }
124
125 // If we want the release date, that's a required element within an optional element. In
126 // other words, it's not an error if "o_AdditionalInfo" is missing, but if it *is* found
127 // then we consider it an error if it does not contain "r_ReleaseDate". This is a mixed
128 // chain, involving both required and optional, which means that element() will return a
129 // Result<Option<&Element>, XmlError>, an Option wrapped in a Result. So we use `if let` and
130 // the `?` operator together.
131 if let Some(release_date) = widget.opt("o_AdditionalInfo").req("r_ReleaseDate").element()? {
132 println!("Release date: {}", release_date.text()?);
133 # assert_eq!(release_date.text().unwrap(), "2021-05-12");
134 }
135
136 Ok(())
137}
138```
139
140Note that the return type of the `element()` and `text()` methods varies depending on whether
141the method chain involves `req` or `opt` or both. This table summarizes the scenarios.
142
143<table>
144 <thead>
145 <tr>
146 <th scope="col">Chain involves</th>
147 <th scope="col"><code>element()</code> returns</th>
148 <th scope="col"><code>text()</code> returns</th>
149 </tr>
150 </thead>
151 <tbody>
152 <tr>
153 <th scope="row">only <code>req</code></th>
154 <td><code>Result<&Element, XmlError></code></td>
155 <td><code>Result<&str, XmlError></code></td>
156 </tr>
157 <tr>
158 <th scope="row">only <code>opt</code></th>
159 <td><code>Option<&Element></code></td>
160 <td><code>Result<Option<&str>, XmlError></code></td>
161 </tr>
162 <tr>
163 <th scope="row">both <code>req</code> and <code>opt</code></th>
164 <td><code>Result<Option<&Element>, XmlError></code></td>
165 <td><code>Result<Option<&str>, XmlError></code></td>
166 </tr>
167 </tbody>
168</table>
169
170Similarly, the return types of `att_req` and `att_opt` methods also vary depending on the method
171chain.
172
173<table>
174 <thead>
175 <tr>
176 <th scope="col">Chain involves</th>
177 <th scope="col"><code>att_req(name)</code> returns</th>
178 <th scope="col"><code>att_opt(name)</code> returns</th>
179 </tr>
180 </thead>
181 <tbody>
182 <tr>
183 <th scope="row">only <code>req</code></th>
184 <td><code>Result<&str, XmlError></code></td>
185 <td><code>Result<Option<&str>, XmlError></code></td>
186 </tr>
187 <tr>
188 <th scope="row">only <code>opt</code></th>
189 <td><code>Result<Option<&str>, XmlError></code></td>
190 <td><code>Option<&str></code></td>
191 </tr>
192 <tr>
193 <th scope="row">both <code>req</code> and <code>opt</code></th>
194 <td><code>Result<Option<&str>, XmlError></code></td>
195 <td><code>Result<Option<&str>, XmlError></code></td>
196 </tr>
197 </tbody>
198</table>
199
200It's easier to remember this as the following: `req`/`att_req` will generate an error if the
201element or attribute does not exist, so their use means that the return type must involve a
202`Result<_, XmlError>` of some sort. And `opt`/`att_opt` may or may not return a value, so their
203use means that the return type must involve an `Option<_>` of some sort. And mixing the two
204(required and optional) means that the return type must involve a `Result<Option<_>, XmlError>`
205of some sort. And `text()` generates an error if the target element does not have simple content
206(no child elements and no processing instructions) so its use also means that the return type
207must involve a `Result` of some sort.
208
209## More complex traversal using `XmlPath`
210
211The methods `req` and `opt` always turn their attention to the **first** child element with the
212given name. It's not possible to use them to target a sibling, say the second "Widget" within a
213list of "Widget" elements. To target siblings, and/or to iterate multiple elements, you instead
214use `XmlPath`. (Don't confuse this with [XPath](https://www.w3.org/TR/xpath-3/) which has a
215similar purpose but very different implementation.)
216
217For example, if you have XML which contains a list of employees, and you want to iterate the
218employees' tasks' deadlines, you could use `XmlPath` like this:
219
220
221```
222# use std::fs::File;
223# use spex::{parsing::XmlReader, xml::XmlDocument, common::XmlError};
224# fn main() {
225# // Panic if either step generates an error.
226# let xml_doc = read_xml().unwrap();
227# extract_xml(xml_doc).unwrap();
228# }
229#
230# // The XML parsing methods might throw an std::io::Error, so they go into their own method.
231# fn read_xml() -> Result<XmlDocument, std::io::Error> {
232# let xml = "
233<roster>
234 <employee>
235 <name>Angelica</name>
236 <department>Finance</department>
237 <task-list>
238 <task>
239 <name>Payroll</name>
240 <deadline>tomorrow</deadline>
241 </task>
242 <task>
243 <name>Reconciliation</name>
244 <deadline>Friday</deadline>
245 </task>
246 </task-list>
247 </employee>
248 <employee>
249 <name>Byron</name>
250 <department>Sales</department>
251 <task-list>
252 <task>
253 <name>Close the big deal</name>
254 <deadline>Saturday night</deadline>
255 </task>
256 </task-list>
257 </employee>
258 <employee>
259 <name>Cat</name>
260 <department>Software</department>
261 <task-list>
262 <task>
263 <name>Fix that bug</name>
264 <deadline>Maybe later this month</deadline>
265 </task>
266 <task>
267 <name>Add that new feature</name>
268 <deadline>Possibly this year</deadline>
269 </task>
270 <task>
271 <name>Make that customer happy</name>
272 <deadline>Good luck with that</deadline>
273 </task>
274 </task-list>
275 </employee>
276</roster>
277# ";
278# let xml_doc = XmlReader::parse_auto(xml.as_bytes());
279# xml_doc
280# }
281
282// Once the above XML is turned into an XmlDocument, it gets passed to this method.
283fn extract_xml(xml_doc: XmlDocument) -> Result<(), XmlError> {
284 for deadline in xml_doc.root()
285 .all("employee")
286 .first("task-list")
287 .all("task")
288 .first("deadline")
289 .iter() {
290 println!("Found task deadline: {}", deadline.text()?);
291 }
292 Ok(())
293}
294```
295
296This creates and iterates an `XmlPath` which represents "the first deadline element within
297every task within the first task-list within every employee". Based on the example XML above,
298this will print out all the text content of all six "deadline" elements.
299
300Note that we could use `first("employee")` if we only wanted the first employee. Or we could
301use `nth("employee", 1)` if we only want the second employee (zero would point to the first).
302Or we could use `last("employee")` if we only want the last employee. Similarly, we could use
303`first("task")` if we only wanted to consider the first task in each employee's list.
304
305## Filtering elements within an `XmlPath`
306
307An `XmlPath` not only lets you specify which child element names are of interest, but also lets
308you specify which xml:lang patterns are of interest, and lets you specify a required attribute
309name-value pair which must be found within a child element in order to include it in the
310iterator.
311
312```
313# use spex::{parsing::XmlReader, xml::XmlDocument, common::XmlError,
314# extraction::ExtendedLanguageRange};
315# fn main() {
316# // Panic if either step generates an error.
317# let xml_doc = read_xml().unwrap();
318# extract_xml(xml_doc).unwrap();
319# }
320#
321# // The XML parsing methods might throw an std::io::Error, so they go into their own method.
322# fn read_xml() -> Result<XmlDocument, std::io::Error> {
323# let xml = "
324<inventory>
325 <box type='games'>
326 <item>
327 <name xml:lang='en'>Command & Conquer: Tiberian Dawn</name>
328 <name xml:lang='en-US'>Command & Conquer</name>
329 <name xml:lang='de'>Command & Conquer: Teil 1 - Der Tiberiumkonflikt</name>
330 </item>
331 <item>
332 <name xml:lang='en'>Doom</name>
333 <name xml:lang='sr'>Zla kob</name>
334 <name xml:lang='ja'>ドゥーム</name>
335 </item>
336 <item>
337 <name xml:lang='en'>Half-Life</name>
338 <name xml:lang='sr'>Polu-život</name>
339 </item>
340 </box>
341 <box type='movies'>
342 <item>
343 <name xml:lang='en'>Aliens</name>
344 <name xml:lang='sv-SE'>Aliens - Återkomsten</name>
345 <name xml:lang='vi'>Quái Vật Không Gian 2</name>
346 </item>
347 <item>
348 <name xml:lang='en'>The Cabin In The Woods</name>
349 <name xml:lang='bg'>Хижа в гората</name>
350 <name xml:lang='fr'>La cabane dans les bois</name>
351 </item>
352 </box>
353</inventory>
354# ";
355# let xml_doc = XmlReader::parse_auto(xml.as_bytes());
356# xml_doc
357# }
358
359// Once the above XML is turned into an XmlDocument, it gets passed to this method.
360fn extract_xml(xml_doc: XmlDocument) -> Result<(), XmlError> {
361 let english = ExtendedLanguageRange::new("en")?;
362
363 for movie in xml_doc.root()
364 .all("box")
365 .with_attribute("type", "games")
366 .all("item")
367 .all("name")
368 .filter_lang_range(&english)
369 .iter() {
370 println!("Found movie title in English: {}", movie.text()?);
371 }
372 # assert_eq!(xml_doc.root()
373 # .all("box")
374 # .with_attribute("type", "games")
375 # .all("item")
376 # .all("name")
377 # .filter_lang_range(&english).iter().count(), 4);
378 Ok(())
379}
380```
381
382This will print out the names of all four English-language titles for the three games. It will
383skip all of the movies, and all names which are rejected by the "en" language filter. Note
384that this "en" filter will match both `xml:lang="en"` and `xml:lang="en-US"` so you'll get two
385matching name elements for the first game.
386
387## Attribute extraction
388
389Getting the value of an attribute is done with the methods `att_req` (generate an error if the
390attribute is missing) and `att_opt` (no error if the attribute is missing).
391
392For example, given this simple XML document, we can grab the attribute values easily.
393
394```
395# use spex::{parsing::XmlReader, xml::XmlDocument, common::XmlError,
396# extraction::ExtendedLanguageRange};
397# fn main() {
398# // Panic if either step generates an error.
399# let xml_doc = read_xml().unwrap();
400# extract_xml(xml_doc).unwrap();
401# }
402#
403# // The XML parsing methods might throw an std::io::Error, so they go into their own method.
404# fn read_xml() -> Result<XmlDocument, std::io::Error> {
405# let xml = "
406<root generationDate='2023-02-09T18:10:00Z'>
407 <record id='35517'>
408 <temp locationId='23'>40.5</temp>
409 </record>
410 <record id='35518'>
411 <temp locationId='36'>38.9</temp>
412 </record>
413</root>
414# ";
415# let xml_doc = XmlReader::parse_auto(xml.as_bytes());
416# xml_doc
417# }
418
419// Once the above XML is turned into an XmlDocument, it gets passed to this method.
420fn extract_xml(xml_doc: XmlDocument) -> Result<(), XmlError> {
421 // Iterate the records using an XmlPath.
422 for record in xml_doc.root().all("record").iter() {
423 // The record@id attribute is required (we consider it an error if it is missing).
424 // So use att_req and then the `?` syntax to throw any XmlError generated.
425 let record_id = record.att_req("id")?;
426
427 let temp = record.req("temp").element()?;
428 let temp_value = temp.text()?;
429
430 // The temp@locationId attribute is optional (we don't consider it an error if it's not
431 // found within this element). So use att_opt and then `if let` to check for it.
432 if let Some(loc_id) = temp.att_opt("locationId") {
433 println!("Found temperature recording {} at location ID {}", temp_value, loc_id);
434 } else {
435 println!("Found temperature recording {} at unspecified location.", temp_value);
436 }
437 }
438 Ok(())
439}
440```
441
442**Note:** the `xml:lang` and `xml:space` values cannot be read from as attribute values from an
443`Element`, because these are "special attributes" whose values are inherited by child elements
444(and the language is inherited by an element's attributes too). To get the effective value of
445these language and space properties, see the methods `language_tag` and `white_space_handling`
446instead.
447
448## Namespace handling
449
450All of the examples so far have used XML without any namespace declarations, which means that
451the element and attribute names are not within any namespace (or put another way, they have a
452namespace which has no value). Specifying the target name of an element or attribute can be
453done with a string slice `&str` when the namespace has no value. But when the target name has
454a namespace value, you must specify the namespace in order to target the desired element.
455
456The most direct way of doing this is to use a `(&str, &str)` tuple which contains the local
457part and then **namespace** (not the prefix) of the element name. But you can also call the
458`pre_ns` (preset or predefined namespace) method to let a cursor or XmlPath know that it should
459assume the given namespace value if you don't use a tuple to directly specify the namespace for
460each element and attribute within the method chain. An example is probably be the easiest way to
461explain this.
462
463```
464# use spex::{parsing::XmlReader, xml::XmlDocument, common::XmlError,
465# extraction::ExtendedLanguageRange};
466# fn main() {
467# // Panic if either step generates an error.
468# let xml_doc = read_xml().unwrap();
469# extract_xml(xml_doc).unwrap();
470# }
471#
472# // The XML parsing methods might throw an std::io::Error, so they go into their own method.
473# fn read_xml() -> Result<XmlDocument, std::io::Error> {
474# let xml = "
475<!-- The root element declares that the default namespace for it and its descendants should
476be the given URI. It also declares that any element/attribute using prefix 'pfx' belongs to a
477namespace with a different URI. -->
478<root xmlns='example.com/DefaultNamespace' xmlns:pfx='example.com/OtherNamespace'>
479 <one>This child element has no prefix, so it inherits the default namespace.</one>
480 <pfx:two>This child element has prefix pfx, so inherits the other namespace.</pfx:two>
481 <pfx:three pfx:key='value'>Attribute names can be prefixed too.</pfx:three>
482 <four key2='value2'>Unprefixed attribute names do *not* inherit namespaces.</four>
483 <five xmlns='' key3='value3'>The default namespace can be cleared too.</five>
484</root>
485# ";
486# let xml_doc = XmlReader::parse_auto(xml.as_bytes());
487# xml_doc
488# }
489
490// Once the above XML is turned into an XmlDocument, it gets passed to this method.
491fn extract_xml(xml_doc: XmlDocument) -> Result<(), XmlError> {
492 let root = xml_doc.root();
493 // You can use a tuple to specify the local part and namespace of the targeted element.
494 let one = root.req(("one", "example.com/DefaultNamespace")).element()?;
495
496 // Or you can call pre_ns before a chain of req/opt/first/all/nth/last method calls.
497 let two = root.pre_ns("example.com/OtherNamespace").req("two").element()?;
498
499 // The effect of pre_ns continues until you call element() or text(), so you can keep
500 // assuming the same namespace for child elements or attributes.
501 let three_key = root.pre_ns("example.com/OtherNamespace").req("three").att_req("key")?;
502 # assert_eq!(three_key, "value");
503
504 // Be careful if the namespace changes (or is cleared) when moving down through child
505 // elements and attributes. If that happens, you can call pre_ns again, or you can use a
506 // tuple to explicitly state the different namespace.
507 let four_key = root
508 .pre_ns("example.com/DefaultNamespace")
509 .req("four")
510 .pre_ns("")
511 .att_req("key2")?;
512 # assert_eq!(four_key, "value2");
513
514 // When no namespace applies to a method or attribute name, you don't need to specify any
515 // namespace to target it, so you don't need to use pre_ns nor a tuple. But you can anyway
516 // if you want to make it more explicit that there is no namespace.
517 let five_key = root.req(("five", "")).att_req(("key3", ""))?;
518 # assert_eq!(five_key, "value3");
519
520 Ok(())
521}
522```
523
524It's important to note that once you call `element()` the effect of pre_ns vanishes. So don't
525forget that you if you do call `element()` in the middle of a method chain, you need to call
526`pre_ns` again in order to specify the preset namespace from that point forward.
527
528```
529# use spex::{parsing::XmlReader, xml::XmlDocument, common::XmlError,
530# extraction::ExtendedLanguageRange};
531# fn main() {
532# // Panic if either step generates an error.
533# let xml_doc = read_xml().unwrap();
534# extract_xml(xml_doc).unwrap();
535# }
536#
537# // The XML parsing methods might throw an std::io::Error, so they go into their own method.
538# fn read_xml() -> Result<XmlDocument, std::io::Error> {
539# let xml = "
540<root xmlns='example.com/DefaultNamespace'>
541 <topLevel>
542 <innerLevel>
543 <list>
544 <item>something</item>
545 <item>whatever</item>
546 <item>more</item>
547 <item>and so on</item>
548 </list>
549 </innerLevel>
550 </topLevel>
551</root>
552# ";
553# let xml_doc = XmlReader::parse_auto(xml.as_bytes());
554# xml_doc
555# }
556
557// Defining a static constant makes it quicker to type namespaces, and easier to read the code.
558const NS_DEF: &str = "example.com/DefaultNamespace";
559
560// Once the above XML is turned into an XmlDocument, it gets passed to this method.
561fn extract_xml(xml_doc: XmlDocument) -> Result<(), XmlError> {
562 // Use a chain of req calls to get to the required list, then use an XmlPath to iterate
563 // however many items are found within the list and count them.
564
565 // This first attempt will actually give us the wrong number, because once we call
566 // element()? we receive an `&Element` reference, and the preset namespace effect is lost.
567 // So the XmlPath we chain on straight after that will be searching the empty namespace and
568 // won't find any matching elements and will report a count of zero.
569 let mistake = xml_doc
570 .root()
571 .pre_ns(NS_DEF)
572 .req("topLevel")
573 .req("innerLevel")
574 .req("list")
575 .element()?
576 .all("item")
577 .iter()
578 .count();
579 # assert_eq!(mistake, 0);
580
581 // You can fix the problem by either using an explicit name tuple `("item", NS_DEF)` or by
582 // calling pre_ns again after element() so that the XmlPath knows which namespace should be
583 // used when searching for items.
584 let correct = xml_doc
585 .root()
586 .pre_ns(NS_DEF)
587 .req("topLevel")
588 .req("innerLevel")
589 .req("list")
590 .element()?
591 .pre_ns(NS_DEF)
592 .all("item")
593 .iter()
594 .count();
595 # assert_eq!(correct, 4);
596
597 // However, to avoid confusion, it's recommended to avoid including `element()` between
598 // two different method chains, and to instead assign it to a variable name for clarity.
599 let list = xml_doc
600 .root()
601 .pre_ns(NS_DEF)
602 .req("topLevel")
603 .req("innerLevel")
604 .req("list")
605 .element()?;
606
607 let cleanest = list.all(("item", NS_DEF)).iter().count();
608 # assert_eq!(cleanest, 4);
609
610 Ok(())
611}
612```
613
614## Error handling
615
616The examples above have simplified the code snippets for brevity, but in a real
617application you will need to handle the different error types returned by the different steps
618of reading/parsing and extracting from XML. Here is a compact example which shows the error
619handling needed for each step.
620
621```
622# use std::fs::File;
623# use spex::{parsing::XmlReader, xml::XmlDocument, common::XmlError};
624fn main() {
625 // Decide what to do if either step returns an error.
626 // For simplicity, we'll simply panic in this example, but in a real application you may
627 // want to remap the error to the type used by your application, or trigger some recovery
628 // logic instead.
629 let xml_doc = match read_xml() {
630 Ok(d) => d,
631 Err(e) => panic!("XML reading or parsing failed!"),
632 };
633 match extract_xml(xml_doc) {
634 Ok(()) => println!("Finished extracting from XML document without errors!"),
635 Err(e) => panic!("XML extraction failed!"),
636 }
637}
638
639// The XML parsing methods might throw an std::io::Error, so they go into their own method.
640fn read_xml() -> Result<XmlDocument, std::io::Error> {
641 let xml = "<root><child/></root>";
642 let xml_doc = XmlReader::parse_auto(xml.as_bytes());
643 xml_doc
644}
645
646// The extraction methods might throw an XmlError, so they go into their own method.
647fn extract_xml(xml_doc: XmlDocument) -> Result<(), XmlError> {
648 let child = xml_doc.root().req("child").element()?;
649 # assert_eq!(child.text().unwrap(), "");
650 Ok(())
651}
652```
653
654# Release notes
655
656## 0.1.0
657Initial release.
658
659## 0.2.0
660Moved to [sipp 0.2.0](https://docs.rs/sipp/0.2.0/sipp/).
661
662*/
663
664#![forbid(unsafe_code)]
665#![warn(missing_docs)]
666pub mod common;
667pub mod extraction;
668pub mod parsing;
669pub mod xml;