diff --git a/README.md b/README.md index 0d1951e..4bfe680 100644 --- a/README.md +++ b/README.md @@ -44,23 +44,39 @@ The library generates maps with atom keys during the struct encode operation.

-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 = %{ @@ -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 @@ -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 = %{ @@ -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 diff --git a/lib/nestru/decoder.ex b/lib/nestru/decoder.ex index 1b55995..5781dff 100644 --- a/lib/nestru/decoder.ex +++ b/lib/nestru/decoder.ex @@ -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 diff --git a/lib/nestru/encoder.ex b/lib/nestru/encoder.ex index 85edd2f..77c8459 100644 --- a/lib/nestru/encoder.ex +++ b/lib/nestru/encoder.ex @@ -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