Skip to content

Commit 84c7fc1

Browse files
committed
Experiment with the API
1 parent 16ba003 commit 84c7fc1

4 files changed

Lines changed: 267 additions & 70 deletions

File tree

lightbug.mojo

Lines changed: 23 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
# Test: curl http://localhost:8080/
55
# curl http://localhost:8080/items
66
# curl http://localhost:8080/items/42
7-
# curl http://localhost:8080/items/42?verbose=true
7+
# curl "http://localhost:8080/items/42?verbose=true"
88
# curl -X POST http://localhost:8080/items \
99
# -H 'Content-Type: application/json' \
1010
# -d '{"name":"Widget","price":9.99}'
@@ -15,7 +15,7 @@
1515
# curl http://localhost:8080/v1/status
1616
# ─────────────────────────────────────────────────────────────────────────────
1717

18-
from lightbug_api import App, Router, HandlerResponse
18+
from lightbug_api import App, GET, POST, PUT, DELETE, mount, HandlerResponse
1919
from lightbug_api.context import Context
2020
from lightbug_api.response import Response
2121
from lightbug_http.http.json import JsonSerializable, JsonDeserializable
@@ -25,8 +25,6 @@ from lightbug_http.http.json import JsonSerializable, JsonDeserializable
2525

2626
@fieldwise_init
2727
struct Item(JsonSerializable, Movable, Defaultable):
28-
"""An item returned in API responses."""
29-
3028
var id: Int
3129
var name: String
3230
var price: Float64
@@ -39,8 +37,6 @@ struct Item(JsonSerializable, Movable, Defaultable):
3937

4038
@fieldwise_init
4139
struct CreateItemRequest(JsonDeserializable, Movable, Defaultable):
42-
"""JSON body expected for POST /items."""
43-
4440
var name: String
4541
var price: Float64
4642

@@ -62,70 +58,59 @@ struct StatusResponse(JsonSerializable, Movable, Defaultable):
6258
# ------------------------------------------------------------------ handlers
6359

6460
fn index(ctx: Context) raises -> HandlerResponse:
65-
"""GET / — plain-text welcome message."""
6661
return Response.text("Welcome to lightbug_api 🔥")
6762

6863

6964
fn list_items(ctx: Context) raises -> HandlerResponse:
70-
"""GET /items — return a hard-coded list as JSON."""
71-
# In a real app you'd query a database here.
7265
return Response.json(Item(1, "Widget", 9.99))
7366

7467

7568
fn get_item(ctx: Context) raises -> HandlerResponse:
76-
"""GET /items/{id} — return one item by ID."""
77-
var id = ctx.path_param("id", "unknown")
78-
var verbose = ctx.query("verbose", "false")
69+
# ctx.param("id", 0) → Int (path param, typed by default value)
70+
# ctx.query("verbose", False) → Bool (query param, typed by default value)
71+
var id = ctx.param("id", 0)
72+
var verbose = ctx.query("verbose", False)
7973

80-
if verbose == "true":
74+
if verbose:
8175
print("GET /items/", id, " (verbose mode)")
8276

83-
return Response.json(Item(42, String("Item ", id), 9.99))
77+
return Response.json(Item(id, String("Item ", id), 9.99))
8478

8579

8680
fn create_item(ctx: Context) raises -> HandlerResponse:
87-
"""POST /items — deserialize JSON body, return 201 Created."""
88-
var body = ctx.json[CreateItemRequest]()
81+
var body = ctx.json[CreateItemRequest]()
8982
var created = Item(100, body.name, body.price)
9083
return Response.created(created)
9184

9285

9386
fn update_item(ctx: Context) raises -> HandlerResponse:
94-
"""PUT /items/{id} — update an item."""
9587
var body = ctx.json[CreateItemRequest]()
96-
return Response.json(Item(42, body.name, body.price))
88+
var id = ctx.param("id", 0)
89+
return Response.json(Item(id, body.name, body.price))
9790

9891

9992
fn delete_item(ctx: Context) raises -> HandlerResponse:
100-
"""DELETE /items/{id} — delete an item, return 204 No Content."""
101-
var id = ctx.path_param("id", "0")
93+
var id = ctx.param("id", 0)
10294
print("Deleting item", id)
10395
return Response.no_content()
10496

10597

