209 lines
8.9 KiB
Markdown
209 lines
8.9 KiB
Markdown
# 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)
|