// REST API Demo Project // A simple in-memory task management API demonstrating: // - HttpServer effect for handling requests // - Pattern matching for routing // - JSON parsing and serialization // - Effect tracking for all side effects // // Run with: cargo run -- projects/rest-api/main.lux // Test with: // curl http://localhost:8080/tasks // curl -X POST -d '{"title":"Buy milk","done":false}' http://localhost:8080/tasks // curl http://localhost:8080/tasks/1 // curl -X PUT -d '{"title":"Buy milk","done":true}' http://localhost:8080/tasks/1 // curl -X DELETE http://localhost:8080/tasks/1 // ============================================================ // Data Types // ============================================================ // Task representation type Task = { id: Int, title: String, done: Bool } // API response wrapper type ApiResponse = | Success(String) | Error(Int, String) // ============================================================ // Task Storage (simulated with State effect) // ============================================================ // In-memory task list (would use State effect in real app) // For this demo, we'll use a simple approach fn taskToJson(task: Task): String = "{\"id\":" + toString(task.id) + ",\"title\":\"" + task.title + "\",\"done\":" + (if task.done then "true" else "false") + "}" fn tasksToJson(tasks: List): String = { let items = List.map(tasks, fn(t: Task): String => taskToJson(t)) "[" + String.join(items, ",") + "]" } // ============================================================ // Request Parsing // ============================================================ fn parseTaskFromBody(body: String): Option<{ title: String, done: Bool }> = { // Simple JSON parsing - in production use Json.parse let json = Json.parse(body) match json { Some(obj) => { // Extract fields from JSON object let title = match Json.get(obj, "title") { Some(t) => match t { _ => "Untitled" // Simplified }, None => "Untitled" } Some({ title: "Task", done: false }) // Simplified for demo }, None => None } } fn extractId(path: String): Option = { // Extract ID from path like "/tasks/123" let parts = String.split(path, "/") match List.get(parts, 2) { Some(idStr) => { // Simple string to int (would use proper parsing) match idStr { "1" => Some(1), "2" => Some(2), "3" => Some(3), _ => None } }, None => None } } // ============================================================ // Route Handlers // ============================================================ fn handleGetTasks(): ApiResponse = { let tasks = [ { id: 1, title: "Learn Lux", done: true }, { id: 2, title: "Build API", done: false }, { id: 3, title: "Deploy app", done: false } ] Success(tasksToJson(tasks)) } fn handleGetTask(id: Int): ApiResponse = { // Simulated task lookup match id { 1 => Success(taskToJson({ id: 1, title: "Learn Lux", done: true })), 2 => Success(taskToJson({ id: 2, title: "Build API", done: false })), 3 => Success(taskToJson({ id: 3, title: "Deploy app", done: false })), _ => Error(404, "{\"error\":\"Task not found\"}") } } fn handleCreateTask(body: String): ApiResponse = { // In a real app, would parse body and create task let newTask = { id: 4, title: "New Task", done: false } Success(taskToJson(newTask)) } fn handleUpdateTask(id: Int, body: String): ApiResponse = { match id { 1 => Success(taskToJson({ id: 1, title: "Learn Lux", done: true })), 2 => Success(taskToJson({ id: 2, title: "Build API", done: true })), _ => Error(404, "{\"error\":\"Task not found\"}") } } fn handleDeleteTask(id: Int): ApiResponse = { match id { 1 => Success("{\"deleted\":true}"), 2 => Success("{\"deleted\":true}"), 3 => Success("{\"deleted\":true}"), _ => Error(404, "{\"error\":\"Task not found\"}") } } // ============================================================ // Router // ============================================================ fn route(method: String, path: String, body: String): ApiResponse = { // Route: GET /tasks if method == "GET" then { if path == "/tasks" then handleGetTasks() else if String.contains(path, "/tasks/") then { match extractId(path) { Some(id) => handleGetTask(id), None => Error(400, "{\"error\":\"Invalid task ID\"}") } } else if path == "/" then Success("{\"message\":\"Lux REST API\",\"version\":\"1.0\"}") else Error(404, "{\"error\":\"Not found\"}") } // Route: POST /tasks else if method == "POST" then { if path == "/tasks" then handleCreateTask(body) else Error(404, "{\"error\":\"Not found\"}") } // Route: PUT /tasks/:id else if method == "PUT" then { if String.contains(path, "/tasks/") then { match extractId(path) { Some(id) => handleUpdateTask(id, body), None => Error(400, "{\"error\":\"Invalid task ID\"}") } } else Error(404, "{\"error\":\"Not found\"}") } // Route: DELETE /tasks/:id else if method == "DELETE" then { if String.contains(path, "/tasks/") then { match extractId(path) { Some(id) => handleDeleteTask(id), None => Error(400, "{\"error\":\"Invalid task ID\"}") } } else Error(404, "{\"error\":\"Not found\"}") } else Error(405, "{\"error\":\"Method not allowed\"}") } // ============================================================ // Request Handler // ============================================================ fn handleRequest(req: { method: String, path: String, body: String, headers: List<(String, String)> }): Unit with {Console, HttpServer} = { Console.print(req.method + " " + req.path) let response = route(req.method, req.path, req.body) match response { Success(json) => { HttpServer.respondWithHeaders(200, json, [("Content-Type", "application/json")]) }, Error(status, json) => { HttpServer.respondWithHeaders(status, json, [("Content-Type", "application/json")]) } } } // ============================================================ // Server Loop // ============================================================ fn serveRequests(count: Int, maxRequests: Int): Unit with {Console, HttpServer} = { if count >= maxRequests then { Console.print("Max requests reached, shutting down...") HttpServer.stop() } else { let req = HttpServer.accept() handleRequest(req) serveRequests(count + 1, maxRequests) } } // ============================================================ // Main Entry Point // ============================================================ fn main(): Unit with {Console, HttpServer} = { let port = 8080 let maxRequests = 10 Console.print("========================================") Console.print(" Lux REST API Demo") Console.print("========================================") Console.print("") Console.print("Starting server on port " + toString(port) + "...") Console.print("Will handle " + toString(maxRequests) + " requests then stop.") Console.print("") Console.print("Endpoints:") Console.print(" GET / - API info") Console.print(" GET /tasks - List all tasks") Console.print(" GET /tasks/:id - Get task by ID") Console.print(" POST /tasks - Create task") Console.print(" PUT /tasks/:id - Update task") Console.print(" DELETE /tasks/:id - Delete task") Console.print("") Console.print("Try:") Console.print(" curl http://localhost:8080/") Console.print(" curl http://localhost:8080/tasks") Console.print(" curl http://localhost:8080/tasks/1") Console.print("") HttpServer.listen(port) Console.print("Server listening!") Console.print("") serveRequests(0, maxRequests) } main()