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 <noreply@anthropic.com>
This commit is contained in:
2026-02-13 23:11:51 -05:00
parent 5edd8aea77
commit cdc4f47272
3 changed files with 64 additions and 82 deletions

View File

@@ -10,11 +10,7 @@ fn handleRequest(req: { method: String, path: String, body: String, headers: Lis
match req.path { match req.path {
"/" => HttpServer.respond(200, "Welcome to Lux HTTP Server!"), "/" => HttpServer.respond(200, "Welcome to Lux HTTP Server!"),
"/hello" => HttpServer.respond(200, "Hello, World!"), "/hello" => HttpServer.respond(200, "Hello, World!"),
"/json" => HttpServer.respondWithHeaders( "/json" => HttpServer.respondWithHeaders(200, "\{\"message\": \"Hello from Lux!\"\}", [("Content-Type", "application/json")]),
200,
"{\"message\": \"Hello from Lux!\"}",
[("Content-Type", "application/json")]
),
"/echo" => HttpServer.respond(200, "You sent: " + req.body), "/echo" => HttpServer.respond(200, "You sent: " + req.body),
_ => HttpServer.respond(404, "Not Found: " + req.path) _ => HttpServer.respond(404, "Not Found: " + req.path)
} }
@@ -45,4 +41,4 @@ fn main(): Unit with {Console, HttpServer} = {
} }
// Run main // Run main
main() let output = run main() with {}

View File

@@ -31,7 +31,10 @@
shellHook = '' shellHook = ''
# Build and add lux to PATH # 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" export PATH="$PWD/target/release:$PATH"
printf "\n" printf "\n"

View File

@@ -2,78 +2,73 @@
// A simple in-memory task management API demonstrating: // A simple in-memory task management API demonstrating:
// - HttpServer effect for handling requests // - HttpServer effect for handling requests
// - Pattern matching for routing // - Pattern matching for routing
// - JSON parsing and serialization // - JSON building
// - Effect tracking for all side effects // - 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: // Test with:
// curl http://localhost:8080/tasks // 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 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 // Data Types
// ============================================================ // ============================================================
// Task representation // Task type is: { id: Int, title: String, done: Bool }
type Task = { id: Int, title: String, done: Bool }
// API response wrapper // API response wrapper
type ApiResponse = type ApiResponse =
| Success(String) | 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) // Build a JSON object string from a task
// For this demo, we'll use a simple approach 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 = fn tasksToJson(tasks: List<{ id: Int, title: String, done: Bool }>): String = {
"{\"id\":" + toString(task.id) + let items = List.map(tasks, fn(t: { id: Int, title: String, done: Bool }): String => taskToJson(t))
",\"title\":\"" + task.title +
"\",\"done\":" + (if task.done then "true" else "false") + "}"
fn tasksToJson(tasks: List<Task>): String = {
let items = List.map(tasks, fn(t: Task): String => taskToJson(t))
"[" + String.join(items, ",") + "]" "[" + 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 // 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<Int> = { fn extractId(path: String): Option<Int> = {
// Extract ID from path like "/tasks/123" // Extract ID from path like "/tasks/123"
let parts = String.split(path, "/") let parts = String.split(path, "/")
match List.get(parts, 2) { match List.get(parts, 2) {
Some(idStr) => { Some(idStr) => {
// Simple string to int (would use proper parsing)
match idStr { match idStr {
"1" => Some(1), "1" => Some(1),
"2" => Some(2), "2" => Some(2),
"3" => Some(3), "3" => Some(3),
"4" => Some(4),
"5" => Some(5),
_ => None _ => None
} }
}, },
@@ -86,26 +81,23 @@ fn extractId(path: String): Option<Int> = {
// ============================================================ // ============================================================
fn handleGetTasks(): ApiResponse = { fn handleGetTasks(): ApiResponse = {
let tasks = [ let task1 = { id: 1, title: "Learn Lux", done: true }
{ id: 1, title: "Learn Lux", done: true }, let task2 = { id: 2, title: "Build API", done: false }
{ id: 2, title: "Build API", done: false }, let task3 = { id: 3, title: "Deploy app", done: false }
{ id: 3, title: "Deploy app", done: false } let tasks = [task1, task2, task3]
]
Success(tasksToJson(tasks)) Success(tasksToJson(tasks))
} }
fn handleGetTask(id: Int): ApiResponse = { fn handleGetTask(id: Int): ApiResponse = {
// Simulated task lookup
match id { match id {
1 => Success(taskToJson({ id: 1, title: "Learn Lux", done: true })), 1 => Success(taskToJson({ id: 1, title: "Learn Lux", done: true })),
2 => Success(taskToJson({ id: 2, title: "Build API", done: false })), 2 => Success(taskToJson({ id: 2, title: "Build API", done: false })),
3 => Success(taskToJson({ id: 3, title: "Deploy app", 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 = { fn handleCreateTask(body: String): ApiResponse = {
// In a real app, would parse body and create task
let newTask = { id: 4, title: "New Task", done: false } let newTask = { id: 4, title: "New Task", done: false }
Success(taskToJson(newTask)) Success(taskToJson(newTask))
} }
@@ -114,16 +106,17 @@ fn handleUpdateTask(id: Int, body: String): ApiResponse = {
match id { match id {
1 => Success(taskToJson({ id: 1, title: "Learn Lux", done: true })), 1 => Success(taskToJson({ id: 1, title: "Learn Lux", done: true })),
2 => Success(taskToJson({ id: 2, title: "Build API", 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 = { fn handleDeleteTask(id: Int): ApiResponse = {
match id { match id {
1 => Success("{\"deleted\":true}"), 1 => Success(deletedJson()),
2 => Success("{\"deleted\":true}"), 2 => Success(deletedJson()),
3 => Success("{\"deleted\":true}"), 3 => Success(deletedJson()),
_ => Error(404, "{\"error\":\"Task not found\"}") _ => NotFound(errorJson("Task not found"))
} }
} }
@@ -132,44 +125,40 @@ fn handleDeleteTask(id: Int): ApiResponse = {
// ============================================================ // ============================================================
fn route(method: String, path: String, body: String): ApiResponse = { fn route(method: String, path: String, body: String): ApiResponse = {
// Route: GET /tasks
if method == "GET" then { if method == "GET" then {
if path == "/tasks" then handleGetTasks() if path == "/tasks" then handleGetTasks()
else if path == "/" then Success(messageJson("Lux REST API", "1.0"))
else if String.contains(path, "/tasks/") then { else if String.contains(path, "/tasks/") then {
match extractId(path) { match extractId(path) {
Some(id) => handleGetTask(id), 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 NotFound(errorJson("Not found"))
else Error(404, "{\"error\":\"Not found\"}")
} }
// Route: POST /tasks
else if method == "POST" then { else if method == "POST" then {
if path == "/tasks" then handleCreateTask(body) 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 { else if method == "PUT" then {
if String.contains(path, "/tasks/") then { if String.contains(path, "/tasks/") then {
match extractId(path) { match extractId(path) {
Some(id) => handleUpdateTask(id, body), 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 { else if method == "DELETE" then {
if String.contains(path, "/tasks/") then { if String.contains(path, "/tasks/") then {
match extractId(path) { match extractId(path) {
Some(id) => handleDeleteTask(id), 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} = { fn handleRequest(req: { method: String, path: String, body: String, headers: List<(String, String)> }): Unit with {Console, HttpServer} = {
Console.print(req.method + " " + req.path) Console.print(req.method + " " + req.path)
let response = route(req.method, req.path, req.body) let response = route(req.method, req.path, req.body)
let contentType = [("Content-Type", "application/json")]
match response { match response {
Success(json) => { Success(json) => HttpServer.respondWithHeaders(200, json, contentType),
HttpServer.respondWithHeaders(200, json, [("Content-Type", "application/json")]) NotFound(json) => HttpServer.respondWithHeaders(404, json, contentType),
}, BadRequest(json) => HttpServer.respondWithHeaders(400, json, contentType),
Error(status, json) => { MethodNotAllowed(json) => HttpServer.respondWithHeaders(405, json, contentType)
HttpServer.respondWithHeaders(status, json, [("Content-Type", "application/json")])
}
} }
} }
@@ -213,7 +199,6 @@ fn serveRequests(count: Int, maxRequests: Int): Unit with {Console, HttpServer}
fn main(): Unit with {Console, HttpServer} = { fn main(): Unit with {Console, HttpServer} = {
let port = 8080 let port = 8080
let maxRequests = 10 let maxRequests = 10
Console.print("========================================") Console.print("========================================")
Console.print(" Lux REST API Demo") Console.print(" Lux REST API Demo")
Console.print("========================================") 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")
Console.print(" curl http://localhost:8080/tasks/1") Console.print(" curl http://localhost:8080/tasks/1")
Console.print("") Console.print("")
HttpServer.listen(port) HttpServer.listen(port)
Console.print("Server listening!") Console.print("Server listening!")
Console.print("") Console.print("")
serveRequests(0, maxRequests) serveRequests(0, maxRequests)
} }
main() let output = run main() with {}