glyde/wire

Decoding and encoding helpers for Discord’s JSON.

Discord tells an absent key apart from a null one, writes the same numeric field as 2 or 2.0, and adds enum values without warning. Every model needs the same handling for all three.

Values

pub fn defaulted_field(
  name: String,
  inner: decode.Decoder(a),
  default: a,
  next: fn(a) -> decode.Decoder(b),
) -> decode.Decoder(b)

The shape behind list_field, flag_field, int_field and string_field: a missing key and a null one both land on default. Public because a model needing it for its own type would otherwise write a copy, and the copies drift on which of absent and null they cover.

pub fn dict_field(
  name: String,
  key: decode.Decoder(k),
  value: decode.Decoder(v),
  next: fn(dict.Dict(k, v)) -> decode.Decoder(b),
) -> decode.Decoder(b)

Missing or null gives an empty dict, so there is no Option to unwrap before a dict.get.

pub fn entries(
  entries: List(#(String, field.Field(json.Json))),
) -> List(#(String, json.Json))

object one step short of a document, for rest/body.Form, which writes the attachments array into the entries before encoding them.

pub fn enum_field(
  name: String,
  from_int: fn(Int) -> a,
  next: fn(a) -> decode.Decoder(b),
) -> decode.Decoder(b)

An int field mapped through a from_int, so the raw number never sits in a local waiting to be converted at the bottom of the decoder. Absent or null is 0, which every one of Discord’s numeric enum tables reads as its default.

pub fn flag(value: Bool) -> field.Field(json.Json)

The encoding side of flag_field: true writes the key, false leaves it out. Discord defaults every one of these to false, and reads an explicit null as an instruction rather than a default.

pub fn flag_field(
  name: String,
  default: Bool,
  next: fn(Bool) -> decode.Decoder(a),
) -> decode.Decoder(a)

A boolean that may be missing or null, with a default. Discord omits most false flags, and a few default to true.

pub fn int_field(
  name: String,
  default: Int,
  next: fn(Int) -> decode.Decoder(a),
) -> decode.Decoder(a)

A whole number that may be missing or null, with a default.

pub fn integer() -> decode.Decoder(Int)

A number Discord documents as whole. Accepts 2 and 2.0, rejects 2.5. Anything that can exceed max_exact is a String, see glyde/id.

pub fn known_field(
  name: String,
  from_int: fn(Int) -> option.Option(a),
  next: fn(option.Option(a)) -> decode.Decoder(b),
) -> decode.Decoder(b)

A closed enum Discord may extend. from_int returns None for a value this build has no name for, and the field yields None for absent, null and unmodelled alike, so a new enum value cannot sink the payload.

pub fn known_list_field(
  name: String,
  inner: decode.Decoder(option.Option(a)),
  next: fn(List(a)) -> decode.Decoder(b),
) -> decode.Decoder(b)

A list where each item may be one this build cannot model. Unmodelled items are dropped, so one new discriminator cannot sink the whole list.

pub fn lenient(
  inner: decode.Decoder(a),
  default: a,
) -> decode.Decoder(a)

Anything that does not decode gives default, so one field Discord changed the shape of cannot lose the whole payload.

pub fn list_field(
  name: String,
  inner: decode.Decoder(a),
  next: fn(List(a)) -> decode.Decoder(b),
) -> decode.Decoder(b)

Missing or null gives []. Discord omits embeds rather than sending [], and sends "roles": null on some partial objects.

pub fn null_flag(value: Bool) -> field.Field(json.Json)

The encoding side of present_field: true writes a null, false leaves the key out.

pub fn nullable_field(
  name: String,
  inner: decode.Decoder(a),
  next: fn(option.Option(a)) -> decode.Decoder(b),
) -> decode.Decoder(b)

Required, and null is a value: absent fails, null gives None. For a key whose null says something, VoiceState.channel_id (the user left) and VoiceServerUpdate.endpoint (the voice server went away). opt_field there would forge that signal out of a payload that never sent the key.

pub fn number() -> decode.Decoder(Float)

A number Discord may write with or without a decimal point: retry_after is documented as fractional seconds and then sent as 30 when it is whole. decode.float refuses one form and decode.int the other.

pub fn number_json(value: Float) -> json.Json

The write side of number: a whole Float goes out as 3, not 3.0. Discord reads either, and picking one keeps a request body byte-exact.

pub fn object(
  entries: List(#(String, field.Field(json.Json))),
) -> json.Json

Build an object, leaving absent keys out entirely. An unset key has to vanish rather than become null: Discord reads a null as “clear this”, so writing one for every untouched field wipes half the record on a PATCH.

pub fn opt(value: option.Option(a)) -> field.Field(a)

None becomes absent. Use field.or_null when it should clear the field.

pub fn opt_field(
  name: String,
  inner: decode.Decoder(a),
  next: fn(option.Option(a)) -> decode.Decoder(b),
) -> decode.Decoder(b)

A field that may be missing, or present and null. Both give None.

pub fn opt_list(values: List(a)) -> field.Field(List(a))

Empty becomes absent: Discord reads [] as “clear this” on several endpoints.

pub fn present_field(
  name: String,
  next: fn(Bool) -> decode.Decoder(a),
) -> decode.Decoder(a)

Whether a key is there at all. Discord marks some booleans, RoleTags.premium_subscriber among them, by writing a null.

pub fn put(
  value: field.Field(a),
  with encode: fn(a) -> json.Json,
) -> field.Field(json.Json)

Absent and Null pass through, so encode never runs on them.

pub fn put_list(
  values: field.Field(List(a)),
  with encode: fn(a) -> json.Json,
) -> field.Field(json.Json)
pub fn soft_field(
  name: String,
  inner: decode.Decoder(a),
  next: fn(option.Option(a)) -> decode.Decoder(b),
) -> decode.Decoder(b)

Absent, null, or the wrong shape all give None. opt_field for a field whose shape is worth failing on, this for one that is not.

pub fn strict(
  from_int: fn(Int) -> option.Option(a),
  zero: a,
  expected: String,
) -> decode.Decoder(a)

The inner half of type_field, so a caller who cannot supply a zero can build the field decoder itself.

pub fn string_field(
  name: String,
  default: String,
  next: fn(String) -> decode.Decoder(a),
) -> decode.Decoder(a)

A string that may be missing or null, with a default.

pub fn tri_field(
  name: String,
  inner: decode.Decoder(a),
  next: fn(field.Field(a)) -> decode.Decoder(b),
) -> decode.Decoder(b)

All three states mean something. On Message.referenced_message, absent means Discord never looked and null means the message was deleted.

pub fn type_field(
  name: String,
  from_int: fn(Int) -> option.Option(a),
  zero: a,
  expected: String,
  next: fn(a) -> decode.Decoder(b),
) -> decode.Decoder(b)

A required discriminator: the key must be present, and a value this build has no name for FAILS the decode. For fields Discord always sends, so the unmodelled case is a glyde bug that reaches on_status as Undecodable rather than an Option every caller unwraps. zero is the error’s placeholder, never returned.

pub fn type_or(
  name: String,
  from_int: fn(Int) -> option.Option(a),
  default: a,
  expected: String,
  next: fn(a) -> decode.Decoder(b),
) -> decode.Decoder(b)

type_field with a default for absent or null. An unmodelled value still fails the decode.

Search Document