Documentation
¶
Overview ¶
Package api holds REST helpers on top of web and orm: resource transformers, pagination envelopes, allowlisted query conventions (filter, sort, fields, include, pagination) and deprecation headers.
Register adds typed handlers (input bound and validated, output written as JSON) that package openapi describes; Collection, Page and Cursor answer list endpoints in an Envelope; Query applies the allowlisted query string to an orm.Query; Idempotency and Deprecate are middleware. API-only apps start from `vukem new <name> --api`.
See https://github.com/AlexGalhardo/vukem/blob/main/docs/api.md.
Index ¶
- Constants
- func Collection[T, R any](c *web.Ctx, items []T, fn func(T) R) error
- func Cursor[T, R any](c *web.Ctx, p orm.CursorPage[T], fn func(T) R) error
- func Deprecate(since, sunset time.Time, link string) func(http.Handler) http.Handler
- func Idempotency(store cache.Store, o IdempotencyOptions) func(http.Handler) http.Handler
- func Map[T, R any](items []T, fn func(T) R) []R
- func Page[T, R any](c *web.Ctx, p orm.Page[T], fn func(T) R) error
- func Register[In, Out any](g Registrar, pattern string, op Op, fn func(*web.Ctx, In) (Out, error)) *router.Route
- type Envelope
- type IdempotencyOptions
- type Links
- type Meta
- type Op
- type Operation
- type Option
- type Registrar
- type Request
Examples ¶
Constants ¶
const IdempotencyHeader = "Idempotency-Key"
IdempotencyHeader is the request header carrying the key.
Variables ¶
This section is empty.
Functions ¶
func Collection ¶
Collection answers 200 with {"data": [...]}.
Example ¶
package main
import (
"fmt"
"net/http"
"net/http/httptest"
"strings"
"github.com/AlexGalhardo/vukem/api"
"github.com/AlexGalhardo/vukem/router"
"github.com/AlexGalhardo/vukem/web"
)
type exProductJSON struct {
ID int `json:"id"`
Name string `json:"name"`
}
func main() {
r := router.New(nil)
r.Get("/products", func(c *web.Ctx) error {
names := []string{"Mug", "Cap"}
return api.Collection(c, names, func(n string) exProductJSON { return exProductJSON{Name: n} })
})
rec := httptest.NewRecorder()
r.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/products", http.NoBody))
fmt.Println(rec.Code, strings.TrimSpace(rec.Body.String()))
}
Output: 200 {"data":[{"id":0,"name":"Mug"},{"id":0,"name":"Cap"}]}
func Cursor ¶
Cursor answers 200 with a keyset page: meta.next_cursor and links.next (null on the last page).
func Deprecate ¶
Deprecate marks routes as deprecated (RFC 9745 Deprecation and RFC 8594 Sunset headers) and links to the replacement or migration guide. Apply it to a version group: g.Use(api.Deprecate(since, sunset, "/docs/v2")).
func Idempotency ¶ added in v0.15.0
Idempotency makes retried requests safe: the first request with an Idempotency-Key runs and its response (status below 500) is stored; repeats replay it with "Idempotent-Replayed: true". A repeat while the first is running gets 409, and reusing a key for a different request gets 422. If the store fails the request is refused with 503 (fail closed) rather than risking a duplicate side effect.
func Map ¶
func Map[T, R any](items []T, fn func(T) R) []R
Map transforms every item with fn; resources use it to turn models into their wire format: func Products(ps []models.Product) []ProductJSON { return api.Map(ps, Product) }.
func Page ¶
Page answers 200 with an offset page: data, meta (page, per_page, total, last_page) and links (self, next, prev).
func Register ¶ added in v0.15.0
func Register[In, Out any](g Registrar, pattern string, op Op, fn func(*web.Ctx, In) (Out, error)) *router.Route
Register adds a typed handler. The input is built from the query string, the JSON or form body and the path parameters (which win), and validated with In's `validate` tags (422 with field errors). The output is written as JSON. GET, HEAD and DELETE ignore the body. Pattern is "METHOD /path".
api.Register(g, "POST /products", api.Op{Name: "products.store"},
func(c *web.Ctx, in StoreProduct) (ProductJSON, error) { ... })
Example ¶
package main
import (
"fmt"
"net/http"
"net/http/httptest"
"strings"
"github.com/AlexGalhardo/vukem/api"
"github.com/AlexGalhardo/vukem/router"
"github.com/AlexGalhardo/vukem/web"
)
type exStoreProduct struct {
Name string `json:"name" validate:"required"`
Price int `json:"price" validate:"min=1"`
}
type exProductJSON struct {
ID int `json:"id"`
Name string `json:"name"`
}
func main() {
r := router.New(nil)
api.Register(r, "POST /products", api.Op{Name: "products.store", Tags: []string{"products"}},
func(_ *web.Ctx, in exStoreProduct) (exProductJSON, error) {
return exProductJSON{ID: 1, Name: in.Name}, nil
})
req := httptest.NewRequest(http.MethodPost, "/products", strings.NewReader(`{"name":"Mug","price":12}`))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
r.ServeHTTP(rec, req)
fmt.Println(rec.Code, strings.TrimSpace(rec.Body.String()))
}
Output: 201 {"id":1,"name":"Mug"}
Types ¶
type Envelope ¶
type Envelope[R any] struct { Data []R `json:"data"` Meta *Meta `json:"meta,omitempty"` Links *Links `json:"links,omitempty"` }
Envelope is the JSON body of collections and pages.
type IdempotencyOptions ¶ added in v0.15.0
type IdempotencyOptions struct {
// Methods guarded (default POST and PATCH).
Methods []string
// Required answers 400 when the header is missing.
Required bool
// TTL keeps completed responses (default 24h).
TTL time.Duration
// LockTTL bounds an in-flight request (default 1 minute).
LockTTL time.Duration
// MaxBody bounds the request body that is fingerprinted (default 1 MiB).
MaxBody int64
// Scope separates callers so one cannot replay another's response.
// Default: the Authorization header; set it for cookie sessions (e.g.
// the user ID). A request with a key whose scope is empty is refused
// with 400; return a constant to share keys on purpose.
Scope func(r *http.Request) string
}
IdempotencyOptions tune Idempotency.
type Links ¶
type Links struct {
Self string `json:"self"`
Next *string `json:"next"`
Prev *string `json:"prev"`
}
Links point to neighbour pages; nil encodes as null.
type Meta ¶
type Meta struct {
Page int `json:"page,omitempty"`
PerPage int `json:"per_page"`
Total *int64 `json:"total,omitempty"`
LastPage int `json:"last_page,omitempty"`
NextCursor string `json:"next_cursor,omitempty"`
}
Meta describes a page.
type Op ¶ added in v0.15.0
type Op struct {
// Name is the route name (and the OpenAPI operationId).
Name string
Summary string
Description string
Tags []string
// Status is the success status; default 201 for POST, 204 when the
// output type is struct{}, 200 otherwise.
Status int
Deprecated bool
// Security lists the security schemes required (e.g. "bearer").
Security []string
}
Op describes a typed operation for the router and the OpenAPI document.
type Operation ¶ added in v0.15.0
type Operation struct {
Op
Method string
Path string
Params []string
In reflect.Type
Out reflect.Type
}
Operation is attached to every typed route (router.RouteInfo.Doc) so generators know its input and output types.
type Option ¶
type Option func(*spec)
Option allowlists one query convention for an endpoint.
type Registrar ¶ added in v0.15.0
type Registrar interface {
Match(methods []string, path string, h web.HandlerFunc) []*router.Route
}
Registrar is a router.Router or router.Group.
type Request ¶
type Request[T any] struct { Query orm.Query[T] Page int PerPage int Cursor string Includes []string }
Request is a query refined by the allowlisted conventions.