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>
This commit is contained in:
527
stdlib/http.lux
527
stdlib/http.lux
@@ -42,6 +42,10 @@ fn httpNotFound(body: String): { status: Int, body: String } =
|
||||
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
|
||||
// ============================================================
|
||||
@@ -84,6 +88,54 @@ fn getPathSegment(path: String, index: Int): Option<String> = {
|
||||
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
|
||||
// ============================================================
|
||||
@@ -130,32 +182,483 @@ fn jsonMessage(text: String): String =
|
||||
jsonObject(jsonString("message", text))
|
||||
|
||||
// ============================================================
|
||||
// Usage Example (copy into your file)
|
||||
// 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("Welcome!")
|
||||
// if method == "GET" && path == "/" then httpOk("Home")
|
||||
// else if method == "GET" && pathMatches(path, "/users/:id") then {
|
||||
// match getPathSegment(path, 1) {
|
||||
// let params = getPathParams(path, "/users/:id")
|
||||
// match getParam(params, "id") {
|
||||
// Some(id) => httpOk(jsonObject(jsonString("id", id))),
|
||||
// None => httpNotFound(jsonErrorMsg("User not found"))
|
||||
// }
|
||||
// }
|
||||
// else httpNotFound(jsonErrorMsg("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()
|
||||
// }
|
||||
//
|
||||
// fn main(): Unit with {Console, HttpServer} = {
|
||||
// HttpServer.listen(8080)
|
||||
// Console.print("Server running on port 8080")
|
||||
// serveLoop(5) // Handle 5 requests
|
||||
// }
|
||||
// For testing with a fixed number of requests:
|
||||
//
|
||||
// fn serveLoop(remaining: Int): Unit with {Console, HttpServer} = {
|
||||
// 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)
|
||||
// let resp = router(req.method, req.path, req.body, req.headers)
|
||||
// HttpServer.respond(resp.status, resp.body)
|
||||
// serveLoop(remaining - 1)
|
||||
// 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()
|
||||
// }
|
||||
|
||||
Reference in New Issue
Block a user