#![doc = include_str!("../README.md")]
#![doc(
html_favicon_url = "https://kura.pro/rssgen/images/favicon.ico",
html_logo_url = "https://kura.pro/rssgen/images/logos/rssgen.svg",
html_root_url = "https://docs.rs/rss-gen"
)]
#![crate_name = "rss_gen"]
#![crate_type = "lib"]
#![warn(missing_docs)]
#![forbid(unsafe_code)]
#![deny(clippy::all)]
#![deny(clippy::cargo)]
#![deny(clippy::pedantic)]
#![allow(clippy::module_name_repetitions)]
pub mod data;
pub mod error;
pub mod generator;
pub mod macros;
pub mod parser;
pub mod validator;
pub use data::{RssData, RssItem, RssVersion};
pub use error::{Result, RssError};
pub use generator::generate_rss;
pub use parser::parse_rss;
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
pub const MAX_TITLE_LENGTH: usize = 256;
pub const MAX_LINK_LENGTH: usize = 2048;
pub const MAX_DESCRIPTION_LENGTH: usize = 100_000;
pub const MAX_GENERAL_LENGTH: usize = 1024;
pub const MAX_FEED_SIZE: usize = 1_048_576; #[must_use = "This function returns a Result that should be handled"]
pub fn quick_rss(
title: &str,
link: &str,
description: &str,
) -> Result<String> {
if title.is_empty() || link.is_empty() || description.is_empty() {
return Err(RssError::InvalidInput(
"Title, link, and description must not be empty"
.to_string(),
));
}
if title.len() > MAX_TITLE_LENGTH
|| link.len() > MAX_LINK_LENGTH
|| description.len() > MAX_DESCRIPTION_LENGTH
{
return Err(RssError::InvalidInput(
"Input exceeds maximum allowed length".to_string(),
));
}
if !link.starts_with("http://") && !link.starts_with("https://") {
return Err(RssError::InvalidInput(
"Link must start with http:// or https://".to_string(),
));
}
let mut rss_data = RssData::new(Some(RssVersion::RSS2_0))
.title(title)
.link(link)
.description(description);
rss_data.add_item(
RssItem::new()
.title("Example Item")
.link(format!("{}/example-item", link))
.description("This is an example item in the RSS feed")
.guid(format!("{}/example-item", link)),
);
generate_rss(&rss_data)
}
pub mod prelude {
pub use crate::data::{RssData, RssItem, RssVersion};
pub use crate::error::{Result, RssError};
pub use crate::generate_rss;
pub use crate::parse_rss;
pub use crate::quick_rss;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_quick_rss() {
let result = quick_rss(
"Test Feed",
"https://example.com",
"A test RSS feed",
);
assert!(result.is_ok());
let feed = result.unwrap();
assert!(feed.contains("<title>Test Feed</title>"));
assert!(feed.contains("<link>https://example.com</link>"));
assert!(
feed.contains("<description>A test RSS feed</description>")
);
assert!(feed.contains("<item>"));
assert!(feed.contains("<title>Example Item</title>"));
assert!(feed
.contains("<link>https://example.com/example-item</link>"));
assert!(feed.contains("<description>This is an example item in the RSS feed</description>"));
}
#[test]
fn test_quick_rss_invalid_input() {
let result =
quick_rss("", "https://example.com", "Description");
assert!(result.is_err());
assert!(matches!(result, Err(RssError::InvalidInput(_))));
let result = quick_rss("Title", "not-a-url", "Description");
assert!(result.is_err());
assert!(matches!(result, Err(RssError::InvalidInput(_))));
}
#[test]
fn test_version_constant() {
assert!(VERSION.starts_with(char::is_numeric));
assert!(VERSION.split('.').count() >= 2);
}
#[test]
fn test_quick_rss_max_title_length() {
let long_title = "a".repeat(MAX_TITLE_LENGTH + 1);
let result = quick_rss(
&long_title,
"https://example.com",
"Description",
);
assert!(result.is_err());
assert!(matches!(result, Err(RssError::InvalidInput(_))));
let max_title = "a".repeat(MAX_TITLE_LENGTH);
let result =
quick_rss(&max_title, "https://example.com", "Description");
assert!(result.is_ok());
}
#[test]
fn test_quick_rss_max_link_length() {
let long_link = format!(
"https://example.com/{}",
"a".repeat(MAX_LINK_LENGTH - 19)
);
let result = quick_rss("Title", &long_link, "Description");
assert!(result.is_err());
assert!(matches!(result, Err(RssError::InvalidInput(_))));
let max_link = format!(
"https://example.com/{}",
"a".repeat(MAX_LINK_LENGTH - 20)
);
let result = quick_rss("Title", &max_link, "Description");
assert!(result.is_ok());
}
#[test]
fn test_quick_rss_max_description_length() {
let long_description = "a".repeat(MAX_DESCRIPTION_LENGTH + 1);
let result = quick_rss(
"Title",
"https://example.com",
&long_description,
);
assert!(result.is_err());
assert!(matches!(result, Err(RssError::InvalidInput(_))));
let max_description = "a".repeat(MAX_DESCRIPTION_LENGTH);
let result =
quick_rss("Title", "https://example.com", &max_description);
assert!(result.is_ok());
}
#[test]
fn test_quick_rss_https() {
let result = quick_rss(
"Test Feed",
"https://example.com",
"A test RSS feed",
);
assert!(result.is_ok());
}
#[test]
fn test_quick_rss_http() {
let result = quick_rss(
"Test Feed",
"http://example.com",
"A test RSS feed",
);
assert!(result.is_ok());
}
#[test]
fn test_rss_data_validate_size() {
let mut rss_data = RssData::new(Some(RssVersion::RSS2_0))
.title("Test Feed")
.link("https://example.com")
.description("A test RSS feed");
let item_content = "a".repeat(10000);
for _ in 0..100 {
rss_data.add_item(
RssItem::new()
.title(&item_content)
.link("https://example.com/item")
.description(&item_content),
);
}
assert!(rss_data.validate_size().is_err());
}
#[test]
fn test_max_general_length() {
let mut rss_data = RssData::new(Some(RssVersion::RSS2_0))
.title("Test Feed")
.link("https://example.com")
.description("A test RSS feed");
let long_general_field = "a".repeat(MAX_GENERAL_LENGTH + 1);
rss_data.category.clone_from(&long_general_field);
assert!(rss_data.validate().is_err());
rss_data.category = "a".repeat(MAX_GENERAL_LENGTH);
assert!(rss_data.validate().is_ok());
}
}