turul_http_mcp_server/middleware/
session_view_adapter.rs

1//! SessionView adapter for BoxedSessionStorage
2//!
3//! Provides a SessionView implementation that bridges between middleware
4//! and the session storage backend. This allows middleware to read/write
5//! session state without needing to know about storage implementation details.
6
7use async_trait::async_trait;
8use serde_json::Value;
9use std::sync::Arc;
10use turul_mcp_session_storage::{BoxedSessionStorage, SessionView};
11
12/// SessionView adapter backed by BoxedSessionStorage
13///
14/// This adapter implements the SessionView trait by delegating to the underlying
15/// session storage backend. It handles:
16/// - Reading/writing session state
17/// - Reading/writing session metadata (with `__meta__:` prefix)
18/// - Error conversion from storage errors to String
19///
20/// # Architecture
21///
22/// ```text
23/// Middleware → SessionView → StorageBackedSessionView → BoxedSessionStorage
24/// ```
25///
26/// # Example
27///
28/// ```rust,no_run
29/// use std::sync::Arc;
30/// use turul_mcp_session_storage::InMemorySessionStorage;
31/// use turul_http_mcp_server::middleware::StorageBackedSessionView;
32///
33/// # async fn example() {
34/// let storage = Arc::new(InMemorySessionStorage::new());
35/// let session_id = "test-session-123".to_string();
36///
37/// // Create adapter
38/// let session_view = StorageBackedSessionView::new(session_id, storage);
39///
40/// // Middleware can now use SessionView trait methods
41/// // session_view.get_state("key").await
42/// # }
43/// ```
44pub struct StorageBackedSessionView {
45    session_id: String,
46    storage: Arc<BoxedSessionStorage>,
47}
48
49impl StorageBackedSessionView {
50    /// Create a new session view adapter
51    ///
52    /// # Parameters
53    ///
54    /// - `session_id`: The session ID to operate on
55    /// - `storage`: The storage backend to delegate to
56    pub fn new(session_id: String, storage: Arc<BoxedSessionStorage>) -> Self {
57        Self {
58            session_id,
59            storage,
60        }
61    }
62}
63
64#[async_trait]
65impl SessionView for StorageBackedSessionView {
66    fn session_id(&self) -> &str {
67        &self.session_id
68    }
69
70    async fn get_state(&self, key: &str) -> Result<Option<Value>, String> {
71        // Get session from storage
72        let session = self
73            .storage
74            .get_session(&self.session_id)
75            .await
76            .map_err(|e| format!("Failed to get session: {}", e))?;
77
78        // Extract state value
79        Ok(session.and_then(|s| s.state.get(key).cloned()))
80    }
81
82    async fn set_state(&self, key: &str, value: Value) -> Result<(), String> {
83        // Get current session
84        let mut session = self
85            .storage
86            .get_session(&self.session_id)
87            .await
88            .map_err(|e| format!("Failed to get session: {}", e))?
89            .ok_or_else(|| format!("Session '{}' not found", self.session_id))?;
90
91        // Update state
92        session.state.insert(key.to_string(), value);
93
94        // Update last activity timestamp
95        session.last_activity = chrono::Utc::now().timestamp_millis() as u64;
96
97        // Write back to storage
98        self.storage
99            .update_session(session)
100            .await
101            .map_err(|e| format!("Failed to update session: {}", e))
102    }
103
104    async fn get_metadata(&self, key: &str) -> Result<Option<Value>, String> {
105        // Metadata is stored in session.metadata with __meta__: prefix for consistency
106        // with turul-mcp-server's SessionContext pattern
107        let prefixed_key = format!("__meta__:{}", key);
108
109        let session = self
110            .storage
111            .get_session(&self.session_id)
112            .await
113            .map_err(|e| format!("Failed to get session: {}", e))?;
114
115        Ok(session.and_then(|s| s.metadata.get(&prefixed_key).cloned()))
116    }
117
118    async fn set_metadata(&self, key: &str, value: Value) -> Result<(), String> {
119        // Metadata is stored in session.metadata with __meta__: prefix
120        let prefixed_key = format!("__meta__:{}", key);
121
122        let mut session = self
123            .storage
124            .get_session(&self.session_id)
125            .await
126            .map_err(|e| format!("Failed to get session: {}", e))?
127            .ok_or_else(|| format!("Session '{}' not found", self.session_id))?;
128
129        // Update metadata
130        session.metadata.insert(prefixed_key, value);
131
132        // Update last activity timestamp
133        session.last_activity = chrono::Utc::now().timestamp_millis() as u64;
134
135        // Write back to storage
136        self.storage
137            .update_session(session)
138            .await
139            .map_err(|e| format!("Failed to update session: {}", e))
140    }
141}
142
143#[cfg(test)]
144mod tests {
145    use super::*;
146    use serde_json::json;
147    use turul_mcp_protocol::ServerCapabilities;
148    use turul_mcp_session_storage::{BoxedSessionStorage, InMemorySessionStorage};
149
150    #[tokio::test]
151    async fn test_session_view_state() {
152        let storage: Arc<BoxedSessionStorage> = Arc::new(InMemorySessionStorage::new());
153
154        // Create a session
155        let session_info = storage
156            .create_session(ServerCapabilities::default())
157            .await
158            .unwrap();
159
160        let session_id = session_info.session_id.clone();
161
162        // Create adapter
163        let view = StorageBackedSessionView::new(session_id.clone(), Arc::clone(&storage));
164
165        // Test state operations
166        assert_eq!(view.get_state("key1").await.unwrap(), None);
167
168        view.set_state("key1", json!("value1")).await.unwrap();
169        assert_eq!(
170            view.get_state("key1").await.unwrap(),
171            Some(json!("value1"))
172        );
173
174        view.set_state("key2", json!({"nested": "object"}))
175            .await
176            .unwrap();
177        assert_eq!(
178            view.get_state("key2").await.unwrap(),
179            Some(json!({"nested": "object"}))
180        );
181    }
182
183    #[tokio::test]
184    async fn test_session_view_metadata() {
185        let storage: Arc<BoxedSessionStorage> = Arc::new(InMemorySessionStorage::new());
186
187        let session_info = storage
188            .create_session(ServerCapabilities::default())
189            .await
190            .unwrap();
191
192        let session_id = session_info.session_id.clone();
193        let view = StorageBackedSessionView::new(session_id.clone(), Arc::clone(&storage));
194
195        // Test metadata operations
196        assert_eq!(view.get_metadata("meta1").await.unwrap(), None);
197
198        view.set_metadata("meta1", json!("metadata_value"))
199            .await
200            .unwrap();
201        assert_eq!(
202            view.get_metadata("meta1").await.unwrap(),
203            Some(json!("metadata_value"))
204        );
205
206        // Verify metadata is stored with __meta__: prefix in underlying storage
207        let session = storage.get_session(&session_id).await.unwrap().unwrap();
208        assert_eq!(
209            session.metadata.get("__meta__:meta1"),
210            Some(&json!("metadata_value"))
211        );
212    }
213
214    #[tokio::test]
215    async fn test_session_view_session_id() {
216        let storage: Arc<BoxedSessionStorage> = Arc::new(InMemorySessionStorage::new());
217        let session_info = storage
218            .create_session(ServerCapabilities::default())
219            .await
220            .unwrap();
221
222        let view = StorageBackedSessionView::new(
223            session_info.session_id.clone(),
224            Arc::clone(&storage),
225        );
226
227        assert_eq!(view.session_id(), &session_info.session_id);
228    }
229
230    #[tokio::test]
231    async fn test_session_view_nonexistent_session() {
232        let storage: Arc<BoxedSessionStorage> = Arc::new(InMemorySessionStorage::new());
233        let view = StorageBackedSessionView::new("nonexistent".to_string(), Arc::clone(&storage));
234
235        // Getting state from nonexistent session returns None
236        assert_eq!(view.get_state("key").await.unwrap(), None);
237
238        // Setting state on nonexistent session fails
239        let result = view.set_state("key", json!("value")).await;
240        assert!(result.is_err());
241        assert!(result.unwrap_err().contains("not found"));
242    }
243}