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:

  • Maptu v1.0.0 library. For converting a matching map to a struct. MIT © 2016 Andrea Leopardi, Aleksei Magusev. Licence

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, module, opts \\ [])
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]
map_to_struct!(map, module, opts \\ [])
map_to_struct!(map, atom, Keyword.t) :: struct | no_return

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]
stringify_map(map)
stringify_map(map) :: {:ok, map} | {:error, String.t}

Convert a map with atom only or atom/string mixed keys to a map with string keys only.

struct_to_struct(old_struct, module, opts \\ [])
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.

struct_to_struct!(old_struct, module)
struct_to_struct!(map, atom) :: struct | no_return

Convert one form of struct into another struct and raises an error on fail.