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}