data v0.5.2 Data.Parser.KV

Creates parsers that accept KeyValue-style Enums as input.

In particular, KV parsers work with:

  • maps (e.g. %{"hello" => "world"})
  • Keyword.ts (e.g. [hello: "world"])
  • Lists of pairs (e.g. [{"hello", "world"}])

KV parsers are higher-order parsers, and operate in roughly the same way as Data.Parser.list/1 or Data.Parser.set/1, but their definition is slightly more involved. A KV parser is created with a list of field_specs, where each field_spec defines what fields of the input to look at, and what parsers to run on them.

Here are some examples of field_specs and their parsing behavior:

{:username, Data.Parser.BuiltIn.string()}

This spec says that the input must contain a :username field, and the value of that field must satisfy Data.Parser.BuiltIn.string/0. The output map will contain the key-value pair username: "some string".

If the field cannot be parsed successfully, the entire KV parser will return {:error, domain_error_with_details_on_parse_failure}.

If the field is not present, the entire KV parser will return {:error, domain_error_with_details_about_field_not_found}

{:birthday, Data.Parser.BuiltIn.date(), optional: true}

This spec says that the input may contain a :birthday field. If the field does exist, it must satisfy Data.Parser.BuiltIn.date/0.

If the field exists and parses successfully, the output map will contain the key-value pair birthday: {:just, ~D[1983-07-18]}.

If the field does not exist, the output map will contain the key-value pair birthday: :nothing.

If the field cannot be parsed successfully, the entire KV parser will return {:error, domain_error_with_parse_failure_details}.

{:country, MyApp.country_parser(), default: "Canada"}

This spec says that the input may contain a :country field, and if so, the value of that field must parse successfully with MyApp.country_parser/0.

If the field exists and parses successfully, the output map will contain a key-value pair such as: country: "Indonesia".

If the field cannot be parsed successfully, the entire Constructor will return {:error, domain_error_with_details_on_parse_failure}.

If the field does not exist, the default value will be used. In this case, the output will contain the key-value pair: country: "Canada"

{:country, MyApp.country_parser(), nullable: true}

This spec says that the input must contain a :country field, and if so, the value of that field must parse successfully with MyApp.country_parser/0 OR be equal to nil.

If the field exists and parses successfully, the output map will contain a key-value pair such as: country: "Indonesia".

If the field cannot be parsed successfully, the entire Constructor will return {:error, domain_error_with_details_on_parse_failure}. However, if the value of the field is nil, this is treated as a successful parse.

If the field does not exist, the parser will fail.

Note that a field spec specified as nullable: true cannot also contain either the optional: true or default: x options. This is an illegal spec and will not be constructed, resulting in an {:error, domain_error} tuple, with :invalid_field_spec as the error reason.

{:country, MyApp.country_parser(), from: :countryName}

This spec says that the parser will use the data from :countryName in the input map. If the value under this key satisfies the MyApp.country_parser(), then the resulting value will be placed under the :country field.

Note that the from keyname MUST always be specified as an atom, but it will be applied automatically to string keys. If the input contains both a string key and an atom key, the atom key will take priority.

{:point, MyApp.point_parser(), recurse: true}

Sometimes you want to run several different parsers on the same input map. For example, let's say your input looks like this:

%{x: 12,
  y: -10,
  value: 34,
  name: "exploding_barrel"}

But the data structure you want after parsing looks like this:

%{point: %{x: 12, y: -10},
  value: 34,
  name: "exploding_barrel"}

And you have MyApp.point_parser() which accepts a map with :x and :y integer keys and constructs %{x: integer(), y: integer()}.

You can define a field_spec with recurse: true and have that particular parser get run on its parent input map, not on the value of a field.

Link to this section Summary

Types

A structure representing a Data.Parser.t(a,b) lifted to operate on a KV.

KV parsers accept atom()s as key names, but will work on inputs where the keys are String.t()s as well.

Options to relax requirements on the fields.

A 2-tuple or 3-tuple describing the field to parse and parsing semantics.

KV parsers accept either a map or a Keyword.t as input.

