StatifierRouter.Config (StatifierRouter v0.2.0)

Copy Markdown View Source

The router's resolved configuration: the host's repo, where this package's tables live in it, the bindings events are routed by, the module each delivery is handed to, and what that module needs to reach statifier_persistence.

new/1 takes a keyword list and returns {:ok, config} or {:error, reason}:

OptionValueDefault
:repothe host's Ecto.Repo modulerequired
:deliverythe module StatifierRouter.route/3 hands each delivery toStatifierRouter.Delivery
:storea %StatifierPersistence.Storage{} built over the same reporequired by StatifierRouter.Delivery
:persistence_optionsthe per-call statifier_persistence snapshot options every create and every step carries: :routes, :invoke_types, :send_types[]
:executorthe StatifierPersistence.Executor effects are handed to: a module or an arity-2 funrequired by StatifierRouter.Delivery
:resolverthe StatifierRouter.Resolver naming the chart a new execution starts on: a module implementing it or an arity-2 funrequired by StatifierRouter.Delivery
:chart_resolvera fun of (content_hash) compiling the chart an existing execution started onrequired by StatifierRouter.Delivery
:bindingsa list of bindings, each a map or keyword list StatifierRouter.Binding.new/1 accepts, or a %StatifierRouter.Binding{} it built[]
:route_adaptersthe route registry, a map from route name to {module, config} where the module implements StatifierRouter.Route%{}
:route_overridesa map from scope to a map from route name to a configuration, merged over that route's registered configuration in that scope%{}
:send_typethe one <send> type string StatifierRouter.SendHandler answers tonil
:on_completethe name of a registered route an execution's donedata is handed to on the delivery that finishes itnil
:timer_queue{module, config} where the module implements StatifierRouter.TimerQueuenil
:table_prefixa string prefixed to every table name"statifier_router_"
:prefixthe Postgres schema the tables live in, as a stringnil (the repo's default)

StatifierRouter documents what a delivery module must answer, and StatifierRouter.Delivery what the four options it requires must be. A configuration that names another delivery module may leave those four out; one it gives is checked all the same. :persistence_options is never required: a configuration that omits it carries no snapshot, which is what statifier_persistence reads as "the built-in set only".

The route registry, and the two routes on this struct

:route_adapters is ADR-0005 decision 2's registry: the named outbound destinations a chart reaches with <send target="...">, each mapped to the module that serves it. It is deliberately not spelled :routes. :routes already means something else here - it is one of :persistence_options, where it is Statifier.Send.Routes, the engine's point-in-time claim about which <send> routes are live - and ADR-0005 decision 6 closed that ambiguity by vocabulary rather than by renaming the engine's. route/3 resolves a name in a scope.

:send_type is the one type string StatifierRouter.SendHandler is registered under. Giving it is what puts send_types: into :persistence_options, as the Statifier.Send.Types snapshot built from that type string and that handler (ADR-0005, decision 6); the registry cannot supply it, because a registry maps a route name to an adapter and holds no type string. A configuration that gives both :send_type and a :send_types of its own is refused with {:declared_send_types, send_type} rather than one silently winning.

:on_complete names one route in that same registry, and a name absent from :route_adapters is refused with {:unregistered_on_complete, name} rather than missed on the one delivery that would have used it. It is what StatifierRouter.Delivery hands an execution's donedata to on the delivery that finishes the execution; the README's "A finished execution reaches a sink" says what that delivery does and what it cannot do.

What the checks here do and do not catch

:store must be built over this configuration's own :repo, so that StatifierPersistence.Executions.create/4 and StatifierPersistence.Executions.step/5 write through the delivery's transaction rather than opening their own (ADR-0003, section 1). A store over another repo writes outside that transaction, and the delivery's rollback then leaves the execution behind. new/1 catches the case it can see: a store whose resolved adapter options carry a :repo that is not this configuration's is refused with {:error, {:invalid_value, :store, store}}. Those options are statifier_persistence's own, and only its Ecto storage resolves a :repo into them, so a store built on any other adapter passes this check without being checked. On such a store the rule is the host's to keep, and keeping it is not optional.

:resolver and :executor are checked to different depths, on purpose. StatifierRouter.Resolver is this package's own behaviour, so a resolver module is held to it: loadable and exporting resolve/2 (StatifierRouter.Resolver.valid?/1). StatifierPersistence.Executor is the dependency's, normalized per effect by StatifierPersistence.Executor.run/3, so an executor is checked for the shape that option takes - a module name or an arity-2 fun - and the dependency's own dispatch rule is left to it. A host that wants the deeper check on its executor gets it from statifier_persistence, not from here.

Each binding is built with StatifierRouter.Binding.new/1, in the order given, and the resolved configuration keeps that order: it is the order StatifierRouter.route/3 returns its outcomes in. A duplicate binding id is a fault of the list rather than of any one binding, so it is refused here, naming the duplicated id, before any event is routed (ADR-0001, section 1).

The four tables are the address table of ADR-0002 (addresses), the dedupe table of ADR-0003 (dedupe), the ledger of ADR-0004 (routing_ledger) and the subscription table of ADR-0007 (subscriptions); table/2 names each one under a configuration. StatifierRouter.Migrations creates them from the same two storage options, and put_meta/2 and queryable/2 point the schemas in StatifierRouter.Schema at them, so the DDL and the rows cannot disagree on a name.

iex> {:ok, config} =
...>   StatifierRouter.Config.new(
...>     repo: MyApp.Repo,
...>     delivery: MyApp.Delivery,
...>     prefix: "routing"
...>   )
iex> StatifierRouter.Config.table(config, :addresses)
"statifier_router_addresses"
iex> config.prefix
"routing"

Summary

Types

The host's compiled chart for a content hash an existing execution records, or :error when it has none: the shape StatifierPersistence.Driver's chart_resolver: takes.

Why new/1 refused a configuration.

The host's answer to which chart a new execution of document starts on, under scope (ADR-0002, section 4): a module implementing StatifierRouter.Resolver, or an arity-2 fun with its callback's signature.

t()

One of the four tables this package owns.

Functions

Validates the options in the table above and resolves them into a configuration.

Points a row of one of the StatifierRouter.Schema modules at this configuration's table and Postgres schema, so that Repo.insert/2 writes it there.

A query over one of the StatifierRouter.Schema modules that reads this configuration's table in its Postgres schema.

The adapter serving the route name under scope, with that scope's override applied over the adapter's registered configuration, or :error when the host registered no such route (ADR-0005, decision 2).

The name of table under this configuration: the table prefix followed by the table's own name.

Types

chart_resolver()

@type chart_resolver() :: (content_hash :: String.t() ->
                       {:ok, Statifier.Machine.t()} | :error)

The host's compiled chart for a content hash an existing execution records, or :error when it has none: the shape StatifierPersistence.Driver's chart_resolver: takes.

new_error()

@type new_error() ::
  {:unknown_key, term()}
  | {:missing_key, :repo | :store | :executor | :resolver | :chart_resolver}
  | {:invalid_value, atom(), term()}
  | {:invalid_config, term()}
  | {:binding, non_neg_integer(), StatifierRouter.Binding.new_error()}
  | {:duplicate_binding_id, String.t()}
  | {:unregistered_route, String.t(), String.t()}
  | {:declared_send_types, String.t()}
  | {:unregistered_on_complete, String.t()}
  | {:reserved_route, String.t()}
  | {:reserved_binding_id, String.t()}

Why new/1 refused a configuration.

resolver()

@type resolver() :: StatifierRouter.Resolver.t()

The host's answer to which chart a new execution of document starts on, under scope (ADR-0002, section 4): a module implementing StatifierRouter.Resolver, or an arity-2 fun with its callback's signature.

t()

@type t() :: %StatifierRouter.Config{
  bindings: [StatifierRouter.Binding.t()],
  chart_resolver: chart_resolver() | nil,
  delivery: module(),
  executor: StatifierPersistence.Executor.t() | nil,
  on_complete: String.t() | nil,
  persistence_options: keyword(),
  prefix: String.t() | nil,
  repo: module(),
  resolver: resolver() | nil,
  route_adapters: %{optional(String.t()) => StatifierRouter.Route.t()},
  route_overrides: %{optional(String.t()) => %{optional(String.t()) => map()}},
  send_type: String.t() | nil,
  store: StatifierPersistence.Storage.t() | nil,
  table_prefix: String.t(),
  timer_queue: StatifierRouter.TimerQueue.t() | nil
}

table()

@type table() :: :addresses | :dedupe | :routing_ledger | :subscriptions

One of the four tables this package owns.

Functions

new(opts)

@spec new(keyword()) :: {:ok, t()} | {:error, new_error()}

Validates the options in the table above and resolves them into a configuration.

Returns {:ok, config}, or {:error, reason} naming the first fault: an unknown option, then a missing or malformed :repo, then a malformed :delivery, then a missing or malformed :store, :executor, :resolver or :chart_resolver, in that order, then a :store whose adapter options name another repo, then a malformed :persistence_options, then a storage value the table does not allow, then the first binding StatifierRouter.Binding.new/1 refuses, as {:binding, index, reason} with index counted from zero, then a binding whose id is the reserved name, then the first duplicated binding id.

An :on_complete naming a route the host did not register is refused with {:unregistered_on_complete, name}, after the registry itself is resolved.

ADR-0006, section 1 reserves one name for the execution target, StatifierRouter.SendHandler.execution_target/0. A :route_adapters entry under it is refused with {:reserved_route, name}, after the registry's own shape is checked, and a binding whose id is it with {:reserved_binding_id, name}.

iex> StatifierRouter.Config.new(repo: MyApp.Repo, delivery: MyApp.Delivery, table_prefix: 7)
{:error, {:invalid_value, :table_prefix, 7}}
iex> StatifierRouter.Config.new(repo: MyApp.Repo)
{:error, {:missing_key, :store}}

put_meta(config, row)

@spec put_meta(t(), struct()) :: struct()

Points a row of one of the StatifierRouter.Schema modules at this configuration's table and Postgres schema, so that Repo.insert/2 writes it there.

queryable(config, schema)

@spec queryable(t(), module()) :: Ecto.Query.t()

A query over one of the StatifierRouter.Schema modules that reads this configuration's table in its Postgres schema.

route(config, scope, name)

@spec route(t(), String.t() | nil, String.t() | nil) ::
  {:ok, StatifierRouter.Route.t()} | :error

The adapter serving the route name under scope, with that scope's override applied over the adapter's registered configuration, or :error when the host registered no such route (ADR-0005, decision 2).

A scope overrides a route's configuration and never its existence, so a name absent from :route_adapters misses in every scope, and a scope of nil - a caller with no scope in reach - resolves the registered configuration unchanged.

table(config, table)

@spec table(t(), table()) :: String.t()

The name of table under this configuration: the table prefix followed by the table's own name.