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:

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

Summary

Functions

Converts a map to a struct

Converts a map to a struct

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 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 to snake_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}

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

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 to snake_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

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]