Skip to content

Commit

Permalink
Update examples in Readme
Browse files Browse the repository at this point in the history
  • Loading branch information
IvanRublev committed Mar 30, 2024
1 parent e079931 commit b27256e
Show file tree
Hide file tree
Showing 3 changed files with 138 additions and 29 deletions.
146 changes: 119 additions & 27 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,23 +44,39 @@ The library generates maps with atom keys during the struct encode operation.
</a>
</p>

Let's say we have an `Order` with `Total` that we want to decode from a map.
First, let's derive `Nestru.Decoder` protocol and specify that field `:total`
should hold a value of `Total` struct like the following:
Let's say we have an `Order` with a total field which is an instance of a `Total` struct.
And we want to serialize between an instance of `Order` and a map.

Firstly, let's derive `Nestru.Encoder` and `Nestru.Decoder` protocols
and give a hint that the field `:total` should hold a value of `Total` struct
like the following:

```elixir
defmodule Order do
@derive {Nestru.Decoder, hint: %{total: Total}}
@derive [Nestru.Encoder, {Nestru.Decoder, hint: %{total: Total}}]
defstruct [:id, :total]
end

defmodule Total do
@derive Nestru.Decoder
@derive [Nestru.Encoder, Nestru.Decoder]
defstruct [:sum]
end
```
```output
{:module, Total, <<70, 79, 82, 49, 0, 0, 8, ...>>, %Total{sum: nil}}
```

Now we decode the `Order` from the nested map like that:
Secondly, we can encode the `Order` into the map like that:

```elixir
model = %Order{id: "A548", total: %Total{sum: 500}}
{:ok, map} = Nestru.encode(model)
```
```output
{:ok, %{id: "A548", total: %{sum: 500}}}
```

And decode the map back into the `Order` like the following:

```elixir
map = %{
Expand All @@ -74,13 +90,17 @@ map = %{
{:ok, %Order{id: "A548", total: %Total{sum: 500}}}
```

We get the order as the expected nested struct. Good!
As you can see the data markup is in place, the `Total` struct is nested within the `Order` struct.

Now we add the `:items` field to `Order1` struct to hold a list of `LineItem`s:
## A list of structs in a field

Let's add the `:items` field to `Order1` struct to hold a list of `LineItem`s
and give a hint to `Nestru` on how to decode that field:

```elixir
defmodule Order1 do
@derive {Nestru.Decoder, hint: %{total: Total}}
@derive {Nestru.Decoder, hint: %{total: Total, items: [LineItem]}}

defstruct [:id, :items, :total]
end

Expand All @@ -89,8 +109,11 @@ defmodule LineItem do
defstruct [:amount]
end
```
```output
{:module, LineItem, <<70, 79, 82, 49, 0, 0, 8, ...>>, %LineItem{amount: nil}}
```

and we decode the `Order1` from the nested map like that:
Let's decode:

```elixir
map = %{
Expand All @@ -102,40 +125,109 @@ map = %{
{:ok, model} = Nestru.decode(map, Order1)
```
```output
{:ok, %Order1{id: "A548", items: [%{"amount" => 150}, %{"amount" => 350}], total: %Total{sum: 500}}}
{:ok,
%Order1{
id: "A548",
items: [%LineItem{amount: 150}, %LineItem{amount: 350}],
total: %Total{sum: 500}
}}
```

The `:items` field value of the `%Order1{}` is still the list of maps
and not structs 🤔 This is because `Nestru` has no clue what kind of struct
these list items should be. So let's give a hint to `Nestru` on how to decode
that field:
Voilà, we have field values as nested structs 🎉

For the case when the list contains several structs of different types, please,
see the Serializing type-dependent fields section below.


## Date Time and URI

Let's say we have an `Order2` struct with some `URI` and `DateTime` fields in it.
These attributes are structs in Elixir, at the same time they usually
kept as binary representations in a map.

`Nestru` supports conversion between binaries
and structs, all we need to do is to implement the `Nestry.Encoder`
and `Nestru.Decoder` protocols for these structs like the following:

