From ad389aa6c21ea06f3c7d849484f1f2cab5a6357b Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Sun, 26 Apr 2026 19:09:16 +0300 Subject: [PATCH] docs+test: OpenAPI serve toggle, route order, re-enable (issue #16) - Document same flag for constructor and set_openapi_served, registration order, re-enable after include_openapi=False, built-in shadowing of /openapi.json - Add ASGI test: set_openapi_served(True) after constructor False serves spec --- docs/openapi.md | 5 ++++- tests/test_openapi.py | 20 ++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/docs/openapi.md b/docs/openapi.md index 117db47..6a7c768 100644 --- a/docs/openapi.md +++ b/docs/openapi.md @@ -9,8 +9,11 @@ OxyRoute maintains a small **OpenAPI 3.0**-shaped JSON document in Rust while ro 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. +- **Order does not change the in-memory spec:** routes are merged into the OpenAPI document as you register them, no matter when you flip the serving toggle. You can call **`set_openapi_served` before or after** adding routes; only HTTP exposure of `GET/HEAD /openapi.json` changes. +- **Re-enable:** an app created with **`include_openapi=False`** can still call **`set_openapi_served(True)`** to start serving the spec at `GET/HEAD /openapi.json` (same for turning back on after `set_openapi_served(False)`). +- **When serving is on** (`include_openapi=True` and not since disabled): the dispatcher answers **`GET /openapi.json`** and **`HEAD /openapi.json`** in an early check, **before** the normal router. A user-registered route on exactly `/openapi.json` will **not** run for those methods while the built-in is active. - **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**. + - 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**. If you **do** register a handler for `/openapi.json`, that handler can serve a custom response. - 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. diff --git a/tests/test_openapi.py b/tests/test_openapi.py index 9dc4603..2fa491b 100644 --- a/tests/test_openapi.py +++ b/tests/test_openapi.py @@ -70,6 +70,26 @@ async def _run() -> None: assert "/z" in json.loads(app.openapi_json())["paths"] +def test_include_openapi_false_reenabled_with_set_openapi_served_get_200() -> None: + app = App(title="R", include_openapi=False) + app.set_openapi_served(True) + + @app.get("/b") + def b() -> str: + return "b" + + 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 == 200, r.text + doc = r.json() + assert doc["info"]["title"] == "R" + assert "/b" in doc["paths"] + + asyncio.run(_run()) + + def test_openapi_post_request_body_from_pydantic() -> None: pydantic = pytest.importorskip("pydantic")