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
4 changes: 4 additions & 0 deletions README.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,8 @@ from azure_functions_openapi import (

app = func.FunctionApp()

> `@openapi` を `@app.route` の下(最内層)に置くと、ルートバインディングより先に実行されます。この順序では、正しいパスを検出するため、パッケージルートの `generate_openapi_spec(app=app)`、`get_openapi_json(app=app)`、`get_openapi_yaml(app=app)`、または `generate_openapi_report(app=app)` を使用してください。`app=` なしでは、検証できないルートに対して非致命的な `unresolved-route` 警告が発生し、関数名へフォールバックします。このフォールバックが Azure の正しい既定ルートである場合があるため、`strict=True` でもエラーにはなりません。


# 普通の Pydantic モデルで API を記述します。
class GreetRequest(BaseModel):
Expand Down Expand Up @@ -209,6 +211,7 @@ def openapi_json(req: func.HttpRequest) -> func.HttpResponse:
get_openapi_json(
title="Sample API",
description="OpenAPI document for the Sample API.",
app=app,
),
mimetype="application/json",
)
Expand All @@ -220,6 +223,7 @@ def openapi_yaml(req: func.HttpRequest) -> func.HttpResponse:
get_openapi_yaml(
title="Sample API",
description="OpenAPI document for the Sample API.",
app=app,
),
mimetype="application/x-yaml",
)
Expand Down
4 changes: 4 additions & 0 deletions README.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,8 @@ from azure_functions_openapi import (

app = func.FunctionApp()

> `@openapi`가 `@app.route` 아래(가장 안쪽)에 있으면 라우트 바인딩보다 먼저 실행됩니다. 이 순서를 사용할 때는 정확한 경로를 검색하도록 패키지 루트의 `generate_openapi_spec(app=app)`, `get_openapi_json(app=app)`, `get_openapi_yaml(app=app)` 또는 `generate_openapi_report(app=app)`를 사용하세요. `app=` 없이 생성하면 확인할 수 없는 경로에 대해 치명적이지 않은 `unresolved-route` 경고가 발생하고 함수 이름으로 대체됩니다. 이 대체 경로가 Azure의 올바른 기본 경로일 수 있으므로 `strict=True`에서도 오류가 발생하지 않습니다.


# 일반 Pydantic 모델로 API를 기술합니다.
class GreetRequest(BaseModel):
Expand Down Expand Up @@ -209,6 +211,7 @@ def openapi_json(req: func.HttpRequest) -> func.HttpResponse:
get_openapi_json(
title="Sample API",
description="OpenAPI document for the Sample API.",
app=app,
),
mimetype="application/json",
)
Expand All @@ -220,6 +223,7 @@ def openapi_yaml(req: func.HttpRequest) -> func.HttpResponse:
get_openapi_yaml(
title="Sample API",
description="OpenAPI document for the Sample API.",
app=app,
),
mimetype="application/x-yaml",
)
Expand Down
15 changes: 10 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -148,11 +148,14 @@ azure-functions-openapi generate --app function_app --title "My API" --pretty --
azure-functions-openapi generate --app function_app --format yaml --output openapi.yaml
```

Pass `module:variable` to resolve the `FunctionApp` instance and also discover
endpoint-metadata routes — those registered by producers like `@validate_http`
or `azure-functions-langgraph` — merging them with your `@openapi` routes into a
single spec. With `module` alone the CLI imports the module (firing `@openapi`
decorators) but does not scan for endpoint metadata:
Pass `module:variable` to resolve and scan the completed `FunctionApp`. This is
required when `@openapi` is below `@app.route`, because the inner decorator runs
before the route binding exists. The scan also discovers endpoint-metadata routes
from producers like `@validate_http` or `azure-functions-langgraph`. With
`module` alone, routes that cannot be verified emit a non-fatal
`unresolved-route` warning and fall back to the function name. This warning
remains non-fatal under `strict=True`, because the fallback can be Azure's
correct default route:

```bash
azure-functions-openapi generate --app function_app:app --title "My API"
Expand Down Expand Up @@ -263,6 +266,7 @@ def openapi_json(req: func.HttpRequest) -> func.HttpResponse:
get_openapi_json(
title="Sample API",
description="OpenAPI document for the Sample API.",
app=app,
),
mimetype="application/json",
)
Expand All @@ -274,6 +278,7 @@ def openapi_yaml(req: func.HttpRequest) -> func.HttpResponse:
get_openapi_yaml(
title="Sample API",
description="OpenAPI document for the Sample API.",
app=app,
),
mimetype="application/x-yaml",
)
Expand Down
4 changes: 4 additions & 0 deletions README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,8 @@ from azure_functions_openapi import (

app = func.FunctionApp()

> 当 `@openapi` 位于 `@app.route` 下方(最内层)时,它会在路由绑定存在之前执行。使用此顺序时,请通过包根目录的 `generate_openapi_spec(app=app)`、`get_openapi_json(app=app)`、`get_openapi_yaml(app=app)` 或 `generate_openapi_report(app=app)` 扫描应用以获得正确路径。省略 `app=` 时,无法验证的路由会发出非致命的 `unresolved-route` 警告并回退到函数名。由于该回退可能正是 Azure 的默认路由,因此即使 `strict=True` 也不会报错。


# 使用普通的 Pydantic 模型描述你的 API。
class GreetRequest(BaseModel):
Expand Down Expand Up @@ -209,6 +211,7 @@ def openapi_json(req: func.HttpRequest) -> func.HttpResponse:
get_openapi_json(
title="Sample API",
description="OpenAPI document for the Sample API.",
app=app,
),
mimetype="application/json",
)
Expand All @@ -220,6 +223,7 @@ def openapi_yaml(req: func.HttpRequest) -> func.HttpResponse:
get_openapi_yaml(
title="Sample API",
description="OpenAPI document for the Sample API.",
app=app,
),
mimetype="application/x-yaml",
)
Expand Down
15 changes: 13 additions & 2 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,17 @@ from azure_functions_openapi import (
!!! note
`get_openapi_json()` and `get_openapi_yaml()` return strings, not `HttpResponse`. Wrap the returned value in `func.HttpResponse` in your Azure Function route.

!!! warning
If `@openapi` is the innermost decorator (below `@app.route`), it runs before
the route binding exists. Pass the completed app to the package-root
`generate_openapi_spec(app=app)`, `get_openapi_json(app=app)`,
`get_openapi_yaml(app=app)`, or `generate_openapi_report(app=app)`. Without
binding evidence, generation emits a non-fatal `unresolved-route` warning
before falling back to the function name. The warning remains non-fatal
under `strict=True`, because the fallback can be Azure's correct default
route. The low-level functions in
`azure_functions_openapi.spec` intentionally remain SDK-free registry compilers.

## Common usage patterns

### Minimal endpoint
Expand Down Expand Up @@ -177,12 +188,12 @@ def search(req: func.HttpRequest) -> func.HttpResponse:
```python
@app.route(route="openapi.json", methods=["GET"])
def openapi_json(req: func.HttpRequest) -> func.HttpResponse:
return func.HttpResponse(get_openapi_json(title="My API", version="1.0.0"), mimetype="application/json")
return func.HttpResponse(get_openapi_json(title="My API", version="1.0.0", app=app), mimetype="application/json")


@app.route(route="openapi.yaml", methods=["GET"])
def openapi_yaml(req: func.HttpRequest) -> func.HttpResponse:
return func.HttpResponse(get_openapi_yaml(title="My API", version="1.0.0"), mimetype="application/x-yaml")
return func.HttpResponse(get_openapi_yaml(title="My API", version="1.0.0", app=app), mimetype="application/x-yaml")


@app.route(route="docs", methods=["GET"])
Expand Down
4 changes: 2 additions & 2 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ The registry is consumed only when a client explicitly requests the spec (`GET /

## How Route Discovery Works

Discovery is how this package finds every HTTP route on a `FunctionApp` and reconciles it with `@openapi(...)` metadata. It runs at **import time** inside `function_app.py` (via `scan_endpoint_metadata`) or on demand from the CLI (`generate --app module:variable`). Route and method come **binding-first**: the Azure `@app.route(...)` binding is the source of truth, and `@openapi(...)` only enriches the operation the binding already defines. For the shared `FunctionBuilder` background this relies on, see [How the worker binds handlers §2](https://yeongseon.dev/azure-functions-python/platform/how-the-worker-binds-handlers/).
Discovery is how this package finds every HTTP route on a `FunctionApp` and reconciles it with `@openapi(...)` metadata. It runs at **import time** inside `function_app.py` (via `scan_endpoint_metadata`), when `generate_openapi_spec(app=app)` receives the app, or on demand from the CLI (`generate --app module:variable`). Route and method come **binding-first**: the Azure `@app.route(...)` binding is the source of truth, and `@openapi(...)` only enriches the operation the binding already defines. For the shared `FunctionBuilder` background this relies on, see [How the worker binds handlers §2](https://yeongseon.dev/azure-functions-python/platform/how-the-worker-binds-handlers/).

### Enumerating functions without breaking boot

Expand All @@ -96,7 +96,7 @@ Because `build()` is idempotent, **re-scanning is safe**: repeated scans neither

### `@openapi` below `@app.route`

Decorator order is a valid degree of freedom. When `@openapi` is applied *below* `@app.route`, the builder has no trigger yet, so `FunctionBuilder.build()` raises `ValueError` and the function is skipped by the normal built-`Function` path. The user handler still exists on the builder, so `get_unbuilt_user_handler()` reads it defensively via the public `Function.get_user_function()` accessor — a guarded, **side-effect-free** inspection that leaves a later `build()` (once the outer `@app.route` applies the trigger) valid and idempotent (`src/azure_functions_openapi/adapters/azure_functions.py:182-206`). In this ordering `@openapi` never sees the HTTP binding and registers with `method=None`; discovery then explodes that single entry into one operation per bound method rather than collapsing every method into one.
Decorator order is a valid degree of freedom. When `@openapi` is applied *below* `@app.route`, it runs before the route decorator and cannot see the binding. Generate with `generate_openapi_spec(app=app)` or call `scan_endpoint_metadata(app)` before generating so discovery can reconcile the registered metadata with the completed binding. It then uses the binding route and explodes the unresolved method into one operation per bound method. Without an app scan, a bare registry has no evidence of a decorator that has not run yet and keeps the historical function-name fallback.

### `--isolate-app` fails closed

Expand Down
37 changes: 20 additions & 17 deletions docs/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,26 +66,28 @@ Generate OpenAPI 3.1 output:
azure-functions-openapi generate --openapi-version 3.1 --output openapi-3.1.json
```

Import your function app so routes are registered:
Import and scan your function app so decorator routes use their real bindings:

```bash
azure-functions-openapi generate --app function_app --title "Todo API"
azure-functions-openapi generate --app function_app:app --title "Todo API"
```

Pretty-print JSON output:

```bash
azure-functions-openapi generate --app function_app --pretty --output openapi.json
azure-functions-openapi generate --app function_app:app --pretty --output openapi.json
```

### Endpoint-metadata discovery (`module:variable`)

`@openapi`-decorated routes register themselves the moment the module is
imported, so `--app module` is enough to see them. Producers that register
**only** through the shared endpoint-metadata namespace — `@validate_http`
(azure-functions-validation), `azure-functions-langgraph`, and other
third-party producers — are attached to the live `FunctionApp` object and are
discovered by scanning it.
`@openapi`-decorated routes register themselves when the module is imported,
but an innermost `@openapi` (below `@app.route`) runs before its route binding
exists. Therefore `--app module:variable` is required to recover the real route.
It is also required for producers that register **only** through the shared
endpoint-metadata namespace — `@validate_http`
(azure-functions-validation), `azure-functions-langgraph`, and others — because
their metadata is attached to the live `FunctionApp` object and discovered by
scanning it.

To include those endpoints, pass an explicit `module:variable` so the CLI can
resolve the `FunctionApp` instance and run discovery on it:
Expand All @@ -98,10 +100,11 @@ azure-functions-openapi generate --app function_app:app --output openapi.json

Discovery semantics:

- **`--app function_app`** (module only): the module is imported so `@openapi`
decorators fire, but endpoint-metadata discovery is **not** run — the CLI
never guesses the `FunctionApp` variable name. A note is printed reminding
you to pass `module:variable` to also discover endpoint-metadata routes.
- **`--app function_app`** (module only): decorators fire, but binding discovery
is **not** run. An innermost `@openapi` therefore emits an `unresolved-route`
warning and uses the historical function-name fallback. This warning remains
non-fatal with `--strict`, because the fallback can be Azure's correct default
route. The CLI never guesses the `FunctionApp` variable name.
- **`--app function_app:app`** (module + variable): the named `FunctionApp`
attribute is resolved and scanned, merging endpoint-metadata routes into the
same registry as the decorator routes.
Expand All @@ -120,7 +123,7 @@ Discovery semantics:

| Option | Alias | Values | Default | Description |
| --- | --- | --- | --- | --- |
| `--app` | - | `module` or `module:var` | - | Import module so `@openapi` decorators register routes; with `module:var`, also resolve the `FunctionApp` and discover endpoint-metadata routes (see [Endpoint-metadata discovery](#endpoint-metadata-discovery-modulevariable)) |
| `--app` | - | `module` or `module:var` | - | Import the module; use `module:var` to scan route bindings (required for innermost `@openapi`) and endpoint metadata |
| `--title` | - | any string | `API` | OpenAPI `info.title` |
| `--version` | - | any string | `1.0.0` | OpenAPI `info.version` |
| `--description` | - | any string | library default | OpenAPI `info.description` (Markdown / CommonMark supported) |
Expand All @@ -131,7 +134,7 @@ Discovery semantics:
| `--route-prefix` | - | any string (or `""`) | `/api` | HTTP route prefix from `host.json` `extensions.http.routePrefix`. See [Route Prefix](route-prefix.md). |
| `--fail-on-empty-paths` | - | flag | `false` | Exit with code 1 if the generated spec has no paths |
| `--strict` | - | flag | `false` | Fail on any malformed registry entry instead of skipping it. Recommended for CI where a missing path should break the build |
| `--fail-on-warnings` | - | flag | `false` | Exit with code 2 if the generator emits any structured warnings (version skew, namespace fallback, or spec-validation issues). Use in CI to stop a wrong-but-plausible spec from being published |
| `--fail-on-warnings` | - | flag | `false` | Exit with code 2 if the generator emits any structured warnings (version skew, namespace fallback, or spec-validation issues; the advisory `unresolved-route` warning never fails). Use in CI to stop a wrong-but-plausible spec from being published |
| `--isolate-app` | - | flag | `false` | Scan the `--app` `FunctionApp` into a fresh, app-scoped registry instead of the shared global one. Requires `--app module:variable`. Use when several apps are imported in one process to avoid cross-app route leakage |

## Exit codes
Expand Down Expand Up @@ -190,10 +193,10 @@ jobs:
### Empty `paths` in output

- Ensure app handlers are decorated with `@openapi`
- Pass `--app <module>` so decorated routes are registered before generation:
- Pass `--app <module>:<variable>` so decorated routes and bindings are reconciled:

```bash
azure-functions-openapi generate --app function_app --title "My API"
azure-functions-openapi generate --app function_app:app --title "My API"
```

- Use `module:variable` syntax when the `FunctionApp` instance is not named `app`:
Expand Down
7 changes: 5 additions & 2 deletions llms-full.txt
Original file line number Diff line number Diff line change
Expand Up @@ -187,10 +187,11 @@ def generate_openapi_spec(
license: dict[str, Any] | None = None,
external_docs: dict[str, Any] | None = None,
tags: list[dict[str, Any]] | None = None,
app: object | None = None,
) -> dict[str, Any]: ...
```

**Purpose**: Compile all registered endpoints into an OpenAPI specification dictionary.
**Purpose**: At the package root, optionally scan `app` and then compile all registered endpoints. Pass `app=app` when `@openapi` is below `@app.route`; the inner decorator runs before the binding exists. The JSON, YAML, and report facades accept the same option. Without route evidence, generation emits a non-fatal structured `unresolved-route` warning before using the function-name fallback, including under `strict=True`, because the fallback can be Azure's correct default route. `azure_functions_openapi.spec` remains the SDK-free low-level registry compiler.

**Returns**: OpenAPI spec dict (can be JSON-serialized or converted to YAML).

Expand All @@ -214,6 +215,7 @@ def get_openapi_json(
registry: OpenAPIRegistry | None = None,
hoist_flat_schemas: bool = False,
infer_auth_level: bool = False,
app: object | None = None,
) -> str: ...
```

Expand Down Expand Up @@ -384,7 +386,8 @@ def openapi_spec(req: func.HttpRequest) -> func.HttpResponse:
spec = get_openapi_json(
title="My API",
version="1.0.0",
description="My Azure Functions API"
description="My Azure Functions API",
app=app,
)
return func.HttpResponse(spec, mimetype="application/json")

Expand Down
7 changes: 4 additions & 3 deletions llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ Renders Swagger UI with enterprise security headers (CSP, HSTS).
- `@openapi(summary, description, ...)` — Decorator for HTTP handlers
- `register_openapi_metadata(path, method, ...)` — Programmatic metadata registration
- `clear_openapi_registry()` — Clear registry for fresh start
- `generate_openapi_spec(title, version, ...)` — Generate spec dict
- `get_openapi_json(title, version, ...)` — Generate OpenAPI JSON string
- Package-root `generate_openapi_spec(..., app=app)` — scan a completed FunctionApp, then generate a spec dict
- Package-root `get_openapi_json(..., app=app)` / `get_openapi_yaml(..., app=app)` — scan then serialize; pass `app=` when `@openapi` is below `@app.route`
- Unverified routes emit a non-fatal structured `unresolved-route` warning and use the function-name fallback, including under `strict=True`
- `get_openapi_yaml(title, version, ...)` — Generate OpenAPI YAML string
- `render_swagger_ui(title, openapi_url, ...)` — Render Swagger UI HTML
- `OpenAPIOperationMetadata` — Type for operation metadata
Expand Down Expand Up @@ -56,7 +57,7 @@ def greet(req: func.HttpRequest) -> func.HttpResponse:
# Expose OpenAPI spec
@app.route(route="openapi.json")
def openapi_spec(req: func.HttpRequest) -> func.HttpResponse:
spec = get_openapi_json(title="My API", version="1.0.0")
spec = get_openapi_json(title="My API", version="1.0.0", app=app)
return func.HttpResponse(spec, mimetype="application/json")
```

Expand Down
Loading
Loading