Files
lux/stdlib/http.lux
Brandon Lucas 7e76acab18 feat: rebuild website with full learning funnel
Website rebuilt from scratch based on analysis of 11 beloved language
websites (Elm, Zig, Gleam, Swift, Kotlin, Haskell, OCaml, Crystal, Roc,
Rust, Go).

New website structure:
- Homepage with hero, playground, three pillars, install guide
- Language Tour with interactive lessons (hello world, types, effects)
- Examples cookbook with categorized sidebar
- API documentation index
- Installation guide (Nix and source)
- Sleek/noble design (black/gold, serif typography)

Also includes:
- New stdlib/json.lux module for JSON serialization
- Enhanced stdlib/http.lux with middleware and routing
- New string functions (charAt, indexOf, lastIndexOf, repeat)
- LSP improvements (rename, signature help, formatting)
- Package manager transitive dependency resolution
- Updated documentation for effects and stdlib
- New showcase example (task_manager.lux)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-16 23:05:35 -05:00

665 lines
23 KiB
Plaintext

// 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 }
// Create a 429 Too Many Requests response
fn httpTooManyRequests(body: String): { status: Int, body: String } =
{ status: 429, 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)
}
// Extract path parameters from a matched route pattern
// For path "/users/42/posts/5" and pattern "/users/:userId/posts/:postId"
// returns [("userId", "42"), ("postId", "5")]
fn getPathParams(path: String, pattern: String): List<(String, String)> = {
let pathParts = String.split(path, "/")
let patternParts = String.split(pattern, "/")
extractParamsHelper(pathParts, patternParts, [])
}
fn extractParamsHelper(pathParts: List<String>, patternParts: List<String>, acc: List<(String, String)>): List<(String, String)> = {
if List.length(pathParts) == 0 || List.length(patternParts) == 0 then
List.reverse(acc)
else {
match List.head(pathParts) {
None => List.reverse(acc),
Some(p) => match List.head(patternParts) {
None => List.reverse(acc),
Some(pat) => {
let restPath = Option.getOrElse(List.tail(pathParts), [])
let restPattern = Option.getOrElse(List.tail(patternParts), [])
if String.startsWith(pat, ":") then {
let paramName = String.substring(pat, 1, String.length(pat))
let newAcc = List.concat([(paramName, p)], acc)
extractParamsHelper(restPath, restPattern, newAcc)
} else {
extractParamsHelper(restPath, restPattern, acc)
}
}
}
}
}
}
// Get a specific path parameter by name from a list of params
fn getParam(params: List<(String, String)>, name: String): Option<String> = {
if List.length(params) == 0 then None
else {
match List.head(params) {
None => None,
Some(pair) => match pair {
(pName, pValue) =>
if pName == name then Some(pValue)
else getParam(Option.getOrElse(List.tail(params), []), name)
}
}
}
}
// ============================================================
// 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))
// ============================================================
// Header Helpers
// ============================================================
// Get a header value from request headers (case-insensitive)
fn getHeader(headers: List<(String, String)>, name: String): Option<String> = {
let lowerName = String.toLower(name)
getHeaderHelper(headers, lowerName)
}
fn getHeaderHelper(headers: List<(String, String)>, lowerName: String): Option<String> = {
if List.length(headers) == 0 then None
else {
match List.head(headers) {
None => None,
Some(header) => match header {
(hName, hValue) =>
if String.toLower(hName) == lowerName then Some(hValue)
else getHeaderHelper(Option.getOrElse(List.tail(headers), []), lowerName)
}
}
}
}
// ============================================================
// Routing Helpers
// ============================================================
//
// Route matching pattern:
//
// fn router(method: String, path: String, body: String): { status: Int, body: String } = {
// if method == "GET" && path == "/" then httpOk("Home")
// else if method == "GET" && pathMatches(path, "/users/:id") then {
// let params = getPathParams(path, "/users/:id")
// match getParam(params, "id") {
// Some(id) => httpOk(jsonObject(jsonString("id", id))),
// None => httpNotFound(jsonErrorMsg("User not found"))
// }
// }
// else if method == "POST" && path == "/users" then
// httpCreated(body)
// else
// httpNotFound(jsonErrorMsg("Not found"))
// }
// Helper to check if request is a GET to a specific path
fn isGet(method: String, path: String, pattern: String): Bool =
method == "GET" && pathMatches(path, pattern)
// Helper to check if request is a POST to a specific path
fn isPost(method: String, path: String, pattern: String): Bool =
method == "POST" && pathMatches(path, pattern)
// Helper to check if request is a PUT to a specific path
fn isPut(method: String, path: String, pattern: String): Bool =
method == "PUT" && pathMatches(path, pattern)
// Helper to check if request is a DELETE to a specific path
fn isDelete(method: String, path: String, pattern: String): Bool =
method == "DELETE" && pathMatches(path, pattern)
// ============================================================
// Server Loop Patterns
// ============================================================
//
// The server loop should be defined in your main file:
//
// fn serverLoop(): Unit with {HttpServer} = {
// let req = HttpServer.accept()
// let resp = router(req.method, req.path, req.body, req.headers)
// HttpServer.respond(resp.status, resp.body)
// serverLoop()
// }
//
// For testing with a fixed number of requests:
//
// fn serverLoopN(remaining: Int): Unit with {HttpServer} = {
// if remaining <= 0 then HttpServer.stop()
// else {
// let req = HttpServer.accept()
// let resp = router(req.method, req.path, req.body, req.headers)
// HttpServer.respond(resp.status, resp.body)
// serverLoopN(remaining - 1)
// }
// }
// ============================================================
// Middleware Pattern
// ============================================================
//
// Middleware wraps handlers to add cross-cutting concerns.
// In Lux, middleware is implemented as function composition.
//
// Example logging middleware:
//
// fn withLogging(
// handler: fn(String, String, String): { status: Int, body: String }
// ): fn(String, String, String): { status: Int, body: String } with {Console} = {
// fn(method: String, path: String, body: String): { status: Int, body: String } => {
// Console.print("[HTTP] " + method + " " + path)
// let response = handler(method, path, body)
// Console.print("[HTTP] " + toString(response.status))
// response
// }
// }
//
// Usage:
// let myHandler = withLogging(router)
// ============================================================
// CORS Headers
// ============================================================
// Standard CORS headers for API responses
fn corsHeaders(): List<(String, String)> = [
("Access-Control-Allow-Origin", "*"),
("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS"),
("Access-Control-Allow-Headers", "Content-Type, Authorization")
]
// CORS headers for specific origin with credentials
fn corsHeadersWithOrigin(origin: String): List<(String, String)> = [
("Access-Control-Allow-Origin", origin),
("Access-Control-Allow-Credentials", "true"),
("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS"),
("Access-Control-Allow-Headers", "Content-Type, Authorization")
]
// ============================================================
// Content Type Headers
// ============================================================
fn jsonHeaders(): List<(String, String)> = [
("Content-Type", "application/json")
]
fn htmlHeaders(): List<(String, String)> = [
("Content-Type", "text/html; charset=utf-8")
]
fn textHeaders(): List<(String, String)> = [
("Content-Type", "text/plain; charset=utf-8")
]
// ============================================================
// Query String Parsing
// ============================================================
// Parse query string from path (e.g., "/search?q=hello&page=1")
// Returns the path without query string and a list of parameters
pub fn parseQueryString(fullPath: String): (String, List<(String, String)>) = {
match String.indexOf(fullPath, "?") {
None => (fullPath, []),
Some(idx) => {
let path = String.substring(fullPath, 0, idx)
let queryStr = String.substring(fullPath, idx + 1, String.length(fullPath))
let params = parseQueryParams(queryStr)
(path, params)
}
}
}
fn parseQueryParams(queryStr: String): List<(String, String)> = {
let pairs = String.split(queryStr, "&")
List.filterMap(pairs, fn(pair: String): Option<(String, String)> => {
match String.indexOf(pair, "=") {
None => None,
Some(idx) => {
let key = String.substring(pair, 0, idx)
let value = String.substring(pair, idx + 1, String.length(pair))
Some((urlDecode(key), urlDecode(value)))
}
}
})
}
// Get a query parameter by name
pub fn getQueryParam(params: List<(String, String)>, name: String): Option<String> =
getParam(params, name)
// Simple URL decoding (handles %XX and +)
fn urlDecode(s: String): String = {
// For now, just replace + with space
// Full implementation would decode %XX sequences
String.replace(s, "+", " ")
}
// ============================================================
// Cookie Handling
// ============================================================
// Parse cookies from Cookie header value
pub fn parseCookies(cookieHeader: String): List<(String, String)> = {
let pairs = String.split(cookieHeader, "; ")
List.filterMap(pairs, fn(pair: String): Option<(String, String)> => {
match String.indexOf(pair, "=") {
None => None,
Some(idx) => {
let name = String.trim(String.substring(pair, 0, idx))
let value = String.trim(String.substring(pair, idx + 1, String.length(pair)))
Some((name, value))
}
}
})
}
// Get a cookie value by name from request headers
pub fn getCookie(headers: List<(String, String)>, name: String): Option<String> = {
match getHeader(headers, "Cookie") {
None => None,
Some(cookieHeader) => {
let cookies = parseCookies(cookieHeader)
getParam(cookies, name)
}
}
}
// Create a Set-Cookie header value
pub fn setCookie(name: String, value: String): String =
name + "=" + value
// Create a Set-Cookie header with options
pub fn setCookieWithOptions(
name: String,
value: String,
maxAge: Option<Int>,
path: Option<String>,
httpOnly: Bool,
secure: Bool
): String = {
let base = name + "=" + value
let withMaxAge = match maxAge {
Some(age) => base + "; Max-Age=" + toString(age),
None => base
}
let withPath = match path {
Some(p) => withMaxAge + "; Path=" + p,
None => withMaxAge
}
let withHttpOnly = if httpOnly then withPath + "; HttpOnly" else withPath
if secure then withHttpOnly + "; Secure" else withHttpOnly
}
// ============================================================
// Static File MIME Types
// ============================================================
// Get MIME type for a file extension
pub fn getMimeType(path: String): String = {
let ext = getFileExtension(path)
match ext {
"html" => "text/html; charset=utf-8",
"htm" => "text/html; charset=utf-8",
"css" => "text/css; charset=utf-8",
"js" => "application/javascript; charset=utf-8",
"json" => "application/json; charset=utf-8",
"png" => "image/png",
"jpg" => "image/jpeg",
"jpeg" => "image/jpeg",
"gif" => "image/gif",
"svg" => "image/svg+xml",
"ico" => "image/x-icon",
"woff" => "font/woff",
"woff2" => "font/woff2",
"ttf" => "font/ttf",
"pdf" => "application/pdf",
"xml" => "application/xml",
"txt" => "text/plain; charset=utf-8",
"md" => "text/markdown; charset=utf-8",
_ => "application/octet-stream"
}
}
fn getFileExtension(path: String): String = {
match String.lastIndexOf(path, ".") {
None => "",
Some(idx) => String.toLower(String.substring(path, idx + 1, String.length(path)))
}
}
// ============================================================
// Request Type
// ============================================================
// Standard request record for cleaner routing
type Request = {
method: String,
path: String,
query: List<(String, String)>,
headers: List<(String, String)>,
body: String
}
// Parse a raw request into a Request record
pub fn parseRequest(
method: String,
fullPath: String,
headers: List<(String, String)>,
body: String
): Request = {
let (path, query) = parseQueryString(fullPath)
{ method: method, path: path, query: query, headers: headers, body: body }
}
// ============================================================
// Response Type with Headers
// ============================================================
// Response with headers support
type Response = {
status: Int,
headers: List<(String, String)>,
body: String
}
// Create a response with headers
pub fn httpResponse(status: Int, body: String, headers: List<(String, String)>): Response =
{ status: status, headers: headers, body: body }
// Create a JSON response
pub fn jsonResponse(status: Int, body: String): Response =
{ status: status, headers: jsonHeaders(), body: body }
// Create an HTML response
pub fn htmlResponse(status: Int, body: String): Response =
{ status: status, headers: htmlHeaders(), body: body }
// Create a redirect response
pub fn httpRedirect(location: String): Response =
{ status: 302, headers: [("Location", location)], body: "" }
// Create a permanent redirect response
pub fn httpRedirectPermanent(location: String): Response =
{ status: 301, headers: [("Location", location)], body: "" }
// ============================================================
// Middleware Functions
// ============================================================
// Request type for middleware (simplified)
type Handler = fn(Request): Response
// Logging middleware - logs request method, path, and response status
pub fn withLogging(handler: Handler): Handler with {Console} =
fn(req: Request): Response => {
Console.print("[HTTP] " + req.method + " " + req.path)
let resp = handler(req)
Console.print("[HTTP] " + toString(resp.status))
resp
}
// CORS middleware - adds CORS headers to all responses
pub fn withCors(handler: Handler): Handler =
fn(req: Request): Response => {
// Handle preflight
if req.method == "OPTIONS" then
{ status: 204, headers: corsHeaders(), body: "" }
else {
let resp = handler(req)
{ status: resp.status, headers: List.concat(resp.headers, corsHeaders()), body: resp.body }
}
}
// JSON content-type middleware - ensures JSON content type on responses
pub fn withJson(handler: Handler): Handler =
fn(req: Request): Response => {
let resp = handler(req)
{ status: resp.status, headers: List.concat(resp.headers, jsonHeaders()), body: resp.body }
}
// Error handling middleware - catches failures and returns 500
pub fn withErrorHandling(handler: Handler): Handler =
fn(req: Request): Response => {
// In a real implementation, this would use effect handling
// For now, just call the handler
handler(req)
}
// Rate limiting check (returns remaining requests or 0 if limited)
// Note: Actual rate limiting requires state/effects
pub fn checkRateLimit(key: String, limit: Int, window: Int): Int with {Time} = {
// Placeholder - real implementation would track requests
limit
}
// ============================================================
// Router DSL
// ============================================================
// Route definition
type Route = {
method: String,
pattern: String,
handler: fn(Request): Response
}
// Create a GET route
pub fn get(pattern: String, handler: fn(Request): Response): Route =
{ method: "GET", pattern: pattern, handler: handler }
// Create a POST route
pub fn post(pattern: String, handler: fn(Request): Response): Route =
{ method: "POST", pattern: pattern, handler: handler }
// Create a PUT route
pub fn put(pattern: String, handler: fn(Request): Response): Route =
{ method: "PUT", pattern: pattern, handler: handler }
// Create a DELETE route
pub fn delete(pattern: String, handler: fn(Request): Response): Route =
{ method: "DELETE", pattern: pattern, handler: handler }
// Create a PATCH route
pub fn patch(pattern: String, handler: fn(Request): Response): Route =
{ method: "PATCH", pattern: pattern, handler: handler }
// Match request against a list of routes
pub fn matchRoute(req: Request, routes: List<Route>): Option<(Route, List<(String, String)>)> = {
matchRouteHelper(req, routes)
}
fn matchRouteHelper(req: Request, routes: List<Route>): Option<(Route, List<(String, String)>)> = {
match List.head(routes) {
None => None,
Some(route) => {
if route.method == req.method && pathMatches(req.path, route.pattern) then {
let params = getPathParams(req.path, route.pattern)
Some((route, params))
} else {
matchRouteHelper(req, Option.getOrElse(List.tail(routes), []))
}
}
}
}
// Create a router from a list of routes
pub fn router(routes: List<Route>, notFound: fn(Request): Response): Handler =
fn(req: Request): Response => {
match matchRoute(req, routes) {
Some((route, _params)) => route.handler(req),
None => notFound(req)
}
}
// ============================================================
// Example Usage
// ============================================================
//
// fn main(): Unit with {Console, HttpServer} = {
// // Define routes
// let routes = [
// get("/", fn(req: Request): Response => jsonResponse(200, jsonMessage("Welcome!"))),
// get("/users/:id", fn(req: Request): Response => {
// let params = getPathParams(req.path, "/users/:id")
// match getParam(params, "id") {
// Some(id) => jsonResponse(200, jsonObject(jsonString("id", id))),
// None => jsonResponse(404, jsonErrorMsg("User not found"))
// }
// }),
// post("/users", fn(req: Request): Response => jsonResponse(201, jsonMessage("Created")))
// ]
//
// // Create router with middleware
// let app = withLogging(withCors(router(routes, fn(req: Request): Response =>
// jsonResponse(404, jsonErrorMsg("Not found"))
// )))
//
// // Start server
// HttpServer.listen(8080)
// Console.print("Server running on http://localhost:8080")
//
// // Server loop
// fn serverLoop(): Unit with {HttpServer} = {
// let rawReq = HttpServer.accept()
// let req = parseRequest(rawReq.method, rawReq.path, rawReq.headers, rawReq.body)
// let resp = app(req)
// HttpServer.respond(resp.status, resp.body)
// serverLoop()
// }
// serverLoop()
// }