# `Texture.HttpStructuredField`
[🔗](https://github.com/lud/texture/blob/main/lib/texture/http_structured_field.ex#L1)

HTTP Structured Field parser implementation following RFC 9651 (which
obsoletes RFC 8941).

All bare item types are supported: Integers, Decimals, Strings, Tokens,
Byte Sequences, Booleans, as well as Dates and Display Strings introduced
by RFC 9651.

This module exposes high-level helpers to parse the three Structured Field
top-level types defined by the RFC:

  * items with `parse_item/2` (a single bare item with optional parameters)
  * lists with `parse_list/2` (a comma separated sequence of items or inner lists)
  * dictionaries with `parse_dict/2` (a comma separated sequence of key/item
    pairs, where a bare key implies a `true` boolean)

Returned data is, by default, tagged with the parsed value type. You can opt
into two orthogonal transformations using options:

  * `unwrap: true` - remove the type tag wrapper from items and attributes
  * `maps: true` - turn attribute collections and dictionaries into maps

### Shapes

By default (no options):

  * item: `{type, value, attributes}`
  * attribute: `{key, {type, value}}`
  * inner list: `{:inner_list, [item, ...], attributes}`
  * dictionary: list of `{key, item}` tuples

With `unwrap: true`:

  * item: `{value, attributes}` (type tag removed)
  * attribute: `{key, value}`
  * inner list: `{[unwrapped_item, ...], attributes}`

With `maps: true`:

  * attribute collections (item / inner list parameters) become a `%{key => attr}` map
  * dictionary becomes a `%{key => item}` map

Both options compose: `unwrap: true, maps: true` yields unwrapped values and
maps for every attribute / dictionary collection.

### Error handling

On invalid input an `{:error, error}` tuple is returned, where the error is
a `Texture.HttpStructuredField.Parser.Error` exception. The exception
carries the full input in `:value` and a `{tag, remainder}` tuple in
`:reason` pointing at the failing part of the input:

    iex> {:error, err} = Texture.HttpStructuredField.parse_item("not@@valid")
    iex> err
    %Texture.HttpStructuredField.Parser.Error{reason: {:invalid_value, "not@@valid"}, value: "not@@valid"}

# `attribute`

```elixir
@type attribute() :: wrapped_attribute() | unwrapped_attribute()
```

# `attrs`

```elixir
@type attrs() :: Enumerable.t(attribute())
```

# `item`

```elixir
@type item() :: wrapped_item() | unwrapped_item()
```

# `option`

```elixir
@type option() :: {:maps, boolean()} | {:unwrap, boolean()}
```

# `tag`

```elixir
@type tag() ::
  :integer
  | :decimal
  | :string
  | :token
  | :byte_sequence
  | :boolean
  | :date
  | :display_string
  | :inner_list
```

# `unwrapped_attribute`

```elixir
@type unwrapped_attribute() :: {binary(), value()}
```

# `unwrapped_item`

```elixir
@type unwrapped_item() :: {value(), attrs()}
```

# `value`

```elixir
@type value() :: term()
```

# `wrapped_attribute`

```elixir
@type wrapped_attribute() :: {binary(), {tag(), value()}}
```

# `wrapped_item`

```elixir
@type wrapped_item() :: {tag(), value(), attrs()}
```

# `parse_dict`

```elixir
@spec parse_dict(binary(), [option()]) ::
  {:ok, Enumerable.t({binary(), item()})} | {:error, term()}
```

Parses a structured field dictionary, a comma separated sequence of key/item
pairs. A bare key implies a `true` boolean value.

Returns `{:ok, pairs}` where pairs is a list of `{key, item}` tuples (or a
map with the `:maps` option), or `{:error, reason}` on invalid input. Accepts
the same options as `parse_item/2`. See the module documentation for the
returned shapes.

### Examples

Example with explicit and implicit boolean members plus inner list:

    iex> Texture.HttpStructuredField.parse_dict("foo=123, bar, baz=\"hi\";a=1;b=2, qux=(1 2);p")
    {:ok,
    [
      {"foo", {:integer, 123, []}},
      {"bar", {:boolean, true, []}},
      {"baz", {:string, "hi", [{"a", {:integer, 1}}, {"b", {:integer, 2}}]}},
      {"qux",
        {:inner_list, [{:integer, 1, []}, {:integer, 2, []}],
        [{"p", {:boolean, true}}]}}
    ]}

Unwrapped:

    iex> Texture.HttpStructuredField.parse_dict("foo=123, bar, baz=\"hi\";a=1;b=2, qux=(1 2);p", unwrap: true)
    {:ok,
    [
      {"foo", {123, []}},
      {"bar", {true, []}},
      {"baz", {"hi", [{"a", 1}, {"b", 2}]}},
      {"qux", {[{1, []}, {2, []}], [{"p", true}]}}
    ]}

As a map (still wrapped):

    iex> Texture.HttpStructuredField.parse_dict("foo=123, bar, baz=\"hi\";a=1;b=2, qux=(1 2);p", maps: true)
    {:ok,
    %{
      "bar" => {:boolean, true, %{}},
      "baz" => {:string, "hi", %{"a" => {:integer, 1}, "b" => {:integer, 2}}},
      "foo" => {:integer, 123, %{}},
      "qux" => {:inner_list, [{:integer, 1, %{}}, {:integer, 2, %{}}],
        %{"p" => {:boolean, true}}}
    }}

Maps + Unwrapped:

    iex> Texture.HttpStructuredField.parse_dict("foo=123, bar, baz=\"hi\";a=1;b=2, qux=(1 2);p", unwrap: true, maps: true)
    {:ok,
    %{
      "bar" => {true, %{}},
      "baz" => {"hi", %{"a" => 1, "b" => 2}},
      "foo" => {123, %{}},
      "qux" => {[{1, %{}}, {2, %{}}], %{"p" => true}}
    }}

# `parse_item`

```elixir
@spec parse_item(binary(), [option()]) :: {:ok, item()} | {:error, term()}
```

Parses a structured field item, a single bare value with optional parameters.

Returns `{:ok, item}` where the item is a `{type, value, params}` tuple, or
`{:error, reason}` on invalid input. See the module documentation for the
returned shapes.

### Options

  * `:unwrap` - when `true`, removes the type tag from values and parameters.
  * `:maps` - when `true`, collects parameters into a map.

### Examples

An item with no parameters:

    iex> Texture.HttpStructuredField.parse_item("123")
    {:ok, {:integer, 123, []}}

Dates are returned as Unix timestamps in seconds (use `DateTime.from_unix!/1`
to convert them):

    iex> Texture.HttpStructuredField.parse_item("@1659578233")
    {:ok, {:date, 1659578233, []}}

Display strings are decoded to UTF-8 binaries:

    iex> Texture.HttpStructuredField.parse_item(~S{%"caf%c3%a9"})
    {:ok, {:display_string, "café", []}}

Item with boolean (implicit) and integer parameters:

    iex> Texture.HttpStructuredField.parse_item("123;a;b=5")
    {:ok, {:integer, 123, [{"a", {:boolean, true}}, {"b", {:integer, 5}}]}}

Unwrapped (type tags removed):

    iex> Texture.HttpStructuredField.parse_item("123;a;b=5", unwrap: true)
    {:ok, {123, [{"a", true}, {"b", 5}]}}

Attributes as a map (still wrapped):

    iex> Texture.HttpStructuredField.parse_item("123;a;b=5", maps: true)
    {:ok, {:integer, 123, %{"a" => {:boolean, true}, "b" => {:integer, 5}}}}

Both together (unwrapped values and attribute map):

    iex> Texture.HttpStructuredField.parse_item("123;a;b=5", unwrap: true, maps: true)
    {:ok, {123, %{"a" => true, "b" => 5}}}

# `parse_list`

```elixir
@spec parse_list(binary(), [option()]) :: {:ok, [item()]} | {:error, term()}
```

Parses a structured field list, a comma separated sequence of items or inner
lists.

Returns `{:ok, items}` or `{:error, reason}` on invalid input. Accepts the
same options as `parse_item/2`. See the module documentation for the returned
shapes.

### Examples

A list can contain bare items and inner lists:

    iex> Texture.HttpStructuredField.parse_list("123, \"hi\";a=1, (1 2 3);p")
    {:ok,
    [
      {:integer, 123, []},
      {:string, "hi", [{"a", {:integer, 1}}]},
      {:inner_list,
        [{:integer, 1, []}, {:integer, 2, []}, {:integer, 3, []}],
        [{"p", {:boolean, true}}]}
    ]}

Unwrapping removes all type tags recursively:

    iex> Texture.HttpStructuredField.parse_list("123, \"hi\";a=1, (1 2 3);p", unwrap: true)
    {:ok,
    [
      {123, []},
      {"hi", [{"a", 1}]},
      {[{1, []}, {2, []}, {3, []}], [{"p", true}]}
    ]}

Using maps for attributes (note inner list parameter map):

    iex> Texture.HttpStructuredField.parse_list("123, \"hi\";a=1, (1 2 3);p", unwrap: true, maps: true)
    {:ok,
    [
      {123, %{}},
      {"hi", %{"a" => 1}},
      {[{1, %{}}, {2, %{}}, {3, %{}}], %{"p" => true}}
    ]}

# `post_process_dict`

```elixir
@spec post_process_dict(Enumerable.t({binary(), item()}), [option()]) ::
  Enumerable.t({binary(), item()})
```

Applies the `:unwrap` and `:maps` transformations to a dictionary already
parsed with the default options.

Accepts the same options as `parse_item/2`.

# `post_process_item`

```elixir
@spec post_process_item(item(), [option()]) :: item()
```

Applies the `:unwrap` and `:maps` transformations to an item already parsed
with the default options.

Accepts the same options as `parse_item/2`.

### Examples

    iex> {:ok, item} = Texture.HttpStructuredField.parse_item("123;a")
    iex> Texture.HttpStructuredField.post_process_item(item, unwrap: true, maps: true)
    {123, %{"a" => true}}

# `post_process_list`

```elixir
@spec post_process_list([item()], [option()]) :: [item()]
```

Applies the `:unwrap` and `:maps` transformations to each item of a list
already parsed with the default options.

Accepts the same options as `parse_item/2`.

---

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