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