feat: serve API docs at /docs/api

- /docs/api → LiveView rendering README.md as HTML
- /docs/api/openapi.yaml → raw OpenAPI 3.1 spec
- /docs/api/README.md → raw markdown source
This commit is contained in:
Graham McIntire 2026-05-09 09:09:45 -05:00
parent c6d2c48264
commit c56b55d5af
No known key found for this signature in database
GPG key ID: F4ABF488E6029E59
5 changed files with 99 additions and 0 deletions

View file

@ -0,0 +1,35 @@
defmodule MicrowavepropWeb.ApiDocsController do
@moduledoc """
Serves the static `docs/api/` artifacts:
* `GET /docs/api/openapi.yaml` OpenAPI 3.1 spec as `application/yaml`.
* `GET /docs/api/README.md` raw markdown source (for agents that
prefer markdown over the rendered LiveView page).
The human-facing rendered docs live at `/docs/api` via
`MicrowavepropWeb.ApiDocsLive`.
"""
use MicrowavepropWeb, :controller
@external_resource "docs/api/openapi.yaml"
@external_resource "docs/api/README.md"
@openapi_yaml File.read!("docs/api/openapi.yaml")
@readme_markdown File.read!("docs/api/README.md")
@spec openapi_yaml(Plug.Conn.t(), map()) :: Plug.Conn.t()
def openapi_yaml(conn, _params) do
conn
|> put_resp_content_type("application/yaml", "utf-8")
|> put_resp_header("cache-control", "public, max-age=3600")
|> send_resp(200, @openapi_yaml)
end
@spec readme_markdown(Plug.Conn.t(), map()) :: Plug.Conn.t()
def readme_markdown(conn, _params) do
conn
|> put_resp_content_type("text/markdown", "utf-8")
|> put_resp_header("cache-control", "public, max-age=3600")
|> send_resp(200, @readme_markdown)
end
end

View file

@ -0,0 +1,23 @@
defmodule MicrowavepropWeb.ApiDocsLive do
@moduledoc "Renders `docs/api/README.md` as HTML at `/docs/api`."
use MicrowavepropWeb, :live_view
@external_resource "docs/api/README.md"
@docs_html "docs/api/README.md" |> File.read!() |> Microwaveprop.Markdown.to_html!()
@impl true
def mount(_params, _session, socket) do
{:ok, assign(socket, page_title: "API Documentation", content: @docs_html)}
end
@impl true
def render(assigns) do
~H"""
<Layouts.app flash={@flash} current_scope={@current_scope}>
<div class="markdown-content">
{raw(@content)}
</div>
</Layouts.app>
"""
end
end

View file

@ -212,7 +212,13 @@ defmodule MicrowavepropWeb.Router do
get "/weather/cells", WeatherTileController, :cells
get "/scores/cells", ScoresController, :cells
# Static API doc artifacts. Outside the live_session so the
# raw-format endpoints don't get the LiveView root layout.
get "/docs/api/openapi.yaml", ApiDocsController, :openapi_yaml
get "/docs/api/README.md", ApiDocsController, :readme_markdown
live_session :public, on_mount: [{MicrowavepropWeb.UserAuth, :default}] do
live "/docs/api", ApiDocsLive
live "/submit", SubmitLive
live "/imports/:id", ImportLive
live "/map", MapLive

View file

@ -0,0 +1,22 @@
defmodule MicrowavepropWeb.ApiDocsControllerTest do
use MicrowavepropWeb.ConnCase, async: true
describe "GET /docs/api/openapi.yaml" do
test "serves the OpenAPI spec as application/yaml", %{conn: conn} do
conn = get(conn, ~p"/docs/api/openapi.yaml")
assert conn.status == 200
assert get_resp_header(conn, "content-type") == ["application/yaml; charset=utf-8"]
assert conn.resp_body =~ "openapi: 3.1.0"
assert conn.resp_body =~ "/api/v1"
end
end
describe "GET /docs/api/README.md" do
test "serves the README markdown source", %{conn: conn} do
conn = get(conn, ~p"/docs/api/README.md")
assert conn.status == 200
assert get_resp_header(conn, "content-type") == ["text/markdown; charset=utf-8"]
assert conn.resp_body =~ "# Microwaveprop REST API"
end
end
end

View file

@ -0,0 +1,13 @@
defmodule MicrowavepropWeb.ApiDocsLiveTest do
use MicrowavepropWeb.ConnCase, async: true
import Phoenix.LiveViewTest
test "renders the API reference at /docs/api", %{conn: conn} do
{:ok, _view, html} = live(conn, ~p"/docs/api")
assert html =~ "Microwaveprop REST API"
assert html =~ "/api/v1"
assert html =~ "POST /auth/tokens"
end
end