lib/bylaw/db/adapters/postgres/ecto_changeset_constraint_options.ex

defmodule Bylaw.Db.Adapters.Postgres.EctoChangesetConstraintOptions do
  @moduledoc false

  alias Bylaw.Db.Adapters.Postgres.RuleOptions

  @allowed_keys [:validate, :otp_app, :paths, :schema_modules, :rules, :schemas, :tables]
  @allowed_matcher_keys [:schema, :table, :constraint, :column]

  @type t :: Keyword.t()

  @doc false
  @spec normalize!(target :: term(), opts :: term(), check :: atom()) :: t()
  def normalize!(target, opts, check) when is_list(opts) do
    opts = RuleOptions.keyword_list!(opts, check)

    RuleOptions.validate_allowed_keys!(opts, @allowed_keys, check)

    opts = maybe_put_repo_otp_app(target, opts)

    RuleOptions.validate_boolean_option!(opts, :validate, check)

    if RuleOptions.enabled?(opts) do
      RuleOptions.reject_top_level_keys_with_rules!(opts, [:schemas, :tables], check)
      validate_schema_discovery_opts!(opts, check)
      validate_required_option!(opts, :paths, check)
      validate_schema_modules_option!(opts, check)
      validate_paths_option!(opts, check)
      RuleOptions.default_rules!(opts, check, @allowed_matcher_keys)
      RuleOptions.filter(opts, :schemas, check)
      RuleOptions.filter(opts, :tables, check)
    end

    opts
  end

  def normalize!(_target, opts, check) do
    RuleOptions.keyword_list!(opts, check)
  end

  @doc false
  @spec allowed_matcher_keys() :: list(atom())
  def allowed_matcher_keys, do: @allowed_matcher_keys

  defp maybe_put_repo_otp_app(%{repo: repo}, opts) when is_atom(repo) and not is_nil(repo) do
    if Keyword.has_key?(opts, :otp_app) or Keyword.has_key?(opts, :schema_modules) do
      opts
    else
      case repo_otp_app(repo) do
        nil -> opts
        otp_app -> Keyword.put(opts, :otp_app, otp_app)
      end
    end
  end

  defp maybe_put_repo_otp_app(_target, opts), do: opts

  defp repo_otp_app(repo) do
    if Code.ensure_loaded?(repo) and function_exported?(repo, :config, 0) do
      repo.config()[:otp_app]
    end
  end

  defp validate_schema_discovery_opts!(opts, check) do
    if not Keyword.has_key?(opts, :otp_app) and not Keyword.has_key?(opts, :schema_modules) do
      raise ArgumentError, "expected #{check} opts to include :otp_app or :schema_modules"
    end
  end

  defp validate_required_option!(opts, key, check) do
    if not Keyword.has_key?(opts, key) do
      raise ArgumentError, "expected #{check} opts to include #{inspect(key)}"
    end
  end

  defp validate_schema_modules_option!(opts, check) do
    case Keyword.fetch(opts, :schema_modules) do
      {:ok, modules} when is_list(modules) ->
        if Enum.empty?(modules) or Enum.any?(modules, &(not is_atom(&1))) do
          raise_schema_modules_error!(check)
        end

      {:ok, _modules} ->
        raise_schema_modules_error!(check)

      :error ->
        :ok
    end
  end

  defp validate_paths_option!(opts, check) do
    case Keyword.fetch!(opts, :paths) do
      paths when is_list(paths) ->
        if Enum.empty?(paths) or Enum.any?(paths, &(not is_binary(&1))) do
          raise_paths_error!(check)
        end

      _paths ->
        raise_paths_error!(check)
    end
  end

  defp raise_paths_error!(check) do
    raise ArgumentError, "expected #{check} :paths to be a non-empty list of strings"
  end

  defp raise_schema_modules_error!(check) do
    raise ArgumentError, "expected #{check} :schema_modules to be a non-empty list of modules"
  end
end