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