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]
31pub enum RssVersion {
32 RSS0_90,
34 RSS0_91,
36 RSS0_92,
38 RSS1_0,
40 RSS2_0,
42}
43
44impl RssVersion {
45 #[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#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
91#[non_exhaustive]
92pub struct RssData {
93 pub atom_link: String,
95 pub author: String,
97 pub category: String,
99 pub copyright: String,
101 pub description: String,
103 pub docs: String,
105 pub generator: String,
107 pub guid: String,
109 pub image_title: String,
111 pub image_url: String,
113 pub image_link: String,
115 pub language: String,
117 pub last_build_date: String,
119 pub link: String,
121 pub managing_editor: String,
123 pub pub_date: String,
125 pub title: String,
127 pub ttl: String,
129 pub webmaster: String,
131 pub items: Vec<RssItem>,
133 pub version: RssVersion,
135 pub creator: String,
137 pub date: String,
139}
140
141impl RssData {
142 #[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 #[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 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 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 for item in &self.items {
258 total_size += item.title.len();
259 total_size += item.link.len();
260 total_size += item.description.len();
261 }
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 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 pub fn add_item(&mut self, item: RssItem) {
294 self.items.push(item);
295 }
296
297 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 #[must_use]
314 pub fn item_count(&self) -> usize {
315 self.items.len()
316 }
317
318 pub fn clear_items(&mut self) {
320 self.items.clear();
321 }
322
323 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 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 #[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 #[must_use]
416 pub fn version(mut self, version: RssVersion) -> Self {
417 self.version = version;
418 self
419 }
420
421 #[must_use]
423 pub fn atom_link<T: Into<String>>(self, value: T) -> Self {
424 self.set(RssDataField::AtomLink, value)
425 }
426
427 #[must_use]
429 pub fn author<T: Into<String>>(self, value: T) -> Self {
430 self.set(RssDataField::Author, value)
431 }
432
433 #[must_use]
435 pub fn category<T: Into<String>>(self, value: T) -> Self {
436 self.set(RssDataField::Category, value)
437 }
438
439 #[must_use]
441 pub fn copyright<T: Into<String>>(self, value: T) -> Self {
442 self.set(RssDataField::Copyright, value)
443 }
444
445 #[must_use]
447 pub fn description<T: Into<String>>(self, value: T) -> Self {
448 self.set(RssDataField::Description, value)
449 }
450
451 #[must_use]
453 pub fn docs<T: Into<String>>(self, value: T) -> Self {
454 self.set(RssDataField::Docs, value)
455 }
456
457 #[must_use]
459 pub fn generator<T: Into<String>>(self, value: T) -> Self {
460 self.set(RssDataField::Generator, value)
461 }
462
463 #[must_use]
465 pub fn guid<T: Into<String>>(self, value: T) -> Self {
466 self.set(RssDataField::Guid, value)
467 }
468
469 #[must_use]
471 pub fn image_title<T: Into<String>>(self, value: T) -> Self {
472 self.set(RssDataField::ImageTitle, value)
473 }
474
475 #[must_use]
477 pub fn image_url<T: Into<String>>(self, value: T) -> Self {
478 self.set(RssDataField::ImageUrl, value)
479 }
480
481 #[must_use]
483 pub fn image_link<T: Into<String>>(self, value: T) -> Self {
484 self.set(RssDataField::ImageLink, value)
485 }
486
487 #[must_use]
489 pub fn language<T: Into<String>>(self, value: T) -> Self {
490 self.set(RssDataField::Language, value)
491 }
492
493 #[must_use]
495 pub fn last_build_date<T: Into<String>>(self, value: T) -> Self {
496 self.set(RssDataField::LastBuildDate, value)
497 }
498
499 #[must_use]
501 pub fn link<T: Into<String>>(self, value: T) -> Self {
502 self.set(RssDataField::Link, value)
503 }
504
505 #[must_use]
507 pub fn managing_editor<T: Into<String>>(self, value: T) -> Self {
508 self.set(RssDataField::ManagingEditor, value)
509 }
510
511 #[must_use]
513 pub fn pub_date<T: Into<String>>(self, value: T) -> Self {
514 self.set(RssDataField::PubDate, value)
515 }
516
517 #[must_use]
519 pub fn title<T: Into<String>>(self, value: T) -> Self {
520 self.set(RssDataField::Title, value)
521 }
522
523 #[must_use]
525 pub fn ttl<T: Into<String>>(self, value: T) -> Self {
526 self.set(RssDataField::Ttl, value)
527 }
528
529 #[must_use]
531 pub fn webmaster<T: Into<String>>(self, value: T) -> Self {
532 self.set(RssDataField::Webmaster, value)
533 }
534}
535
536#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
538pub enum RssDataField {
539 AtomLink,
541 Author,
543 Category,
545 Copyright,
547 Description,
549 Docs,
551 Generator,
553 Guid,
555 ImageTitle,
557 ImageUrl,
559 ImageLink,
561 Language,
563 LastBuildDate,
565 Link,
567 ManagingEditor,
569 PubDate,
571 Title,
573 Ttl,
575 Webmaster,
577}
578
579#[derive(
581 Debug, Default, PartialEq, Eq, Clone, Serialize, Deserialize,
582)]
583#[non_exhaustive]
584pub struct RssItem {
585 pub guid: String,
587 pub category: Option<String>,
589 pub description: String,
591 pub link: String,
593 pub pub_date: String,
595 pub title: String,
597 pub author: String,
599 pub comments: Option<String>,
601 pub enclosure: Option<String>,
603 pub source: Option<String>,
605 pub creator: Option<String>,
607 pub date: Option<String>,
609}
610
611impl RssItem {
612 #[must_use]
614 pub fn new() -> Self {
615 Self::default()
616 }
617
618 #[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 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 if !errors.is_empty() {
686 return Err(RssError::ValidationErrors(errors));
687 }
688
689 Ok(())
690 }
691
692 pub fn pub_date_parsed(&self) -> Result<DateTime> {
704 parse_date(&self.pub_date)
705 }
706
707 #[must_use]
711 pub fn guid<T: Into<String>>(self, value: T) -> Self {
712 self.set(RssItemField::Guid, value)
713 }
714
715 #[must_use]
717 pub fn category<T: Into<String>>(self, value: T) -> Self {
718 self.set(RssItemField::Category, value)
719 }
720
721 #[must_use]
723 pub fn description<T: Into<String>>(self, value: T) -> Self {
724 self.set(RssItemField::Description, value)
725 }
726
727 #[must_use]
729 pub fn link<T: Into<String>>(self, value: T) -> Self {
730 self.set(RssItemField::Link, value)
731 }
732
733 #[must_use]
735 pub fn pub_date<T: Into<String>>(self, value: T) -> Self {
736 self.set(RssItemField::PubDate, value)
737 }
738
739 #[must_use]
741 pub fn title<T: Into<String>>(self, value: T) -> Self {
742 self.set(RssItemField::Title, value)
743 }
744
745 #[must_use]
747 pub fn author<T: Into<String>>(self, value: T) -> Self {
748 self.set(RssItemField::Author, value)
749 }
750
751 #[must_use]
753 pub fn comments<T: Into<String>>(self, value: T) -> Self {
754 self.set(RssItemField::Comments, value)
755 }
756
757 #[must_use]
759 pub fn enclosure<T: Into<String>>(self, value: T) -> Self {
760 self.set(RssItemField::Enclosure, value)
761 }
762
763 #[must_use]
765 pub fn source<T: Into<String>>(self, value: T) -> Self {
766 self.set(RssItemField::Source, value)
767 }
768}
769
770#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
772pub enum RssItemField {
773 Guid,
775 Category,
777 Description,
779 Link,
781 PubDate,
783 Title,
785 Author,
787 Comments,
789 Enclosure,
791 Source,
793}
794
795pub 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
823pub 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 Err(RssError::DateParseError(date_str.to_string()))
859}
860
861fn sanitize_input(input: &str) -> String {
871 input
872 .replace('&', "&")
873 .replace('<', "<")
874 .replace('>', ">")
875 .replace('"', """)
876 .replace('\'', "'")
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); assert!(errors.contains(&"Link is missing".to_string())); } 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 <script>alert('XSS')</script>"
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 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}