10698
fn health(ctx: Context) raises -> HandlerResponse:
107-
"""GET /v1/status — health check mounted under the v1 sub-router."""
10899
return Response.json(StatusResponse("ok", "1.0.0"))
109100

110101

111102
# --------------------------------------------------------------------- main
112103

113104
fn main() raises:
114-
var app = App()
115-
116-
# Root
117-
app.get("/", index)
118-
119-
# Items resource — all HTTP verbs
120-
app.get("/items", list_items)
121-
app.get("/items/{id}", get_item)
122-
app.post("/items", create_item)
123-
app.put("/items/{id}", update_item)
124-
app.delete("/items/{id}", delete_item)
125-
126-
# Sub-router mounted at /v1
127-
var v1 = Router("v1")
128-
v1.get("status", health)
129-
app.add_router(v1^)
130-
105+
var app = App(
106+
GET("/", index),
107+
GET("/items", list_items),
108+
GET("/items/{id}", get_item),
109+
POST("/items", create_item),
110+
PUT("/items/{id}", update_item),
111+
DELETE("/items/{id}", delete_item),
112+
mount("v1",
113+
GET("status", health),
114+
),
115+
)
131116
app.run()

lightbug_api/__init__.mojo

Lines changed: 46 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,21 @@ from lightbug_api.routing import (
1111
MiddlewareEntry,
1212
MiddlewareResult,
1313
PathPattern,
14+
Route,
1415
RouteMatch,
1516
RootRouter,
1617
Router,
18+
GET,
19+
POST,
20+
PUT,
21+
DELETE,
22+
PATCH,
23+
OPTIONS,
24+
HEAD,
25+
mount,
1726
abort,
1827
next,
28+
_apply_routes,
1929
)
2030

2131

@@ -38,40 +48,56 @@ struct StartupHookEntry(Copyable):
3848
struct App:
3949
"""The top-level application — register routes then call ``run()``.
4050
41-
**Quick-start**::
51+
**Declarative style** (recommended)::
4252
4353
fn main() raises:
44-
var app = App()
45-
46-
# Routes
47-
app.get("/", index)
48-
app.get("/users/{id}", get_user)
49-
app.post("/users", create_user)
50-
app.delete("/users/{id}", delete_user)
51-
52-
# Sub-router mounted at /v1
53-
var api = Router("v1")
54-
api.get("status", health)
55-
app.add_router(api^)
54+
App(
55+
GET("/", index),
56+
GET("/users/{id}", get_user),
57+
POST("/users", create_user),
58+
DELETE("/users/{id}", delete_user),
59+
mount("v1",
60+
GET("status", health),
61+
),
62+
).run()
63+
64+
**Builder style** (for dynamic or conditional registration)::
5665
57-
# Middleware — runs before every handler
66+
fn main() raises:
67+
var app = App()
68+
app.get("/", index)
69+
app.post("/users", create_user)
5870
app.use(require_auth)
59-
60-
# Error handler — catches unhandled exceptions from handlers
61-
app.on_error(my_error_handler)
62-
63-
# Startup hook — runs once before the server starts
6471
app.on_startup(connect_db)
65-
72+
app.on_error(my_error_handler)
6673
app.run()
6774
"""
6875

6976
var router: RootRouter
7077
var startup_hooks: List[StartupHookEntry]
7178

7279
def __init__(out self) raises:
80+
"""Create an empty app for use with the builder style."""
81+
self.router = RootRouter()
82+
self.startup_hooks = List[StartupHookEntry]()
83+
84+
def __init__(out self, *routes: Route) raises:
85+
"""Create an app from a declarative list of Route specs.
86+
87+
Example::
88+
89+
App(
90+
GET("/", index),
91+
POST("/items", create_item),
92+
mount("v1", GET("status", health)),
93+
).run()
94+
"""
7395
self.router = RootRouter()
7496
self.startup_hooks = List[StartupHookEntry]()
97+
var route_list = List[Route]()
98+
for i in range(len(routes)):
99+
route_list.append(routes[i].copy())
100+
_apply_routes[True](self.router, route_list)
75101

76102
# ------------------------------------------ route registration
77103

lightbug_api/context.mojo

Lines changed: 73 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,52 @@ struct Context(Copyable):
145145
return result.value()
146146
return default
147147

