// 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, 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) } // 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, patternParts: List, 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 = { 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.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 = { let lowerName = String.toLower(name) getHeaderHelper(headers, lowerName) } fn getHeaderHelper(headers: List<(String, String)>, lowerName: String): Option = { 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 = 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 = { 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, path: Option, 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): Option<(Route, List<(String, String)>)> = { matchRouteHelper(req, routes) } fn matchRouteHelper(req: Request, routes: List): 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, notFound: fn(Request): Response): Handler = fn(req: Request): Response => { match matchRoute(req, routes) { Some((route, _params)) => route.handler(req), None => notFound(req) } } // ============================================================ // Static File Serving // ============================================================ // Serve a static file from disk pub fn serveStaticFile(basePath: String, requestPath: String): Response with {File} = { let filePath = basePath + requestPath if File.exists(filePath) then { let content = File.read(filePath) let mime = getMimeType(filePath) { status: 200, headers: [("Content-Type", mime)], body: content } } else { status: 404, headers: textHeaders(), body: "Not Found" } } // ============================================================ // Form Body Parsing // ============================================================ // Parse URL-encoded form body (same format as query strings) pub fn parseFormBody(body: String): List<(String, String)> = parseQueryParams(body) // Get a form field value by name pub fn getFormField(fields: List<(String, String)>, name: String): Option = getParam(fields, name) // ============================================================ // Response Helpers // ============================================================ // Send a Response using HttpServer effect (convenience wrapper) pub fn sendResponse(resp: Response): Unit with {HttpServer} = HttpServer.respondWithHeaders(resp.status, resp.body, resp.headers) // ============================================================ // 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() // }