Skip to main content

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}