openapi

package
v0.0.28 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 29, 2026 License: MIT Imports: 22 Imported by: 0

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

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

func BuildRouteTable(
	fdsBytes []byte,
	exempt map[string]bool,
) (map[string][]RouteEntry, error)

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

func MergeSwaggerFiles(fsys interfaces.FS, files []string) (map[string]any, error)

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

func Classify(b Binding) (Role, error)

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

func CheckCompleteness(
	fdsBytes []byte,
	exempt map[string]bool,
) ([]Violation, error)

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.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL