1use facet::Facet;
39use facet_format::FormatDeserializer;
40use facet_xml as xml;
41use facet_xml::{XmlParser, to_vec};
42
43pub const ATOM_NS: &str = "http://www.w3.org/2005/Atom";
45
46pub type Error = facet_format::DeserializeError<facet_xml::XmlError>;
48
49pub type SerializeError = facet_format::SerializeError<facet_xml::XmlSerializeError>;
51
52pub fn from_str<'input, T>(xml: &'input str) -> Result<T, Error>
54where
55 T: Facet<'input>,
56{
57 let parser = XmlParser::new(xml.as_bytes());
58 let mut de = FormatDeserializer::new(parser);
59 de.deserialize()
60}
61
62pub fn from_slice<'input, T>(xml: &'input [u8]) -> Result<T, Error>
64where
65 T: Facet<'input>,
66{
67 let parser = XmlParser::new(xml);
68 let mut de = FormatDeserializer::new(parser);
69 de.deserialize()
70}
71
72pub fn to_string<'facet, T>(value: &T) -> Result<String, SerializeError>
74where
75 T: Facet<'facet> + ?Sized,
76{
77 let bytes = to_vec(value)?;
78 Ok(String::from_utf8(bytes).expect("XmlSerializer produces valid UTF-8"))
79}
80
81#[derive(Facet, Debug, Clone, Default)]
106#[facet(
107 xml::ns_all = "http://www.w3.org/2005/Atom",
108 rename = "feed",
109 skip_all_unless_truthy
110)]
111pub struct Feed {
112 #[facet(xml::element)]
115 pub id: Option<String>,
116
117 #[facet(xml::element)]
119 pub title: Option<TextContent>,
120
121 #[facet(xml::element)]
124 pub updated: Option<String>,
125
126 #[facet(xml::elements, rename = "author")]
128 pub authors: Vec<Person>,
129
130 #[facet(xml::elements, rename = "link")]
132 pub links: Vec<Link>,
133
134 #[facet(xml::elements, rename = "category")]
136 pub categories: Vec<Category>,
137
138 #[facet(xml::elements, rename = "contributor")]
140 pub contributors: Vec<Person>,
141
142 #[facet(xml::element)]
144 pub generator: Option<Generator>,
145
146 #[facet(xml::element)]
148 pub icon: Option<String>,
149
150 #[facet(xml::element)]
152 pub logo: Option<String>,
153
154 #[facet(xml::element)]
156 pub rights: Option<TextContent>,
157
158 #[facet(xml::element)]
160 pub subtitle: Option<TextContent>,
161
162 #[facet(xml::elements, rename = "entry")]
164 pub entries: Vec<Entry>,
165}
166
167#[derive(Facet, Debug, Clone, Default)]
179#[facet(
180 xml::ns_all = "http://www.w3.org/2005/Atom",
181 rename = "entry",
182 skip_all_unless_truthy
183)]
184pub struct Entry {
185 #[facet(xml::element)]
187 pub id: Option<String>,
188
189 #[facet(xml::element)]
191 pub title: Option<TextContent>,
192
193 #[facet(xml::element)]
195 pub updated: Option<String>,
196
197 #[facet(xml::elements, rename = "author")]
199 pub authors: Vec<Person>,
200
201 #[facet(xml::elements, rename = "link")]
203 pub links: Vec<Link>,
204
205 #[facet(xml::elements, rename = "category")]
207 pub categories: Vec<Category>,
208
209 #[facet(xml::elements, rename = "contributor")]
211 pub contributors: Vec<Person>,
212
213 #[facet(xml::element)]
215 pub content: Option<Content>,
216
217 #[facet(xml::element)]
219 pub published: Option<String>,
220
221 #[facet(xml::element)]
223 pub rights: Option<TextContent>,
224
225 #[facet(xml::element)]
227 pub summary: Option<TextContent>,
228
229 #[facet(xml::element)]
231 pub source: Option<Source>,
232}
233
234#[derive(Facet, Debug, Clone, Default)]
239#[facet(
240 xml::ns_all = "http://www.w3.org/2005/Atom",
241 rename = "source",
242 skip_all_unless_truthy
243)]
244pub struct Source {
245 #[facet(xml::element)]
247 pub id: Option<String>,
248
249 #[facet(xml::element)]
251 pub title: Option<TextContent>,
252
253 #[facet(xml::element)]
255 pub updated: Option<String>,
256
257 #[facet(xml::elements, rename = "author")]
259 pub authors: Vec<Person>,
260
261 #[facet(xml::elements, rename = "link")]
263 pub links: Vec<Link>,
264
265 #[facet(xml::elements, rename = "category")]
267 pub categories: Vec<Category>,
268
269 #[facet(xml::elements, rename = "contributor")]
271 pub contributors: Vec<Person>,
272
273 #[facet(xml::element)]
275 pub generator: Option<Generator>,
276
277 #[facet(xml::element)]
279 pub icon: Option<String>,
280
281 #[facet(xml::element)]
283 pub logo: Option<String>,
284
285 #[facet(xml::element)]
287 pub rights: Option<TextContent>,
288
289 #[facet(xml::element)]
291 pub subtitle: Option<TextContent>,
292}
293
294#[derive(Facet, Debug, Clone, Default)]
302#[facet(xml::ns_all = "http://www.w3.org/2005/Atom", skip_all_unless_truthy)]
303pub struct Person {
304 #[facet(xml::element)]
306 pub name: Option<String>,
307
308 #[facet(xml::element)]
310 pub uri: Option<String>,
311
312 #[facet(xml::element)]
314 pub email: Option<String>,
315}
316
317#[derive(Facet, Debug, Clone, Copy, Default, PartialEq, Eq)]
323#[facet(rename_all = "lowercase")]
324#[repr(u8)]
325pub enum TextType {
326 #[default]
328 Text,
329 Html,
331 Xhtml,
333}
334
335#[derive(Facet, Debug, Clone, Default)]
342#[facet(xml::ns_all = "http://www.w3.org/2005/Atom", skip_all_unless_truthy)]
343pub struct TextContent {
344 #[facet(xml::attribute, rename = "type")]
346 pub content_type: Option<TextType>,
347
348 #[facet(xml::text)]
351 pub content: Option<String>,
352}
353
354#[derive(Facet, Debug, Clone, Default)]
362#[facet(xml::ns_all = "http://www.w3.org/2005/Atom", skip_all_unless_truthy)]
363pub struct Link {
364 #[facet(xml::attribute)]
366 pub href: Option<String>,
367
368 #[facet(xml::attribute)]
371 pub rel: Option<String>,
372
373 #[facet(xml::attribute, rename = "type")]
375 pub media_type: Option<String>,
376
377 #[facet(xml::attribute)]
379 pub hreflang: Option<String>,
380
381 #[facet(xml::attribute)]
383 pub title: Option<String>,
384
385 #[facet(xml::attribute)]
387 pub length: Option<u64>,
388}
389
390#[derive(Facet, Debug, Clone, Default)]
396#[facet(xml::ns_all = "http://www.w3.org/2005/Atom", skip_all_unless_truthy)]
397pub struct Category {
398 #[facet(xml::attribute)]
400 pub term: Option<String>,
401
402 #[facet(xml::attribute)]
404 pub scheme: Option<String>,
405
406 #[facet(xml::attribute)]
408 pub label: Option<String>,
409}
410
411#[derive(Facet, Debug, Clone, Default)]
417#[facet(xml::ns_all = "http://www.w3.org/2005/Atom", skip_all_unless_truthy)]
418pub struct Generator {
419 #[facet(xml::attribute)]
421 pub uri: Option<String>,
422
423 #[facet(xml::attribute)]
425 pub version: Option<String>,
426
427 #[facet(xml::text)]
429 pub name: Option<String>,
430}
431
432#[derive(Facet, Debug, Clone, Default)]
441#[facet(xml::ns_all = "http://www.w3.org/2005/Atom", skip_all_unless_truthy)]
442pub struct Content {
443 #[facet(xml::attribute, rename = "type")]
446 pub content_type: Option<String>,
447
448 #[facet(xml::attribute)]
450 pub src: Option<String>,
451
452 #[facet(xml::text)]
455 pub body: Option<String>,
456}
457
458pub use facet_xml;
460
461#[cfg(test)]
462mod tests {
463 use super::*;
464 use indoc::indoc;
465
466 #[test]
467 fn test_parse_basic_feed() {
468 let xml = indoc! {r#"
469 <?xml version="1.0" encoding="utf-8"?>
470 <feed xmlns="http://www.w3.org/2005/Atom">
471 <title>Example Feed</title>
472 <id>urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6</id>
473 <updated>2003-12-13T18:30:02Z</updated>
474 <author>
475 <name>John Doe</name>
476 </author>
477 <link href="http://example.org/"/>
478 </feed>
479 "#};
480
481 let feed: Feed = from_str(xml).unwrap();
482
483 assert_eq!(
484 feed.id.as_deref(),
485 Some("urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6")
486 );
487 assert_eq!(
488 feed.title.as_ref().and_then(|t| t.content.as_deref()),
489 Some("Example Feed")
490 );
491 assert_eq!(feed.updated.as_deref(), Some("2003-12-13T18:30:02Z"));
492 assert_eq!(feed.authors.len(), 1);
493 assert_eq!(
494 feed.authors.first().and_then(|a| a.name.as_deref()),
495 Some("John Doe")
496 );
497 assert_eq!(feed.links.len(), 1);
498 assert_eq!(
499 feed.links.first().and_then(|l| l.href.as_deref()),
500 Some("http://example.org/")
501 );
502 }
503
504 #[test]
505 fn test_parse_feed_with_entries() {
506 let xml = indoc! {r#"
507 <?xml version="1.0" encoding="utf-8"?>
508 <feed xmlns="http://www.w3.org/2005/Atom">
509 <title>Example Feed</title>
510 <id>urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6</id>
511 <updated>2003-12-13T18:30:02Z</updated>
512 <entry>
513 <title>Atom-Powered Robots Run Amok</title>
514 <id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a</id>
515 <updated>2003-12-13T18:30:02Z</updated>
516 <link href="http://example.org/2003/12/13/atom03"/>
517 <summary>Some text.</summary>
518 </entry>
519 </feed>
520 "#};
521
522 let feed: Feed = from_str(xml).unwrap();
523
524 assert_eq!(feed.entries.len(), 1);
525 let entry = &feed.entries[0];
526 assert_eq!(
527 entry.title.as_ref().and_then(|t| t.content.as_deref()),
528 Some("Atom-Powered Robots Run Amok")
529 );
530 assert_eq!(
531 entry.id.as_deref(),
532 Some("urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a")
533 );
534 assert_eq!(
535 entry.summary.as_ref().and_then(|s| s.content.as_deref()),
536 Some("Some text.")
537 );
538 }
539
540 #[test]
541 fn test_parse_entry_with_content() {
542 let xml = indoc! {r#"
543 <?xml version="1.0" encoding="utf-8"?>
544 <feed xmlns="http://www.w3.org/2005/Atom">
545 <title>Test</title>
546 <id>test:feed</id>
547 <updated>2024-01-01T00:00:00Z</updated>
548 <entry>
549 <title>Test Entry</title>
550 <id>test:entry:1</id>
551 <updated>2024-01-01T00:00:00Z</updated>
552 <content type="html"><p>Hello, World!</p></content>
553 </entry>
554 </feed>
555 "#};
556
557 let feed: Feed = from_str(xml).unwrap();
558 let entry = &feed.entries[0];
559 let content = entry.content.as_ref().unwrap();
560
561 assert_eq!(content.content_type.as_deref(), Some("html"));
562 assert_eq!(content.body.as_deref(), Some("<p>Hello, World!</p>"));
563 }
564
565 #[test]
566 fn test_parse_link_attributes() {
567 let xml = indoc! {r#"
568 <?xml version="1.0" encoding="utf-8"?>
569 <feed xmlns="http://www.w3.org/2005/Atom">
570 <title>Test</title>
571 <id>test:feed</id>
572 <updated>2024-01-01T00:00:00Z</updated>
573 <link href="http://example.org/" rel="alternate" type="text/html" hreflang="en" title="Example"/>
574 <link href="http://example.org/feed.atom" rel="self" type="application/atom+xml"/>
575 </feed>
576 "#};
577
578 let feed: Feed = from_str(xml).unwrap();
579
580 assert_eq!(feed.links.len(), 2);
581
582 let alternate = &feed.links[0];
583 assert_eq!(alternate.href.as_deref(), Some("http://example.org/"));
584 assert_eq!(alternate.rel.as_deref(), Some("alternate"));
585 assert_eq!(alternate.media_type.as_deref(), Some("text/html"));
586 assert_eq!(alternate.hreflang.as_deref(), Some("en"));
587 assert_eq!(alternate.title.as_deref(), Some("Example"));
588
589 let self_link = &feed.links[1];
590 assert_eq!(
591 self_link.href.as_deref(),
592 Some("http://example.org/feed.atom")
593 );
594 assert_eq!(self_link.rel.as_deref(), Some("self"));
595 }
596
597 #[test]
598 fn test_parse_category() {
599 let xml = indoc! {r#"
600 <?xml version="1.0" encoding="utf-8"?>
601 <feed xmlns="http://www.w3.org/2005/Atom">
602 <title>Test</title>
603 <id>test:feed</id>
604 <updated>2024-01-01T00:00:00Z</updated>
605 <category term="technology" scheme="http://example.org/categories" label="Technology"/>
606 </feed>
607 "#};
608
609 let feed: Feed = from_str(xml).unwrap();
610
611 assert_eq!(feed.categories.len(), 1);
612 let cat = &feed.categories[0];
613 assert_eq!(cat.term.as_deref(), Some("technology"));
614 assert_eq!(cat.scheme.as_deref(), Some("http://example.org/categories"));
615 assert_eq!(cat.label.as_deref(), Some("Technology"));
616 }
617
618 #[test]
619 fn test_parse_generator() {
620 let xml = indoc! {r#"
621 <?xml version="1.0" encoding="utf-8"?>
622 <feed xmlns="http://www.w3.org/2005/Atom">
623 <title>Test</title>
624 <id>test:feed</id>
625 <updated>2024-01-01T00:00:00Z</updated>
626 <generator uri="http://example.org/generator" version="1.0">Example Generator</generator>
627 </feed>
628 "#};
629
630 let feed: Feed = from_str(xml).unwrap();
631
632 let generator = feed.generator.as_ref().unwrap();
633 assert_eq!(generator.name.as_deref(), Some("Example Generator"));
634 assert_eq!(
635 generator.uri.as_deref(),
636 Some("http://example.org/generator")
637 );
638 assert_eq!(generator.version.as_deref(), Some("1.0"));
639 }
640
641 #[test]
642 fn test_parse_person_full() {
643 let xml = indoc! {r#"
644 <?xml version="1.0" encoding="utf-8"?>
645 <feed xmlns="http://www.w3.org/2005/Atom">
646 <title>Test</title>
647 <id>test:feed</id>
648 <updated>2024-01-01T00:00:00Z</updated>
649 <author>
650 <name>John Doe</name>
651 <uri>http://example.org/johndoe</uri>
652 <email>john@example.org</email>
653 </author>
654 <contributor>
655 <name>Jane Smith</name>
656 </contributor>
657 </feed>
658 "#};
659
660 let feed: Feed = from_str(xml).unwrap();
661
662 assert_eq!(feed.authors.len(), 1);
663 let author = &feed.authors[0];
664 assert_eq!(author.name.as_deref(), Some("John Doe"));
665 assert_eq!(author.uri.as_deref(), Some("http://example.org/johndoe"));
666 assert_eq!(author.email.as_deref(), Some("john@example.org"));
667
668 assert_eq!(feed.contributors.len(), 1);
669 assert_eq!(feed.contributors[0].name.as_deref(), Some("Jane Smith"));
670 }
671
672 #[test]
673 fn test_roundtrip_simple_feed() {
674 let feed = Feed {
675 id: Some("urn:uuid:test".to_string()),
676 title: Some(TextContent {
677 content_type: None,
678 content: Some("Test Feed".to_string()),
679 }),
680 updated: Some("2024-01-01T00:00:00Z".to_string()),
681 authors: vec![Person {
682 name: Some("Test Author".to_string()),
683 uri: None,
684 email: None,
685 }],
686 links: vec![Link {
687 href: Some("http://example.org/".to_string()),
688 rel: Some("alternate".to_string()),
689 ..Default::default()
690 }],
691 ..Default::default()
692 };
693
694 let xml = to_string(&feed).unwrap();
695 let parsed: Feed = from_str(&xml).unwrap();
696
697 assert_eq!(parsed.id, feed.id);
698 assert_eq!(
699 parsed.title.as_ref().and_then(|t| t.content.as_ref()),
700 feed.title.as_ref().and_then(|t| t.content.as_ref())
701 );
702 assert_eq!(parsed.updated, feed.updated);
703 assert_eq!(parsed.authors.len(), feed.authors.len());
704 }
705}