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