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}