9c632ce389
- 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.
183 lines
5.4 KiB
Markdown
183 lines
5.4 KiB
Markdown
# Boko Framework
|
|
|
|
**Async web framework for Bux — inspired by FastAPI.**
|
|
|
|
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
|
|
./build/boko-framework
|
|
```
|
|
|
|
Open `http://localhost:8080` — you'll see the demo landing page.
|
|
|
|
## How It Works
|
|
|
|
Boko follows a **single-dispatch-function** pattern. You define one function — `Boko_Router` — and the framework calls it for every incoming request:
|
|
|
|
```bux
|
|
import Boko::{Request, Response, Response_Json, Response_Html, Response_NotFound,
|
|
Path_Match,
|
|
App, App_New, App_Run};
|
|
|
|
func Boko_Router(req: Request) -> Response {
|
|
// Route: GET /
|
|
if String_Eq(req.path, "/") {
|
|
return Response_Html("<h1>Hello Boko!</h1>");
|
|
}
|
|
|
|
// Route: GET /api/health
|
|
if String_Eq(req.path, "/api/health") {
|
|
return Response_Json("{\"status\":\"ok\"}");
|
|
}
|
|
|
|
// Route: GET /users/{id} — path parameter
|
|
if Path_Match("/users/{id}", req.path, &req) {
|
|
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 = req.GetQuery("q");
|
|
return Response_Json(...);
|
|
}
|
|
|
|
return Response_NotFound();
|
|
}
|
|
|
|
func Main() -> int {
|
|
let app: App = App_New(8080, 4); // port, threads
|
|
App_Run(&app);
|
|
return 0;
|
|
}
|
|
```
|
|
|
|
## API Reference
|
|
|
|
### Request
|
|
|
|
| Field | Type | Description |
|
|
|-------|------|-------------|
|
|
| `method` | `HttpVerb` | GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS |
|
|
| `path` | `String` | Request path (without query string) |
|
|
| `body` | `String` | Request body (for POST/PUT) |
|
|
| `headers` | `StringMap<String>` | Parsed request headers |
|
|
| `query` | `StringMap<String>` | Parsed query parameters |
|
|
| `pathParams` | `StringMap<String>` | Extracted path parameters |
|
|
|
|
| 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
|
|
|
|
| Constructor | Content-Type | Status |
|
|
|-------------|-------------|--------|
|
|
| `Response_Html(html)` | `text/html` | 200 |
|
|
| `Response_Json(json)` | `application/json` | 200 |
|
|
| `Response_Text(text)` | `text/plain` | 200 |
|
|
| `Response_Redirect(url)` | — | 302 |
|
|
| `Response_NotFound()` | `application/json` | 404 |
|
|
| `Response_Error(code, msg)` | `application/json` | custom |
|
|
| `Response_NoContent()` | — | 204 |
|
|
|
|
### Path Matching
|
|
|
|
`Path_Match(pattern, actualPath, req)` matches a pattern with `{param}` placeholders:
|
|
|
|
```bux
|
|
// Pattern: /users/{id}/posts/{postId}
|
|
// Actual: /users/42/posts/7
|
|
// Extracts: id=42, postId=7
|
|
|
|
if Path_Match("/users/{id}/posts/{postId}", req.path, &req) {
|
|
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.pathParams`.
|
|
|
|
### App
|
|
|
|
```bux
|
|
let app: App = App_New(8080, 4); // port 8080, 4 worker threads
|
|
App_Run(&app); // blocks, handles requests
|
|
```
|
|
|
|
## Architecture
|
|
|
|
```
|
|
Incoming connection
|
|
│
|
|
▼
|
|
Net_Accept() ─── worker thread (1 of N)
|
|
│
|
|
▼
|
|
Net_Recv() → raw HTTP bytes
|
|
│
|
|
▼
|
|
Request_Parse() → method, path, query, headers, body
|
|
│
|
|
▼
|
|
Boko_Router(req) ←── YOUR CODE
|
|
│
|
|
▼
|
|
Response_Build() → HTTP/1.1 response string
|
|
│
|
|
▼
|
|
Net_Send() → bytes to client
|
|
│
|
|
▼
|
|
Net_Close()
|
|
```
|
|
|
|
## Endpoints in Demo App
|
|
|
|
| Method | Path | Description |
|
|
|--------|------|-------------|
|
|
| GET | `/` | Landing page (HTML) |
|
|
| GET | `/api/health` | Health check (JSON) |
|
|
| GET | `/api/info` | Framework info (JSON) |
|
|
| GET | `/hello?name=X` | Query param demo |
|
|
| GET | `/users/{id}` | Path param demo |
|
|
| GET | `/posts/{id}/comments/{cid}` | Multi path param demo |
|
|
| GET | `/redirect` | 302 redirect to `/` |
|
|
| POST | `/api/echo` | Echo request body |
|
|
|
|
## Project Structure
|
|
|
|
```
|
|
apps/boko-framework/
|
|
├── bux.toml # Package manifest
|
|
├── README.md # This file
|
|
└── src/
|
|
├── Boko.bux # Framework core (~500 lines)
|
|
└── Main.bux # Example app (~150 lines)
|
|
```
|
|
|
|
## Design Philosophy
|
|
|
|
Boko is intentionally simple. Instead of complex DSLs or code generation, it gives you:
|
|
|
|
- **One function to write** — `Boko_Router(req) -> Response`
|
|
- **Direct control** — you write plain if/else or match for routing
|
|
- **No magic** — path params, query params, headers are explicit function calls
|
|
- **Fast** — multi-threaded accept loop, zero allocations where possible
|
|
|
|
This is the same philosophy as Go's `net/http` — simple, explicit, composable.
|
|
|
|
## License
|
|
|
|
Part of the Bux project. See root [LICENSE](../../LICENSE).
|