Skip to content
Open
Show file tree
Hide file tree
Changes from 4 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
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,7 @@
*.egg-info

# magic environments
.magic
.magic

external
.lightbug
48 changes: 46 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,14 +95,58 @@ Once you have a Mojo project set up locally,
fn main() raises:
var app = App()

app.get("/", hello)
app.post("/", printer)
app.get("/", hello, "hello")
app.post("/printer", printer, "printer")

app.start_server()
```
7. Excellent 😈. Your app is now listening on the selected port.
You've got yourself a pure-Mojo API! 🔥

## API Docs
Lightbug serves API docs for your app automatically at `/docs` by default.
To disable this, add the `docs_enabled=False` flag when creating a new app instance: `App(docs_enabled=False)`.
To describe your routes, add Mojo docstring annotations like below:

```mojo

@always_inline
fn printer(req: HTTPRequest) -> HTTPResponse:
"""Prints the request body and returns it.

Args:
req: Any arbitrary HTTP request with a body.

Returns:
HTTPResponse: 200 OK with the request body.
"""
print("Got a request on ", req.uri.path, " with method ", req.method)
return OK(req.body_raw)

@always_inline
fn hello(req: HTTPRequest) -> HTTPResponse:
"""Simple hello world function.

Args:
req: Any arbitrary HTTP request.

Returns:
HTTPResponse: 200 OK with a Hello World message.

Tags:
hello.
"""
return OK("Hello 🔥!", "text/plain; charset=utf-8")

fn main() raises:
var app = App()

app.get("/", hello, "hello")
app.post("/printer", printer, "printer")

