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::alloc_type::Box::new($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}
502
503/// Asserts that a result is an error and contains a specific tag or message.
504///
505/// This macro iterates through the error's core message and all attached contexts
506/// (both simple and grouped) to find a match with the expected string.
507///
508/// # Arguments
509///
510/// * `$result` - A `Result` or `ComposableResult` to check.
511/// * `$expected` - The string to search for in tags, messages, or the core error.
512///
513/// # Panics
514///
515/// Panics if the result is `Ok` or if the error does not contain the expected string.
516#[macro_export]
517macro_rules! assert_err_eq {
518 ($result:expr, $expected:expr) => {
519 match &$result {
520 Ok(v) => panic!("Expected Err, but got Ok({:?})", v),
521 Err(e) => {
522 use $crate::types::ErrorContext;
523 let expected = $expected;
524 let mut found = false;
525
526 // Check core error message
527 if e.core_error().to_string() == expected {
528 found = true;
529 }
530
531 // Check contexts
532 if !found {
533 for ctx in e.context_iter() {
534 match ctx {
535 ErrorContext::Simple(s) => {
536 if s.as_ref() == expected {
537 found = true;
538 break;
539 }
540 }
541 ErrorContext::Group(g) => {
542 if g.message.as_deref().is_some_and(|m| m == expected) {
543 found = true;
544 break;
545 }
546 if g.tags.iter().any(|t| t.as_ref() == expected) {
547 found = true;
548 break;
549 }
550 if g.metadata.iter().any(|(_, v)| v.as_ref() == expected) {
551 found = true;
552 break;
553 }
554 }
555 }
556 }
557 }
558
559 if !found {
560 panic!(
561 "Assertion failed: error does not contain tag or message '{}'.\nActual error: {}",
562 expected, e
563 );
564 }
565 }
566 }
567 };
568}