defmodule Bylaw.Db do
@moduledoc """
Runs database validation checks against explicit targets.
Adapter packages usually expose their own `validate/2` function and delegate
to this module after building `t:Bylaw.Db.Target.t/0` structs.
## Examples
iex> target = %Bylaw.Db.Target{adapter: MyApp.DbAdapter, meta: %{database: :primary}}
iex> Bylaw.Db.validate([target], [])
:ok
"""
alias Bylaw.CheckRunner
alias Bylaw.Db.Check
alias Bylaw.Db.Issue
alias Bylaw.Db.Target
@typedoc """
A check module with optional check-specific options.
"""
@type check_spec :: module() | {module(), keyword()}
@doc """
Runs `checks` against a non-empty list of targets.
Each check runs independently for each target. Returns `:ok` when every check
passes, or `{:error, issues}` with a non-empty list of
`t:Bylaw.Db.Issue.t/0` values.
Invalid target and check arguments raise `ArgumentError`.
## Examples
iex> Bylaw.Db.validate([], [])
** (ArgumentError) expected at least one database target
"""
@spec validate(targets :: list(Target.t()), checks :: list(check_spec())) :: Check.result()
def validate(targets, checks) do
checks = validate_checks!(checks)
targets
|> validate_targets!()
|> Enum.flat_map(&target_issues(&1, checks))
|> result()
end
defp target_issues(%Target{} = target, checks) do
Enum.flat_map(checks, &check_issues(target, &1))
end
defp target_issues(target, _checks) do
raise ArgumentError, "expected a database target, got: #{inspect(target)}"
end
defp validate_targets!([]), do: raise(ArgumentError, "expected at least one database target")
defp validate_targets!(targets) when is_list(targets), do: targets
defp validate_targets!(targets) do
raise ArgumentError, "expected database targets to be a list, got: #{inspect(targets)}"
end
defp validate_checks!(checks) when is_list(checks), do: checks
defp validate_checks!(checks) do
raise ArgumentError, "expected checks to be a list, got: #{inspect(checks)}"
end
defp check_issues(target, check_spec) do
{check, opts} = normalize_check!(check_spec)
result = check.validate(target, opts)
apply(CheckRunner, :result!, [check, result, Issue, 2])
end
defp normalize_check!({check, opts}) when is_atom(check) do
if is_list(opts) and Keyword.keyword?(opts) do
{check, opts}
else
raise ArgumentError, "expected check opts to be a keyword list, got: #{inspect(opts)}"
end
end
defp normalize_check!(check) when is_atom(check), do: {check, []}
defp normalize_check!(check) do
raise ArgumentError, "expected a check module or {check, opts}, got: #{inspect(check)}"
end
defp result([]), do: :ok
defp result(issues), do: {:error, issues}
end