Texture.HttpStructuredField (texture v0.3.2)

View Source

HTTP Structured Field parser implementation following RFC 8941.

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

  • items (single bare item with optional parameters)
  • lists (comma separated sequence of items or inner lists)
  • dictionaries (comma separated key / item pairs – bare keys imply 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.

Parsing a single item

An item with no parameters:

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

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}}}

Parsing a list

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}}
]}

Parsing a dictionary

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}}
}}

Error handling

On invalid input an {:error, {reason, remainder}} tuple is returned:

iex> Texture.HttpStructuredField.parse_item("not@@valid")
{:error, {:invalid_value, "not@@valid"}}

The low-level tokenization lives in the private Parser module; only the post-processing (unwrap / maps) occurs here.

Summary

Types

attribute()

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

attrs()

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

item()

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

option()

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

tag()

@type tag() ::
  :integer
  | :decimal
  | :string
  | :token
  | :byte_sequence
  | :boolean
  | :inner_list

unwrapped_attribute()

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

unwrapped_item()

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

value()

@type value() :: term()

wrapped_attribute()

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

wrapped_item()

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

Functions

parse_dict(input, opts \\ [])

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

parse_item(input, opts \\ [])

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

parse_list(input, opts \\ [])

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

post_process_dict(dict, opts)

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

post_process_item(elem, opts)

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

post_process_list(list, opts)

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