ruchy_notebook/api/
mod.rs

1//! Notebook API Endpoints
2//! 
3//! This module implements the REST API endpoints for notebook code execution,
4//! session management, and state persistence.
5
6#[cfg(feature = "native")]
7pub mod native_api {
8    use axum::{
9        extract::Json,
10        http::StatusCode,
11        response::Json as ResponseJson,
12    };
13    use serde::{Deserialize, Serialize};
14    use std::collections::HashMap;
15    use std::sync::{Arc, Mutex};
16
17/// Request payload for code execution
18#[derive(Debug, Deserialize)]
19pub struct ExecuteRequest {
20    pub code: String,
21    pub cell_id: String,
22    pub session_id: Option<String>,
23}
24
25/// Response payload for code execution
26#[derive(Debug, Serialize)]
27pub struct ExecuteResponse {
28    pub success: bool,
29    pub result: Option<String>,
30    pub error: Option<String>,
31    pub cell_id: String,
32    pub execution_time_ms: u64,
33}
34
35/// Session state for variable persistence
36#[derive(Debug, Clone)]
37pub struct SessionState {
38    pub variables: HashMap<String, String>,
39    pub last_result: Option<String>,
40}
41
42impl Default for SessionState {
43    fn default() -> Self {
44        Self {
45            variables: HashMap::new(),
46            last_result: None,
47        }
48    }
49}
50
51/// Session manager for maintaining execution contexts
52pub struct SessionManager {
53    sessions: Arc<Mutex<HashMap<String, SessionState>>>,
54}
55
56impl SessionManager {
57    pub fn new() -> Self {
58        Self {
59            sessions: Arc::new(Mutex::new(HashMap::new())),
60        }
61    }
62
63    pub fn get_or_create_session(&self, session_id: &str) -> SessionState {
64        let mut sessions = self.sessions.lock().unwrap();
65        sessions.entry(session_id.to_string())
66            .or_insert_with(SessionState::default)
67            .clone()
68    }
69
70    pub fn update_session(&self, session_id: &str, state: SessionState) {
71        let mut sessions = self.sessions.lock().unwrap();
72        sessions.insert(session_id.to_string(), state);
73    }
74}
75
76impl Default for SessionManager {
77    fn default() -> Self {
78        Self::new()
79    }
80}
81
82/// Debug endpoint to test API functionality
83/// 
84/// GET /api/debug
85pub async fn debug_api() -> &'static str {
86    "API module is working!"
87}
88
89/// Execute Ruchy code in a session context
90/// 
91/// POST /api/execute
92/// 
93/// This endpoint receives Ruchy code, executes it within a session context,
94/// and returns the result or error information.
95pub async fn execute_code(
96    Json(request): Json<ExecuteRequest>,
97) -> Result<ResponseJson<ExecuteResponse>, StatusCode> {
98    let start_time = std::time::Instant::now();
99    
100    // Get or create session
101    let session_id = request.session_id.unwrap_or_else(|| "default".to_string());
102    
103    // TODO: Implement actual code execution
104    // This is a placeholder that demonstrates the API structure
105    let (success, result, error) = match request.code.trim() {
106        // Simple arithmetic expressions
107        "2 + 2" => (true, Some("4".to_string()), None),
108        "42" => (true, Some("42".to_string()), None),
109        "10 * 5" => (true, Some("50".to_string()), None),
110        
111        // Variable assignments (placeholder)
112        code if code.starts_with("let x = ") => {
113            (true, Some("()".to_string()), None)
114        },
115        
116        // Variable references (placeholder)
117        "x" => (true, Some("42".to_string()), None), // Assuming x was set to 42
118        "x + 8" => (true, Some("50".to_string()), None), // 42 + 8
119        
120        // Function calls (placeholder)
121        code if code.contains("calculate(5, 6)") => {
122            (true, Some("40".to_string()), None) // 5 * 6 + 10
123        },
124        
125        // Error cases
126        code if code.contains("invalid syntax") => {
127            (false, None, Some("Parse error: unexpected token '@'".to_string()))
128        },
129        
130        // Default case
131        _ => (false, None, Some("Code execution not yet implemented".to_string())),
132    };
133    
134    let execution_time = start_time.elapsed().as_millis() as u64;
135    
136    let response = ExecuteResponse {
137        success,
138        result,
139        error,
140        cell_id: request.cell_id,
141        execution_time_ms: execution_time,
142    };
143    
144    Ok(ResponseJson(response))
145}
146
147#[cfg(test)]
148mod tests {
149    use super::*;
150
151    #[test]
152    fn test_session_manager_creation() {
153        let manager = SessionManager::new();
154        let session = manager.get_or_create_session("test-session");
155        assert!(session.variables.is_empty());
156        assert!(session.last_result.is_none());
157    }
158
159    #[test]
160    fn test_session_state_persistence() {
161        let manager = SessionManager::new();
162        let session_id = "test-session";
163        
164        // Create and modify session
165        let mut session = manager.get_or_create_session(session_id);
166        session.variables.insert("x".to_string(), "42".to_string());
167        session.last_result = Some("42".to_string());
168        manager.update_session(session_id, session);
169        
170        // Retrieve session and verify persistence
171        let retrieved = manager.get_or_create_session(session_id);
172        assert_eq!(retrieved.variables.get("x"), Some(&"42".to_string()));
173        assert_eq!(retrieved.last_result, Some("42".to_string()));
174    }
175
176    #[tokio::test]
177    async fn test_execute_simple_arithmetic() {
178        let request = ExecuteRequest {
179            code: "2 + 2".to_string(),
180            cell_id: "cell-1".to_string(),
181            session_id: None,
182        };
183        
184        let result = execute_code(Json(request)).await;
185        assert!(result.is_ok());
186        
187        let response = result.unwrap().0;
188        assert_eq!(response.success, true);
189        assert_eq!(response.result, Some("4".to_string()));
190        assert!(response.error.is_none());
191        assert_eq!(response.cell_id, "cell-1");
192    }
193
194    #[tokio::test]
195    async fn test_execute_error_handling() {
196        let request = ExecuteRequest {
197            code: "invalid syntax here @#$%".to_string(),
198            cell_id: "cell-2".to_string(),
199            session_id: None,
200        };
201        
202        let result = execute_code(Json(request)).await;
203        assert!(result.is_ok());
204        
205        let response = result.unwrap().0;
206        assert_eq!(response.success, false);
207        assert!(response.result.is_none());
208        assert!(response.error.is_some());
209        assert!(response.error.unwrap().contains("Parse error"));
210    }
211}
212
213} // Close native_api module
214
215#[cfg(feature = "native")]
216pub use native_api::*;