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