Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Automatically generate API Docs #4

Open
wants to merge 12 commits into
base: main
Choose a base branch
from
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:
saviorand marked this conversation as resolved.
Show resolved Hide resolved
self.router = Router()
self.lightbug_dir = Path()
self.docs_enabled = True

fn __init__(inout self, docs_enabled: Bool) raises:
saviorand marked this conversation as resolved.
Show resolved Hide resolved
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")
saviorand marked this conversation as resolved.
Show resolved Hide resolved
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__()
saviorand marked this conversation as resolved.
Show resolved Hide resolved
)
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:
saviorand marked this conversation as resolved.
Show resolved Hide resolved
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
saviorand marked this conversation as resolved.
Show resolved Hide resolved

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