lrwf_core/http.rs
1//! HTTP abstraction traits: IHttpContext, IHttpRequest, IHttpResponse.
2//!
3//! This module defines the core interfaces for HTTP request/response handling
4//! in the LRWF framework. All types are defined as traits to enable testability
5//! and middleware composition.
6//!
7//! # Traits
8//!
9//! - [`IHttpContext`] — The central context for a single HTTP request, providing
10//! access to request, response, and authentication claims.
11//! - [`IHttpRequest`] — Read-only access to the incoming HTTP request.
12//! - [`IHttpResponse`] — Mutable access to the outgoing HTTP response.
13//! - [`IClaimsExt`] — Extension for storing/retrieving authentication claims.
14//! - [`FromHttpContext`] — Build a request struct from HTTP request data.
15//!
16//! # Helpers
17//!
18//! - [`read_json_body`] — Deserialize JSON from the request body.
19//! - [`write_json_response`] — Serialize a value as JSON into the response.
20
21use crate::auth::IClaims;
22use crate::error::Result;
23use std::collections::HashMap;
24
25/// Common HTTP status codes.
26pub struct HttpStatus;
27
28impl HttpStatus {
29 pub const OK: u16 = 200;
30 pub const CREATED: u16 = 201;
31 pub const NO_CONTENT: u16 = 204;
32 pub const BAD_REQUEST: u16 = 400;
33 pub const UNAUTHORIZED: u16 = 401;
34 pub const FORBIDDEN: u16 = 403;
35 pub const NOT_FOUND: u16 = 404;
36 pub const INTERNAL_SERVER_ERROR: u16 = 500;
37}
38
39// ---------------------------------------------------------------------------
40// Claims extension for IHttpContext — MUST be defined before IHttpContext
41// ---------------------------------------------------------------------------
42
43/// Extension trait that adds claims storage to an `IHttpContext`.
44///
45/// Implemented by `HttpContext` in `lrwf-http`. This trait is a supertrait
46/// of `IHttpContext` so that authentication/authorization middleware can
47/// store and retrieve claims through `&mut dyn IHttpContext` directly.
48pub trait IClaimsExt {
49 /// Store authentication claims in the context.
50 fn set_claims(&mut self, claims: Box<dyn IClaims>);
51
52 /// Retrieve authentication claims from the context, if present.
53 fn claims(&self) -> Option<&dyn IClaims>;
54}
55
56/// HTTP context encapsulating the request, response, and service provider
57/// for the duration of a single HTTP request.
58///
59/// Extends `IClaimsExt` so middleware can store/retrieve auth claims.
60///
61/// Analogous to ASP.NET Core's HttpContext.
62pub trait IHttpContext: IClaimsExt + Send {
63 fn request(&self) -> &dyn IHttpRequest;
64 fn request_mut(&mut self) -> &mut dyn IHttpRequest;
65 fn response(&self) -> &dyn IHttpResponse;
66 fn response_mut(&mut self) -> &mut dyn IHttpResponse;
67}
68
69/// HTTP request abstraction.
70///
71/// Analogous to ASP.NET Core's HttpRequest.
72///
73/// Methods are non-generic to maintain dyn-compatibility.
74/// Use `serde_json::from_slice` on the raw body bytes for JSON deserialization.
75#[async_trait::async_trait]
76pub trait IHttpRequest: Send {
77 fn method(&self) -> &str;
78 fn path(&self) -> &str;
79 fn header(&self, name: &str) -> Option<&str>;
80 fn query(&self) -> &HashMap<String, String>;
81 fn route_params(&self) -> &HashMap<String, String>;
82 fn route_params_mut(&mut self) -> &mut HashMap<String, String>;
83
84 /// The original route pattern that was matched (e.g., `"/api/users/{id}"`).
85 /// Set by the router after successful route matching.
86 fn route_pattern(&self) -> Option<&str>;
87 fn route_pattern_mut(&mut self) -> &mut Option<String>;
88
89 /// Return the raw request body bytes.
90 async fn body_bytes(&self) -> Result<Vec<u8>>;
91
92 /// Return the request body as a UTF-8 string.
93 async fn body_text(&self) -> Result<String> {
94 let bytes = self.body_bytes().await?;
95 String::from_utf8(bytes).map_err(|e| crate::error::Error::Http(e.to_string()))
96 }
97}
98
99/// HTTP response abstraction.
100///
101/// Analogous to ASP.NET Core's HttpResponse.
102///
103/// Methods are non-generic to maintain dyn-compatibility.
104#[async_trait::async_trait]
105pub trait IHttpResponse: Send {
106 /// Get the current HTTP status code.
107 fn status(&self) -> u16;
108 fn set_status(&mut self, code: u16);
109 fn set_header(&mut self, key: &str, value: &str);
110
111 /// Whether a response body has been written.
112 fn has_body(&self) -> bool {
113 false
114 }
115
116 /// Write raw bytes as the response body.
117 async fn write_bytes(&mut self, data: Vec<u8>) -> Result<()>;
118
119 /// Write a UTF-8 string as the response body.
120 async fn write_text(&mut self, text: &str) -> Result<()> {
121 self.write_bytes(text.as_bytes().to_vec()).await
122 }
123}
124
125/// Helper: build a JSON response by serializing a value and writing it.
126pub async fn write_json_response<T: serde::Serialize + Send>(
127 resp: &mut dyn IHttpResponse,
128 value: &T,
129) -> Result<()> {
130 let json = serde_json::to_vec(value)?;
131 resp.set_header("content-type", "application/json");
132 resp.write_bytes(json).await
133}
134
135/// Helper: read JSON from the request body.
136pub async fn read_json_body<T: serde::de::DeserializeOwned>(req: &dyn IHttpRequest) -> Result<T> {
137 let bytes = req.body_bytes().await?;
138 serde_json::from_slice(&bytes).map_err(crate::error::Error::Serialization)
139}
140
141/// Type alias for JSON responses in controller methods.
142pub type Json<T> = T;
143
144/// Trait for constructing a request struct from the HTTP context.
145///
146/// Implemented automatically by the `#[get]`, `#[post]`, `#[put]`, `#[delete]`
147/// proc macros for request types. Custom implementations are also supported.
148///
149/// # Automatic implementation
150///
151/// For POST/PUT/PATCH routes, the macro will attempt `serde_json::from_slice`
152/// on the request body. For GET/DELETE routes with path parameters, the macro
153/// will extract parameters from `ctx.request().route_params()` by field name.
154///
155/// # Manual implementation
156///
157/// ```ignore
158/// #[async_trait::async_trait]
159/// impl FromHttpContext for MyRequest {
160/// async fn from_http_context(ctx: &dyn IHttpContext) -> Result<Self> {
161/// // custom construction logic
162/// }
163/// }
164/// ```
165#[async_trait::async_trait]
166pub trait FromHttpContext: Sized + Send + 'static {
167 async fn from_http_context(ctx: &dyn IHttpContext) -> Result<Self>;
168}