error_rail/macros/
mod.rs

1//! Ergonomic macros for creating lazy or structured [`ErrorContext`](crate::types::ErrorContext).
2//!
3//! These macros provide convenient shortcuts for attaching rich metadata to errors:
4//!
5//! - [`macro@crate::rail`] - Wraps a `Result`-producing block and converts it into a
6//!   [`BoxedComposableResult`](crate::types::BoxedComposableResult) via `ErrorPipeline::finish_boxed`.
7//!   **Always returns boxed errors.**
8//! - [`macro@crate::rail_unboxed`] - Wraps a `Result`-producing block and converts it into an
9//!   unboxed [`ComposableResult`](crate::types::ComposableResult) via `ErrorPipeline::finish`.
10//! - [`macro@crate::context`] - Defers formatting until the context is consumed, avoiding
11//!   unnecessary allocations on the success path.
12//! - [`macro@crate::group`] - Creates a lazily-evaluated grouped context that combines
13//!   multiple fields (message, tags, location, metadata) into one cohesive unit while deferring
14//!   all formatting until the error occurs.
15//!
16//! # Examples
17//!
18//! ```
19//! use error_rail::{context, rail, rail_unboxed, group, ErrorPipeline};
20//!
21//! let result: Result<(), &str> = Err("failed");
22//! let pipeline = ErrorPipeline::new(result)
23//!     .with_context(context!("user_id: {}", 123))
24//!     .with_context(group!(
25//!         tag("auth"),
26//!         location(file!(), line!()),
27//!         metadata("retry_count", "3")
28//!     ))
29//!     .finish_boxed();
30//!
31//! assert!(pipeline.is_err());
32//!
33//! // rail! - ALWAYS returns boxed error (8 bytes stack size)
34//! let boxed_result = rail!({
35//!     Err::<(), &str>("failed")
36//! });
37//!
38//! // rail_unboxed! - returns unboxed error (larger stack size)
39//! let unboxed_result = rail_unboxed!({
40//!     Err::<(), &str>("failed")
41//! });
42//! ```
43//!
44//! ## Choosing Between rail! and rail_unboxed!
45//!
46//! - **Use `rail!`** for public APIs and most cases - smaller stack footprint (8 bytes)
47//! - **Use `rail_unboxed!`** for internal code or performance-critical paths where you want to avoid heap allocation
48
49/// Wraps a `Result`-producing expression or block and converts it into a
50/// [`BoxedComposableResult`](crate::types::BoxedComposableResult).
51///
52/// **⚠️ IMPORTANT: This macro ALWAYS returns a boxed composable result.**
53/// The error type is wrapped in a `Box<ComposableError<E>>` to reduce stack size.
54/// If you need an unboxed result, use [`rail_unboxed!`](crate::rail_unboxed) instead.
55///
56/// This macro provides a convenient shorthand for creating an [`ErrorPipeline`](crate::ErrorPipeline)
57/// and immediately calling `finish_boxed()` to box the result. It accepts either a single expression
58/// or a block of code that produces a `Result`.
59///
60/// # Syntax
61///
62/// - `rail!(expr)` - Wraps a single `Result`-producing expression
63/// - `rail!({ ... })` - Wraps a block that produces a `Result`
64///
65/// # Returns
66///
67/// A [`BoxedComposableResult<T, E>`](crate::types::BoxedComposableResult) where the error type
68/// is wrapped in a [`ComposableError`](crate::types::ComposableError) and boxed.
69///
70/// # Examples
71///
72/// ```rust
73/// use error_rail::{rail, group};
74///
75/// // Simple expression - ALWAYS returns boxed error
76/// let result = rail!(Err::<(), &str>("failed"));
77/// assert!(result.is_err());
78/// // Error type is Box<ComposableError<&str>>
79/// let _: Box<error_rail::ComposableError<&str>> = result.unwrap_err();
80///
81/// // Block syntax with multiple statements
82/// let result = rail!({
83///     let value = std::fs::read_to_string("config.txt");
84///     value
85/// });
86///
87/// // Using with group! macro for structured context
88/// let result = rail!({
89///     std::fs::read_to_string("config.txt")
90/// })
91/// .map_err(|e| e.with_context(group!(
92///     tag("config"),
93///     location(file!(), line!()),
94///     metadata("file", "config.txt")
95/// )));
96/// ```
97#[macro_export]
98macro_rules! rail {
99    ($expr:expr $(,)?) => {
100        $crate::ErrorPipeline::new($expr).finish_boxed()
101    };
102}
103
104/// Wraps a `Result`-producing expression or block and converts it into an
105/// unboxed [`ComposableResult`](crate::types::ComposableResult).
106///
107/// This macro is similar to [`rail!`](crate::rail) but returns an unboxed error.
108/// Use this when you need to avoid heap allocation or when working with APIs
109/// that expect the unboxed `ComposableError<E>` type.
110///
111/// # Syntax
112///
113/// - `rail_unboxed!(expr)` - Wraps a single `Result`-producing expression
114/// - `rail_unboxed!({ ... })` - Wraps a block that produces a `Result`
115///
116/// # Returns
117///
118/// A [`ComposableResult<T, E>`](crate::types::ComposableResult) where the error type
119/// is wrapped in a [`ComposableError`](crate::types::ComposableError) but not boxed.
120///
121/// # Examples
122///
123/// ```rust
124/// use error_rail::{rail_unboxed, group};
125///
126/// // Simple expression - returns unboxed error
127/// let result = rail_unboxed!(Err::<(), &str>("failed"));
128/// assert!(result.is_err());
129/// // Error type is ComposableError<&str> (not boxed)
130/// let _: error_rail::ComposableError<&str> = result.unwrap_err();
131///
132/// // Block syntax with multiple statements
133/// let result = rail_unboxed!({
134///     let value = std::fs::read_to_string("config.txt");
135///     value
136/// });
137/// ```
138#[macro_export]
139macro_rules! rail_unboxed {
140    ($expr:expr $(,)?) => {
141        $crate::ErrorPipeline::new($expr).finish()
142    };
143}
144
145/// Creates a lazily-evaluated error context that defers string formatting.
146///
147/// This macro wraps the provided format string and arguments in a [`LazyContext`](crate::types::LazyContext),
148/// which only evaluates the closure when the error actually occurs. This avoids the performance
149/// overhead of string formatting on the success path.
150///
151/// # Arguments
152///
153/// Accepts the same arguments as the standard `format!` macro.
154///
155/// # Examples
156///
157/// ```
158/// use error_rail::{context, ComposableError};
159///
160/// let user_id = 42;
161/// let err = ComposableError::<&str>::new("auth failed")
162///     .with_context(context!("user_id: {}", user_id));
163/// ```
164#[macro_export]
165macro_rules! context {
166    ($($arg:tt)*) => {
167        $crate::types::LazyContext::new(move || format!($($arg)*))
168    };
169}
170
171/// Implements `IntoErrorContext` for a custom type.
172///
173/// This macro simplifies the implementation of the [`IntoErrorContext`](crate::traits::IntoErrorContext)
174/// trait for user-defined types. It converts the type into an [`ErrorContext`](crate::types::ErrorContext)
175/// using its `Display` implementation.
176///
177/// # Arguments
178///
179/// * `$type` - The type to implement `IntoErrorContext` for.
180///
181/// # Examples
182///
183/// ```
184/// use error_rail::{impl_error_context, ErrorContext, traits::IntoErrorContext};
185/// use std::fmt;
186///
187/// struct MyError {
188///     code: u32,
189/// }
190///
191/// impl fmt::Display for MyError {
192///     fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
193///         write!(f, "Error code: {}", self.code)
194///     }
195/// }
196///
197/// impl_error_context!(MyError);
198///
199/// let err = MyError { code: 404 };
200/// let ctx = err.into_error_context();
201/// assert_eq!(ctx.to_string(), "Error code: 404");
202/// ```
203#[macro_export]
204macro_rules! impl_error_context {
205    ($type:ty) => {
206        impl $crate::traits::IntoErrorContext for $type {
207            fn into_error_context(self) -> $crate::types::ErrorContext {
208                $crate::types::ErrorContext::new(self.to_string())
209            }
210        }
211    };
212}
213
214/// Creates a grouped error context that combines multiple context types.
215///
216/// This macro creates a lazily-evaluated grouped context that combines message,
217/// tags, location, and metadata into a single cohesive unit. All formatting is
218/// deferred until the error actually occurs, avoiding unnecessary allocations
219/// on the success path.
220///
221/// # Arguments
222///
223/// The macro accepts function-call style arguments:
224/// * `message("format string", args...)` - Optional formatted message
225/// * `tag("label")` - Categorical tags (can be repeated)
226/// * `location(file, line)` - Source file and line number
227/// * `metadata("key", "value")` - Key-value pairs (can be repeated)
228///
229/// # Examples
230///
231/// ```
232/// use error_rail::{group, ComposableError};
233///
234/// let attempts = 3;
235/// let err = ComposableError::<&str>::new("auth failed")
236///     .with_context(group!(
237///         message("user_id: {}", attempts),
238///         tag("auth"),
239///         location(file!(), line!()),
240///         metadata("retry_count", "3"),
241///         metadata("timeout", "30s")
242///     ));
243/// ```
244#[macro_export]
245macro_rules! group {
246    // Empty group
247    () => {
248        $crate::types::LazyGroupContext::new(move || {
249            $crate::types::ErrorContext::Group($crate::types::GroupContext::default())
250        })
251    };
252
253    // With fields - use function-call style
254    (
255        $($field:ident($($arg:tt)*)),* $(,)?
256    ) => {
257        $crate::types::LazyGroupContext::new(move || {
258            let mut builder = $crate::types::ErrorContext::builder();
259            $(
260                $crate::__group_field!(builder, $field, $($arg)*);
261            )*
262            builder.build()
263        })
264    };
265}
266
267/// Internal macro for processing individual group fields
268#[macro_export]
269#[doc(hidden)]
270macro_rules! __group_field {
271    // Message field
272    ($builder:expr, message, $($arg:tt)*) => {
273        $builder = $builder.message(format!($($arg)*));
274    };
275
276    // Tag field
277    ($builder:expr, tag, $tag:expr) => {
278        $builder = $builder.tag($tag);
279    };
280
281    // Location field
282    ($builder:expr, location, $file:expr, $line:expr) => {
283        $builder = $builder.location($file, $line);
284    };
285
286    // Metadata field
287    ($builder:expr, metadata, $key:expr, $value:expr) => {
288        $builder = $builder.metadata($key, $value);
289    };
290}
291
292/// Captures the current backtrace as lazy error context.
293///
294/// This macro creates a [`LazyContext`](crate::types::LazyContext) that captures the stack
295/// backtrace only when the error actually occurs, avoiding the performance overhead of
296/// backtrace generation on the success path.
297///
298/// The backtrace is captured using [`std::backtrace::Backtrace::capture()`] and converted
299/// to a string representation when the context is evaluated.
300///
301/// # Examples
302///
303/// ```
304/// use error_rail::{ComposableError, backtrace};
305///
306/// let err = ComposableError::<&str>::new("panic occurred")
307///     .with_context(backtrace!());
308/// ```
309#[macro_export]
310#[cfg(feature = "std")]
311macro_rules! backtrace {
312    () => {{
313        $crate::types::LazyContext::new(|| std::backtrace::Backtrace::capture().to_string())
314    }};
315}
316
317/// Creates a backtrace context that always captures regardless of environment.
318///
319/// This macro uses `force_capture()` to always generate a backtrace, ignoring
320/// `RUST_BACKTRACE`/`RUST_LIB_BACKTRACE` settings. Use this for debugging
321/// scenarios where you need guaranteed backtrace information.
322///
323/// # Performance Note
324///
325/// This has higher overhead than `backtrace!()` since it always captures
326/// stack frames, regardless of environment settings.
327///
328/// # Examples
329///
330/// ```
331/// use error_rail::{ComposableError, backtrace_force};
332///
333/// let err = ComposableError::<&str>::new("panic occurred")
334///     .with_context(backtrace_force!());
335/// ```
336#[macro_export]
337#[cfg(feature = "std")]
338macro_rules! backtrace_force {
339    () => {{
340        $crate::types::LazyContext::new(|| std::backtrace::Backtrace::force_capture().to_string())
341    }};
342}
343
344/// Combines multiple `Validation` results into a single one.
345///
346/// This macro allows you to validate multiple fields in parallel and accumulate
347/// all errors if any occur. If all validations succeed, it returns a tuple
348/// containing the successful values.
349///
350/// # Syntax
351///
352/// ```rust,ignore
353/// validate!(
354///     field1 = validation_expr1,
355///     field2 = validation_expr2,
356///     ...
357/// )
358/// ```
359///
360/// # Returns
361///
362/// `Validation<E, (T1, T2, ...)>` where `T1`, `T2` are the success types of the expressions.
363///
364/// # Examples
365///
366/// ```
367/// use error_rail::{validate, validation::Validation};
368///
369/// let v1 = Validation::<&str, i32>::valid(1);
370/// let v2 = Validation::<&str, i32>::valid(2);
371///
372/// let result = validate!(
373///     a = v1,
374///     b = v2
375/// );
376///
377/// assert!(result.is_valid());
378/// assert_eq!(result.into_value(), Some((1, 2)));
379///
380/// let e1 = Validation::<&str, i32>::invalid("error1");
381/// let e2 = Validation::<&str, i32>::invalid("error2");
382///
383/// let result = validate!(
384///     a = e1,
385///     b = e2
386/// );
387///
388/// assert!(result.is_invalid());
389/// assert_eq!(result.into_errors().unwrap().len(), 2);
390/// ```
391#[macro_export]
392macro_rules! validate {
393    ($($key:ident = $val:expr),+ $(,)?) => {{
394        match ($($val),+) {
395            ( $( $crate::validation::Validation::Valid($key) ),+ ) => {
396                $crate::validation::Validation::Valid( ($($key),+) )
397            }
398            ( $( ref $key ),+ ) => {
399                let mut errors = $crate::ErrorVec::new();
400                $(
401                    if let $crate::validation::Validation::Invalid(e) = $key {
402                        errors.extend(e.iter().cloned());
403                    }
404                )+
405                $crate::validation::Validation::Invalid(errors.into())
406            }
407        }
408    }};
409}
410
411/// Wraps a future in an [`AsyncErrorPipeline`](crate::async_ext::AsyncErrorPipeline).
412///
413/// This macro provides a convenient way to create an async error pipeline
414/// from a future that returns a `Result`.
415///
416/// # Examples
417///
418/// ```rust,no_run
419/// use error_rail::prelude_async::*;
420///
421/// #[derive(Debug)]
422/// struct Data;
423///
424/// #[derive(Debug)]
425/// struct ApiError;
426///
427/// async fn fetch_data(_id: u64) -> Result<Data, ApiError> {
428///     Err(ApiError)
429/// }
430///
431/// async fn example(id: u64) -> BoxedResult<Data, ApiError> {
432///     rail_async!(fetch_data(id))
433///         .with_context("fetching data")
434///         .finish_boxed()
435///         .await
436/// }
437/// ```
438#[macro_export]
439#[cfg(feature = "async")]
440macro_rules! rail_async {
441    ($fut:expr $(,)?) => {
442        $crate::async_ext::AsyncErrorPipeline::new($fut)
443    };
444}
445
446/// Attaches context to a future's error with format string support.
447///
448/// This macro provides a convenient shorthand for attaching context to
449/// async operations, similar to how `context!` works for sync code.
450///
451/// # Syntax
452///
453/// - `ctx_async!(future, "literal message")` - Static message
454/// - `ctx_async!(future, "format {}", arg)` - Formatted message (lazy)
455///
456/// # Examples
457///
458/// ```rust,no_run
459/// use error_rail::prelude_async::*;
460///
461/// #[derive(Debug)]
462/// struct User;
463///
464/// #[derive(Debug)]
465/// struct Profile;
466///
467/// #[derive(Debug)]
468/// struct ApiError;
469///
470/// async fn fetch_user(_id: u64) -> Result<User, ApiError> {
471///     Err(ApiError)
472/// }
473///
474/// async fn fetch_profile(_id: u64) -> Result<Profile, ApiError> {
475///     Err(ApiError)
476/// }
477///
478/// async fn example(id: u64) -> BoxedResult<User, ApiError> {
479///     // Static message
480///     let user = ctx_async!(fetch_user(id), "fetching user")
481///         .await
482///         .map_err(Box::new)?;
483///
484///     // With formatting (lazy evaluation)
485///     let _profile = ctx_async!(fetch_profile(id), "fetching profile for user {}", id)
486///         .await
487///         .map_err(Box::new)?;
488///
489///     Ok(user)
490/// }
491/// ```
492#[macro_export]
493#[cfg(feature = "async")]
494macro_rules! ctx_async {
495    ($fut:expr, $msg:literal $(,)?) => {
496        $crate::async_ext::FutureResultExt::ctx($fut, $msg)
497    };
498    ($fut:expr, $fmt:literal, $($arg:tt)* $(,)?) => {
499        $crate::async_ext::FutureResultExt::with_ctx($fut, || format!($fmt, $($arg)*))
500    };
501}