howdy/openapi/schema
Schemas describe a JSON value once and give you everything built from that description: the decoder a handler reads it with, the encoder a response writes it with, and the JSON Schema the OpenAPI document shows. Because all three come from one definition, the document cannot drift from what the server really accepts and sends.
import howdy/openapi/schema.{type Schema}
pub fn user() -> Schema(User) {
{
use id <- schema.field("id", schema.int(), fn(user: User) { user.id })
use name <- schema.field(
"name",
schema.string() |> schema.max_length(50),
fn(user: User) { user.name },
)
schema.success(User(id:, name:))
}
|> schema.named("User")
}
Objects are built like gleam/dynamic/decode decoders, with one extra
argument per field: a function reading the field back out of the value,
which is what lets the same schema encode.
To document an object, the fields after each one are found by calling
the rest of the chain with a placeholder value, such as "" or 0. So
keep the chain to field calls and a final success: code that
branches on a field’s value only has its placeholder branch documented.
Constraints such as max_length are checked while decoding, and shown
in the document as the matching JSON Schema keyword. rule runs any
howdy/validate rule too, but the document cannot show what it checks.
Types
Named schemas seen while rendering, as their JSON and its text, so a second, different schema with the same name can be caught.
pub opaque type Components
The JSON Schema of a schema, without its types. What howdy/openapi
renders into the document.
A named node is shown as a reference to its component, with outer
keywords, those added after naming, beside the reference. kind is the
JSON type, rendered as the type keyword; a node that is a choice
between others, such as nullable or one_of, has none.
pub opaque type Node
A JSON value’s description, decoder and encoder, for values of type a.
pub opaque type Schema(a)
Values
pub fn decoder(schema: Schema(a)) -> decode.Decoder(a)
The decoder for a schema, for reading JSON outside a handler.
pub fn dict(of inner: Schema(a)) -> Schema(dict.Dict(String, a))
A JSON object used as a map from string keys to values of one schema.
pub fn email(schema: Schema(String)) -> Schema(String)
The light email check from howdy/validate, shown as format: email.
pub fn enum(
variants: List(a),
name: fn(a) -> String,
) -> Schema(a)
A string that is the name of one of variants, as name gives it.
Decoding accepts only the listed variants; encoding names any value with
name, so write it as a case over the type and the compiler sees that
every variant has a name. Panics if variants is empty.
schema.enum([Admin, Member], fn(role) {
case role {
Admin -> "admin"
Member -> "member"
}
})
pub fn example(schema: Schema(a), value: a) -> Schema(a)
An example value, encoded with the schema itself.
pub fn field(
name: String,
schema: Schema(a),
get: fn(r) -> a,
next: fn(a) -> Schema(r),
) -> Schema(r)
A required object field. get reads the field from the finished value,
for encoding. See the module docs for how chains are built.
pub fn float_maximum(
schema: Schema(Float),
value: Float,
) -> Schema(Float)
maximum for a float, with the message "must be at most 9.5".
pub fn float_minimum(
schema: Schema(Float),
value: Float,
) -> Schema(Float)
minimum for a float, with the message "must be at least 0.5".
pub fn format(schema: Schema(a), name: String) -> Schema(a)
A format hint such as "date-time" or "uuid". Only documents the
value; pair it with try_map or rule to check it.
pub fn list(of inner: Schema(a)) -> Schema(List(a))
A JSON array. As a query parameter, every occurrence of the key, as in
?tag=a&tag=b.
pub fn map(
schema: Schema(a),
to to: fn(a) -> b,
from from: fn(b) -> a,
) -> Schema(b)
Change the type a schema decodes to and encodes from. Both directions
must always succeed; see try_map for conversions that can fail.
pub fn user_id() -> Schema(UserId) {
schema.int() |> schema.map(to: UserId, from: fn(id) { id.value })
}
pub fn max_items(
schema: Schema(List(a)),
count: Int,
) -> Schema(List(a))
At most count items, shown as maxItems, with the message
"must have at most 10 items".
pub fn message(error: decode.DecodeError) -> String
The English message for a decode error from a schema.
pub fn min_items(
schema: Schema(List(a)),
count: Int,
) -> Schema(List(a))
At least count items, shown as minItems, with the message
"must have at least 1 item".
pub fn named(schema: Schema(a), name: String) -> Schema(a)
Show the schema once, under components/schemas/<name>, and refer to it
by name everywhere it is used. Two different schemas with the same name
make building the document panic.
pub fn not_empty(schema: Schema(String)) -> Schema(String)
At least one character. Shown as minLength: 1, with the message
"must not be empty".
pub fn nullable(inner: Schema(a)) -> Schema(option.Option(a))
A value that may be null. For a field that may be left out, use
optional_field instead.
pub fn one_of(
variants: List(Variant(a)),
encode encode: fn(a) -> json.Json,
) -> Schema(a)
One of several shapes, each described by its own schema and turned into
a variant of one type with variant. Decoding tries the variants in
order and takes the first that matches, so put the most specific first.
Encoding is encode, which picks the variant’s schema for a value:
write it as a case over the type, with schema.to_json, and the
compiler sees that every variant is covered. Shown as anyOf over the
variants’ schemas. Panics if variants is empty.
schema.one_of(
[
schema.variant(circle(), fn(c) { Circle(c) }),
schema.variant(square(), fn(s) { Square(s) }),
],
encode: fn(shape) {
case shape {
Circle(c) -> schema.to_json(c, circle())
Square(s) -> schema.to_json(s, square())
}
},
)
pub fn optional_field(
name: String,
schema: Schema(a),
get: fn(r) -> option.Option(a),
next: fn(option.Option(a)) -> Schema(r),
) -> Schema(r)
An object field that may be missing or null, both of which decode to
None. None is left out when encoding.
pub fn problem(error: decode.DecodeError) -> problem.Problem
The problem a decode error from a schema describes. A rule’s problem is
read back as it was; the decoders’ type names become the matching
problem; any other text, such as a try_map message, is a Custom
problem with the code invalid.
pub fn rule(
schema: Schema(a),
rule: fn(a) -> Result(a, problem.Problem),
) -> Schema(a)
Run a howdy/validate rule while decoding. Rules may change the value,
as validate.trim() does, and a failing rule reports its problem for
the field. The document does not show what a rule checks; add a
description if clients need to know.
pub fn success(value: r) -> Schema(r)
Finish an object with the value built from its fields.
pub fn to_json(value: a, schema: Schema(a)) -> json.Json
Encode a value, for a response body. Pass it to service.respond as
schema.to_json(_, user()).