Skip to main content

lib/access_grid/params.ex

defmodule AccessGrid.Params do
  @moduledoc """
  Helpers for composing SDK request params. Currently just `require/2` for
  client-side required-field presence checks — used internally by `Console`
  and `AccessPasses` create/action functions to surface "you forgot X" without
  a round-trip to the API.
  """

  @doc """
  Returns `:ok` if every atom key in `required_keys` has a non-blank value
  in `params`. Returns `{:error, :missing_required, missing}` where `missing`
  is the non-empty list of every missing/blank key (in input order —
  deterministic).

  "Blank" means `nil`, key absent, an empty string `""`, or a whitespace-only
  string (`"   "`, `"\\t\\n"`, etc.). Empty lists, maps, and other values pass
  through — the server validates whether an empty collection is acceptable
  for a given field (e.g. the server rejects `subscribed_events: []` with its own
  clear 422 message).

  Atom-keyed maps only; mirrors the SDK's input convention.

  ## Examples

      iex> AccessGrid.Params.require(%{name: "X", protocol: "desfire"}, [:name, :protocol])
      :ok

      iex> AccessGrid.Params.require(%{name: "X"}, [:name, :protocol])
      {:error, :missing_required, [:protocol]}

      iex> AccessGrid.Params.require(%{}, [:name, :platform, :protocol])
      {:error, :missing_required, [:name, :platform, :protocol]}

      iex> AccessGrid.Params.require(%{name: "  "}, [:name])
      {:error, :missing_required, [:name]}

  """
  @spec require(map(), [atom()]) ::
          :ok | {:error, :missing_required, [atom(), ...]}
  def require(params, required_keys) when is_map(params) and is_list(required_keys) do
    case Enum.filter(required_keys, &blank?(Map.get(params, &1))) do
      [] -> :ok
      missing -> {:error, :missing_required, missing}
    end
  end

  @doc """
  Returns `:ok` if `value` is non-blank. Returns
  `{:error, :missing_required, [name]}` otherwise (single-element list for
  shape consistency with `require/2`). Used to validate positional arguments
  (path-segment IDs like `template_id`, `card_id`, etc.) where the SDK
  function takes a positional string rather than a key in a params map.

  ## Examples

      iex> AccessGrid.Params.require_present("tpl_abc", :template_id)
      :ok

      iex> AccessGrid.Params.require_present(nil, :template_id)
      {:error, :missing_required, [:template_id]}

      iex> AccessGrid.Params.require_present("  ", :template_id)
      {:error, :missing_required, [:template_id]}

  """
  @spec require_present(any(), atom()) ::
          :ok | {:error, :missing_required, [atom(), ...]}
  def require_present(value, name) when is_atom(name) do
    if blank?(value), do: {:error, :missing_required, [name]}, else: :ok
  end

  defp blank?(nil), do: true
  defp blank?(val) when is_binary(val), do: String.trim(val) == ""
  defp blank?(_), do: false
end