Files
openapi-first/docs/wiki/02_components.md

209 lines
8.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Components — What Ships in the Box
The library is deliberately small. Everything you need to run an OpenAPI-first service and talk to it with a strict client fits in a handful of modules.
---
## 🗂️ 1. Module Map
| Module | Responsibility | Import |
|---------------------------------|---------------------------------------------------|--------|
| `openapi_first.loader` | Load/validate the spec, resolve `{ENV_VAR}` | `load_openapi` |
| `openapi_first.app` | OpenAPI-first FastAPI application bootstrap | `OpenAPIFirstApp` |
| `openapi_first.binder` | Spec → route binding via `operationId` | `bind_routes` |
| `openapi_first.client` | `operationId`-driven HTTP client | `OpenAPIClient` |
| `openapi_first.errors` | Explicit error hierarchy | `OpenAPIFirstError`, `OpenAPIClientError`, `MissingOperationHandler` |
| `openapi_first.security` | Security-dependency construction from the spec | `parse_security_schemes`, `make_security_dependencies` |
| `openapi_first.codegen` | Pydantic model generation (build-time) | `generate_models` |
| `openapi_first.codegen_routes` | Route-handler stub generation (build-time) | `generate_routes` |
| `openapi_first.cli` | `scaffold` / `models` / `routes` command surface | CLI entry point |
| `openapi_first.templates` | Copyable application templates (NOT library API) | `openapi-first scaffold` |
---
## ⚙️ 2. `loader` — Load & Validate
`load_openapi(path: str | Path) -> dict[str, Any]` (in `openapi_first/loader.py`).
`loader.py` ensures a spec is real, readable, parseable, and schema-valid *before* anything else runs — a golden rule of fail-fast.
```python
from openapi_first.loader import load_openapi
spec = load_openapi("openapi.yaml")
```
Behavior:
- Accepts `.json`, `.yaml`, `.yml` (parsed by extension).
- Runs strict OpenAPI 3.x validation (`openapi-spec-validator`) at load time.
- Raises `OpenAPISpecLoadError` on: missing file, unparseable content, or spec-validation failure.
- Does **not** modify, coerce, or "fix" the spec — it's a read-only gate.
**Env-var resolution** lives at the *security* layer rather than the loader (see [Security](05_security.md)).
---
## 🧬 3. `app` — The Application Bootstrap
`OpenAPIFirstApp` (in `openapi_first/app.py`) is a FastAPI subclass that replaces manual route registration with OpenAPI-driven binding.
```python
from openapi_first.app import OpenAPIFirstApp
import routes
app = OpenAPIFirstApp(
openapi_path="openapi.yaml",
routes_module=routes,
title="My Service",
)
```
**Startup pipeline (fail-fast, in this order):**
1. **Load** the spec (`.yaml`/`.json`).
2. **Validate** it against OpenAPI 3.x schema.
3. **Parse** `securitySchemes` and per-operation `security`.
4. **Build** per-route security dependencies.
5. **Bind** every path/method → handler by `operationId`; a missing handler, a missing `operationId` on a declared operation, or an unbound operation raises at startup.
**Guarantees:**
- Every route has a spec declaration (no undocumented routes).
- Every spec operation has a handler (no unhandled operations).
- Auth enforcement is spec-driven, not hand-wired.
- `/openapi.json` + Swagger UI always reflect the provided spec.
Keyword arguments beyond `openapi_path` / `routes_module` pass straight through to `fastapi.FastAPI` (it's a subclass — `title`, `version`, middleware, lifespan, … all work).
---
## 🔗 4. `binder` — Spec → Route Binding
`bind_routes(app, spec, routes_module, security_deps=None) -> None` (in `openapi_first/binder.py`).
This is the heart of the OpenAPI-first guarantee. For each `path` + HTTP method in the spec:
1. Reads the operation's `operationId`.
2. Looks up `routes_module.<operationId>` — a **plain callable**.
3. Registers a FastAPI `APIRoute` bound to that handler, injecting `Depends(...)` for any matching security requirements.
Failures are explicit and early:
| Condition | Raised |
|---------------------------------------|---------------------------------------|
| No `operationId` on an operation | `MissingOperationHandler` |
| `operationId` has no handler function | `MissingOperationHandler` |
| A path/method declared but unbound | `MissingOperationHandler` |
Handlers stay framework-agnostic: they're plain functions `(payload, id, response)` named after `operationId`s — no decorators, no routing metadata.
---
## 📡 5. `client` — The Other Side of the Contract
`OpenAPIClient(spec, base_url=None, client=None)` (in `openapi_first/client.py`).
The same spec that builds the server builds its client — one callable per `operationId`, keyed by name:
```python
from openapi_first.loader import load_openapi
from openapi_first.client import OpenAPIClient
spec = load_openapi("openapi.yaml")
client = OpenAPIClient(spec)
response = client.get_health()
response = client.get_user(path_params={"user_id": 1})
response = client.create_user(body={"name": "Ada"})
```
How operations become methods:
- Each `operationId` → a dynamically-built callable on the client.
- **Path parameters** → `path_params={"user_id": 1}`.
- **Request body** → `body={...}` (JSON) or raw content for non-JSON media types.
- **Query/headers** → `query=` / `headers=`.
- Returns the raw `httpx.Response` (no implicit deserialization, no hidden schema inference).
Client-construction guarantees (fail-fast, same philosophy as the app):
- Spec must declare at least one `servers` entry (`base_url` falls back to it).
- Spec must have `paths`.
- Every operation must have a **unique** `operationId`.
- Required params must match the spec at call time.
Construction errors raise `OpenAPIClientError`; request-time errors surface as `httpx` exceptions with `operationId` context.
---
## 💥 6. `errors` — Explicit Error Hierarchy
Everything raised by `openapi-first` derives from `OpenAPIFirstError` (in `openapi_first/errors.py`), so callers can handle first-party failures with a single `except`:
- `OpenAPIFirstError` — base (comparable / aims to be picklable share).
- `OpenAPIClientError` — client-side contract violations.
- `MissingOperationHandler` — spec declares an operation whose handler is missing or unresolvable; carries `path`, `method`, and optional `operationId`.
See [Error Handling](06_error_handling.md) for the full table.
---
## 🛡️ 7. `security` — Auth from the Spec
`security.py` turns an OpenAPI `securitySchemes` section into FastAPI `Depends(...)` objects — **no manual middleware**.
- `parse_security_schemes(spec) -> dict[str, dict]` — collects schemes, resolving `{ENV_VAR}` placeholders.
- `make_security_dependencies(spec, security_schemes) -> dict[str, list[Depends]]` — builds `METHOD:/path` → dependency list from per-operation `security`, falling back to top-level `security`.
Supported scheme types (extensible):
- `type: http, scheme: bearer``OpenAPIFirstSecurityDependency(HTTPBearer)`. With `x-introspect-path`/`x-server-url`, it introspects the JWT against an auth service; optionally sets `request.state.user`. Without introspection, it validates token presence and stores it on `request.state.token`.
- API-key style schemes via the same resolution path.
Key properties: resolution is **per-operation**, env placeholders resolve once at startup, and dependencies are injected by `binder` — your handlers never mention security.
---
## 🏭 8. `codegen` & `cli` — Build-Time Tooling
Generation is strictly **build-time** — spec → code, run once by a developer, committed:
### `openapi-first models <spec> -o <file>`
Generates Pydantic models from spec schemas (wraps `datamodel_code_generator`):
```python
# cli path
openapi-first models openapi.yaml -o app/models.py
```
### `openapi-first routes <spec> -o <dir> [--use-models] [--models-module models]`
Generates one `routes_<resource>.py` stub file per resource, with `NotImplementedError` handlers bound to `operationId`s (optionally importing your generated models):
```python
openapi-first routes openapi.yaml -o app/routes --use-models
```
### `openapi-first scaffold <template> [path]`
Copies a bundled application template verbatim (see [Templates](03_use_cases/02_templates.md)).
---
## 🧱 9. `templates` — Copyable Applications
Four bundled, runnable applications live under `openapi_first/templates/`: `health_app`, `crud_app`, `model_app`, and `vet_app`. They are **not** part of the library API — no lint/format/type gates, never imported at runtime. They exist to be copied via:
```bash
openapi-first scaffold <template> [target-dir]
openapi-first scaffold --list
```
---
## 🔗 Related
- [01 Overview](01_overview.md) · [02 Templates](03_use_cases/02_templates.md) · [03 Security](05_security.md) · [04 Design](04_design.md)