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:
@@ -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 {}
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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 {}
|
||||||
|
|||||||
Reference in New Issue
Block a user