130 lines
4.9 KiB
Markdown
130 lines
4.9 KiB
Markdown
# Shuntgen
|
|
|
|
Generates runnable [shunts](#artery-shunts) from the two things people actually
|
|
have: a service contract, or a folder of spreadsheets.
|
|
|
|
Writing a shunt by hand means copying `artery/shunts/example/` and filling in
|
|
`responses.json` entry by entry. That is fine for three endpoints and untenable
|
|
for eighty — and it is the wrong work anyway, because the endpoints are already
|
|
described in the spec somebody handed you.
|
|
|
|
```bash
|
|
# a spec you were handed
|
|
python -m station.tools.shuntgen from-openapi -s api.yaml -o artery/shunts/petstore
|
|
|
|
# sheets a client sent
|
|
python -m station.tools.shuntgen from-tabular -s ./sheets -o artery/shunts/books
|
|
|
|
python -m station.tools.shuntgen list
|
|
```
|
|
|
|
Run from `soleprint/`. Also in the browser at `/station/tools/shuntgen/`, where
|
|
you can upload a spec, preview the routes it would serve, and generate.
|
|
|
|
## What comes out
|
|
|
|
```
|
|
artery/shunts/<name>/
|
|
main.py builds the app from the spec
|
|
run.py uvicorn entry point (PORT, or depot/config.json)
|
|
shunt_runtime.py vendored runtime — no soleprint import
|
|
models.py pydantic, via modelgen
|
|
datagen_<name>.py BaseDataGenerator subclass, via modelgen
|
|
depot/spec.json routes, collections and schema
|
|
depot/responses.json pinned overrides — yours, never overwritten
|
|
depot/config.json delays, error rate, prefill — yours, never overwritten
|
|
depot/data.json imported rows
|
|
templates/index.html config UI
|
|
README.md
|
|
```
|
|
|
|
```bash
|
|
cd artery/shunts/books && python run.py
|
|
curl localhost:8098/customers
|
|
```
|
|
|
|
The routes are built at startup from `spec.json` rather than written out as
|
|
source. That keeps the generated code short enough to read, and puts the
|
|
behaviour in one reviewable place: fixing `runtime.py` fixes every shunt, and
|
|
regenerating is a copy.
|
|
|
|
## Where a response comes from
|
|
|
|
First hit wins:
|
|
|
|
1. `depot/responses.json` — a pinned override, keyed `"METHOD /path"`
|
|
2. the store — rows imported from sheets, plus anything POSTed since
|
|
3. the spec's `example`, if the source document carried one
|
|
4. `datagen_<name>.py`, synthesising from the schema
|
|
5. `{}`
|
|
|
|
The store is what makes it behave like a service rather than a random-value
|
|
faucet: POST something and GET it back, ask for `/pets/7` and get the pet whose
|
|
id is 7. Collections that arrived with no rows are prefilled with generated
|
|
ones, so the first call answers with something.
|
|
|
|
## Two sources, one pipeline
|
|
|
|
Both inputs are [modelgen](#station-modelgen) extractors, so the same shapes
|
|
also generate pydantic, TypeScript, prisma and a
|
|
[graphgen](#station-graphgen) schema:
|
|
|
|
```bash
|
|
python -m station.tools.modelgen from-openapi -s api.yaml -o out/ -t pydantic,typescript
|
|
python -m station.tools.modelgen from-tabular -s ./sheets -o out/ -t schema,datagen
|
|
```
|
|
|
|
| Source | Becomes | Routes |
|
|
| --- | --- | --- |
|
|
| OpenAPI 3.x / Swagger 2.0 | one model per schema; enums become real Enums, `$ref` becomes a relation | the operations the document declares |
|
|
| `.csv` / `.tsv` / `.ods` | one model per file or sheet, types inferred per column | five CRUD routes per table |
|
|
|
|
Keys and relations are inferred by name and then **checked against the data**:
|
|
an `id` column that is not unique is not treated as a key, and `customer_id` is
|
|
only a foreign key if a `customers` sheet came with it.
|
|
|
|
ODS is read with `zipfile` and `ElementTree` — no odfpy, no pandas — which is
|
|
what lets modelgen stay dependency-free and publishable on its own.
|
|
|
|
## Control endpoints
|
|
|
|
Every generated shunt serves these:
|
|
|
|
| Endpoint | Purpose |
|
|
| --- | --- |
|
|
| `GET /health` | liveness |
|
|
| `GET /mock/spec` | the routes it was built from |
|
|
| `GET /mock/stats` | call counts and row counts |
|
|
| `POST /mock/reset` | restore the imported rows, clear counters |
|
|
| `GET,POST /mock/config` | delays, error rate, `unknown_id`, page size |
|
|
| `GET,POST /mock/responses` | pin an override; set a key to `null` to drop it |
|
|
|
|
```bash
|
|
# make it slow and flaky, the way the real thing is
|
|
curl -X POST localhost:8098/mock/config \
|
|
-H 'content-type: application/json' \
|
|
-d '{"enable_random_delays": true, "error_rate": 0.2}'
|
|
|
|
# make one call answer something specific
|
|
curl -X POST localhost:8098/mock/responses \
|
|
-H 'content-type: application/json' \
|
|
-d '{"GET /customers/1": {"id": 1, "name": "PINNED"}}'
|
|
```
|
|
|
|
`unknown_id` decides what an unknown key does: `generate` (the default) invents
|
|
a record wearing the id that was asked for; `404` refuses it. Generate by
|
|
default, because a client pointed at a fresh shunt should just work — flip it
|
|
when the error path is what you are testing.
|
|
|
|
## Dependency containers
|
|
|
|
`--cabinet postgres,redis` writes a `cabinet.json` declaring what the shunt
|
|
expects. `build.py` composes those services into the room's compose file, and on
|
|
a cluster they install as rig addons of the same name. See
|
|
[Cabinets](#station-cabinets).
|
|
|
|
## Regenerating
|
|
|
|
Everything is overwritten except `depot/responses.json` and `depot/config.json`.
|
|
Those two are yours.
|