1mod parser;
4
5use facet_xml as xml;
6use std::collections::HashMap;
7
8pub use parser::{
9 ElementParseError, ElementParser, ElementSerializeError, ElementSerializer, from_element,
10 to_element,
11};
12
13#[derive(Debug, Clone, PartialEq, Eq)]
15pub enum PathError {
16 EmptyPath { path: Vec<usize> },
18 IndexOutOfBounds {
20 path: Vec<usize>,
21 index: usize,
22 len: usize,
23 },
24 TextNodeHasNoChildren { path: Vec<usize> },
26}
27
28impl std::fmt::Display for PathError {
29 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30 match self {
31 PathError::EmptyPath { path } => write!(f, "empty path: {path:?}"),
32 PathError::IndexOutOfBounds { path, index, len } => {
33 write!(
34 f,
35 "index {index} out of bounds (len={len}) at path {path:?}"
36 )
37 }
38 PathError::TextNodeHasNoChildren { path } => {
39 write!(f, "text node has no children at path {path:?}")
40 }
41 }
42 }
43}
44
45impl std::error::Error for PathError {}
46
47#[derive(Debug, Clone, PartialEq, Eq, facet::Facet)]
49#[repr(u8)]
50pub enum Content {
51 #[facet(xml::text)]
53 Text(String),
54 #[facet(xml::custom_element)]
56 Element(Element),
57}
58
59impl Content {
60 pub fn as_text(&self) -> Option<&str> {
62 match self {
63 Content::Text(t) => Some(t),
64 _ => None,
65 }
66 }
67
68 pub fn as_element(&self) -> Option<&Element> {
70 match self {
71 Content::Element(e) => Some(e),
72 _ => None,
73 }
74 }
75}
76
77#[derive(Debug, Clone, PartialEq, Eq, Default, facet::Facet)]
82pub struct Element {
83 #[facet(xml::tag, default)]
85 pub tag: String,
86
87 #[facet(flatten, default)]
89 pub attrs: HashMap<String, String>,
90
91 #[facet(flatten, default)]
93 #[facet(recursive_type)]
94 pub children: Vec<Content>,
95}
96
97impl Element {
98 pub fn new(tag: impl Into<String>) -> Self {
100 Self {
101 tag: tag.into(),
102 attrs: HashMap::new(),
103 children: Vec::new(),
104 }
105 }
106
107 pub fn with_attr(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
109 self.attrs.insert(name.into(), value.into());
110 self
111 }
112
113 pub fn with_child(mut self, child: Element) -> Self {
115 self.children.push(Content::Element(child));
116 self
117 }
118
119 pub fn with_text(mut self, text: impl Into<String>) -> Self {
121 self.children.push(Content::Text(text.into()));
122 self
123 }
124
125 pub fn get_attr(&self, name: &str) -> Option<&str> {
127 self.attrs.get(name).map(|s| s.as_str())
128 }
129
130 pub fn child_elements(&self) -> impl Iterator<Item = &Element> {
132 self.children.iter().filter_map(|c| c.as_element())
133 }
134
135 pub fn text_content(&self) -> String {
137 let mut result = String::new();
138 for child in &self.children {
139 match child {
140 Content::Text(t) => result.push_str(t),
141 Content::Element(e) => result.push_str(&e.text_content()),
142 }
143 }
144 result
145 }
146
147 pub fn get_content_mut(&mut self, path: &[usize]) -> Result<&mut Content, PathError> {
150 if path.is_empty() {
151 return Err(PathError::EmptyPath { path: vec![] });
152 }
153
154 let idx = path[0];
155 let len = self.children.len();
156 let child = self
157 .children
158 .get_mut(idx)
159 .ok_or_else(|| PathError::IndexOutOfBounds {
160 path: path.to_vec(),
161 index: idx,
162 len,
163 })?;
164
165 if path.len() == 1 {
166 return Ok(child);
167 }
168
169 match child {
170 Content::Element(e) => e.get_content_mut(&path[1..]),
171 Content::Text(_) => Err(PathError::TextNodeHasNoChildren {
172 path: path.to_vec(),
173 }),
174 }
175 }
176
177 pub fn children_mut(&mut self, path: &[usize]) -> Result<&mut Vec<Content>, PathError> {
179 if path.is_empty() {
180 return Ok(&mut self.children);
181 }
182 match self.get_content_mut(path)? {
183 Content::Element(e) => Ok(&mut e.children),
184 Content::Text(_) => Err(PathError::TextNodeHasNoChildren {
185 path: path.to_vec(),
186 }),
187 }
188 }
189
190 pub fn attrs_mut(&mut self, path: &[usize]) -> Result<&mut HashMap<String, String>, PathError> {
192 if path.is_empty() {
193 return Ok(&mut self.attrs);
194 }
195 match self.get_content_mut(path)? {
196 Content::Element(e) => Ok(&mut e.attrs),
197 Content::Text(_) => Err(PathError::TextNodeHasNoChildren {
198 path: path.to_vec(),
199 }),
200 }
201 }
202
203 pub fn to_html(&self) -> String {
205 let mut out = String::new();
206 self.write_html(&mut out);
207 out
208 }
209
210 pub fn write_html(&self, out: &mut String) {
212 out.push('<');
213 out.push_str(&self.tag);
214 let mut attr_list: Vec<_> = self.attrs.iter().collect();
216 attr_list.sort_by_key(|(k, _)| *k);
217 for (k, v) in attr_list {
218 out.push(' ');
219 out.push_str(k);
220 out.push_str("=\"");
221 out.push_str(&html_escape(v));
222 out.push('"');
223 }
224 out.push('>');
225 for child in &self.children {
226 match child {
227 Content::Text(s) => out.push_str(s),
228 Content::Element(e) => e.write_html(out),
229 }
230 }
231 out.push_str("</");
232 out.push_str(&self.tag);
233 out.push('>');
234 }
235}
236
237fn html_escape(s: &str) -> String {
238 s.replace('&', "&")
239 .replace('<', "<")
240 .replace('>', ">")
241 .replace('"', """)
242}
243
244impl From<Element> for Content {
245 fn from(e: Element) -> Self {
246 Content::Element(e)
247 }
248}
249
250impl From<String> for Content {
251 fn from(s: String) -> Self {
252 Content::Text(s)
253 }
254}
255
256impl From<&str> for Content {
257 fn from(s: &str) -> Self {
258 Content::Text(s.to_owned())
259 }
260}
261
262#[cfg(test)]
263mod tests {
264 use std::{fmt::Display, str::FromStr};
265
266 use super::*;
267 use facet::Facet;
268 use facet_testhelpers::test;
269
270 #[test]
271 fn element_builder_api() {
272 let elem = Element::new("root")
273 .with_attr("id", "123")
274 .with_child(Element::new("child").with_text("hello world"));
275
276 assert_eq!(elem.tag, "root");
277 assert_eq!(elem.get_attr("id"), Some("123"));
278 assert_eq!(elem.children.len(), 1);
279
280 let child = elem.child_elements().next().unwrap();
281 assert_eq!(child.tag, "child");
282 assert_eq!(child.text_content(), "hello world");
283 }
284
285 #[test]
286 fn parse_simple_xml() {
287 let xml = r#"<root><child>hello</child></root>"#;
288 let elem: Element = facet_xml::from_str(xml).unwrap();
289
290 assert_eq!(elem.tag, "root");
291 assert_eq!(elem.children.len(), 1);
292
293 let child = elem.child_elements().next().unwrap();
294 assert_eq!(child.tag, "child");
295 assert_eq!(child.text_content(), "hello");
296 }
297
298 #[test]
299 fn parse_with_attributes() {
300 let xml = r#"<root id="123" class="test"><child name="foo">bar</child></root>"#;
301 let elem: Element = facet_xml::from_str(xml).unwrap();
302
303 assert_eq!(elem.tag, "root");
304 assert_eq!(elem.get_attr("id"), Some("123"));
305 assert_eq!(elem.get_attr("class"), Some("test"));
306
307 let child = elem.child_elements().next().unwrap();
308 assert_eq!(child.get_attr("name"), Some("foo"));
309 assert_eq!(child.text_content(), "bar");
310 }
311
312 #[test]
313 fn parse_mixed_content() {
314 let xml = r#"<p>Hello <b>world</b>!</p>"#;
315 let elem: Element = facet_xml::from_str(xml).unwrap();
316
317 assert_eq!(elem.tag, "p");
318 assert_eq!(elem.children.len(), 3);
319 assert_eq!(elem.children[0].as_text(), Some("Hello"));
321 assert_eq!(elem.children[1].as_element().unwrap().tag, "b");
322 assert_eq!(elem.children[2].as_text(), Some("!"));
323 assert_eq!(elem.text_content(), "Helloworld!");
324 }
325
326 #[test]
327 fn from_element_to_struct() {
328 #[derive(facet::Facet, Debug, PartialEq)]
329 struct Person {
330 name: String,
331 age: u32,
332 }
333
334 let elem = Element::new("person")
335 .with_child(Element::new("name").with_text("Alice"))
336 .with_child(Element::new("age").with_text("30"));
337
338 let person: Person = from_element(&elem).unwrap();
339 assert_eq!(person.name, "Alice");
340 assert_eq!(person.age, 30);
341 }
342
343 #[test]
344 fn from_element_with_attrs() {
345 #[derive(facet::Facet, Debug, PartialEq)]
346 struct Item {
347 #[facet(xml::attribute)]
348 id: String,
349 value: String,
350 }
351
352 let elem = Element::new("item")
353 .with_attr("id", "123")
354 .with_child(Element::new("value").with_text("hello"));
355
356 let item: Item = from_element(&elem).unwrap();
357 assert_eq!(item.id, "123");
358 assert_eq!(item.value, "hello");
359 }
360
361 #[test]
362 fn to_element_simple() {
363 #[derive(facet::Facet, Debug, PartialEq)]
364 struct Person {
365 name: String,
366 age: u32,
367 }
368
369 let person = Person {
370 name: "Alice".to_string(),
371 age: 30,
372 };
373
374 let elem = to_element(&person).unwrap();
375 assert_eq!(elem.tag, "person");
376 assert_eq!(elem.children.len(), 2);
377
378 let name_child = elem.child_elements().find(|e| e.tag == "name").unwrap();
379 assert_eq!(name_child.text_content(), "Alice");
380
381 let age_child = elem.child_elements().find(|e| e.tag == "age").unwrap();
382 assert_eq!(age_child.text_content(), "30");
383 }
384
385 #[test]
386 fn to_element_with_attrs() {
387 #[derive(facet::Facet, Debug, PartialEq)]
388 struct Item {
389 #[facet(xml::attribute)]
390 id: String,
391 value: String,
392 }
393
394 let item = Item {
395 id: "123".to_string(),
396 value: "hello".to_string(),
397 };
398
399 let elem = to_element(&item).unwrap();
400 assert_eq!(elem.tag, "item");
401 assert_eq!(elem.get_attr("id"), Some("123"));
402
403 let value_child = elem.child_elements().find(|e| e.tag == "value").unwrap();
404 assert_eq!(value_child.text_content(), "hello");
405 }
406
407 #[test]
408 fn roundtrip_simple() {
409 #[derive(facet::Facet, Debug, PartialEq)]
410 struct Person {
411 name: String,
412 age: u32,
413 }
414
415 let original = Person {
416 name: "Bob".to_string(),
417 age: 42,
418 };
419
420 let elem = to_element(&original).unwrap();
421 let roundtripped: Person = from_element(&elem).unwrap();
422
423 assert_eq!(original, roundtripped);
424 }
425
426 #[test]
427 fn roundtrip_with_attrs() {
428 #[derive(facet::Facet, Debug, PartialEq)]
429 struct Item {
430 #[facet(xml::attribute)]
431 id: String,
432 #[facet(xml::attribute)]
433 version: u32,
434 value: String,
435 }
436
437 let original = Item {
438 id: "test-123".to_string(),
439 version: 5,
440 value: "content".to_string(),
441 };
442
443 let elem = to_element(&original).unwrap();
444 let roundtripped: Item = from_element(&elem).unwrap();
445
446 assert_eq!(original, roundtripped);
447 }
448
449 #[test]
452 fn vec_element_matches_any_tag() {
453 #[derive(facet::Facet, Debug)]
454 #[facet(rename = "any")]
455 struct AnyContainer {
456 #[facet(xml::elements)]
457 elements: Vec<Element>,
458 }
459
460 let xml = r#"<any><foo a="b" /><bar c="d" /></any>"#;
461 let result: AnyContainer = facet_xml::from_str(xml).unwrap();
462
463 assert_eq!(result.elements.len(), 2);
464 assert_eq!(result.elements[0].tag, "foo");
465 assert_eq!(result.elements[0].get_attr("a"), Some("b"));
466 assert_eq!(result.elements[1].tag, "bar");
467 assert_eq!(result.elements[1].get_attr("c"), Some("d"));
468 }
469
470 #[test]
472 fn vec_element_catch_all_with_specific_field() {
473 #[derive(facet::Facet, Debug)]
474 #[facet(rename = "container")]
475 struct MixedContainer {
476 name: String,
478 #[facet(xml::elements)]
480 others: Vec<Element>,
481 }
482
483 let xml = r#"<container><name>test</name><foo>a</foo><bar>b</bar></container>"#;
484 let result: MixedContainer = facet_xml::from_str(xml).unwrap();
485
486 assert_eq!(result.name, "test");
487 assert_eq!(result.others.len(), 2);
488 assert_eq!(result.others[0].tag, "foo");
489 assert_eq!(result.others[1].tag, "bar");
490 }
491
492 #[test]
494 fn vec_element_ignores_text_nodes() {
495 #[derive(facet::Facet, Debug)]
496 #[facet(rename = "any")]
497 struct AnyContainer {
498 #[facet(xml::elements)]
499 elements: Vec<Element>,
500 }
501
502 let xml = r#"<any>text before<foo/>middle text<bar/>text after</any>"#;
504 let result: AnyContainer = facet_xml::from_str(xml).unwrap();
505
506 assert_eq!(result.elements.len(), 2);
507 assert_eq!(result.elements[0].tag, "foo");
508 assert_eq!(result.elements[1].tag, "bar");
509 }
510
511 #[test]
516 fn vec_element_roundtrip() {
517 #[derive(facet::Facet, Debug, PartialEq)]
518 #[facet(rename = "container")]
519 struct Container {
520 #[facet(xml::elements)]
521 elements: Vec<Element>,
522 }
523
524 let original = Container {
525 elements: vec![
526 Element::new("foo").with_attr("a", "1"),
527 Element::new("bar").with_text("hello"),
528 ],
529 };
530
531 let xml = facet_xml::to_string(&original).unwrap();
532
533 assert!(xml.contains("<foo"), "expected <foo>, got: {}", xml);
536 assert!(xml.contains("<bar"), "expected <bar>, got: {}", xml);
537
538 let roundtripped: Container = facet_xml::from_str(&xml).unwrap();
540 assert_eq!(roundtripped.elements.len(), 2);
541 assert_eq!(roundtripped.elements[0].tag, "foo");
542 assert_eq!(roundtripped.elements[0].get_attr("a"), Some("1"));
543 assert_eq!(roundtripped.elements[1].tag, "bar");
544 assert_eq!(roundtripped.elements[1].text_content(), "hello");
545 }
546
547 #[test]
549 fn vec_element_empty_container() {
550 #[derive(facet::Facet, Debug)]
551 #[facet(rename = "empty")]
552 struct EmptyContainer {
553 #[facet(xml::elements)]
554 elements: Vec<Element>,
555 }
556
557 let xml = r#"<empty></empty>"#;
558 let result: EmptyContainer = facet_xml::from_str(xml).unwrap();
559
560 assert!(result.elements.is_empty());
561 }
562
563 #[derive(Debug, Facet)]
564 #[facet(proxy = StringRepr)]
565 struct ConstantName;
566
567 #[derive(Debug, Facet)]
569 #[repr(transparent)]
570 pub(crate) struct StringRepr(pub String);
571
572 impl Display for ConstantName {
573 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
574 write!(f, "CONSTANT")
575 }
576 }
577
578 impl FromStr for ConstantName {
579 type Err = &'static str;
580 fn from_str(s: &str) -> Result<Self, Self::Err> {
581 if s == "CONSTANT" {
582 Ok(Self)
583 } else {
584 Err("expected `CONSTANT`")
585 }
586 }
587 }
588
589 impl From<ConstantName> for StringRepr {
590 fn from(value: ConstantName) -> Self {
591 Self(value.to_string())
592 }
593 }
594 impl From<&ConstantName> for StringRepr {
595 fn from(value: &ConstantName) -> Self {
596 Self(value.to_string())
597 }
598 }
599 impl TryFrom<StringRepr> for ConstantName {
600 type Error = <ConstantName as core::str::FromStr>::Err;
601 fn try_from(value: StringRepr) -> Result<Self, Self::Error> {
602 value.0.parse()
603 }
604 }
605 impl TryFrom<&StringRepr> for ConstantName {
606 type Error = <ConstantName as core::str::FromStr>::Err;
607 fn try_from(value: &StringRepr) -> Result<Self, Self::Error> {
608 value.0.parse()
609 }
610 }
611
612 #[derive(Debug, Facet)]
613 #[repr(C)]
614 enum Foo {
615 #[facet(rename = "foo")]
616 Value {
617 #[facet(xml::attribute)]
618 #[allow(unused)]
619 name: ConstantName,
620 #[facet(xml::attribute)]
621 #[allow(unused)]
622 exists: String,
623 },
624 }
625
626 #[test]
627 fn transparent_attribute_not_discarded() {
628 let raw_xml = r#"
629<foo name="CONSTANT" exists="i do exist and am not discarded"></foo>"#;
630 let x: Foo = facet_xml::from_str(raw_xml).unwrap();
631 let element = crate::to_element(&x).unwrap();
632 let _ = facet_xml::to_string(&x).unwrap();
633 let _ = facet_xml::to_string(&element).unwrap();
634 assert!(
635 element.attrs.contains_key("exists"),
636 "this attribute is not discarded"
637 );
638 assert_eq!(element.attrs["name"], "CONSTANT", "name is not discarded");
639 }
640}