api

package
v1.0.0 Latest Latest
Warning

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

Go to latest
Published: Sep 30, 2026 License: MIT Imports: 7 Imported by: 0

Documentation

Overview

Package api is the fleet's HTTP API envelope: one error shape, one type and code vocabulary, one status inference, and the list/pagination shapes that go with them. It depends on nothing outside the standard library, so every library in the fleet can speak it. Framework bindings live in sub-modules (adapters/gin).

The canonical body is the shape AuthKit and OpenRails deploy today:

{"error":{"type":"...","code":"...","message":"...","param":"...","request_id":"...","metadata":{}}}

GinAPI wraps that in a top-level "object":"error" discriminator. That is a COMPATIBILITY DIFFERENCE, not the canonical form: see CompatEnvelope, and compat/DECISION-object-discriminator.md for the evidence and the open decision. Nothing here emits the discriminator on its own.

Index

Constants

View Source
const (
	DefaultLimit = 20
	MaxLimit     = 100
)

Offset pagination defaults.

Variables

This section is empty.

Functions

func AllowedMetadataKeys

func AllowedMetadataKeys() []string

AllowedMetadataKeys lists every currently allowlisted key, sorted.

func HasMore

func HasMore(offset, limit int, total int64) bool

HasMore reports whether items follow a full page of limit items.

func HasMoreFromLen

func HasMoreFromLen(offset, resultLen int, total int64) bool

HasMoreFromLen reports whether items follow, using the rows actually read — correct when the last page is short.

func MetadataRejections

func MetadataRejections(m map[string]any) map[string]string

MetadataRejections reports, per key, why SanitizeMetadata would drop it. It exists so a test can assert a leak is refused instead of hoping it was.

func RegisterMetadataKey

func RegisterMetadataKey(keys ...string)

RegisterMetadataKey allowlists a metadata key for this process. A reserved name is refused: it would shadow an envelope field downstream. Call it from a package initializer, next to the codes the domain defines.

func SanitizeMetadata

func SanitizeMetadata(m map[string]any) map[string]any

SanitizeMetadata returns the subset of m that may go on the wire, or nil.

func WriteError

func WriteError(w http.ResponseWriter, err error)

WriteError writes err as the canonical envelope over net/http.

func WriteJSON

func WriteJSON(w http.ResponseWriter, status int, v any)

WriteJSON writes v as a JSON body with the canonical content type.

Types

type Code

type Code string

Code is the stable, machine-readable reason, and the field a client branches on. Transport-level codes are below; each library keeps its own domain catalog (AuthKit's hundreds of auth codes, OpenRails' decline codes) and emits those in the same field.

const (
	CodeInvalidParam  Code = "invalid_param"
	CodeMissingParam  Code = "missing_param"
	CodeInvalidFormat Code = "invalid_format"

	CodeResourceNotFound Code = "resource_not_found"
	CodeResourceConflict Code = "resource_conflict"

	CodeAuthenticationRequired Code = "authentication_required"
	CodeInvalidToken           Code = "invalid_token"
	CodeTokenExpired           Code = "token_expired"
	CodeResourceAccessDenied   Code = "resource_access_denied"

	CodeRateLimitExceeded Code = "rate_limit_exceeded"

	// CodeModerationRejected is a moderation/policy refusal of submitted
	// content: 422 under TypeInvalidRequest. The transport category says
	// "the request was not acted on"; this code says why, and is what a SPA
	// branches on to show the author a reason instead of a retry.
	CodeModerationRejected Code = "moderation_rejected"
	// CodeNotConfigured is an optional capability the operator never wired.
	CodeNotConfigured Code = "not_configured"
	// CodeNotImplemented is a capability this build does not have: 501 under
	// TypeAPI. Same transport handling as any 5xx; the code is what tells the
	// client to hide the feature rather than retry.
	CodeNotImplemented Code = "not_implemented"

	CodePaymentFailed      Code = "payment_failed"
	CodeInternalError      Code = "internal_error"
	CodeServiceUnavailable Code = "service_unavailable"
)

func CodeForStatus

func CodeForStatus(status int) Code

CodeForStatus is the default code for a status, matching OpenRails' inferErrorTypeAndCode. Root writers do NOT apply it: an absent code means "no machine reason beyond the status", and filling one in is wire-visible — Doujins' client displays code in preference to message, so an invented code replaces a human sentence with a machine string (compat/golden/parsers.json). OpenRails calls it explicitly because its writers have always emitted a code.

type CompatEnvelope

type CompatEnvelope struct {
	Object string      `json:"object"` // always "error"
	Error  ErrorObject `json:"error"`
}

CompatEnvelope is Envelope plus GinAPI's top-level discriminator. It exists so a host that must keep the discriminator can, and so both candidate bodies can be shown side by side. Nothing in api emits it by default: whether the discriminator belongs in the canonical envelope is an open owner decision (compat/DECISION-object-discriminator.md).

func WithObject

func WithObject(env Envelope) CompatEnvelope

WithObject renders env in the GinAPI-compatible shape.

type DeletedObject

type DeletedObject struct {
	Object  string `json:"object"`
	ID      string `json:"id"`
	Deleted bool   `json:"deleted"`
}

DeletedObject confirms a deletion: {"object":"gallery","id":"…","deleted":true}.

func NewDeleted

func NewDeleted(objectType, id string) DeletedObject

NewDeleted builds a deletion confirmation.

type Envelope

type Envelope struct {
	Error ErrorObject `json:"error"`
}

Envelope is the canonical error body: {"error":{...}}.

func EnvelopeFor

func EnvelopeFor(err error) (int, Envelope)

