Skip to main content

anycms_core/frameworks/
actix.rs

1#[cfg(feature = "actix")]
2use crate::result::{ApiResult, ErrorCode};
3#[cfg(feature = "actix")]
4use actix_web::Error;
5#[cfg(feature = "actix")]
6use actix_web::HttpMessage;
7#[cfg(feature = "actix")]
8use actix_web::dev::{Service, ServiceRequest, ServiceResponse, Transform, forward_ready};
9#[cfg(feature = "actix")]
10use actix_web::{HttpResponse, Responder};
11#[cfg(feature = "actix")]
12use futures_util::future::LocalBoxFuture;
13#[cfg(feature = "actix")]
14use serde::Serialize;
15#[cfg(feature = "actix")]
16use std::rc::Rc;
17
18#[cfg(feature = "actix")]
19fn error_code_to_status(code: ErrorCode) -> actix_web::http::StatusCode {
20    match code {
21        ErrorCode::Success => actix_web::http::StatusCode::OK,
22        ErrorCode::BadRequest => actix_web::http::StatusCode::BAD_REQUEST,
23        ErrorCode::Unauthorized => actix_web::http::StatusCode::UNAUTHORIZED,
24        ErrorCode::Forbidden => actix_web::http::StatusCode::FORBIDDEN,
25        ErrorCode::NotFound => actix_web::http::StatusCode::NOT_FOUND,
26        ErrorCode::Conflict => actix_web::http::StatusCode::CONFLICT,
27        ErrorCode::ValidationError => actix_web::http::StatusCode::UNPROCESSABLE_ENTITY,
28        ErrorCode::InternalError => actix_web::http::StatusCode::INTERNAL_SERVER_ERROR,
29    }
30}
31
32// ============================================================
33// Request Extension: trace_id carrier
34// ============================================================
35
36/// Internal extension that carries the trace ID through the request lifecycle.
37///
38/// Set by [`ApiResultLayer`] middleware and read by the `Responder` implementation
39/// to auto-inject `trace_id` into `ApiResult` responses.
40#[cfg(feature = "actix")]
41#[derive(Clone)]
42pub struct ApiTraceId(pub String);
43
44// ============================================================
45// Middleware Configuration
46// ============================================================
47
48/// Configuration for the [`ApiResultLayer`] middleware.
49#[cfg(feature = "actix")]
50#[derive(Clone)]
51pub struct ApiResultMiddlewareConfig {
52    /// Header names to check for an existing trace ID (checked in order).
53    ///
54    /// Default: `["X-Request-ID", "X-Trace-ID"]`
55    pub trace_id_headers: Vec<String>,
56
57    /// Whether to auto-inject the current Unix-millisecond timestamp.
58    ///
59    /// Default: `false`
60    pub inject_timestamp: bool,
61}
62
63#[cfg(feature = "actix")]
64impl Default for ApiResultMiddlewareConfig {
65    fn default() -> Self {
66        Self {
67            trace_id_headers: vec!["X-Request-ID".to_string(), "X-Trace-ID".to_string()],
68            inject_timestamp: false,
69        }
70    }
71}
72
73// ============================================================
74// Middleware Layer (Transform factory)
75// ============================================================
76
77/// Actix-web middleware layer that auto-injects `trace_id` (and optionally `timestamp`)
78/// into `ApiResult` responses.
79///
80/// # How it works
81///
82/// 1. Extracts a trace ID from request headers (or generates a UUID v4)
83/// 2. Stores it in request extensions as [`ApiTraceId`]
84/// 3. The `Responder` impl for `ApiResult<T>` reads the extension and injects it
85///
86/// This approach is **zero-overhead**: no body re-serialization needed.
87///
88/// # Usage
89///
90/// ```ignore
91/// use actix_web::App;
92/// use anycms_core::actix::ApiResultLayer;
93///
94/// App::new()
95///     .wrap(ApiResultLayer::default())
96///     .route("/users", web::get().to(list_users))
97/// ```
98#[cfg(feature = "actix")]
99pub struct ApiResultLayer {
100    config: ApiResultMiddlewareConfig,
101}
102
103#[cfg(feature = "actix")]
104impl ApiResultLayer {
105    /// Create a new layer with the given configuration.
106    pub fn new(config: ApiResultMiddlewareConfig) -> Self {
107        Self { config }
108    }
109}
110
111#[cfg(feature = "actix")]
112impl Default for ApiResultLayer {
113    fn default() -> Self {
114        Self::new(ApiResultMiddlewareConfig::default())
115    }
116}
117
118#[cfg(feature = "actix")]
119impl<S, B> Transform<S, ServiceRequest> for ApiResultLayer
120where
121    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
122    S::Future: 'static,
123    B: 'static,
124{
125    type Response = ServiceResponse<B>;
126    type Error = Error;
127    type InitError = ();
128    type Transform = ApiResultMiddleware<S>;
129    type Future = std::future::Ready<Result<Self::Transform, Self::InitError>>;
130
131    fn new_transform(&self, service: S) -> Self::Future {
132        std::future::ready(Ok(ApiResultMiddleware {
133            service: Rc::new(service),
134            config: self.config.clone(),
135        }))
136    }
137}
138
139// ============================================================
140// Middleware Service
141// ============================================================
142
143/// The actual middleware service that processes each request.
144#[cfg(feature = "actix")]
145pub struct ApiResultMiddleware<S> {
146    service: Rc<S>,
147    config: ApiResultMiddlewareConfig,
148}
149
150#[cfg(feature = "actix")]
151impl<S, B> Service<ServiceRequest> for ApiResultMiddleware<S>
152where
153    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
154    S::Future: 'static,
155    B: 'static,
156{
157    type Response = ServiceResponse<B>;
158    type Error = Error;
159    type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
160
161    forward_ready!(service);
162
163    fn call(&self, req: ServiceRequest) -> Self::Future {
164        let service = Rc::clone(&self.service);
165
166        // Extract trace_id from headers or generate a new one
167        let trace_id = self
168            .config
169            .trace_id_headers
170            .iter()
171            .find_map(|h| {
172                req.headers()
173                    .get(h.as_str())
174                    .and_then(|v| v.to_str().ok())
175                    .map(|s| s.to_string())
176            })
177            .unwrap_or_else(|| uuid::Uuid::new_v4().to_string());
178
179        // Store in request extensions for the Responder to pick up
180        req.extensions_mut().insert(ApiTraceId(trace_id));
181
182        Box::pin(async move {
183            let res = service.call(req).await?;
184            Ok(res)
185        })
186    }
187}
188
189// ============================================================
190// Responder implementation (enhanced with auto trace_id injection)
191// ============================================================
192
193/// Actix-web framework integration for ApiResult.
194///
195/// When [`ApiResultLayer`] middleware is active, the `trace_id` is automatically
196/// injected from request extensions into the response.
197#[cfg(feature = "actix")]
198impl<T: Serialize> Responder for ApiResult<T> {
199    type Body = actix_web::body::BoxBody;
200
201    fn respond_to(self, req: &actix_web::HttpRequest) -> actix_web::HttpResponse<Self::Body> {
202        let mut result = self;
203
204        // Auto-inject trace_id from middleware if not already set
205        if result.trace_id.is_none()
206            && let Some(tid) = req.extensions().get::<ApiTraceId>()
207        {
208            result = result.with_trace_id(&tid.0);
209        }
210
211        let status = result
212            .code
213            .and_then(ErrorCode::from_i32)
214            .map(error_code_to_status)
215            .unwrap_or_else(|| {
216                if result.success {
217                    actix_web::http::StatusCode::OK
218                } else {
219                    actix_web::http::StatusCode::INTERNAL_SERVER_ERROR
220                }
221            });
222
223        HttpResponse::build(status).json(result)
224    }
225}