Chapter 7: Records, guards and deriving
Deriving
- @derive
- derivers
- JSON
@derive(decoder, encoder) on a record writes
functions that follow its fields one by one: user_decoder()
and user_encoder(), which Bea code calls as
User.decoder() and User.encoder(). They are
exactly the code you would write by hand, as the Gleam tab
shows.
The compiler knows no format. What a derived function does comes from
the deriver, the imported module that @derive names:
an ordinary Bea (or Gleam) module that exports record,
field and done, and a function per type a field
can have (string, int, list,
option, …). The bea_json/decoder.bea tab is a
complete one, and its decoders are the standard library's
decode.Decoders.
A field whose type is another record uses that type's function
(address_decoder()). You get only what you ask for: a type
that should never be serialised simply doesn't derive an encoder.
//// `@derive` writes functions that follow a record's fields, like JSON
//// decoders and encoders. What they do comes from a deriver: an ordinary
//// module, here `bea_json/decoder` and `bea_json/encoder`.
import bea_json/decoder
import bea_json/encoder
@derive(decoder, encoder)
pub type Address { street: String, city: String }
@derive(decoder, encoder)
pub type User {
name: String,
age: Int,
tags: List(String),
address: Option(Address),
}
pub def main() {
json = `{"name": "Ada", "age": 36, "tags": ["admin"],
"address": {"street": "1 Hive Lane", "city": "Kanpur"}}`
Ok(user) = decoder.from_json(json, User.decoder()) else panic
echo user
// A field of the wrong type: the error says which one.
Error(problem) = decoder.from_json(
`{"name": "Bea", "age": "two"}`,
User.decoder(),
) else panic
echo problem
echo encoder.to_string(User(user | address: None), User.encoder())
}//// `@derive` writes functions that follow a record's fields, like JSON
//// decoders and encoders. What they do comes from a deriver: an ordinary
//// module, here `bea_json/decoder` and `bea_json/encoder`.
import bea_json/decoder
import bea_json/encoder
import gleam/option.{type Option, None}
pub type Address {
Address(street: String, city: String)
}
/// Derived by `@derive(decoder)` on `Address`.
pub fn address_decoder() {
use <- decoder.record("Address")
use street <- decoder.field(
"street",
decoder.string(),
fn(value: Address) { value.street },
)
use city <- decoder.field(
"city",
decoder.string(),
fn(value: Address) { value.city },
)
decoder.done(Address(street: street, city: city))
}
/// Derived by `@derive(encoder)` on `Address`.
pub fn address_encoder() {
use <- encoder.record("Address")
use street <- encoder.field(
"street",
encoder.string(),
fn(value: Address) { value.street },
)
use city <- encoder.field(
"city",
encoder.string(),
fn(value: Address) { value.city },
)
encoder.done(Address(street: street, city: city))
}
pub type User {
User(name: String, age: Int, tags: List(String), address: Option(Address))
}
/// Derived by `@derive(decoder)` on `User`.
pub fn user_decoder() {
use <- decoder.record("User")
use name <- decoder.field(
"name",
decoder.string(),
fn(value: User) { value.name },
)
use age <- decoder.field("age", decoder.int(), fn(value: User) { value.age })
use tags <- decoder.field(
"tags",
decoder.list(decoder.string()),
fn(value: User) { value.tags },
)
use address <- decoder.field(
"address",
decoder.option(address_decoder()),
fn(value: User) { value.address },
)
decoder.done(User(name: name, age: age, tags: tags, address: address))
}
/// Derived by `@derive(encoder)` on `User`.
pub fn user_encoder() {
use <- encoder.record("User")
use name <- encoder.field(
"name",
encoder.string(),
fn(value: User) { value.name },
)
use age <- encoder.field("age", encoder.int(), fn(value: User) { value.age })
use tags <- encoder.field(
"tags",
encoder.list(encoder.string()),
fn(value: User) { value.tags },
)
use address <- encoder.field(
"address",
encoder.option(address_encoder()),
fn(value: User) { value.address },
)
encoder.done(User(name: name, age: age, tags: tags, address: address))
}
pub fn main() {
let json = "{\"name\": \"Ada\", \"age\": 36, \"tags\": [\"admin\"],
\"address\": {\"street\": \"1 Hive Lane\", \"city\": \"Kanpur\"}}"
case decoder.from_json(json, user_decoder()) {
Ok(user) -> {
echo user
case decoder.from_json(
"{\"name\": \"Bea\", \"age\": \"two\"}",
user_decoder(),
) {
Error(problem) -> {
echo problem
echo encoder.to_string(User(..user, address: None), user_encoder())
}
_ -> panic
}
}
_ -> panic
}
}//// A deriver of JSON decoders, written as ordinary Bea code:
////
//// ```bea
//// import bea_json/decoder
////
//// @derive(decoder)
//// pub type Credentials {
//// Credentials(username: String, password: String)
//// }
////
//// credentials = decoder.from_json(body, Credentials.decoder())?
//// ```
////
//// A derived decoder is a plain `decode.Decoder` from the standard
//// library, so it works with `json.parse`, `decode.run` on any `Dynamic`,
//// and every Gleam library that takes decoders.
////
//// The conventions are this module's, not the compiler's: a record is an
//// object with a key per field label; a field whose decoder accepts `null`
//// (an `Option`) may also be left out; an integer is accepted where a float
//// is expected; errors are the standard library's, with the path to the
//// failing value.
import gleam/dynamic
import gleam/dynamic/decode.{type Decoder}
import gleam/json
// --- What `@derive(decoder)` calls --------------------------------------------------
/// A record is decoded from its fields.
pub def record(_constructor: String, fields: fn() -> Decoder(r)) -> Decoder(r) {
fields()
}
/// One field, under its label. Missing is the same as `null`: fine when the
/// field's decoder accepts `null`, a "missing field" error otherwise.
pub def field(
label: String,
of: Decoder(a),
_get: fn(r) -> a,
next: fn(a) -> Decoder(r),
) -> Decoder(r) {
case decode.run(dynamic.nil(), of) {
Ok(when_missing) -> decode.optional_field(label, when_missing, of, next)
Error(_) -> decode.field(label, of, next)
}
}
pub def done(value: r) -> Decoder(r) {
decode.success(value)
}
// --- Decoders for the types fields can have -------------------------------------------
pub def string() -> Decoder(String) {
decode.string
}
pub def int() -> Decoder(Int) {
decode.int
}
pub def bool() -> Decoder(Bool) {
decode.bool
}
/// JSON doesn't tell `1` from `1.0`: both are fine for a `Float`.
pub def float() -> Decoder(Float) {
decode.one_of(decode.float, [decode.int |> decode.map(int.to_float)])
}
pub def list(of: Decoder(a)) -> Decoder(List(a)) {
decode.list(of)
}
/// `null` is `None`.
pub def option(of: Decoder(a)) -> Decoder(Option(a)) {
decode.optional(of)
}
pub def dict(keys: Decoder(k), values: Decoder(v)) -> Decoder(Dict(k, v)) {
decode.dict(keys, values)
}
// --- Running ------------------------------------------------------------------------
pub def from_json(
text: String,
decoder: Decoder(a),
) -> Result(a, json.DecodeError) {
json.parse(text, decoder)
}src/deriving.bea:23
User("Ada", 36, ["admin"], Some(Address("1 Hive Lane", "Kanpur")))
src/deriving.bea:29
UnableToDecode([DecodeError("Int", "String", ["age"]), DecodeError("Field", "Nothing", ["tags"])])
src/deriving.bea:30
"{\"name\":\"Ada\",\"age\":36,\"tags\":[\"admin\"],\"address\":null}"This lesson's code is a module of a small Bea project, compiled and run by bea for this page: content/features/src/deriving.bea