diff --git "a/lightbug.\360\237\224\245" "b/lightbug.\360\237\224\245" index 63119d4..d1bdaac 100644 --- "a/lightbug.\360\237\224\245" +++ "b/lightbug.\360\237\224\245" @@ -1,35 +1,71 @@ from lightbug_api import ( App, + BaseRequest, Router, + HandlerResponse, + JSONType ) from lightbug_http import HTTPRequest, HTTPResponse, OK @always_inline -fn printer(req: HTTPRequest) -> HTTPResponse: - print("Got a request on ", req.uri.path, " with method ", req.method) - return OK(req.body_raw) - +fn printer(req: BaseRequest) raises -> HandlerResponse: + print("Got a request on ", req.request.uri.path, " with method ", req.request.method) + return OK(req.request.body_raw) @always_inline -fn hello(req: HTTPRequest) -> HTTPResponse: +fn hello(payload: BaseRequest) raises -> HandlerResponse: return OK("Hello 🔥!") @always_inline -fn nested(req: HTTPRequest) -> HTTPResponse: - print("Handling route:", req.uri.path) - return OK(req.uri.path) +fn nested(req: BaseRequest) raises -> HandlerResponse: + print("Handling route:", req.request.uri.path) + + # Returning a string will get marshaled to a proper `OK` response + return req.request.uri.path + +@value +struct Payload: + var request: HTTPRequest + var json: JSONType + var a: Int + + fn __init__(out self, request: HTTPRequest, json: JSONType): + self.a = 1 + self.request = request + self.json = json + + fn __str__(self) -> String: + return str(self.a) + + fn from_request(mut self, req: HTTPRequest) raises -> Self: + self.a = 2 + return self + + +@always_inline +fn custom_request_payload(payload: Payload) raises -> HandlerResponse: + print(payload.a) + + # Returning a JSON as the response, this is a very limited placeholder for now + var json_response = JSONType() + json_response["a"] = str(payload.a) + return json_response fn main() raises: var app = App() - app.get("/", hello) + app.get[BaseRequest]("/", hello) + + app.get[Payload]("custom/", custom_request_payload) + + # We can skip specifying payload when using BaseRequest app.post("/", printer) var nested_router = Router("nested") nested_router.get(path="all/echo/", handler=nested) - app.add_router(nested_router^) + app.add_router(nested_router) app.start_server() diff --git a/lightbug_api/__init__.mojo b/lightbug_api/__init__.mojo index 8aa9fa3..992c6e0 100644 --- a/lightbug_api/__init__.mojo +++ b/lightbug_api/__init__.mojo @@ -1,8 +1,11 @@ from lightbug_http import HTTPRequest, HTTPResponse, Server from lightbug_api.routing import ( + BaseRequest, + FromReq, RootRouter, Router, - HTTPHandler, + HandlerResponse, + JSONType, ) @@ -13,19 +16,23 @@ struct App: fn __init__(inout self) raises: self.router = RootRouter() - fn get( + fn get[ + T: FromReq = BaseRequest + ]( inout self, path: String, - handler: HTTPHandler, + handler: fn (T) raises -> HandlerResponse, ) raises: - self.router.get(path, handler) + self.router.get[T](path, handler) - fn post( + fn post[ + T: FromReq = BaseRequest + ]( inout self, path: String, - handler: HTTPHandler, + handler: fn (T) raises -> HandlerResponse, ) raises: - self.router.post(path, handler) + self.router.post[T](path, handler) fn add_router(inout self, owned router: Router) raises -> None: self.router.add_router(router) diff --git a/lightbug_api/routing.mojo b/lightbug_api/routing.mojo index 9b7a2d2..6f8518b 100644 --- a/lightbug_api/routing.mojo +++ b/lightbug_api/routing.mojo @@ -1,9 +1,10 @@ from utils.variant import Variant -from collections import Dict, Optional +from collections import Dict, List, Optional from collections.dict import _DictEntryIter from lightbug_http import NotFound, OK, HTTPService, HTTPRequest, HTTPResponse -from lightbug_http.strings import RequestMethod +from lightbug_http.http import RequestMethod +from lightbug_http.uri import URIDelimiters alias MAX_SUB_ROUTER_DEPTH = 20 @@ -14,15 +15,84 @@ struct RouterErrors: alias INVALID_PATH_FRAGMENT_ERROR = "INVALID_PATH_FRAGMENT_ERROR" -alias HTTPHandler = fn (req: HTTPRequest) -> HTTPResponse +alias HTTPHandlerWrapper = fn (req: HTTPRequest) raises escaping -> HTTPResponse + +# TODO: Placeholder type, what can the JSON container look like +alias JSONType = Dict[String, String] + +alias HandlerResponse = Variant[HTTPResponse, String, JSONType] + + +trait FromReq(Movable, Copyable): + fn __init__(out self, request: HTTPRequest, json: JSONType): + ... + + fn from_request(mut self, req: HTTPRequest) raises -> Self: + ... + + fn __str__(self) -> String: + ... @value -struct HandlerMeta: - var handler: HTTPHandler +struct BaseRequest: + var request: HTTPRequest + var json: JSONType + + fn __init__(out self, request: HTTPRequest, json: JSONType): + self.request = request + self.json = json + fn __str__(self) -> String: + return str("") -alias HTTPHandlersMap = Dict[String, HandlerMeta] + fn from_request(mut self, req: HTTPRequest) raises -> Self: + return self + + +@value +struct RouteHandler[T: FromReq](CollectionElement): + var handler: fn (T) raises -> HandlerResponse + + fn __init__(inout self, h: fn (T) raises -> HandlerResponse): + self.handler = h + + fn _encode_response(self, res: HandlerResponse) raises -> HTTPResponse: + if res.isa[HTTPResponse](): + return res[HTTPResponse] + elif res.isa[String](): + return OK(res[String]) + elif res.isa[JSONType](): + return OK(self._serialize_json(res[JSONType])) + else: + raise Error("Unsupported response type") + + fn _serialize_json(self, json: JSONType) raises -> String: + # TODO: Placeholder json serialize implementation + fn ser(j: JSONType) raises -> String: + var str_frags = List[String]() + for kv in j.items(): + str_frags.append( + '"' + str(kv[].key) + '": "' + str(kv[].value) + '"' + ) + + var str_res = str("{") + str(",").join(str_frags) + str("}") + return str_res + + return ser(json) + + fn _deserialize_json(self, req: HTTPRequest) raises -> JSONType: + # TODO: Placeholder json deserialize implementation + return JSONType() + + fn handle(self, req: HTTPRequest) raises -> HTTPResponse: + var payload = T(request=req, json=self._deserialize_json(req)) + payload = payload.from_request(req) + var handler_response = self.handler(payload) + return self._encode_response(handler_response^) + + +alias HTTPHandlersMap = Dict[String, HTTPHandlerWrapper] @value @@ -54,7 +124,7 @@ struct RouterBase[is_main_app: Bool = False](HTTPService): fn _route( mut self, partial_path: String, method: String, depth: Int = 0 - ) raises -> HandlerMeta: + ) raises -> HTTPHandlerWrapper: if depth > MAX_SUB_ROUTER_DEPTH: raise Error(RouterErrors.ROUTE_NOT_FOUND_ERROR) @@ -63,8 +133,7 @@ struct RouterBase[is_main_app: Bool = False](HTTPService): var handler_path = partial_path if partial_path: - # TODO: (Hrist) Update to lightbug_http.uri.URIDelimiters.PATH when available - var fragments = partial_path.split("/", 1) + var fragments = partial_path.split(URIDelimiters.PATH, 1) sub_router_name = fragments[0] if len(fragments) == 2: @@ -73,8 +142,7 @@ struct RouterBase[is_main_app: Bool = False](HTTPService): remaining_path = "" else: - # TODO: (Hrist) Update to lightbug_http.uri.URIDelimiters.PATH when available - handler_path = "/" + handler_path = URIDelimiters.PATH if sub_router_name in self.sub_routers: return self.sub_routers[sub_router_name]._route( @@ -87,9 +155,8 @@ struct RouterBase[is_main_app: Bool = False](HTTPService): fn func(mut self, req: HTTPRequest) raises -> HTTPResponse: var uri = req.uri - # TODO: (Hrist) Update to lightbug_http.uri.URIDelimiters.PATH when available - var path = uri.path.split("/", 1)[1] - var route_handler_meta: HandlerMeta + var path = uri.path.split(URIDelimiters.PATH, 1)[1] + var route_handler_meta: HTTPHandlerWrapper try: route_handler_meta = self._route(path, req.method) except e: @@ -97,7 +164,7 @@ struct RouterBase[is_main_app: Bool = False](HTTPService): return NotFound(uri.path) raise e - return route_handler_meta.handler(req) + return route_handler_meta(req) fn _validate_path_fragment(self, path_fragment: String) -> Bool: # TODO: Validate fragment @@ -110,31 +177,51 @@ struct RouterBase[is_main_app: Bool = False](HTTPService): fn add_router(mut self, owned router: RouterBase[False]) raises -> None: self.sub_routers[router.path_fragment] = router - fn add_route( + # fn register[T: FromReq](inout self, path: String, handler: fn(T) raises): + # + # fn handle(req: Request) raises: + # RouteHandler[T](handler).handle(req) + # + # self.routes[path] = handle + # + # fn route(self, path: String, req: Request) raises: + # if path in self.routes: + # self.routes[path](req) + # else: + + fn add_route[ + T: FromReq + ]( mut self, partial_path: String, - handler: HTTPHandler, + handler: fn (T) raises -> HandlerResponse, method: RequestMethod = RequestMethod.get, ) raises -> None: if not self._validate_path(partial_path): raise Error(RouterErrors.INVALID_PATH_ERROR) - var handler_meta = HandlerMeta(handler) - self.routes[method.value][partial_path] = handler_meta^ + fn handle(req: HTTPRequest) raises -> HTTPResponse: + return RouteHandler[T](handler).handle(req) + + self.routes[method.value][partial_path] = handle^ - fn get( + fn get[ + T: FromReq = BaseRequest + ]( inout self, path: String, - handler: HTTPHandler, + handler: fn (T) raises -> HandlerResponse, ) raises: - self.add_route(path, handler, RequestMethod.get) + self.add_route[T](path, handler, RequestMethod.get) - fn post( + fn post[ + T: FromReq = BaseRequest + ]( inout self, path: String, - handler: HTTPHandler, + handler: fn (T) raises -> HandlerResponse, ) raises: - self.add_route(path, handler, RequestMethod.post) + self.add_route[T](path, handler, RequestMethod.post) alias RootRouter = RouterBase[True] diff --git a/magic.lock b/magic.lock index 0314f59..d44dbe7 100644 --- a/magic.lock +++ b/magic.lock @@ -131,7 +131,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.13.5-h064dc61_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-h4ab18f5_1.conda - - conda: https://repo.prefix.dev/mojo-community/linux-64/lightbug_http-0.1.8-hb0f4dca_0.conda + - conda: https://repo.prefix.dev/mojo-community/linux-64/lightbug_http-0.1.10-hb0f4dca_0.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/lz4-c-1.10.0-h5888daf_1.conda - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-3.0.0-pyhd8ed1ab_1.conda - conda: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-3.0.2-py312h178313f_1.conda @@ -343,7 +343,7 @@ environments: - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxcb-1.17.0-hdb1d25a_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libxml2-2.13.5-h376fa9f_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/libzlib-1.3.1-hfb2fe0b_1.conda - - conda: https://repo.prefix.dev/mojo-community/osx-arm64/lightbug_http-0.1.8-h60d57d3_0.conda + - conda: https://repo.prefix.dev/mojo-community/osx-arm64/lightbug_http-0.1.10-h60d57d3_0.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/llvm-openmp-18.1.8-hde57baf_1.conda - conda: https://conda.anaconda.org/conda-forge/osx-arm64/lz4-c-1.9.4-hb7217d7_0.conda - conda: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-3.0.0-pyhd8ed1ab_1.conda @@ -4014,34 +4014,34 @@ packages: timestamp: 1716874262512 - kind: conda name: lightbug_http - version: 0.1.8 + version: 0.1.10 build: h60d57d3_0 subdir: osx-arm64 - url: https://repo.prefix.dev/mojo-community/osx-arm64/lightbug_http-0.1.8-h60d57d3_0.conda - sha256: 0ba3009ed9dbdae4728e503288c656131b1d90e555e07e6539d162f869807038 + url: https://repo.prefix.dev/mojo-community/osx-arm64/lightbug_http-0.1.10-h60d57d3_0.conda + sha256: 14a2cbebc60a793165758641e35c3cb66610addec05bafbe2ee023e718631c3f depends: - max >=24.6.0 - small_time ==0.1.6 arch: arm64 platform: osx license: MIT - size: 1020548 - timestamp: 1736359491861 + size: 1246576 + timestamp: 1738523886756 - kind: conda name: lightbug_http - version: 0.1.8 + version: 0.1.10 build: hb0f4dca_0 subdir: linux-64 - url: https://repo.prefix.dev/mojo-community/linux-64/lightbug_http-0.1.8-hb0f4dca_0.conda - sha256: 966b4ccbcd538df590108e7a6844004cfbd49f07d4a4a2a749147e496a5f8539 + url: https://repo.prefix.dev/mojo-community/linux-64/lightbug_http-0.1.10-hb0f4dca_0.conda + sha256: 1b652b7b5bc0cfa31d6c38beadf57a923b731ad035c550e01dbec2c9e0960fda depends: - max >=24.6.0 - small_time ==0.1.6 arch: x86_64 platform: linux license: MIT - size: 1020613 - timestamp: 1736359486142 + size: 1246593 + timestamp: 1738523883901 - kind: conda name: llvm-openmp version: 18.1.8 diff --git a/mojoproject.toml b/mojoproject.toml index 390b6c7..e185478 100644 --- a/mojoproject.toml +++ b/mojoproject.toml @@ -20,4 +20,4 @@ format = { cmd = "magic run mojo format -l 120 lightbug_api" } [dependencies] max = ">=24.6.0,<25" -lightbug_http = "0.1.8" +lightbug_http = "0.1.10"