Both published APIs serve a complete OpenAPI 3.1 description of themselves.
| Service | Document |
|---|---|
| Roost | https://roost.ceagle.cc/api/openapi.json |
| Accounts | https://account.ceagle.cc/api/openapi.json |
These docs render them at Roost and Accounts.
The developer console is not among them. Its API is not published, so there is no document to fetch.
curl -s https://roost.ceagle.cc/api/openapi.json > roost.jsonEach is served with a permissive cross-origin header and a short cache, so a browser-based tool can fetch one directly.
They are generated#
The descriptions are not written by hand and kept in step. Each service builds its document at startup by reflecting over the same Go types its handlers bind and return.
That has a few consequences worth relying on:
A field added to a response type appears in the document without anyone remembering to add it.
The validation tags on request types become required, maxLength, minimum, enum and
format in the schema.
Fields appear in struct order rather than alphabetically, so a generated client reads like the Go type.
Paths appear only when their routes are registered, so the document never advertises something that is not there.
What the reflection produces#
A request type like this:
type listingRequest struct {
Title string `json:"title" validate:"required,min=1,max=64"`
Summary string `json:"summary" validate:"omitempty,max=140"`
Categories []string `json:"categories" validate:"required,min=1,max=4,dive,required,max=32"`
Hidden bool `json:"hidden"`
}becomes:
{
"type": "object",
"properties": {
"title": { "type": "string", "minLength": 1, "maxLength": 64 },
"summary": { "type": "string", "maxLength": 140 },
"categories": {
"type": "array",
"minItems": 1,
"maxItems": 4,
"items": { "type": "string", "maxLength": 32 }
},
"hidden": { "type": "boolean" }
},
"required": ["title", "categories", "hidden"]
}required- Adds the field to
required. omitempty- Leaves it optional, as does a pointer field or a JSON
omitempty. min, maxminLength/maxLengthfor strings,minimum/maximumfor numbers,minItems/maxItemsfor arrays.oneof- Becomes an
enum. email, url, uuid, hostname- Become the matching
format. dive- Applies the rules after it to the array's items rather than the array.
Embedded structs are flattened exactly the way encoding/json flattens them, so a response type that
embeds another produces one object rather than a nested one.
Generating a client#
The documents are ordinary OpenAPI 3.1, so any generator works.
bunx openapi-typescript https://roost.ceagle.cc/api/openapi.json -o roost.d.tsgo tool oapi-codegen -package roost \
-generate types,client \
https://roost.ceagle.cc/api/openapi.json > roost.gen.goopenapi-python-client generate --url https://roost.ceagle.cc/api/openapi.jsonKeeping a generated client honest#
Commit the document you generated from
Alongside the generated code, so both move in one commit.
Re-fetch it on a schedule
A daily job that downloads the document and diffs it against the committed copy turns a silent schema change into a pull request.
Ignore unknown fields
Fields are added within
v1and never removed or repurposed, so a client that tolerates unknown fields keeps working across additions.
Extensions used#
The documents carry a few vendor extensions that renderers understand and generators ignore:
x-tagGroups- Groups tags into sections in the sidebar.
x-codeSamples- Hand-written examples on operations where the generated one is not the useful one.
x-badges- Small labels shown next to an operation name.