diff --git a/docs/openapi.md b/docs/openapi.md index 7f759ed..364757f 100644 --- a/docs/openapi.md +++ b/docs/openapi.md @@ -6,13 +6,18 @@ OxyRoute maintains a small **OpenAPI 3.0**-shaped JSON document in Rust while ro ## Constructor and toggles -- **`App(..., include_openapi=True)`** (Python) maps to the native `App` constructor. When `include_openapi` is false, the special **`GET /openapi.json`** response from the engine is not served, and the document may be empty in those modes depending on the implementation. -- **`set_openapi_served(False)`** can disable serving the document at runtime by flipping the same flag in state (naming mirrors the native `set_openapi_served`). +Served vs built: one flag, two ways to set it. + +- **`include_openapi`** (constructor) and **`set_openapi_served(enabled)`** both update the same native field (`AppState::include_openapi`). Use the constructor to choose the default; use **`set_openapi_served(False)`** to turn off HTTP serving later (or `True` to re-enable) without recreating the app. +- **While serving is off** (`include_openapi=False` at build time, or after `set_openapi_served(False)`): + - The engine does **not** return the spec for **`GET /openapi.json`** or **`HEAD /openapi.json`**. The request is **not** special-cased, so it goes through normal routing. Unless you add your own handler for that path, the client usually gets **404 Not Found**. + - Route registration still **merges** operations into the in-memory OpenAPI document. The document is not discarded. +- **Export:** **`openapi_json()`** on the Python `App` still returns the current JSON as a string (handy in tests, admin tools, or a custom response), regardless of the serving toggle. ## Title and export - **`set_openapi_title`** is applied from `App(..., title="...")` at construction. -- **`openapi_json()`** on the Python `App` returns the current document as a **string** (for debugging or an alternate transport). +- **`openapi_json()`** is described above; it is independent of whether `/openapi.json` is exposed over HTTP. ## What is in the document today diff --git a/tests/test_openapi.py b/tests/test_openapi.py index eff9c71..202f593 100644 --- a/tests/test_openapi.py +++ b/tests/test_openapi.py @@ -1,5 +1,7 @@ +import asyncio import json +import httpx from oxyroute import App @@ -25,3 +27,43 @@ def m() -> str: doc = json.loads(app.openapi_json()) assert doc["paths"]["/m"]["patch"]["operationId"] == "m" + + +def test_openapi_serving_off_constructor_get_and_head_404_openapi_json_still_filled() -> None: + app = App(title="S", include_openapi=False) + + @app.get("/a") + def a() -> str: + return "1" + + async def _run() -> None: + transport = httpx.ASGITransport(app=app) + async with httpx.AsyncClient(transport=transport, base_url="http://test") as c: + g = await c.get("/openapi.json") + h = await c.request("HEAD", "/openapi.json") + assert g.status_code == 404 + assert h.status_code == 404 + + asyncio.run(_run()) + + doc = json.loads(app.openapi_json()) + assert doc["info"]["title"] == "S" + assert "/a" in doc["paths"] + + +def test_set_openapi_served_false_stops_serving_still_exports_json() -> None: + app = App() + app.set_openapi_served(False) + + @app.get("/z") + def z() -> str: + return "z" + + async def _run() -> None: + transport = httpx.ASGITransport(app=app) + async with httpx.AsyncClient(transport=transport, base_url="http://test") as c: + r = await c.get("/openapi.json") + assert r.status_code == 404 + + asyncio.run(_run()) + assert "/z" in json.loads(app.openapi_json())["paths"]