Rupa.Format (rupa v0.1.0)

Copy Markdown View Source

The built-in formats, and nothing else.

A format is a fixed pair Rupa owns: a decoder from the wire string, and the encoder that inverts it. There is no way to add one, which is what keeps a schema pure data and keeps every format expressible as JSON Schema.

Each one means what JSON Schema's format of the same name means, and the official format corpus is the test of that: test/rupa/format_suite_test.exs runs every string case in it and allows no disagreement. In practice:

  • :date_time, :date and :time are RFC 3339's date-time, full-date and full-time: four-digit years, two-digit fields, . for a fraction, T and Z in either case, and an offset on every time. A time decodes to UTC, as a date-time does, and a leap second (23:59:60, in UTC) decodes to the second before it, because Calendar has no second 60.
  • :duration is RFC 3339 appendix A's grammar: whole numbers, weeks only on their own, and a year, month and day (or hour, minute and second) nested so that none is skipped between two that are written. P1Y2D is not a duration; P1Y0M2D is.
  • :email is RFC 5321's Mailbox: a dot-atom or a quoted string, then @, then a hostname or an address literal ([127.0.0.1], [IPv6:::1]). ASCII only.
  • :hostname is RFC 1123's, and an xn-- label in it must be an IDNA2008 A-label: Punycode for a Unicode label in NFC, every code point of which RFC 5892 allows, its contextual rules included. The Bidi rule (RFC 5893) is the one check not applied.
  • :uri needs a scheme and well-formed percent-encoding; :ipv4 and :ipv6 are the dotted quad and RFC 4291's text form, with no zone id.

Four of them change the value: :date_time, :date, :time and :duration decode to DateTime, Date, Time and Duration. The other six validate and hand the string back.

encode/2 is the inverse, and inverse is meant literally: whatever encode/2 produces, decode/2 accepts and returns what you started with. That is why it refuses what the wire cannot say rather than writing something close: a negative duration, a fraction of a second in one, a week beside another unit, a year outside 0000–9999. A DateTime in another zone is written in UTC, which is where decoding it will put it.

Summary

Functions

Decodes a string in the named format.

Encodes a decoded value back to its wire string.

Every format, in the order the documentation lists them.

Functions

decode(format, value)

@spec decode(atom(), String.t()) :: {:ok, term()} | :error

Decodes a string in the named format.

Returns :error rather than a reason: the caller knows the format and the path, which is the whole message.

iex> Rupa.Format.decode(:date, "2026-09-16")
{:ok, ~D[2026-09-16]}

iex> Rupa.Format.decode(:time, "08:30:00+07:00")
{:ok, ~T[01:30:00]}

iex> Rupa.Format.decode(:uuid, "not-a-uuid")
:error

encode(format, value)

@spec encode(atom(), term()) :: {:ok, String.t()} | :error

Encodes a decoded value back to its wire string.

Returns :error for a value the format cannot represent — including the wrong type, since encoding is where a DateTime field finds out it was handed a string.

iex> Rupa.Format.encode(:date, ~D[2026-09-16])
{:ok, "2026-09-16"}

iex> Rupa.Format.encode(:time, ~T[01:30:00])
{:ok, "01:30:00Z"}

iex> Rupa.Format.encode(:date, "2026-09-16")
:error

names()

@spec names() :: [atom()]

Every format, in the order the documentation lists them.

iex> length(Rupa.Format.names())
10