rss_gen/
macros.rs

1// Copyright © 2024 RSS Gen. All rights reserved.
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3
4//! This module provides macros for generating RSS feeds and setting fields for RSS data.
5//!
6//! It includes macros for generating RSS feeds from provided data using the quick_xml crate,
7//! as well as macros for setting fields in the `RssData` struct.
8
9/// Generates an RSS feed from the given `RssData` struct.
10///
11/// This macro generates a complete RSS feed in XML format based on the data contained in the provided `RssData`.
12/// It dynamically generates XML elements for each field of the `RssData` using the provided metadata values and
13/// writes them to the specified Writer instance.
14///
15/// # Arguments
16///
17/// * `$writer` - The Writer instance to write the generated XML events.
18/// * `$options` - The `RssData` instance containing the metadata values for generating the RSS feed.
19///
20/// # Returns
21///
22/// Returns `Result<Writer<std::io::Cursor<Vec<u8>>>, Box<dyn Error>>` indicating success or an error if XML writing fails.
23///
24/// # Example
25///
26/// ```text
27/// use rss_gen::{RssData, macro_generate_rss, macro_write_element};
28/// use quick_xml::Writer;
29/// use std::io::Cursor;
30///
31/// fn generate_rss() -> Result<(), Box<dyn std::error::Error>> {
32///     let mut writer = Writer::new(Cursor::new(Vec::new()));
33///     let options = RssData::new()
34///         .Title("My Blog")
35///         .Link("https://example.com")
36///         .Description("A blog about Rust");
37///
38///     let result: Result<Writer<Cursor<Vec<u8>>>, Box<dyn std::error::Error>> = macro_generate_rss!(writer, options);
39///     assert!(result.is_ok());
40///     Ok(())
41/// }
42/// ```text
43/// generate_rss().unwrap();
44/// ```
45#[macro_export]
46macro_rules! macro_generate_rss {
47    ($writer:expr, $options:expr) => {{
48        use quick_xml::events::{
49            BytesDecl, BytesEnd, BytesStart, BytesText, Event,
50        };
51
52        let mut writer = $writer;
53
54        writer.write_event(Event::Decl(BytesDecl::new(
55            "1.0",
56            Some("utf-8"),
57            None,
58        )))?;
59
60        let mut rss_start = BytesStart::new("rss");
61        rss_start.push_attribute(("version", "2.0"));
62        rss_start.push_attribute((
63            "xmlns:atom",
64            "http://www.w3.org/2005/Atom",
65        ));
66        writer.write_event(Event::Start(rss_start))?;
67
68        writer.write_event(Event::Start(BytesStart::new("channel")))?;
69
70        macro_write_element!(writer, "title", &$options.title)?;
71        macro_write_element!(writer, "link", &$options.link)?;
72        macro_write_element!(
73            writer,
74            "description",
75            &$options.description
76        )?;
77        macro_write_element!(writer, "language", &$options.language)?;
78        macro_write_element!(writer, "pubDate", &$options.pub_date)?;
79        macro_write_element!(
80            writer,
81            "lastBuildDate",
82            &$options.last_build_date
83        )?;
84        macro_write_element!(writer, "docs", &$options.docs)?;
85        macro_write_element!(writer, "generator", &$options.generator)?;
86        macro_write_element!(
87            writer,
88            "managingEditor",
89            &$options.managing_editor
90        )?;
91        macro_write_element!(writer, "webMaster", &$options.webmaster)?;
92        macro_write_element!(writer, "category", &$options.category)?;
93        macro_write_element!(writer, "ttl", &$options.ttl)?;
94
95        // Write image element
96        if !$options.image_url.is_empty() {
97            writer
98                .write_event(Event::Start(BytesStart::new("image")))?;
99            macro_write_element!(writer, "url", &$options.image_url)?;
100            macro_write_element!(writer, "title", &$options.title)?;
101            macro_write_element!(writer, "link", &$options.link)?;
102            writer.write_event(Event::End(BytesEnd::new("image")))?;
103        }
104
105        // Write atom:link
106        if !$options.atom_link.is_empty() {
107            let mut atom_link_start = BytesStart::new("atom:link");
108            atom_link_start
109                .push_attribute(("href", $options.atom_link.as_str()));
110            atom_link_start.push_attribute(("rel", "self"));
111            atom_link_start
112                .push_attribute(("type", "application/rss+xml"));
113            writer.write_event(Event::Empty(atom_link_start))?;
114        }
115
116        // Write item
117        writer.write_event(Event::Start(BytesStart::new("item")))?;
118        macro_write_element!(writer, "title", &$options.title)?;
119        macro_write_element!(writer, "link", &$options.link)?;
120        macro_write_element!(
121            writer,
122            "description",
123            &$options.description
124        )?;
125        macro_write_element!(writer, "author", &$options.author)?;
126        macro_write_element!(writer, "guid", &$options.guid)?;
127        macro_write_element!(writer, "pubDate", &$options.pub_date)?;
128        writer.write_event(Event::End(BytesEnd::new("item")))?;
129
130        writer.write_event(Event::End(BytesEnd::new("channel")))?;
131        writer.write_event(Event::End(BytesEnd::new("rss")))?;
132
133        Ok(writer)
134    }};
135}
136
137/// Writes an XML element with the given name and content.
138///
139/// This macro is used internally by the `macro_generate_rss` macro to write individual XML elements.
140///
141/// # Arguments
142///
143/// * `$writer` - The Writer instance to write the XML element.
144/// * `$name` - The name of the XML element.
145/// * `$content` - The content of the XML element.
146///
147/// # Example
148///
149/// ```
150/// use rss_gen::macro_write_element;
151/// use quick_xml::Writer;
152/// use std::io::Cursor;
153/// use quick_xml::events::{BytesStart, BytesEnd, BytesText, Event};
154///
155/// fn _doctest_main_rss_gen_src_macros_rs_153_0() -> Result<(), Box<dyn std::error::Error>> {
156/// let mut writer = Writer::new(Cursor::new(Vec::new()));
157/// macro_write_element!(writer, "title", "My Blog").unwrap();
158///
159/// Ok(())
160/// }
161/// ```
162#[macro_export]
163macro_rules! macro_write_element {
164    ($writer:expr, $name:expr, $content:expr) => {{
165        if !$content.is_empty() {
166            $writer
167                .write_event(Event::Start(BytesStart::new($name)))?;
168            $writer
169                .write_event(Event::Text(BytesText::new($content)))?;
170            $writer.write_event(Event::End(BytesEnd::new($name)))?;
171        }
172        Ok::<(), quick_xml::Error>(())
173    }};
174}
175
176/// Sets fields of the `RssData` struct.
177///
178/// This macro provides a convenient way to set multiple fields of an `RssData` struct in one go.
179///
180/// # Arguments
181///
182/// * `$rss_data` - The `RssData` struct to set fields for.
183/// * `$($field:ident = $value:expr),+` - A comma-separated list of field-value pairs to set.
184///
185/// # Example
186///
187/// ```
188/// use rss_gen::{RssData, macro_set_rss_data_fields};
189///
190/// let mut rss_data = RssData::new(None);
191/// macro_set_rss_data_fields!(rss_data,
192///     Title = "My Blog",
193///     Link = "https://example.com",
194///     Description = "A blog about Rust"
195/// );
196/// assert_eq!(rss_data.title, "My Blog");
197/// assert_eq!(rss_data.link, "https://example.com");
198/// assert_eq!(rss_data.description, "A blog about Rust");
199/// ```
200#[macro_export]
201macro_rules! macro_set_rss_data_fields {
202    ($rss_data:expr, $($field:ident = $value:expr),+ $(,)?) => {
203        $rss_data = $rss_data $(.set($crate::data::RssDataField::$field, $value))+
204    };
205}
206
207/// # `macro_get_args` Macro
208///
209/// Retrieve a named argument from a `clap::ArgMatches` object.
210///
211/// ## Arguments
212///
213/// * `$matches` - A `clap::ArgMatches` object representing the parsed command-line arguments.
214/// * `$name` - A string literal specifying the name of the argument to retrieve.
215///
216/// ## Behaviour
217///
218/// The `macro_get_args` macro retrieves the value of the named argument `$name` from the `$matches` object. If the argument is found and its value can be converted to `String`, the macro returns the value as a `Result<String, String>`. If the argument is not found or its value cannot be converted to `String`, an `Err` variant is returned with an error message indicating the omission of the required parameter.
219///
220/// The error message includes the name of the omitted parameter (`$name`) to assist with troubleshooting and providing meaningful feedback to users.
221///
222/// ## Notes
223///
224/// - This macro assumes the availability of the `clap` crate and the presence of a valid `ArgMatches` object.
225/// - Make sure to adjust the code example by providing a valid `ArgMatches` object and replacing `"arg_name"` with the actual name of the argument you want to retrieve.
226///
227#[macro_export]
228macro_rules! macro_get_args {
229    ($matches:ident, $name:expr) => {
230        $matches.get_one::<String>($name).ok_or(format!(
231            "❌ Error: A required parameter was omitted. Add the required parameter. \"{}\".",
232            $name
233        ))?
234    };
235}
236
237/// # `macro_metadata_option` Macro
238///
239/// Extracts an option value from metadata.
240///
241/// ## Usage
242///
243/// ```rust
244/// use std::collections::HashMap;
245/// use rss_gen::macro_metadata_option;
246///
247/// let mut metadata = HashMap::new();
248/// metadata.insert("key", "value");
249/// let value = macro_metadata_option!(metadata, "key");
250/// println!("{}", value);
251/// ```
252///
253/// ## Arguments
254///
255/// * `$metadata` - A mutable variable that represents the metadata (of type `HashMap<String, String>` or any other type that supports the `get` and `cloned` methods).
256/// * `$key` - A string literal that represents the key to search for in the metadata.
257///
258/// ## Behaviour
259///
260/// The `macro_metadata_option` macro is used to extract an option value from metadata. It takes a mutable variable representing the metadata and a string literal representing the key as arguments, and uses the `get` method of the metadata to find an option value with the specified key.
261///
262/// If the key exists in the metadata, the macro clones the value and returns it. If the key does not exist, it returns the default value for the type of the metadata values.
263///
264/// The macro is typically used in contexts where metadata is stored in a data structure that supports the `get` and `cloned` methods, such as a `HashMap<String, String>`.
265///
266/// # Example
267///
268/// ```
269/// use std::collections::HashMap;
270/// use rss_gen::macro_metadata_option;
271///
272/// let mut metadata = HashMap::new();
273/// metadata.insert("key".to_string(), "value".to_string());
274/// let value = macro_metadata_option!(metadata, "key");
275/// assert_eq!(value, "value");
276/// ```
277///
278#[macro_export]
279macro_rules! macro_metadata_option {
280    ($metadata:ident, $key:expr) => {
281        $metadata.get($key).cloned().unwrap_or_default()
282    };
283}
284
285#[cfg(test)]
286mod tests {
287    use crate::RssData;
288    use quick_xml::Writer;
289    use std::collections::HashMap;
290    use std::io::Cursor;
291
292    #[test]
293    fn test_macro_generate_rss() -> Result<(), quick_xml::Error> {
294        let options = RssData::new(None)
295            .title("Test RSS Feed")
296            .link("https://example.com")
297            .description("A test RSS feed");
298
299        let writer = Writer::new(Cursor::new(Vec::new()));
300        let result: Result<
301            Writer<Cursor<Vec<u8>>>,
302            Box<dyn std::error::Error>,
303        > = macro_generate_rss!(writer, options);
304
305        assert!(result.is_ok());
306        let writer = result.unwrap();
307        let content =
308            String::from_utf8(writer.into_inner().into_inner())
309                .unwrap();
310
311        assert!(content.contains(r#"<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">"#));
312        assert!(content.contains("<title>Test RSS Feed</title>"));
313        assert!(content.contains("<link>https://example.com</link>"));
314        assert!(content
315            .contains("<description>A test RSS feed</description>"));
316
317        Ok(())
318    }
319
320    /// Test generating an RSS feed using valid RSS data.
321    /// Ensures that the macro generates valid XML elements for required fields.
322    #[test]
323    fn test_macro_generate_rss_valid_data(
324    ) -> Result<(), quick_xml::Error> {
325        let options = RssData::new(None)
326            .title("Test RSS Feed")
327            .link("https://example.com")
328            .description("A test RSS feed");
329
330        let writer = Writer::new(Cursor::new(Vec::new()));
331        let result: Result<
332            Writer<Cursor<Vec<u8>>>,
333            Box<dyn std::error::Error>,
334        > = macro_generate_rss!(writer, options);
335
336        assert!(result.is_ok());
337        let writer = result.unwrap();
338        let content =
339            String::from_utf8(writer.into_inner().into_inner())
340                .unwrap();
341
342        assert!(content.contains(r#"<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">"#));
343        assert!(content.contains("<title>Test RSS Feed</title>"));
344        assert!(content.contains("<link>https://example.com</link>"));
345        assert!(content
346            .contains("<description>A test RSS feed</description>"));
347
348        Ok(())
349    }
350
351    /// Test generating an RSS feed with missing optional fields.
352    /// Ensures that the macro still generates valid RSS but excludes the missing fields.
353    #[test]
354    fn test_macro_generate_rss_missing_fields(
355    ) -> Result<(), quick_xml::Error> {
356        let options = RssData::new(None)
357            .title("Test RSS Feed")
358            .link("https://example.com");
359
360        let writer = Writer::new(Cursor::new(Vec::new()));
361        let result: Result<
362            Writer<Cursor<Vec<u8>>>,
363            Box<dyn std::error::Error>,
364        > = macro_generate_rss!(writer, options);
365
366        assert!(result.is_ok());
367        let writer = result.unwrap();
368        let content =
369            String::from_utf8(writer.into_inner().into_inner())
370                .unwrap();
371
372        assert!(content.contains("<title>Test RSS Feed</title>"));
373        assert!(content.contains("<link>https://example.com</link>"));
374        assert!(!content.contains("<description>")); // No description in this case
375
376        Ok(())
377    }
378
379    /// Test setting multiple fields on an `RssData` struct using the macro.
380    /// Ensures that all fields are set correctly and in order.
381    #[test]
382    fn test_macro_set_rss_data_fields() {
383        let mut rss_data = RssData::new(None);
384        macro_set_rss_data_fields!(
385            rss_data,
386            Title = "My Blog",
387            Link = "https://example.com",
388            Description = "A blog about Rust"
389        );
390
391        assert_eq!(rss_data.title, "My Blog");
392        assert_eq!(rss_data.link, "https://example.com");
393        assert_eq!(rss_data.description, "A blog about Rust");
394    }
395
396    /// Test metadata option macro when the key exists.
397    /// Ensures the correct value is returned for a given key.
398    #[test]
399    fn test_macro_metadata_option_existing_key() {
400        let mut metadata = HashMap::new();
401        metadata.insert("author".to_string(), "John Doe".to_string());
402
403        let value = macro_metadata_option!(metadata, "author");
404        assert_eq!(value, "John Doe");
405    }
406
407    /// Test metadata option macro when the key is missing.
408    /// Ensures that it returns an empty string or default value in case the key is not found.
409    #[test]
410    fn test_macro_metadata_option_missing_key() {
411        let mut metadata = HashMap::new();
412        metadata.insert("title".to_string(), "Rust Blog".to_string());
413
414        let value = macro_metadata_option!(metadata, "author"); // Key "author" does not exist
415        assert_eq!(value, ""); // Should return empty string by default
416    }
417
418    /// Test metadata option macro with an empty `HashMap`.
419    /// Ensures it handles an empty metadata collection gracefully.
420    #[test]
421    fn test_macro_metadata_option_empty_metadata() {
422        let metadata: HashMap<String, String> = HashMap::new();
423
424        let value = macro_metadata_option!(metadata, "nonexistent_key");
425        assert_eq!(value, "");
426    }
427}