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