Mapail v0.2.1 Mapail
Helper library to convert a map
with string keys into structs.
Rationale
It can be laborious to reconstruct the key-value pairs of a map manually into a struct on a case by case basis.
This particularly can be the case when maps originate from json
and the formatting of the json is not
immediately amenable to conversion to a struct. Often, the user may have to perform string transformations
on the json
in order to convert it to an atom. Doing a simple Enum.map(String.to_atom/1)
runs the risk of
exceeding the maximum number of atoms in the erlang VM. This library tries to assist in the coercion of json
to a map by providing a map_to_struct/2
function.
Features
Transformations: Optionally, string manipulations can be applied to the string of the map so as to attempt to force the key to match the key of the struct. Currently, the only transformation option is conversion to snake_case.
Residual maps: Optionally, the part of the map leftover after the struct has been built can be retrieved or merged back into the returned struct.
Limitations
- Currently, only converts one level deep, that is, it does not convert nested structs. This is a potential TODO task.
Example
defmodule User do
defstruct [:first_name, :username, :password]
end
As seen below the map does not exactly match the struct keys due to
CamelCasing in this instance. Mapáil
will attempt to match with the
struct keys by converting the unmatched keys to snake_case.
user = %{
"FirstName" => "John",
"Username" => "john",
"password" => "pass",
"age": 30
}
Mapail.map_to_struct(user, User)
{:ok, %User{
first_name: "John",
username: "john",
password: "pass"
}
}
If the same conversion is attempted with transformations
turned off and
rest
turned on, the keys would not match and the leftover map can optionally be
returned separately.
Mapail.map_to_struct(user, User, transformations: [], rest: :true)
{:ok, %User{
first_name: :nil,
username: :nil,
password: "pass"
},
%{
"FirstName" => "John",
"Username" => "john",
"age" => 30
}
}
Dependencies
This library has a dependency on the following libraries:
Summary
Functions
map_to_struct(map, atom, Keyword.t) :: {:error, Maptu.Extension.non_strict_error_reason} | {:ok, struct} | {:ok, struct, map}
Converts a map to a struct.
Arguments
module
: The module of the struct to be created.map
: The map to be converted to a struct.opts
: See opts section.
opts
transformations
: A list of transformations to apply to keys in the map in an attempt to make more matches with the keys in the struct. Defaults to[:snake_case]
. Warning: In the event of a match to a transformed key such as a key transformed to snake_case, the original map key is modified tosnake_case
and it’s value is added to the struct. If set to[]
, then no transformations are applied and only strings matching exactly to the struct atoms are matched. The transformations are only applied to keys of the map that do not initially match any key in the struct.rest
::false
,:true
or:merge
- By setting
rest
to:true
, the ‘leftover’ unmatched key-value pairs of the original map will also be returned in separate map with the keys in their original form. Returns as a tuple in the format{:ok, struct, rest}
- By setting
rest
to:merge
, the ‘leftover’ unmatched key-value pairs of the original map will be merged into the struct under the key:mapail
. Returns as a tuple in the format{:ok, struct}
- By setting
rest
to:false
, unmatched keys are silently discarded and only the struct is returned with matching keys. Defaults to:false
Returns as a tuple in the format{:ok, struct}
- By setting
Example (matching keys):
%Range{first: 1, last: 5} <==> %{"first" => 1, "last" => 5}
iex> Mapail.map_to_struct(%{"first" => 1, "last" => 5}, Range)
{:ok, 1..5}
Example (non-matching keys - with snake_case
transformations):
%Range{first: 1, last: 5} <==> %{"first" => 1, "Last" => 5}
iex> Mapail.map_to_struct(%{"first" => 1, "Last" => 5}, Range)
{:ok, 1..5}
Example (non-matching keys - without transformations):
%Range{first: 1, last: 5} <==> %{"first" => 1, "Last" => 5}
iex> {:ok, r} = Mapail.map_to_struct(%{"first" => 1, "Last" => 5}, Range, transformations: []); Map.keys(r);
[:__struct__, :first, :last]
iex> {:ok, r} = Mapail.map_to_struct(%{"first" => 1, "Last" => 5}, Range, transformations: []); Map.values(r);
[Range, 1, nil]
Example (non-matching keys):
%Range{first: 1, last: 5} <==> %{"first" => 1, "last" => 5, "next" => 3}
iex> Mapail.map_to_struct(%{"first" => 1, "last" => 5, "next" => 3}, Range)
{:ok, 1..5}
Example (non-matching keys - capturing excess key-value pairs in separate map):
%Range{first: 1, last: 5} <==> %{"first" => 1, "last" => 5, "next" => 3}
iex> Mapail.map_to_struct(%{"first" => 1, "last" => 5, "next" => 3}, Range, rest: :true)
{:ok, 1..5, %{"next" => 3}}
Example (non-matching keys - capturing excess key-value pairs and merging into struct under :mapail
key):
%Range{first: 1, last: 5} <==> %{"first" => 1, "last" => 5, "next" => 3}
iex> {:ok, r} = Mapail.map_to_struct(%{"first" => 1, "last" => 5, "next" => 3}, Range, rest: :merge); Map.values(r);
[Range, 1, 5, %{"next" => 3}]
iex> {:ok, r} = Mapail.map_to_struct(%{"first" => 1, "last" => 5, "next" => 3}, Range, rest: :merge); Map.keys(r)
[:__struct__, :first, :last, :mapail]
Converts a map to a struct.
Arguments
module
: The module of the struct to be created.map
: The map to be converted to a struct.opts
: See opts section.
opts
transformations
: A list of transformations to apply to keys in the map in an attempt to make more matches with the keys in the struct. Defaults to[:snake_case]
. Warning: In the event of a match to a transformed key such as a key transformed to snake_case, the original map key is modified tosnake_case
and it’s value is added to the struct. If set to[]
, then no transformations are applied and only strings matching exactly to the struct atoms are matched. The transformations are only applied to keys of the map that do not initially match any key in the struct.rest
::false
or:merge
- By setting
rest
to:merge
, the ‘leftover’ unmatched key-value pairs of the original map will be merged into the struct under the key:mapail
. - By setting
rest
to:false
, unmatched keys are silently discarded and only the struct is returned with matching keys. Defaults to:false
- By setting
Example (matching keys):
%Range{first: 1, last: 5} <==> %{"first" => 1, "last" => 5}
iex> Mapail.map_to_struct!(%{"first" => 1, "last" => 5}, Range)
1..5
Example (non-matching keys - with snake_case
transformations):
%Range{first: 1, last: 5} <==> %{"first" => 1, "Last" => 5}
iex> Mapail.map_to_struct!(%{"first" => 1, "Last" => 5}, Range)
1..5
Example (non-matching keys - without transformations):
%Range{first: 1, last: 5} <==> %{"first" => 1, "Last" => 5}
iex> Mapail.map_to_struct!(%{"first" => 1, "Last" => 5}, Range, transformations: []) |> Map.values
[Range, 1, nil]
iex> Mapail.map_to_struct!(%{"first" => 1, "Last" => 5}, Range, transformations: []) |> Map.keys
[:__struct__, :first, :last]
Example (non-matching keys):
%Range{first: 1, last: 5} <==> %{"first" => 1, "last" => 5, "next" => 3}
iex> Mapail.map_to_struct!(%{"first" => 1, "last" => 5, "next" => 3}, Range)
1..5
Example (non-matching keys - capturing excess key-value pairs in separate map):
%Range{first: 1, last: 5} <==> %{"first" => 1, "last" => 5, "next" => 3}
iex> Mapail.map_to_struct!(%{"first" => 1, "last" => 5, "next" => 3}, Range, rest: :merge) |> Map.values
[Range, 1, 5, %{"next" => 3}]
iex> Mapail.map_to_struct!(%{"first" => 1, "last" => 5, "next" => 3}, Range, rest: :merge) |> Map.keys
[:__struct__, :first, :last, :mapail]