error_rail/
tower.rs

1//! Tower integration for error-rail.
2//!
3//! This module provides Tower `Layer` and `Service` implementations
4//! that automatically attach error context to service errors.
5//!
6//! # Feature Flag
7//!
8//! Requires the `tower` feature:
9//!
10//! ```toml
11//! [dependencies]
12//! error-rail = { version = "0.8", features = ["tower"] }
13//! ```
14//!
15//! # Example
16//!
17//! ```rust,ignore
18//! use error_rail::tower::ErrorRailLayer;
19//! use tower::ServiceBuilder;
20//!
21//! let service = ServiceBuilder::new()
22//!     .layer(ErrorRailLayer::new("api-gateway"))
23//!     .service(my_service);
24//! ```
25
26use core::future::Future;
27use core::pin::Pin;
28use core::task::{Context, Poll};
29
30use pin_project_lite::pin_project;
31use tower::{Layer, Service};
32
33use crate::traits::IntoErrorContext;
34use crate::types::ComposableError;
35
36/// A Tower [`Layer`] that wraps service errors in [`ComposableError`] with context.
37///
38/// This layer intercepts errors from the wrapped service and adds the configured
39/// context, making it easy to add consistent error context at service boundaries.
40///
41/// # Type Parameters
42///
43/// * `C` - The context type, must implement [`IntoErrorContext`] and [`Clone`]
44///
45/// # Example
46///
47/// ```rust,ignore
48/// use error_rail::tower::ErrorRailLayer;
49/// use tower::ServiceBuilder;
50///
51/// // Add static context
52/// let layer = ErrorRailLayer::new("user-service");
53///
54/// // Or use structured context
55/// let layer = ErrorRailLayer::new(error_rail::group!(
56///     tag("service"),
57///     metadata("version", "1.0")
58/// ));
59/// ```
60#[derive(Clone, Debug)]
61pub struct ErrorRailLayer<C> {
62    context: C,
63}
64
65impl<C> ErrorRailLayer<C> {
66    /// Creates a new `ErrorRailLayer` with the given context.
67    ///
68    /// The context will be attached to all errors from the wrapped service.
69    #[inline]
70    pub fn new(context: C) -> Self {
71        Self { context }
72    }
73
74    /// Returns a reference to the context.
75    #[inline]
76    pub fn context(&self) -> &C {
77        &self.context
78    }
79}
80
81impl<S, C: Clone> Layer<S> for ErrorRailLayer<C> {
82    type Service = ErrorRailService<S, C>;
83
84    fn layer(&self, inner: S) -> Self::Service {
85        ErrorRailService { inner, context: self.context.clone() }
86    }
87}
88
89/// A Tower [`Service`] that wraps errors in [`ComposableError`] with context.
90///
91/// This is created by [`ErrorRailLayer`] and wraps an inner service,
92/// adding error context to any errors it produces.
93#[derive(Clone, Debug)]
94pub struct ErrorRailService<S, C> {
95    inner: S,
96    context: C,
97}
98
99impl<S, C> ErrorRailService<S, C> {
100    /// Creates a new `ErrorRailService` wrapping the given service.
101    #[inline]
102    pub fn new(inner: S, context: C) -> Self {
103        Self { inner, context }
104    }
105
106    /// Returns a reference to the inner service.
107    #[inline]
108    pub fn inner(&self) -> &S {
109        &self.inner
110    }
111
112    /// Returns a mutable reference to the inner service.
113    #[inline]
114    pub fn inner_mut(&mut self) -> &mut S {
115        &mut self.inner
116    }
117
118    /// Consumes the wrapper and returns the inner service.
119    #[inline]
120    pub fn into_inner(self) -> S {
121        self.inner
122    }
123
124    /// Returns a reference to the context.
125    #[inline]
126    pub fn context(&self) -> &C {
127        &self.context
128    }
129}
130
131impl<S, C, Request> Service<Request> for ErrorRailService<S, C>
132where
133    S: Service<Request>,
134    S::Error: core::fmt::Debug,
135    C: IntoErrorContext + Clone,
136{
137    type Response = S::Response;
138    type Error = ComposableError<S::Error>;
139    type Future = ErrorRailFuture<S::Future, C>;
140
141    #[inline]
142    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
143        self.inner.poll_ready(cx).map_err(ComposableError::new)
144    }
145
146    #[inline]
147    fn call(&mut self, request: Request) -> Self::Future {
148        ErrorRailFuture { inner: self.inner.call(request), context: Some(self.context.clone()) }
149    }
150}
151
152pin_project! {
153    /// Future returned by [`ErrorRailService`].
154    ///
155    /// Wraps the inner service's future and adds context on error.
156    pub struct ErrorRailFuture<F, C> {
157        #[pin]
158        inner: F,
159        context: Option<C>,
160    }
161}
162
163impl<F, T, E, C> Future for ErrorRailFuture<F, C>
164where
165    F: Future<Output = Result<T, E>>,
166    C: IntoErrorContext,
167{
168    type Output = Result<T, ComposableError<E>>;
169
170    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
171        let this = self.project();
172
173        match this.inner.poll(cx) {
174            Poll::Ready(Ok(response)) => Poll::Ready(Ok(response)),
175            Poll::Ready(Err(error)) => {
176                let context = this.context.take().expect("polled after completion");
177                let composable = ComposableError::new(error).with_context(context);
178                Poll::Ready(Err(composable))
179            },
180            Poll::Pending => Poll::Pending,
181        }
182    }
183}
184
185/// Extension trait for easily wrapping services with error context.
186pub trait ServiceErrorExt<Request>: Service<Request> + Sized {
187    /// Wraps this service to add error context to all errors.
188    ///
189    /// # Example
190    ///
191    /// ```rust,ignore
192    /// use error_rail::tower::ServiceErrorExt;
193    ///
194    /// let wrapped = my_service.with_error_context("database-layer");
195    /// ```
196    fn with_error_context<C>(self, context: C) -> ErrorRailService<Self, C>
197    where
198        C: IntoErrorContext + Clone,
199    {
200        ErrorRailService::new(self, context)
201    }
202}
203
204impl<S, Request> ServiceErrorExt<Request> for S where S: Service<Request> {}