From c81de3bfd4ea5dae3cb0aa62043b3a7db24d1b65 Mon Sep 17 00:00:00 2001 From: Yeongseon Choe Date: Mon, 26 Oct 2026 23:16:10 +0900 Subject: [PATCH 1/2] docs: name OpenAPIRegistry in architecture diagrams --- DESIGN.md | 2 +- README.md | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/DESIGN.md b/DESIGN.md index cc37723..91218f9 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -39,7 +39,7 @@ The architecture separates metadata capture (import-time) from document generati ```mermaid flowchart TD subgraph Registration ["Import-Time Registration"] - DEC["@openapi(...) decorator"] --> REG["Thread-safe Registry
_openapi_registry"] + DEC["@openapi(...) decorator"] --> REG["Thread-safe OpenAPIRegistry
registry.py"] end subgraph Consumers ["On-Demand Consumers"] diff --git a/README.md b/README.md index 5eaead5..fde91df 100644 --- a/README.md +++ b/README.md @@ -102,12 +102,17 @@ parsing, status codes, or response bodies match unless a validation layer such a ```mermaid flowchart LR - DEC["@openapi decorator"] --> REG["_openapi_registry"] + DEC["@openapi decorator"] --> REG["OpenAPIRegistry"] REG --> GEN["spec.generate_openapi_spec()"] GEN --> EP["GET /api/openapi.json | .yaml"] UI["render_swagger_ui()
GET /api/docs"] -.->|browser fetches spec| EP ``` +`registry.py` owns `OpenAPIRegistry` and its process-wide `registry` instance. +Decorators register metadata there; selected-app generation seeds a separate +registry with only that app's handlers. Swagger UI fetches the generated document +and does not read registry state directly. + ## FastAPI comparison **FastAPI also runs on Azure Functions.** The choice is the HTTP programming From 447f6eb587ac3cb29bb1112b081da1d79e6740f7 Mon Sep 17 00:00:00 2001 From: Yeongseon Choe Date: Mon, 26 Oct 2026 23:16:10 +0900 Subject: [PATCH 2/2] docs: clarify registry ownership and app-scoped discovery --- docs/architecture.md | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index 45c9159..b7d7f3d 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -17,7 +17,7 @@ The architecture operates in two phases: import-time registration and on-demand 1. Python imports function modules. 2. `@openapi(...)` decorator executes and registers operation metadata. -3. Metadata is stored in the thread-safe `_openapi_registry`. +3. Metadata is stored in the thread-safe `registry` instance of `OpenAPIRegistry`, owned by `registry.py`. ### Phase 2: On-Demand Consumption @@ -26,7 +26,7 @@ sequenceDiagram participant Client participant Endpoint as Function Endpoint participant Gen as generate_openapi_spec() - participant Reg as _openapi_registry + participant Reg as OpenAPIRegistry participant UI as render_swagger_ui() rect rgb(240, 248, 255) @@ -62,7 +62,7 @@ sequenceDiagram participant Host as Azure Functions Host participant Worker as Python Worker participant Handler as Function Handler - participant Reg as _openapi_registry + participant Reg as OpenAPIRegistry rect rgb(240, 248, 255) note over Worker,Reg: Import Time (startup) @@ -100,7 +100,11 @@ Decorator order is a valid degree of freedom. When `@openapi` is applied *below* ### `--isolate-app` fails closed -By default discovery seeds into the process-wide global registry, matching the common single-app layout. When several apps are imported in one process, `generate --isolate-app` scans the `--app` target into a fresh, app-scoped registry so each spec is limited to its own routes. Isolation **fails closed**: an explicit `--isolate-app` that cannot be honored (no `--app`, or an `--app` without a resolvable `:variable`) prints an error and exits non-zero rather than silently emitting a non-isolated spec with a success code (`src/azure_functions_openapi/cli.py:171-179,246-253`). +Direct `scan_endpoint_metadata(app)` calls default to the process-wide registry. +Python generation with `app=` and CLI `--app module:variable` instead seed a fresh +app-scoped `OpenAPIRegistry` by canonical handler identity. The compatibility flag +`--isolate-app` still **fails closed** if no resolvable `module:variable` is supplied. +Python callers injecting `registry=` retain caller-owned additive behavior. ### Mismatch consequences @@ -111,7 +115,8 @@ Because route and method are binding-first, `@openapi(route=...)` / `@openapi(me ```mermaid flowchart TD INIT["__init__.py
Public API exports"] - DEC["decorator.py
@openapi + registry"] + DEC["decorator.py
@openapi metadata"] + REG["registry.py
OpenAPIRegistry + registry"] OAI["spec.py / generate_openapi_spec()
Spec compiler"] UTL["utils.py
Schema extraction + validation"] SUI["swagger_ui.py
Swagger UI rendering"] @@ -124,7 +129,8 @@ flowchart TD INIT --> EXC DEC --> UTL DEC --> EXC - OAI --> DEC + DEC --> REG + OAI --> REG OAI --> UTL OAI --> EXC UTL --> EXC @@ -136,7 +142,7 @@ flowchart TD - Provides `@openapi(...)` decorator. - Validates and sanitizes decorator inputs. -- Stores operation metadata in `_openapi_registry` (protected by `threading.RLock`). +- Registers operation metadata in `registry.py`'s shared `registry` instance. - Exposes `get_openapi_registry()` snapshot accessor. - Tags default to `['default']` when not provided; invalid route path or operation ID raises `ValueError`. @@ -147,6 +153,12 @@ flowchart TD - Supports OpenAPI 3.0.0, 3.1.0, and 3.2.0 output (3.1/3.2 convert `nullable` to union types, `example` to `examples`; 3.2.0 is a backward-compatible superset of 3.1.0). - Resolves routes, methods, request/response schemas, Pydantic model components, and security schemes. +### `registry.py` + +- Owns the thread-safe `OpenAPIRegistry` container and default `registry` instance. +- Guards metadata access with an `RLock` and provides detached snapshots. +- Supports isolated app-scoped generation without another registry implementation. + ### `utils.py` - Pydantic v2 schema extraction (`model_to_schema`). @@ -235,7 +247,7 @@ Every ceiling is more than three times the measured baseline. A separate, noise- ### Thread-Safe Registration -The `_openapi_registry` is protected by `threading.RLock`, ensuring safe concurrent decorator execution during module import. +`OpenAPIRegistry` protects its entries with `threading.RLock`, ensuring safe concurrent decorator execution during module import. ### Extension Points @@ -247,7 +259,7 @@ The `_openapi_registry` is protected by `threading.RLock`, ensuring safe concurr `register_openapi_metadata()` in `decorator.py` allows external packages to register route metadata without using the `@openapi` decorator. This is the integration contract for ecosystem packages that define their own HTTP endpoints and want OpenAPI documentation generated by this package. -The function accepts a similar core metadata shape to `@openapi(...)` (path, method, operation_id, summary, request/response schemas, etc.) and writes directly to the shared `_openapi_registry`. Once registered, routes appear in the generated spec alongside decorator-registered routes. +The function accepts a similar core metadata shape to `@openapi(...)` (path, method, operation_id, summary, request/response schemas, etc.) and writes to the shared `registry` unless one is injected. Unbound programmatic entries appear in registry-based generation, but are not seeded into selected-app generation. **Reference consumer:** [`azure-functions-langgraph`](https://github.com/yeongseon/azure-functions-langgraph-python) uses its bridge module (`azure_functions_langgraph.openapi.register_with_openapi`) to read route metadata from its `get_app_metadata()` API and forward it to `register_openapi_metadata()`. This pattern demonstrates how any Azure Functions package can contribute routes to the OpenAPI spec without depending on the `@openapi` decorator. @@ -264,7 +276,7 @@ The architecture intentionally keeps bridge implementation in the *consumer* pac - OpenAPI spec generation from decorated handlers and programmatic metadata - Swagger UI rendering with security defaults - CLI spec generation for CI pipelines -- The `_openapi_registry` as the single source of truth for operation metadata +- `OpenAPIRegistry` as the operation-metadata container, with a shared default and isolated app-scoped instances - `register_openapi_metadata()` as the integration contract for ecosystem packages ## What this package does not own