facet_atom/
lib.rs

1//! Atom Syndication Format (RFC 4287) types for `facet-xml`.
2//!
3//! This crate provides strongly-typed Rust representations of Atom feed elements,
4//! enabling parsing and generation of Atom feeds using `facet-xml`.
5//!
6//! # Example
7//!
8//! ```rust
9//! use facet_atom::{Feed, Entry, Person, Link, TextContent, TextType};
10//!
11//! let atom_xml = r#"<?xml version="1.0" encoding="utf-8"?>
12//! <feed xmlns="http://www.w3.org/2005/Atom">
13//!     <title>Example Feed</title>
14//!     <id>urn:uuid:60a76c80-d399-11d9-b93C-0003939e0af6</id>
15//!     <updated>2003-12-13T18:30:02Z</updated>
16//!     <author>
17//!         <name>John Doe</name>
18//!     </author>
19//!     <link href="http://example.org/"/>
20//!     <entry>
21//!         <title>Atom-Powered Robots Run Amok</title>
22//!         <id>urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a</id>
23//!         <updated>2003-12-13T18:30:02Z</updated>
24//!         <link href="http://example.org/2003/12/13/atom03"/>
25//!         <summary>Some text.</summary>
26//!     </entry>
27//! </feed>"#;
28//!
29//! let feed: Feed = facet_atom::from_str(atom_xml).unwrap();
30//! assert_eq!(feed.title.as_ref().unwrap().content.as_deref(), Some("Example Feed"));
31//! assert_eq!(feed.entries.len(), 1);
32//! ```
33//!
34//! # Atom Namespace
35//!
36//! All types use the Atom namespace `http://www.w3.org/2005/Atom` as specified in RFC 4287.
37
38use facet::Facet;
39use facet_format::FormatDeserializer;
40use facet_xml as xml;
41use facet_xml::{XmlParser, to_vec};
42
43/// Atom namespace URI as defined in RFC 4287
44pub const ATOM_NS: &str = "http://www.w3.org/2005/Atom";
45
46/// Error type for Atom parsing
47pub type Error = facet_format::DeserializeError<facet_xml::XmlError>;
48
49/// Error type for Atom serialization
50pub type SerializeError = facet_format::SerializeError<facet_xml::XmlSerializeError>;
51
52/// Deserialize an Atom document from a string.
53pub 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
62/// Deserialize an Atom document from bytes.
63pub 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
72/// Serialize an Atom value to a string.
73pub 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// =============================================================================
82// Container Elements
83// =============================================================================
84
85/// The top-level Atom feed document (`<feed>`).
86///
87/// A feed contains metadata about the feed itself and zero or more entries.
88///
89/// # Required Elements (per RFC 4287)
90/// - `id`: Permanent, universally unique identifier
91/// - `title`: Human-readable title
92/// - `updated`: Most recent modification time
93///
94/// # Optional Elements
95/// - `author`: One or more feed authors (required if entries lack authors)
96/// - `link`: Links to related resources
97/// - `category`: Categories for the feed
98/// - `contributor`: Contributors to the feed
99/// - `generator`: Software that generated the feed
100/// - `icon`: Small image for the feed (1:1 aspect ratio)
101/// - `logo`: Larger image for the feed (2:1 aspect ratio)
102/// - `rights`: Copyright/usage rights
103/// - `subtitle`: Human-readable description
104/// - `entry`: Individual content entries
105#[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    /// Permanent, universally unique identifier for the feed.
113    /// Must be an IRI (Internationalized Resource Identifier).
114    #[facet(xml::element)]
115    pub id: Option<String>,
116
117    /// Human-readable title for the feed.
118    #[facet(xml::element)]
119    pub title: Option<TextContent>,
120
121    /// Most recent time the feed was modified in a significant way.
122    /// Format: RFC 3339 timestamp (e.g., "2003-12-13T18:30:02Z")
123    #[facet(xml::element)]
124    pub updated: Option<String>,
125
126    /// Authors of the feed.
127    #[facet(xml::elements, rename = "author")]
128    pub authors: Vec<Person>,
129
130    /// Links to related resources.
131    #[facet(xml::elements, rename = "link")]
132    pub links: Vec<Link>,
133
134    /// Categories that the feed belongs to.
135    #[facet(xml::elements, rename = "category")]
136    pub categories: Vec<Category>,
137
138    /// Contributors to the feed.
139    #[facet(xml::elements, rename = "contributor")]
140    pub contributors: Vec<Person>,
141
142    /// Software agent used to generate the feed.
143    #[facet(xml::element)]
144    pub generator: Option<Generator>,
145
146    /// IRI reference to a small image (favicon-style, 1:1 aspect ratio).
147    #[facet(xml::element)]
148    pub icon: Option<String>,
149
150    /// IRI reference to a larger image (banner-style, 2:1 aspect ratio).
151    #[facet(xml::element)]
152    pub logo: Option<String>,
153
154    /// Copyright/usage rights information.
155    #[facet(xml::element)]
156    pub rights: Option<TextContent>,
157
158    /// Human-readable description or subtitle.
159    #[facet(xml::element)]
160    pub subtitle: Option<TextContent>,
161
162    /// Individual entries in the feed.
163    #[facet(xml::elements, rename = "entry")]
164    pub entries: Vec<Entry>,
165}
166
167/// An individual entry in an Atom feed (`<entry>`).
168///
169/// # Required Elements (per RFC 4287)
170/// - `id`: Permanent, universally unique identifier
171/// - `title`: Human-readable title
172/// - `updated`: Most recent modification time
173///
174/// # Conditionally Required
175/// - `author`: Required unless the feed or source provides one
176/// - `link` with `rel="alternate"`: Required if no `content` element
177/// - `summary`: Required if content has `src` attribute or is non-text
178#[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    /// Permanent, universally unique identifier for the entry.
186    #[facet(xml::element)]
187    pub id: Option<String>,
188
189    /// Human-readable title for the entry.
190    #[facet(xml::element)]
191    pub title: Option<TextContent>,
192
193    /// Most recent time the entry was modified in a significant way.
194    #[facet(xml::element)]
195    pub updated: Option<String>,
196
197    /// Authors of the entry.
198    #[facet(xml::elements, rename = "author")]
199    pub authors: Vec<Person>,
200
201    /// Links to related resources.
202    #[facet(xml::elements, rename = "link")]
203    pub links: Vec<Link>,
204
205    /// Categories that the entry belongs to.
206    #[facet(xml::elements, rename = "category")]
207    pub categories: Vec<Category>,
208
209    /// Contributors to the entry.
210    #[facet(xml::elements, rename = "contributor")]
211    pub contributors: Vec<Person>,
212
213    /// The entry content.
214    #[facet(xml::element)]
215    pub content: Option<Content>,
216
217    /// Time when the entry was first created or published.
218    #[facet(xml::element)]
219    pub published: Option<String>,
220
221    /// Copyright/usage rights information.
222    #[facet(xml::element)]
223    pub rights: Option<TextContent>,
224
225    /// Brief summary or excerpt of the entry.
226    #[facet(xml::element)]
227    pub summary: Option<TextContent>,
228
229    /// Metadata from the original feed if this entry was copied.
230    #[facet(xml::element)]
231    pub source: Option<Source>,
232}
233
234/// Metadata about the original feed when an entry is copied (`<source>`).
235///
236/// Contains a subset of feed metadata to preserve attribution
237/// when entries are aggregated from multiple sources.
238#[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    /// Identifier of the original feed.
246    #[facet(xml::element)]
247    pub id: Option<String>,
248
249    /// Title of the original feed.
250    #[facet(xml::element)]
251    pub title: Option<TextContent>,
252
253    /// Last update time of the original feed.
254    #[facet(xml::element)]
255    pub updated: Option<String>,
256
257    /// Authors of the original feed.
258    #[facet(xml::elements, rename = "author")]
259    pub authors: Vec<Person>,
260
261    /// Links from the original feed.
262    #[facet(xml::elements, rename = "link")]
263    pub links: Vec<Link>,
264
265    /// Categories from the original feed.
266    #[facet(xml::elements, rename = "category")]
267    pub categories: Vec<Category>,
268
269    /// Contributors from the original feed.
270    #[facet(xml::elements, rename = "contributor")]
271    pub contributors: Vec<Person>,
272
273    /// Generator of the original feed.
274    #[facet(xml::element)]
275    pub generator: Option<Generator>,
276
277    /// Icon from the original feed.
278    #[facet(xml::element)]
279    pub icon: Option<String>,
280
281    /// Logo from the original feed.
282    #[facet(xml::element)]
283    pub logo: Option<String>,
284
285    /// Rights from the original feed.
286    #[facet(xml::element)]
287    pub rights: Option<TextContent>,
288
289    /// Subtitle from the original feed.
290    #[facet(xml::element)]
291    pub subtitle: Option<TextContent>,
292}
293
294// =============================================================================
295// Person Construct
296// =============================================================================
297
298/// A person (author or contributor) in an Atom feed.
299///
300/// Used for both `<author>` and `<contributor>` elements.
301#[derive(Facet, Debug, Clone, Default)]
302#[facet(xml::ns_all = "http://www.w3.org/2005/Atom", skip_all_unless_truthy)]
303pub struct Person {
304    /// Human-readable name for the person (required).
305    #[facet(xml::element)]
306    pub name: Option<String>,
307
308    /// IRI associated with the person (e.g., homepage).
309    #[facet(xml::element)]
310    pub uri: Option<String>,
311
312    /// Email address for the person (RFC 2822 format).
313    #[facet(xml::element)]
314    pub email: Option<String>,
315}
316
317// =============================================================================
318// Text Construct
319// =============================================================================
320
321/// Content type for text constructs.
322#[derive(Facet, Debug, Clone, Copy, Default, PartialEq, Eq)]
323#[facet(rename_all = "lowercase")]
324#[repr(u8)]
325pub enum TextType {
326    /// Plain text (default). Content should be displayed as-is.
327    #[default]
328    Text,
329    /// HTML content. Markup should be escaped in the XML.
330    Html,
331    /// XHTML content. Markup is embedded as child elements.
332    Xhtml,
333}
334
335/// A text construct used for title, subtitle, summary, and rights.
336///
337/// Per RFC 4287, text constructs can contain:
338/// - Plain text (`type="text"`, default)
339/// - Escaped HTML (`type="html"`)
340/// - Inline XHTML (`type="xhtml"`)
341#[derive(Facet, Debug, Clone, Default)]
342#[facet(xml::ns_all = "http://www.w3.org/2005/Atom", skip_all_unless_truthy)]
343pub struct TextContent {
344    /// The content type. Defaults to "text" if not specified.
345    #[facet(xml::attribute, rename = "type")]
346    pub content_type: Option<TextType>,
347
348    /// The text content (for type="text" or type="html").
349    /// For type="xhtml", the content is within a div element.
350    #[facet(xml::text)]
351    pub content: Option<String>,
352}
353
354// =============================================================================
355// Link Element
356// =============================================================================
357
358/// A link to a related resource (`<link>`).
359///
360/// Links define relationships between the feed/entry and external resources.
361#[derive(Facet, Debug, Clone, Default)]
362#[facet(xml::ns_all = "http://www.w3.org/2005/Atom", skip_all_unless_truthy)]
363pub struct Link {
364    /// The URI of the referenced resource (required).
365    #[facet(xml::attribute)]
366    pub href: Option<String>,
367
368    /// The link relation type.
369    /// Common values: "alternate", "self", "enclosure", "related", "via"
370    #[facet(xml::attribute)]
371    pub rel: Option<String>,
372
373    /// Advisory media type of the resource.
374    #[facet(xml::attribute, rename = "type")]
375    pub media_type: Option<String>,
376
377    /// Language of the referenced resource (RFC 3066 tag).
378    #[facet(xml::attribute)]
379    pub hreflang: Option<String>,
380
381    /// Human-readable description of the link.
382    #[facet(xml::attribute)]
383    pub title: Option<String>,
384
385    /// Advisory length of the resource in bytes.
386    #[facet(xml::attribute)]
387    pub length: Option<u64>,
388}
389
390// =============================================================================
391// Category Element
392// =============================================================================
393
394/// A category for the feed or entry (`<category>`).
395#[derive(Facet, Debug, Clone, Default)]
396#[facet(xml::ns_all = "http://www.w3.org/2005/Atom", skip_all_unless_truthy)]
397pub struct Category {
398    /// The category identifier (required).
399    #[facet(xml::attribute)]
400    pub term: Option<String>,
401
402    /// IRI identifying the categorization scheme.
403    #[facet(xml::attribute)]
404    pub scheme: Option<String>,
405
406    /// Human-readable label for display.
407    #[facet(xml::attribute)]
408    pub label: Option<String>,
409}
410
411// =============================================================================
412// Generator Element
413// =============================================================================
414
415/// Information about the software that generated the feed (`<generator>`).
416#[derive(Facet, Debug, Clone, Default)]
417#[facet(xml::ns_all = "http://www.w3.org/2005/Atom", skip_all_unless_truthy)]
418pub struct Generator {
419    /// IRI reference to the generator's website.
420    #[facet(xml::attribute)]
421    pub uri: Option<String>,
422
423    /// Version of the generating software.
424    #[facet(xml::attribute)]
425    pub version: Option<String>,
426
427    /// Human-readable name of the generator.
428    #[facet(xml::text)]
429    pub name: Option<String>,
430}
431
432// =============================================================================
433// Content Element
434// =============================================================================
435
436/// The content of an entry (`<content>`).
437///
438/// Content can be inline (text, HTML, XHTML, or other XML) or referenced
439/// via a `src` attribute for external content.
440#[derive(Facet, Debug, Clone, Default)]
441#[facet(xml::ns_all = "http://www.w3.org/2005/Atom", skip_all_unless_truthy)]
442pub struct Content {
443    /// The content type. For inline content: "text", "html", "xhtml", or a MIME type.
444    /// For external content: a MIME type hint.
445    #[facet(xml::attribute, rename = "type")]
446    pub content_type: Option<String>,
447
448    /// IRI reference to external content. If present, the element should be empty.
449    #[facet(xml::attribute)]
450    pub src: Option<String>,
451
452    /// The inline content (when `src` is not present).
453    /// For non-XML MIME types, this is Base64-encoded.
454    #[facet(xml::text)]
455    pub body: Option<String>,
456}
457
458// Re-export XML utilities for convenience
459pub 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">&lt;p&gt;Hello, World!&lt;/p&gt;</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}