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;