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 futures_core::future::FusedFuture;
31use pin_project_lite::pin_project;
32use tower::{Layer, Service};
33
34use crate::traits::IntoErrorContext;
35use crate::types::ComposableError;
36
37/// A Tower [`Layer`] that wraps service errors in [`ComposableError`] with context.
38///
39/// This layer intercepts errors from the wrapped service and adds the configured
40/// context, making it easy to add consistent error context at service boundaries.
41///
42/// # Type Parameters
43///
44/// * `C` - The context type, must implement [`IntoErrorContext`] and [`Clone`]
45///
46/// # Example
47///
48/// ```rust,ignore
49/// use error_rail::tower::ErrorRailLayer;
50/// use tower::ServiceBuilder;
51///
52/// // Add static context
53/// let layer = ErrorRailLayer::new("user-service");
54///
55/// // Or use structured context
56/// let layer = ErrorRailLayer::new(error_rail::group!(
57///     tag("service"),
58///     metadata("version", "1.0")
59/// ));
60/// ```
61#[derive(Clone, Debug)]
62pub struct ErrorRailLayer<C> {
63    context: C,
64}
65
66impl<C> ErrorRailLayer<C> {
67    /// Creates a new `ErrorRailLayer` with the given context.
68    ///
69    /// The context will be attached to all errors from the wrapped service.
70    #[inline]
71    pub const fn new(context: C) -> Self {
72        Self { context }
73    }
74
75    /// Returns a reference to the context.
76    #[inline]
77    pub const fn context(&self) -> &C {
78        &self.context
79    }
80}
81
82impl<S, C: Clone> Layer<S> for ErrorRailLayer<C> {
83    type Service = ErrorRailService<S, C>;
84
85    #[inline]
86    fn layer(&self, inner: S) -> Self::Service {
87        ErrorRailService { inner, context: self.context.clone() }
88    }
89}
90
91/// A Tower [`Service`] that wraps errors in [`ComposableError`] with context.
92///
93/// This is created by [`ErrorRailLayer`] and wraps an inner service,
94/// adding error context to any errors it produces.
95#[derive(Clone, Debug)]
96pub struct ErrorRailService<S, C> {
97    inner: S,
98    context: C,
99}
100
101impl<S, C> ErrorRailService<S, C> {
102    /// Creates a new `ErrorRailService` wrapping the given service.
103    #[inline]
104    pub const fn new(inner: S, context: C) -> Self {
105        Self { inner, context }
106    }
107
108    /// Returns a reference to the inner service.
109    #[inline]
110    pub const fn inner(&self) -> &S {
111        &self.inner
112    }
113
114    /// Returns a mutable reference to the inner service.
115    #[inline]
116    pub fn inner_mut(&mut self) -> &mut S {
117        &mut self.inner
118    }
119
120    /// Consumes the wrapper and returns the inner service.
121    #[inline]
122    pub fn into_inner(self) -> S {
123        self.inner
124    }
125
126    /// Returns a reference to the context.
127    #[inline]
128    pub const fn context(&self) -> &C {
129        &self.context
130    }
131}
132
133impl<S, C, Request> Service<Request> for ErrorRailService<S, C>
134where
135    S: Service<Request>,
136    S::Error: core::fmt::Debug,
137    C: IntoErrorContext + Clone,
138{
139    type Response = S::Response;
140    type Error = ComposableError<S::Error>;
141    type Future = ErrorRailFuture<S::Future, C>;
142
143    #[inline]
144    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
145        self.inner
146            .poll_ready(cx)
147            .map_err(|e| ComposableError::new(e).with_context(self.context.clone()))
148    }
149
150    #[inline]
151    fn call(&mut self, request: Request) -> Self::Future {
152        ErrorRailFuture::new(self.inner.call(request), self.context.clone())
153    }
154}
155
156pin_project! {
157    /// Future returned by [`ErrorRailService`].
158    ///
159    /// Wraps the inner service's future and adds context on error.
160    #[must_use = "futures do nothing unless polled"]
161    pub struct ErrorRailFuture<F, C> {
162        #[pin]
163        inner: F,
164        context: Option<C>,
165    }
166}
167
168impl<F, C> ErrorRailFuture<F, C> {
169    /// Creates a new `ErrorRailFuture` with the given inner future and context.
170    #[inline]
171    fn new(inner: F, context: C) -> Self {
172        Self { inner, context: Some(context) }
173    }
174}
175
176impl<F, T, E, C> Future for ErrorRailFuture<F, C>
177where
178    F: Future<Output = Result<T, E>>,
179    C: IntoErrorContext,
180{
181    type Output = Result<T, ComposableError<E>>;
182
183    #[inline]
184    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
185        let this = self.project();
186
187        match this.inner.poll(cx) {
188            Poll::Ready(Ok(response)) => Poll::Ready(Ok(response)),
189            Poll::Ready(Err(error)) => {
190                // SAFETY: context is always Some until first Ready result
191                let context = this.context.take().expect("polled after completion");
192                Poll::Ready(Err(ComposableError::new(error).with_context(context)))
193            },
194            Poll::Pending => Poll::Pending,
195        }
196    }
197}
198
199impl<F, T, E, C> FusedFuture for ErrorRailFuture<F, C>
200where
201    F: FusedFuture<Output = Result<T, E>>,
202    C: IntoErrorContext,
203{
204    #[inline]
205    fn is_terminated(&self) -> bool {
206        self.context.is_none() || self.inner.is_terminated()
207    }
208}
209
210/// Extension trait for easily wrapping services with error context.
211pub trait ServiceErrorExt<Request>: Service<Request> + Sized {
212    /// Wraps this service to add error context to all errors.
213    ///
214    /// # Example
215    ///
216    /// ```rust,ignore
217    /// use error_rail::tower::ServiceErrorExt;
218    ///
219    /// let wrapped = my_service.with_error_context("database-layer");
220    /// ```
221    fn with_error_context<C>(self, context: C) -> ErrorRailService<Self, C>
222    where
223        C: IntoErrorContext + Clone,
224    {
225        ErrorRailService::new(self, context)
226    }
227}
228
229impl<S, Request> ServiceErrorExt<Request> for S where S: Service<Request> {}