server_less_core/extract.rs
1//! Context and parameter extraction types.
2
3use std::collections::HashMap;
4
5#[cfg(feature = "ws")]
6use std::sync::Arc;
7#[cfg(feature = "ws")]
8use tokio::sync::Mutex;
9
10/// Protocol-agnostic request context.
11///
12/// Provides unified access to protocol-specific metadata like headers, user info, and trace IDs.
13///
14/// # Automatic Injection
15///
16/// When using protocol macros (`#[http]`, `#[ws]`, etc.), methods can receive a `Context`
17/// parameter that is automatically populated with request metadata:
18///
19/// ```ignore
20/// use server_less::{http, Context};
21///
22/// #[http]
23/// impl UserService {
24/// async fn create_user(&self, ctx: Context, name: String) -> Result<User> {
25/// // Access request metadata
26/// let user_id = ctx.user_id()?; // Authenticated user
27/// let request_id = ctx.request_id()?; // Request trace ID
28/// let auth = ctx.authorization(); // Authorization header
29///
30/// // Create user with context...
31/// }
32/// }
33/// ```
34///
35/// # Protocol-Specific Metadata
36///
37/// Different protocols populate Context with relevant data:
38/// - **HTTP**: All headers via `header()`, request ID from `x-request-id`
39/// - **gRPC**: Metadata fields (not yet implemented)
40/// - **CLI**: Environment variables via `env()` (not yet implemented)
41/// - **MCP**: Conversation context (not yet implemented)
42///
43/// # Name Collision
44///
45/// If you have your own `Context` type, qualify the server-less version:
46/// ```ignore
47/// fn handler(&self, ctx: server_less::Context) { }
48/// ```
49///
50/// See the `#[http]` macro documentation for details on collision handling.
51#[derive(Debug, Clone, Default)]
52pub struct Context {
53 /// Key-value metadata (headers, env vars, etc.)
54 metadata: HashMap<String, String>,
55 /// The authenticated user ID, if any
56 user_id: Option<String>,
57 /// Request ID for tracing
58 request_id: Option<String>,
59}
60
61impl Context {
62 /// Create a new empty context
63 pub fn new() -> Self {
64 Self::default()
65 }
66
67 /// Create context with metadata
68 pub fn with_metadata(metadata: HashMap<String, String>) -> Self {
69 Self {
70 metadata,
71 ..Default::default()
72 }
73 }
74
75 /// Get a metadata value
76 pub fn get(&self, key: &str) -> Option<&str> {
77 self.metadata.get(key).map(|s| s.as_str())
78 }
79
80 /// Set a metadata value
81 pub fn set(&mut self, key: impl Into<String>, value: impl Into<String>) {
82 self.metadata.insert(key.into(), value.into());
83 }
84
85 /// Get the authenticated user ID
86 pub fn user_id(&self) -> Option<&str> {
87 self.user_id.as_deref()
88 }
89
90 /// Set the authenticated user ID
91 pub fn set_user_id(&mut self, user_id: impl Into<String>) {
92 self.user_id = Some(user_id.into());
93 }
94
95 /// Get the request ID
96 pub fn request_id(&self) -> Option<&str> {
97 self.request_id.as_deref()
98 }
99
100 /// Set the request ID
101 pub fn set_request_id(&mut self, request_id: impl Into<String>) {
102 self.request_id = Some(request_id.into());
103 }
104
105 /// Get all metadata
106 pub fn metadata(&self) -> &HashMap<String, String> {
107 &self.metadata
108 }
109
110 // HTTP-specific helpers
111
112 /// Get an HTTP header value (case-insensitive lookup)
113 pub fn header(&self, name: &str) -> Option<&str> {
114 let name_lower = name.to_lowercase();
115 self.metadata
116 .iter()
117 .find(|(k, _)| k.to_lowercase() == name_lower)
118 .map(|(_, v)| v.as_str())
119 }
120
121 /// Get the Authorization header
122 pub fn authorization(&self) -> Option<&str> {
123 self.header("authorization")
124 }
125
126 /// Get the Content-Type header
127 pub fn content_type(&self) -> Option<&str> {
128 self.header("content-type")
129 }
130
131 // CLI-specific helpers
132
133 /// Get an environment variable
134 pub fn env(&self, name: &str) -> Option<&str> {
135 self.get(&format!("env:{name}"))
136 }
137}
138
139/// WebSocket sender for server-push messaging.
140///
141/// Allows WebSocket handlers to send messages to the client independently of
142/// the request/response cycle, enabling true bidirectional communication.
143///
144/// # Automatic Injection
145///
146/// Methods in `#[ws]` impl blocks can receive a `WsSender` parameter that is
147/// automatically injected:
148///
149/// ```ignore
150/// use server_less::{ws, WsSender};
151///
152/// #[ws(path = "/chat")]
153/// impl ChatService {
154/// async fn join_room(&self, sender: WsSender, room: String) -> String {
155/// // Store sender for later use
156/// self.rooms.add_user(room, sender);
157/// "Joined room".to_string()
158/// }
159///
160/// async fn broadcast(&self, room: String, message: String) {
161/// // Send to all users in room (senders stored earlier)
162/// for sender in self.rooms.get_senders(&room) {
163/// sender.send_json(&json!({"type": "broadcast", "msg": message})).await.ok();
164/// }
165/// }
166/// }
167/// ```
168///
169/// # Thread Safety
170///
171/// `WsSender` is cheaply cloneable (via `Arc`) and thread-safe, so you can:
172/// - Store it in application state
173/// - Clone and send it to background tasks
174/// - Share it across threads
175///
176/// ```ignore
177/// // Clone and use in background task
178/// let sender_clone = sender.clone();
179/// tokio::spawn(async move {
180/// sender_clone.send("Background message").await.ok();
181/// });
182/// ```
183#[cfg(feature = "ws")]
184#[derive(Clone)]
185pub struct WsSender {
186 sender: Arc<
187 Mutex<futures::stream::SplitSink<axum::extract::ws::WebSocket, axum::extract::ws::Message>>,
188 >,
189}
190
191#[cfg(feature = "ws")]
192impl WsSender {
193 /// Create a new WebSocket sender (internal use by macros)
194 #[doc(hidden)]
195 pub fn new(
196 sender: futures::stream::SplitSink<
197 axum::extract::ws::WebSocket,
198 axum::extract::ws::Message,
199 >,
200 ) -> Self {
201 Self {
202 sender: Arc::new(Mutex::new(sender)),
203 }
204 }
205
206 /// Send a text message to the WebSocket client
207 ///
208 /// # Errors
209 ///
210 /// Returns an error if the connection is closed or the message cannot be sent.
211 ///
212 /// # Example
213 ///
214 /// ```ignore
215 /// sender.send("Hello, client!").await?;
216 /// ```
217 pub async fn send(&self, text: impl Into<String>) -> Result<(), String> {
218 use futures::sink::SinkExt;
219 let mut guard = self.sender.lock().await;
220 guard
221 .send(axum::extract::ws::Message::Text(text.into().into()))
222 .await
223 .map_err(|e| format!("Failed to send WebSocket message: {}", e))
224 }
225
226 /// Send a JSON value to the WebSocket client
227 ///
228 /// The value is serialized to JSON and sent as a text message.
229 ///
230 /// # Errors
231 ///
232 /// Returns an error if serialization fails or the connection is closed.
233 ///
234 /// # Example
235 ///
236 /// ```ignore
237 /// use serde_json::json;
238 ///
239 /// sender.send_json(&json!({
240 /// "type": "notification",
241 /// "message": "New message received"
242 /// })).await?;
243 /// ```
244 pub async fn send_json<T: serde::Serialize>(&self, value: &T) -> Result<(), String> {
245 let json =
246 serde_json::to_string(value).map_err(|e| format!("Failed to serialize JSON: {}", e))?;
247 self.send(json).await
248 }
249
250 /// Close the WebSocket connection
251 ///
252 /// Sends a close frame to the client and terminates the connection.
253 ///
254 /// # Example
255 ///
256 /// ```ignore
257 /// sender.close().await?;
258 /// ```
259 pub async fn close(&self) -> Result<(), String> {
260 use futures::sink::SinkExt;
261 let mut guard = self.sender.lock().await;
262 guard
263 .send(axum::extract::ws::Message::Close(None))
264 .await
265 .map_err(|e| format!("Failed to close WebSocket: {}", e))
266 }
267}
268
269/// Marker type for parameters that should be extracted from the URL path
270#[derive(Debug, Clone, PartialEq, Eq)]
271pub struct Path<T>(pub T);
272
273impl<T> Path<T> {
274 pub fn into_inner(self) -> T {
275 self.0
276 }
277}
278
279impl<T> std::ops::Deref for Path<T> {
280 type Target = T;
281
282 fn deref(&self) -> &Self::Target {
283 &self.0
284 }
285}
286
287/// Marker type for parameters that should be extracted from query string
288#[derive(Debug, Clone, PartialEq, Eq)]
289pub struct Query<T>(pub T);
290
291impl<T> Query<T> {
292 pub fn into_inner(self) -> T {
293 self.0
294 }
295}
296
297impl<T> std::ops::Deref for Query<T> {
298 type Target = T;
299
300 fn deref(&self) -> &Self::Target {
301 &self.0
302 }
303}
304
305/// Marker type for parameters that should be extracted from request body
306#[derive(Debug, Clone, PartialEq, Eq)]
307pub struct Json<T>(pub T);
308
309impl<T> Json<T> {
310 pub fn into_inner(self) -> T {
311 self.0
312 }
313}
314
315impl<T> std::ops::Deref for Json<T> {
316 type Target = T;
317
318 fn deref(&self) -> &Self::Target {
319 &self.0
320 }
321}
322
323#[cfg(test)]
324mod tests {
325 use super::*;
326
327 #[test]
328 fn test_context_metadata() {
329 let mut ctx = Context::new();
330 ctx.set("Content-Type", "application/json");
331 ctx.set("X-Request-Id", "abc123");
332
333 assert_eq!(ctx.get("Content-Type"), Some("application/json"));
334 assert_eq!(ctx.header("content-type"), Some("application/json"));
335 }
336
337 #[test]
338 fn test_context_user() {
339 let mut ctx = Context::new();
340 assert!(ctx.user_id().is_none());
341
342 ctx.set_user_id("user_123");
343 assert_eq!(ctx.user_id(), Some("user_123"));
344 }
345}