Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<br/>_openapi_registry"]
DEC["@openapi(...) decorator"] --> REG["Thread-safe OpenAPIRegistry<br/>registry.py"]
end

subgraph Consumers ["On-Demand Consumers"]
Expand Down
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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()<br/>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
Expand Down
32 changes: 22 additions & 10 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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

Expand All @@ -111,7 +115,8 @@ Because route and method are binding-first, `@openapi(route=...)` / `@openapi(me
```mermaid
flowchart TD
INIT["__init__.py<br/>Public API exports"]
DEC["decorator.py<br/>@openapi + registry"]
DEC["decorator.py<br/>@openapi metadata"]
REG["registry.py<br/>OpenAPIRegistry + registry"]
OAI["spec.py / generate_openapi_spec()<br/>Spec compiler"]
UTL["utils.py<br/>Schema extraction + validation"]
SUI["swagger_ui.py<br/>Swagger UI rendering"]
Expand All @@ -124,7 +129,8 @@ flowchart TD
INIT --> EXC
DEC --> UTL
DEC --> EXC
OAI --> DEC
DEC --> REG
OAI --> REG
OAI --> UTL
OAI --> EXC
UTL --> EXC
Expand All @@ -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`.

Expand All @@ -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`).
Expand Down Expand Up @@ -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

Expand All @@ -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.

Expand All @@ -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
Expand Down
Loading