rust_webx_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 rust-webx 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 `rust-webx-host`. 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 /// Remove a response header by name. Default is a no-op.
112 fn remove_header(&mut self, _key: &str) {}
113
114 /// Whether a response body has been written.
115 fn has_body(&self) -> bool {
116 false
117 }
118
119 /// Write raw bytes as the response body.
120 async fn write_bytes(&mut self, data: Vec<u8>) -> Result<()>;
121
122 /// Write a UTF-8 string as the response body.
123 async fn write_text(&mut self, text: &str) -> Result<()> {
124 self.write_bytes(text.as_bytes().to_vec()).await
125 }
126
127 /// Read the current response body bytes.
128 ///
129 /// Returns an empty Vec if no body has been written.
130 /// Used by post-processing middleware (e.g. compression) in `after` hooks.
131 fn body_bytes(&self) -> Vec<u8> {
132 Vec::new()
133 }
134
135 /// Read a response header value by name.
136 ///
137 /// Returns None if the header is not set.
138 /// Used by post-processing middleware (e.g. compression) to inspect content-type.
139 fn header(&self, _key: &str) -> Option<&str> {
140 None
141 }
142}
143
144/// Helper: build a JSON response by serializing a value and writing it.
145pub async fn write_json_response<T: serde::Serialize + Send>(
146 resp: &mut dyn IHttpResponse,
147 value: &T,
148) -> Result<()> {
149 let json = serde_json::to_vec(value)?;
150 resp.set_header("content-type", "application/json");
151 resp.write_bytes(json).await
152}
153
154/// Helper: read JSON from the request body.
155pub async fn read_json_body<T: serde::de::DeserializeOwned>(req: &dyn IHttpRequest) -> Result<T> {
156 let bytes = req.body_bytes().await?;
157 serde_json::from_slice(&bytes).map_err(crate::error::Error::Serialization)
158}
159
160/// Type alias for JSON responses in controller methods.
161pub type Json<T> = T;
162
163/// Trait for constructing a request struct from the HTTP context.
164///
165/// Implemented automatically by the `#[get]`, `#[post]`, `#[put]`, `#[delete]`
166/// proc macros for request types. Custom implementations are also supported.
167///
168/// # Automatic implementation
169///
170/// For POST/PUT/PATCH routes, the macro will attempt `serde_json::from_slice`
171/// on the request body. For GET/DELETE routes with path parameters, the macro
172/// will extract parameters from `ctx.request().route_params()` by field name.
173///
174/// # Manual implementation
175///
176/// ```ignore
177/// #[async_trait::async_trait]
178/// impl FromHttpContext for MyRequest {
179/// async fn from_http_context(ctx: &dyn IHttpContext) -> Result<Self> {
180/// // custom construction logic
181/// }
182/// }
183/// ```
184#[async_trait::async_trait]
185pub trait FromHttpContext: Sized + Send + 'static {
186 async fn from_http_context(ctx: &dyn IHttpContext) -> Result<Self>;
187}