From 204950357f780fc917ebcdf3dfab4c675c8d0526 Mon Sep 17 00:00:00 2001 From: Brandon Lucas Date: Mon, 16 Feb 2026 04:21:57 -0500 Subject: [PATCH] feat: add HTTP framework with routing and JSON helpers - Add stdlib/http.lux with: - Response builders (httpOk, httpNotFound, etc.) - Path pattern matching with parameter extraction - JSON construction helpers (jsonStr, jsonNum, jsonObj, etc.) - Add examples/http_api.lux demonstrating a complete REST API - Add examples/http_router.lux showing the routing pattern - Update stdlib/lib.lux to include http module The framework provides functional building blocks for web apps: - Route matching: pathMatches("/users/:id", path) - Path params: getPathSegment(path, 1) - Response building: httpOk(jsonObj(...)) Note: Due to current type system limitations with type aliases and function types, the framework uses inline types rather than abstract Request/Response types. Co-Authored-By: Claude Opus 4.5 --- examples/http_api.lux | 177 +++++++++++++++++++++++++++++++++++++++ examples/http_router.lux | 98 ++++++++++++++++++++++ stdlib/http.lux | 161 +++++++++++++++++++++++++++++++++++ stdlib/lib.lux | 3 +- 4 files changed, 438 insertions(+), 1 deletion(-) create mode 100644 examples/http_api.lux create mode 100644 examples/http_router.lux create mode 100644 stdlib/http.lux diff --git a/examples/http_api.lux b/examples/http_api.lux new file mode 100644 index 0000000..40f270d --- /dev/null +++ b/examples/http_api.lux @@ -0,0 +1,177 @@ +// HTTP API Example +// +// A complete REST API demonstrating: +// - Route matching with path parameters +// - Response builders +// - JSON construction +// +// Run with: lux examples/http_api.lux +// Test with: +// curl http://localhost:8080/ +// curl http://localhost:8080/users +// curl http://localhost:8080/users/42 + +// ============================================================ +// Response Helpers +// ============================================================ + +fn httpOk(body: String): { status: Int, body: String } = + { status: 200, body: body } + +fn httpCreated(body: String): { status: Int, body: String } = + { status: 201, body: body } + +fn httpNotFound(body: String): { status: Int, body: String } = + { status: 404, body: body } + +fn httpBadRequest(body: String): { status: Int, body: String } = + { status: 400, body: body } + +// ============================================================ +// JSON Helpers +// ============================================================ + +fn jsonEscape(s: String): String = + String.replace(String.replace(s, "\\", "\\\\"), "\"", "\\\"") + +fn jsonStr(key: String, value: String): String = + "\"" + jsonEscape(key) + "\":\"" + jsonEscape(value) + "\"" + +fn jsonNum(key: String, value: Int): String = + "\"" + jsonEscape(key) + "\":" + toString(value) + +fn jsonObj(content: String): String = + "{" + content + "}" + +fn jsonArr(content: String): String = + "[" + content + "]" + +fn jsonError(message: String): String = + jsonObj(jsonStr("error", message)) + +// ============================================================ +// Path Matching +// ============================================================ + +fn pathMatches(path: String, pattern: String): Bool = { + let pathParts = String.split(path, "/") + let patternParts = String.split(pattern, "/") + if List.length(pathParts) != List.length(patternParts) then false + else matchParts(pathParts, patternParts) +} + +fn matchParts(pathParts: List, patternParts: List): Bool = { + if List.length(pathParts) == 0 then true + else { + match List.head(pathParts) { + None => true, + Some(pathPart) => { + match List.head(patternParts) { + None => true, + Some(patternPart) => { + let isMatch = if String.startsWith(patternPart, ":") then true else pathPart == patternPart + if isMatch then { + let restPath = Option.getOrElse(List.tail(pathParts), []) + let restPattern = Option.getOrElse(List.tail(patternParts), []) + matchParts(restPath, restPattern) + } else false + } + } + } + } + } +} + +fn getPathSegment(path: String, index: Int): Option = { + let parts = String.split(path, "/") + List.get(parts, index + 1) +} + +// ============================================================ +// Handlers +// ============================================================ + +fn indexHandler(): { status: Int, body: String } = + httpOk(jsonObj(jsonStr("message", "Welcome to Lux HTTP API"))) + +fn healthHandler(): { status: Int, body: String } = + httpOk(jsonObj(jsonStr("status", "healthy"))) + +fn listUsersHandler(): { status: Int, body: String } = { + let user1 = jsonObj(jsonNum("id", 1) + "," + jsonStr("name", "Alice")) + let user2 = jsonObj(jsonNum("id", 2) + "," + jsonStr("name", "Bob")) + httpOk(jsonArr(user1 + "," + user2)) +} + +fn getUserHandler(path: String): { status: Int, body: String } = { + match getPathSegment(path, 1) { + Some(id) => { + let body = jsonObj(jsonStr("id", id) + "," + jsonStr("name", "User " + id)) + httpOk(body) + }, + None => httpNotFound(jsonError("User not found")) + } +} + +fn createUserHandler(body: String): { status: Int, body: String } = { + let newUser = jsonObj(jsonNum("id", 3) + "," + jsonStr("name", "New User")) + httpCreated(newUser) +} + +// ============================================================ +// Router +// ============================================================ + +fn router(method: String, path: String, body: String): { status: Int, body: String } = { + if method == "GET" && path == "/" then indexHandler() + else if method == "GET" && path == "/health" then healthHandler() + else if method == "GET" && path == "/users" then listUsersHandler() + else if method == "GET" && pathMatches(path, "/users/:id") then getUserHandler(path) + else if method == "POST" && path == "/users" then createUserHandler(body) + else httpNotFound(jsonError("Not found: " + path)) +} + +// ============================================================ +// Server +// ============================================================ + +fn serveLoop(remaining: Int): Unit with {Console, HttpServer} = { + if remaining <= 0 then { + Console.print("Max requests reached, stopping server.") + HttpServer.stop() + } else { + let req = HttpServer.accept() + Console.print(req.method + " " + req.path) + let resp = router(req.method, req.path, req.body) + HttpServer.respond(resp.status, resp.body) + serveLoop(remaining - 1) + } +} + +fn main(): Unit with {Console, HttpServer} = { + let port = 8080 + let maxRequests = 10 + Console.print("========================================") + Console.print(" Lux HTTP API Demo") + Console.print("========================================") + Console.print("") + Console.print("Endpoints:") + Console.print(" GET / - API info") + Console.print(" GET /health - Health check") + Console.print(" GET /users - List users") + Console.print(" GET /users/:id - Get user by ID") + Console.print(" POST /users - Create user") + Console.print("") + Console.print("Try:") + Console.print(" curl http://localhost:8080/") + Console.print(" curl http://localhost:8080/users") + Console.print(" curl http://localhost:8080/users/42") + Console.print(" curl -X POST http://localhost:8080/users") + Console.print("") + Console.print("Starting server on port " + toString(port) + "...") + HttpServer.listen(port) + Console.print("Server listening!") + serveLoop(maxRequests) +} + +let output = run main() with {} diff --git a/examples/http_router.lux b/examples/http_router.lux new file mode 100644 index 0000000..2c77718 --- /dev/null +++ b/examples/http_router.lux @@ -0,0 +1,98 @@ +// HTTP Router Example +// +// Demonstrates the HTTP helper library with: +// - Path pattern matching +// - Response builders +// - JSON helpers +// +// Run with: lux examples/http_router.lux +// Test with: +// curl http://localhost:8080/ +// curl http://localhost:8080/users +// curl http://localhost:8080/users/42 + +import stdlib/http + +// ============================================================ +// Route Handlers +// ============================================================ + +fn indexHandler(): { status: Int, body: String } = + httpOk("Welcome to Lux HTTP Framework!") + +fn listUsersHandler(): { status: Int, body: String } = { + let user1 = jsonObject(jsonJoin([jsonNumber("id", 1), jsonString("name", "Alice")])) + let user2 = jsonObject(jsonJoin([jsonNumber("id", 2), jsonString("name", "Bob")])) + let users = jsonArray(jsonJoin([user1, user2])) + httpOk(users) +} + +fn getUserHandler(path: String): { status: Int, body: String } = { + match getPathSegment(path, 1) { + Some(id) => { + let body = jsonObject(jsonJoin([jsonString("id", id), jsonString("name", "User " + id)])) + httpOk(body) + }, + None => httpNotFound(jsonErrorMsg("User ID required")) + } +} + +fn healthHandler(): { status: Int, body: String } = + httpOk(jsonObject(jsonString("status", "healthy"))) + +// ============================================================ +// Router +// ============================================================ + +fn router(method: String, path: String, body: String): { status: Int, body: String } = { + if method == "GET" && path == "/" then indexHandler() + else if method == "GET" && path == "/health" then healthHandler() + else if method == "GET" && path == "/users" then listUsersHandler() + else if method == "GET" && pathMatches(path, "/users/:id") then getUserHandler(path) + else httpNotFound(jsonErrorMsg("Not found: " + path)) +} + +// ============================================================ +// Server +// ============================================================ + +fn serveLoop(remaining: Int): Unit with {Console, HttpServer} = { + if remaining <= 0 then { + Console.print("Max requests reached, stopping server.") + HttpServer.stop() + } else { + let req = HttpServer.accept() + Console.print(req.method + " " + req.path) + let resp = router(req.method, req.path, req.body) + HttpServer.respond(resp.status, resp.body) + serveLoop(remaining - 1) + } +} + +fn main(): Unit with {Console, HttpServer} = { + let port = 8080 + let maxRequests = 10 + Console.print("========================================") + Console.print(" Lux HTTP Router Demo") + Console.print("========================================") + Console.print("") + Console.print("Endpoints:") + Console.print(" GET / - Welcome message") + Console.print(" GET /health - Health check") + Console.print(" GET /users - List all users") + Console.print(" GET /users/:id - Get user by ID") + Console.print("") + Console.print("Try:") + Console.print(" curl http://localhost:8080/") + Console.print(" curl http://localhost:8080/users") + Console.print(" curl http://localhost:8080/users/42") + Console.print("") + Console.print("Starting server on port " + toString(port) + "...") + Console.print("Will handle " + toString(maxRequests) + " requests then stop.") + Console.print("") + HttpServer.listen(port) + Console.print("Server listening!") + serveLoop(maxRequests) +} + +let output = run main() with {} diff --git a/stdlib/http.lux b/stdlib/http.lux new file mode 100644 index 0000000..4656e4a --- /dev/null +++ b/stdlib/http.lux @@ -0,0 +1,161 @@ +// HTTP Framework for Lux +// +// Provides helpers for building web applications. +// +// Note: Due to current type system limitations, this module provides +// helper functions rather than abstract types. Use the HttpServer effect +// directly for the server loop. + +// ============================================================ +// Response Builder Functions +// ============================================================ + +// Create a 200 OK response +fn httpOk(body: String): { status: Int, body: String } = + { status: 200, body: body } + +// Create a 201 Created response +fn httpCreated(body: String): { status: Int, body: String } = + { status: 201, body: body } + +// Create a 204 No Content response +fn httpNoContent(): { status: Int, body: String } = + { status: 204, body: "" } + +// Create a 400 Bad Request response +fn httpBadRequest(body: String): { status: Int, body: String } = + { status: 400, body: body } + +// Create a 401 Unauthorized response +fn httpUnauthorized(body: String): { status: Int, body: String } = + { status: 401, body: body } + +// Create a 403 Forbidden response +fn httpForbidden(body: String): { status: Int, body: String } = + { status: 403, body: body } + +// Create a 404 Not Found response +fn httpNotFound(body: String): { status: Int, body: String } = + { status: 404, body: body } + +// Create a 500 Server Error response +fn httpServerError(body: String): { status: Int, body: String } = + { status: 500, body: body } + +// ============================================================ +// Path Matching +// ============================================================ + +// Check if a path matches a pattern with wildcards +// Pattern "/users/:id" matches "/users/42" +fn pathMatches(path: String, pattern: String): Bool = { + let pathParts = String.split(path, "/") + let patternParts = String.split(pattern, "/") + if List.length(pathParts) != List.length(patternParts) then false + else matchPathParts(pathParts, patternParts) +} + +fn matchPathParts(pathParts: List, patternParts: List): Bool = { + if List.length(pathParts) == 0 then true + else { + match List.head(pathParts) { + None => true, + Some(pathPart) => { + match List.head(patternParts) { + None => true, + Some(patternPart) => { + let isMatch = if String.startsWith(patternPart, ":") then true else pathPart == patternPart + if isMatch then { + let restPath = Option.getOrElse(List.tail(pathParts), []) + let restPattern = Option.getOrElse(List.tail(patternParts), []) + matchPathParts(restPath, restPattern) + } else false + } + } + } + } + } +} + +// Extract a path segment by position (0-indexed, skipping leading empty segment) +// For path "/users/42", getPathSegment(path, 1) returns Some("42") +fn getPathSegment(path: String, index: Int): Option = { + let parts = String.split(path, "/") + List.get(parts, index + 1) +} + +// ============================================================ +// JSON Helpers +// ============================================================ + +// Escape a string for JSON (handles quotes and backslashes) +fn jsonEscape(s: String): String = { + String.replace(String.replace(s, "\\", "\\\\"), "\"", "\\\"") +} + +// Create a JSON string field: "key": "value" +fn jsonString(key: String, value: String): String = { + "\"" + jsonEscape(key) + "\":\"" + jsonEscape(value) + "\"" +} + +// Create a JSON number field: "key": 42 +fn jsonNumber(key: String, value: Int): String = { + "\"" + jsonEscape(key) + "\":" + toString(value) +} + +// Create a JSON boolean field: "key": true +fn jsonBool(key: String, value: Bool): String = { + let boolStr = if value then "true" else "false" + "\"" + jsonEscape(key) + "\":" + boolStr +} + +// Wrap content in JSON object braces +fn jsonObject(content: String): String = + "{" + content + "}" + +// Wrap content in JSON array brackets +fn jsonArray(content: String): String = + "[" + content + "]" + +// Join multiple JSON fields/items with commas +fn jsonJoin(items: List): String = + String.join(items, ",") + +// Create a JSON error object: {"error": "message"} +fn jsonErrorMsg(message: String): String = + jsonObject(jsonString("error", message)) + +// Create a JSON message object: {"message": "text"} +fn jsonMessage(text: String): String = + jsonObject(jsonString("message", text)) + +// ============================================================ +// Usage Example (copy into your file) +// ============================================================ +// +// fn router(method: String, path: String, body: String): { status: Int, body: String } = { +// if method == "GET" && path == "/" then httpOk("Welcome!") +// else if method == "GET" && pathMatches(path, "/users/:id") then { +// match getPathSegment(path, 1) { +// Some(id) => httpOk(jsonObject(jsonString("id", id))), +// None => httpNotFound(jsonErrorMsg("User not found")) +// } +// } +// else httpNotFound(jsonErrorMsg("Not found")) +// } +// +// fn main(): Unit with {Console, HttpServer} = { +// HttpServer.listen(8080) +// Console.print("Server running on port 8080") +// serveLoop(5) // Handle 5 requests +// } +// +// fn serveLoop(remaining: Int): Unit with {Console, HttpServer} = { +// if remaining <= 0 then HttpServer.stop() +// else { +// let req = HttpServer.accept() +// let resp = router(req.method, req.path, req.body) +// HttpServer.respond(resp.status, resp.body) +// serveLoop(remaining - 1) +// } +// } diff --git a/stdlib/lib.lux b/stdlib/lib.lux index a985563..7cce28e 100644 --- a/stdlib/lib.lux +++ b/stdlib/lib.lux @@ -3,6 +3,7 @@ // This module re-exports the core standard library modules. // Import with: import stdlib -// Re-export Html module +// Re-export core modules pub import html pub import browser +pub import http