```elixir
defmodule Order2 do
@derive {Nestru.Decoder, hint: %{total: Total, items: [LineItem]}}
# DateTime
defimpl Nestru.Encoder, for: DateTime do
def gather_fields_from_struct(struct, _context) do
{:ok, DateTime.to_string(struct)}
end
end

defstruct [:id, :items, :total]
defimpl Nestru.Decoder, for: DateTime do
def decode_fields_hint(_empty_struct, _context, value) do
case DateTime.from_iso8601(value) do
{:ok, date_time, _offset} -> {:ok, date_time}
error -> error
end
end
end

# URI
defimpl Nestru.Encoder, for: URI do
def gather_fields_from_struct(struct, _context) do
{:ok, URI.to_string(struct)}
end
end

defimpl Nestru.Decoder, for: URI do
def decode_fields_hint(_empty_struct, _context, value) do
URI.new(value)
end
end
```
```output
{:module, Nestru.Decoder.URI, <<70, 79, 82, 49, 0, 0, 8, ...>>, {:decode_fields_hint, 3}}
```

Let's decode again:
`Order2` is defined like this:

```elixir
{:ok, model} = Nestru.decode(map, Order2)
defmodule Order2 do
@derive [Nestru.Encoder, {Nestru.Decoder, hint: %{date: DateTime, website: URI}}]
defstruct [:id, :date, :website]
end
```
```output
{:module, Order2, <<70, 79, 82, 49, 0, 0, 8, ...>>, %Order2{id: nil, date: nil, website: nil}}
```

We can encode it to a map with binary fields like the following:

```elixir
order = %Order2{id: "B445", date: ~U[2024-03-15 22:42:03Z], website: URI.parse("https://www.example.com/?book=branch")}

{:ok, map} = Nestru.encode(order)
```
```output
{:ok, %{id: "B445", date: "2024-03-15 22:42:03Z", website: "https://www.example.com/?book=branch"}}
```

And decode it back:

```elixir
Nestru.decode(map, Order2)
```
```output
{:ok,
%Order2{
id: "A548",
items: [%LineItem{amount: 150}, %LineItem{amount: 350}],
total: %Total{sum: 500}
id: "B445",
date: ~U[2024-03-15 22:42:03Z],
website: %URI{
scheme: "https",
userinfo: nil,
host: "www.example.com",
port: 443,
path: "/",
query: "book=branch",
fragment: nil
}
}}
```

Voilà, we have field values as nested structs 🎉

For the case when the list contains several structs of different types, please,
see the Serializing type-dependent fields section below.

## Error handling and path to the failed part of the map

Expand Down
10 changes: 9 additions & 1 deletion lib/nestru/decoder.ex
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,14 @@ defimpl Nestru.Decoder, for: Any do
end

def decode_fields_hint(%module{} = _empty_struct, _context, _value) do
raise "Please, @derive Nestru.Decoder protocol before defstruct/1 call in #{inspect(module)} or defimpl the protocol in the module explicitly to support decoding from a map or a binary."
exception_text =
if module in [DateTime, URI, Range] do
"Please, defimpl the protocol for the #{inspect(module)} module explicitly to support decoding from a map or a binary. \
Please, see an example on how to decode modules from Elixir on https://github.com/IvanRublev/Nestru#date-time-and-uri"
else
"Please, @derive Nestru.Decoder protocol before defstruct/1 call in #{inspect(module)} or defimpl the protocol in the module explicitly to support decoding from a map or a binary."
end

raise exception_text
end
end
11 changes: 10 additions & 1 deletion lib/nestru/encoder.ex
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,15 @@ defimpl Nestru.Encoder, for: Any do
end

def gather_fields_from_struct(%module{}, _context) do
raise "Please, @derive Nestru.Encoder protocol before defstruct/1 call in #{inspect(module)} or defimpl the protocol in the module explicitly to support encoding into a map or a binary."

exception_text =
if module in [DateTime, URI, Range] do
"Please, defimpl the protocol for the #{inspect(module)} module explicitly to support encoding into a map or a binary. \
Please, see an example on how to encode modules from Elixir on https://github.com/IvanRublev/Nestru#date-time-and-uri"
else
"Please, @derive Nestru.Encoder protocol before defstruct/1 call in #{inspect(module)} or defimpl the protocol in the module explicitly to support encoding into a map or a binary."
end

raise exception_text
end
end

0 comments on commit b27256e

Please sign in to comment.