use crate::{
error::{Result, RssError},
MAX_FEED_SIZE, MAX_GENERAL_LENGTH,
};
use dtt::datetime::DateTime;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fmt;
use std::str::FromStr;
use time::{
format_description::well_known::Iso8601,
format_description::well_known::Rfc2822, OffsetDateTime,
};
use url::Url;
#[derive(
Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize,
)]
#[non_exhaustive]
pub enum RssVersion {
RSS0_90,
RSS0_91,
RSS0_92,
RSS1_0,
RSS2_0,
}
impl RssVersion {
#[must_use]
pub const fn as_str(&self) -> &'static str {
match self {
Self::RSS0_90 => "0.90",
Self::RSS0_91 => "0.91",
Self::RSS0_92 => "0.92",
Self::RSS1_0 => "1.0",
Self::RSS2_0 => "2.0",
}
}
}
impl Default for RssVersion {
fn default() -> Self {
Self::RSS2_0
}
}
impl fmt::Display for RssVersion {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.as_str())
}
}
impl FromStr for RssVersion {
type Err = RssError;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
match s {
"0.90" => Ok(Self::RSS0_90),
"0.91" => Ok(Self::RSS0_91),
"0.92" => Ok(Self::RSS0_92),
"1.0" => Ok(Self::RSS1_0),
"2.0" => Ok(Self::RSS2_0),
_ => Err(RssError::InvalidRssVersion(s.to_string())),
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, Default)]
#[non_exhaustive]
pub struct RssData {
pub atom_link: String,
pub author: String,
pub category: String,
pub copyright: String,
pub description: String,
pub docs: String,
pub generator: String,
pub guid: String,
pub image_title: String,
pub image_url: String,
pub image_link: String,
pub language: String,
pub last_build_date: String,
pub link: String,
pub managing_editor: String,
pub pub_date: String,
pub title: String,
pub ttl: String,
pub webmaster: String,
pub items: Vec<RssItem>,
pub version: RssVersion,
pub creator: String,
pub date: String,
}
impl RssData {
#[must_use]
pub fn new(version: Option<RssVersion>) -> Self {
Self {
version: version.unwrap_or_default(),
..Default::default()
}
}
#[must_use]
pub fn set<T: Into<String>>(
mut self,
field: RssDataField,
value: T,
) -> Self {
let value = sanitize_input(&value.into());
match field {
RssDataField::AtomLink => self.atom_link = value,
RssDataField::Author => self.author = value,
RssDataField::Category => self.category = value,
RssDataField::Copyright => self.copyright = value,
RssDataField::Description => self.description = value,
RssDataField::Docs => self.docs = value,
RssDataField::Generator => self.generator = value,
RssDataField::Guid => self.guid = value,
RssDataField::ImageTitle => self.image_title = value,
RssDataField::ImageUrl => self.image_url = value,
RssDataField::ImageLink => self.image_link = value,
RssDataField::Language => self.language = value,
RssDataField::LastBuildDate => self.last_build_date = value,
RssDataField::Link => self.link = value,
RssDataField::ManagingEditor => {
self.managing_editor = value;
}
RssDataField::PubDate => self.pub_date = value,
RssDataField::Title => self.title = value,
RssDataField::Ttl => self.ttl = value,
RssDataField::Webmaster => self.webmaster = value,
}
self
}
pub fn set_item_field<T: Into<String>>(
&mut self,
field: RssItemField,
value: T,
) {
let value = sanitize_input(&value.into());
if self.items.is_empty() {
self.items.push(RssItem::new());
}
let item = self.items.last_mut().unwrap();
match field {
RssItemField::Guid => item.guid = value,
RssItemField::Category => item.category = Some(value),
RssItemField::Description => item.description = value,
RssItemField::Link => item.link = value,
RssItemField::PubDate => item.pub_date = value,
RssItemField::Title => item.title = value,
RssItemField::Author => item.author = value,
RssItemField::Comments => item.comments = Some(value),
RssItemField::Enclosure => item.enclosure = Some(value),
RssItemField::Source => item.source = Some(value),
}
}
pub fn validate_size(&self) -> Result<()> {
let mut total_size = 0;
total_size += self.title.len();
total_size += self.link.len();
total_size += self.description.len();
for item in &self.items {
total_size += item.title.len();
total_size += item.link.len();
total_size += item.description.len();
}
if total_size > MAX_FEED_SIZE {
return Err(RssError::InvalidInput(
format!("Total feed size exceeds maximum allowed size of {} bytes", MAX_FEED_SIZE)
));
}
Ok(())
}
pub fn set_image(&mut self, title: &str, url: &str, link: &str) {
self.image_title = sanitize_input(title);
self.image_url = sanitize_input(url);
self.image_link = sanitize_input(link);
}
pub fn add_item(&mut self, item: RssItem) {
self.items.push(item);
}
pub fn remove_item(&mut self, guid: &str) -> bool {
let initial_len = self.items.len();
self.items.retain(|item| item.guid != guid);
self.items.len() < initial_len
}
#[must_use]
pub fn item_count(&self) -> usize {
self.items.len()
}
pub fn clear_items(&mut self) {
self.items.clear();
}
pub fn validate(&self) -> Result<()> {
let mut errors = Vec::new();
if self.title.is_empty() {
errors.push("Title is missing".to_string());
}
if self.link.is_empty() {
errors.push("Link is missing".to_string());
} else if let Err(e) = validate_url(&self.link) {
errors.push(format!("Invalid link: {}", e));
}
if self.description.is_empty() {
errors.push("Description is missing".to_string());
}
if self.category.len() > MAX_GENERAL_LENGTH {
return Err(RssError::InvalidInput(format!(
"Category exceeds maximum allowed length of {} characters",
MAX_GENERAL_LENGTH
)));
}
if !self.pub_date.is_empty() {
if let Err(e) = parse_date(&self.pub_date) {
errors.push(format!("Invalid publication date: {}", e));
}
}
if !errors.is_empty() {
return Err(RssError::ValidationErrors(errors));
}
Ok(())
}
#[must_use]
pub fn to_hash_map(&self) -> HashMap<String, String> {
let mut map = HashMap::new();
map.insert("atom_link".to_string(), self.atom_link.clone());
map.insert("author".to_string(), self.author.clone());
map.insert("category".to_string(), self.category.clone());
map.insert("copyright".to_string(), self.copyright.clone());
map.insert("description".to_string(), self.description.clone());
map.insert("docs".to_string(), self.docs.clone());
map.insert("generator".to_string(), self.generator.clone());
map.insert("guid".to_string(), self.guid.clone());
map.insert("image_title".to_string(), self.image_title.clone());
map.insert("image_url".to_string(), self.image_url.clone());
map.insert("image_link".to_string(), self.image_link.clone());
map.insert("language".to_string(), self.language.clone());
map.insert(
"last_build_date".to_string(),
self.last_build_date.clone(),
);
map.insert("link".to_string(), self.link.clone());
map.insert(
"managing_editor".to_string(),
self.managing_editor.clone(),
);
map.insert("pub_date".to_string(), self.pub_date.clone());
map.insert("title".to_string(), self.title.clone());
map.insert("ttl".to_string(), self.ttl.clone());
map.insert("webmaster".to_string(), self.webmaster.clone());
map
}
#[must_use]
pub fn version(mut self, version: RssVersion) -> Self {
self.version = version;
self
}
#[must_use]
pub fn atom_link<T: Into<String>>(self, value: T) -> Self {
self.set(RssDataField::AtomLink, value)
}
#[must_use]
pub fn author<T: Into<String>>(self, value: T) -> Self {
self.set(RssDataField::Author, value)
}
#[must_use]
pub fn category<T: Into<String>>(self, value: T) -> Self {
self.set(RssDataField::Category, value)
}
#[must_use]
pub fn copyright<T: Into<String>>(self, value: T) -> Self {
self.set(RssDataField::Copyright, value)
}
#[must_use]
pub fn description<T: Into<String>>(self, value: T) -> Self {
self.set(RssDataField::Description, value)
}
#[must_use]
pub fn docs<T: Into<String>>(self, value: T) -> Self {
self.set(RssDataField::Docs, value)
}
#[must_use]
pub fn generator<T: Into<String>>(self, value: T) -> Self {
self.set(RssDataField::Generator, value)
}
#[must_use]
pub fn guid<T: Into<String>>(self, value: T) -> Self {
self.set(RssDataField::Guid, value)
}
#[must_use]
pub fn image_title<T: Into<String>>(self, value: T) -> Self {
self.set(RssDataField::ImageTitle, value)
}
#[must_use]
pub fn image_url<T: Into<String>>(self, value: T) -> Self {
self.set(RssDataField::ImageUrl, value)
}
#[must_use]
pub fn image_link<T: Into<String>>(self, value: T) -> Self {
self.set(RssDataField::ImageLink, value)
}
#[must_use]
pub fn language<T: Into<String>>(self, value: T) -> Self {
self.set(RssDataField::Language, value)
}
#[must_use]
pub fn last_build_date<T: Into<String>>(self, value: T) -> Self {
self.set(RssDataField::LastBuildDate, value)
}
#[must_use]
pub fn link<T: Into<String>>(self, value: T) -> Self {
self.set(RssDataField::Link, value)
}
#[must_use]
pub fn managing_editor<T: Into<String>>(self, value: T) -> Self {
self.set(RssDataField::ManagingEditor, value)
}
#[must_use]
pub fn pub_date<T: Into<String>>(self, value: T) -> Self {
self.set(RssDataField::PubDate, value)
}
#[must_use]
pub fn title<T: Into<String>>(self, value: T) -> Self {
self.set(RssDataField::Title, value)
}
#[must_use]
pub fn ttl<T: Into<String>>(self, value: T) -> Self {
self.set(RssDataField::Ttl, value)
}
#[must_use]
pub fn webmaster<T: Into<String>>(self, value: T) -> Self {
self.set(RssDataField::Webmaster, value)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum RssDataField {
AtomLink,
Author,
Category,
Copyright,
Description,
Docs,
Generator,
Guid,
ImageTitle,
ImageUrl,
ImageLink,
Language,
LastBuildDate,
Link,
ManagingEditor,
PubDate,
Title,
Ttl,
Webmaster,
}
#[derive(
Debug, Default, PartialEq, Eq, Clone, Serialize, Deserialize,
)]
#[non_exhaustive]
pub struct RssItem {
pub guid: String,
pub category: Option<String>,
pub description: String,
pub link: String,
pub pub_date: String,
pub title: String,
pub author: String,
pub comments: Option<String>,
pub enclosure: Option<String>,
pub source: Option<String>,
pub creator: Option<String>,
pub date: Option<String>,
}
impl RssItem {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn set<T: Into<String>>(
mut self,
field: RssItemField,
value: T,
) -> Self {
let value = sanitize_input(&value.into());
match field {
RssItemField::Guid => self.guid = value,
RssItemField::Category => self.category = Some(value),
RssItemField::Description => self.description = value,
RssItemField::Link => self.link = value,
RssItemField::PubDate => self.pub_date = value,
RssItemField::Title => self.title = value,
RssItemField::Author => self.author = value,
RssItemField::Comments => self.comments = Some(value),
RssItemField::Enclosure => self.enclosure = Some(value),
RssItemField::Source => self.source = Some(value),
}
self
}
pub fn validate(&self) -> Result<()> {
let mut errors = Vec::new();
if self.title.is_empty() {
errors.push("Title is missing".to_string());
}
if self.link.is_empty() {
errors.push("Link is missing".to_string());
} else if let Err(e) = validate_url(&self.link) {
errors.push(format!("Invalid link: {}", e));
}
if self.description.is_empty() {
errors.push("Description is missing".to_string());
}
if !errors.is_empty() {
return Err(RssError::ValidationErrors(errors));
}
Ok(())
}
pub fn pub_date_parsed(&self) -> Result<DateTime> {
parse_date(&self.pub_date)
}
#[must_use]
pub fn guid<T: Into<String>>(self, value: T) -> Self {
self.set(RssItemField::Guid, value)
}
#[must_use]
pub fn category<T: Into<String>>(self, value: T) -> Self {
self.set(RssItemField::Category, value)
}
#[must_use]
pub fn description<T: Into<String>>(self, value: T) -> Self {
self.set(RssItemField::Description, value)
}
#[must_use]
pub fn link<T: Into<String>>(self, value: T) -> Self {
self.set(RssItemField::Link, value)
}
#[must_use]
pub fn pub_date<T: Into<String>>(self, value: T) -> Self {
self.set(RssItemField::PubDate, value)
}
#[must_use]
pub fn title<T: Into<String>>(self, value: T) -> Self {
self.set(RssItemField::Title, value)
}
#[must_use]
pub fn author<T: Into<String>>(self, value: T) -> Self {
self.set(RssItemField::Author, value)
}
#[must_use]
pub fn comments<T: Into<String>>(self, value: T) -> Self {
self.set(RssItemField::Comments, value)
}
#[must_use]
pub fn enclosure<T: Into<String>>(self, value: T) -> Self {
self.set(RssItemField::Enclosure, value)
}
#[must_use]
pub fn source<T: Into<String>>(self, value: T) -> Self {
self.set(RssItemField::Source, value)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum RssItemField {
Guid,
Category,
Description,
Link,
PubDate,
Title,
Author,
Comments,
Enclosure,
Source,
}
pub fn validate_url(url: &str) -> Result<()> {
let parsed_url = Url::parse(url)
.map_err(|_| RssError::InvalidUrl(url.to_string()))?;
if parsed_url.scheme() != "http" && parsed_url.scheme() != "https" {
return Err(RssError::InvalidUrl(
"URL must use http or https protocol".to_string(),
));
}
Ok(())
}
pub fn parse_date(date_str: &str) -> Result<DateTime> {
if OffsetDateTime::parse(date_str, &Rfc2822).is_ok() {
return Ok(
DateTime::new_with_tz("UTC").expect("UTC is always valid")
);
}
if OffsetDateTime::parse(date_str, &Iso8601::DEFAULT).is_ok() {
return Ok(
DateTime::new_with_tz("UTC").expect("UTC is always valid")
);
}
Err(RssError::DateParseError(date_str.to_string()))
}
fn sanitize_input(input: &str) -> String {
input
.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
.replace('\'', "'")
}
#[cfg(test)]
mod tests {
use super::*;
use quick_xml::de::from_str;
#[derive(Debug, Deserialize, PartialEq)]
struct Image {
title: String,
url: String,
link: String,
}
#[derive(Debug, Deserialize, PartialEq)]
struct Channel {
title: String,
link: String,
description: String,
image: Image,
}
#[derive(Debug, Deserialize, PartialEq)]
struct Rss {
#[serde(rename = "channel")]
channel: Channel,
}
#[test]
fn test_rss_version() {
assert_eq!(RssVersion::RSS2_0.as_str(), "2.0");
assert_eq!(RssVersion::default(), RssVersion::RSS2_0);
assert_eq!(RssVersion::RSS1_0.to_string(), "1.0");
assert!(matches!(
"2.0".parse::<RssVersion>(),
Ok(RssVersion::RSS2_0)
));
assert!("3.0".parse::<RssVersion>().is_err());
}
#[test]
fn test_rss_data_new() {
let rss_data = RssData::new(Some(RssVersion::RSS2_0));
assert_eq!(rss_data.version, RssVersion::RSS2_0);
}
#[test]
fn test_rss_data_setters() {
let rss_data = RssData::new(None)
.title("Test Feed")
.link("https://example.com")
.description("A test feed")
.generator("RSS Gen")
.guid("unique-guid")
.pub_date("2024-03-21T12:00:00Z")
.language("en");
assert_eq!(rss_data.title, "Test Feed");
assert_eq!(rss_data.link, "https://example.com");
assert_eq!(rss_data.description, "A test feed");
assert_eq!(rss_data.generator, "RSS Gen");
assert_eq!(rss_data.guid, "unique-guid");
assert_eq!(rss_data.pub_date, "2024-03-21T12:00:00Z");
assert_eq!(rss_data.language, "en");
}
#[test]
fn test_rss_data_validate() {
let valid_rss_data = RssData::new(None)
.title("Valid Feed")
.link("https://example.com")
.description("A valid RSS feed");
assert!(valid_rss_data.validate().is_ok());
let invalid_rss_data = RssData::new(None)
.title("Invalid Feed")
.link("not a valid url")
.description("An invalid RSS feed");
let result = invalid_rss_data.validate();
assert!(result.is_err());
if let Err(RssError::ValidationErrors(errors)) = result {
assert!(errors.iter().any(|e| e.contains("Invalid link")),
"Expected an error containing 'Invalid link', but got: {:?}", errors);
} else {
panic!("Expected ValidationErrors");
}
}
#[test]
fn test_add_item() {
let mut rss_data = RssData::new(None)
.title("Test RSS Feed")
.link("https://example.com")
.description("A test RSS feed");
let item = RssItem::new()
.title("Test Item")
.link("https://example.com/item")
.description("A test item")
.guid("unique-id-1")
.pub_date("2024-03-21");
rss_data.add_item(item);
assert_eq!(rss_data.items.len(), 1);
assert_eq!(rss_data.items[0].title, "Test Item");
assert_eq!(rss_data.items[0].link, "https://example.com/item");
assert_eq!(rss_data.items[0].description, "A test item");
assert_eq!(rss_data.items[0].guid, "unique-id-1");
assert_eq!(rss_data.items[0].pub_date, "2024-03-21");
}
#[test]
fn test_remove_item() {
let mut rss_data = RssData::new(None)
.title("Test RSS Feed")
.link("https://example.com")
.description("A test RSS feed");
let item1 = RssItem::new()
.title("Item 1")
.link("https://example.com/item1")
.description("First item")
.guid("guid1");
let item2 = RssItem::new()
.title("Item 2")
.link("https://example.com/item2")
.description("Second item")
.guid("guid2");
rss_data.add_item(item1);
rss_data.add_item(item2);
assert_eq!(rss_data.item_count(), 2);
assert!(rss_data.remove_item("guid1"));
assert_eq!(rss_data.item_count(), 1);
assert_eq!(rss_data.items[0].title, "Item 2");
assert!(!rss_data.remove_item("non-existent-guid"));
assert_eq!(rss_data.item_count(), 1);
}
#[test]
fn test_clear_items() {
let mut rss_data = RssData::new(None)
.title("Test RSS Feed")
.link("https://example.com")
.description("A test RSS feed");
rss_data.add_item(RssItem::new().title("Item 1").guid("guid1"));
rss_data.add_item(RssItem::new().title("Item 2").guid("guid2"));
assert_eq!(rss_data.item_count(), 2);
rss_data.clear_items();
assert_eq!(rss_data.item_count(), 0);
}
#[test]
fn test_rss_item_validate() {
let valid_item = RssItem::new()
.title("Valid Item")
.link("https://example.com/valid")
.description("A valid item")
.guid("valid-guid");
assert!(valid_item.validate().is_ok());
let invalid_item = RssItem::new()
.title("Invalid Item")
.description("An invalid item");
let result = invalid_item.validate();
assert!(result.is_err());
if let Err(RssError::ValidationErrors(errors)) = result {
assert_eq!(errors.len(), 1); assert!(errors.contains(&"Link is missing".to_string())); } else {
panic!("Expected ValidationErrors");
}
}
#[test]
fn test_validate_url() {
assert!(validate_url("https://example.com").is_ok());
assert!(validate_url("not a url").is_err());
}
#[test]
fn test_parse_date() {
assert!(parse_date("Mon, 01 Jan 2024 00:00:00 GMT").is_ok());
assert!(parse_date("2024-03-21T12:00:00Z").is_ok());
assert!(parse_date("invalid date").is_err());
}
#[test]
fn test_sanitize_input() {
let input = "Test <script>alert('XSS')</script>";
let sanitized = sanitize_input(input);
assert_eq!(
sanitized,
"Test <script>alert('XSS')</script>"
);
}
#[test]
fn test_rss_data_set_with_enum() {
let rss_data = RssData::new(None)
.set(RssDataField::Title, "Test Title")
.set(RssDataField::Link, "https://example.com")
.set(RssDataField::Description, "Test Description");
assert_eq!(rss_data.title, "Test Title");
assert_eq!(rss_data.link, "https://example.com");
assert_eq!(rss_data.description, "Test Description");
}
#[test]
fn test_rss_item_set_with_enum() {
let item = RssItem::new()
.set(RssItemField::Title, "Test Item")
.set(RssItemField::Link, "https://example.com/item")
.set(RssItemField::Guid, "unique-id");
assert_eq!(item.title, "Test Item");
assert_eq!(item.link, "https://example.com/item");
assert_eq!(item.guid, "unique-id");
}
#[test]
fn test_to_hash_map() {
let rss_data = RssData::new(None)
.title("Test Title")
.link("https://example.com/rss")
.description("A test RSS feed")
.atom_link("https://example.com/atom")
.language("en")
.managing_editor("[email protected]")
.webmaster("[email protected]")
.last_build_date("2024-03-21T12:00:00Z")
.pub_date("2024-03-21T12:00:00Z")
.ttl("60")
.generator("RSS Gen")
.guid("unique-guid")
.image_title("Image Title".to_string())
.docs("https://docs.example.com");
let map = rss_data.to_hash_map();
assert_eq!(map.get("title").unwrap(), "Test Title");
assert_eq!(map.get("link").unwrap(), "https://example.com/rss");
assert_eq!(
map.get("atom_link").unwrap(),
"https://example.com/atom"
);
assert_eq!(map.get("language").unwrap(), "en");
assert_eq!(
map.get("managing_editor").unwrap(),
"[email protected]"
);
assert_eq!(
map.get("webmaster").unwrap(),
"[email protected]"
);
assert_eq!(
map.get("last_build_date").unwrap(),
"2024-03-21T12:00:00Z"
);
assert_eq!(
map.get("pub_date").unwrap(),
"2024-03-21T12:00:00Z"
);
assert_eq!(map.get("ttl").unwrap(), "60");
assert_eq!(map.get("generator").unwrap(), "RSS Gen");
assert_eq!(map.get("guid").unwrap(), "unique-guid");
assert_eq!(map.get("image_title").unwrap(), "Image Title");
assert_eq!(
map.get("docs").unwrap(),
"https://docs.example.com"
);
}
#[test]
fn test_set_image() {
let mut rss_data = RssData::new(None);
rss_data.set_image(
"Test Image Title",
"https://example.com/image.jpg",
"https://example.com",
);
rss_data.title = "RSS Feed Title".to_string();
assert_eq!(rss_data.image_title, "Test Image Title");
assert_eq!(rss_data.image_url, "https://example.com/image.jpg");
assert_eq!(rss_data.image_link, "https://example.com");
assert_eq!(rss_data.title, "RSS Feed Title");
}
#[test]
fn test_rss_feed_parsing() {
let rss_xml = r#"
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/"
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:taxo="http://purl.org/rss/1.0/modules/taxonomy/">
<channel>
<title>GETS Open Tenders or Quotes</title>
<link>https://www.gets.govt.nz//ExternalIndex.htm</link>
<description>This feed lists the current open tenders or requests for quote listed on the GETS.</description>
<image>
<title>Open tenders or Requests for Quote from GETS</title>
<url>https://www.gets.govt.nz//ext/default/img/getsLogo.jpg</url>
<link>https://www.gets.govt.nz//ExternalIndex.htm</link>
</image>
</channel>
</rss>
"#;
let parsed: Rss =
from_str(rss_xml).expect("Failed to parse RSS XML");
assert_eq!(parsed.channel.title, "GETS Open Tenders or Quotes");
assert_eq!(
parsed.channel.link,
"https://www.gets.govt.nz//ExternalIndex.htm"
);
assert_eq!(parsed.channel.description, "This feed lists the current open tenders or requests for quote listed on the GETS.");
assert_eq!(
parsed.channel.image.title,
"Open tenders or Requests for Quote from GETS"
);
assert_eq!(
parsed.channel.image.url,
"https://www.gets.govt.nz//ext/default/img/getsLogo.jpg"
);
assert_eq!(
parsed.channel.image.link,
"https://www.gets.govt.nz//ExternalIndex.htm"
);
}
#[test]
fn test_rss_version_from_str() {
assert_eq!(
RssVersion::from_str("0.90").unwrap(),
RssVersion::RSS0_90
);
assert_eq!(
RssVersion::from_str("0.91").unwrap(),
RssVersion::RSS0_91
);
assert_eq!(
RssVersion::from_str("0.92").unwrap(),
RssVersion::RSS0_92
);
assert_eq!(
RssVersion::from_str("1.0").unwrap(),
RssVersion::RSS1_0
);
assert_eq!(
RssVersion::from_str("2.0").unwrap(),
RssVersion::RSS2_0
);
assert!(RssVersion::from_str("3.0").is_err());
}
#[test]
fn test_rss_version_display() {
assert_eq!(format!("{}", RssVersion::RSS0_90), "0.90");
assert_eq!(format!("{}", RssVersion::RSS0_91), "0.91");
assert_eq!(format!("{}", RssVersion::RSS0_92), "0.92");
assert_eq!(format!("{}", RssVersion::RSS1_0), "1.0");
assert_eq!(format!("{}", RssVersion::RSS2_0), "2.0");
}
#[test]
fn test_rss_data_set_methods() {
let rss_data = RssData::new(None)
.atom_link("https://example.com/atom")
.author("John Doe")
.category("Technology")
.copyright("© 2024 Example Inc.")
.description("A sample RSS feed")
.docs("https://example.com/rss-docs")
.generator("RSS Gen v1.0")
.guid("unique-guid-123")
.image_title("Feed Image")
.image_url("https://example.com/image.jpg")
.image_link("https://example.com")
.language("en-US")
.last_build_date("2024-03-21T12:00:00Z")
.link("https://example.com")
.managing_editor("[email protected]")
.pub_date("2024-03-21T00:00:00Z")
.title("Sample Feed")
.ttl("60")
.webmaster("[email protected]");
assert_eq!(rss_data.atom_link, "https://example.com/atom");
assert_eq!(rss_data.author, "John Doe");
assert_eq!(rss_data.category, "Technology");
assert_eq!(rss_data.copyright, "© 2024 Example Inc.");
assert_eq!(rss_data.description, "A sample RSS feed");
assert_eq!(rss_data.docs, "https://example.com/rss-docs");
assert_eq!(rss_data.generator, "RSS Gen v1.0");
assert_eq!(rss_data.guid, "unique-guid-123");
assert_eq!(rss_data.image_title, "Feed Image");
assert_eq!(rss_data.image_url, "https://example.com/image.jpg");
assert_eq!(rss_data.image_link, "https://example.com");
assert_eq!(rss_data.language, "en-US");
assert_eq!(rss_data.last_build_date, "2024-03-21T12:00:00Z");
assert_eq!(rss_data.link, "https://example.com");
assert_eq!(rss_data.managing_editor, "[email protected]");
assert_eq!(rss_data.pub_date, "2024-03-21T00:00:00Z");
assert_eq!(rss_data.title, "Sample Feed");
assert_eq!(rss_data.ttl, "60");
assert_eq!(rss_data.webmaster, "[email protected]");
}
#[test]
fn test_rss_data_empty() {
let rss_data = RssData::new(None);
assert!(rss_data.title.is_empty());
assert!(rss_data.link.is_empty());
assert!(rss_data.description.is_empty());
assert_eq!(rss_data.items.len(), 0);
}
#[test]
fn test_rss_item_empty() {
let item = RssItem::new();
assert!(item.title.is_empty());
assert!(item.link.is_empty());
assert!(item.guid.is_empty());
assert!(item.description.is_empty());
}
#[test]
fn test_rss_data_to_hash_map() {
let rss_data = RssData::new(None)
.title("Test Feed")
.link("https://example.com")
.description("A test feed");
let hash_map = rss_data.to_hash_map();
assert_eq!(hash_map.get("title").unwrap(), "Test Feed");
assert_eq!(
hash_map.get("link").unwrap(),
"https://example.com"
);
assert_eq!(hash_map.get("description").unwrap(), "A test feed");
}
#[test]
fn test_rss_data_version_setter() {
let rss_data = RssData::new(None).version(RssVersion::RSS1_0);
assert_eq!(rss_data.version, RssVersion::RSS1_0);
}
#[test]
fn test_remove_item_not_found() {
let mut rss_data = RssData::new(None);
let item = RssItem::new().guid("existing-guid");
rss_data.add_item(item);
let removed = rss_data.remove_item("non-existent-guid");
assert!(!removed);
assert_eq!(rss_data.items.len(), 1);
}
#[test]
fn test_set_item_field_empty_items() {
let mut rss_data = RssData::new(None);
rss_data.set_item_field(RssItemField::Title, "Test Item Title");
assert_eq!(rss_data.items.len(), 1);
assert_eq!(rss_data.items[0].title, "Test Item Title");
}
#[test]
fn test_set_image_empty() {
let mut rss_data = RssData::new(None);
rss_data.set_image("", "", "");
assert!(rss_data.image_title.is_empty());
assert!(rss_data.image_url.is_empty());
assert!(rss_data.image_link.is_empty());
}
#[test]
fn test_rss_item_set_empty_field() {
let item = RssItem::new().set(RssItemField::Title, "");
assert!(item.title.is_empty());
}
}