howdy/version
Route requests to controllers by API version.
A version group resolves the requested version with one strategy, then matches the request against that version’s controllers. Later versions fall back to earlier ones for any route they do not override, so a new version only declares what changed.
import howdy/version
let api =
version.new(version.path())
|> version.default("v1")
|> version.add("v1", [user_v1.controller(), order.controller()])
|> version.add("v2", [user_v2.controller()])
howdy.new()
|> howdy.controller(status.controller())
|> howdy.versions(api)
|> howdy.handler
With the path strategy GET /v2/users hits user_v2 and GET /v2/orders
falls back to the v1 order controller. GET /users uses the default
version. Handlers read the resolved version from ctx.version.
Strategies:
path(): the first path segment, such as/v2/users. The segment is removed before routing. A segment that is not a declared version is left alone, so/statusstill reaches unversioned controllers.header("x-api-version"): a request header holding the version name.accept("vnd.howdy"): a vendor media type such asapplication/vnd.howdy.v2+jsonin theacceptheader.custom(fn): any function from the request to an optional version.
Only one strategy applies per app. Mixing strategies with precedence rules
makes requests ambiguous; if you need it, write a custom resolver.
Header, accept and custom strategies answer 400 for an unknown version
and, when no default is set, for a missing one. Header and accept also
add a vary header to every response from the group so caches keep
versions apart. The path strategy answers 404 in both cases, since the
path simply does not match anything.
Versions are opaque strings ordered by declaration. Configuration errors such as duplicate version names panic when the app is built.
Types
How a group reads the version, as set by its resolver.
pub type Strategy {
PathStrategy
HeaderStrategy(name: String)
AcceptStrategy(vendor: String)
CustomStrategy
}
Constructors
-
PathStrategypath(): the first path segment, as in/v2/users. -
HeaderStrategy(name: String)header(name): a request header.nameis lowercase. -
AcceptStrategy(vendor: String)accept(vendor): a vendor media type such asapplication/vnd.howdy.v2+json. -
CustomStrategycustom(fn): a function that cannot be described.
Values
pub fn accept(vendor: String) -> Resolver
Read the version from a vendor media type in the accept header.
accept("vnd.howdy") matches application/vnd.howdy.v2+json.
pub fn add(
group: Group,
name: String,
controllers: List(controller.Builder(Nil)),
) -> Group
Add a version. Declaration order is the fallback order: a version falls back to the ones added before it, newest first. Adding a version twice or with an empty name panics.
pub fn controllers(
group: Group,
name: String,
) -> Result(List(controller.Builder(Nil)), Nil)
The controllers that answer for a version, with fallback applied: its
own first, then earlier versions’, newest first. The first route that
matches a request wins, so a version’s overrides come before what it
inherits. Error for a version that was never added.
pub fn custom(
resolve: fn(request.Request(context.Body)) -> option.Option(
String,
),
) -> Resolver
Read the version with your own function. Return None when the request
carries no version.
pub fn default(group: Group, name: String) -> Group
The version used when a request carries none. Must be added with add
before the app is built.
pub fn default_version(group: Group) -> option.Option(String)
The version a request without one gets, if the group has a default.
pub fn header(name: String) -> Resolver
Read the version from a request header, such as x-api-version: v2.
pub fn new(resolver: Resolver) -> Group
Create a group that resolves versions with resolver. Fallback to
earlier versions is on; see no_fallback.