# `DDD.Specification`
[🔗](https://github.com/blackuprise/vicander/blob/v0.1.1/lib/ddd/model/specification.ex#L1)

Specification pattern: a named, reusable, composable business rule.

A specification is a pure function of an entity (no database access).

    defmodule MyApp.Specs.MaxOpenItems do
      use DDD.Specification, field: :items

      def check(%{items: items}, opts) do
        max = Keyword.fetch!(opts, :max)

        if Enum.count(items, &(&1.status == :open)) <= max,
          do: :ok,
          else: {:error, :too_many_open_items, "max #{max} open items", %{max: max}}
      end
    end

`check/2` returns `:ok`, `{:error, code, message}` or `{:error, code, message, details}`.
`use DDD.Specification` also derives `satisfied_by?/2`. `:field` (default `:base`) is the
field a failure is reported under.

## Value specs

A spec that judges one field's value is declared with `scope: :value` and bound to the field
where it is attached, so one spec serves any field and nothing relies on a naming convention:

    defmodule MyApp.Specs.ValidEmail do
      use DDD.Specification, scope: :value

      def check(email, _opts), do: ...      # the value, never nil
    end

    invariant MyApp.Specs.ValidEmail, on: :email

Failures are reported under the `on:` field. A missing `on:`, or a field the entity doesn't
have, is a compile error in `invariant` (and an `ArgumentError` when evaluated directly).

Entities attach specs two ways:

  * `invariant MyApp.Specs.MaxOpenItems, max: 10` - must hold after every change.
  * `defop name(entity, ...), requires: SomeSpec` - must hold *before* the operation may run
    (a precondition). The operation's other arguments arrive in `opts[:args]`, so a rule can
    depend on them: `def check(list, opts)` with `[item_id] = Keyword.fetch!(opts, :args)`.

## Composition

Specs combine with `all_of/1`, `any_of/1` and `not_spec/1`, which return
composite spec values accepted everywhere a spec module is:

    spec = DDD.Specification.all_of([NotBlank, {MaxLength, max: 100}])
    DDD.Specification.evaluate(spec, entity)

`all_of` returns the first failure, `any_of` returns the last failure when
every branch fails.

## Searching with a specification

A specification is a function of an entity, so the database can't run it. A spec that also
defines `to_filter/1` can be used to *find* the entities that satisfy it, by translating it
to a `DDD.Filter`:

    defmodule MyApp.Specs.Overdue do
      use DDD.Specification

      def check(%{due: due}, _opts), do: ...      # the in-memory rule

      @impl DDD.Specification
      def to_filter(_opts), do: {:due, {:lt, Date.utc_today()}}   # the same rule, as SQL
    end

    MyApp.TodoRepository.list(spec: DDD.Specification.all_of([Overdue, {Owned, owner: id}]))

`all_of`, `any_of` and `not_spec` translate to `{:and, _}`, `{:or, _}` and `{:not, _}`. Keep
`check/2` and `to_filter/1` in step; a test that runs both over the same rows is cheap.

# `result`

```elixir
@type result() ::
  :ok | {:error, atom(), String.t()} | {:error, atom(), String.t(), map()}
```

# `spec`

```elixir
@type spec() ::
  module()
  | {module(), keyword()}
  | {:all_of, [spec()]}
  | {:any_of, [spec()]}
  | {:not, spec()}
```

# `check`

```elixir
@callback check(entity_or_value :: term(), opts :: keyword()) :: result()
```

# `to_filter`
*optional* 

```elixir
@callback to_filter(opts :: keyword()) :: term()
```

The same rule as a `DDD.Filter`, so the database can search with it. Optional.

# `all_of`

```elixir
@spec all_of([spec()]) :: spec()
```

Satisfied only when every spec is satisfied.

# `any_of`

```elixir
@spec any_of([spec()]) :: spec()
```

Satisfied when at least one spec is satisfied.

# `evaluate`

```elixir
@spec evaluate(spec(), struct(), keyword()) :: :ok | {:error, DDD.Error.violation()}
```

Evaluate a spec against an entity.

Returns `:ok` or `{:error, violation}` where violation is
`%{field, code, message, details}`.

# `not_spec`

```elixir
@spec not_spec(spec()) :: spec()
```

Satisfied when the given spec is not.

# `to_filter`

```elixir
@spec to_filter(spec(), keyword()) :: term()
```

Translate a spec to a `DDD.Filter` expression (see "Searching with a specification").

Raises `ArgumentError` for a spec module without `to_filter/1`: it can be checked in memory
but not searched for.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
