Data.Constructor (data v0.6.0)
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.
Functions
@spec struct( [Data.Parser.KV.field_spec(any(), any())], module(), Data.Parser.KV.input() ) :: FE.Result.t( struct(), Error.t() )
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
@spec update( [Data.Parser.KV.field_spec(any(), any())], module(), Data.Parser.KV.input() ) :: FE.Result.t( Data.Parser.t( struct(), Error.t() ), Error.t() )
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"}}