Maptu v1.0.0 Maptu

Provides functions to convert from “dumped” maps to Elixir structs.

This module provides functions to safely convert maps (with string keys) that represent structs (usually decoded from some kind of protocol, like MessagePack or JSON) to Elixir structs.

Rationale

Many Elixir libraries need to encode and decode maps as well as structs. Encoding is often straightforward (think of libraries like poison or msgpax): the map is encoded by converting keys to strings and encoding values recursively. This works natively with structs as well, since structs are just maps with anadditional :__struct__ key.

The problem arises when such structs have to be decoded back to Elixir terms: decoding will often result in a map with string keys (as the information about keys being atoms were lost in the encoding), including a "__struct__" key. Trying to blindly convert all these string keys to atoms and building a struct by reading the :__struct__ key is dangerous: converting dynamic input to atoms is one of the most frequent culprit of memory leaks in Erlang applications. This is where Maptu becomes useful: it provides functions that safely convert from this kind of maps to Elixir structs.

Use case

Let’s pretend we’re writing a JSON encoder/decoder for Elixir. We have generic encoder/decoder that work for all kinds of maps:

def encode(map) when is_map(map) do
  # some binary is built here
end

def decode(bin) when is_binary(bin) do
  # decoding happens here
end

When we encode and then decode a struct, something like this is likely to happen:

%URI{port: 8080} |> encode() |> decode()
#=> %{"__struct__" => "Elixir.URI", "port" => 8080}

To properly decode the struct back to a %URI{} struct, we would have to check the value of "__struct__" (check that it’s an existing atom and then an existing module), then check each key-value pair in the map to see if it’s a field of the URI struct and so on. Maptu does exactly this!

%URI{port: 8080} |> encode() |> decode() |> Maptu.struct!()
#=> %URI{port: 8080}

This is just one use case Maptu is good at; read the documentation for the provided functions for more information on the capabilities of this library.

Summary

Functions

Converts a map to a struct, failing on erroneous keys

Builds the mod struct with the given fields, failing on erroneous keys

Behaves like Maptu.strict_struct/1 but raises in case of error

Behaves like Maptu.strict_struct/2 but raises in case of error

Converts a map to a struct, silently ignoring erroneous keys

Builds the mod struct with the given fields, silently ignoring erroneous keys

Behaves like Maptu.struct/1 but raises in case of error

Behaves like Maptu.struct/2 but raises in case of error

Types

non_strict_error_reason ::
  :missing_struct_key |
  {:bad_module_name, binary} |
  {:non_existing_module, binary} |
  {:non_struct, module}
strict_error_reason ::
  non_strict_error_reason |
  {:non_existing_atom, binary} |
  {:unknown_struct_field, module, atom}

Functions

strict_struct(map)

Specs

strict_struct(%{}) ::
  {:ok, %{}} |
  {:error, strict_error_reason}

Converts a map to a struct, failing on erroneous keys.

This function behaves like Maptu.struct/1, except that it returns an error if one of the fields in map isn’t a field of the resulting struct.

This function returns {:ok, struct} if the conversion is successful, {:error, reason} otherwise.

Examples

iex> Maptu.strict_struct(%{"__struct__" => "Elixir.URI", "port" => 8080})
{:ok, %URI{port: 8080}}

iex> Maptu.strict_struct(%{"__struct__" => "Elixir.URI", "pid" => 1})
{:error, {:unknown_struct_field, URI, :pid}}
strict_struct(mod, fields)

Specs

strict_struct(module, %{}) ::
  {:ok, %{}} |
  {:error, strict_error_reason}

Builds the mod struct with the given fields, failing on erroneous keys.

This function behaves like Maptu.strict_struct/2, except it returns an error when keys in fields don’t map to fields in the resulting struct.

This function returns {:ok, struct} if the building is successful, {:error, reason} otherwise.

Examples

iex> Maptu.strict_struct(URI, %{"port" => 8080})
{:ok, %URI{port: 8080}}
iex> Maptu.strict_struct(URI, %{"pid" => 1})
{:error, {:unknown_struct_field, URI, :pid}}
strict_struct!(map)