EnvelopeFor derives the wire status and envelope for any error. An error that is not an *Error — and any 500 — is rendered as a bare internal error, so an internal message never reaches the wire. 501, 502 and 503 state a deliberate operational condition and keep theirs.

func NewEnvelope

func NewEnvelope(obj ErrorObject) Envelope

NewEnvelope builds the canonical envelope, defaulting the type and sanitizing metadata. Every path to the wire goes through it.

type Error

type Error struct {
	Status    int
	Type      Type
	Code      Code
	Message   string
	RequestID string
	Metadata  map[string]any
	// contains filtered or unexported fields
}

Error is the transport carrier: a library maps its own sentinel onto one of these and a single writer renders it. It is not a catalog — libraries keep their own.

func AsError

func AsError(err error) *Error

AsError returns the *Error in err's chain, or nil.

func E

func E(status int, code Code, message string) *Error

E builds an Error, inferring the type from the status when none is given.

func (*Error) Envelope

func (e *Error) Envelope() Envelope

Envelope renders the error as its wire body.

func (*Error) Error

func (e *Error) Error() string

func (*Error) Is

func (e *Error) Is(target error) bool

Is matches any *Error carrying the same Code, so a sentinel, a fresh E() and a wrapped copy are one identity.

func (*Error) Param

func (e *Error) Param() string

Param is the offending request field, or "" when none was named.

func (*Error) Unwrap

func (e *Error) Unwrap() error

func (*Error) WithCause

func (e *Error) WithCause(cause error) *Error

func (*Error) WithMetadata

func (e *Error) WithMetadata(m map[string]any) *Error

WithMetadata merges public metadata. It sanitizes on the way in, so an unsafe value never reaches the struct, let alone the wire.

func (*Error) WithParam

func (e *Error) WithParam(param string) *Error

WithParam names the offending field. It takes a plain string; the pointer the wire needs is this package's problem, not the caller's.

func (*Error) WithRequestID

func (e *Error) WithRequestID(id string) *Error

func (*Error) WithType

func (e *Error) WithType(t Type) *Error

type ErrorObject

type ErrorObject struct {
	Type      Type           `json:"type"`
	Code      Code           `json:"code,omitempty"`
	Message   string         `json:"message"`
	Param     *string        `json:"param,omitempty"`
	RequestID string         `json:"request_id,omitempty"`
	Metadata  map[string]any `json:"metadata,omitempty"`
}

ErrorObject is the error detail carried under the envelope's "error" key. Param is a pointer so "absent" and "empty" stay distinguishable in Go; on the wire omitempty makes it identical to AuthKit's and OpenRails' shape. Callers never build the pointer — the constructors take a plain string.

type List

type List[T any] struct {
	Object  string `json:"object"` // always "list"
	Data    []T    `json:"data"`
	Total   int64  `json:"total"`
	Limit   int    `json:"limit"`
	Offset  int    `json:"offset"`
	HasMore bool   `json:"has_more"`
}

List is the offset/limit list body: {"object":"list","data":[...],...}.

func NewList

func NewList[T any](data []T, total int64, limit, offset int) List[T]

NewList builds a list body, computing has_more and never emitting a null data.

type ListParams

type ListParams struct {
	Limit  int
	Offset int
	Sort   string
}

ListParams is the offset/limit/sort input a list endpoint reads from the request. Framework bindings fill it; see adapters/gin.

func (*ListParams) Normalize

func (p *ListParams) Normalize(defaultLimit, maxLimit int)

Normalize applies the caller's default and cap and floors the offset.

type Message

type Message struct {
	Object  string `json:"object"` // always "message"
	Message string `json:"message"`
}

Message is a bare human-readable success body.

func NewMessage

func NewMessage(message string) Message

NewMessage builds a message body.

type Type

type Type string

Type is the transport-level category of a failure: what a client does next. The constants are the ones AuthKit and OpenRails already put on the wire. Type is an open string so a domain may add its own.

Adding a transport type is a wire migration for every parser in the fleet. A failure that only needs explaining gets a Code instead — that is the field a SPA branches on.

const (
	// TypeInvalidRequest: fix the input, then retry. 400, and every other
	// unclassified 4xx — including 404, 409, 415 and 422. Both deployed
	// writers map them here; the reason lives in Code.
	TypeInvalidRequest Type = "invalid_request_error"
	// TypeAuthentication: authenticate or refresh, then retry. 401.
	TypeAuthentication Type = "authentication_error"
	// TypeAuthorization: never retry as this principal. 403.
	TypeAuthorization Type = "authorization_error"
	// TypeRateLimit: back off and retry later. 429.
	TypeRateLimit Type = "rate_limit_error"
	// TypeAPI: the server failed or lacks the capability. 5xx, 501 included.
	TypeAPI Type = "api_error"
	// TypeCard is OpenRails' domain extension for 402: collect a new payment
	// method. It is inferred only for 402, which no other fleet service uses.
	TypeCard Type = "card_error"
)

func TypeForStatus

func TypeForStatus(status int) Type

TypeForStatus is the transport category an HTTP status determines, matching what AuthKit and OpenRails deploy today. 404, 409, 415 and 422 are deliberately TypeInvalidRequest: both writers already map them there, and splitting them out is a wire migration, not a bug fix.

Directories

Path Synopsis
Package ginapi writes api envelopes, lists and objects through a gin.Context, binds list parameters from the query string, and carries the locale middleware.
Package ginapi writes api envelopes, lists and objects through a gin.Context, binds list parameters from the query string, and carries the locale middleware.

Jump to

Keyboard shortcuts

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