From cdc4f47272efa8c9629703c3a49a15e8a04b8ded Mon Sep 17 00:00:00 2001 From: Brandon Lucas Date: Fri, 13 Feb 2026 23:11:51 -0500 Subject: [PATCH] fix: fix REST API and HTTP server examples - Fix string interpolation issues: escape curly braces with \{ and \} since unescaped { triggers string interpolation - Fix effectful function invocation: use "let _ = run main() with {}" instead of bare "main()" calls - Use inline record types instead of type aliases (structural typing) - Fix flake.nix to show build progress instead of suppressing output Co-Authored-By: Claude Opus 4.5 --- examples/http_server.lux | 8 +-- flake.nix | 5 +- projects/rest-api/main.lux | 133 ++++++++++++++++--------------------- 3 files changed, 64 insertions(+), 82 deletions(-) diff --git a/examples/http_server.lux b/examples/http_server.lux index f9bca6c..1c09e3f 100644 --- a/examples/http_server.lux +++ b/examples/http_server.lux @@ -10,11 +10,7 @@ fn handleRequest(req: { method: String, path: String, body: String, headers: Lis match req.path { "/" => HttpServer.respond(200, "Welcome to Lux HTTP Server!"), "/hello" => HttpServer.respond(200, "Hello, World!"), - "/json" => HttpServer.respondWithHeaders( - 200, - "{\"message\": \"Hello from Lux!\"}", - [("Content-Type", "application/json")] - ), + "/json" => HttpServer.respondWithHeaders(200, "\{\"message\": \"Hello from Lux!\"\}", [("Content-Type", "application/json")]), "/echo" => HttpServer.respond(200, "You sent: " + req.body), _ => HttpServer.respond(404, "Not Found: " + req.path) } @@ -45,4 +41,4 @@ fn main(): Unit with {Console, HttpServer} = { } // Run main -main() +let output = run main() with {} diff --git a/flake.nix b/flake.nix index 99776d9..f5ef40b 100644 --- a/flake.nix +++ b/flake.nix @@ -31,7 +31,10 @@ shellHook = '' # Build and add lux to PATH - cargo build --release --quiet 2>/dev/null + if [ ! -f target/release/lux ] || [ Cargo.toml -nt target/release/lux ] || [ src/main.rs -nt target/release/lux ]; then + echo "Building lux..." + cargo build --release + fi export PATH="$PWD/target/release:$PATH" printf "\n" diff --git a/projects/rest-api/main.lux b/projects/rest-api/main.lux index a259efd..eec1ac3 100644 --- a/projects/rest-api/main.lux +++ b/projects/rest-api/main.lux @@ -2,78 +2,73 @@ // A simple in-memory task management API demonstrating: // - HttpServer effect for handling requests // - Pattern matching for routing -// - JSON parsing and serialization +// - JSON building // - Effect tracking for all side effects // -// Run with: cargo run -- projects/rest-api/main.lux +// Run with: lux 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 } +// Task type is: { id: Int, title: String, done: Bool } // API response wrapper type ApiResponse = | Success(String) - | Error(Int, String) + | NotFound(String) + | BadRequest(String) + | MethodNotAllowed(String) // ============================================================ -// Task Storage (simulated with State effect) +// JSON Helpers // ============================================================ -// In-memory task list (would use State effect in real app) -// For this demo, we'll use a simple approach +// Build a JSON object string from a task +fn taskToJson(task: { id: Int, title: String, done: Bool }): String = { + let doneStr = if task.done then "true" else "false" + let q = "\"" + "\{" + q + "id" + q + ":" + toString(task.id) + "," + q + "title" + q + ":" + q + task.title + q + "," + q + "done" + q + ":" + doneStr + "\}" +} -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)) +fn tasksToJson(tasks: List<{ id: Int, title: String, done: Bool }>): String = { + let items = List.map(tasks, fn(t: { id: Int, title: String, done: Bool }): String => taskToJson(t)) "[" + String.join(items, ",") + "]" } +fn errorJson(msg: String): String = { + let q = "\"" + "\{" + q + "error" + q + ":" + q + msg + q + "\}" +} + +fn messageJson(msg: String, version: String): String = { + let q = "\"" + "\{" + q + "message" + q + ":" + q + msg + q + "," + q + "version" + q + ":" + q + version + q + "\}" +} + +fn deletedJson(): String = { + let q = "\"" + "\{" + q + "deleted" + q + ":true\}" +} + // ============================================================ // 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), + "4" => Some(4), + "5" => Some(5), _ => None } }, @@ -86,26 +81,23 @@ fn extractId(path: String): Option = { // ============================================================ 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 } - ] + let task1 = { id: 1, title: "Learn Lux", done: true } + let task2 = { id: 2, title: "Build API", done: false } + let task3 = { id: 3, title: "Deploy app", done: false } + let tasks = [task1, task2, task3] 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\"}") + _ => NotFound(errorJson("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)) } @@ -114,16 +106,17 @@ 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\"}") + 3 => Success(taskToJson({ id: 3, title: "Deploy app", done: true })), + _ => NotFound(errorJson("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\"}") + 1 => Success(deletedJson()), + 2 => Success(deletedJson()), + 3 => Success(deletedJson()), + _ => NotFound(errorJson("Task not found")) } } @@ -132,44 +125,40 @@ fn handleDeleteTask(id: Int): ApiResponse = { // ============================================================ fn route(method: String, path: String, body: String): ApiResponse = { - // Route: GET /tasks if method == "GET" then { if path == "/tasks" then handleGetTasks() + else if path == "/" then Success(messageJson("Lux REST API", "1.0")) else if String.contains(path, "/tasks/") then { match extractId(path) { Some(id) => handleGetTask(id), - None => Error(400, "{\"error\":\"Invalid task ID\"}") + None => BadRequest(errorJson("Invalid task ID")) } } - else if path == "/" then Success("{\"message\":\"Lux REST API\",\"version\":\"1.0\"}") - else Error(404, "{\"error\":\"Not found\"}") + else NotFound(errorJson("Not found")) } - // Route: POST /tasks else if method == "POST" then { if path == "/tasks" then handleCreateTask(body) - else Error(404, "{\"error\":\"Not found\"}") + else NotFound(errorJson("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\"}") + None => BadRequest(errorJson("Invalid task ID")) } } - else Error(404, "{\"error\":\"Not found\"}") + else NotFound(errorJson("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\"}") + None => BadRequest(errorJson("Invalid task ID")) } } - else Error(404, "{\"error\":\"Not found\"}") + else NotFound(errorJson("Not found")) } - else Error(405, "{\"error\":\"Method not allowed\"}") + else MethodNotAllowed(errorJson("Method not allowed")) } // ============================================================ @@ -178,16 +167,13 @@ fn route(method: String, path: String, body: String): ApiResponse = { 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) - + let contentType = [("Content-Type", "application/json")] match response { - Success(json) => { - HttpServer.respondWithHeaders(200, json, [("Content-Type", "application/json")]) - }, - Error(status, json) => { - HttpServer.respondWithHeaders(status, json, [("Content-Type", "application/json")]) - } + Success(json) => HttpServer.respondWithHeaders(200, json, contentType), + NotFound(json) => HttpServer.respondWithHeaders(404, json, contentType), + BadRequest(json) => HttpServer.respondWithHeaders(400, json, contentType), + MethodNotAllowed(json) => HttpServer.respondWithHeaders(405, json, contentType) } } @@ -213,7 +199,6 @@ fn serveRequests(count: Int, maxRequests: Int): Unit with {Console, HttpServer} fn main(): Unit with {Console, HttpServer} = { let port = 8080 let maxRequests = 10 - Console.print("========================================") Console.print(" Lux REST API Demo") Console.print("========================================") @@ -234,12 +219,10 @@ fn main(): Unit with {Console, HttpServer} = { 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() +let output = run main() with {}