Rewrite jwt-pitbul and boko-framework with modern Bux constructs

- jwt-pitbul: algebraic enums, Array<String>, methods on JwtAlg,
  string interpolation, structured Result/Option error handling.
- boko-framework: StringMap<String> for headers/query/path params,
  Request/Response methods, StringBuilder, for loops, algebraic enums.
- Update READMEs and bump versions to 0.2.0.
This commit is contained in:
2026-06-16 10:12:52 +03:00
parent aaeb01e518
commit 9c632ce389
7 changed files with 570 additions and 458 deletions
+199 -231
View File
@@ -1,28 +1,22 @@
// =============================================================================
// 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_StartsWith, String_Contains, String_Offset};
import Std::Task::{Task_Join, TaskHandle};
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};
extern func bux_alloc(size: uint) -> *void;
extern func bux_strlen(s: String) -> uint;
extern func bux_strcmp(a: String, b: String) -> int;
extern func bux_strstr(haystack: String, needle: String) -> String;
extern func bux_str_slice(s: String, start: uint, len: uint) -> String;
extern func bux_str_split_count(s: String, delim: String) -> uint;
extern func bux_str_split_part(s: String, delim: String, index: uint) -> String;
extern func bux_sb_new(initial_cap: uint) -> *void;
extern func bux_sb_append(sb: *void, s: String);
extern func bux_sb_append_int(sb: *void, n: int64);
extern func bux_sb_append_char(sb: *void, c: char8);
extern func bux_sb_build(sb: *void) -> String;
extern func bux_sb_free(sb: *void);
extern func bux_base64url_encode(data: String, len: int) -> String;
extern func bux_base64url_decode(data: String, len: int, outlen: *int) -> String;
// =============================================================================
// HTTP Methods
@@ -37,6 +31,28 @@ enum HttpVerb {
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
// =============================================================================
@@ -44,15 +60,36 @@ struct Request {
method: HttpVerb,
path: String,
body: String,
headerKeys: *String,
headerValues: *String,
headerCount: int,
queryKeys: *String,
queryValues: *String,
queryCount: int,
pathParamKeys: *String,
pathParamValues: *String,
pathParamCount: int,
headers: StringMap<String>,
query: StringMap<String>,
pathParams: StringMap<String>,
}
extend Request {
func GetHeader(self: Request, name: String) -> String {
if StringMap_Has<String>(&self.headers, name) {
return StringMap_Get<String>(&self.headers, name);
}
return "";
}
func GetQuery(self: Request, name: String) -> String {
if StringMap_Has<String>(&self.query, name) {
return StringMap_Get<String>(&self.query, name);
}
return "";
}
func HasQuery(self: Request, name: String) -> bool {
return StringMap_Has<String>(&self.query, name);
}
func GetPathParam(self: Request, name: String) -> String {
if StringMap_Has<String>(&self.pathParams, name) {
return StringMap_Get<String>(&self.pathParams, name);
}
return "";
}
}
// =============================================================================
@@ -65,63 +102,9 @@ struct Response {
extraHeaders: String,
}
// =============================================================================
// App — server instance
// =============================================================================
struct App {
port: int,
threadCount: int,
serverName: String,
}
// =============================================================================
// Request helpers
// =============================================================================
func Request_GetHeader(req: *Request, name: String) -> String {
var i: int = 0;
while i < req.headerCount {
if bux_strcmp(req.headerKeys[i], name) == 0 {
return req.headerValues[i];
}
i = i + 1;
}
return "";
}
func Request_GetQuery(req: *Request, name: String) -> String {
var i: int = 0;
while i < req.queryCount {
if bux_strcmp(req.queryKeys[i], name) == 0 {
return req.queryValues[i];
}
i = i + 1;
}
return "";
}
func Request_GetPathParam(req: *Request, name: String) -> String {
var i: int = 0;
while i < req.pathParamCount {
if bux_strcmp(req.pathParamKeys[i], name) == 0 {
return req.pathParamValues[i];
}
i = i + 1;
}
return "";
}
// =============================================================================
// Response constructors
// =============================================================================
// --- Constructors ---
func Response_New(status: int, contentType: String, body: String) -> Response {
var resp: Response;
resp.statusCode = status;
resp.contentType = contentType;
resp.body = body;
resp.extraHeaders = "";
return resp;
return Response { statusCode: status, contentType: contentType, body: body, extraHeaders: "" };
}
func Response_Ok(body: String) -> Response {
@@ -141,118 +124,143 @@ func Response_Text(text: String) -> Response {
}
func Response_Redirect(url: String) -> Response {
var resp: Response;
resp.statusCode = 302;
resp.contentType = "";
resp.body = "";
let sb: *void = bux_sb_new(256);
bux_sb_append(sb, "Location: ");
bux_sb_append(sb, url);
bux_sb_append(sb, "\r\n");
resp.extraHeaders = bux_sb_build(sb);
bux_sb_free(sb);
return resp;
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\"}");
return Response_New(404, "application/json; charset=utf-8", "{\"error\":\"not_found\"}");
}
func Response_Error(status: int, message: String) -> Response {
return Response_New(status, "application/json; charset=utf-8", message);
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: *void = bux_sb_new(2048);
let sb: StringBuilder = StringBuilder_New();
// Status line
bux_sb_append(sb, "HTTP/1.1 ");
bux_sb_append_int(sb, resp.statusCode as int64);
bux_sb_append(sb, " ");
if resp.statusCode == 200 { bux_sb_append(sb, "OK"); }
else if resp.statusCode == 201 { bux_sb_append(sb, "Created"); }
else if resp.statusCode == 204 { bux_sb_append(sb, "No Content"); }
else if resp.statusCode == 302 { bux_sb_append(sb, "Found"); }
else if resp.statusCode == 400 { bux_sb_append(sb, "Bad Request"); }
else if resp.statusCode == 404 { bux_sb_append(sb, "Not Found"); }
else if resp.statusCode == 500 { bux_sb_append(sb, "Internal Server Error"); }
else { bux_sb_append(sb, "OK"); }
bux_sb_append(sb, "\r\n");
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
bux_sb_append(sb, "Server: Boko/0.1.0 (Bux)\r\n");
StringBuilder_Append(&sb, "Server: Boko/0.2.0 (Bux)\r\n");
// Extra headers (redirects, CORS, etc.)
if bux_strlen(resp.extraHeaders) > 0 {
bux_sb_append(sb, resp.extraHeaders);
// Extra headers
if String_Len(resp.extraHeaders) > 0 {
StringBuilder_Append(&sb, resp.extraHeaders);
}
// Content-Type
if bux_strlen(resp.contentType) > 0 {
bux_sb_append(sb, "Content-Type: ");
bux_sb_append(sb, resp.contentType);
bux_sb_append(sb, "\r\n");
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 = bux_strlen(resp.body);
bux_sb_append(sb, "Content-Length: ");
bux_sb_append_int(sb, bodyLen as int64);
bux_sb_append(sb, "\r\n");
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
bux_sb_append(sb, "Connection: close\r\n");
bux_sb_append(sb, "\r\n");
StringBuilder_Append(&sb, "Connection: close\r\n");
StringBuilder_Append(&sb, "\r\n");
if bodyLen > 0 {
bux_sb_append(sb, resp.body);
StringBuilder_Append(&sb, resp.body);
}
let result: String = bux_sb_build(sb);
bux_sb_free(sb);
let result: String = StringBuilder_Build(&sb);
StringBuilder_Free(&sb);
return result;
}
// =============================================================================
// Parse query string: ?key=val&key2=val2 → key-value arrays
// Parse query string: ?key=val&key2=val2 → generic StringMap
// =============================================================================
func Query_Parse(req: *Request, queryString: String) {
if bux_strlen(queryString) == 0 { return; }
let pairCount: uint = bux_str_split_count(queryString, "&");
let cap: int = pairCount as int + 1;
req.queryKeys = bux_alloc(cap as uint * 8) as *String;
req.queryValues = bux_alloc(cap as uint * 8) as *String;
req.queryCount = 0;
func Query_Parse(queryString: String) -> StringMap<String> {
let map: StringMap<String> = StringMap_New<String>(8);
if String_Len(queryString) == 0 {
return map;
}
let pairCount: uint = String_SplitCount(queryString, "&");
var i: uint = 0;
while i < pairCount {
let pair: String = bux_str_split_part(queryString, "&", i);
let eqPos: String = bux_strstr(pair, "=");
let pair: String = String_SplitPart(queryString, "&", i);
let eqPos: String = String_Find(pair, "=");
var key: String = pair;
var value: String = "";
if String_Len(eqPos) > 0 {
if !String_IsNull(eqPos) {
let keyLen: uint = String_Offset(eqPos, pair);
key = bux_str_slice(pair, 0, keyLen);
key = String_Slice(pair, 0, keyLen);
let valStart: uint = keyLen + 1;
let pairLen: uint = bux_strlen(pair);
let pairLen: uint = String_Len(pair);
if valStart < pairLen {
value = bux_str_slice(pair, valStart, pairLen - valStart);
value = String_Slice(pair, valStart, pairLen - valStart);
}
}
req.queryKeys[req.queryCount] = key;
req.queryValues[req.queryCount] = value;
req.queryCount = req.queryCount + 1;
StringMap_Set<String>(&map, key, value);
i = i + 1;
}
return map;
}
// =============================================================================
@@ -263,50 +271,45 @@ func Request_Parse(raw: String) -> Request {
req.method = HttpVerb { tag: HttpVerb_GET };
req.path = "/";
req.body = "";
req.headerCount = 0;
req.queryCount = 0;
req.pathParamCount = 0;
req.headers = StringMap_New<String>(8);
req.query = StringMap_New<String>(4);
req.pathParams = StringMap_New<String>(4);
if String_Len(raw) == 0 { return req; }
let rawLen: uint = bux_strlen(raw);
if rawLen == 0 { return req; }
// Find header/body boundary
let boundary: String = bux_strstr(raw, "\r\n\r\n");
let boundary: String = String_Find(raw, "\r\n\r\n");
let rawLen: uint = String_Len(raw);
var headerLen: uint = rawLen;
if String_Len(boundary) > 0 {
if !String_IsNull(boundary) {
headerLen = String_Offset(boundary, raw);
let bodyStart: uint = headerLen + 4;
if bodyStart < rawLen {
req.body = bux_str_slice(raw, bodyStart, rawLen - bodyStart);
req.body = String_Slice(raw, bodyStart, rawLen - bodyStart);
}
}
if headerLen == 0 { return req; }
let headerBlock: String = bux_str_slice(raw, 0, headerLen);
let lineCount: uint = bux_str_split_count(headerBlock, "\r\n");
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 = bux_str_split_part(headerBlock, "\r\n", 0);
if bux_strlen(requestLine) > 0 {
let methodStr: String = bux_str_split_part(requestLine, " ", 0);
if String_Eq(methodStr, "GET") { req.method = HttpVerb { tag: HttpVerb_GET }; }
else if String_Eq(methodStr, "POST") { req.method = HttpVerb { tag: HttpVerb_POST }; }
else if String_Eq(methodStr, "PUT") { req.method = HttpVerb { tag: HttpVerb_PUT }; }
else if String_Eq(methodStr, "DELETE") { req.method = HttpVerb { tag: HttpVerb_DELETE }; }
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 = bux_str_split_part(requestLine, " ", 1);
// Split path and query string
let qmark: String = bux_strstr(fullPath, "?");
if String_Len(qmark) > 0 {
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 = bux_str_slice(fullPath, 0, pathLen);
req.path = String_Slice(fullPath, 0, pathLen);
let qsStart: uint = pathLen + 1;
let fullLen: uint = bux_strlen(fullPath);
let fullLen: uint = String_Len(fullPath);
if qsStart < fullLen {
let qs: String = bux_str_slice(fullPath, qsStart, fullLen - qsStart);
Query_Parse(&req, qs);
let qs: String = String_Slice(fullPath, qsStart, fullLen - qsStart);
req.query = Query_Parse(qs);
}
} else {
req.path = fullPath;
@@ -314,26 +317,20 @@ func Request_Parse(raw: String) -> Request {
}
// Parse headers
let cap: int = lineCount as int + 1;
req.headerKeys = bux_alloc(cap as uint * 8) as *String;
req.headerValues = bux_alloc(cap as uint * 8) as *String;
var i: uint = 1;
while i < lineCount {
let line: String = bux_str_split_part(headerBlock, "\r\n", i);
let colonPos: String = bux_strstr(line, ": ");
if String_Len(colonPos) > 0 {
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 = bux_str_slice(line, 0, keyLen);
let key: String = String_Slice(line, 0, keyLen);
let valStart: uint = keyLen + 2;
let lineLen: uint = bux_strlen(line);
let lineLen: uint = String_Len(line);
var value: String = "";
if valStart < lineLen {
value = bux_str_slice(line, valStart, lineLen - valStart);
value = String_Slice(line, valStart, lineLen - valStart);
}
req.headerKeys[req.headerCount] = key;
req.headerValues[req.headerCount] = value;
req.headerCount = req.headerCount + 1;
StringMap_Set<String>(&req.headers, key, value);
}
i = i + 1;
}
@@ -345,48 +342,23 @@ func Request_Parse(raw: String) -> Request {
// Path pattern matching: /users/{id} against /users/42 → extracts id=42
// =============================================================================
func Path_Match(pattern: String, path: String, req: *Request) -> bool {
// Split both by "/"
let patParts: uint = bux_str_split_count(pattern, "/");
let pathParts: uint = bux_str_split_count(path, "/");
let patParts: uint = String_SplitCount(pattern, "/");
let pathParts: uint = String_SplitCount(path, "/");
if patParts != pathParts { return false; }
// Count params to allocate
var paramCount: int = 0;
var pi: uint = 0;
while pi < patParts {
let patPart: String = bux_str_split_part(pattern, "/", pi);
if String_StartsWith(patPart, "{") && String_Contains(patPart, "}") {
paramCount = paramCount + 1;
}
pi = pi + 1;
}
req.pathParams = StringMap_New<String>(8);
let cap: int = paramCount + 1;
req.pathParamKeys = bux_alloc(cap as uint * 8) as *String;
req.pathParamValues = bux_alloc(cap as uint * 8) as *String;
req.pathParamCount = 0;
// Match and extract
var i: uint = 0;
while i < patParts {
let patPart: String = bux_str_split_part(pattern, "/", i);
let pathPart: String = bux_str_split_part(path, "/", i);
let patPart: String = String_SplitPart(pattern, "/", i);
let pathPart: String = String_SplitPart(path, "/", i);
if String_StartsWith(patPart, "{") && String_Contains(patPart, "}") {
// Extract param name from {name}
let braceOpen: uint = 1; // skip "{"
let braceClose: String = bux_strstr(patPart, "}");
var nameLen: uint = 0;
if String_Len(braceClose) > 0 {
nameLen = String_Offset(braceClose, patPart) - 1;
}
let name: String = bux_str_slice(patPart, braceOpen, nameLen);
req.pathParamKeys[req.pathParamCount] = name;
req.pathParamValues[req.pathParamCount] = pathPart;
req.pathParamCount = req.pathParamCount + 1;
let nameLen: uint = String_Len(patPart) - 2;
let name: String = String_Slice(patPart, 1, nameLen);
StringMap_Set<String>(&req.pathParams, name, pathPart);
} else {
// Literal match
if !String_Eq(patPart, pathPart) { return false; }
}
i = i + 1;
@@ -396,14 +368,16 @@ func Path_Match(pattern: String, path: String, req: *Request) -> bool {
}
// =============================================================================
// App constructor
// App — server instance
// =============================================================================
struct App {
port: int,
threadCount: int,
serverName: String,
}
func App_New(port: int, threadCount: int) -> App {
var app: App;
app.port = port;
app.threadCount = threadCount;
app.serverName = "Boko/0.1.0 (Bux)";
return app;
return App { port: port, threadCount: threadCount, serverName: "Boko/0.2.0 (Bux)" };
}
// =============================================================================
@@ -417,19 +391,13 @@ func Boko_Router(req: Request) -> Response;
func App_HandleConnection(clientFd: int) {
let raw: String = Net_Recv(clientFd, 8192);
if String_Len(raw) == 0 { return; }
if bux_strlen(raw) == 0 { return; }
let req: Request = Request_Parse(raw);
// Call user-defined dispatch (forward declaration — user must implement)
let resp: Response = Boko_Router(req);
// Log
if req.method.tag == HttpVerb_GET { Print("GET "); }
else if req.method.tag == HttpVerb_POST { Print("POST "); }
else if req.method.tag == HttpVerb_PUT { Print("PUT "); }
else if req.method.tag == HttpVerb_DELETE { Print("DELETE "); }
else { Print("? "); }
Print(HttpVerb_MethodName(req.method));
Print(" ");
Print(req.path);
Print(" → ");
PrintInt(resp.statusCode);
@@ -457,9 +425,9 @@ func App_Worker(serverFd: int) {
// =============================================================================
func App_Run(app: *App) {
PrintLine("╔══════════════════════════════════════╗");
PrintLine("║ Boko Framework v0.1.0 ║");
PrintLine("║ Boko Framework v0.2.0 ║");
PrintLine("║ Async web framework for Bux ║");
PrintLine("╚══════════════════════════════════════╝");
PrintLine("╚══════════════════════════════════════════════════════╝");
PrintLine("");
let fd: int = Net_Create();