data v0.5.0 Data.Constructor

Link to this section Summary

Functions

Define and run a smart constructor on a Key-Value input, returning either well-defined structs or descriptive errors. The motto: parse, don't validate!

Define a smart update function based on a list of field specifications, the struct type to be updated, and a Keyword or map of input params.

Link to this section Functions

Link to this function

struct(field_specs, struct_module, input)

Define and run a smart constructor on a Key-Value input, returning either well-defined structs or descriptive errors. The motto: parse, don't validate!

Given a list of Data.Parser.KV.field_spec/2s, a module, and an input map or Keyword, create and run a parser which will either parse successfully and return an {:ok, %__MODULE__{}} struct, or fail and return an {:error, Error.t} with details about the parsing failure.

Examples

iex> defmodule SensorReading do
...>   defstruct [:sensor_id, :microfrobs, :datetime]
...>   def new(input) do
...>     Data.Constructor.struct([
...>      {:sensor_id, Data.Parser.BuiltIn.string()},
...>      {:microfrobs, Data.Parser.BuiltIn.integer()},
...>      {:datetime, Data.Parser.BuiltIn.datetime()}],
...>     __MODULE__,
...>     input)
...>   end
...> end
...>
...> {:ok, reading} = SensorReading.new(sensor_id: "1234-1234-1234",
...>                                    microfrobs: 23,
...>                                    datetime: ~U[2018-12-20 12:00:00Z])
...>
...> ~U[2018-12-20 12:00:00Z] = reading.datetime
...> 23 = reading.microfrobs
...> "1234-1234-1234" = reading.sensor_id
...> {:error, e} = SensorReading.new(%{"sensor_id" => nil,
...>                                   "microfrobs" => 23,
...>                                   "datetime" => "2018-12-20 12:00:00Z"})
...> :failed_to_parse_field = Error.reason(e)
...>  %{field: :sensor_id, input: %{"datetime" => "2018-12-20 12:00:00Z",
...>                                "microfrobs" => 23,
...>                                "sensor_id" => nil}} = Error.details(e)
...> {:just, inner_error} = Error.caused_by(e)
...> Error.reason(inner_error)
:not_a_string
Link to this function

update(field_specs, struct_type, params)

Define a smart update function based on a list of field specifications, the struct type to be updated, and a Keyword or map of input params.

Given a list of Data.Parser.KV.field_spec/2s, a module (which defines a struct), and an input map or Keyword, create an {:ok, &fun/1} tuple, or fail and return an {:error, Error.t} with details about the parsing failure.

The &fun/1 in the ok-tuple can be applied to any struct as defined by module, and will update it with the fields provided in the constructor input.

The crucial advantage here is that the input parameters are validated according to the provided Data.Parser.KV.field_spec/2s, so that using the same list of field_specs for new/3 and update/3 will result in correct-by-construction data both from construction and after updates.

Additionally, if any of the field_specs define a default: value, that value will be explicitly allowed in updates for that field, along with the specified type.

Examples

iex> defmodule ReadingWComment do
...>   defstruct [:sensor_id, :microfrobs, :datetime, :comments]
...>
...>   defp fields, do: [
...>      {:sensor_id, Data.Parser.BuiltIn.string()},
...>      {:microfrobs, Data.Parser.BuiltIn.integer()},
...>      {:datetime, Data.Parser.BuiltIn.datetime()},
...>      {:comments, Data.Parser.BuiltIn.string(), default: nil}]
...>
...>   def new(input) do
...>     Data.Constructor.struct(fields(), __MODULE__, input)
...>   end
...>
...>   def update(sensor_reading, input) do
...>     case Data.Constructor.update(fields(), __MODULE__, input) do
...>      {:ok, update_fun} -> update_fun.(sensor_reading)
...>      {:error, e} -> {:error, e}
...>     end
...>   end
...> end
...>
...> {:ok, reading} = ReadingWComment.new(sensor_id: "1234-1234-1234",
...>                                      microfrobs: 23,
...>                                      datetime: ~U[2018-12-20 12:00:00Z],
...>                                      comments: "delete me later")
...> "delete me later" = reading.comments
...>
...>
...> {:ok, reading2} = ReadingWComment.update(reading,
...>                                          microfrobs: 25,
...>                                          datetime: ~U[2018-12-20 13:00:00Z])
...> ~U[2018-12-20 13:00:00Z] = reading2.datetime
...> 25 = reading2.microfrobs
...>
...>
...> {:ok, reading3} = ReadingWComment.update(reading2,
...>                                          comments: nil)
...> nil = reading3.comments
...>
...>
...> {:error, e} = ReadingWComment.update(reading3,
...>                                      microfrobs: [1,2,3])
...> :invalid_parameter = Error.reason(e)
...> %{key: :microfrobs, value: [1,2,3]} = Error.details(e)
...>
...>
...> {:error, e} = ReadingWComment.update(%{"my" => "special", "map" => "type"},
...>                                      microfrobs: 25,
...>                                      datetime: ~U[2018-12-20 13:00:00Z])
...> :struct_type_mismatch = Error.reason(e)
...> Error.details(e)
%{expecting: Data.ConstructorTest.ReadingWComment, got: %{"map" => "type", "my" => "special"}}