Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 46 additions & 10 deletions lightbug.🔥
Original file line number Diff line number Diff line change
@@ -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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll probably add convenience methods on BaseRequest like uri() that would return request.uri.path, just so that the user doesn't have to write the whole req.request.uri.path etc.
Also, request on BaseRequest should probably be renamed to _inner or similar

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would have been nice if we had struct inheritance so people could extend BaseRequeststruct and automatically get all the convenience methods on their custom struct for free.
But on this post in the forum from Dec Chris says they don't plan to add this feature in Mojo. I wonder if we can work around this limitation somehow

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()
21 changes: 14 additions & 7 deletions lightbug_api/__init__.mojo
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
from lightbug_http import HTTPRequest, HTTPResponse, Server
from lightbug_api.routing import (
BaseRequest,
FromReq,
RootRouter,
Router,
HTTPHandler,
HandlerResponse,
JSONType,
)


Expand All @@ -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)
Expand Down
137 changes: 112 additions & 25 deletions lightbug_api/routing.mojo
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this we can prob omit from the trait as not all will have json payloads. either that, or the payload can just be passed inside request

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So the reason that is required on the trait is that since we don't have struct inheritance or reflection we have to have the user deal with deconstructing the data on custom payloads so we want to make sure their type is able to handle this ... we could make them add a method that tells us if we they need json had_json or something like that but I think maybe just defaulting this to an empty json object when no json is needed might be easier... more ergonomic ?

...

fn from_request(mut self, req: HTTPRequest) raises -> Self:
...

fn __str__(self) -> String:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this a required method?

...


@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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this then calls either EmberJSON .to_string() if the user has a JSON object here, or user provides a custom implementation if they want to serialize their custom struct

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:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this could either call emberJSON .parse() and then user gets a JSON object or a custom implementation the user provides to return a struct

# 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
Expand Down Expand Up @@ -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)

Expand All @@ -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:
Expand All @@ -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(
Expand All @@ -87,17 +155,16 @@ 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:
if str(e) == RouterErrors.ROUTE_NOT_FOUND_ERROR:
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
Expand All @@ -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]
Expand Down
Loading