-
Notifications
You must be signed in to change notification settings - Fork 290
Expand file tree
/
Copy pathreader_patterns.rs
More file actions
325 lines (294 loc) · 13 KB
/
Copy pathreader_patterns.rs
File metadata and controls
325 lines (294 loc) · 13 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
//! Two patterns for structuring a hand-written reader.
//!
//! Once a document is more than a flat list of elements, the naive "one big
//! `match` in one big loop" becomes hard to follow. This example shows the two
//! patterns most people reach for, parsing the *same* document into the *same*
//! `Vec<Book>` so you can compare them directly:
//!
//! 1. `parse_state_machine` — track "where am I?" in an explicit `enum` and
//! `match` on `(state, event)` pairs. Matching the state and the event
//! together makes the grammar you accept very visible, and it reuses a
//! single buffer for the whole document. This version is written to
//! *strictly validate* structure: any `(state, event)` combination the
//! grammar doesn't allow is an error rather than silently ignored.
//!
//! 2. `parse_nested_readers` — when you recognize the start of a subtree (a
//! `<book>`), hand control to a helper function that consumes just that
//! subtree with its own inner loop. This reads naturally when the document
//! is deeply nested, at the cost of being harder to share one read buffer
//! across levels (each helper tends to want its own). It validates just as
//! strictly as the state machine, but the context lives in the call stack
//! instead of an explicit `enum`.
//!
//! Both parse the same document and reject anything the grammar doesn't allow;
//! each shows, in comments, how to relax that where you'd want to. Neither
//! structuring pattern is "correct" — pick whichever keeps *your* document
//! readable. And before writing either by hand, check whether serde (`serde_roundtrip.rs`)
//! would do the whole job for you. See `examples/README.md` for the full
//! decision guide.
//!
//! Run it with:
//!
//! ```console
//! cargo run --example reader_patterns
//! ```
use quick_xml::XmlVersion;
use quick_xml::events::{BytesStart, Event};
use quick_xml::reader::Reader;
const XML: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
<catalog>
<book id="b1">
<title>The Rust Programming Language</title>
<author>Steve Klabnik</author>
<author>Carol Nichols</author>
</book>
<book id="b2">
<title>Programming Rust</title>
<author>Jim Blandy</author>
</book>
</catalog>"#;
#[derive(Debug, PartialEq, Default)]
struct Book {
id: String,
title: String,
authors: Vec<String>,
}
/// Read the `id` attribute off a start tag, or return an empty string.
fn id_of(e: &BytesStart, version: XmlVersion) -> Result<String, quick_xml::Error> {
for attr in e.attributes() {
let attr = attr?;
if attr.key.as_ref() == "id" {
return Ok(attr.normalized_value(version)?.into_owned());
}
}
Ok(String::new())
}
// ---------------------------------------------------------------------------
// Pattern 1: explicit state machine
// ---------------------------------------------------------------------------
/// Where we currently are in the document.
#[derive(Debug)]
enum State {
/// Before the root element. The XML declaration may still appear here.
BeforeRoot,
/// Inside `<catalog>`, between books.
InCatalog,
/// Inside a `<book>`, accumulating its fields.
InBook(Book),
/// Inside a text-bearing child of a book; remember which one so we know
/// where to store the upcoming `Text` event.
InField(Book, Field),
/// After the root element closed; only `Eof` may follow.
AfterRoot,
}
#[derive(Debug, Clone, Copy)]
enum Field {
Title,
Author,
}
fn parse_state_machine(xml: &str) -> Result<Vec<Book>, Box<dyn std::error::Error>> {
// There is a `trim_text` option available on `Reader` which can sometimes
// simplify the parser greatly, however, it is a bit bug-prone in some cases.
// We will not use it here - the reader hands us the whitespace between
// elements as `Text` events, and we skip it explicitly below. That keeps
// control over which whitespace matters (see the `Text` arms) and avoids
// `trim_text`'s known issues around comments/CDATA.
let mut reader = Reader::from_str(xml);
let mut xml_version = XmlVersion::Implicit1_0;
let mut books = Vec::new();
let mut state = State::BeforeRoot;
loop {
// Matching on `(state, event)` together makes the accepted grammar
// explicit: each arm says "in this state, on this event, do X and move
// to state Y". Every arm returns the next state. Because the final arm
// rejects everything else, the set of arms below *is* the grammar this
// parser accepts.
state = match (state, reader.read_event()?) {
// The optional `<?xml ...?>` declaration before the root element.
(State::BeforeRoot, Event::Decl(e)) => {
xml_version = e.xml_version()?;
State::BeforeRoot
}
// The root element opens the catalog.
(State::BeforeRoot, Event::Start(e)) if e.name().as_ref() == "catalog" => {
State::InCatalog
}
// Enter a book, capturing its id up front.
(State::InCatalog, Event::Start(e)) if e.name().as_ref() == "book" => {
State::InBook(Book {
id: id_of(&e, xml_version)?,
..Default::default()
})
}
// The root element closes; nothing but `Eof` is allowed after this.
(State::InCatalog, Event::End(e)) if e.name().as_ref() == "catalog" => State::AfterRoot,
// Enter a known field inside a book (`Field` is `Copy`, so the text
// arm below can store it and hand it back unchanged).
(State::InBook(book), Event::Start(e)) if e.name().as_ref() == "title" => {
State::InField(book, Field::Title)
}
(State::InBook(book), Event::Start(e)) if e.name().as_ref() == "author" => {
State::InField(book, Field::Author)
}
// Closing the book stores it.
(State::InBook(book), Event::End(e)) if e.name().as_ref() == "book" => {
books.push(book);
State::InCatalog
}
// To *tolerate* unknown children instead of rejecting them (e.g. for
// forward compatibility), add an arm here that skips the subtree and
// stays in the book:
//
// (State::InBook(book), Event::Start(e)) => {
// reader.read_to_end(e.name())?;
// State::InBook(book)
// }
// The text inside a field goes to the slot the state remembered.
(State::InField(mut book, field), Event::Text(e)) => {
let text = e.xml_content(xml_version).into_owned();
match field {
Field::Title => book.title = text,
Field::Author => book.authors.push(text),
}
State::InField(book, field)
}
// Closing a field returns us to the book.
(State::InField(book, _), Event::End(_)) => State::InBook(book),
// Insignificant whitespace between elements (indentation, newlines).
// Because this grammar has no mixed content, any all-whitespace text
// outside a field can be ignored; binding `state` by value hands it
// back unchanged, `Book` payload and all. Real field text is caught
// by the `InField` arm above, so it never reaches here.
(state, Event::Text(e)) if e.trim().is_empty() => state,
(State::AfterRoot, Event::Eof) => break,
// Anything else is a structural error. This is the strict default:
// an unexpected event stops parsing rather than being silently
// ignored.
//
// To make the parser *lax* instead — ignoring anything the grammar
// doesn't mention and simply advancing — replace this arm with one
// that keeps the state unchanged:
//
// (state, _) => state,
(state, event) => {
return Err(format!("unexpected {event:?} in state {state:?}").into());
}
};
}
Ok(books)
}
// ---------------------------------------------------------------------------
// Pattern 2: nested readers
// ---------------------------------------------------------------------------
// Like `parse_state_machine`, this version *strictly validates* structure:
// instead of ignoring events it doesn't recognize, each loop rejects them. The
// call stack carries the context (top level vs. inside a book) that the state
// machine had to spell out in an `enum`.
fn parse_nested_readers(xml: &str) -> Result<Vec<Book>, Box<dyn std::error::Error>> {
// As in `parse_state_machine`, `trim_text` stays off and we skip the
// whitespace between elements ourselves. Here that costs one `Text` arm per
// loop, since each loop is its own `match`.
let mut reader = Reader::from_str(xml);
let mut xml_version = XmlVersion::Implicit1_0;
// Preamble and root: skip an optional `<?xml ...?>` declaration, then
// require the opening `<catalog>` before anything else.
loop {
match reader.read_event()? {
Event::Decl(e) => xml_version = e.xml_version()?,
Event::Text(e) if e.trim().is_empty() => {}
Event::Start(e) if e.name().as_ref() == "catalog" => break,
event => return Err(format!("expected <catalog>, got {event:?}").into()),
}
}
// Inside the catalog: each child must be a `<book>` (delegated to
// `read_book`) until the catalog's own end tag.
let mut books = Vec::new();
loop {
match reader.read_event()? {
Event::Start(e) if e.name().as_ref() == "book" => {
books.push(read_book(&mut reader, &e, xml_version)?);
}
Event::End(e) if e.name().as_ref() == "catalog" => break,
Event::Text(e) if e.trim().is_empty() => {}
event => return Err(format!("unexpected {event:?} inside <catalog>").into()),
}
}
// After the root closes, only trailing whitespace and end-of-file remain.
loop {
match reader.read_event()? {
Event::Text(e) if e.trim().is_empty() => {}
Event::Eof => break,
event => return Err(format!("expected end of document, got {event:?}").into()),
}
}
Ok(books)
}
/// Consume a single `<book>...</book>` subtree, starting *after* its start tag
/// has been read, and stopping once its matching end tag is consumed.
fn read_book(
reader: &mut Reader<&[u8]>,
start: &BytesStart,
version: XmlVersion,
) -> Result<Book, Box<dyn std::error::Error>> {
let mut book = Book {
id: id_of(start, version)?,
..Default::default()
};
loop {
match reader.read_event()? {
Event::Start(e) => match e.name().as_ref() {
// `read_text` consumes the child element's text and its end tag
// in one call, which keeps this loop flat.
"title" => {
book.title = reader
.read_text(e.name())?
.xml_content(version)
.into_owned();
}
"author" => {
book.authors.push(
reader
.read_text(e.name())?
.xml_content(version)
.into_owned(),
);
}
// Unknown child: a structural error under strict parsing. To
// *tolerate* unknown children instead (e.g. for forward
// compatibility), skip the subtree and continue:
//
// name => { reader.read_to_end(e.name())?; }
name => {
return Err(format!("unexpected <{name}> inside <book>").into());
}
},
// The book's own end tag: we are done with this subtree.
Event::End(e) if e.name().as_ref() == "book" => break,
// Whitespace between the book's children: ignore it.
Event::Text(e) if e.trim().is_empty() => {}
event => return Err(format!("unexpected {event:?} inside <book>").into()),
}
}
Ok(book)
}
fn main() -> Result<(), Box<dyn std::error::Error>> {
let expected = vec![
Book {
id: "b1".to_string(),
title: "The Rust Programming Language".to_string(),
authors: vec!["Steve Klabnik".to_string(), "Carol Nichols".to_string()],
},
Book {
id: "b2".to_string(),
title: "Programming Rust".to_string(),
authors: vec!["Jim Blandy".to_string()],
},
];
let via_state_machine = parse_state_machine(XML)?;
let via_nested = parse_nested_readers(XML)?;
println!("state machine: {via_state_machine:#?}");
// Both patterns produce the same result.
assert_eq!(via_state_machine, expected);
assert_eq!(via_nested, expected);
Ok(())
}