Skip to main content

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}