Mapail v1.0.2 Mapail
Helper library to convert a map into a struct or a struct to a struct.
Convert string-keyed maps to structs by calling the
map_to_struct/3
function.
Convert atom-keyed and atom/string mixed key maps to
structs by piping the stringify_map/1
into the map_to_struct/3
function.
Convert structs to structs by calling the struct_to_struct/3
function.
Note
The Maptu library already provides many of the functions necessary for converting “encoded” maps to Elixir structs. Maptu may be all you need - see Maptu. Mapail builds on top of
Maptu
and incorporates it as a dependency.Mapail
offers a few additional more lenient approaches to the conversion process to a struct as explained in use cases. Maptu may be all you need though.
Features
String keyed maps: Convert maps with string keys to a corresponding struct.
Transformations: Optionally, string manipulations can be applied to the key 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.
Helper function for converting atom-keyed maps or string/atom mixed keyed maps to string-keyed only maps.
Helper function for converting a struct to another struct.
Limitations
- Currently, only converts one level deep, that is, it does not convert nested structs. This is a potential TODO task.
Use Cases
- Scenario 1:
Map and Struct has a perfect match on the keys.
map_to_struct(map, MODULE)` returns `{:ok, %MODULE{} = new_struct}
- Scenario 2:
Map and Struct has an imperfect match on the keys
map_to_struct(map, MODULE, rest: :true)` returns `{:ok, %MODULE{} = new_struct, rest}
- Scenario 3:
Map and Struct has an imperfect match on the keys and a struct with and additional
field named :mapail
is returned. The value for the :mapail
fields is a
nested map with all non-matching key-pairs.
map_to_struct(map, MODULE, rest: :merge)` returns `{:ok, %MODULE{} = new_struct}
where `new_struct.mapail` contains the non-mathing `key-value` pairs.
- Scenario 4:
Map and Struct has an imperfect match on the keys. After an initial attempt to
match the map keys to those of the struct keys, any non-matching keys are piped
through transformation function(s) which modify the key of the map in an attempt
to make a new match with the modified key. For now, the only transformations supported
are [:snake_case]
. :snake_case
converts the non-matching keys to snake_case.
NOTE: This approach is lenient and will make matches that
otherwise would not have matched. It might prove useful where a json
encoded map
returned from a server uses camelcasing and matches are otherwise missed. Only
use this approach when it is explicitly desired behaviour
map_to_struct(map, MODULE, transformations: [:snake_case], rest: :true)
returns `{:ok, new_struct, rest}`
- Scenario 5:
Map and Struct has a perfect match but the keys in the map are mixed case. Mapail provides a utility function which can help in this situation.
stringify_map(map) |> map_to_struct(map, MODULE, rest: :false)
returns {:ok, %MODULE{} = new_struct}
- Scenario 6:
Struct and Struct has a perfect match but the struct fields are non-matching.
struct_to_struct(%Notifications.Email{}, User.Email)` returns `{:ok, %User.Email{} = new_struct}
Example - exact key matching (no transformations)
defmodule User do
defstruct [:first_name, :username, :password]
end
user = %{
"FirstName" => "John",
"Username" => "john",
"password" => "pass",
"age" => 30
}
Mapail.map_to_struct(user, User)
{:ok, %User{
first_name: :nil,
username: :nil,
password: "pass"
}
}
Example - key matching with transformations: [:snake_case]
defmodule User do
defstruct [:first_name, :username, :password]
end
user = %{
"FirstName" => "John",
"Username" => "john",
"password" => "pass",
"age" => 30
}
Mapail.map_to_struct(user, User, transformations: [:snake_case])
{:ok, %User{
first_name: "John",
username: "john",
password: "pass"
}
}
Example - getting unmatched elements in a separate map
defmodule User do
defstruct [:first_name, :username, :password]
end
user = %{
"FirstName" => "John",
"Username" => "john",
"password" => "pass",
"age" => 30
}
{:ok, user_struct, leftover} = Mapail.map_to_struct(user, User, rest: :true)
{:ok, %User{
first_name: :nil,
username: "pass",
password: :nil
},
%{
"FirstName" => "John",
"Username" => "john",
"age" => 30
}
}
Example - getting unmatched elements in a merged nested map
defmodule User do
defstruct [:first_name, :username, :password]
end
user = %{
"FirstName" => "John",
"Username" => "john",
"password" => "pass",
"age" => 30
}
Mapail.map_to_struct(user, User, rest: :merge)
{:ok, %User{
first_name: :nil,
username: "pass",
password: :nil,
mapail: %{
"FirstName" => "John",
"Username" => "john",
"age" => 30
}
}
Dependencies
This library has a dependency on the following library:
Summary
Functions
Converts a string-keyed map to a struct
Converts a string-keyed map to a struct and raises if it fails
Convert a map with atom only or atom/string mixed keys to a map with string keys only
Convert one form of struct into another struct
Convert one form of struct into another struct and raises an error on fail
Functions
map_to_struct(map, atom, Keyword.t) :: {:error, Maptu.Extension.non_strict_error_reason} | {:ok, struct} | {:ok, struct, map}
Converts a string-keyed 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 below
transformations: [atom]
:A list of transformations to apply to keys in the map where there are
non-matching
keys after the inital attempt to match.Defaults to
transformations: []
ie. no transformations are applied and only exactly matching keys are used to build a struct.If set to
transformations: [:snake_case]
, then after an initial run, non-matching keys are converted to snake_case form and another attempt is made to match the keys with the snake_case keys. This means less than exactly matching keys are considered a match when building the struct.rest: atom
:Defaults to
rest: :false
By setting
rest: :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: :merge
, the ‘leftover’ unmatched key-value pairs of the original map will be merged into the struct as a nested map under the key:mapail
. Returns as a tuple in the format{:ok, struct}
By setting
rest: :false
, unmatched keys are silently discarded and only the struct is returned with matching keys. Returns as a tuple in the format{:ok, struct}
.
Example (matching keys):
iex> Mapail.map_to_struct(%{"first" => 1, "last" => 5}, Range)
{:ok, 1..5}
Example (non-matching keys):
iex> Mapail.map_to_struct(%{"line_or_bytes" => [], "Raw" => :false}, File.Stream)
{:ok, %File.Stream{line_or_bytes: [], modes: [], path: nil, raw: true}}
Example (non-matching keys - with snake_case
transformations):
iex> Mapail.map_to_struct(%{"first" => 1, "Last" => 5}, Range, transformations: [:snake_case])
{:ok, 1..5}
Example (non-matching keys):
iex> {:ok, r} = Mapail.map_to_struct(%{"first" => 1, "Last" => 5}, Range); Map.keys(r);
[:__struct__, :first, :last]
Example (non-matching keys - with transformations):
iex> {:ok, r} = Mapail.map_to_struct(%{"first" => 1, "Last" => 5}, Range, transformations: [:snake_case]); Map.values(r);
[Range, 1, 5]
Example (non-matching keys):
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 called rest):
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):
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 string-keyed map to a struct and raises if it fails.
See map_to_struct/3
Example (matching keys):
iex> Mapail.map_to_struct!(%{"first" => 1, "last" => 5}, Range)
1..5
Example (non-matching keys):
iex> Mapail.map_to_struct!(%{"line_or_bytes" => [], "Raw" => :false}, File.Stream)
%File.Stream{line_or_bytes: [], modes: [], path: nil, raw: true}
Example (non-matching keys - with snake_case
transformations):
iex> Mapail.map_to_struct!(%{"first" => 1, "Last" => 5}, Range, transformations: [:snake_case])
1..5
Example (non-matching keys):
iex> Mapail.map_to_struct!(%{"first" => 1, "Last" => 5}, Range) |> Map.keys();
[:__struct__, :first, :last]
iex> Mapail.map_to_struct!(%{"first" => 1, "Last" => 5}, Range) |> Map.values();
[Range, 1, :nil]
Example (non-matching keys - with transformations):
iex> Mapail.map_to_struct!(%{"first" => 1, "Last" => 5}, Range, transformations: [:snake_case]) |> Map.values();
[Range, 1, 5]
Example (non-matching keys):
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):
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]
Convert a map with atom only or atom/string mixed keys to a map with string keys only.
struct_to_struct(map, atom, list) :: {:ok, struct} | {:ok, struct, map} | {:error, String.t}
Convert one form of struct into another struct.
opts
[]
- same as [rest: :false]
, {:ok, struct}
is returned and any non-matching pairs
will be discarded.
[rest: :true]
, {:ok, struct, map}
is returned where map are the non-matching
key-value pairs.
[rest: :false]
, {:ok, struct}
is returned and any non-matching pairs
will be discarded.