1use 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#[derive(
28 Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize,
29)]
30#[non_exhaustive]
31#[derive(Default)]
32pub enum RssVersion {
33 RSS0_90,
35 RSS0_91,
37 RSS0_92,
39 RSS1_0,
41 #[default]
43 RSS2_0,
44}
45
46impl RssVersion {
47 #[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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
87#[non_exhaustive]
88pub struct RssData {
89 pub atom_link: String,
91 pub author: String,
93 pub category: String,
95 pub copyright: String,
97 pub description: String,
99 pub docs: String,
101 pub generator: String,
103 pub guid: String,
105 pub image_title: String,
107 pub image_url: String,
109 pub image_link: String,
111 pub language: String,
113 pub last_build_date: String,
115 pub link: String,
117 pub managing_editor: String,
119 pub pub_date: String,
121 pub title: String,
123 pub ttl: String,
125 pub webmaster: String,
127 pub items: Vec<RssItem>,
129 pub version: RssVersion,
131 pub creator: String,
133 pub date: String,
135}
136
137impl RssData {
138 #[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 #[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 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 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 for item in &self.items {
254 total_size += item.title.len();
255 total_size += item.link.len();
256 total_size += item.description.len();
257 }
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 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 pub fn add_item(&mut self, item: RssItem) {
290 self.items.push(item);
291 }
292
293 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 #[must_use]
310 pub fn item_count(&self) -> usize {
311 self.items.len()
312 }
313
314 pub fn clear_items(&mut self) {
316 self.items.clear();
317 }
318
319 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 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 #[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 #[must_use]
411 pub fn version(mut self, version: RssVersion) -> Self {
412 self.version = version;
413 self
414 }
415
416 #[must_use]
418 pub fn atom_link<T: Into<String>>(self, value: T) -> Self {
419 self.set(RssDataField::AtomLink, value)
420 }
421
422 #[must_use]
424 pub fn author<T: Into<String>>(self, value: T) -> Self {
425 self.set(RssDataField::Author, value)
426 }
427
428 #[must_use]
430 pub fn category<T: Into<String>>(self, value: T) -> Self {
431 self.set(RssDataField::Category, value)
432 }
433
434 #[must_use]
436 pub fn copyright<T: Into<String>>(self, value: T) -> Self {
437 self.set(RssDataField::Copyright, value)
438 }
439
440 #[must_use]
442 pub fn description<T: Into<String>>(self, value: T) -> Self {
443 self.set(RssDataField::Description, value)
444 }
445
446 #[must_use]
448 pub fn docs<T: Into<String>>(self, value: T) -> Self {
449 self.set(RssDataField::Docs, value)
450 }
451
452 #[must_use]
454 pub fn generator<T: Into<String>>(self, value: T) -> Self {
455 self.set(RssDataField::Generator, value)
456 }
457
458 #[must_use]
460 pub fn guid<T: Into<String>>(self, value: T) -> Self {
461 self.set(RssDataField::Guid, value)
462 }
463
464 #[must_use]
466 pub fn image_title<T: Into<String>>(self, value: T) -> Self {
467 self.set(RssDataField::ImageTitle, value)
468 }
469
470 #[must_use]
472 pub fn image_url<T: Into<String>>(self, value: T) -> Self {
473 self.set(RssDataField::ImageUrl, value)
474 }
475
476 #[must_use]
478 pub fn image_link<T: Into<String>>(self, value: T) -> Self {
479 self.set(RssDataField::ImageLink, value)
480 }
481
482 #[must_use]
484 pub fn language<T: Into<String>>(self, value: T) -> Self {
485 self.set(RssDataField::Language, value)
486 }
487
488 #[must_use]
490 pub fn last_build_date<T: Into<String>>(self, value: T) -> Self {
491 self.set(RssDataField::LastBuildDate, value)
492 }
493
494 #[must_use]
496 pub fn link<T: Into<String>>(self, value: T) -> Self {
497 self.set(RssDataField::Link, value)
498 }
499
500 #[must_use]
502 pub fn managing_editor<T: Into<String>>(self, value: T) -> Self {
503 self.set(RssDataField::ManagingEditor, value)
504 }
505
506 #[must_use]
508 pub fn pub_date<T: Into<String>>(self, value: T) -> Self {
509 self.set(RssDataField::PubDate, value)
510 }
511
512 #[must_use]
514 pub fn title<T: Into<String>>(self, value: T) -> Self {
515 self.set(RssDataField::Title, value)
516 }
517
518 #[must_use]
520 pub fn ttl<T: Into<String>>(self, value: T) -> Self {
521 self.set(RssDataField::Ttl, value)
522 }
523
524 #[must_use]
526 pub fn webmaster<T: Into<String>>(self, value: T) -> Self {
527 self.set(RssDataField::Webmaster, value)
528 }
529}
530
531#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
533pub enum RssDataField {
534 AtomLink,
536 Author,
538 Category,
540 Copyright,
542 Description,
544 Docs,
546 Generator,
548 Guid,
550 ImageTitle,
552 ImageUrl,
554 ImageLink,
556 Language,
558 LastBuildDate,
560 Link,
562 ManagingEditor,
564 PubDate,
566 Title,
568 Ttl,
570 Webmaster,
572}
573
574#[derive(
576 Debug, Default, PartialEq, Eq, Clone, Serialize, Deserialize,
577)]
578#[non_exhaustive]
579pub struct RssItem {
580 pub guid: String,
582 pub category: Option<String>,
584 pub description: String,
586 pub link: String,
588 pub pub_date: String,
590 pub title: String,
592 pub author: String,
594 pub comments: Option<String>,
596 pub enclosure: Option<String>,
598 pub source: Option<String>,
600 pub creator: Option<String>,
602 pub date: Option<String>,
604}
605
606impl RssItem {
607 #[must_use]
609 pub fn new() -> Self {
610 Self::default()
611 }
612
613 #[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 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 if !errors.is_empty() {
681 return Err(RssError::ValidationErrors(errors));
682 }
683
684 Ok(())
685 }
686
687 pub fn pub_date_parsed(&self) -> Result<DateTime> {
699 parse_date(&self.pub_date)
700 }
701
702 #[must_use]
706 pub fn guid<T: Into<String>>(self, value: T) -> Self {
707 self.set(RssItemField::Guid, value)
708 }
709
710 #[must_use]
712 pub fn category<T: Into<String>>(self, value: T) -> Self {
713 self.set(RssItemField::Category, value)
714 }
715
716 #[must_use]
718 pub fn description<T: Into<String>>(self, value: T) -> Self {
719 self.set(RssItemField::Description, value)
720 }
721
722 #[must_use]
724 pub fn link<T: Into<String>>(self, value: T) -> Self {
725 self.set(RssItemField::Link, value)
726 }
727
728 #[must_use]
730 pub fn pub_date<T: Into<String>>(self, value: T) -> Self {
731 self.set(RssItemField::PubDate, value)
732 }
733
734 #[must_use]
736 pub fn title<T: Into<String>>(self, value: T) -> Self {
737 self.set(RssItemField::Title, value)
738 }
739
740 #[must_use]
742 pub fn author<T: Into<String>>(self, value: T) -> Self {
743 self.set(RssItemField::Author, value)
744 }
745
746 #[must_use]
748 pub fn comments<T: Into<String>>(self, value: T) -> Self {
749 self.set(RssItemField::Comments, value)
750 }
751
752 #[must_use]
754 pub fn enclosure<T: Into<String>>(self, value: T) -> Self {
755 self.set(RssItemField::Enclosure, value)
756 }
757
758 #[must_use]
760 pub fn source<T: Into<String>>(self, value: T) -> Self {
761 self.set(RssItemField::Source, value)
762 }
763}
764
765#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
767pub enum RssItemField {
768 Guid,
770 Category,
772 Description,
774 Link,
776 PubDate,
778 Title,
780 Author,
782 Comments,
784 Enclosure,
786 Source,
788}
789
790pub 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
818pub 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 Err(RssError::DateParseError(date_str.to_string()))
854}
855
856fn sanitize_input(input: &str) -> String {
866 input
867 .replace('&', "&")
868 .replace('<', "<")
869 .replace('>', ">")
870 .replace('"', """)
871 .replace('\'', "'")
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); assert!(errors.contains(&"Link is missing".to_string())); } 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 <script>alert('XSS')</script>"
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 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 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}