zirv_macros/
lib.rs

1//! # zirv-macros
2//!
3//! The `zirv-macros` crate provides a collection of custom macros designed to ease backend
4//! development in Rust, especially for projects using Actix and SQLx. The macros help reduce boilerplate,
5//! improve logging, enhance error handling, and aid with instrumentation and performance measurement.
6//!
7//! ## Features
8//!
9//! - **Error Handling & Assertions:**
10//!   - `try_log!`: Evaluates an expression returning a `Result`, logs on error, and returns an error.
11//!   - `unwrap_or_log!`: Unwraps a result and uses a default value if it fails, logging the error.
12//!   - `assert_msg!`: Asserts a condition with a custom error message.
13//!
14//! - **Timing & Instrumentation:**
15//!   - `time_it!`: Measures and logs the execution time of a code block.
16//!   - `log_duration!`: Logs the duration of a code block using tracing.
17//!   - `span_wrap!`: Wraps a block of code inside a tracing span.
18//!   - `call_with_trace!`: Calls a function inside a tracing span.
19//!
20//! - **JSON & Environment Helpers:**
21//!   - `json_merge!`: Merges two JSON objects.
22//!   - `parse_env!`: Reads an environment variable with a default fallback.
23//!   - `pretty_debug!`: Pretty-prints a JSON representation of an object.
24//!
25//! - **SQL Debugging:**
26//!   - `debug_query!`: Logs the full SQL query string before executing it.
27//!
28//! - **Retry Utilities:**
29//!   - `with_retry!`: Synchronously retries an expression a fixed number of times.
30//!   - `retry_async!`: Asynchronously retries an expression a fixed number of times.
31//!
32//! ## Usage
33//!
34//! Add `zirv-macros` as a dependency in your Cargo.toml and import the macros:
35//!
36//! ```toml
37//! [dependencies]
38//! zirv-macros = { path = "../zirv-macros" }
39//! ```
40//!
41//! ```rust
42//! use zirv_macros::*;
43//! ```
44//!
45//! See the examples below for details.
46
47/// Attempts to evaluate an expression returning a `Result`.
48/// If the result is `Ok`, returns the value.
49/// Otherwise, logs an error with file and line info and returns an error as a `String`.
50///
51/// # Examples
52///
53/// ```rust
54/// # use std::error::Error;
55/// # use zirv_macros::*;
56/// fn main() -> Result<(), String> {
57///     let value = try_log!(Ok::<u32, Box<dyn Error>>(42));
58///     assert_eq!(value, 42);
59///     Ok(())
60/// }
61/// ```
62#[macro_export]
63macro_rules! try_log {
64    ($expr:expr) => {
65        match $expr {
66            Ok(val) => val,
67            Err(err) => {
68                eprintln!("Error at {}:{} - {:?}", file!(), line!(), err);
69                return Err(err.to_string());
70            }
71        }
72    };
73}
74
75/// Attempts to unwrap a result, returning a default value if an error occurs.
76/// Logs an error with file and line info if the unwrap fails.
77///
78/// # Examples
79///
80/// ```rust
81/// # use zirv_macros::*;
82/// let value = unwrap_or_log!(Ok::<String, &str>("value".to_string()), "default".to_string());
83/// assert_eq!(value, "value".to_string());
84/// ```
85#[macro_export]
86macro_rules! unwrap_or_log {
87    ($expr:expr, $default:expr) => {
88        match $expr {
89            Ok(val) => val,
90            Err(err) => {
91                eprintln!(
92                    "Unwrap failed at {}:{} - {:?}. Using default: {:?}",
93                    file!(),
94                    line!(),
95                    err,
96                    $default
97                );
98                $default
99            }
100        }
101    };
102}
103
104/// Measures the execution time of a block of code and prints the duration with the provided label.
105///
106/// # Examples
107///
108/// ```rust
109/// # use zirv_macros::*;
110/// let result = time_it!("Computation", { 42 });
111/// assert_eq!(result, 42);
112/// ```
113#[macro_export]
114macro_rules! time_it {
115    ($label:expr, $block:block) => {{
116        let start = std::time::Instant::now();
117        let result = { $block };
118        let duration = start.elapsed();
119        println!("{} took {:?}", $label, duration);
120        result
121    }};
122}
123
124/// Merges two `serde_json::Value` objects (expected to be JSON objects).
125/// Keys in the second object override those in the first.
126///
127/// # Examples
128///
129/// ```rust
130/// # use zirv_macros::*;
131/// use serde_json::json;
132/// let a = json!({ "a": 1, "b": 2 });
133/// let b = json!({ "b": 3, "c": 4 });
134/// let merged = json_merge!(a, b);
135/// assert_eq!(merged["a"], 1);
136/// assert_eq!(merged["b"], 3);
137/// assert_eq!(merged["c"], 4);
138/// ```
139#[macro_export]
140macro_rules! json_merge {
141    ($base:expr, $other:expr) => {{
142        let mut base = $base;
143        if let (Some(base_obj), Some(other_obj)) = (base.as_object_mut(), $other.as_object()) {
144            for (k, v) in other_obj {
145                base_obj.insert(k.clone(), v.clone());
146            }
147        }
148        base
149    }};
150}
151
152/// Logs the SQL query string (and optionally its bind parameters) before executing it.
153/// Useful for debugging SQLx queries.
154///
155/// # Examples
156///
157/// ```rust
158/// # use zirv_macros::*;
159/// // Dummy struct to simulate a query with a sql() method.
160/// struct DummyQuery { sql: &'static str }
161/// impl DummyQuery {
162///     fn sql(&self) -> &str { self.sql }
163/// }
164/// let query = DummyQuery { sql: "SELECT * FROM users" };
165/// let _ = debug_query!(query);
166/// ```
167#[macro_export]
168macro_rules! debug_query {
169    ($query:expr) => {{
170        let sql = $query.sql();
171        println!("SQL Query: {}", sql);
172        $query
173    }};
174}
175
176/// Retries a synchronous expression (returning a `Result`) a specified number of times,
177/// waiting a fixed number of milliseconds between attempts.
178///
179/// # Examples
180///
181/// ```rust
182/// # use zirv_macros::*;
183/// fn dummy_op() -> Result<u32, &'static str> { Ok(42) }
184/// let result = with_retry!(3, 10, dummy_op());
185/// assert_eq!(result.unwrap(), 42);
186/// ```
187#[macro_export]
188macro_rules! with_retry {
189    ($retries:expr, $delay_ms:expr, $expr:expr) => {{
190        let mut attempts = 0;
191        loop {
192            match $expr {
193                Ok(val) => break Ok(val),
194                Err(err) => {
195                    attempts += 1;
196                    if attempts >= $retries {
197                        break Err(err);
198                    }
199                    std::thread::sleep(std::time::Duration::from_millis($delay_ms));
200                }
201            }
202        }
203    }};
204}
205
206/// Retries an asynchronous expression (returning a `Result`) a specified number of times,
207/// waiting a fixed number of milliseconds between attempts.
208/// Uses `tokio::time::sleep`.
209///
210/// # Examples
211///
212/// ```rust
213/// # use zirv_macros::*;
214/// # async fn dummy_async_op() -> Result<u32, &'static str> { Ok(42) }
215/// # #[tokio::main]
216/// # async fn main() {
217/// let result = retry_async!(3, 10, dummy_async_op());
218/// assert_eq!(result.unwrap(), 42);
219/// # }
220/// ```
221#[macro_export]
222macro_rules! retry_async {
223    ($retries:expr, $delay_ms:expr, $async_expr:expr) => {{
224        use std::time::Duration;
225        let mut attempts = 0;
226        loop {
227            match $async_expr.await {
228                Ok(val) => break Ok(val),
229                Err(err) => {
230                    attempts += 1;
231                    if attempts >= $retries {
232                        break Err(err);
233                    }
234                    tokio::time::sleep(Duration::from_millis($delay_ms)).await;
235                }
236            }
237        }
238    }};
239}
240
241/// Wraps a block of code in a tracing span with the given name, enabling automatic instrumentation.
242///
243/// # Examples
244///
245/// ```rust
246/// # use zirv_macros::*;
247/// span_wrap!("my_span", {
248///     println!("Inside span");
249/// });
250/// ```
251#[macro_export]
252macro_rules! span_wrap {
253    ($span_name:expr, $block:block) => {{
254        let span = tracing::span!(tracing::Level::INFO, $span_name);
255        let _enter = span.enter();
256        $block
257    }};
258}
259
260/// Logs the duration of a code block using tracing.
261/// Executes the block, logs the elapsed time with the provided label, and returns the result.
262///
263/// # Examples
264///
265/// ```rust
266/// # use zirv_macros::*;
267/// let result = log_duration!("test", { 42 });
268/// assert_eq!(result, 42);
269/// ```
270#[macro_export]
271macro_rules! log_duration {
272    ($label:expr, $block:block) => {{
273        let start = std::time::Instant::now();
274        let result = { $block };
275        let elapsed = start.elapsed();
276        tracing::info!("{} took {:?}", $label, elapsed);
277        result
278    }};
279}
280
281/// Calls a function with the provided arguments, wrapping the call in a tracing span with the specified name.
282///
283/// # Examples
284///
285/// ```rust
286/// # use zirv_macros::*;
287/// fn add(a: i32, b: i32) -> i32 { a + b }
288/// let result = call_with_trace!("processing", add, 2, 3);
289/// assert_eq!(result, 5);
290/// ```
291#[macro_export]
292macro_rules! call_with_trace {
293    ($span_name:expr, $func:expr $(, $args:expr)*) => {{
294        let span = tracing::span!(tracing::Level::INFO, $span_name);
295        let _enter = span.enter();
296        $func($($args),*)
297    }};
298}
299
300/// Asserts a condition and logs an error with a custom message if it fails, then panics.
301///
302/// # Examples
303///
304/// ```rust,should_panic
305/// # use zirv_macros::*;
306/// let value = 0;
307/// assert_msg!(value > 0, "Value must be positive");
308/// ```
309#[macro_export]
310macro_rules! assert_msg {
311    ($cond:expr, $msg:expr) => {
312        if !$cond {
313            tracing::error!("Assertion failed: {}", $msg);
314            panic!($msg);
315        }
316    };
317}
318
319/// Attempts to evaluate an expression returning a `Result` and logs an error if it fails,
320/// returning a default value instead.
321///
322/// # Examples
323///
324/// ```rust
325/// # use zirv_macros::*;
326/// fn fail_op() -> Result<u32, &'static str> { Err("failure") }
327/// let value = log_error!(fail_op(), 0);
328/// assert_eq!(value, 0);
329/// ```
330#[macro_export]
331macro_rules! log_error {
332    ($expr:expr, $default:expr) => {{
333        match $expr {
334            Ok(val) => val,
335            Err(err) => {
336                tracing::error!("Error: {:?}", err);
337                $default
338            }
339        }
340    }};
341}
342
343/// Attempts to read an environment variable. If the variable is not set,
344/// logs a warning and returns a default value as a String.
345///
346/// # Examples
347///
348/// ```rust
349/// # use zirv_macros::*;
350/// unsafe {
351/// std::env::remove_var("TEST_VAR");
352/// }
353/// let value = parse_env!("TEST_VAR", "default");
354/// assert_eq!(value, "default".to_string());
355/// ```
356#[macro_export]
357macro_rules! parse_env {
358    ($var:expr, $default:expr) => {{
359        std::env::var($var).unwrap_or_else(|_| {
360            tracing::warn!(
361                "Environment variable {} not set. Using default: {:?}",
362                $var,
363                $default
364            );
365            $default.to_string()
366        })
367    }};
368}
369
370/// Prints a pretty-printed JSON representation of an object that implements Serialize.
371///
372/// # Examples
373///
374/// ```rust
375/// # use zirv_macros::*;
376/// let data = serde_json::json!({ "a": 1, "b": 2 });
377/// pretty_debug!(data);
378/// ```
379#[macro_export]
380macro_rules! pretty_debug {
381    ($obj:expr) => {
382        println!("{}", serde_json::to_string_pretty(&$obj).unwrap())
383    };
384}
385
386#[cfg(test)]
387mod tests {
388    use super::*;
389    use serde_json::json;
390    use std::env;
391    use std::error::Error;
392    use std::sync::atomic::{AtomicUsize, Ordering};
393    use std::time::Duration;
394
395    // Test try_log! with a successful result.
396    #[test]
397    fn test_try_log_ok() {
398        fn test_fn() -> Result<i32, String> {
399            let x = try_log!(Ok::<_, Box<dyn Error>>(10));
400            Ok(x)
401        }
402        assert_eq!(test_fn().unwrap(), 10);
403    }
404
405    // Test try_log! when an error occurs. It should return early.
406    #[test]
407    fn test_try_log_err() {
408        fn test_fn() -> Result<i32, String> {
409            // This will trigger the error branch in try_log!.
410            let _x = try_log!(Err("error".to_string()));
411            // This line should never be reached.
412            Ok(42)
413        }
414        let res = test_fn();
415        assert!(res.is_err());
416        assert_eq!(res.unwrap_err(), "error".to_string());
417    }
418
419    // Test unwrap_or_log! macro.
420    #[test]
421    fn test_unwrap_or_log() {
422        let ok_val: Result<&str, &str> = Ok("hello");
423        let err_val: Result<&str, &str> = Err("fail");
424        let v1 = unwrap_or_log!(ok_val, "default");
425        assert_eq!(v1, "hello");
426        let v2 = unwrap_or_log!(err_val, "default");
427        assert_eq!(v2, "default");
428    }
429
430    // Test time_it! macro.
431    #[test]
432    fn test_time_it() {
433        let result = time_it!("sleep test", {
434            std::thread::sleep(Duration::from_millis(50));
435            5
436        });
437        assert_eq!(result, 5);
438    }
439
440    // Test json_merge! macro.
441    #[test]
442    fn test_json_merge() {
443        let base = json!({"a": 1, "b": 2});
444        let other = json!({"b": 3, "c": 4});
445        let merged = json_merge!(base, other);
446        assert_eq!(merged["a"], 1);
447        assert_eq!(merged["b"], 3);
448        assert_eq!(merged["c"], 4);
449    }
450
451    // For debug_query!, create a dummy type with a .sql() method.
452    struct DummyQuery {
453        sql: String,
454    }
455    impl DummyQuery {
456        fn new(sql: &str) -> Self {
457            DummyQuery {
458                sql: sql.to_string(),
459            }
460        }
461        fn sql(&self) -> &str {
462            &self.sql
463        }
464    }
465    #[test]
466    fn test_debug_query() {
467        let query = DummyQuery::new("SELECT 1");
468        let _ = debug_query!(query);
469        // The macro prints the SQL; we simply ensure it does not panic.
470    }
471
472    // Test with_retry! macro.
473    #[test]
474    fn test_with_retry_success() {
475        static ATTEMPTS: AtomicUsize = AtomicUsize::new(0);
476        let res = with_retry!(3, 10, {
477            let current = ATTEMPTS.fetch_add(1, Ordering::SeqCst);
478            if current < 2 {
479                Err("fail")
480            } else {
481                Ok("success")
482            }
483        });
484        assert_eq!(res.unwrap(), "success");
485    }
486
487    #[test]
488    fn test_with_retry_failure() {
489        let res: Result<&str, &str> = with_retry!(2, 10, { Err("always fails") });
490        assert!(res.is_err());
491    }
492
493    // Test retry_async! macro.
494    #[tokio::test]
495    async fn test_retry_async_success() {
496        use std::sync::Arc;
497        use tokio::sync::Mutex;
498        let attempts = Arc::new(Mutex::new(0));
499        let res = retry_async!(3, 10, {
500            let attempts = attempts.clone();
501            async move {
502                let mut att = attempts.lock().await;
503                if *att < 2 {
504                    *att += 1;
505                    Err("fail")
506                } else {
507                    Ok("success")
508                }
509            }
510        });
511        assert_eq!(res.unwrap(), "success");
512    }
513
514    #[tokio::test]
515    async fn test_retry_async_failure() {
516        let res: Result<&str, &str> = retry_async!(2, 10, async { Err("fail") });
517        assert!(res.is_err());
518    }
519
520    // Test span_wrap! macro.
521    #[test]
522    fn test_span_wrap() {
523        let value = span_wrap!("test_span", { 123 });
524        assert_eq!(value, 123);
525    }
526
527    // Test log_duration! macro.
528    #[test]
529    fn test_log_duration() {
530        let value = log_duration!("duration test", { 456 });
531        assert_eq!(value, 456);
532    }
533
534    // Test call_with_trace! macro.
535    #[test]
536    fn test_call_with_trace() {
537        fn add(a: i32, b: i32) -> i32 {
538            a + b
539        }
540        let result = call_with_trace!("add", add, 3, 4);
541        assert_eq!(result, 7);
542    }
543
544    // Test assert_msg! macro. This test expects a panic.
545    #[test]
546    #[should_panic(expected = "Assertion failed: test failure")]
547    fn test_assert_msg() {
548        assert_msg!(false, "Assertion failed: test failure");
549    }
550
551    // Test log_error! macro.
552    #[test]
553    fn test_log_error() {
554        let ok_val: Result<&str, &str> = Ok("ok");
555        let err_val: Result<&str, &str> = Err("error");
556        let v1 = log_error!(ok_val, "default");
557        assert_eq!(v1, "ok");
558        let v2 = log_error!(err_val, "default");
559        assert_eq!(v2, "default");
560    }
561
562    // Test parse_env! macro.
563    #[test]
564    fn test_parse_env() {
565        // Set an environment variable temporarily.
566        unsafe {
567            env::set_var("TEST_VAR", "value1");
568        }
569        let result = parse_env!("TEST_VAR", "default");
570        assert_eq!(result, "value1".to_string());
571        unsafe {
572            env::remove_var("TEST_VAR");
573        }
574
575        // Now TEST_VAR is not set, so we get the default.
576        let result = parse_env!("TEST_VAR", "default");
577        assert_eq!(result, "default".to_string());
578    }
579
580    // Test pretty_debug! macro.
581    #[test]
582    fn test_pretty_debug() {
583        let obj = json!({"x": 1, "y": 2});
584        // Call the macro to ensure it doesn't panic.
585        pretty_debug!(obj);
586    }
587}