Documentation
¶
Overview ¶
Package jsonapi provides HTTP middleware that transforms Vanguard REST responses into [JSON:API](https://jsonapi.org/format/) envelope format.
Only REST requests are transformed; Connect, gRPC, and gRPC-Web pass through unchanged. The middleware sits between the HTTP server and Vanguard, rewriting response bodies into {data, meta, links, errors} and validating/unwrapping the inbound {data:{type,attributes}} request envelope.
The transformation is driven by an operation-keyed route table the consumer supplies (typically generated from its API definitions): each REST RPC classifies into a Role, and the middleware transforms by role rather than re-guessing method/path/data-presence.
Index ¶
- func IsRESTRequest(r *http.Request) bool
- func Middleware(inner http.Handler, cfg Config) http.Handler
- func TransformCollection(body map[string]any, cfg ResourceConfig, selfLink string) map[string]any
- func TransformError(statusCode int, body []byte) []byte
- func TransformMeta(body map[string]any) map[string]any
- func TransformSingleResource(body map[string]any, cfg ResourceConfig, selfLink string) map[string]any
- type Config
- type ResourceConfig
- type Role
- type RouteEntry
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
func IsRESTRequest ¶
IsRESTRequest returns true if the request appears to be a REST request (not Connect, gRPC, or gRPC-Web).
func Middleware ¶
Middleware wraps an http.Handler with JSON:API response/request transformation, driven by the generated operation-keyed route table. Only REST requests are transformed; Connect, gRPC, and gRPC-Web pass through unchanged.
Request handling order is match-first: the route is resolved by (method, path) before any body handling, so an unmatched path passes through untouched. A matched CREATE/UPDATE route validates + unwraps the inbound {data:{type,attributes}} envelope (400 before the handler on a malformed, absent, or type-mismatched envelope); other roles pass their body through. The response is then transformed by the route's Role — never by guessing method/path/data-presence.
func TransformCollection ¶
func TransformCollection( body map[string]any, cfg ResourceConfig, selfLink string, ) map[string]any
TransformCollection converts a Vanguard list response to JSON:API format.
Input: {"users": [...], "pagination": {"nextPageToken":"abc", "totalCount":45, "totalIsEstimate":true}} Output: {"data": [...], "meta": {"totalCount":45, "totalIsEstimate":true}, "links":{"self":"...", "next":"..."}}
func TransformError ¶
TransformError converts a Connect/Vanguard error response body to JSON:API error format.
Input: {"code":5, "message":"user not found"} (Vanguard: numeric google.rpc.Code)
or {"code":"not_found", "message":"user not found"} (this package's own writers)
Output: {"errors":[{"code":"not_found","status":"404","detail":"user not found"}]}
The member carries three fields: "code" (the machine-readable app error code, defaulting to "unknown" when the upstream body is empty, unparseable, or carries an unrecognized code), "status" (the HTTP status as a string) and "detail" (the human-readable message). Only "detail" is optional — it is omitted rather than filled with a placeholder when the upstream body carries no message, so a client can tell "no message was sent" from "this is the message". No "title" is emitted: it was derived from the status code alone and so duplicated "status".
Vanguard renders a transcoded Connect/gRPC error as a google.rpc.Status whose "code" is a NUMBER, so the numeric code is normalized to its canonical connect name (see normalizeErrorCode); without that every REST error would degrade to "unknown", stripping the machine-readable code every caller keys on.
The document always holds exactly one error member; JSON:API models "errors" as an array, and that shape is kept even though nothing here reports more than one failure at a time.
func TransformMeta ¶
TransformMeta converts a non-resource response to a JSON:API meta-only document. Per the JSON:API spec, a valid document must contain at least one of: data, errors, or meta. Action endpoints that return status info (not a resource) use this format.
Input: {"sent": 5, "failed": 0, "failedEmails": []} Output: {"meta": {"sent": 5, "failed": 0, "failedEmails": []}}
func TransformSingleResource ¶
func TransformSingleResource( body map[string]any, cfg ResourceConfig, selfLink string, ) map[string]any
TransformSingleResource converts a Vanguard single-resource response to JSON:API format.
Input: {"user": {"id":"123", "email":"a@b.com", ...}} Output: {"data": {"type":"users", "id":"123", "attributes":{...}}, "links":{"self":"..."}}
Types ¶
type Config ¶
type Config struct {
// BasePath is the URL prefix all routes share (e.g. "/api/v1/demo"). Paths
// outside it skip the route-table scan (and pass through untransformed).
BasePath string
// Routes is the generated operation-keyed route table for the service.
Routes []RouteEntry
}
Config holds the JSON:API middleware configuration: the generated route table plus the service base path used only as a fast-reject prefix.
func (Config) MatchRoute ¶
func (c Config) MatchRoute(method, path string) (RouteEntry, bool)
MatchRoute finds the RouteEntry whose method and path template match a concrete request (method, path). A template segment matches when it is a "{param}" wildcard or byte-equal to the concrete segment. When more than one template could match, a literal-segment match beats a "{param}" wildcard at the same position (so "/connectors/test" resolves to the literal action route, not "/connectors/{id}"). Returns false when no route matches.
type ResourceConfig ¶
type ResourceConfig struct {
// Type is the JSON:API resource type (e.g. "users").
Type string
// SingleKey is the protobuf JSON key for a single resource response
// (e.g. "user" from GetUserResponse).
SingleKey string
// CollectionKey is the protobuf JSON key for a list response
// (e.g. "users" from ListUsersResponse).
CollectionKey string
// PaginationKey is the protobuf JSON key for pagination metadata
// (e.g. "pagination" from ListUsersResponse).
PaginationKey string
}
ResourceConfig describes how a single protobuf resource type maps to JSON:API response structure. It is the transform functions' input, derived from the matched RouteEntry.
type Role ¶
type Role string
Role is the JSON:API operation role a REST route classifies into. The consumer's route table (typically emitted by its code generator) tags each route with one; the middleware switches on it.
const ( // RoleList is a collection read: GET returning data[] + meta. RoleList Role = "LIST" // RoleGetOne is a single-resource read: GET on an instance or a singleton. RoleGetOne Role = "GET_ONE" // RoleCreate is a resource creation: POST returning the created resource. 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, never a data resource. RoleAction Role = "ACTION" )
The JSON:API operation roles. Every REST route carries exactly one.
type RouteEntry ¶
type RouteEntry struct {
// Method is the upper-case HTTP verb (GET/POST/PUT/PATCH/DELETE).
Method string
// PathTemplate is the raw google.api.http path template, base path included
// (e.g. "/api/v1/widgets/{widget_id}/parts/{part_id}"). A
// "{param}" segment is a wildcard when matching a concrete request path.
PathTemplate string
// Role is the JSON:API operation role driving the transform.
Role Role
// ResourceType is the JSON:API data.type (e.g. "users"); empty for a
// DELETE/action response that carries no resource shape.
ResourceType string
// SingleKey is the response's single-resource key (e.g. "user"); empty when
// the response carries no single resource.
SingleKey string
// CollectionKey is the response's collection key (e.g. "users"); empty when
// the response carries no collection.
CollectionKey string
// PaginationKey is the response's pagination-metadata key; carried for fidelity.
PaginationKey string
// RPC is the fully-qualified method name (for diagnostics); unused at runtime.
RPC string
// RequestEnvelope is true when the inbound body is JSON:API-enveloped
// ({data:{type,attributes}}) and must be validated + unwrapped — CREATE and
// UPDATE only. (Ordered last to keep the string fields packed.)
RequestEnvelope bool
}
RouteEntry is one operation-keyed row of the route table: an HTTP binding (method + path template) tagged with its JSON:API Role and the response resource keys. It is the single contract the middleware consumes; a consumer's route table (hand-written or generated) is a slice of this type.