lib/chore_runner/input.ex

defmodule ChoreRunner.Input do
  @valid_types ~w(string int float file bool)a

  @type input_type :: :string | :int | :float | :file | :bool
  @type reason :: atom() | String.t()
  @type validator_function ::
          (term() -> {:ok, term()} | :ok | true | {:error, reason} | nil | false)
  @type input_options :: [
          validators: [validator_function],
          description: String.t()
        ]
  @type t :: {input_type, atom, input_options}

  defguard valid_type(type) when type in @valid_types

  for type <- @valid_types do
    @spec unquote(type)(atom(), input_options) :: t
    def unquote(type)(name, opts \\ []) do
      {unquote(type), name, opts}
    end
  end

  def types, do: @valid_types

  def validate_field(type, value) when valid_type(type) do
    do_validate(type, do_cast(value, type))
  end

  defp do_cast(value, :string), do: to_string(value)

  defp do_cast(value, :int) when is_binary(value) do
    case Integer.parse(value) do
      {int, _} -> int
      _ -> value
    end
  end

  defp do_cast(value, :float) when is_binary(value) do
    case Float.parse(value) do
      {float, _} -> float
      _ -> value
    end
  end

  defp do_cast(value, :bool) when is_binary(value) do
    case String.downcase(value) do
      "true" -> true
      "false" -> false
      _ -> value
    end
  end

  defp do_cast(value, :int) when is_integer(value), do: value
  defp do_cast(value, :float) when is_float(value), do: value
  defp do_cast(value, :bool) when is_boolean(value), do: value
  defp do_cast(value, _), do: value

  defp do_validate(:string, value) when is_binary(value), do: {:ok, value}
  defp do_validate(:int, value) when is_integer(value), do: {:ok, value}
  defp do_validate(:float, value) when is_float(value), do: {:ok, value}
  defp do_validate(:bool, value) when is_boolean(value), do: {:ok, value}
  defp do_validate(:file, %module{} = value) when module == Plug.Upload, do: {:ok, value}

  defp do_validate(:file, path) when is_binary(path) do
    if File.exists?(path), do: {:ok, path}, else: {:error, :does_not_exist}
  end

  defp do_validate(_, _), do: {:error, :invalid}
end