feedparser_rs/types/entry.rs
1use super::{
2 common::{
3 Content, Enclosure, Link, MediaContent, MediaThumbnail, Person, Source, Tag, TextConstruct,
4 },
5 generics::LimitedCollectionExt,
6 podcast::{ItunesEntryMeta, PodcastEntryMeta, PodcastPerson, PodcastTranscript},
7 thread::InReplyTo,
8};
9use chrono::{DateTime, Utc};
10
11/// Feed entry/item
12#[derive(Debug, Clone, Default)]
13pub struct Entry {
14 /// Unique entry identifier (stored inline for IDs ≤24 bytes)
15 pub id: Option<super::common::SmallString>,
16 /// Entry title
17 pub title: Option<String>,
18 /// Detailed title with metadata
19 pub title_detail: Option<TextConstruct>,
20 /// Primary link
21 pub link: Option<String>,
22 /// All links associated with this entry
23 pub links: Vec<Link>,
24 /// Entry subtitle (Atom §4.2.12 at entry level)
25 pub subtitle: Option<String>,
26 /// Detailed subtitle with metadata
27 pub subtitle_detail: Option<TextConstruct>,
28 /// Short description/summary
29 pub summary: Option<String>,
30 /// Detailed summary with metadata
31 pub summary_detail: Option<TextConstruct>,
32 /// Full content blocks
33 pub content: Vec<Content>,
34 /// Publication date
35 pub published: Option<DateTime<Utc>>,
36 /// Last update date
37 pub updated: Option<DateTime<Utc>>,
38 /// Creation date
39 pub created: Option<DateTime<Utc>>,
40 /// Expiration date
41 pub expired: Option<DateTime<Utc>>,
42 /// Primary author name (stored inline for names ≤24 bytes)
43 pub author: Option<super::common::SmallString>,
44 /// Detailed author information
45 pub author_detail: Option<Person>,
46 /// All authors
47 pub authors: Vec<Person>,
48 /// Contributors
49 pub contributors: Vec<Person>,
50 /// Publisher name (stored inline for names ≤24 bytes)
51 pub publisher: Option<super::common::SmallString>,
52 /// Detailed publisher information
53 pub publisher_detail: Option<Person>,
54 /// Tags/categories
55 pub tags: Vec<Tag>,
56 /// Media enclosures (audio, video, etc.)
57 pub enclosures: Vec<Enclosure>,
58 /// Comments URL or text
59 pub comments: Option<String>,
60 /// Source feed reference
61 pub source: Option<Source>,
62 /// iTunes episode metadata (if present)
63 pub itunes: Option<Box<ItunesEntryMeta>>,
64 /// Dublin Core creator (author fallback) - stored inline for names ≤24 bytes
65 pub dc_creator: Option<super::common::SmallString>,
66 /// Dublin Core date (publication date fallback)
67 pub dc_date: Option<DateTime<Utc>>,
68 /// Dublin Core subjects (tags)
69 pub dc_subject: Vec<String>,
70 /// Dublin Core rights (copyright)
71 pub dc_rights: Option<String>,
72 /// Media RSS thumbnails
73 pub media_thumbnails: Vec<MediaThumbnail>,
74 /// Media RSS content items
75 pub media_content: Vec<MediaContent>,
76 /// Podcast 2.0 transcripts for this episode
77 pub podcast_transcripts: Vec<PodcastTranscript>,
78 /// Podcast 2.0 persons for this episode (hosts, guests, etc.)
79 pub podcast_persons: Vec<PodcastPerson>,
80 /// Podcast 2.0 episode metadata
81 pub podcast: Option<Box<PodcastEntryMeta>>,
82 /// `GeoRSS` location data
83 pub geo: Option<Box<crate::namespace::georss::GeoLocation>>,
84 /// License URL (Creative Commons, etc.)
85 pub license: Option<String>,
86 /// Atom Threading Extensions: entries this is a reply to (thr:in-reply-to)
87 pub in_reply_to: Vec<InReplyTo>,
88 /// Atom Threading Extensions: total response count (thr:total)
89 ///
90 /// Stored as u32 in Rust for type safety. Python binding converts
91 /// to string to match Python feedparser's API.
92 pub thr_total: Option<u32>,
93 /// Slash namespace: comment count (`slash:comments`)
94 pub slash_comments: Option<u32>,
95 /// WFW namespace: comment RSS feed URL (`wfw:commentRss`)
96 pub wfw_comment_rss: Option<String>,
97}
98
99impl Entry {
100 /// Creates `Entry` with pre-allocated capacity for collections
101 ///
102 /// Pre-allocates space for typical entry fields:
103 /// - 1-2 links (alternate, related)
104 /// - 1 content block
105 /// - 1 author
106 /// - 2-3 tags
107 /// - 0-1 enclosures
108 /// - 2 podcast transcripts (typical for podcasts with multiple languages)
109 /// - 4 podcast persons (host, co-hosts, guests)
110 ///
111 /// # Examples
112 ///
113 /// ```
114 /// use feedparser_rs::Entry;
115 ///
116 /// let entry = Entry::with_capacity();
117 /// ```
118 #[must_use]
119 pub fn with_capacity() -> Self {
120 Self {
121 links: Vec::with_capacity(2),
122 content: Vec::with_capacity(1),
123 authors: Vec::with_capacity(1),
124 contributors: Vec::with_capacity(0),
125 tags: Vec::with_capacity(3),
126 enclosures: Vec::with_capacity(1),
127 dc_subject: Vec::with_capacity(2),
128 media_thumbnails: Vec::with_capacity(1),
129 media_content: Vec::with_capacity(1),
130 podcast_transcripts: Vec::with_capacity(2),
131 podcast_persons: Vec::with_capacity(4),
132 // Most entries reply to at most one parent
133 in_reply_to: Vec::with_capacity(1),
134 ..Default::default()
135 }
136 }
137
138 /// Sets title field with `TextConstruct`, storing both simple and detailed versions
139 ///
140 /// # Examples
141 ///
142 /// ```
143 /// use feedparser_rs::{Entry, TextConstruct};
144 ///
145 /// let mut entry = Entry::default();
146 /// entry.set_title(TextConstruct::text("Great Article"));
147 /// assert_eq!(entry.title.as_deref(), Some("Great Article"));
148 /// ```
149 #[inline]
150 pub fn set_title(&mut self, mut text: TextConstruct) {
151 self.title = Some(std::mem::take(&mut text.value));
152 self.title_detail = Some(text);
153 }
154
155 /// Sets subtitle field with `TextConstruct`, storing both simple and detailed versions
156 ///
157 /// # Examples
158 ///
159 /// ```
160 /// use feedparser_rs::{Entry, TextConstruct};
161 ///
162 /// let mut entry = Entry::default();
163 /// entry.set_subtitle(TextConstruct::text("A teaser"));
164 /// assert_eq!(entry.subtitle.as_deref(), Some("A teaser"));
165 /// ```
166 #[inline]
167 pub fn set_subtitle(&mut self, mut text: TextConstruct) {
168 self.subtitle = Some(std::mem::take(&mut text.value));
169 self.subtitle_detail = Some(text);
170 }
171
172 /// Sets summary field with `TextConstruct`, storing both simple and detailed versions
173 ///
174 /// # Examples
175 ///
176 /// ```
177 /// use feedparser_rs::{Entry, TextConstruct};
178 ///
179 /// let mut entry = Entry::default();
180 /// entry.set_summary(TextConstruct::text("A summary"));
181 /// assert_eq!(entry.summary.as_deref(), Some("A summary"));
182 /// ```
183 #[inline]
184 pub fn set_summary(&mut self, mut text: TextConstruct) {
185 self.summary = Some(std::mem::take(&mut text.value));
186 self.summary_detail = Some(text);
187 }
188
189 /// Sets author field with `Person`, storing both simple and detailed versions
190 ///
191 /// # Examples
192 ///
193 /// ```
194 /// use feedparser_rs::{Entry, Person};
195 ///
196 /// let mut entry = Entry::default();
197 /// entry.set_author(Person::from_name("Jane Doe"));
198 /// assert_eq!(entry.author.as_deref(), Some("Jane Doe"));
199 /// ```
200 #[inline]
201 pub fn set_author(&mut self, mut person: Person) {
202 self.author = person.name.take();
203 self.author_detail = Some(person);
204 }
205
206 /// Sets publisher field with `Person`, storing both simple and detailed versions
207 ///
208 /// # Examples
209 ///
210 /// ```
211 /// use feedparser_rs::{Entry, Person};
212 ///
213 /// let mut entry = Entry::default();
214 /// entry.set_publisher(Person::from_name("ACME Corp"));
215 /// assert_eq!(entry.publisher.as_deref(), Some("ACME Corp"));
216 /// ```
217 #[inline]
218 pub fn set_publisher(&mut self, mut person: Person) {
219 self.publisher = person.name.take();
220 self.publisher_detail = Some(person);
221 }
222
223 /// Sets the primary link and adds it to the links collection
224 ///
225 /// This is a convenience method that:
226 /// 1. Sets the `link` field (if not already set)
227 /// 2. Adds an "alternate" link to the `links` collection
228 ///
229 /// # Examples
230 ///
231 /// ```
232 /// use feedparser_rs::Entry;
233 ///
234 /// let mut entry = Entry::default();
235 /// entry.set_alternate_link("https://example.com/post/1".to_string(), 10);
236 /// assert_eq!(entry.link.as_deref(), Some("https://example.com/post/1"));
237 /// assert_eq!(entry.links.len(), 1);
238 /// assert_eq!(entry.links[0].rel.as_deref(), Some("alternate"));
239 /// ```
240 #[inline]
241 pub fn set_alternate_link(&mut self, href: String, max_links: usize) {
242 if self.link.is_none() {
243 self.link = Some(href.clone());
244 }
245 self.links.try_push_limited(
246 Link {
247 href: href.into(),
248 rel: Some("alternate".into()),
249 ..Default::default()
250 },
251 max_links,
252 );
253 }
254}
255
256#[cfg(test)]
257mod tests {
258 use super::*;
259
260 #[test]
261 fn test_entry_default() {
262 let entry = Entry::default();
263 assert!(entry.id.is_none());
264 assert!(entry.title.is_none());
265 assert!(entry.links.is_empty());
266 assert!(entry.content.is_empty());
267 assert!(entry.authors.is_empty());
268 }
269
270 #[test]
271 #[allow(clippy::redundant_clone)]
272 fn test_entry_clone() {
273 fn create_entry() -> Entry {
274 Entry {
275 title: Some("Test".to_string()),
276 links: vec![Link::default()],
277 ..Default::default()
278 }
279 }
280 let entry = create_entry();
281 let cloned = entry.clone();
282 assert_eq!(cloned.title.as_deref(), Some("Test"));
283 assert_eq!(cloned.links.len(), 1);
284 }
285}