Nestru (Nestru v0.1.0) View Source

A library to serialize between maps and nested structs.

Turns map of any shape into a model of nested structs according to hints given to the library. Turns any nested struct into a map.

The library's primary purpose is to serialize between JSON map and an application model; at the same time, the map can be of any origin.

Typical usage looks like the following:

defmodule Order do
  @derive Nestru.Encoder
  defstruct [:id, :items, :total]

  # Giving a hint to Nestru how to process the items list of structs
  # and the total struct, other fields go to struct as is.
  defimpl Nestru.Decoder do
    def from_map_hint(_value, _context, _map) do
      {:ok, %{
        items: &Nestru.from_list_of_maps(&1, LineItem),
        total: Total
      }}
    end
  end
end

defmodule LineItem do
  @derive [Nestru.Decoder, Nestru.Encoder]
  defstruct [:amount]
end

map = %{
  "id" => "A548",
  "items" => [%{"amount" => 150}, %{"amount" => 350}],
  "total" => %{"sum" => 500}
}

{:ok, model} = Nestru.from_map(map, Order)
{:ok,
  %OrderA{
    id: "A548",
    items: [%LineItemA{amount: 150}, %LineItemA{amount: 350}],
    total: %Total{sum: 500}
  }}

And going back to the map is as simple as that:

map = Nestru.to_map(model)
%{
  id: "A548",
  items: [%{amount: 150}, %{amount: 350}],
  total: %{sum: 500}
}

Maps with different key names

In some cases, the map's keys have slightly different names compared to the target's struct field names. Fields that should be decoded into the struct can be gathered by adopting Nestru.PreDecoder protocol like the following:

defmodule Quote do
  @derive Nestru.Decoder

  defstruct [:cost]

  defimpl Nestru.PreDecoder do
    def gather_fields_map(_value, _context, map) do
      {:ok, %{cost: map.cost_value}}
    end
  end
end

map = %{
  "cost_value" => 1280
}

Nestru.from_map(map, Quote)
{:ok, %Quote{cost: 1280}}

Serializing type-dependent fields

To convert a struct with a field that can have the value of multiple struct types into the map and back, the type of the field's value should be persisted. It's possible to do that like the following:

defmodule BookCollection do
  defstruct [:name, :items]

  defimpl Nestru.Encoder do
    def to_map(struct) do
      items_kinds = Enum.map(struct.items, fn %module{} ->
        module
        |> Module.split()
        |> Enum.join(".")
      end)

      items = Enum.map(struct.items, fn item ->
        {:ok, map} = Nestru.to_map(item)
        map
      end)

      {:ok, %{name: struct.name, items_kinds: items_kinds, items: items}}
    end
  end

  defimpl Nestru.Decoder do
    def from_map_hint(_value, _context, map) do
      items_kinds = Enum.map(map.items_kinds, fn module_string ->
        module_string
        |> String.split(".")
        |> Module.safe_concat()
      end)

      {:ok, %{items: &Nestru.from_list_of_maps(&1, items_kinds)}}
    end
  end
end

defmodule BookCollection.Book do
  @derive [Nestru.Encoder, Nestru.Decoder]
  defstruct [:title]
end

defmodule BookCollection.Magazine do
  @derive [Nestru.Encoder, Nestru.Decoder]
  defstruct [:issue]
end

collection = %BookCollection{
  name: "Duke of Norfolk's archive",
  items: [
    %Book{title: "The Spell in the Chasm"},
    %Magazine{issue: "Strange Hunt"}
  ]
}

{:ok, map} = Nestru.to_map(collection)
{:ok, 
 %{
  name: "Duke of Norfolk's archive",
  items_kinds: ["BookCollection.Book", "BookCollection.Magazine"],
  items: [%{title: "The Spell in the Chasm"}, %{issue: "Strange Hunt"}]
 }}

And restoring of the original nested struct is as simple as that:

{:ok, collection} = Nestru.from_map(map, BookCollection)
{:ok, 
 %BookCollection{
  name: "Duke of Norfolk's archive",
  items: [
    %Book{title: "The Spell in the Chasm"},
    %Magazine{issue: "Strange Hunt"}
  ]
 }}

Use with other libraries

Jason

JSON maps decoded with Jason library are supported with both strings and atoms keys.

Domo

To validate the types of the nested struct values, consider Domo library that ensures struct's t() type and associated preconditions.

Link to this section Summary

Functions

Creates a list of nested structs from the given list of maps.

Similar to from_list_of_maps/2 but checks if enforced struct's fields keys exist in the given maps.

Creates a nested struct from the given map.

Similar to from_map/3 but checks if enforced struct's fields keys exist in the given map.

Creates a map from the given nested struct.

Link to this section Functions

Link to this function

from_list_of_maps(list, struct_atoms, context \\ [])

View Source

Creates a list of nested structs from the given list of maps.

The first argument is a list of maps.

If the second argument is a struct's module atom, then the function calls the from_map/3 on each input list item.

If the second argument is a list of struct module atoms, the function calls the from_map/3 function on each input list item with the module atom taken at the same index of the second list. In this case, both arguments should be of equal length.

The third argument is a context value to be passed to implemented functions of Nestru.PreDecoder and Nestru.Decoder protocols.

The function returns a list of structs or the first error from from_map/3 function.

Link to this function

from_list_of_maps!(list, struct_atoms, context \\ [])

View Source

Similar to from_list_of_maps/2 but checks if enforced struct's fields keys exist in the given maps.

Returns a struct or raises an error.

Link to this function

from_map(map, struct_module, context \\ [])

View Source

Creates a nested struct from the given map.

The first argument is a map having key-value pairs. Supports both string and atom keys in the map.

The second argument is a struct's module atom.

The third argument is a context value to be passed to implemented functions of Nestru.PreDecoder and Nestru.Decoder protocols.

To give a hint on how to decode nested struct values or a list of such values for the given field, implement Nestru.Decoder protocol for the struct.

Function calls struct/2 to build the struct's value. Keys in the map that don't exist in the struct are automatically discarded.

Link to this function

from_map!(map, struct_module, context \\ [])

View Source

Similar to from_map/3 but checks if enforced struct's fields keys exist in the given map.

Returns a struct or raises an error.

Creates a map from the given nested struct.

Casts each field's value to a map recursively, whether it is a struct or a list of structs.

To give a hint to the function of how to generate a map, implement Nestru.Encoder protocol for the struct. That can be used to keep additional type information for the field that can have a value of various struct types.

Similar to to_map/1.

Returns a map or raises an error.