// ============================================================================= // Boko — Async Web Framework for Bux (inspired by FastAPI) // Rewritten with modern Bux: methods on structs, generic StringMap, for-in // loops, algebraic enums, StringBuilder, and string interpolation. // ============================================================================= module Boko { import Std::Io::{PrintLine, Print, PrintInt}; import Std::Net::{Net_Create, Net_SetReuse, Net_Bind, Net_Listen, Net_Accept, Net_Send, Net_Recv, Net_Close, Net_LastError}; import Std::String::{ String_Len, String_Eq, String_IsNull, String_StartsWith, String_Contains, String_Find, String_Offset, String_Slice, String_SplitCount, String_SplitPart, StringBuilder, StringBuilder_New, StringBuilder_Append, StringBuilder_AppendInt, StringBuilder_Build, StringBuilder_Free }; import Std::Map::{StringMap, StringMap_New, StringMap_Set, StringMap_Get, StringMap_Has}; // ============================================================================= // HTTP Methods // ============================================================================= enum HttpVerb { GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, } func HttpVerb_MethodName(verb: HttpVerb) -> String { if verb.tag == HttpVerb_GET { return "GET"; } if verb.tag == HttpVerb_POST { return "POST"; } if verb.tag == HttpVerb_PUT { return "PUT"; } if verb.tag == HttpVerb_DELETE { return "DELETE"; } if verb.tag == HttpVerb_PATCH { return "PATCH"; } if verb.tag == HttpVerb_HEAD { return "HEAD"; } if verb.tag == HttpVerb_OPTIONS { return "OPTIONS"; } return "?"; } func HttpVerb_Parse(methodStr: String) -> HttpVerb { if String_Eq(methodStr, "GET") { return HttpVerb { tag: HttpVerb_GET }; } if String_Eq(methodStr, "POST") { return HttpVerb { tag: HttpVerb_POST }; } if String_Eq(methodStr, "PUT") { return HttpVerb { tag: HttpVerb_PUT }; } if String_Eq(methodStr, "DELETE") { return HttpVerb { tag: HttpVerb_DELETE }; } if String_Eq(methodStr, "PATCH") { return HttpVerb { tag: HttpVerb_PATCH }; } if String_Eq(methodStr, "HEAD") { return HttpVerb { tag: HttpVerb_HEAD }; } if String_Eq(methodStr, "OPTIONS") { return HttpVerb { tag: HttpVerb_OPTIONS }; } return HttpVerb { tag: HttpVerb_GET }; } // ============================================================================= // Request — parsed incoming HTTP request // ============================================================================= struct Request { method: HttpVerb, path: String, body: String, headers: StringMap, query: StringMap, pathParams: StringMap, } extend Request { func GetHeader(self: Request, name: String) -> String { if StringMap_Has(&self.headers, name) { return StringMap_Get(&self.headers, name); } return ""; } func GetQuery(self: Request, name: String) -> String { if StringMap_Has(&self.query, name) { return StringMap_Get(&self.query, name); } return ""; } func HasQuery(self: Request, name: String) -> bool { return StringMap_Has(&self.query, name); } func GetPathParam(self: Request, name: String) -> String { if StringMap_Has(&self.pathParams, name) { return StringMap_Get(&self.pathParams, name); } return ""; } } // ============================================================================= // Response — outgoing HTTP response // ============================================================================= struct Response { statusCode: int, contentType: String, body: String, extraHeaders: String, } // --- Constructors --- func Response_New(status: int, contentType: String, body: String) -> Response { return Response { statusCode: status, contentType: contentType, body: body, extraHeaders: "" }; } func Response_Ok(body: String) -> Response { return Response_New(200, "text/html; charset=utf-8", body); } func Response_Html(html: String) -> Response { return Response_New(200, "text/html; charset=utf-8", html); } func Response_Json(json: String) -> Response { return Response_New(200, "application/json; charset=utf-8", json); } func Response_Text(text: String) -> Response { return Response_New(200, "text/plain; charset=utf-8", text); } func Response_Redirect(url: String) -> Response { let sb: StringBuilder = StringBuilder_New(); StringBuilder_Append(&sb, "Location: "); StringBuilder_Append(&sb, url); StringBuilder_Append(&sb, "\r\n"); let headers: String = StringBuilder_Build(&sb); StringBuilder_Free(&sb); return Response { statusCode: 302, contentType: "", body: "", extraHeaders: headers }; } func Response_NotFound() -> Response { return Response_New(404, "application/json; charset=utf-8", "{\"error\":\"not_found\"}"); } func Response_Error(status: int, message: String) -> Response { let sb: StringBuilder = StringBuilder_New(); StringBuilder_Append(&sb, "{\"error\":\""); StringBuilder_Append(&sb, message); StringBuilder_Append(&sb, "\"}"); let body: String = StringBuilder_Build(&sb); StringBuilder_Free(&sb); return Response_New(status, "application/json; charset=utf-8", body); } func Response_NoContent() -> Response { return Response_New(204, "", ""); } // --- Instance helpers --- extend Response { func WithHeader(self: Response, key: String, value: String) -> Response { let sb: StringBuilder = StringBuilder_New(); StringBuilder_Append(&sb, self.extraHeaders); StringBuilder_Append(&sb, key); StringBuilder_Append(&sb, ": "); StringBuilder_Append(&sb, value); StringBuilder_Append(&sb, "\r\n"); self.extraHeaders = StringBuilder_Build(&sb); StringBuilder_Free(&sb); return self; } } // ============================================================================= // Status phrase lookup // ============================================================================= func StatusPhrase(code: int) -> String { if code == 200 { return "OK"; } if code == 201 { return "Created"; } if code == 204 { return "No Content"; } if code == 301 { return "Moved Permanently"; } if code == 302 { return "Found"; } if code == 400 { return "Bad Request"; } if code == 401 { return "Unauthorized"; } if code == 403 { return "Forbidden"; } if code == 404 { return "Not Found"; } if code == 500 { return "Internal Server Error"; } if code == 502 { return "Bad Gateway"; } if code == 503 { return "Service Unavailable"; } return "OK"; } // ============================================================================= // Build HTTP response string from Response struct // ============================================================================= func Response_Build(resp: Response) -> String { let sb: StringBuilder = StringBuilder_New(); // Status line StringBuilder_Append(&sb, "HTTP/1.1 "); StringBuilder_AppendInt(&sb, resp.statusCode as int64); StringBuilder_Append(&sb, " "); StringBuilder_Append(&sb, StatusPhrase(resp.statusCode)); StringBuilder_Append(&sb, "\r\n"); // Server StringBuilder_Append(&sb, "Server: Boko/0.2.0 (Bux)\r\n"); // Extra headers if String_Len(resp.extraHeaders) > 0 { StringBuilder_Append(&sb, resp.extraHeaders); } // Content-Type if String_Len(resp.contentType) > 0 { StringBuilder_Append(&sb, "Content-Type: "); StringBuilder_Append(&sb, resp.contentType); StringBuilder_Append(&sb, "\r\n"); } // Content-Length let bodyLen: uint = String_Len(resp.body); StringBuilder_Append(&sb, "Content-Length: "); StringBuilder_AppendInt(&sb, bodyLen as int64); StringBuilder_Append(&sb, "\r\n"); // Connection close StringBuilder_Append(&sb, "Connection: close\r\n"); StringBuilder_Append(&sb, "\r\n"); if bodyLen > 0 { StringBuilder_Append(&sb, resp.body); } let result: String = StringBuilder_Build(&sb); StringBuilder_Free(&sb); return result; } // ============================================================================= // Parse query string: ?key=val&key2=val2 → generic StringMap // ============================================================================= func Query_Parse(queryString: String) -> StringMap { let map: StringMap = StringMap_New(8); if String_Len(queryString) == 0 { return map; } let pairCount: uint = String_SplitCount(queryString, "&"); for i in 0..pairCount { let pair: String = String_SplitPart(queryString, "&", i); let eqPos: String = String_Find(pair, "="); var key: String = pair; var value: String = ""; if !String_IsNull(eqPos) { let keyLen: uint = String_Offset(eqPos, pair); key = String_Slice(pair, 0, keyLen); let valStart: uint = keyLen + 1; let pairLen: uint = String_Len(pair); if valStart < pairLen { value = String_Slice(pair, valStart, pairLen - valStart); } } StringMap_Set(&map, key, value); } return map; } // ============================================================================= // Parse incoming HTTP request from raw bytes // ============================================================================= func Request_Parse(raw: String) -> Request { var req: Request; req.method = HttpVerb { tag: HttpVerb_GET }; req.path = "/"; req.body = ""; req.headers = StringMap_New(8); req.query = StringMap_New(4); req.pathParams = StringMap_New(4); if String_Len(raw) == 0 { return req; } // Find header/body boundary let boundary: String = String_Find(raw, "\r\n\r\n"); let rawLen: uint = String_Len(raw); var headerLen: uint = rawLen; if !String_IsNull(boundary) { headerLen = String_Offset(boundary, raw); let bodyStart: uint = headerLen + 4; if bodyStart < rawLen { req.body = String_Slice(raw, bodyStart, rawLen - bodyStart); } } if headerLen == 0 { return req; } let headerBlock: String = String_Slice(raw, 0, headerLen); let lineCount: uint = String_SplitCount(headerBlock, "\r\n"); if lineCount == 0 { return req; } // Parse request line let requestLine: String = String_SplitPart(headerBlock, "\r\n", 0); if String_Len(requestLine) > 0 { let methodStr: String = String_SplitPart(requestLine, " ", 0); req.method = HttpVerb_Parse(methodStr); let fullPath: String = String_SplitPart(requestLine, " ", 1); let qmark: String = String_Find(fullPath, "?"); if !String_IsNull(qmark) { let pathLen: uint = String_Offset(qmark, fullPath); req.path = String_Slice(fullPath, 0, pathLen); let qsStart: uint = pathLen + 1; let fullLen: uint = String_Len(fullPath); if qsStart < fullLen { let qs: String = String_Slice(fullPath, qsStart, fullLen - qsStart); req.query = Query_Parse(qs); } } else { req.path = fullPath; } } // Parse headers for i in 1..lineCount { let line: String = String_SplitPart(headerBlock, "\r\n", i); let colonPos: String = String_Find(line, ": "); if !String_IsNull(colonPos) { let keyLen: uint = String_Offset(colonPos, line); let key: String = String_Slice(line, 0, keyLen); let valStart: uint = keyLen + 2; let lineLen: uint = String_Len(line); var value: String = ""; if valStart < lineLen { value = String_Slice(line, valStart, lineLen - valStart); } StringMap_Set(&req.headers, key, value); } } return req; } // ============================================================================= // Path pattern matching: /users/{id} against /users/42 → extracts id=42 // ============================================================================= func Path_Match(pattern: String, path: String, req: *Request) -> bool { let patParts: uint = String_SplitCount(pattern, "/"); let pathParts: uint = String_SplitCount(path, "/"); if patParts != pathParts { return false; } req.pathParams = StringMap_New(8); for i in 0..patParts { let patPart: String = String_SplitPart(pattern, "/", i); let pathPart: String = String_SplitPart(path, "/", i); if String_StartsWith(patPart, "{") && String_Contains(patPart, "}") { let nameLen: uint = String_Len(patPart) - 2; let name: String = String_Slice(patPart, 1, nameLen); StringMap_Set(&req.pathParams, name, pathPart); } else { if !String_Eq(patPart, pathPart) { return false; } } } return true; } // ============================================================================= // App — server instance // ============================================================================= struct App { port: int, threadCount: int, serverName: String, } func App_New(port: int, threadCount: int) -> App { return App { port: port, threadCount: threadCount, serverName: "Boko/0.2.0 (Bux)" }; } // ============================================================================= // Forward declaration — user must implement this in their module // ============================================================================= func Boko_Router(req: Request) -> Response; // ============================================================================= // Handle a single connection: parse → dispatch → respond // ============================================================================= func App_HandleConnection(clientFd: int) { let raw: String = Net_Recv(clientFd, 8192); if String_Len(raw) == 0 { return; } let req: Request = Request_Parse(raw); let resp: Response = Boko_Router(req); // Log Print(HttpVerb_MethodName(req.method)); Print(" "); Print(req.path); Print(" → "); PrintInt(resp.statusCode); PrintLine(""); // Send let respStr: String = Response_Build(resp); Net_Send(clientFd, respStr); } // ============================================================================= // Worker thread: accept loop // ============================================================================= func App_Worker(serverFd: int) { while true { let clientFd: int = Net_Accept(serverFd); if clientFd < 0 { continue; } App_HandleConnection(clientFd); Net_Close(clientFd); } } // ============================================================================= // App_Run: start the server (blocking) // ============================================================================= func App_Run(app: *App) { PrintLine("╔══════════════════════════════════════╗"); PrintLine("║ Boko Framework v0.2.0 ║"); PrintLine("║ Async web framework for Bux ║"); PrintLine("╚══════════════════════════════════════════════════════╝"); PrintLine(""); let fd: int = Net_Create(); if fd < 0 { PrintLine("FATAL: socket() failed"); return; } Net_SetReuse(fd); if !Net_Bind(fd, "0.0.0.0", app.port) { Print("FATAL: bind(:"); PrintInt(app.port); Print(") failed: "); PrintLine(Net_LastError()); Net_Close(fd); return; } if !Net_Listen(fd, 128) { PrintLine("FATAL: listen() failed"); Net_Close(fd); return; } Print("✓ Boko running on http://0.0.0.0:"); PrintInt(app.port); PrintLine(""); Print("✓ Workers: "); PrintInt(app.threadCount); PrintLine(""); PrintLine(""); // Spawn workers for i in 0..(app.threadCount - 1) { spawn App_Worker(fd); } // Main thread is the last worker App_Worker(fd); } } // module Boko