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 atrueboolean)
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 attributesmaps: 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"}
Summary
Functions
Parses a structured field dictionary, a comma separated sequence of key/item
pairs. A bare key implies a true boolean value.
Parses a structured field item, a single bare value with optional parameters.
Parses a structured field list, a comma separated sequence of items or inner lists.
Applies the :unwrap and :maps transformations to a dictionary already
parsed with the default options.
Applies the :unwrap and :maps transformations to an item already parsed
with the default options.
Applies the :unwrap and :maps transformations to each item of a list
already parsed with the default options.
Types
@type attribute() :: wrapped_attribute() | unwrapped_attribute()
@type attrs() :: Enumerable.t(attribute())
@type item() :: wrapped_item() | unwrapped_item()
@type tag() ::
:integer
| :decimal
| :string
| :token
| :byte_sequence
| :boolean
| :date
| :display_string
| :inner_list
@type value() :: term()
Functions
@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}}
}}
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- whentrue, removes the type tag from values and parameters.:maps- whentrue, 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}}}
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}}
]}
@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.
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}}
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.