Skip to content

Commit 62e8fc8

Browse files
committed
typed handler returns and resource trait
1 parent 84c7fc1 commit 62e8fc8

4 files changed

Lines changed: 204 additions & 19 deletions

File tree

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,5 @@
1010
*.egg-info
1111

1212
# magic environments
13-
.magic
13+
.magic
14+
*.xml

lightbug.mojo

Lines changed: 75 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# lightbug_api showcase
1+
# lightbug_api showcase — metaprogramming ergonomics demo
22
# ─────────────────────────────────────────────────────────────────────────────
33
# Run: mojo lightbug.mojo
44
# Test: curl http://localhost:8080/
@@ -13,9 +13,11 @@
1313
# -d '{"name":"Updated","price":19.99}'
1414
# curl -X DELETE http://localhost:8080/items/42
1515
# curl http://localhost:8080/v1/status
16+
# curl http://localhost:8080/notes
17+
# curl http://localhost:8080/notes/1
1618
# ─────────────────────────────────────────────────────────────────────────────
1719

18-
from lightbug_api import App, GET, POST, PUT, DELETE, mount, HandlerResponse
20+
from lightbug_api import App, GET, POST, PUT, DELETE, mount, HandlerResponse, Resource, resource
1921
from lightbug_api.context import Context
2022
from lightbug_api.response import Response
2123
from lightbug_http.http.json import JsonSerializable, JsonDeserializable
@@ -55,62 +57,117 @@ struct StatusResponse(JsonSerializable, Movable, Defaultable):
5557
self.version = ""
5658

5759

60+
@fieldwise_init
61+
struct Note(JsonSerializable, Movable, Defaultable):
62+
var id: Int
63+
var text: String
64+
65+
fn __init__(out self):
66+
self.id = 0
67+
self.text = ""
68+
69+
5870
# ------------------------------------------------------------------ handlers
71+
# Handlers that need full control (non-200 status, redirects, plain text) keep
72+
# the HandlerResponse return type. Handlers that just return a model use the
73+
# model type directly — the framework auto-serialises as JSON 200 OK.
5974

6075
fn index(ctx: Context) raises -> HandlerResponse:
6176
return Response.text("Welcome to lightbug_api 🔥")
6277

6378

64-
fn list_items(ctx: Context) raises -> HandlerResponse:
65-
return Response.json(Item(1, "Widget", 9.99))
79+
# ── Before (old style) ───────────────────────────────────────────────────────
80+
81+
fn list_items(ctx: Context) raises -> Item: # ← returns Item directly
82+
return Item(1, "Widget", 9.99)
6683

6784

68-
fn get_item(ctx: Context) raises -> HandlerResponse:
69-
# ctx.param("id", 0) → Int (path param, typed by default value)
70-
# ctx.query("verbose", False) → Bool (query param, typed by default value)
85+
fn get_item(ctx: Context) raises -> Item: # ← returns Item directly
7186
var id = ctx.param("id", 0)
7287
var verbose = ctx.query("verbose", False)
73-
7488
if verbose:
7589
print("GET /items/", id, " (verbose mode)")
76-
77-
return Response.json(Item(id, String("Item ", id), 9.99))
90+
return Item(id, String("Item ", id), 9.99)
7891

7992

8093
fn create_item(ctx: Context) raises -> HandlerResponse:
94+
# Still HandlerResponse — needs 201 Created status code
8195
var body = ctx.json[CreateItemRequest]()
8296
var created = Item(100, body.name, body.price)
8397
return Response.created(created)
8498

8599

86-
fn update_item(ctx: Context) raises -> HandlerResponse:
100+
fn update_item(ctx: Context) raises -> Item: # ← returns Item directly
87101
var body = ctx.json[CreateItemRequest]()
88102
var id = ctx.param("id", 0)
89-
return Response.json(Item(id, body.name, body.price))
103+
return Item(id, body.name, body.price)
90104

91105

92106
fn delete_item(ctx: Context) raises -> HandlerResponse:
107+
# Still HandlerResponse — needs 204 No Content
93108
var id = ctx.param("id", 0)
94109
print("Deleting item", id)
95110
return Response.no_content()
96111

97112

