Skip to main content

lib/constraints.ex

defmodule EnvGuard.Constraints do
  @moduledoc """
  Defines functions that can be used to check constraints on environment variables.
  """
  @type constraint :: EnvGuard.Types.constraint()
  @type type :: EnvGuard.Types.type()

  @spec check_constraints(any(), type, [constraint]) ::
          {:ok, any()} | {:error, :constraint_violation, String.t()}
  def check_constraints(value, type, constraints) do
    Enum.reduce_while(constraints, {:ok, value}, fn constraint, {:ok, value} ->
      # ...>   if x < 5 do
      #   ...>     {:cont, acc + x}
      #   ...>   else
      #   ...>     {:halt, acc}
      #   ...>   end
      case check(value, type, constraint) do
        {:ok, value} ->
          {:cont, {:ok, value}}

        err ->
          {:halt, err}
      end
    end)
  end

  @spec check(any(), type, constraint) ::
          {:ok, any()} | {:error, :constraint_violation, String.t()}

  def check(value, {:list, _type}, {:length, length}) do
    if Enum.count(value) == length do
      {:ok, value}
    else
      {:error, :constraint_violation, "List length is not equal to #{length}"}
    end
  end

  def check(value, {:list, _type}, {:min_length, length}) do
    if Enum.count(value) >= length do
      {:ok, value}
    else
      {:error, :constraint_violation, "List length is less than #{length}"}
    end
  end

  def check(value, {:list, _type}, {:max_length, length}) do
    if Enum.count(value) <= length do
      {:ok, value}
    else
      {:error, :constraint_violation, "List length is greater than #{length}"}
    end
  end

  def check(value, :integer, {:min, minimum}) do
    if value >= minimum do
      {:ok, value}
    else
      {:error, :constraint_violation, "integer is less than than #{minimum}"}
    end
  end

  def check(value, :integer, {:max, maximum}) do
    if value <= maximum do
      {:ok, value}
    else
      {:error, :constraint_violation, "integer is larger than than #{maximum}"}
    end
  end

  def check(value, :string, {:allow_empty?, allow_empty?}) do
    if trimmed_length(value) == 0 and not allow_empty? do
      {:error, :constraint_violation, "string is empty"}
    else
      {:ok, value}
    end
  end

  def check(value, :string, {:max_length, max}) do
    if trimmed_length(value) > max do
      {:error, :constraint_violation, "string is longer than #{max} characters"}
    else
      {:ok, value}
    end
  end

  def check(value, :string, {:min_length, min}) do
    if trimmed_length(value) < min do
      {:error, :constraint_violation, "string is shorter than #{min} characters"}
    else
      {:ok, value}
    end
  end

  def check(_value, type, constraint) do
    {:error, :constraint_violation,
     "constraint #{inspect(constraint)} not supported for type #{inspect(type)}"}
  end

  @spec trimmed_length(String.t()) :: non_neg_integer()
  defp trimmed_length(value) do
    value |> String.trim() |> String.length()
  end
end