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}