app.start_server()
```


<!-- MARKDOWN LINKS & IMAGES -->
<!-- https://www.markdownguide.org/basic-syntax/#reference-style-links -->
Expand Down
28 changes: 24 additions & 4 deletions lightbug.🔥
Original file line number Diff line number Diff line change
@@ -1,19 +1,39 @@
from lightbug_api import App
from lightbug_api.app import App
from lightbug_http import HTTPRequest, HTTPResponse, OK

@always_inline
fn printer(req: HTTPRequest) -> HTTPResponse:
"""Prints the request body and returns it.

Args:
req: Any arbitrary HTTP request with a body.

Returns:
HTTPResponse: 200 OK with the request body.
"""
print("Got a request on ", req.uri.path, " with method ", req.method)
return OK(req.body_raw)

@always_inline
fn hello(req: HTTPRequest) -> HTTPResponse:
return OK("Hello 🔥!")
"""Simple hello world function.

Args:
req: Any arbitrary HTTP request.

Returns:
HTTPResponse: 200 OK with a Hello World message.

Tags:
hello.
"""
return OK("Hello 🔥!", "text/plain; charset=utf-8")

fn main() raises:
var app = App()

app.get("/", hello)
app.post("/", printer)
app.get("/", hello, "hello")
app.post("/printer", printer, "printer")

app.start_server()

27 changes: 0 additions & 27 deletions lightbug_api/__init__.mojo
Original file line number Diff line number Diff line change
@@ -1,27 +0,0 @@
from lightbug_http import HTTPRequest, HTTPResponse, SysServer, NotFound
from lightbug_api.routing import Router


@value
struct App:
var router: Router

fn __init__(inout self):
self.router = Router()

fn func(self, req: HTTPRequest) raises -> HTTPResponse:
for route_ptr in self.router.routes:
var route = route_ptr[]
if route.path == req.uri.path and route.method == req.method:
return route.handler(req)
return NotFound(req.uri.path)

fn get(inout self, path: String, handler: fn (HTTPRequest) -> HTTPResponse):
self.router.add_route(path, "GET", handler)

fn post(inout self, path: String, handler: fn (HTTPRequest) -> HTTPResponse):
self.router.add_route(path, "POST", handler)

fn start_server(inout self, address: StringLiteral = "0.0.0.0:8080") raises:
var server = SysServer()
server.listen_and_serve(address, self)
105 changes: 105 additions & 0 deletions lightbug_api/app.mojo
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
from os import mkdir
from os.path import exists
from pathlib import Path
from sys.ffi import external_call
from lightbug_http import HTTPRequest, HTTPResponse, Server, NotFound
from external.emberjson import JSON, Array, Object, Value, to_string
from lightbug_api.openapi.generate import OpenAPIGenerator
from lightbug_api.routing import Router
from lightbug_api.logger import logger
from lightbug_api.docs import DocsApp

@value
struct App:
var router: Router
var lightbug_dir: Path
var docs_enabled: Bool

fn __init__(inout self) raises:
self.router = Router()
self.lightbug_dir = Path()
self.docs_enabled = True

fn __init__(inout self, docs_enabled: Bool) raises:
self.router = Router()
self.lightbug_dir = Path()
self.docs_enabled = docs_enabled

fn set_lightbug_dir(mut self, lightbug_dir: Path):
self.lightbug_dir = lightbug_dir

fn func(mut self, req: HTTPRequest) raises -> HTTPResponse:
if self.docs_enabled and req.uri.path == "/docs" and req.method == "GET":
var openapi_spec = self.generate_openapi_spec()
var docs = DocsApp(to_string(openapi_spec))
return docs.func(req)
for route_ptr in self.router.routes:
var route = route_ptr[]
if route.path == req.uri.path and route.method == req.method:
return route.handler(req)
return NotFound(req.uri.path)

fn get(mut self, path: String, handler: fn (HTTPRequest) -> HTTPResponse, operation_id: String):
self.router.add_route(path, "GET", handler, operation_id)

fn post(mut self, path: String, handler: fn (HTTPRequest) -> HTTPResponse, operation_id: String):
self.router.add_route(path, "POST", handler, operation_id)

fn put(mut self, path: String, handler: fn (HTTPRequest) -> HTTPResponse, operation_id: String):
self.router.add_route(path, "PUT", handler, operation_id)

fn delete(mut self, path: String, handler: fn (HTTPRequest) -> HTTPResponse, operation_id: String):
self.router.add_route(path, "DELETE", handler, operation_id)

fn update_temporary_files(mut self) raises:
var routes_obj = Object()
var routes = List[Value]()

for route_ptr in self.router.routes:
var route = route_ptr[]
var route_obj = Object()
route_obj["path"] = route.path
route_obj["method"] = route.method
route_obj["handler"] = route.operation_id
routes.append(route_obj)

routes_obj["routes"] = Array.from_list(routes)
var cwd = Path()
var lightbug_dir = cwd / ".lightbug"
self.set_lightbug_dir(lightbug_dir)

if not exists(lightbug_dir):
logger.info("Creating .lightbug directory")
mkdir(lightbug_dir)

with open((lightbug_dir / "routes.json"), "w") as f:
f.write(to_string[pretty=True](routes_obj))

var mojodoc_status = external_call["system", UInt8]("magic run mojo doc ./lightbug.🔥 -o " + lightbug_dir.__str__() + "/mojodoc.json")
if mojodoc_status != 0:
logger.error("Failed to generate mojodoc.json")
return

fn generate_openapi_spec(self) raises -> JSON:
var generator = OpenAPIGenerator()

var mojo_doc_json = generator.read_mojo_doc(
(self.lightbug_dir / "mojodoc.json").__str__()
)
var router_metadata_json = generator.read_router_metadata(
(self.lightbug_dir / "routes.json").__str__()
)

var openapi_spec = generator.generate_spec(mojo_doc_json, router_metadata_json)
generator.save_spec(
openapi_spec,
(self.lightbug_dir / "openapi_spec.json").__str__()
)
return openapi_spec

fn start_server(mut self, address: StringLiteral = "0.0.0.0:8080") raises:
if self.docs_enabled:
logger.info("API Docs ready at: " + "http://" + String(address) + "/docs")
self.update_temporary_files()
var server = Server()
server.listen_and_serve(address, self)
37 changes: 37 additions & 0 deletions lightbug_api/docs.mojo
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
from lightbug_http import Server, HTTPRequest, HTTPResponse, OK
from lightbug_api.logger import logger

@value
struct DocsApp:
var openapi_spec: String

fn func(mut self, req: HTTPRequest) raises -> HTTPResponse:
var html_response = String("""
<!doctype html>
<html>
<head>
<title>Scalar API Reference</title>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1" />
</head>
<body>
<script
id="api-reference"
type="application/json">
{}
</script>
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
</body>
</html>
""").format(self.openapi_spec)
return OK(html_response, "text/html; charset=utf-8")

fn set_openapi_spec(mut self, openapi_spec: String):
self.openapi_spec = openapi_spec

fn start_docs_server(mut self, address: StringLiteral = "0.0.0.0:8888") raises:
logger.info("Starting docs at " + String(address))
var server = Server()
server.listen_and_serve(address, self)
66 changes: 66 additions & 0 deletions lightbug_api/logger.mojo
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
from memory import memcpy, Span

struct LogLevel():
alias FATAL = 0
alias ERROR = 1
alias WARN = 2
alias INFO = 3
alias DEBUG = 4


@value
struct Logger():
var level: Int

fn __init__(out self, level: Int = LogLevel.INFO):
self.level = level

fn _log_message(self, message: String, level: Int):
if self.level >= level:
if level < LogLevel.WARN:
print(message, file=2)
else:
print(message)

fn info[*Ts: Writable](self, *messages: *Ts):
var msg = String.write("\033[36mINFO\033[0m - ")
@parameter
fn write_message[T: Writable](message: T):
msg.write(message, " ")
messages.each[write_message]()
self._log_message(msg, LogLevel.INFO)

fn warn[*Ts: Writable](self, *messages: *Ts):
var msg = String.write("\033[33mWARN\033[0m - ")
@parameter
fn write_message[T: Writable](message: T):
msg.write(message, " ")
messages.each[write_message]()
self._log_message(msg, LogLevel.WARN)

fn error[*Ts: Writable](self, *messages: *Ts):
var msg = String.write("\033[31mERROR\033[0m - ")
@parameter
fn write_message[T: Writable](message: T):
msg.write(message, " ")
messages.each[write_message]()
self._log_message(msg, LogLevel.ERROR)

fn debug[*Ts: Writable](self, *messages: *Ts):
var msg = String.write("\033[34mDEBUG\033[0m - ")
@parameter
fn write_message[T: Writable](message: T):
msg.write(message, " ")
messages.each[write_message]()
self._log_message(msg, LogLevel.DEBUG)

fn fatal[*Ts: Writable](self, *messages: *Ts):
var msg = String.write("\033[35mFATAL\033[0m - ")
@parameter
fn write_message[T: Writable](message: T):
msg.write(message, " ")
messages.each[write_message]()
self._log_message(msg, LogLevel.FATAL)


alias logger = Logger()
Empty file.
Loading
Loading