148+
# -------------------------------------------- unified typed path params
149+
# Overload dispatch is based on the type of *default*:
150+
#
151+
# ctx.param("id") → Optional[String]
152+
# ctx.param("id", "0") → String
153+
# ctx.param("id", 0) → Int
154+
# ctx.param("flag", False) → Bool
155+
156+
fn param(self, name: String) -> Optional[String]:
157+
"""Look up a path parameter by name (alias for ``path_param``)."""
158+
return self.path_param(name)
159+
160+
fn param(self, name: String, default: String) -> String:
161+
"""Look up a path parameter, falling back to *default*."""
162+
return self.path_param(name, default)
163+
164+
fn param(self, name: String, default: Int) -> Int:
165+
"""Look up a path parameter and parse it as ``Int``.
166+
167+
Example::
168+
169+
var id = ctx.param("id", 0)
170+
"""
171+
var s = self.path_param(name)
172+
if s:
173+
try:
174+
return atol(s.value())
175+
except:
176+
pass
177+
return default
178+
179+
fn param(self, name: String, default: Bool) -> Bool:
180+
"""Look up a path parameter and parse it as ``Bool``.
181+
182+
Truthy values: ``"true"``, ``"1"``, ``"yes"``::
183+
184+
var flag = ctx.param("enabled", False)
185+
"""
186+
var s = self.path_param(name)
187+
if s:
188+
var v = s.value()
189+
return v == "true" or v == "1" or v == "yes"
190+
return default
191+
148192
# ------------------------------------------------------- typed path params
193+
# Kept for backwards compatibility.
149194

150195
fn path_int(self, name: String) -> Optional[Int]:
151196
"""Parse a path parameter as ``Int``.
@@ -162,17 +207,22 @@ struct Context(Copyable):
162207

163208
fn path_int(self, name: String, default: Int) -> Int:
164209
"""Parse a path parameter as ``Int``, falling back to *default*."""
165-
var result = self.path_int(name)
166-
if result:
167-
return result.value()
168-
return default
210+
return self.param(name, default)
169211

170-
# ------------------------------------------------------ typed query params
212+
# ---------------------------------------------- unified typed query params
213+
# Overload dispatch is based on the type of *default*:
214+
#
215+
# ctx.query("page") → Optional[String] (existing overload above)
216+
# ctx.query("page", "1") → String (existing overload above)
217+
# ctx.query("page", 1) → Int
218+
# ctx.query("verbose", False) → Bool
171219

172-
fn query_int(self, name: String, default: Int = 0) -> Int:
173-
"""Parse a query parameter as ``Int``, falling back to *default*.
220+
fn query(self, name: String, default: Int) -> Int:
221+
"""Look up a query parameter and parse it as ``Int``.
222+
223+
Example::
174224
175-
Example: ``ctx.query_int("page", 1)``
225+
var page = ctx.query("page", 1)
176226
"""
177227
var s = self.query(name)
178228
if s:
@@ -182,19 +232,30 @@ struct Context(Copyable):
182232
pass
183233
return default
184234

185-
fn query_bool(self, name: String, default: Bool = False) -> Bool:
186-
"""Parse a query parameter as ``Bool``, falling back to *default*.
235+
fn query(self, name: String, default: Bool) -> Bool:
236+
"""Look up a query parameter and parse it as ``Bool``.
187237
188-
Truthy string values: ``"true"``, ``"1"``, ``"yes"``.
238+
Truthy values: ``"true"``, ``"1"``, ``"yes"``::
189239
190-
Example: ``ctx.query_bool("verbose")``
240+
var verbose = ctx.query("verbose", False)
191241
"""
192242
var s = self.query(name)
193243
if s:
194244
var v = s.value()
195245
return v == "true" or v == "1" or v == "yes"
196246
return default
197247

248+
# ------------------------------------------------------ typed query params
249+
# Kept for backwards compatibility.
250+
251+
fn query_int(self, name: String, default: Int = 0) -> Int:
252+
"""Parse a query parameter as ``Int``, falling back to *default*."""
253+
return self.query(name, default)
254+
255+
fn query_bool(self, name: String, default: Bool = False) -> Bool:
256+
"""Parse a query parameter as ``Bool``, falling back to *default*."""
257+
return self.query(name, default)
258+
198259
# ---------------------------------------------------------------- headers
199260

200261
fn header(self, name: String) -> Optional[String]:

0 commit comments

Comments
 (0)