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>
665 lines
23 KiB
Plaintext
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()
|
|
// }
|