Nestru (Nestru v0.1.1) View Source
A library to serialize between maps and nested structs.
Turns a map into a nested struct according to hints given to the library. And vice versa turns any nested struct into a map.
It works with maps/structs of any shape and complexity. For example, when map keys are named differently than struct's fields. Or when fields can hold values of various struct types conditionally.
The library's primary purpose is to serialize a JSON map; at the same time, the map can be of any origin.
The map can have atom or binary keys. The library takes the binary key first and then the same-named atom key if the binary key is missing during the decoding of the map.
Mix.install [:nestru], force: true, consolidate_protocols: false
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
defmodule Total do
@derive [Nestru.Decoder, Nestru.Encoder]
defstruct [:sum]
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
alias BookCollection.{Book, Magazine}
Let's convert the nested struct into a map. The returned map gets
extra items_kinds
field with types information:
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"}
]
}}
Error handling and path to the failed part of the map
Every implemented function of Nestru protocols can return {error, message}
tuple
in case of failure.
When Nestru
receives the error tuple, it stops conversion and bypasses the error to the caller.
However, before doing so, the library wraps the error message into a map and adds path
and get_in_keys
fields to it. The path values point to the failed part of the map
like the following:
defmodule Location do
@derive {Nestru.Decoder, %{street: Street}}
defstruct [:street]
end
defmodule Street do
@derive {Nestru.Decoder, %{house: House}}
defstruct [:house]
end
defmodule House do
defstruct [:number]
defimpl Nestru.Decoder do
def from_map_hint(_value, _context, map) do
if Nestru.has_key?(map, :number) do
{:ok, Nestru.get(map, :number)}
else
{:error, "Can't continue without house number."}
end
end
end
end
map = %{
"street" => %{
"house" => %{
"name" => "Party house"
}
}
}
{:error, error} = Nestru.from_map(map, Location)
{:error,
%{
get_in_keys: [#Function<8.5372299/3 in Access.key!/1>, #Function<8.5372299/3 in Access.key!/1>],
message: "Can't continue without house number.",
path: [:street, :house]
}}
The failed part of the map can be returned like the following:
get_in(map, error.get_in_keys)
%{name: "Party house"}
Use with other libraries
Jason
JSON maps decoded with Jason library are supported with both binary and atoms keys.
ExJSONPath
ExJsonPath library allows querying maps
(JSON objects) and lists (JSON arrays), using JSONPath expressions.
The queries can be useful in Nestru.PreDecoder.gather_fields_map/3
function to assemble fields for decoding from a map having a very different shape
from the target struct.
Domo
Consider using the Domo library
to validate the types of the nested struct values after decoding with Nestru
.
Domo
can validate a nested struct in one pass, ensuring that
the struct's field values match its 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.
Gets the value for a specific key in map. Lookups a binary or an atom key.
Returns whether the given key exists in the given map as a binary or as an atom.
Creates a map from the given nested struct.
Similar to to_map/1
.
Link to this section Functions
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.
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.
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.
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.
Gets the value for a specific key in map. Lookups a binary or an atom key.
If key is present in map then its value value is returned. Otherwise, default is returned.
If default is not provided, nil is used.
Returns whether the given key exists in the given map as a binary or as an atom.
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.