# 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.` โ€” 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 -o ` 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 -o [--use-models] [--models-module models]` Generates one `routes_.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