docs: add OpenAPI-first wiki (overview, components, use cases for templates/client/codegen, design, security, error handling, testing) and refresh lib docs index
This commit is contained in:
122
docs/wiki/03_use_cases/01_quickstart.md
Normal file
122
docs/wiki/03_use_cases/01_quickstart.md
Normal file
@@ -0,0 +1,122 @@
|
||||
# Use Case 1: Quickstart — Build Your First OpenAPI-First Service
|
||||
|
||||
This guide walks you from an empty directory to a running, contract-driven service in a few minutes, then talks to it with the generated client.
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ 1. Prerequisites
|
||||
|
||||
- Python 3.10+
|
||||
- `openapi-first` installed (see [Overview](../01_overview.md#installation))
|
||||
- `pip install "fastapi[standard]"` (or `uvicorn`) to run the app
|
||||
|
||||
---
|
||||
|
||||
## 📄 2. Write the OpenAPI document
|
||||
|
||||
OpenAPI comes first. Create `openapi.yaml`:
|
||||
|
||||
```yaml
|
||||
openapi: 3.0.3
|
||||
info:
|
||||
title: Greeting Service
|
||||
version: 1.0.0
|
||||
servers:
|
||||
- url: http://localhost:8000
|
||||
paths:
|
||||
/greet/{name}:
|
||||
get:
|
||||
operationId: get_greeting
|
||||
parameters:
|
||||
- name: name
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
responses:
|
||||
"200":
|
||||
description: A greeting
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
greeting:
|
||||
type: string
|
||||
```
|
||||
|
||||
Key points: every operation needs `operationId`, and every route must exist *only* here.
|
||||
|
||||
---
|
||||
|
||||
## 🧑💻 3. Write the handlers
|
||||
|
||||
Create `routes.py` — plain functions, no decorators, named exactly like the `operationId`s:
|
||||
|
||||
```python
|
||||
# routes.py
|
||||
def get_greeting(name: str) -> dict:
|
||||
"""Return a greeting for the given name."""
|
||||
return {"greeting": f"Hello, {name}!"}
|
||||
```
|
||||
|
||||
If a handler is missing at startup, the app **refuses to boot** (`MissingOperationHandler`) — the fail-fast guarantee catches contract drift immediately.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 4. Bootstrap the app
|
||||
|
||||
Create `main.py`:
|
||||
|
||||
```python
|
||||
# main.py
|
||||
from openapi_first.app import OpenAPIFirstApp
|
||||
import routes
|
||||
|
||||
app = OpenAPIFirstApp(
|
||||
openapi_path="openapi.yaml",
|
||||
routes_module=routes,
|
||||
title="Greeting Service",
|
||||
)
|
||||
```
|
||||
|
||||
Run it:
|
||||
|
||||
```bash
|
||||
uvicorn main:app --reload
|
||||
```
|
||||
|
||||
Visit `http://localhost:8000/docs` (Swagger UI) and `http://localhost:8000/openapi.json` — both are generated from *your* spec.
|
||||
|
||||
---
|
||||
|
||||
## 📡 5. Call it with the client
|
||||
|
||||
The same spec builds a strict client:
|
||||
|
||||
```python
|
||||
# client.py
|
||||
from openapi_first.loader import load_openapi
|
||||
from openapi_first.client import OpenAPIClient
|
||||
|
||||
spec = load_openapi("openapi.yaml")
|
||||
client = OpenAPIClient(spec)
|
||||
|
||||
response = client.get_greeting(path_params={"name": "Ada"})
|
||||
print(response.status_code) # 200
|
||||
print(response.json()) # {"greeting": "Hello, Ada!"}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💡 6. Next Steps
|
||||
|
||||
- Copy a fuller example: [02 – Templates](02_templates.md)
|
||||
- Generate models/routes from a bigger spec: [04 – Codegen](04_codegen.md)
|
||||
- Drive everything from a client: [03 – Client](03_client.md)
|
||||
|
||||
---
|
||||
|
||||
## Related
|
||||
|
||||
- [01 – Overview](../01_overview.md) · [02 – Components](../02_components.md) · [01 – Quickstart](01_quickstart.md)
|
||||
167
docs/wiki/03_use_cases/02_templates.md
Normal file
167
docs/wiki/03_use_cases/02_templates.md
Normal file
@@ -0,0 +1,167 @@
|
||||
# Use Case 2: Templates — Copyable Reference Applications
|
||||
|
||||
`openapi-first` ships four runnable, copyable applications under `openapi_first/templates/`. They are **not part of the library API** — they are bundled scaffold examples you copy into your own project and build on.
|
||||
|
||||
---
|
||||
|
||||
## 🎬 1. What Templates Are
|
||||
|
||||
A template is a complete, self-contained OpenAPI-first service:
|
||||
|
||||
- A bundled directory inside `openapi_first/templates/<name>/`
|
||||
- Copyable **verbatim** — no code generation, no mutation — via the CLI
|
||||
- Each one demonstrates a specific set of OpenAPI-first behaviors and FastAPI features
|
||||
|
||||
All templates share the same skeleton:
|
||||
|
||||
```
|
||||
<name>_app/
|
||||
├── __init__.py # explains the template + how to scaffold it
|
||||
├── openapi.yaml # the contract (source of truth)
|
||||
├── main.py # assembles OpenAPIFirstApp from the spec
|
||||
├── routes.py # operationId-bound handler functions
|
||||
└── data.py # in-memory data store (demo only)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📋 2. The Four Templates
|
||||
|
||||
### 2.1 `health_app` — minimal liveness probe
|
||||
|
||||
```bash
|
||||
openapi-first scaffold health_app
|
||||
# or into a custom directory:
|
||||
openapi-first scaffold health_app my-health-service
|
||||
```
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `openapi.yaml` | `GET /health` → `operationId: get_health` |
|
||||
| `routes.py` | `get_health()` returns `{"status": "ok"}` |
|
||||
| `main.py` | `OpenAPIFirstApp(openapi_path="openapi.yaml", routes_module=routes)` |
|
||||
|
||||
**Why it exists:** the absolute minimal OpenAPI-first round trip — one operation, one handler, zero moving parts. The best starting point to internalize the mental model.
|
||||
|
||||
**Smoke test:**
|
||||
|
||||
```bash
|
||||
pip install -e .
|
||||
uvicorn main:app
|
||||
curl http://localhost:8000/health
|
||||
# → {"status": "ok"}
|
||||
```
|
||||
|
||||
### 2.2 `crud_app` — dict-based CRUD
|
||||
|
||||
```bash
|
||||
openapi-first scaffold crud_app my-crud-service
|
||||
```
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `openapi.yaml` | Full CRUD over `/items` (list/get/create/update/delete) |
|
||||
| `routes.py` | Handlers bound via `operationId`s `list_items`, `get_item`, `create_item`, `update_item`, `delete_item` |
|
||||
| `data.py` | In-memory dict store with auto-incrementing `id` |
|
||||
|
||||
Behaviors you learn:
|
||||
|
||||
- **Explicit status codes** — `create_item`/`delete_item` take `response: Response` and set `201`/`204`; `get_item`/`update_item` raise `HTTPException(404)` on `KeyError`
|
||||
- **Handlers as plain callables** — no FastAPI decorators, routing comes solely from the spec
|
||||
- **Mock data store** — `data.py` is a copyable in-memory store, explicitly **not** production-ready
|
||||
|
||||
### 2.3 `model_app` — Pydantic model CRUD
|
||||
|
||||
```bash
|
||||
openapi-first scaffold model_app my-model-service
|
||||
```
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `openapi.yaml` | Same CRUD surface, schemas reference models |
|
||||
| `models.py` | Pydantic `Item`, `ItemCreate`, `ItemBase` (request/response models) |
|
||||
| `routes.py` | Handlers type-annotated with the models; `create_item` sets `201` |
|
||||
| `data.py` | In-memory store returning real model instances |
|
||||
|
||||
Behaviors you learn:
|
||||
|
||||
- **Pydantic request/response models** — payloads validated and serialized via FastAPI
|
||||
- **Same handler contracts** — identical `operationId` set as `crud_app`, so the two are interchangeable
|
||||
- **Models in the client too** — the same spec drives `OpenAPIClient` body handling
|
||||
|
||||
### 2.4 `vet_app` — the full-featured demo
|
||||
|
||||
```bash
|
||||
openapi-first scaffold vet_app my-vet-clinic
|
||||
```
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `openapi.yaml` | Five resources (parents, vets, treatments, pets, appointments) + SSE + upload + discriminated unions |
|
||||
| `models.py` | Pydantic models incl. **discriminated unions** (`noteType` literal fields) |
|
||||
| `routes.py` | ~20 handlers across all resources, incl. pagination, filtering, photo upload, SSE streaming |
|
||||
| `sse.py` | Server-Sent Events helper (`StreamingResponse`, per-pet subscriber queues, background `asyncio` workers) |
|
||||
| `data.py` | Larger in-memory store (parents → vets → treatments → pets → appointments) |
|
||||
| `main.py` | App + CORS + lifespan example |
|
||||
|
||||
Behaviors you learn — the *advanced* tier:
|
||||
|
||||
- **Discriminated unions** — `ProcedureNotes` uses `oneOf` + `discriminator.noteType` mapping; Pydantic models use `Literal[...]` discriminator fields
|
||||
- **SSE streaming** — a `GET /pets/{id}/actions` operation streaming `text/event-stream` via `StreamingResponse` with background task workers
|
||||
- **File upload** — `UploadFile` handler setting a multi-part body
|
||||
- **CORS + middleware** — `add_middleware(CORSMiddleware, ...)` alongside the spec-driven setup
|
||||
- **Response injection** — handlers set `201`/`204` explicitly via injected `Response`
|
||||
|
||||
---
|
||||
|
||||
## 🚀 3. CLI Reference
|
||||
|
||||
```bash
|
||||
# List available templates
|
||||
openapi-first scaffold --list
|
||||
|
||||
# Copy a template into its default directory (template name, dashes)
|
||||
openapi-first scaffold health_app
|
||||
|
||||
# Copy into a custom target directory
|
||||
openapi-first scaffold crud_app my-project/crud
|
||||
```
|
||||
|
||||
> **Protip:** `DEFAULT_TEMPLATE` is `health_app`, so `openapi-first scaffold` with no template name scaffolds the health app.
|
||||
|
||||
---
|
||||
|
||||
## 🧩 4. Anatomy of a Scaffolded Service
|
||||
|
||||
After `openapi-first scaffold health_app my-health-service`, your directory contains a drop-in FastAPI service:
|
||||
|
||||
```
|
||||
my-health-service/
|
||||
├── openapi.yaml # THE contract
|
||||
├── main.py # `app = OpenAPIFirstApp(openapi_path=..., routes_module=routes)`
|
||||
└── routes.py # `def get_health(): ...`
|
||||
```
|
||||
|
||||
Run it:
|
||||
|
||||
```bash
|
||||
cd my-health-service
|
||||
pip install -e .
|
||||
uvicorn main:app --reload
|
||||
```
|
||||
|
||||
`/docs`, `/openapi.json`, and every declared route now exist — all derived from `openapi.yaml`.
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 5. Production Disclaimer
|
||||
|
||||
Templates use **in-memory, non-persistent, non-concurrency-safe** data stores. They are learning scaffolds — **not** production references. Swap in a real data layer (SQL/REDIS/object store) the moment you go beyond a demo.
|
||||
|
||||
See the `__init__.py` of each template for detailed client examples, CLI examples, and design notes.
|
||||
|
||||
---
|
||||
|
||||
## Related
|
||||
|
||||
- [01 – Quickstart](01_quickstart.md) · [04 – Codegen](04_codegen.md) · [02 – Components](../02_components.md)
|
||||
156
docs/wiki/03_use_cases/03_client.md
Normal file
156
docs/wiki/03_use_cases/03_client.md
Normal file
@@ -0,0 +1,156 @@
|
||||
# Use Case 3: The OperationId-Driven Client
|
||||
|
||||
`OpenAPIClient` is the other side of the contract. It reads the *same* OpenAPI document the server runs on and exposes one callable per `operationId` — so "client" and "server" are two views of one truth.
|
||||
|
||||
---
|
||||
|
||||
## 🔍 1. Before You Start
|
||||
|
||||
The client is `httpx`-based and returns raw `httpx.Response` objects — no magic deserialization, no hidden schema inference:
|
||||
|
||||
- **No** response Pydantic models
|
||||
- **No** implicit URL construction (path params are explicit)
|
||||
- **No** hand-written `requests.get(...)` scattered through your code
|
||||
|
||||
---
|
||||
|
||||
## 🧬 2. Constructing the Client
|
||||
|
||||
```python
|
||||
from openapi_first.loader import load_openapi
|
||||
from openapi_first.client import OpenAPIClient
|
||||
|
||||
spec = load_openapi("openapi.yaml")
|
||||
client = OpenAPIClient(spec)
|
||||
```
|
||||
|
||||
The base URL comes from the spec's `servers` list (first entry) unless you pass `base_url` explicitly:
|
||||
|
||||
```python
|
||||
client = OpenAPIClient(spec, base_url="https://api.internal.myco/v1")
|
||||
```
|
||||
|
||||
You can also hand over a preconfigured `httpx.Client` (custom transport, TLS, retries):
|
||||
|
||||
```python
|
||||
import httpx
|
||||
|
||||
transport = httpx.HTTPTransport(retries=3)
|
||||
client = OpenAPIClient(
|
||||
spec,
|
||||
client=httpx.Client(transport=transport),
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 💥 3. Fail-Fast at Construction
|
||||
|
||||
`OpenAPIClient(...)` **raises immediately** if the contract is broken — you find out at startup, not on the first request:
|
||||
|
||||
| Violation | Error |
|
||||
|----------------------------------------|-----------------------------------|
|
||||
| Spec has no `servers` entry | `OpenAPIClientError` |
|
||||
| Spec has no `paths` | `OpenAPIClientError` |
|
||||
| Operation missing `operationId` | `OpenAPIClientError` |
|
||||
| Duplicate `operationId` | `OpenAPIClientError` |
|
||||
| Operation references unknown parameters| `OpenAPIClientError` |
|
||||
|
||||
Missing *required parameters* fail at **call time** — pydoclint-grade strictness on the wire.
|
||||
|
||||
---
|
||||
|
||||
## 📞 4. Calling Operations
|
||||
|
||||
Every `operationId` becomes a method. The call signature is uniform across the whole client:
|
||||
|
||||
```python
|
||||
response = client.<operationId>(
|
||||
*,
|
||||
path_params: dict | None = None,
|
||||
query: dict | None = None,
|
||||
headers: dict | None = None,
|
||||
body: Any | None = None,
|
||||
timeout: float | None = None,
|
||||
) -> httpx.Response
|
||||
```
|
||||
|
||||
Concrete examples (from the `crud_app` / `health_app` templates):
|
||||
|
||||
```python
|
||||
# No parameters — simplest
|
||||
response = client.get_health()
|
||||
assert response.status_code == 200
|
||||
|
||||
# Path parameter
|
||||
response = client.get_item(path_params={"item_id": 3})
|
||||
|
||||
# Query parameters
|
||||
response = client.list_items(query={"limit": 10, "offset": 20})
|
||||
|
||||
# JSON request body
|
||||
response = client.create_item(body={"name": "Orange", "price": 0.8})
|
||||
|
||||
# Custom headers / timeout
|
||||
response = client.get_user(
|
||||
path_params={"user_id": 1},
|
||||
headers={"X-Internal-Key": "..."},
|
||||
timeout=30,
|
||||
)
|
||||
```
|
||||
|
||||
**Returns:** the raw `httpx.Response`, so `status_code`, `.json()`, `.headers` are all yours to inspect.
|
||||
|
||||
---
|
||||
|
||||
## 🧠 5. How Parameters Are Bound
|
||||
|
||||
For each operation the client knows exactly *where* each parameter belongs:
|
||||
|
||||
| OpenAPI location | Client kwarg |
|
||||
|------------------|-------------------|
|
||||
| `in: path` | `path_params[name]` |
|
||||
| `in: query` | `query[name]` |
|
||||
| `in: header` | `headers[name]` |
|
||||
| `requestBody` | `body` |
|
||||
|
||||
JSON media types are sent as `json=`; other media types as raw `content=`.
|
||||
|
||||
---
|
||||
|
||||
## 🔄 6. Server ↔ Client, One Spec
|
||||
|
||||
```bash
|
||||
# One directory, two processes
|
||||
uvicorn main:app --port 8000 # server
|
||||
python -c "import asyncio; from client_script import run; asyncio.run(run())" # client
|
||||
```
|
||||
|
||||
Or the same client against a remote environment:
|
||||
|
||||
```python
|
||||
client = OpenAPIClient(
|
||||
spec,
|
||||
base_url="https://staging.internal.myco",
|
||||
)
|
||||
```
|
||||
|
||||
The URL is the *only* thing that changes between environments — the contract never does.
|
||||
|
||||
---
|
||||
|
||||
## ✍️ 7. OperationId as the API
|
||||
|
||||
Because the client is operationId-driven:
|
||||
|
||||
- Adding an operation = adding a method (and vice versa)
|
||||
- No operation can be "forgotten" by the client — it's constructed from the spec
|
||||
- `operationId` is the one name you remember; the HTTP verb/path is an implementation detail
|
||||
|
||||
If an `operationId` you call doesn't exist, you get an `AttributeError` at construction-scan time — never a silent 404.
|
||||
|
||||
---
|
||||
|
||||
## Related
|
||||
|
||||
- [01 – Overview](../01_overview.md) · [01 – Quickstart](01_quickstart.md) · [02 – Templates](02_templates.md)
|
||||
70
docs/wiki/03_use_cases/04_codegen.md
Normal file
70
docs/wiki/03_use_cases/04_codegen.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# Codegen — Generate Models & Routes From Your Spec
|
||||
|
||||
> **TL;DR** — point `openapi-first codegen` at your spec and get `models` + `routes` scaffolds you can bind with one line. The codegen output is deterministic, and it's a *starting point* — not a maintained artifact.
|
||||
|
||||
---
|
||||
|
||||
## 🧭 1. Why Celebrate Codegen?
|
||||
|
||||
Two pain points kill OpenAPI projects:
|
||||
|
||||
1. **The "write the spec, then write the same thing as Pydantic models" step** — exactly where server/client param types drift (your route says `item_id: int`, your client sends a string…).
|
||||
2. **Writing the docs/spec catalog by hand**, once you have more than a handful of operations.
|
||||
|
||||
Codegen collapses both. One command, one source file, same generated shapes everywhere.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 2. Model Generation
|
||||
|
||||
```bash
|
||||
openapi-first codegen models --module my_project.models --input openapi.yaml
|
||||
```
|
||||
|
||||
The generated model module mirrors the spec's `components.schemas` **by name**:
|
||||
|
||||
| Spec | Generated |
|
||||
|------|-----------|
|
||||
| `components.schemas.User` | `class User(BaseModel)` |
|
||||
| `components.schemas.Item` | `class Item(BaseModel)` |
|
||||
| every `$ref` (schema) | a `type: ClassVar` alias ✓ |
|
||||
| `required + type` from schema | Pydantic `Field(...)` / type hints ✓ |
|
||||
|
||||
**Never hand-edit those files.** If the spec changes, regenerate — just like you'd re-run `cargo build` after editing `Cargo.toml`.
|
||||
|
||||
### Why constructor-time validation still applies
|
||||
|
||||
Codegen doesn't change the design: the generated models are plain Pydantic, and the **client/server still validate against the spec** at startup. Codegen is a *convenience accelerator* on top of the fail-fast guarantees in [04 – Design](../04_design.md) and [06 – Error Handling](../06_error_handling.md).
|
||||
|
||||
---
|
||||
|
||||
## 🔁 3. Route Generation / Verification
|
||||
|
||||
```bash
|
||||
# dry-run verification against your routes module
|
||||
openapi-first codegen routes --module my_routes --input openapi.yaml --check
|
||||
|
||||
# scaffold an operation skeleton (generates the handler with a TODO)
|
||||
openapi-first codegen routes --module my_routes --input openapi.yaml
|
||||
```
|
||||
|
||||
Why bother? Because `bind_routes` (in [02 – Components](../02_components.md)) needs an `operationId` → handler **exactly** matching the spec. Codegen guarantees you never type a handler name wrong — it wears the same `operationId` as the spec says.
|
||||
|
||||
> **Note:** `codegen` is build-time tooling. It does **not** run at runtime, and templates (the Bake-your-stuff section of [02 – Templates](02_templates.md)) make scaffolding a server out-of-the-box even simpler for greenfield projects.
|
||||
|
||||
---
|
||||
|
||||
## 🧪 4. Idempotent Output
|
||||
|
||||
Generated output is deterministic w.r.t. the spec:
|
||||
- Same spec → byte-identical files (unless you hand-edit — you won't)
|
||||
- Ordering follows spec declaration order
|
||||
- No timestamps, no machine names, no hidden randomness
|
||||
|
||||
That determinism is what lets you `git diff` after a spec change and see **exactly** what moved.
|
||||
|
||||
---
|
||||
|
||||
## 🧩 Related
|
||||
|
||||
- [01 – Overview](../01_overview.md) · [02 – Templates](02_templates.md) · [03 – Client](03_client.md) · [04 – Design](../04_design.md) · [07 – Testing](../07_testing.md)
|
||||
Reference in New Issue
Block a user