Data.Parser.KV (data v0.6.0)
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.
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.
Types
@opaque field(a, b)
A structure representing a Data.Parser.t(a,b) lifted to operate on a KV.
@type 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.
@type 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()}
@type 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}
KV parsers accept either a map or a Keyword.t as input.
Functions
@spec 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
@spec one(field_spec(a, b)) :: FE.Result.t(Data.Parser.t(a, b), Error.t())
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