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
+19 -14
View File
@@ -4,12 +4,14 @@
Boko is a lightweight, multi-threaded web framework that brings FastAPI-style routing to the Bux programming language. It handles HTTP parsing, path pattern matching, query parameter extraction, and response building so you can focus on your application logic.
This version (0.2.0) is rewritten with modern Bux: `struct` methods, generic `StringMap<String>` for headers/query/path params, `for` loops, algebraic enums, `StringBuilder`, and string interpolation.
## Quick Start
```bash
cd apps/boko-framework
../../buxc build
./boko-framework
./build/boko-framework
```
Open `http://localhost:8080` — you'll see the demo landing page.
@@ -20,7 +22,7 @@ Boko follows a **single-dispatch-function** pattern. You define one function —
```bux
import Boko::{Request, Response, Response_Json, Response_Html, Response_NotFound,
Path_Match, Request_GetQuery, Request_GetPathParam,
Path_Match,
App, App_New, App_Run};
func Boko_Router(req: Request) -> Response {
@@ -36,14 +38,14 @@ func Boko_Router(req: Request) -> Response {
// Route: GET /users/{id} — path parameter
if Path_Match("/users/{id}", req.path, &req) {
let id: String = Request_GetPathParam(&req, "id");
let id: String = req.GetPathParam("id");
// Build JSON response with id
return Response_Json(...);
}
// Route: GET /search?q=... — query parameter
if String_Eq(req.path, "/search") {
let q: String = Request_GetQuery(&req, "q");
let q: String = req.GetQuery("q");
return Response_Json(...);
}
@@ -63,16 +65,19 @@ func Main() -> int {
| Field | Type | Description |
|-------|------|-------------|
| `method` | `HttpVerb` | GET, POST, PUT, DELETE |
| `method` | `HttpVerb` | GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS |
| `path` | `String` | Request path (without query string) |
| `body` | `String` | Request body (for POST/PUT) |
| `headerCount` | `int` | Number of headers |
| `headers` | `StringMap<String>` | Parsed request headers |
| `query` | `StringMap<String>` | Parsed query parameters |
| `pathParams` | `StringMap<String>` | Extracted path parameters |
| Function | Returns | Description |
|----------|---------|-------------|
| `Request_GetHeader(req, name)` | `String` | Get header value by name |
| `Request_GetQuery(req, name)` | `String` | Get query parameter by name |
| `Request_GetPathParam(req, name)` | `String` | Get extracted path parameter |
| Method | Returns | Description |
|--------|---------|-------------|
| `req.GetHeader(name)` | `String` | Get header value by name |
| `req.GetQuery(name)` | `String` | Get query parameter by name |
| `req.HasQuery(name)` | `bool` | Check if query parameter exists |
| `req.GetPathParam(name)` | `String` | Get extracted path parameter |
### Response
@@ -96,12 +101,12 @@ func Main() -> int {
// Extracts: id=42, postId=7
if Path_Match("/users/{id}/posts/{postId}", req.path, &req) {
let userId: String = Request_GetPathParam(&req, "id"); // "42"
let postId: String = Request_GetPathParam(&req, "postId"); // "7"
let userId: String = req.GetPathParam("id"); // "42"
let postId: String = req.GetPathParam("postId"); // "7"
}
```
The function returns `true` if the pattern matches and populates `req.pathParamKeys` / `req.pathParamValues`.
The function returns `true` if the pattern matches and populates `req.pathParams`.
### App
+1 -1
View File
@@ -1,6 +1,6 @@
[Package]
Name = "boko-framework"
Version = "0.1.0"
Version = "0.2.0"
Type = "bin"
Description = "Boko — async web framework for Bux, inspired by FastAPI"
+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();
+35 -43
View File
@@ -1,25 +1,22 @@
// =============================================================================
// Boko Example App — demonstrates the framework API
// Boko Example App — demonstrates the modern framework API
// =============================================================================
module Main {
import Std::Io::{PrintLine, Print};
import Std::String::{String_Eq};
import Std::String::{
String_Eq, String_Len,
StringBuilder, StringBuilder_New, StringBuilder_Append,
StringBuilder_Build, StringBuilder_Free
};
import Boko::{
App, App_New, App_Run,
Request, Response,
Request_GetQuery, Request_GetPathParam,
Response_Html, Response_Json, Response_NotFound, Response_Redirect,
Path_Match,
HttpVerb
};
extern func bux_strlen(s: String) -> uint;
extern func bux_sb_new(initial_cap: uint) -> *void;
extern func bux_sb_append(sb: *void, s: String);
extern func bux_sb_build(sb: *void) -> String;
extern func bux_sb_free(sb: *void);
// =============================================================================
// Boko_Router — user-defined dispatch (called by the framework)
// =============================================================================
@@ -31,7 +28,7 @@ func Boko_Router(req: Request) -> Response {
// --- GET /api/health ---
if String_Eq(req.path, "/api/health") {
return Response_Json("{\"status\":\"ok\",\"framework\":\"Boko\",\"version\":\"0.1.0\"}");
return Response_Json("{\"status\":\"ok\",\"framework\":\"Boko\",\"version\":\"0.2.0\"}");
}
// --- GET /api/info ---
@@ -41,43 +38,38 @@ func Boko_Router(req: Request) -> Response {
// --- GET /hello?name=World ---
if String_Eq(req.path, "/hello") {
let name: String = Request_GetQuery(&req, "name");
if bux_strlen(name) == 0 { name = "World"; }
let sb: *void = bux_sb_new(256);
bux_sb_append(sb, "<h1>Hello, ");
bux_sb_append(sb, name);
bux_sb_append(sb, "!</h1>");
let html: String = bux_sb_build(sb);
bux_sb_free(sb);
var name: String = req.GetQuery("name");
if String_Len(name) == 0 { name = "World"; }
let html: String = f"<h1>Hello, {name}!</h1>";
return Response_Html(html);
}
// --- GET /users/{id} ---
if Path_Match("/users/{id}", req.path, &req) {
let id: String = Request_GetPathParam(&req, "id");
let sb: *void = bux_sb_new(128);
bux_sb_append(sb, "{\"id\":");
bux_sb_append(sb, id);
bux_sb_append(sb, ",\"name\":\"User ");
bux_sb_append(sb, id);
bux_sb_append(sb, "\"}");
let json: String = bux_sb_build(sb);
bux_sb_free(sb);
let id: String = req.GetPathParam("id");
let sb: StringBuilder = StringBuilder_New();
StringBuilder_Append(&sb, "{\"id\":");
StringBuilder_Append(&sb, id);
StringBuilder_Append(&sb, ",\"name\":\"User ");
StringBuilder_Append(&sb, id);
StringBuilder_Append(&sb, "\"}");
let json: String = StringBuilder_Build(&sb);
StringBuilder_Free(&sb);
return Response_Json(json);
}
// --- GET /posts/{postId}/comments/{commentId} ---
if Path_Match("/posts/{postId}/comments/{commentId}", req.path, &req) {
let pid: String = Request_GetPathParam(&req, "postId");
let cid: String = Request_GetPathParam(&req, "commentId");
let sb: *void = bux_sb_new(128);
bux_sb_append(sb, "{\"postId\":");
bux_sb_append(sb, pid);
bux_sb_append(sb, ",\"commentId\":");
bux_sb_append(sb, cid);
bux_sb_append(sb, ",\"text\":\"Great post!\"}");
let json: String = bux_sb_build(sb);
bux_sb_free(sb);
let pid: String = req.GetPathParam("postId");
let cid: String = req.GetPathParam("commentId");
let sb: StringBuilder = StringBuilder_New();
StringBuilder_Append(&sb, "{\"postId\":");
StringBuilder_Append(&sb, pid);
StringBuilder_Append(&sb, ",\"commentId\":");
StringBuilder_Append(&sb, cid);
StringBuilder_Append(&sb, ",\"text\":\"Great post!\"}");
let json: String = StringBuilder_Build(&sb);
StringBuilder_Free(&sb);
return Response_Json(json);
}
@@ -88,12 +80,12 @@ func Boko_Router(req: Request) -> Response {
// --- POST /api/echo ---
if String_Eq(req.path, "/api/echo") && req.method.tag == HttpVerb_POST {
let sb: *void = bux_sb_new(256);
bux_sb_append(sb, "{\"echo\":");
bux_sb_append(sb, req.body);
bux_sb_append(sb, "}");
let json: String = bux_sb_build(sb);
bux_sb_free(sb);
let sb: StringBuilder = StringBuilder_New();
StringBuilder_Append(&sb, "{\"echo\":");
StringBuilder_Append(&sb, req.body);
StringBuilder_Append(&sb, "}");
let json: String = StringBuilder_Build(&sb);
StringBuilder_Free(&sb);
return Response_Json(json);
}