98-
fn health(ctx: Context) raises -> HandlerResponse:
99-
return Response.json(StatusResponse("ok", "1.0.0"))
113+
fn health(ctx: Context) raises -> StatusResponse: # ← returns StatusResponse directly
114+
return StatusResponse("ok", "1.0.0")
115+
116+
117+
# ── Resource / controller pattern ────────────────────────────────────────────
118+
# Group CRUD handlers in a struct; `resource[Notes]("notes")` registers all
119+
# five standard routes under /notes in one call.
120+
121+
struct Notes(Resource):
122+
@staticmethod
123+
fn index(ctx: Context) raises -> HandlerResponse:
124+
return Response.json(Note(0, "all notes"))
125+
126+
@staticmethod
127+
fn show(ctx: Context) raises -> HandlerResponse:
128+
var id = ctx.param("id", 0)
129+
return Response.json(Note(id, String("note ", id)))
130+
131+
@staticmethod
132+
fn create(ctx: Context) raises -> HandlerResponse:
133+
return Response.created(Note(1, "new note"))
134+
135+
@staticmethod
136+
fn update(ctx: Context) raises -> HandlerResponse:
137+
var id = ctx.param("id", 0)
138+
return Response.json(Note(id, "updated"))
139+
140+
@staticmethod
141+
fn destroy(ctx: Context) raises -> HandlerResponse:
142+
return Response.no_content()
100143

101144

102145
# --------------------------------------------------------------------- main
103146

104147
fn main() raises:
105148
var app = App(
149+
# Plain-text response — HandlerResponse (unchanged)
106150
GET("/", index),
107-
GET("/items", list_items),
108-
GET("/items/{id}", get_item),
151+
152+
# Typed return — framework auto-serialises Item as JSON 200 OK
153+
GET[Item, list_items]("/items"),
154+
GET[Item, get_item]("/items/{id}"),
155+
156+
# 201 Created — still HandlerResponse (needs explicit status)
109157
POST("/items", create_item),
110-
PUT("/items/{id}", update_item),
158+
159+
# Typed return
160+
PUT[Item, update_item]("/items/{id}"),
161+
162+
# 204 No Content — still HandlerResponse
111163
DELETE("/items/{id}", delete_item),
164+
112165
mount("v1",
113-
GET("status", health),
166+
# Typed return inside a mount
167+
GET[StatusResponse, health]("status"),
114168
),
169+
170+
# Resource controller — five routes registered in one line
171+
resource[Notes]("notes"),
115172
)
116173
app.run()

lightbug_api/__init__.mojo

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ from lightbug_api.routing import (
2626
abort,
2727
next,
2828
_apply_routes,
29+
Resource,
30+
resource,
2931
)
3032

3133

lightbug_api/routing.mojo

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ from lightbug_http.http import RequestMethod
66
from lightbug_http.http.common_response import InternalError
77
from lightbug_http.uri import URIDelimiters
88

9+
from lightbug_http import OK
10+
from lightbug_http.http.json import Json
11+
912
from lightbug_api.context import Context
1013

1114

@@ -31,6 +34,16 @@ comptime HandlerResponse = Variant[HTTPResponse, String]
3134
# Use Context to access the request, path params, query params, headers, body.
3235
comptime Handler = fn (Context) raises -> HandlerResponse
3336

37+
38+
# Compile-time adapter: specialises into a concrete Handler for any fn that
39+
# returns a JSON-serialisable T. Because `h` is a compile-time parameter the
40+
# resulting specialisation has zero extra overhead vs writing the wrapper by hand.
41+
fn _json_adapter[T: Movable & ImplicitlyDestructible, h: fn (Context) raises -> T](
42+
ctx: Context,
43+
) raises -> HandlerResponse:
44+
return HandlerResponse(OK(Json(h(ctx))))
45+
46+
3447
# ------------------------------------------------------------ middleware types
3548

