1#![doc = include_str!("../README.md")]
7#![doc(
8 html_favicon_url = "https://cloudcdn.pro/rssgen/v1/favicon.ico",
9 html_logo_url = "https://cloudcdn.pro/rssgen/v1/logos/rssgen.svg",
10 html_root_url = "https://docs.rs/rss-gen"
11)]
12#![crate_name = "rss_gen"]
13#![crate_type = "lib"]
14#![deny(missing_docs)]
15#![forbid(unsafe_code)]
16#![deny(clippy::all)]
17#![deny(clippy::cargo)]
18#![deny(clippy::pedantic)]
19#![allow(clippy::module_name_repetitions)]
20#![allow(unknown_lints)]
21#![allow(clippy::assert_is_empty)]
22
23pub mod atom;
25pub mod data;
27pub mod error;
29pub mod generator;
31pub mod macros;
33pub mod parser;
35pub mod validator;
37
38pub use atom::{
39 detect_feed_format, generate_atom, AtomEntry, AtomFeed, AtomLink,
40 AtomPerson, AtomTextType, FeedFormat,
41};
42pub use data::{RssData, RssItem, RssVersion};
43pub use error::{Result, RssError, ValidationError};
44pub use generator::generate_rss;
45pub use parser::parse_rss;
46
47pub const VERSION: &str = env!("CARGO_PKG_VERSION");
49
50pub const MAX_TITLE_LENGTH: usize = 256;
52pub const MAX_LINK_LENGTH: usize = 2048;
54pub const MAX_DESCRIPTION_LENGTH: usize = 100_000;
56pub const MAX_GENERAL_LENGTH: usize = 1024;
58pub const MAX_FEED_SIZE: usize = 1_048_576; #[must_use = "This function returns a Result that should be handled"]
108pub fn quick_rss(
109 title: &str,
110 link: &str,
111 description: &str,
112) -> Result<String> {
113 if title.is_empty() || link.is_empty() || description.is_empty() {
115 return Err(RssError::InvalidInput(
116 "Title, link, and description must not be empty"
117 .to_string(),
118 ));
119 }
120
121 if title.len() > MAX_TITLE_LENGTH
122 || link.len() > MAX_LINK_LENGTH
123 || description.len() > MAX_DESCRIPTION_LENGTH
124 {
125 return Err(RssError::InvalidInput(
126 "Input exceeds maximum allowed length".to_string(),
127 ));
128 }
129
130 if !link.starts_with("http://") && !link.starts_with("https://") {
132 return Err(RssError::InvalidInput(
133 "Link must start with http:// or https://".to_string(),
134 ));
135 }
136
137 let mut rss_data = RssData::new(Some(RssVersion::RSS2_0))
138 .title(title)
139 .link(link)
140 .description(description);
141
142 rss_data.add_item(
144 RssItem::new()
145 .title("Example Item")
146 .link(format!("{link}/example-item"))
147 .description("This is an example item in the RSS feed")
148 .guid(format!("{link}/example-item")),
149 );
150
151 generate_rss(&rss_data)
152}
153
154pub mod prelude {
156 pub use crate::atom::{
157 detect_feed_format, generate_atom, AtomEntry, AtomFeed,
158 AtomLink, AtomPerson, AtomTextType, FeedFormat,
159 };
160 pub use crate::data::{RssData, RssItem, RssVersion};
161 pub use crate::error::{Result, RssError, ValidationError};
162 pub use crate::generate_rss;
163 pub use crate::parse_rss;
164 pub use crate::quick_rss;
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170
171 #[test]
172 fn test_quick_rss() {
173 let result = quick_rss(
174 "Test Feed",
175 "https://example.com",
176 "A test RSS feed",
177 );
178 assert!(result.is_ok());
179 let feed = result.unwrap();
180 assert!(feed.contains("<title>Test Feed</title>"));
181 assert!(feed.contains("<link>https://example.com</link>"));
182 assert!(
183 feed.contains("<description>A test RSS feed</description>")
184 );
185 assert!(feed.contains("<item>"));
186 assert!(feed.contains("<title>Example Item</title>"));
187 assert!(feed
188 .contains("<link>https://example.com/example-item</link>"));
189 assert!(feed.contains("<description>This is an example item in the RSS feed</description>"));
190 }
191
192 #[test]
193 fn test_quick_rss_invalid_input() {
194 let result =
195 quick_rss("", "https://example.com", "Description");
196 assert!(result.is_err());
197 assert!(matches!(result, Err(RssError::InvalidInput(_))));
198
199 let result = quick_rss("Title", "not-a-url", "Description");
200 assert!(result.is_err());
201 assert!(matches!(result, Err(RssError::InvalidInput(_))));
202 }
203
204 #[test]
205 fn test_version_constant() {
206 assert!(VERSION.starts_with(char::is_numeric));
207 assert!(VERSION.split('.').count() >= 2);
208 }
209
210 #[test]
211 fn test_quick_rss_max_title_length() {
212 let long_title = "a".repeat(MAX_TITLE_LENGTH + 1);
213 let result = quick_rss(
214 &long_title,
215 "https://example.com",
216 "Description",
217 );
218 assert!(result.is_err());
219 assert!(matches!(result, Err(RssError::InvalidInput(_))));
220
221 let max_title = "a".repeat(MAX_TITLE_LENGTH);
222 let result =
223 quick_rss(&max_title, "https://example.com", "Description");
224 assert!(result.is_ok());
225 }
226
227 #[test]
228 fn test_quick_rss_max_link_length() {
229 let long_link = format!(
230 "https://example.com/{}",
231 "a".repeat(MAX_LINK_LENGTH - 19)
232 );
233 let result = quick_rss("Title", &long_link, "Description");
234 assert!(result.is_err());
235 assert!(matches!(result, Err(RssError::InvalidInput(_))));
236
237 let max_link = format!(
238 "https://example.com/{}",
239 "a".repeat(MAX_LINK_LENGTH - 20)
240 );
241 let result = quick_rss("Title", &max_link, "Description");
242 assert!(result.is_ok());
243 }
244
245 #[test]
246 fn test_quick_rss_max_description_length() {
247 let long_description = "a".repeat(MAX_DESCRIPTION_LENGTH + 1);
248 let result = quick_rss(
249 "Title",
250 "https://example.com",
251 &long_description,
252 );
253 assert!(result.is_err());
254 assert!(matches!(result, Err(RssError::InvalidInput(_))));
255
256 let max_description = "a".repeat(MAX_DESCRIPTION_LENGTH);
257 let result =
258 quick_rss("Title", "https://example.com", &max_description);
259 assert!(result.is_ok());
260 }
261
262 #[test]
263 fn test_quick_rss_https() {
264 let result = quick_rss(
265 "Test Feed",
266 "https://example.com",
267 "A test RSS feed",
268 );
269 assert!(result.is_ok());
270 }
271
272 #[test]
273 fn test_quick_rss_http() {
274 let result = quick_rss(
275 "Test Feed",
276 "http://example.com",
277 "A test RSS feed",
278 );
279 assert!(result.is_ok());
280 }
281
282 #[test]
287 fn test_rss_data_validate_size() {
288 let mut rss_data = RssData::new(Some(RssVersion::RSS2_0))
289 .title("Test Feed")
290 .link("https://example.com")
291 .description("A test RSS feed");
292
293 let item_content = "a".repeat(10000);
295 for _ in 0..100 {
296 rss_data.add_item(
297 RssItem::new()
298 .title(&item_content)
299 .link("https://example.com/item")
300 .description(&item_content),
301 );
302 }
303
304 assert!(rss_data.validate_size().is_err());
305 }
306
307 #[test]
308 fn test_max_general_length() {
309 let mut rss_data = RssData::new(Some(RssVersion::RSS2_0))
310 .title("Test Feed")
311 .link("https://example.com")
312 .description("A test RSS feed");
313
314 let long_general_field = "a".repeat(MAX_GENERAL_LENGTH + 1);
315 rss_data.category.clone_from(&long_general_field);
316
317 assert!(rss_data.validate().is_err());
318
319 rss_data.category = "a".repeat(MAX_GENERAL_LENGTH);
320 assert!(rss_data.validate().is_ok());
321 }
322}