Functions

Given a list of field_specs, verify that all specs are well-formed and return {:ok, parser}, where parser will accept a map or Keyword input and apply the appropriate parsers to their corresponding fields.

Given one field_spec, verify that it is well-formed and return {:ok, parser}, where parser will accept a map or Keyword input and attempt to parse that single field out of the map.

Link to this section Types

Link to this opaque

field(a, b)

(opaque)
field(a, b)

A structure representing a Data.Parser.t(a,b) lifted to operate on a KV.

Link to this type

field_name()

field_name() :: atom()

KV parsers accept atom()s as key names, but will work on inputs where the keys are String.t()s as well.

Link to this type

field_opts(a)

field_opts(a) :: [optional: bool(), default: a, from: field_name()]

Options to relax requirements on the fields.

This is a list that consists of zero or one of the below options: {:optional, bool()} {:default, any} {:from, field_name()}

Link to this type

field_spec(a, b)

field_spec(a, b) ::
  {field_name(), Data.Parser.t(a, b)}
  | {field_name(), Data.Parser.t(a, b), field_opts(b)}

A 2-tuple or 3-tuple describing the field to parse and parsing semantics.

{field_name, parser} {field_name, parser, opts}

Link to this type

input()

input() :: map() | Keyword.t()

KV parsers accept either a map or a Keyword.t as input.

Link to this section Functions

Link to this function

new(field_specs)

new([field_spec(a, b)]) :: FE.Result.t(Data.Parser.t(a, b), Error.t())

Given a list of field_specs, verify that all specs are well-formed and return {:ok, parser}, where parser will accept a map or Keyword input and apply the appropriate parsers to their corresponding fields.

If the field_specs are not well-formed, return {:error, Error.t} with details about the invalid field_specs.

Examples

iex> {:ok, p} = Data.Parser.KV.new([{:username, Data.Parser.BuiltIn.string()}])
...> p.(username: "johndoe")
{:ok, %{username: "johndoe"}}

iex> {:ok, p} = Data.Parser.KV.new([{:username, Data.Parser.BuiltIn.string()}])
...> p.(%{"username" => "johndoe"})
{:ok, %{username: "johndoe"}}

iex> {:error, e} = Data.Parser.KV.new(["not a spec"])
...> e.reason
:invalid_field_spec
...> e.details
%{spec: "not a spec"}

iex> {:ok, p} = Data.Parser.KV.new([{:a, Data.Parser.BuiltIn.integer(), optional: true}])
...> p.(a: 1)
{:ok, %{a: {:just, 1}}}

iex> {:ok, p} = Data.Parser.KV.new([{:a, Data.Parser.BuiltIn.integer(), optional: true}])
...> p.([])
{:ok, %{a: :nothing}}

iex> {:ok, p} = Data.Parser.KV.new([{:a, Data.Parser.BuiltIn.integer(), nullable: true}])
...> {:error, e} = p.([])
...> Error.reason(e)
:field_not_found_in_input

iex> {:ok, p} = Data.Parser.KV.new([{:a, Data.Parser.BuiltIn.integer(), nullable: true}])
...> p.([a: nil])
{:ok, %{a: nil}}

iex> {:ok, p} = Data.Parser.KV.new([{:a, Data.Parser.BuiltIn.integer(), nullable: true}])
...> p.([a: 1])
{:ok, %{a: 1}}

iex> {:error, e} = Data.Parser.KV.new([{:a, Data.Parser.BuiltIn.integer(), nullable: true, default: nil}])
...> Error.reason(e)
:invalid_field_spec

iex> {:error, e} = Data.Parser.KV.new([{:a, Data.Parser.BuiltIn.integer(), nullable: true, optional: true}])
...> Error.reason(e)
:invalid_field_spec

iex> {:ok, p} = Data.Parser.KV.new([{:b, Data.Parser.BuiltIn.integer(), default: 0}])
...> p.([])
{:ok, %{b: 0}}

iex> {:ok, p} = Data.Parser.KV.new([{:b, Data.Parser.BuiltIn.integer(), default: 0}])
...> p.(b: 10)
{:ok, %{b: 10}}

