Hoare.State behaviour (hoare v0.2.0)

Copy Markdown View Source

A named record state: the status that tags it and the properties that refine it.

A state module is a struct of witnesses, record plus whatever its properties extract, and match/2 builds it or says why the record is not in that state. A Hoare.Transition names states as its from and to.

defmodule Issued do
  use Hoare.State, status: :issued, witnesses: [:lines], preloads: [:lines]

  @impl Hoare.State
  def properties, do: [&billable_lines/1]

  defp billable_lines(%Issued{record: %{lines: []}}), do: {:error, :no_lines}
  defp billable_lines(%Issued{record: %{lines: lines}} = state), do: {:ok, %{state | lines: lines}}
end

Hoare.State.match(Issued, invoice)
#=> {:ok, %Issued{record: invoice, lines: [...]}} | {:error, :not_issued | :no_lines}

A property is an arrow over the state struct: it refines the tag, and may fill a witness so nothing downstream has to look again. use is a convenience over the behaviour; a module may implement the callbacks and define the struct itself. defstate/3 declares several states in one module.

Summary

Callbacks

The reason reported when a record's status does not tag it.

What the record must have loaded for the properties to read it.

Arrows over the state struct that refine the tag and fill its witnesses.

The status value that tags a record as being in this state.

Functions

Declares the state from its options; properties/0 stays overridable.

Declares a state as a module nested in the caller, so one module holds a record's states and each stays a struct of its own.

Matches a sum of states: the first whose status tags the record decides.

The state's declared preloads; none when it declares none.

Types

property()

@type property() :: (struct() -> {:ok, struct()} | {:error, reason()})

reason()

@type reason() :: atom()

subject()

@type subject() :: %{:status => atom(), optional(atom()) => term()}

Callbacks

missing()

@callback missing() :: reason()

The reason reported when a record's status does not tag it.

preloads()

(optional)
@callback preloads() :: [term()]

What the record must have loaded for the properties to read it.

properties()

@callback properties() :: [property()]

Arrows over the state struct that refine the tag and fill its witnesses.

status()

@callback status() :: atom()

The status value that tags a record as being in this state.

Functions

__using__(opts)

(macro)

Declares the state from its options; properties/0 stays overridable.

use Hoare.State, status: :issued, witnesses: [:lines], preloads: [:lines]

:missing defaults to :not_<status>, downcased; :witnesses are the struct's keys after record; :preloads defaults to none. The struct's type is t/0.

defstate(name, opts, block \\ [do: nil])

(macro)

Declares a state as a module nested in the caller, so one module holds a record's states and each stays a struct of its own.

defmodule Invoice.State do
  import Hoare.State, only: [defstate: 2, defstate: 3]

  defstate Draft, status: :draft
  defstate Paid, status: :paid

  defstate Issued, status: :issued, witnesses: [:lines], preloads: [:lines] do
    @impl Hoare.State
    def properties, do: [&billable_lines/1]
    ...
  end
end

The states are Invoice.State.Draft and so on. Add import_deps: [:hoare] to .formatter.exs to keep defstate free of parentheses.

match(state, record)

@spec match(module(), subject()) :: {:ok, struct()} | {:error, reason()}

match_any(states, record)

@spec match_any([module(), ...], subject()) :: {:ok, struct()} | {:error, reason()}

Matches a sum of states: the first whose status tags the record decides.

When no state tags the record, the first state's missing/0 is the reason.

preloads(state)

@spec preloads(module()) :: [term()]

The state's declared preloads; none when it declares none.