lib/plugs/validate_path_param.ex

defmodule Solicit.Plugs.ValidatePathParam do
  @moduledoc """
  Casts the given path param to the given type. If the cast fails, returns a 404.
  Uses Solicit for 404 responses, so only API routes are supported.
  """

  @typedoc """
  Plug options.

  - `:param` - the string name of the parameter to be cast. **Required.**
  - `:type` - the type to cast to using `Ecto.Type.cast/2`. **Required.**
  - `:required` - if true, requires the parameter to not be nil or an empty string after
                  casting. **Default:** `true`.
  """
  @type options :: [
          param: binary(),
          type: Ecto.Type.t(),
          required: boolean()
        ]

  @spec init(keyword()) :: keyword()
  def init(opts) do
    unless is_binary(Keyword.get(opts, :param)) do
      raise ":param is required and must be a string"
    end

    unless Keyword.has_key?(opts, :type) do
      raise ":type is required"
    end

    opts
  end

  @spec call(Plug.Conn.t(), keyword) :: Plug.Conn.t()
  def call(conn, opts) do
    param_name = Keyword.get(opts, :param)
    type = Keyword.get(opts, :type)
    required = Keyword.get(opts, :required, true)

    value = Map.get(conn.path_params, param_name)

    case Ecto.Type.cast(type, value) do
      {:ok, cast_value} ->
        if required && (is_nil(cast_value) || cast_value == "") do
          Solicit.Response.not_found(conn)
        else
          # don't update conn because changing path_params breaks spandex_phoenix
          conn
        end

      :error ->
        Solicit.Response.not_found(conn)
    end
  end
end