Specs

strict_struct!(%{}) :: %{} | no_return

Behaves like Maptu.strict_struct/1 but raises in case of error.

This function behaves like Maptu.strict_struct/1, but it returns struct (instead of {:ok, struct}) if the conversion is valid, and raises an ArgumentError exception if it’s not valid.

Examples

iex> Maptu.strict_struct!(%{"__struct__" => "Elixir.URI", "port" => 8080})
%URI{port: 8080}

iex> Maptu.strict_struct!(%{"__struct__" => "Elixir.URI", "pid" => 1})
** (ArgumentError) unknown field :pid for struct URI
strict_struct!(mod, fields)

Specs

strict_struct!(module, %{}) :: %{} | no_return

Behaves like Maptu.strict_struct/2 but raises in case of error.

This function behaves like Maptu.strict_struct/2, but it returns struct (instead of {:ok, struct}) if the conversion is valid, and raises an ArgumentError exception if it’s not valid.

Examples

iex> Maptu.strict_struct!(URI, %{"port" => 8080})
%URI{port: 8080}

iex> Maptu.strict_struct!(URI, %{"pid" => 1})
** (ArgumentError) unknown field :pid for struct URI
struct(map)

Specs

struct(%{}) ::
  {:ok, %{}} |
  {:error, non_strict_error_reason}

Converts a map to a struct, silently ignoring erroneous keys.

map is a map with binary keys that represents a “dumped” struct; it must contain a "__struct__" key with a binary value that can be converted to a valid module name. If the value of "__struct__" is not a module name or it’s a module that isn’t a struct, then an error is returned.

Keys in map that are not fields of the resulting struct are simply discarded.

This function returns {:ok, struct} if the conversion is successful, {:error, reason} otherwise.

Examples

iex> Maptu.struct(%{"__struct__" => "Elixir.URI", "port" => 8080, "foo" => 1})
{:ok, %URI{port: 8080}}

iex> Maptu.struct(%{"__struct__" => "Elixir.GenServer"})
{:error, {:non_struct, GenServer}}
struct(mod, fields)

Specs

struct(module, %{}) ::
  {:ok, %{}} |
  {:error, non_strict_error_reason}

Builds the mod struct with the given fields, silently ignoring erroneous keys.

This function takes a struct mod (mod should be a module that defines a struct) and a map of fields with binary keys. It builds the mod struct by safely parsing the fields in fields.

If a key in fields doesn’t map to a field in the resulting struct, it’s ignored.

This function returns {:ok, struct} if the building is successful, {:error, reason} otherwise.

Examples

iex> Maptu.struct(URI, %{"port" => 8080, "nonexisting_field" => 1})
{:ok, %URI{port: 8080}}
iex> Maptu.struct(GenServer, %{})
{:error, {:non_struct, GenServer}}
struct!(map)

Specs

struct!(%{}) :: %{} | no_return

Behaves like Maptu.struct/1 but raises in case of error.

This function behaves like Maptu.struct/1, but it returns struct (instead of {:ok, struct}) if the conversion is valid, and raises an ArgumentError exception if it’s not valid.

Examples

iex> Maptu.struct!(%{"__struct__" => "Elixir.URI", "port" => 8080})
%URI{port: 8080}

iex> Maptu.struct!(%{"__struct__" => "Elixir.GenServer"})
** (ArgumentError) module is not a struct: GenServer
struct!(mod, fields)

Specs

struct!(module, %{}) :: %{} | no_return

Behaves like Maptu.struct/2 but raises in case of error.

This function behaves like Maptu.struct/2, but it returns struct (instead of {:ok, struct}) if the conversion is valid, and raises an ArgumentError exception if it’s not valid.

Examples

iex> Maptu.struct!(URI, %{"port" => 8080})
%URI{port: 8080}

iex> Maptu.struct!(GenServer, %{})
** (ArgumentError) module is not a struct: GenServer