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    /// License URL (Creative Commons, etc.)
78    pub license: Option<String>,
79}
80
81impl Entry {
82    /// Creates `Entry` with pre-allocated capacity for collections
83    ///
84    /// Pre-allocates space for typical entry fields:
85    /// - 1-2 links (alternate, related)
86    /// - 1 content block
87    /// - 1 author
88    /// - 2-3 tags
89    /// - 0-1 enclosures
90    /// - 2 podcast transcripts (typical for podcasts with multiple languages)
91    /// - 4 podcast persons (host, co-hosts, guests)
92    ///
93    /// # Examples
94    ///
95    /// ```
96    /// use feedparser_rs::Entry;
97    ///
98    /// let entry = Entry::with_capacity();
99    /// ```
100    #[must_use]
101    pub fn with_capacity() -> Self {
102        Self {
103            links: Vec::with_capacity(2),
104            content: Vec::with_capacity(1),
105            authors: Vec::with_capacity(1),
106            contributors: Vec::with_capacity(0),
107            tags: Vec::with_capacity(3),
108            enclosures: Vec::with_capacity(1),
109            dc_subject: Vec::with_capacity(2),
110            media_thumbnails: Vec::with_capacity(1),
111            media_content: Vec::with_capacity(1),
112            podcast_transcripts: Vec::with_capacity(2),
113            podcast_persons: Vec::with_capacity(4),
114            ..Default::default()
115        }
116    }
117
118    /// Sets title field with `TextConstruct`, storing both simple and detailed versions
119    ///
120    /// # Examples
121    ///
122    /// ```
123    /// use feedparser_rs::{Entry, TextConstruct};
124    ///
125    /// let mut entry = Entry::default();
126    /// entry.set_title(TextConstruct::text("Great Article"));
127    /// assert_eq!(entry.title.as_deref(), Some("Great Article"));
128    /// ```
129    #[inline]
130    pub fn set_title(&mut self, mut text: TextConstruct) {
131        self.title = Some(std::mem::take(&mut text.value));
132        self.title_detail = Some(text);
133    }
134
135    /// Sets summary field with `TextConstruct`, storing both simple and detailed versions
136    ///
137    /// # Examples
138    ///
139    /// ```
140    /// use feedparser_rs::{Entry, TextConstruct};
141    ///
142    /// let mut entry = Entry::default();
143    /// entry.set_summary(TextConstruct::text("A summary"));
144    /// assert_eq!(entry.summary.as_deref(), Some("A summary"));
145    /// ```
146    #[inline]
147    pub fn set_summary(&mut self, mut text: TextConstruct) {
148        self.summary = Some(std::mem::take(&mut text.value));
149        self.summary_detail = Some(text);
150    }
151
152    /// Sets author field with `Person`, storing both simple and detailed versions
153    ///
154    /// # Examples
155    ///
156    /// ```
157    /// use feedparser_rs::{Entry, Person};
158    ///
159    /// let mut entry = Entry::default();
160    /// entry.set_author(Person::from_name("Jane Doe"));
161    /// assert_eq!(entry.author.as_deref(), Some("Jane Doe"));
162    /// ```
163    #[inline]
164    pub fn set_author(&mut self, mut person: Person) {
165        self.author = person.name.take();
166        self.author_detail = Some(person);
167    }
168
169    /// Sets publisher field with `Person`, storing both simple and detailed versions
170    ///
171    /// # Examples
172    ///
173    /// ```
174    /// use feedparser_rs::{Entry, Person};
175    ///
176    /// let mut entry = Entry::default();
177    /// entry.set_publisher(Person::from_name("ACME Corp"));
178    /// assert_eq!(entry.publisher.as_deref(), Some("ACME Corp"));
179    /// ```
180    #[inline]
181    pub fn set_publisher(&mut self, mut person: Person) {
182        self.publisher = person.name.take();
183        self.publisher_detail = Some(person);
184    }
185
186    /// Sets the primary link and adds it to the links collection
187    ///
188    /// This is a convenience method that:
189    /// 1. Sets the `link` field (if not already set)
190    /// 2. Adds an "alternate" link to the `links` collection
191    ///
192    /// # Examples
193    ///
194    /// ```
195    /// use feedparser_rs::Entry;
196    ///
197    /// let mut entry = Entry::default();
198    /// entry.set_alternate_link("https://example.com/post/1".to_string(), 10);
199    /// assert_eq!(entry.link.as_deref(), Some("https://example.com/post/1"));
200    /// assert_eq!(entry.links.len(), 1);
201    /// assert_eq!(entry.links[0].rel.as_deref(), Some("alternate"));
202    /// ```
203    #[inline]
204    pub fn set_alternate_link(&mut self, href: String, max_links: usize) {
205        if self.link.is_none() {
206            self.link = Some(href.clone());
207        }
208        self.links.try_push_limited(
209            Link {
210                href,
211                rel: Some("alternate".to_string()),
212                ..Default::default()
213            },
214            max_links,
215        );
216    }
217}
218
219#[cfg(test)]
220mod tests {
221    use super::*;
222
223    #[test]
224    fn test_entry_default() {
225        let entry = Entry::default();
226        assert!(entry.id.is_none());
227        assert!(entry.title.is_none());
228        assert!(entry.links.is_empty());
229        assert!(entry.content.is_empty());
230        assert!(entry.authors.is_empty());
231    }
232
233    #[test]
234    #[allow(clippy::redundant_clone)]
235    fn test_entry_clone() {
236        fn create_entry() -> Entry {
237            Entry {
238                title: Some("Test".to_string()),
239                links: vec![Link::default()],
240                ..Default::default()
241            }
242        }
243        let entry = create_entry();
244        let cloned = entry.clone();
245        assert_eq!(cloned.title.as_deref(), Some("Test"));
246        assert_eq!(cloned.links.len(), 1);
247    }
248}