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 <noreply@anthropic.com>
This commit is contained in:
2026-02-16 04:21:57 -05:00
parent 3a46299404
commit 204950357f
4 changed files with 438 additions and 1 deletions

161
stdlib/http.lux Normal file
View File

@@ -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<String>, patternParts: List<String>): 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<String> = {
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 =
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)
// }
// }

View File

@@ -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