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}