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 11 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
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@
*.egg-info

# magic environments
.magic
.magic

.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
30 changes: 25 additions & 5 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()
var app = App[docs_enabled=True]()

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)
94 changes: 94 additions & 0 deletions lightbug_api/app.mojo
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
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 lightbug_http.utils import logger
from emberjson import JSON, Array, Object, Value, to_string
from lightbug_api.openapi.generate import OpenAPIGenerator
from lightbug_api.routing import Router
from lightbug_api.docs import DocsApp


@value
struct App[docs_enabled: Bool = False]:
var router: Router
var lightbug_dir: Path

fn __init__(out self) raises:
self.router = Router()
self.lightbug_dir = Path()

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

fn func(mut self, req: HTTPRequest) raises -> HTTPResponse:
if 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 " + str(lightbug_dir) + "/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(str(self.lightbug_dir / "mojodoc.json"))
var router_metadata_json = generator.read_router_metadata(str(self.lightbug_dir / "routes.json"))

var openapi_spec = generator.generate_spec(mojo_doc_json, router_metadata_json)
generator.save_spec(openapi_spec, str(self.lightbug_dir / "openapi_spec.json"))
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 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)
40 changes: 40 additions & 0 deletions lightbug_api/docs.mojo
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from lightbug_http import Server, HTTPRequest, HTTPResponse, OK
from lightbug_http.utils 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)
Empty file.
Loading
Loading