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