iex> {:ok, p} = Data.Parser.KV.new([{:b, Data.Parser.BuiltIn.integer(), default: 0}])
...> {:error, e} = p.(b: "i am of the wrong type")
...> Error.reason(e)
:failed_to_parse_field
...> {:just, inner_error} = Error.caused_by(e)
...> Error.reason(inner_error)
:not_an_integer

iex> {:ok, p} = Data.Parser.KV.new([{:a, Data.Parser.BuiltIn.integer(), from: :theAValue}])
...> p.(%{theAValue: 123})
{:ok, %{a: 123}}

iex> {:ok, p} = Data.Parser.KV.new([{:a, Data.Parser.BuiltIn.integer(), from: :aStringKey}])
...> p.(%{"aStringKey" => 1234})
{:ok, %{a: 1234}}

iex> {:ok, point} = Data.Parser.KV.new([{:x, Data.Parser.BuiltIn.integer()}, {:y, Data.Parser.BuiltIn.integer()}])
...> {:ok, item} = Data.Parser.KV.new([{:point, point, recurse: true}, {:value, Data.Parser.BuiltIn.integer()}])
...> item.(%{x: 1, y: -1, value: 34})
{:ok, %{value: 34, point: %{x: 1, y: -1}}}

iex> {:ok, point} = Data.Parser.KV.new([{:x, Data.Parser.BuiltIn.integer()}, {:y, Data.Parser.BuiltIn.integer()}])
...> {:ok, item} = Data.Parser.KV.new([{:point, point, recurse: true}, {:value, Data.Parser.BuiltIn.integer()}])
...> {:error, e} = item.(%{x: "wrong", y: -1, value: 34})
...> {:just, e2} = e.caused_by
...> e2.reason
:failed_to_parse_field


iex> {:ok, point} = Data.Parser.KV.new([{:x, Data.Parser.BuiltIn.integer()}, {:y, Data.Parser.BuiltIn.integer()}])
...> {:ok, item} = Data.Parser.KV.new([{:point, point, recurse: true}, {:value, Data.Parser.BuiltIn.integer()}])
...> {:error, e} = item.(%{y: -1, value: 34})
...> {:just, e2} = e.caused_by
...> e2.reason
:field_not_found_in_input

Given one field_spec, verify that it is well-formed and return {:ok, parser}, where parser will accept a map or Keyword input and attempt to parse that single field out of the map.

Any field_spec with default semantics will be 'lifted' to also accept the default value if present under the key.

Any field_spec with optional semantics will be stripped of these, so that it can 'select' only fields which exist.

If the field_spec is not well-formed, return {:error, Error.t} with details about the invalid field_spec.

Examples

iex> {:ok, p} = Data.Parser.KV.one({:username, Data.Parser.BuiltIn.string()})
...> p.(username: "johndoe")
{:ok, %{username: "johndoe"}}

iex> {:ok, p} = Data.Parser.KV.one({:username, Data.Parser.BuiltIn.string()})
...> p.(%{"username" => "johndoe"})
{:ok, %{username: "johndoe"}}

iex> {:ok, p} = Data.Parser.KV.one({:username, Data.Parser.BuiltIn.string(), default: nil})
...> p.(username: nil)
{:ok, %{username: nil}}

iex> {:ok, p} = Data.Parser.KV.one({:username, Data.Parser.BuiltIn.string(), default: nil})
...> {:error, e} = p.(%{username: [1,2,3]})
...> Error.reason(e)
:failed_to_parse_field

iex> {:ok, p} = Data.Parser.KV.one({:username, Data.Parser.BuiltIn.string(), default: nil})
...> {:error, e} = p.(%{"a" => "b"})
...> Error.reason(e)
:field_not_found_in_input

iex> {:ok, p} = Data.Parser.KV.one({:username, Data.Parser.BuiltIn.string(), optiona: true})
...> {:error, e} = p.(%{"a" => "b"})
...> Error.reason(e)
:field_not_found_in_input