3649
# Middleware return type:
@@ -583,3 +596,115 @@ fn _apply_routes[is_main: Bool](mut router: RouterBase[is_main], routes: List[Ro
583596
router.add_router(sub^)
584597
else:
585598
router.add_route(r.path, r.handler, RequestMethod(r.method))
599+
600+
601+
# ================================================================ Typed route builders
602+
# Parametric overloads that let handlers return model types directly instead of
603+
# wrapping everything in ``HandlerResponse``. ``_json_adapter`` is specialised
604+
# at compile time so there is zero runtime overhead over hand-written wrappers.
605+
#
606+
# Usage::
607+
#
608+
# fn get_item(ctx: Context) raises -> Item: # no Response.json() needed
609+
# return Item(ctx.param("id", 0), "Widget", 9.99)
610+
#
611+
# GET[Item, get_item]("/items/{id}") # T is often inferable
612+
613+
fn GET[T: Movable & ImplicitlyDestructible, h: fn (Context) raises -> T](path: String) -> Route:
614+
"""GET route whose handler returns *T* directly — auto-serialised as JSON."""
615+
return Route("GET", path, _json_adapter[T, h])
616+
617+
618+
fn POST[T: Movable & ImplicitlyDestructible, h: fn (Context) raises -> T](path: String) -> Route:
619+
"""POST route whose handler returns *T* directly — auto-serialised as JSON."""
620+
return Route("POST", path, _json_adapter[T, h])
621+
622+
623+
fn PUT[T: Movable & ImplicitlyDestructible, h: fn (Context) raises -> T](path: String) -> Route:
624+
"""PUT route whose handler returns *T* directly — auto-serialised as JSON."""
625+
return Route("PUT", path, _json_adapter[T, h])
626+
627+
628+
fn DELETE[T: Movable & ImplicitlyDestructible, h: fn (Context) raises -> T](path: String) -> Route:
629+
"""DELETE route whose handler returns *T* directly — auto-serialised as JSON."""
630+
return Route("DELETE", path, _json_adapter[T, h])
631+
632+
633+
fn PATCH[T: Movable & ImplicitlyDestructible, h: fn (Context) raises -> T](path: String) -> Route:
634+
"""PATCH route whose handler returns *T* directly — auto-serialised as JSON."""
635+
return Route("PATCH", path, _json_adapter[T, h])
636+
637+
638+
# ================================================================ Resource trait
639+
# Declare a struct-based CRUD controller and register all five standard routes
640+
# in one call::
641+
#
642+
# struct Items(Resource):
643+
# @staticmethod
644+
# fn index(ctx: Context) raises -> HandlerResponse: ...
645+
# @staticmethod
646+
# fn show(ctx: Context) raises -> HandlerResponse: ...
647+
# @staticmethod
648+
# fn create(ctx: Context) raises -> HandlerResponse: ...
649+
# @staticmethod
650+
# fn update(ctx: Context) raises -> HandlerResponse: ...
651+
# @staticmethod
652+
# fn destroy(ctx: Context) raises -> HandlerResponse: ...
653+
#
654+
# App(resource[Items]("items")).run()
655+
# # → GET /items, GET /items/{id}, POST /items, PUT /items/{id}, DELETE /items/{id}
656+
657+
trait Resource:
658+
"""Struct-based CRUD resource controller.
659+
660+
Implement all five static methods then register with ``resource[R](fragment)``.
661+
Static methods mean no instance is needed — the struct is purely a namespace.
662+
"""
663+
664+
@staticmethod
665+
fn index(ctx: Context) raises -> HandlerResponse:
666+
"""GET / — list all resources."""
667+
...
668+
669+
@staticmethod
670+
fn show(ctx: Context) raises -> HandlerResponse:
671+
"""GET /{id} — retrieve one resource."""
672+
...
673+
674+
@staticmethod
675+
fn create(ctx: Context) raises -> HandlerResponse:
676+
"""POST / — create a resource."""
677+
...
678+
679+
@staticmethod
680+
fn update(ctx: Context) raises -> HandlerResponse:
681+
"""PUT /{id} — replace a resource."""
682+
...
683+
684+
@staticmethod
685+
fn destroy(ctx: Context) raises -> HandlerResponse:
686+
"""DELETE /{id} — remove a resource."""
687+
...
688+
689+
690+
fn resource[R: Resource](fragment: String) -> Route:
691+
"""Declare a full CRUD resource mounted at *fragment*.
692+
693+
Equivalent to::
694+
695+
mount(fragment,
696+
GET("", R.index),
697+
GET("{id}", R.show),
698+
POST("", R.create),
699+
PUT("{id}", R.update),
700+
DELETE("{id}", R.destroy),
701+
)
702+
"""
703+
return mount(
704+
fragment,
705+
GET("", R.index),
706+
GET("{id}", R.show),
707+
POST("", R.create),
708+
PUT("{id}", R.update),
709+
DELETE("{id}", R.destroy),
710+
)

0 commit comments

Comments
 (0)