Skip to main content

rss_gen/
data.rs

1// Copyright © 2024 RSS Gen. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4// src/data.rs
5
6//! This module contains the core data structures and functionality for RSS feeds.
7//!
8//! It includes definitions for RSS versions, RSS data, and RSS items, as well as
9//! utility functions for URL validation and date parsing.
10
11use crate::{
12    error::{Result, RssError},
13    MAX_FEED_SIZE, MAX_GENERAL_LENGTH,
14};
15use dtt::datetime::DateTime;
16use serde::{Deserialize, Serialize};
17use std::collections::HashMap;
18use std::fmt;
19use std::str::FromStr;
20use time::{
21    format_description::well_known::Iso8601,
22    format_description::well_known::Rfc2822, OffsetDateTime,
23};
24use url::Url;
25
26/// Represents the different versions of RSS.
27#[derive(
28    Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize,
29)]
30#[non_exhaustive]
31#[derive(Default)]
32pub enum RssVersion {
33    /// RSS version 0.90
34    RSS0_90,
35    /// RSS version 0.91
36    RSS0_91,
37    /// RSS version 0.92
38    RSS0_92,
39    /// RSS version 1.0
40    RSS1_0,
41    /// RSS version 2.0
42    #[default]
43    RSS2_0,
44}
45
46impl RssVersion {
47    /// Returns the string representation of the RSS version.
48    ///
49    /// # Returns
50    ///
51    /// A static string slice representing the RSS version.
52    #[must_use]
53    pub const fn as_str(&self) -> &'static str {
54        match self {
55            Self::RSS0_90 => "0.90",
56            Self::RSS0_91 => "0.91",
57            Self::RSS0_92 => "0.92",
58            Self::RSS1_0 => "1.0",
59            Self::RSS2_0 => "2.0",
60        }
61    }
62}
63
64impl fmt::Display for RssVersion {
65    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66        write!(f, "{}", self.as_str())
67    }
68}
69
70impl FromStr for RssVersion {
71    type Err = RssError;
72
73    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
74        match s {
75            "0.90" => Ok(Self::RSS0_90),
76            "0.91" => Ok(Self::RSS0_91),
77            "0.92" => Ok(Self::RSS0_92),
78            "1.0" => Ok(Self::RSS1_0),
79            "2.0" => Ok(Self::RSS2_0),
80            _ => Err(RssError::InvalidRssVersion(s.to_string())),
81        }
82    }
83}
84
85/// Represents the main structure for an RSS feed.
86#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
87#[non_exhaustive]
88pub struct RssData {
89    /// The Atom link of the RSS feed.
90    pub atom_link: String,
91    /// The author of the RSS feed.
92    pub author: String,
93    /// The category of the RSS feed.
94    pub category: String,
95    /// The copyright notice for the content of the feed.
96    pub copyright: String,
97    /// The description of the RSS feed.
98    pub description: String,
99    /// The docs link of the RSS feed.
100    pub docs: String,
101    /// The generator of the RSS feed.
102    pub generator: String,
103    /// The GUID of the RSS feed.
104    pub guid: String,
105    /// The image title of the RSS feed.
106    pub image_title: String,
107    /// The image URL of the RSS feed.
108    pub image_url: String,
109    /// The image link of the RSS feed.
110    pub image_link: String,
111    /// The language of the RSS feed.
112    pub language: String,
113    /// The last build date of the RSS feed.
114    pub last_build_date: String,
115    /// The main link to the RSS feed.
116    pub link: String,
117    /// The managing editor of the RSS feed.
118    pub managing_editor: String,
119    /// The publication date of the RSS feed.
120    pub pub_date: String,
121    /// The title of the RSS feed.
122    pub title: String,
123    /// Time To Live (TTL), the number of minutes the feed should be cached before refreshing.
124    pub ttl: String,
125    /// The webmaster of the RSS feed.
126    pub webmaster: String,
127    /// A collection of additional items in the RSS feed.
128    pub items: Vec<RssItem>,
129    /// The version of the RSS feed.
130    pub version: RssVersion,
131    /// The creator of the RSS feed.
132    pub creator: String,
133    /// The date the RSS feed was created.
134    pub date: String,
135}
136
137impl RssData {
138    /// Creates a new `RssData` instance with default values and a specified RSS version.
139    ///
140    /// # Arguments
141    ///
142    /// * `version` - An optional `RssVersion` specifying the RSS version for the feed.
143    ///
144    /// # Returns
145    ///
146    /// A new `RssData` instance.
147    #[must_use]
148    pub fn new(version: Option<RssVersion>) -> Self {
149        Self {
150            version: version.unwrap_or_default(),
151            ..Default::default()
152        }
153    }
154
155    /// Sets the value of a specified field and returns the `RssData` instance for method chaining.
156    ///
157    /// # Arguments
158    ///
159    /// * `field` - The field to set.
160    /// * `value` - The value to assign to the field.
161    ///
162    /// # Returns
163    ///
164    /// The updated `RssData` instance.
165    #[must_use]
166    pub fn set<T: Into<String>>(
167        mut self,
168        field: RssDataField,
169        value: T,
170    ) -> Self {
171        let value = sanitize_input(&value.into());
172        match field {
173            RssDataField::AtomLink => self.atom_link = value,
174            RssDataField::Author => self.author = value,
175            RssDataField::Category => self.category = value,
176            RssDataField::Copyright => self.copyright = value,
177            RssDataField::Description => self.description = value,
178            RssDataField::Docs => self.docs = value,
179            RssDataField::Generator => self.generator = value,
180            RssDataField::Guid => self.guid = value,
181            RssDataField::ImageTitle => self.image_title = value,
182            RssDataField::ImageUrl => self.image_url = value,
183            RssDataField::ImageLink => self.image_link = value,
184            RssDataField::Language => self.language = value,
185            RssDataField::LastBuildDate => self.last_build_date = value,
186            RssDataField::Link => self.link = value,
187            RssDataField::ManagingEditor => {
188                self.managing_editor = value;
189            }
190            RssDataField::PubDate => self.pub_date = value,
191            RssDataField::Title => self.title = value,
192            RssDataField::Ttl => self.ttl = value,
193            RssDataField::Webmaster => self.webmaster = value,
194        }
195        self
196    }
197
198    /// Sets the value of a specified field for the last `RssItem` and updates it.
199    ///
200    /// # Arguments
201    ///
202    /// * `field` - The field to set for the `RssItem`.
203    /// * `value` - The value to assign to the field.
204    ///
205    /// # Panics
206    ///
207    /// This function will panic if `self.items` is empty, as it uses `.unwrap()` to
208    /// retrieve the last mutable item in the list.
209    ///
210    /// Ensure that `self.items` contains at least one `RssItem` before calling this method.
211    pub fn set_item_field<T: Into<String>>(
212        &mut self,
213        field: RssItemField,
214        value: T,
215    ) {
216        let value = sanitize_input(&value.into());
217        if self.items.is_empty() {
218            self.items.push(RssItem::new());
219        }
220        let item = self.items.last_mut().unwrap();
221        match field {
222            RssItemField::Guid => item.guid = value,
223            RssItemField::Category => item.category = Some(value),
224            RssItemField::Description => item.description = value,
225            RssItemField::Link => item.link = value,
226            RssItemField::PubDate => item.pub_date = value,
227            RssItemField::Title => item.title = value,
228            RssItemField::Author => item.author = value,
229            RssItemField::Comments => item.comments = Some(value),
230            RssItemField::Enclosure => item.enclosure = Some(value),
231            RssItemField::Source => item.source = Some(value),
232        }
233    }
234
235    /// Validates the size of the RSS feed to ensure it does not exceed the maximum allowed size.
236    ///
237    /// # Returns
238    ///
239    /// * `Ok(())` if the feed size is valid.
240    /// * `Err(RssError)` if the feed size exceeds the maximum allowed size.
241    ///
242    /// # Errors
243    ///
244    /// This function returns an `Err(RssError::InvalidInput)` if the total size of the feed
245    /// exceeds the maximum allowed size (`MAX_FEED_SIZE`).
246    pub fn validate_size(&self) -> Result<()> {
247        let mut total_size = 0;
248        total_size += self.title.len();
249        total_size += self.link.len();
250        total_size += self.description.len();
251        // Add sizes of other fields...
252
253        for item in &self.items {
254            total_size += item.title.len();
255            total_size += item.link.len();
256            total_size += item.description.len();
257            // Add sizes of other item fields...
258        }
259
260        if total_size > MAX_FEED_SIZE {
261            return Err(RssError::InvalidInput(
262                format!("Total feed size exceeds maximum allowed size of {MAX_FEED_SIZE} bytes")
263            ));
264        }
265
266        Ok(())
267    }
268
269    /// Sets the image for the RSS feed.
270    ///
271    /// # Arguments
272    ///
273    /// * `title` - The title of the image.
274    /// * `url` - The URL of the image.
275    /// * `link` - The link associated with the image.
276    pub fn set_image(&mut self, title: &str, url: &str, link: &str) {
277        self.image_title = sanitize_input(title);
278        self.image_url = sanitize_input(url);
279        self.image_link = sanitize_input(link);
280    }
281
282    /// Adds an item to the RSS feed.
283    ///
284    /// This method appends the given `RssItem` to the `items` vector of the `RssData` struct.
285    ///
286    /// # Arguments
287    ///
288    /// * `item` - The `RssItem` to be added to the feed.
289    pub fn add_item(&mut self, item: RssItem) {
290        self.items.push(item);
291    }
292
293    /// Removes an item from the RSS feed by its GUID.
294    ///
295    /// # Arguments
296    ///
297    /// * `guid` - The GUID of the item to remove.
298    ///
299    /// # Returns
300    ///
301    /// `true` if an item was removed, `false` otherwise.
302    pub fn remove_item(&mut self, guid: &str) -> bool {
303        let initial_len = self.items.len();
304        self.items.retain(|item| item.guid != guid);
305        self.items.len() < initial_len
306    }
307
308    /// Returns the number of items in the RSS feed.
309    #[must_use]
310    pub fn item_count(&self) -> usize {
311        self.items.len()
312    }
313
314    /// Clears all items from the RSS feed.
315    pub fn clear_items(&mut self) {
316        self.items.clear();
317    }
318
319    /// Validates the `RssData` to ensure that all required fields are set and valid.
320    ///
321    /// # Returns
322    ///
323    /// * `Ok(())` if the `RssData` is valid.
324    /// * `Err(RssError)` if any validation errors are found.
325    ///
326    /// # Errors
327    ///
328    /// This function returns an `Err(RssError)` in the following cases:
329    ///
330    /// * `RssError::InvalidInput` if the category exceeds the maximum allowed length.
331    /// * `RssError::ValidationErrors` if there are missing or invalid fields (e.g., title, link, description, publication date).
332    ///
333    /// Additionally, it can return an error if the link format is invalid or the publication date cannot be parsed.
334    pub fn validate(&self) -> Result<()> {
335        let mut errors = Vec::new();
336
337        if self.title.is_empty() {
338            errors.push("Title is missing".to_string());
339        }
340
341        if self.link.is_empty() {
342            errors.push("Link is missing".to_string());
343        } else if let Err(e) = validate_url(&self.link) {
344            errors.push(format!("Invalid link: {e}"));
345        }
346
347        if self.description.is_empty() {
348            errors.push("Description is missing".to_string());
349        }
350
351        // Check category length
352        if self.category.len() > MAX_GENERAL_LENGTH {
353            return Err(RssError::InvalidInput(format!(
354            "Category exceeds maximum allowed length of {MAX_GENERAL_LENGTH} characters"
355        )));
356        }
357
358        if !self.pub_date.is_empty() {
359            if let Err(e) = parse_date(&self.pub_date) {
360                errors.push(format!("Invalid publication date: {e}"));
361            }
362        }
363
364        if !errors.is_empty() {
365            return Err(RssError::ValidationErrors(errors));
366        }
367
368        Ok(())
369    }
370
371    /// Converts the `RssData` into a `HashMap<String, String>` for easier manipulation.
372    ///
373    /// # Returns
374    ///
375    /// A `HashMap<String, String>` containing the RSS feed data.
376    #[must_use]
377    pub fn to_hash_map(&self) -> HashMap<String, String> {
378        let mut map = HashMap::new();
379        map.insert("atom_link".to_string(), self.atom_link.clone());
380        map.insert("author".to_string(), self.author.clone());
381        map.insert("category".to_string(), self.category.clone());
382        map.insert("copyright".to_string(), self.copyright.clone());
383        map.insert("description".to_string(), self.description.clone());
384        map.insert("docs".to_string(), self.docs.clone());
385        map.insert("generator".to_string(), self.generator.clone());
386        map.insert("guid".to_string(), self.guid.clone());
387        map.insert("image_title".to_string(), self.image_title.clone());
388        map.insert("image_url".to_string(), self.image_url.clone());
389        map.insert("image_link".to_string(), self.image_link.clone());
390        map.insert("language".to_string(), self.language.clone());
391        map.insert(
392            "last_build_date".to_string(),
393            self.last_build_date.clone(),
394        );
395        map.insert("link".to_string(), self.link.clone());
396        map.insert(
397            "managing_editor".to_string(),
398            self.managing_editor.clone(),
399        );
400        map.insert("pub_date".to_string(), self.pub_date.clone());
401        map.insert("title".to_string(), self.title.clone());
402        map.insert("ttl".to_string(), self.ttl.clone());
403        map.insert("webmaster".to_string(), self.webmaster.clone());
404        map
405    }
406
407    // Field setter methods
408
409    /// Sets the RSS version.
410    #[must_use]
411    pub fn version(mut self, version: RssVersion) -> Self {
412        self.version = version;
413        self
414    }
415
416    /// Sets the Atom link.
417    #[must_use]
418    pub fn atom_link<T: Into<String>>(self, value: T) -> Self {
419        self.set(RssDataField::AtomLink, value)
420    }
421
422    /// Sets the author.
423    #[must_use]
424    pub fn author<T: Into<String>>(self, value: T) -> Self {
425        self.set(RssDataField::Author, value)
426    }
427
428    /// Sets the category.
429    #[must_use]
430    pub fn category<T: Into<String>>(self, value: T) -> Self {
431        self.set(RssDataField::Category, value)
432    }
433
434    /// Sets the copyright.
435    #[must_use]
436    pub fn copyright<T: Into<String>>(self, value: T) -> Self {
437        self.set(RssDataField::Copyright, value)
438    }
439
440    /// Sets the description.
441    #[must_use]
442    pub fn description<T: Into<String>>(self, value: T) -> Self {
443        self.set(RssDataField::Description, value)
444    }
445
446    /// Sets the docs link.
447    #[must_use]
448    pub fn docs<T: Into<String>>(self, value: T) -> Self {
449        self.set(RssDataField::Docs, value)
450    }
451
452    /// Sets the generator.
453    #[must_use]
454    pub fn generator<T: Into<String>>(self, value: T) -> Self {
455        self.set(RssDataField::Generator, value)
456    }
457
458    /// Sets the GUID.
459    #[must_use]
460    pub fn guid<T: Into<String>>(self, value: T) -> Self {
461        self.set(RssDataField::Guid, value)
462    }
463
464    /// Sets the image title.
465    #[must_use]
466    pub fn image_title<T: Into<String>>(self, value: T) -> Self {
467        self.set(RssDataField::ImageTitle, value)
468    }
469
470    /// Sets the image URL.
471    #[must_use]
472    pub fn image_url<T: Into<String>>(self, value: T) -> Self {
473        self.set(RssDataField::ImageUrl, value)
474    }
475
476    /// Sets the image link.
477    #[must_use]
478    pub fn image_link<T: Into<String>>(self, value: T) -> Self {
479        self.set(RssDataField::ImageLink, value)
480    }
481
482    /// Sets the language.
483    #[must_use]
484    pub fn language<T: Into<String>>(self, value: T) -> Self {
485        self.set(RssDataField::Language, value)
486    }
487
488    /// Sets the last build date.
489    #[must_use]
490    pub fn last_build_date<T: Into<String>>(self, value: T) -> Self {
491        self.set(RssDataField::LastBuildDate, value)
492    }
493
494    /// Sets the main link.
495    #[must_use]
496    pub fn link<T: Into<String>>(self, value: T) -> Self {
497        self.set(RssDataField::Link, value)
498    }
499
500    /// Sets the managing editor.
501    #[must_use]
502    pub fn managing_editor<T: Into<String>>(self, value: T) -> Self {
503        self.set(RssDataField::ManagingEditor, value)
504    }
505
506    /// Sets the publication date.
507    #[must_use]
508    pub fn pub_date<T: Into<String>>(self, value: T) -> Self {
509        self.set(RssDataField::PubDate, value)
510    }
511
512    /// Sets the title.
513    #[must_use]
514    pub fn title<T: Into<String>>(self, value: T) -> Self {
515        self.set(RssDataField::Title, value)
516    }
517
518    /// Sets the TTL (Time To Live).
519    #[must_use]
520    pub fn ttl<T: Into<String>>(self, value: T) -> Self {
521        self.set(RssDataField::Ttl, value)
522    }
523
524    /// Sets the webmaster.
525    #[must_use]
526    pub fn webmaster<T: Into<String>>(self, value: T) -> Self {
527        self.set(RssDataField::Webmaster, value)
528    }
529}
530
531/// Represents the fields of an RSS data structure.
532#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
533pub enum RssDataField {
534    /// The Atom link of the RSS feed.
535    AtomLink,
536    /// The author of the RSS feed.
537    Author,
538    /// The category of the RSS feed.
539    Category,
540    /// The copyright notice.
541    Copyright,
542    /// The description of the RSS feed.
543    Description,
544    /// The docs link of the RSS feed.
545    Docs,
546    /// The generator of the RSS feed.
547    Generator,
548    /// The GUID of the RSS feed.
549    Guid,
550    /// The image title of the RSS feed.
551    ImageTitle,
552    /// The image URL of the RSS feed.
553    ImageUrl,
554    /// The image link of the RSS feed.
555    ImageLink,
556    /// The language of the RSS feed.
557    Language,
558    /// The last build date of the RSS feed.
559    LastBuildDate,
560    /// The main link to the RSS feed.
561    Link,
562    /// The managing editor of the RSS feed.
563    ManagingEditor,
564    /// The publication date of the RSS feed.
565    PubDate,
566    /// The title of the RSS feed.
567    Title,
568    /// Time To Live (TTL), the number of minutes the feed should be cached before refreshing.
569    Ttl,
570    /// The webmaster of the RSS feed.
571    Webmaster,
572}
573
574/// Represents an item in the RSS feed.
575#[derive(
576    Debug, Default, PartialEq, Eq, Clone, Serialize, Deserialize,
577)]
578#[non_exhaustive]
579pub struct RssItem {
580    /// The GUID of the RSS item (unique identifier).
581    pub guid: String,
582    /// The category of the RSS item.
583    pub category: Option<String>,
584    /// The description of the RSS item.
585    pub description: String,
586    /// The link to the RSS item.
587    pub link: String,
588    /// The publication date of the RSS item.
589    pub pub_date: String,
590    /// The title of the RSS item.
591    pub title: String,
592    /// The author of the RSS item.
593    pub author: String,
594    /// The comments URL related to the RSS item (optional).
595    pub comments: Option<String>,
596    /// The enclosure (typically for media like podcasts) (optional).
597    pub enclosure: Option<String>,
598    /// The source of the RSS item (optional).
599    pub source: Option<String>,
600    /// The creator of the RSS item (optional).
601    pub creator: Option<String>,
602    /// The date the RSS item was created (optional).
603    pub date: Option<String>,
604}
605
606impl RssItem {
607    /// Creates a new `RssItem` with default values.
608    #[must_use]
609    pub fn new() -> Self {
610        Self::default()
611    }
612
613    /// Sets the value of a field and returns the `RssItem` instance for method chaining.
614    ///
615    /// # Arguments
616    ///
617    /// * `field` - The field to set.
618    /// * `value` - The value to assign to the field.
619    ///
620    /// # Returns
621    ///
622    /// The updated `RssItem` instance.
623    #[must_use]
624    pub fn set<T: Into<String>>(
625        mut self,
626        field: RssItemField,
627        value: T,
628    ) -> Self {
629        let value = sanitize_input(&value.into());
630        match field {
631            RssItemField::Guid => self.guid = value,
632            RssItemField::Category => self.category = Some(value),
633            RssItemField::Description => self.description = value,
634            RssItemField::Link => self.link = value,
635            RssItemField::PubDate => self.pub_date = value,
636            RssItemField::Title => self.title = value,
637            RssItemField::Author => self.author = value,
638            RssItemField::Comments => self.comments = Some(value),
639            RssItemField::Enclosure => self.enclosure = Some(value),
640            RssItemField::Source => self.source = Some(value),
641        }
642        self
643    }
644
645    /// Validates the `RssData` to ensure that all required fields are set and valid.
646    ///
647    /// # Returns
648    ///
649    /// * `Ok(())` if the `RssData` is valid.
650    /// * `Err(RssError)` if any validation errors are found.
651    ///
652    /// # Errors
653    ///
654    /// This function returns an `Err(RssError)` in the following cases:
655    ///
656    /// * `RssError::InvalidInput` if any fields such as `title`, `link`, or `description` are missing or invalid.
657    /// * `RssError::ValidationErrors` if there are multiple validation issues found (e.g., invalid link, missing title, etc.).
658    /// * `RssError::DateParseError` if the `pub_date` cannot be parsed into a valid date.
659    ///
660    /// Additionally, it can return an error if any of the custom validation rules are violated (e.g., maximum length for certain fields).
661    pub fn validate(&self) -> Result<()> {
662        let mut errors = Vec::new();
663
664        if self.title.is_empty() {
665            errors.push("Title is missing".to_string());
666        }
667
668        if self.link.is_empty() {
669            errors.push("Link is missing".to_string());
670        } else if let Err(e) = validate_url(&self.link) {
671            errors.push(format!("Invalid link: {e}"));
672        }
673
674        if self.description.is_empty() {
675            errors.push("Description is missing".to_string());
676        }
677
678        // Add more field validations as needed...
679
680        if !errors.is_empty() {
681            return Err(RssError::ValidationErrors(errors));
682        }
683
684        Ok(())
685    }
686
687    /// Parses the `pub_date` string into a `DateTime` object.
688    ///
689    /// # Returns
690    ///
691    /// * `Ok(DateTime)` if the date is valid and successfully parsed.
692    /// * `Err(RssError)` if the date is invalid or cannot be parsed.
693    ///
694    /// # Errors
695    ///
696    /// This function returns an `Err(RssError)` if the `pub_date` is invalid or
697    /// cannot be parsed into a `DateTime` object.
698    pub fn pub_date_parsed(&self) -> Result<DateTime> {
699        parse_date(&self.pub_date)
700    }
701
702    // Field setter methods
703
704    /// Sets the GUID.
705    #[must_use]
706    pub fn guid<T: Into<String>>(self, value: T) -> Self {
707        self.set(RssItemField::Guid, value)
708    }
709
710    /// Sets the category.
711    #[must_use]
712    pub fn category<T: Into<String>>(self, value: T) -> Self {
713        self.set(RssItemField::Category, value)
714    }
715
716    /// Sets the description.
717    #[must_use]
718    pub fn description<T: Into<String>>(self, value: T) -> Self {
719        self.set(RssItemField::Description, value)
720    }
721
722    /// Sets the link.
723    #[must_use]
724    pub fn link<T: Into<String>>(self, value: T) -> Self {
725        self.set(RssItemField::Link, value)
726    }
727
728    /// Sets the publication date.
729    #[must_use]
730    pub fn pub_date<T: Into<String>>(self, value: T) -> Self {
731        self.set(RssItemField::PubDate, value)
732    }
733
734    /// Sets the title.
735    #[must_use]
736    pub fn title<T: Into<String>>(self, value: T) -> Self {
737        self.set(RssItemField::Title, value)
738    }
739
740    /// Sets the author.
741    #[must_use]
742    pub fn author<T: Into<String>>(self, value: T) -> Self {
743        self.set(RssItemField::Author, value)
744    }
745
746    /// Sets the comments URL.
747    #[must_use]
748    pub fn comments<T: Into<String>>(self, value: T) -> Self {
749        self.set(RssItemField::Comments, value)
750    }
751
752    /// Sets the enclosure.
753    #[must_use]
754    pub fn enclosure<T: Into<String>>(self, value: T) -> Self {
755        self.set(RssItemField::Enclosure, value)
756    }
757
758    /// Sets the source.
759    #[must_use]
760    pub fn source<T: Into<String>>(self, value: T) -> Self {
761        self.set(RssItemField::Source, value)
762    }
763}
764
765/// Represents the fields of an RSS item.
766#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
767pub enum RssItemField {
768    /// The GUID of the RSS item.
769    Guid,
770    /// The category of the RSS item.
771    Category,
772    /// The description of the RSS item.
773    Description,
774    /// The link to the RSS item.
775    Link,
776    /// The publication date of the RSS item.
777    PubDate,
778    /// The title of the RSS item.
779    Title,
780    /// The author of the RSS item.
781    Author,
782    /// The comments URL related to the RSS item.
783    Comments,
784    /// The enclosure (typically for media like podcasts).
785    Enclosure,
786    /// The source of the RSS item.
787    Source,
788}
789
790/// Validates a URL string.
791///
792/// # Arguments
793///
794/// * `url` - A string slice that holds the URL to validate.
795///
796/// # Returns
797///
798/// * `Ok(())` if the URL is valid.
799/// * `Err(RssError)` if the URL is invalid.
800///
801/// # Errors
802///
803/// This function returns an `Err(RssError::InvalidUrl)` if the URL is not valid or
804/// if it does not use the `http` or `https` protocol.
805pub fn validate_url(url: &str) -> Result<()> {
806    let parsed_url = Url::parse(url)
807        .map_err(|_| RssError::InvalidUrl(url.to_string()))?;
808
809    if parsed_url.scheme() != "http" && parsed_url.scheme() != "https" {
810        return Err(RssError::InvalidUrl(
811            "URL must use http or https protocol".to_string(),
812        ));
813    }
814
815    Ok(())
816}
817
818/// Parses a date string into a `DateTime`.
819///
820/// # Arguments
821///
822/// * `date_str` - A string slice that holds the date to parse.
823///
824/// # Returns
825///
826/// * `Ok(DateTime)` if the date is valid and successfully parsed.
827/// * `Err(RssError)` if the date is invalid or cannot be parsed.
828///
829/// # Errors
830///
831/// This function returns an `Err(RssError::DateParseError)` if the date cannot
832/// be parsed into a valid `DateTime`.
833///
834/// # Panics
835///
836/// This function will panic if the "UTC" time zone is invalid, but this is
837/// highly unlikely as "UTC" is always valid.
838pub fn parse_date(date_str: &str) -> Result<DateTime> {
839    if OffsetDateTime::parse(date_str, &Rfc2822).is_ok() {
840        return Ok(
841            DateTime::new_with_tz("UTC").expect("UTC is always valid")
842        );
843    }
844
845    if OffsetDateTime::parse(date_str, &Iso8601::DEFAULT).is_ok() {
846        return Ok(
847            DateTime::new_with_tz("UTC").expect("UTC is always valid")
848        );
849    }
850
851    // Handle custom parsing logic here...
852
853    Err(RssError::DateParseError(date_str.to_string()))
854}
855
856/// Sanitizes input by escaping HTML special characters.
857///
858/// # Arguments
859///
860/// * `input` - A string slice containing the input to sanitize.
861///
862/// # Returns
863///
864/// A `String` with HTML special characters escaped.
865fn sanitize_input(input: &str) -> String {
866    input
867        .replace('&', "&amp;")
868        .replace('<', "&lt;")
869        .replace('>', "&gt;")
870        .replace('"', "&quot;")
871        .replace('\'', "&#x27;")
872}
873
874#[cfg(test)]
875mod tests {
876    use super::*;
877    use quick_xml::de::from_str;
878
879    #[derive(Debug, Deserialize, PartialEq)]
880    struct Image {
881        title: String,
882        url: String,
883        link: String,
884    }
885
886    #[derive(Debug, Deserialize, PartialEq)]
887    struct Channel {
888        title: String,
889        link: String,
890        description: String,
891        image: Image,
892    }
893
894    #[derive(Debug, Deserialize, PartialEq)]
895    struct Rss {
896        #[serde(rename = "channel")]
897        channel: Channel,
898    }
899
900    #[test]
901    fn test_rss_version() {
902        assert_eq!(RssVersion::RSS2_0.as_str(), "2.0");
903        assert_eq!(RssVersion::default(), RssVersion::RSS2_0);
904        assert_eq!(RssVersion::RSS1_0.to_string(), "1.0");
905        assert!(matches!(
906            "2.0".parse::<RssVersion>(),
907            Ok(RssVersion::RSS2_0)
908        ));
909        assert!("3.0".parse::<RssVersion>().is_err());
910    }
911
912    #[test]
913    fn test_rss_data_new() {
914        let rss_data = RssData::new(Some(RssVersion::RSS2_0));
915        assert_eq!(rss_data.version, RssVersion::RSS2_0);
916    }
917
918    #[test]
919    fn test_rss_data_setters() {
920        let rss_data = RssData::new(None)
921            .title("Test Feed")
922            .link("https://example.com")
923            .description("A test feed")
924            .generator("RSS Gen")
925            .guid("unique-guid")
926            .pub_date("2024-03-21T12:00:00Z")
927            .language("en");
928
929        assert_eq!(rss_data.title, "Test Feed");
930        assert_eq!(rss_data.link, "https://example.com");
931        assert_eq!(rss_data.description, "A test feed");
932        assert_eq!(rss_data.generator, "RSS Gen");
933        assert_eq!(rss_data.guid, "unique-guid");
934        assert_eq!(rss_data.pub_date, "2024-03-21T12:00:00Z");
935        assert_eq!(rss_data.language, "en");
936    }
937
938    #[test]
939    fn test_rss_data_validate() {
940        let valid_rss_data = RssData::new(None)
941            .title("Valid Feed")
942            .link("https://example.com")
943            .description("A valid RSS feed");
944
945        assert!(valid_rss_data.validate().is_ok());
946
947        let invalid_rss_data = RssData::new(None)
948            .title("Invalid Feed")
949            .link("not a valid url")
950            .description("An invalid RSS feed");
951
952        let result = invalid_rss_data.validate();
953        assert!(result.is_err());
954        if let Err(RssError::ValidationErrors(errors)) = result {
955            assert!(errors.iter().any(|e| e.contains("Invalid link")),
956                "Expected an error containing 'Invalid link', but got: {errors:?}");
957        } else {
958            panic!("Expected ValidationErrors");
959        }
960    }
961
962    #[test]
963    fn test_add_item() {
964        let mut rss_data = RssData::new(None)
965            .title("Test RSS Feed")
966            .link("https://example.com")
967            .description("A test RSS feed");
968
969        let item = RssItem::new()
970            .title("Test Item")
971            .link("https://example.com/item")
972            .description("A test item")
973            .guid("unique-id-1")
974            .pub_date("2024-03-21");
975
976        rss_data.add_item(item);
977
978        assert_eq!(rss_data.items.len(), 1);
979        assert_eq!(rss_data.items[0].title, "Test Item");
980        assert_eq!(rss_data.items[0].link, "https://example.com/item");
981        assert_eq!(rss_data.items[0].description, "A test item");
982        assert_eq!(rss_data.items[0].guid, "unique-id-1");
983        assert_eq!(rss_data.items[0].pub_date, "2024-03-21");
984    }
985
986    #[test]
987    fn test_remove_item() {
988        let mut rss_data = RssData::new(None)
989            .title("Test RSS Feed")
990            .link("https://example.com")
991            .description("A test RSS feed");
992
993        let item1 = RssItem::new()
994            .title("Item 1")
995            .link("https://example.com/item1")
996            .description("First item")
997            .guid("guid1");
998
999        let item2 = RssItem::new()
1000            .title("Item 2")
1001            .link("https://example.com/item2")
1002            .description("Second item")
1003            .guid("guid2");
1004
1005        rss_data.add_item(item1);
1006        rss_data.add_item(item2);
1007
1008        assert_eq!(rss_data.item_count(), 2);
1009
1010        assert!(rss_data.remove_item("guid1"));
1011        assert_eq!(rss_data.item_count(), 1);
1012        assert_eq!(rss_data.items[0].title, "Item 2");
1013
1014        assert!(!rss_data.remove_item("non-existent-guid"));
1015        assert_eq!(rss_data.item_count(), 1);
1016    }
1017
1018    #[test]
1019    fn test_clear_items() {
1020        let mut rss_data = RssData::new(None)
1021            .title("Test RSS Feed")
1022            .link("https://example.com")
1023            .description("A test RSS feed");
1024
1025        rss_data.add_item(RssItem::new().title("Item 1").guid("guid1"));
1026        rss_data.add_item(RssItem::new().title("Item 2").guid("guid2"));
1027
1028        assert_eq!(rss_data.item_count(), 2);
1029
1030        rss_data.clear_items();
1031
1032        assert_eq!(rss_data.item_count(), 0);
1033    }
1034
1035    #[test]
1036    fn test_rss_item_validate() {
1037        let valid_item = RssItem::new()
1038            .title("Valid Item")
1039            .link("https://example.com/valid")
1040            .description("A valid item")
1041            .guid("valid-guid");
1042
1043        assert!(valid_item.validate().is_ok());
1044
1045        let invalid_item = RssItem::new()
1046            .title("Invalid Item")
1047            .description("An invalid item");
1048
1049        let result = invalid_item.validate();
1050        assert!(result.is_err());
1051
1052        if let Err(RssError::ValidationErrors(errors)) = result {
1053            assert_eq!(errors.len(), 1); // Adjust to expect 1 error if only one is returned
1054            assert!(errors.contains(&"Link is missing".to_string())); // Adjust to the actual error returned
1055        } else {
1056            panic!("Expected ValidationErrors");
1057        }
1058    }
1059
1060    #[test]
1061    fn test_validate_url() {
1062        assert!(validate_url("https://example.com").is_ok());
1063        assert!(validate_url("not a url").is_err());
1064    }
1065
1066    #[test]
1067    fn test_parse_date() {
1068        assert!(parse_date("Mon, 01 Jan 2024 00:00:00 GMT").is_ok());
1069        assert!(parse_date("2024-03-21T12:00:00Z").is_ok());
1070        assert!(parse_date("invalid date").is_err());
1071    }
1072
1073    #[test]
1074    fn test_sanitize_input() {
1075        let input = "Test <script>alert('XSS')</script>";
1076        let sanitized = sanitize_input(input);
1077        assert_eq!(
1078            sanitized,
1079            "Test &lt;script&gt;alert(&#x27;XSS&#x27;)&lt;/script&gt;"
1080        );
1081    }
1082
1083    #[test]
1084    fn test_rss_data_set_with_enum() {
1085        let rss_data = RssData::new(None)
1086            .set(RssDataField::Title, "Test Title")
1087            .set(RssDataField::Link, "https://example.com")
1088            .set(RssDataField::Description, "Test Description");
1089
1090        assert_eq!(rss_data.title, "Test Title");
1091        assert_eq!(rss_data.link, "https://example.com");
1092        assert_eq!(rss_data.description, "Test Description");
1093    }
1094
1095    #[test]
1096    fn test_rss_item_set_with_enum() {
1097        let item = RssItem::new()
1098            .set(RssItemField::Title, "Test Item")
1099            .set(RssItemField::Link, "https://example.com/item")
1100            .set(RssItemField::Guid, "unique-id");
1101
1102        assert_eq!(item.title, "Test Item");
1103        assert_eq!(item.link, "https://example.com/item");
1104        assert_eq!(item.guid, "unique-id");
1105    }
1106
1107    #[test]
1108    fn test_to_hash_map() {
1109        let rss_data = RssData::new(None)
1110            .title("Test Title")
1111            .link("https://example.com/rss")
1112            .description("A test RSS feed")
1113            .atom_link("https://example.com/atom")
1114            .language("en")
1115            .managing_editor("editor@example.com")
1116            .webmaster("webmaster@example.com")
1117            .last_build_date("2024-03-21T12:00:00Z")
1118            .pub_date("2024-03-21T12:00:00Z")
1119            .ttl("60")
1120            .generator("RSS Gen")
1121            .guid("unique-guid")
1122            .image_title("Image Title".to_string())
1123            .docs("https://docs.example.com");
1124
1125        let map = rss_data.to_hash_map();
1126
1127        assert_eq!(map.get("title").unwrap(), "Test Title");
1128        assert_eq!(map.get("link").unwrap(), "https://example.com/rss");
1129        assert_eq!(
1130            map.get("atom_link").unwrap(),
1131            "https://example.com/atom"
1132        );
1133        assert_eq!(map.get("language").unwrap(), "en");
1134        assert_eq!(
1135            map.get("managing_editor").unwrap(),
1136            "editor@example.com"
1137        );
1138        assert_eq!(
1139            map.get("webmaster").unwrap(),
1140            "webmaster@example.com"
1141        );
1142        assert_eq!(
1143            map.get("last_build_date").unwrap(),
1144            "2024-03-21T12:00:00Z"
1145        );
1146        assert_eq!(
1147            map.get("pub_date").unwrap(),
1148            "2024-03-21T12:00:00Z"
1149        );
1150        assert_eq!(map.get("ttl").unwrap(), "60");
1151        assert_eq!(map.get("generator").unwrap(), "RSS Gen");
1152        assert_eq!(map.get("guid").unwrap(), "unique-guid");
1153        assert_eq!(map.get("image_title").unwrap(), "Image Title");
1154        assert_eq!(
1155            map.get("docs").unwrap(),
1156            "https://docs.example.com"
1157        );
1158    }
1159
1160    #[test]
1161    fn test_set_image() {
1162        let mut rss_data = RssData::new(None);
1163        rss_data.set_image(
1164            "Test Image Title",
1165            "https://example.com/image.jpg",
1166            "https://example.com",
1167        );
1168        rss_data.title = "RSS Feed Title".to_string();
1169
1170        assert_eq!(rss_data.image_title, "Test Image Title");
1171        assert_eq!(rss_data.image_url, "https://example.com/image.jpg");
1172        assert_eq!(rss_data.image_link, "https://example.com");
1173        assert_eq!(rss_data.title, "RSS Feed Title");
1174    }
1175
1176    #[test]
1177    fn test_rss_feed_parsing() {
1178        let rss_xml = r#"
1179        <?xml version="1.0" encoding="UTF-8"?>
1180        <rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"
1181             xmlns:dc="http://purl.org/dc/elements/1.1/"
1182             xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
1183             xmlns:taxo="http://purl.org/rss/1.0/modules/taxonomy/">
1184          <channel>
1185            <title>GETS Open Tenders or Quotes</title>
1186            <link>https://www.gets.govt.nz//ExternalIndex.htm</link>
1187            <description>This feed lists the current open tenders or requests for quote listed on the GETS.</description>
1188            <image>
1189              <title>Open tenders or Requests for Quote from GETS</title>
1190              <url>https://www.gets.govt.nz//ext/default/img/getsLogo.jpg</url>
1191              <link>https://www.gets.govt.nz//ExternalIndex.htm</link>
1192            </image>
1193          </channel>
1194        </rss>
1195        "#;
1196
1197        let parsed: Rss =
1198            from_str(rss_xml).expect("Failed to parse RSS XML");
1199
1200        assert_eq!(parsed.channel.title, "GETS Open Tenders or Quotes");
1201        assert_eq!(
1202            parsed.channel.link,
1203            "https://www.gets.govt.nz//ExternalIndex.htm"
1204        );
1205        assert_eq!(parsed.channel.description, "This feed lists the current open tenders or requests for quote listed on the GETS.");
1206        assert_eq!(
1207            parsed.channel.image.title,
1208            "Open tenders or Requests for Quote from GETS"
1209        );
1210        assert_eq!(
1211            parsed.channel.image.url,
1212            "https://www.gets.govt.nz//ext/default/img/getsLogo.jpg"
1213        );
1214        assert_eq!(
1215            parsed.channel.image.link,
1216            "https://www.gets.govt.nz//ExternalIndex.htm"
1217        );
1218    }
1219
1220    #[test]
1221    fn test_rss_version_from_str() {
1222        assert_eq!(
1223            RssVersion::from_str("0.90").unwrap(),
1224            RssVersion::RSS0_90
1225        );
1226        assert_eq!(
1227            RssVersion::from_str("0.91").unwrap(),
1228            RssVersion::RSS0_91
1229        );
1230        assert_eq!(
1231            RssVersion::from_str("0.92").unwrap(),
1232            RssVersion::RSS0_92
1233        );
1234        assert_eq!(
1235            RssVersion::from_str("1.0").unwrap(),
1236            RssVersion::RSS1_0
1237        );
1238        assert_eq!(
1239            RssVersion::from_str("2.0").unwrap(),
1240            RssVersion::RSS2_0
1241        );
1242        assert!(RssVersion::from_str("3.0").is_err());
1243    }
1244
1245    #[test]
1246    fn test_rss_version_display() {
1247        assert_eq!(format!("{}", RssVersion::RSS0_90), "0.90");
1248        assert_eq!(format!("{}", RssVersion::RSS0_91), "0.91");
1249        assert_eq!(format!("{}", RssVersion::RSS0_92), "0.92");
1250        assert_eq!(format!("{}", RssVersion::RSS1_0), "1.0");
1251        assert_eq!(format!("{}", RssVersion::RSS2_0), "2.0");
1252    }
1253
1254    #[test]
1255    fn test_rss_data_set_methods() {
1256        let rss_data = RssData::new(None)
1257            .atom_link("https://example.com/atom")
1258            .author("John Doe")
1259            .category("Technology")
1260            .copyright("© 2024 Example Inc.")
1261            .description("A sample RSS feed")
1262            .docs("https://example.com/rss-docs")
1263            .generator("RSS Gen v1.0")
1264            .guid("unique-guid-123")
1265            .image_title("Feed Image")
1266            .image_url("https://example.com/image.jpg")
1267            .image_link("https://example.com")
1268            .language("en-US")
1269            .last_build_date("2024-03-21T12:00:00Z")
1270            .link("https://example.com")
1271            .managing_editor("editor@example.com")
1272            .pub_date("2024-03-21T00:00:00Z")
1273            .title("Sample Feed")
1274            .ttl("60")
1275            .webmaster("webmaster@example.com");
1276
1277        assert_eq!(rss_data.atom_link, "https://example.com/atom");
1278        assert_eq!(rss_data.author, "John Doe");
1279        assert_eq!(rss_data.category, "Technology");
1280        assert_eq!(rss_data.copyright, "© 2024 Example Inc.");
1281        assert_eq!(rss_data.description, "A sample RSS feed");
1282        assert_eq!(rss_data.docs, "https://example.com/rss-docs");
1283        assert_eq!(rss_data.generator, "RSS Gen v1.0");
1284        assert_eq!(rss_data.guid, "unique-guid-123");
1285        assert_eq!(rss_data.image_title, "Feed Image");
1286        assert_eq!(rss_data.image_url, "https://example.com/image.jpg");
1287        assert_eq!(rss_data.image_link, "https://example.com");
1288        assert_eq!(rss_data.language, "en-US");
1289        assert_eq!(rss_data.last_build_date, "2024-03-21T12:00:00Z");
1290        assert_eq!(rss_data.link, "https://example.com");
1291        assert_eq!(rss_data.managing_editor, "editor@example.com");
1292        assert_eq!(rss_data.pub_date, "2024-03-21T00:00:00Z");
1293        assert_eq!(rss_data.title, "Sample Feed");
1294        assert_eq!(rss_data.ttl, "60");
1295        assert_eq!(rss_data.webmaster, "webmaster@example.com");
1296    }
1297
1298    #[test]
1299    fn test_rss_data_empty() {
1300        let rss_data = RssData::new(None);
1301        assert!(rss_data.title.is_empty());
1302        assert!(rss_data.link.is_empty());
1303        assert!(rss_data.description.is_empty());
1304        assert_eq!(rss_data.items.len(), 0);
1305    }
1306
1307    #[test]
1308    fn test_rss_item_empty() {
1309        let item = RssItem::new();
1310        assert!(item.title.is_empty());
1311        assert!(item.link.is_empty());
1312        assert!(item.guid.is_empty());
1313        assert!(item.description.is_empty());
1314    }
1315
1316    #[test]
1317    fn test_rss_data_to_hash_map() {
1318        let rss_data = RssData::new(None)
1319            .title("Test Feed")
1320            .link("https://example.com")
1321            .description("A test feed");
1322
1323        let hash_map = rss_data.to_hash_map();
1324        assert_eq!(hash_map.get("title").unwrap(), "Test Feed");
1325        assert_eq!(
1326            hash_map.get("link").unwrap(),
1327            "https://example.com"
1328        );
1329        assert_eq!(hash_map.get("description").unwrap(), "A test feed");
1330    }
1331
1332    #[test]
1333    fn test_rss_data_version_setter() {
1334        let rss_data = RssData::new(None).version(RssVersion::RSS1_0);
1335        assert_eq!(rss_data.version, RssVersion::RSS1_0);
1336    }
1337
1338    #[test]
1339    fn test_remove_item_not_found() {
1340        let mut rss_data = RssData::new(None);
1341        let item = RssItem::new().guid("existing-guid");
1342        rss_data.add_item(item);
1343
1344        // Try removing an item with a non-existent GUID
1345        let removed = rss_data.remove_item("non-existent-guid");
1346        assert!(!removed);
1347        assert_eq!(rss_data.items.len(), 1);
1348    }
1349
1350    #[test]
1351    fn test_set_item_field_empty_items() {
1352        let mut rss_data = RssData::new(None);
1353        rss_data.set_item_field(RssItemField::Title, "Test Item Title");
1354
1355        assert_eq!(rss_data.items.len(), 1);
1356        assert_eq!(rss_data.items[0].title, "Test Item Title");
1357    }
1358
1359    #[test]
1360    fn test_set_image_empty() {
1361        let mut rss_data = RssData::new(None);
1362        rss_data.set_image("", "", "");
1363
1364        assert!(rss_data.image_title.is_empty());
1365        assert!(rss_data.image_url.is_empty());
1366        assert!(rss_data.image_link.is_empty());
1367    }
1368
1369    #[test]
1370    fn test_rss_item_set_empty_field() {
1371        let item = RssItem::new().set(RssItemField::Title, "");
1372        assert!(item.title.is_empty());
1373    }
1374
1375    #[test]
1376    fn test_rss_data_validate_invalid_pub_date() {
1377        let rss_data = RssData::new(None)
1378            .title("Test Feed")
1379            .link("https://example.com")
1380            .description("A test feed")
1381            .pub_date("not a valid date");
1382
1383        let result = rss_data.validate();
1384        assert!(result.is_err());
1385        if let Err(RssError::ValidationErrors(errors)) = result {
1386            assert!(
1387                errors
1388                    .iter()
1389                    .any(|e| e.contains("Invalid publication date")),
1390                "Expected 'Invalid publication date' error, got: {errors:?}"
1391            );
1392        } else {
1393            panic!("Expected ValidationErrors");
1394        }
1395    }
1396
1397    #[test]
1398    fn test_rss_item_validate_invalid_link() {
1399        let item = RssItem::new()
1400            .title("Item")
1401            .link("not-a-valid-url")
1402            .description("Desc");
1403
1404        let result = item.validate();
1405        assert!(result.is_err());
1406        if let Err(RssError::ValidationErrors(errors)) = result {
1407            assert!(
1408                errors.iter().any(|e| e.contains("Invalid link")),
1409                "Expected 'Invalid link' error, got: {errors:?}"
1410            );
1411        } else {
1412            panic!("Expected ValidationErrors");
1413        }
1414    }
1415
1416    #[test]
1417    fn test_validate_size_exceeds_max() {
1418        let mut rss_data = RssData::new(Some(RssVersion::RSS2_0));
1419        // Fill description with enough data to exceed MAX_FEED_SIZE (1MB)
1420        rss_data.description = "x".repeat(MAX_FEED_SIZE + 1);
1421        let result = rss_data.validate_size();
1422        assert!(result.is_err());
1423        if let Err(RssError::InvalidInput(msg)) = result {
1424            assert!(msg.contains("exceeds maximum allowed size"));
1425        } else {
1426            panic!("Expected InvalidInput error");
1427        }
1428    }
1429
1430    #[test]
1431    fn test_validate_size_ok() {
1432        let rss_data = RssData::new(Some(RssVersion::RSS2_0))
1433            .title("Test")
1434            .link("https://example.com")
1435            .description("Short description");
1436        assert!(rss_data.validate_size().is_ok());
1437    }
1438
1439    #[test]
1440    fn test_set_item_field_all_variants() {
1441        let mut rss_data = RssData::new(None);
1442        rss_data.set_item_field(RssItemField::Title, "Title");
1443        rss_data
1444            .set_item_field(RssItemField::Link, "https://example.com");
1445        rss_data.set_item_field(RssItemField::Description, "Desc");
1446        rss_data.set_item_field(RssItemField::Guid, "guid-1");
1447        rss_data.set_item_field(
1448            RssItemField::PubDate,
1449            "Mon, 01 Jan 2024 00:00:00 GMT",
1450        );
1451        rss_data
1452            .set_item_field(RssItemField::Author, "author@example.com");
1453        rss_data.set_item_field(RssItemField::Category, "tech");
1454        rss_data.set_item_field(
1455            RssItemField::Comments,
1456            "https://example.com/comments",
1457        );
1458        rss_data.set_item_field(
1459            RssItemField::Enclosure,
1460            "https://example.com/file.mp3",
1461        );
1462        rss_data.set_item_field(
1463            RssItemField::Source,
1464            "https://example.com/source",
1465        );
1466
1467        let item = &rss_data.items[0];
1468        assert_eq!(item.title, "Title");
1469        assert_eq!(item.link, "https://example.com");
1470        assert_eq!(item.description, "Desc");
1471        assert_eq!(item.guid, "guid-1");
1472        assert_eq!(item.author, "author@example.com");
1473        assert_eq!(item.category, Some("tech".to_string()));
1474        assert_eq!(
1475            item.comments,
1476            Some("https://example.com/comments".to_string())
1477        );
1478        assert_eq!(
1479            item.enclosure,
1480            Some("https://example.com/file.mp3".to_string())
1481        );
1482        assert_eq!(
1483            item.source,
1484            Some("https://example.com/source".to_string())
1485        );
1486    }
1487
1488    #[test]
1489    fn test_rss_item_builder_all_fields() {
1490        let item = RssItem::new()
1491            .title("Title")
1492            .link("https://example.com")
1493            .description("Desc")
1494            .guid("guid-1")
1495            .pub_date("Mon, 01 Jan 2024 00:00:00 GMT")
1496            .author("author@example.com")
1497            .category("tech")
1498            .comments("https://example.com/comments")
1499            .enclosure("https://example.com/file.mp3")
1500            .source("https://example.com/source");
1501
1502        assert_eq!(item.category, Some("tech".to_string()));
1503        assert_eq!(
1504            item.comments,
1505            Some("https://example.com/comments".to_string())
1506        );
1507        assert_eq!(
1508            item.enclosure,
1509            Some("https://example.com/file.mp3".to_string())
1510        );
1511        assert_eq!(
1512            item.source,
1513            Some("https://example.com/source".to_string())
1514        );
1515    }
1516
1517    #[test]
1518    fn test_rss_item_pub_date_parsed_valid() {
1519        let item =
1520            RssItem::new().pub_date("Sat, 07 Sep 2002 09:42:31 GMT");
1521        let result = item.pub_date_parsed();
1522        assert!(result.is_ok());
1523    }
1524
1525    #[test]
1526    fn test_rss_item_pub_date_parsed_invalid() {
1527        let item = RssItem::new().pub_date("not-a-date");
1528        let result = item.pub_date_parsed();
1529        assert!(result.is_err());
1530    }
1531
1532    #[test]
1533    fn test_parse_date_iso8601() {
1534        let result = parse_date("2024-01-15T10:30:00Z");
1535        assert!(result.is_ok());
1536    }
1537
1538    #[test]
1539    fn test_parse_date_invalid() {
1540        let result = parse_date("completely invalid date");
1541        assert!(result.is_err());
1542    }
1543}