diff --git a/lib/microwaveprop_web/controllers/api_docs_controller.ex b/lib/microwaveprop_web/controllers/api_docs_controller.ex new file mode 100644 index 00000000..79be6aa2 --- /dev/null +++ b/lib/microwaveprop_web/controllers/api_docs_controller.ex @@ -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 diff --git a/lib/microwaveprop_web/live/api_docs_live.ex b/lib/microwaveprop_web/live/api_docs_live.ex new file mode 100644 index 00000000..b8f19043 --- /dev/null +++ b/lib/microwaveprop_web/live/api_docs_live.ex @@ -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""" + +
+ {raw(@content)} +
+
+ """ + end +end diff --git a/lib/microwaveprop_web/router.ex b/lib/microwaveprop_web/router.ex index 777f736a..1f341ae3 100644 --- a/lib/microwaveprop_web/router.ex +++ b/lib/microwaveprop_web/router.ex @@ -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 diff --git a/test/microwaveprop_web/controllers/api_docs_controller_test.exs b/test/microwaveprop_web/controllers/api_docs_controller_test.exs new file mode 100644 index 00000000..e6853118 --- /dev/null +++ b/test/microwaveprop_web/controllers/api_docs_controller_test.exs @@ -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 diff --git a/test/microwaveprop_web/live/api_docs_live_test.exs b/test/microwaveprop_web/live/api_docs_live_test.exs new file mode 100644 index 00000000..00a28752 --- /dev/null +++ b/test/microwaveprop_web/live/api_docs_live_test.exs @@ -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