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}