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