# `Rupa.Format`
[🔗](https://github.com/zero-one-group/rupa/blob/v0.1.0/lib/rupa/format.ex#L1)

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.

# `decode`

```elixir
@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`

```elixir
@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`

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

Every format, in the order the documentation lists them.

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

---

*Consult [api-reference.md](api-reference.md) for complete listing*
