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 fn new(context: C) -> Self {
72        Self { context }
73    }
74
75    /// Returns a reference to the context.
76    #[inline]
77    pub 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    fn layer(&self, inner: S) -> Self::Service {
86        ErrorRailService { inner, context: self.context.clone() }
87    }
88}
89
90/// A Tower [`Service`] that wraps errors in [`ComposableError`] with context.
91///
92/// This is created by [`ErrorRailLayer`] and wraps an inner service,
93/// adding error context to any errors it produces.
94#[derive(Clone, Debug)]
95pub struct ErrorRailService<S, C> {
96    inner: S,
97    context: C,
98}
99
100impl<S, C> ErrorRailService<S, C> {
101    /// Creates a new `ErrorRailService` wrapping the given service.
102    #[inline]
103    pub fn new(inner: S, context: C) -> Self {
104        Self { inner, context }
105    }
106
107    /// Returns a reference to the inner service.
108    #[inline]
109    pub fn inner(&self) -> &S {
110        &self.inner
111    }
112
113    /// Returns a mutable reference to the inner service.
114    #[inline]
115    pub fn inner_mut(&mut self) -> &mut S {
116        &mut self.inner
117    }
118
119    /// Consumes the wrapper and returns the inner service.
120    #[inline]
121    pub fn into_inner(self) -> S {
122        self.inner
123    }
124
125    /// Returns a reference to the context.
126    #[inline]
127    pub fn context(&self) -> &C {
128        &self.context
129    }
130}
131
132impl<S, C, Request> Service<Request> for ErrorRailService<S, C>
133where
134    S: Service<Request>,
135    S::Error: core::fmt::Debug,
136    C: IntoErrorContext + Clone,
137{
138    type Response = S::Response;
139    type Error = ComposableError<S::Error>;
140    type Future = ErrorRailFuture<S::Future, C>;
141
142    #[inline]
143    fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
144        self.inner
145            .poll_ready(cx)
146            .map_err(|e| ComposableError::new(e).with_context(self.context.clone()))
147    }
148
149    #[inline]
150    fn call(&mut self, request: Request) -> Self::Future {
151        ErrorRailFuture { inner: self.inner.call(request), context: Some(self.context.clone()) }
152    }
153}
154
155pin_project! {
156    /// Future returned by [`ErrorRailService`].
157    ///
158    /// Wraps the inner service's future and adds context on error.
159    pub struct ErrorRailFuture<F, C> {
160        #[pin]
161        inner: F,
162        context: Option<C>,
163    }
164}
165
166impl<F, T, E, C> Future for ErrorRailFuture<F, C>
167where
168    F: Future<Output = Result<T, E>>,
169    C: IntoErrorContext,
170{
171    type Output = Result<T, ComposableError<E>>;
172
173    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
174        let this = self.project();
175
176        match this.inner.poll(cx) {
177            Poll::Ready(Ok(response)) => Poll::Ready(Ok(response)),
178            Poll::Ready(Err(error)) => {
179                let context = this.context.take().expect("polled after completion");
180                let composable = ComposableError::new(error).with_context(context);
181                Poll::Ready(Err(composable))
182            },
183            Poll::Pending => Poll::Pending,
184        }
185    }
186}
187
188impl<F, T, E, C> FusedFuture for ErrorRailFuture<F, C>
189where
190    F: FusedFuture<Output = Result<T, E>>,
191    C: IntoErrorContext,
192{
193    fn is_terminated(&self) -> bool {
194        self.context.is_none() || self.inner.is_terminated()
195    }
196}
197
198/// Extension trait for easily wrapping services with error context.
199pub trait ServiceErrorExt<Request>: Service<Request> + Sized {
200    /// Wraps this service to add error context to all errors.
201    ///
202    /// # Example
203    ///
204    /// ```rust,ignore
205    /// use error_rail::tower::ServiceErrorExt;
206    ///
207    /// let wrapped = my_service.with_error_context("database-layer");
208    /// ```
209    fn with_error_context<C>(self, context: C) -> ErrorRailService<Self, C>
210    where
211        C: IntoErrorContext + Clone,
212    {
213        ErrorRailService::new(self, context)
214    }
215}
216
217impl<S, Request> ServiceErrorExt<Request> for S where S: Service<Request> {}