Skip to main content

lib/cast.ex

defmodule EnvGuard.Cast do
  @moduledoc """
  Defines functions that cast values to a specific type.
  """

  @type type :: EnvGuard.Types.type()

  @doc """
  Takes in a string value and casts it to the specified type.

  Returns an error if the cast is not possible.
  """
  @spec cast(any(), type) :: {:ok, any()} | {:error, :cast_fail, String.t()}
  def cast(value, :boolean) when is_binary(value) do
    cond do
      value in ["true", "TRUE", "1"] -> {:ok, true}
      value in ["false", "FALSE", "0"] -> {:ok, false}
      true -> {:error, :cast_fail, "boolean expected, got '#{value}'."}
    end
  end

  def cast(value, :string) when is_binary(value) do
    {:ok, value}
  end

  def cast(value, :atom) when is_binary(value) do
    {:ok, String.to_atom(value)}
  end

  def cast(value, :charlist) when is_binary(value) do
    {:ok, to_charlist(value)}
  end

  def cast(value, :integer) when is_binary(value) do
    case Integer.parse(value) do
      {int, ""} -> {:ok, int}
      _ -> {:error, :cast_fail, "integer expected, got '#{value}'"}
    end
  end

  def cast(value, :float) when is_binary(value) do
    case Float.parse(value) do
      {float, ""} -> {:ok, float}
      _ -> {:error, :cast_fail, "float expected, got '#{value}'"}
    end
  end

  def cast(value, {:list, type}) when is_binary(value) do
    value
    |> String.trim()
    |> String.split(",")
    |> case do
      [""] -> {:ok, []}
      list -> cast_list(list, type)
    end
  end

  def cast(value, {:enum, options}) when is_binary(value) do
    if value in options do
      {:ok, value}
    else
      {:error, :cast_fail, "'#{value}' not in enum #{inspect(options)}"}
    end
  end

  def cast(value, type) when not is_binary(value) do
    {:error, :cast_fail, "binary expected for #{inspect(type)}, got #{inspect(value)}"}
  end

  @spec cast_list([String.t()], type) :: {:ok, [any()]} | {:error, :cast_fail, String.t()}
  defp cast_list(values, type) do
    values
    |> Enum.reduce_while({:ok, []}, fn value, {:ok, acc} ->
      case cast(value, type) do
        {:ok, casted} -> {:cont, {:ok, [casted | acc]}}
        error -> {:halt, error}
      end
    end)
    |> case do
      {:ok, list} -> {:ok, Enum.reverse(list)}
      error -> error
    end
  end
end