Documentation
¶
Overview ¶
Package openapi post-processes the raw Swagger 2.0 specs generated by protoc-gen-openapiv2 into OpenAPI 3 specs with JSON:API envelope schemas. The Swagger 2.0 → OpenAPI 3 conversion is kin-openapi's openapi2conv (convert.go adds the API-key header schemes it is given and keeps each $ref's description and readOnly); the JSON:API envelope, the descriptor reading and the route table are this package's.
The JSON:API envelope semantics (service name, base path, and per-resource single/collection keys) are declared in the proto via the jsonapi.v1 annotations and read from a buf descriptor set — there is no hand-maintained service map. The package is generic: it carries zero per-service API data and reads whatever descriptor set it is given (see descriptor.go).
Index ¶
- func BuildDescriptorSet(ctx context.Context, runner interfaces.CommandRunner, protoDir string) ([]byte, error)
- func BuildRouteTable(fdsBytes []byte, exempt map[string]bool) (map[string][]RouteEntry, error)
- func MergeSwaggerFiles(fsys interfaces.FS, files []string) (map[string]any, error)
- func WriteRuntimeRouteTable(fsys interfaces.FS, routes map[string][]RouteEntry, ...) error
- type Binding
- type ResourceKeys
- type Result
- type Role
- type RouteEntry
- type ServiceView
- type Violation
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func BuildDescriptorSet ¶
func BuildDescriptorSet( ctx context.Context, runner interfaces.CommandRunner, protoDir string, ) ([]byte, error)
BuildDescriptorSet runs `buf build -o -` in protoDir and returns the marshaled FileDescriptorSet buf writes to stdout. `buf build` does not run gen managed mode, so the custom jsonapi.v1 options survive — the descriptor is what both the route-table generator (BuildRouteTable) and the completeness gate (CheckCompleteness) classify over. The bytes never touch disk.
func BuildRouteTable ¶
BuildRouteTable walks every service→method in a marshaled FileDescriptorSet and emits, per JSON:API service (grouped by the file's (jsonapi.v1.service) name), one RouteEntry for each google.api.http binding. For each binding it assembles a Binding from the HTTP verb/path, the response message's (jsonapi.v1.resource) keys, and the method's (jsonapi.v1.operation){action} marker, then runs the Classify. An unclassifiable, non-exempt RPC hard-fails (propagating Classify's coded apperr.CodeInvalidInput) rather than being silently skipped — exactly the completeness guarantees. An RPC named in exempt is skipped (no entry) when unclassifiable, so a genuine action/internal RPC does not fail the generator for the whole surface (the exempt set is config data shared with the completeness gate). Files without a (jsonapi.v1.service) option and gRPC-only methods (no http binding) are not part of the REST surface and are skipped. Each service's entries are sorted by (path, method) for deterministic output.
func MergeSwaggerFiles ¶
MergeSwaggerFiles reads multiple swagger JSON files and merges their paths and definitions into a single spec. Files are sorted first so the base file (which provides info/host/etc. verbatim) is deterministic regardless of the caller's order — the file list is built from map iteration, which otherwise flipped the merged info.title between runs. Others contribute their paths and definitions. The files are read through fsys.
func WriteRuntimeRouteTable ¶
func WriteRuntimeRouteTable( fsys interfaces.FS, routes map[string][]RouteEntry, outDir, runtimeImport string, ) error
WriteRuntimeRouteTable writes a generated Go file per service (<outDir>/<service>/routes.go) holding the routes' shared `BasePath` and a `var Routes []jsonapi.RouteEntry` literal — the runtime route table the JSON:API middleware consumes. runtimeImport is the import path of the runtime jsonapi package that declares RouteEntry (a parameter so this generator stays repo-agnostic). Each file is gofmt-formatted; a malformed emission fails loudly rather than writing invalid Go. Output is deterministic (services and entries are already sorted). The files are written through fsys.
Types ¶
type Binding ¶
type Binding struct {
// RPC is the fully-qualified method name (e.g.
// "library.v1.BookService.GetBook"), used only in error messages.
RPC string
// HTTPMethod is the binding's verb (GET/POST/PUT/PATCH/DELETE). Compared
// case-insensitively.
HTTPMethod string
// PathTemplate is the raw google.api.http path template, base path included
// and no trailing slash (e.g. "/v1/libraries/{library_id}/books").
PathTemplate string
// SingleKey is the response's single-resource key from (jsonapi.v1.resource);
// empty when the response carries no single resource.
SingleKey string
// CollectionKey is the response's collection key from (jsonapi.v1.resource);
// empty when the response carries no collection. SingleKey and CollectionKey
// both empty means the response carries no resource shape.
CollectionKey string
// Action is the (jsonapi.v1.operation){action} marker: it forces a POST with
// a resource-shaped response to classify as ACTION rather than CREATE.
Action bool
}
Binding is the minimal, descriptor-free seam Classify operates over: the HTTP binding plus the response's JSON:API resource keys and the optional action marker. Keeping Classify pure over this struct (rather than over protoreflect descriptors) is what lets the route-table generator and the completeness gate share one classifier and unit-test it without a FileDescriptorSet. The descriptor → Binding extraction is.
type ResourceKeys ¶
type ResourceKeys struct {
// Type is the JSON:API data.type value (e.g. "users").
Type string
// SingleKey is the response property holding one resource (e.g. "user");
// empty for collection-only resources.
SingleKey string
// CollectionKey is the response property holding a resource list (e.g.
// "users"); empty for single-only resources.
CollectionKey string
}
ResourceKeys is the exported, output-affecting identity of a decoded JSON:API resource — the response-property names the converter reads when wrapping a response. paginationKey is omitted because it is inert (the collection envelope's shape is fixed). It is the black-box surface over the unexported resourceConfig for ReadServiceConfigs and ValidateResourceKeys.
type Result ¶
type Result struct {
// Written holds the output paths of every per-service OpenAPI spec written.
Written []string
// Skipped holds the input swagger file paths whose proto package declared no
// jsonapi.v1.service annotation, so no service could be resolved for them.
Skipped []string
}
Result reports the per-service specs written and the proto packages skipped (those declaring no jsonapi.v1.service annotation).
func Convert ¶
func Convert( fsys interfaces.FS, inDir, outDir, descriptorPath string, routes map[string][]RouteEntry, headers map[string]string, ) (Result, error)
Convert reads every generated swagger file under inDir and writes a per-service JSON:API OpenAPI 3 spec under outDir, driven by the jsonapi.v1 annotations carried in the buf descriptor set at descriptorPath. Every read and write goes through fsys.
headers maps each API-key security scheme every spec declares and requires to its request header (e.g. the identity headers a gateway injects); empty declares none.
routes is the operation-keyed route table (from BuildRouteTable), keyed by service name; each service's entries drive the per-operation envelope. A nil/empty table makes every operation fall back to the by-resource-property response match, so callers that don't need per-operation enveloping (or that have no descriptor-derived table) get the prior behavior unchanged.
type Role ¶
type Role string
Role is the JSON:API operation role a REST RPC is classified into. It is the single notion of "what kind of operation is this" that the OpenAPI/TS-client generator, the runtime middleware and the completeness gate all consume, so the three never re-derive it separately.
const ( // RoleList is a collection read: GET returning data[] + pagination. RoleList Role = "LIST" // RoleGetOne is a single-resource read: GET on a specific instance (trailing // {id}) or a singleton (e.g. /me). RoleGetOne Role = "GET_ONE" // RoleCreate is a resource creation: POST returning the created resource(s) // (a single resource, or a collection for a bulk create). RoleCreate Role = "CREATE" // RoleUpdate is a resource replacement/patch: PUT or PATCH. RoleUpdate Role = "UPDATE" // RoleDelete is a resource deletion: DELETE (204, no body). RoleDelete Role = "DELETE" // RoleAction is a non-CRUD operation: meta-only, or a POST whose // resource-shaped response is semantically an action (via the marker). RoleAction Role = "ACTION" )
The JSON:API operation roles. Every REST RPC classifies into exactly one.
func Classify ¶
Classify derives the JSON:API Role of a REST RPC from its Binding, or returns a coded error (apperr.CodeInvalidInput) naming the RPC when the operation is unclassifiable — never a silent default. Derivation:
- DELETE → DELETE
- PUT / PATCH → UPDATE
- GET + trailing {id} → GET_ONE
- GET + collection key → LIST
- GET + single key (no trailing id, singleton e.g. /me) → GET_ONE
- POST + action marker → ACTION
- POST + resource response + not a trailing-{id} path → CREATE
- anything else (a bare POST with no resource and no marker, a POST that returns a resource on a trailing-{id} path without the action marker, or an unhandled method) → error
A POST returning a resource on a specific-instance path is intentionally an error: it is ambiguous between an idempotent create and an instance action, so the author must add the (jsonapi.v1.operation){action} marker or restructure.
type RouteEntry ¶
type RouteEntry struct {
// Method is the binding's HTTP verb (GET/POST/PUT/PATCH/DELETE), upper-cased.
Method string
// PathTemplate is the raw google.api.http path template for this binding,
// base path included (e.g. "/v1/libraries/{library_id}/books").
PathTemplate string
// Role is the JSON:API operation role this binding classifies into (see
// Classify).
Role Role
// ResourceType is the response's JSON:API `data.type` value; empty when the
// response carries no resource.
ResourceType string
// SingleKey is the response property holding one resource; empty for
// collection-only or resource-less responses.
SingleKey string
// CollectionKey is the response property holding a resource list; empty for
// single-only or resource-less responses.
CollectionKey string
// PaginationKey is the response property holding pagination metadata;
// carried for fidelity but inert (the collection envelope's shape is fixed).
PaginationKey string
// RPC is the fully-qualified method name (e.g.
// "library.v1.BookService.GetBook").
RPC string
// RequestEnvelope reports whether the operation's request body should be
// wrapped in the JSON:API {data: {type, attributes}} shape — true only for
// the CREATE and UPDATE roles.
RequestEnvelope bool
}
RouteEntry is one operation-keyed row of the generated route table: a single REST binding (method + path) tagged with the JSON:API Role it classifies into and the response's resource keys. It is the single generated contract that drives the per-operation OpenAPI envelope, the runtime middleware and the completeness gate, so the three never re-derive it separately.
type ServiceView ¶
type ServiceView struct {
// Resources holds the service's decoded JSON:API resource keys, keyed by
// resource type.
Resources map[string]ResourceKeys
// BasePath is the service's REST base path (e.g. "/v1/library").
BasePath string
}
ServiceView is the exported projection of a service's decoded JSON:API config: its REST base path and the per-resource keys the jsonapi.v1 annotations encode, keyed by resource type. ReadServiceConfigs returns it so callers (and black-box tests) can inspect the decoded contract without depending on the unexported serviceConfig/resourceConfig structs.
type Violation ¶
type Violation struct {
// RPC is the fully-qualified method name (e.g.
// "library.v1.BookService.MarkRead").
RPC string
// Reason is why the RPC is unclassifiable — the classifier's message, which
// names the RPC, method, and path so the failure points at the exact contract.
Reason string
}
Violation is one REST RPC the completeness gate rejects: its fully-qualified name and the human-readable reason it could not be classified into a JSON:API role (and is not exempt). It is the output of CheckCompleteness — a completeness gate turns a non-empty slice into a loud, named failure.
func CheckCompleteness ¶
CheckCompleteness walks the same REST surface as BuildRouteTable over a marshaled FileDescriptorSet and returns EVERY REST RPC that cannot be classified into a JSON:API role and is not exempt — the collect-all sibling of BuildRouteTable's fail-fast behavior, so a completeness gate reports the whole backlog in one run instead of one offender at a time. It gates the identical surface the generator does (only files carrying (jsonapi.v1.service); only methods with a google.api.http binding), so the gate and the generator never disagree.
A structural/reader error (unparseable descriptor, malformed annotation, custom binding) is returned as the error result, not a Violation — it is a build defect the generator also rejects, distinct from a completeness gap. Violations are sorted by RPC for deterministic output.