Skip to main content

rss_gen/
lib.rs

1// Copyright © 2024 RSS Gen. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4// src/lib.rs
5
6#![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
23/// Atom 1.0 feed generation, validation, and feed-format detection.
24pub mod atom;
25/// Contains the main types and data structures used to represent RSS feeds.
26pub mod data;
27/// Defines error types used throughout the library.
28pub mod error;
29/// Implements RSS feed generation functionality.
30pub mod generator;
31/// Provides procedural macros for simplifying RSS operations.
32pub mod macros;
33/// Implements RSS feed parsing functionality.
34pub mod parser;
35/// Provides utilities for validating RSS feeds.
36pub 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
47/// The current version of the rss-gen crate, set at compile-time from Cargo.toml.
48pub const VERSION: &str = env!("CARGO_PKG_VERSION");
49
50/// Maximum length for title fields in the RSS feed.
51pub const MAX_TITLE_LENGTH: usize = 256;
52/// Maximum length for link fields in the RSS feed.
53pub const MAX_LINK_LENGTH: usize = 2048;
54/// Maximum length for description fields in the RSS feed.
55pub const MAX_DESCRIPTION_LENGTH: usize = 100_000;
56/// Maximum length for general fields in the RSS feed.
57pub const MAX_GENERAL_LENGTH: usize = 1024;
58/// Maximum size for the entire RSS feed.
59pub const MAX_FEED_SIZE: usize = 1_048_576; // 1 MB
60
61/// A convenience function to generate a minimal valid RSS 2.0 feed.
62///
63/// This function creates an RSS 2.0 feed with the provided title, link, and description,
64/// and includes one example item.
65///
66/// # Arguments
67///
68/// * `title` - The title of the RSS feed.
69/// * `link` - The link to the website associated with the RSS feed.
70/// * `description` - A brief description of the RSS feed.
71///
72/// # Returns
73///
74/// A `Result` containing the generated RSS feed as a `String` if successful,
75/// or an `RssError` if generation fails.
76///
77/// # Examples
78///
79/// ```rust
80/// use rss_gen::quick_rss;
81///
82/// let rss = quick_rss(
83///     "My Rust Blog",
84///     "https://myrustblog.com",
85///     "A blog about Rust programming"
86/// );
87///
88/// match rss {
89///     Ok(feed) => println!("Generated RSS feed: {}", feed),
90///     Err(e) => eprintln!("Error: {}", e),
91/// }
92/// ```
93///
94/// # Errors
95///
96/// This function will return an error if:
97/// - Any of the input strings are empty
98/// - Any of the input strings exceed their respective maximum lengths
99/// - The `link` is not a valid URL starting with "http://" or "https://"
100/// - RSS generation fails for any reason
101///
102/// # Security
103///
104/// This function performs basic input validation, but it's recommended to sanitize
105/// the input parameters before passing them to this function, especially if they
106/// come from untrusted sources.
107#[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    // Validate input
114    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    // Basic URL validation
131    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    // Add an example item
143    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
154/// Prelude module for convenient importing of common types and functions.
155pub 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    // Note: The following tests depend on the implementation of RssData and its methods,
283    // which are not shown in the provided code. You may need to adjust these tests
284    // based on your actual implementation.
285
286    #[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        // Add items until we exceed MAX_FEED_SIZE
294        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}