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