Current section

Files

Jump to
fun_with_flags lib fun_with_flags gate.ex
Raw

lib/fun_with_flags/gate.ex

defmodule FunWithFlags.Gate do
@moduledoc false
alias FunWithFlags.{Actor, Group}
defmodule InvalidGroupNameError do
defexception [:message]
end
defmodule InvalidTargetError do
defexception [:message]
end
defstruct [:type, :for, :enabled]
@type t :: %FunWithFlags.Gate{type: atom, for: (nil | String.t), enabled: boolean}
@type options :: Keyword.t
def new(:boolean, enabled) when is_boolean(enabled) do
%__MODULE__{type: :boolean, for: nil, enabled: enabled}
end
# Don't accept 0 or 1 because a boolean gate should be used instead.
#
def new(:percentage_of_time, ratio)
when is_float(ratio) and ratio > 0 and ratio < 1 do
%__MODULE__{type: :percentage_of_time, for: ratio, enabled: true}
end
def new(:percentage_of_time, ratio)
when is_float(ratio) and ratio <= 0 or ratio >= 1 do
raise InvalidTargetError, "percentage_of_time gates must have a ratio in the range '0.0 < r < 1.0'."
end
def new(:percentage_of_actors, ratio)
when is_float(ratio) and ratio > 0 and ratio < 1 do
%__MODULE__{type: :percentage_of_actors, for: ratio, enabled: true}
end
def new(:percentage_of_actors, ratio)
when is_float(ratio) and ratio <= 0 or ratio >= 1 do
raise InvalidTargetError, "percentage_of_actors gates must have a ratio in the range '0.0 < r < 1.0'."
end
def new(:actor, actor, enabled) when is_boolean(enabled) do
%__MODULE__{type: :actor, for: Actor.id(actor), enabled: enabled}
end
def new(:group, group_name, enabled) when is_boolean(enabled) do
validate_group_name(group_name)
%__MODULE__{type: :group, for: to_string(group_name), enabled: enabled}
end
defp validate_group_name(name) when is_binary(name) or is_atom(name), do: nil
defp validate_group_name(name) do
raise InvalidGroupNameError, "invalid group name '#{inspect(name)}', it should be a binary or an atom."
end
def boolean?(%__MODULE__{type: :boolean}), do: true
def boolean?(%__MODULE__{type: _}), do: false
def actor?(%__MODULE__{type: :actor}), do: true
def actor?(%__MODULE__{type: _}), do: false
def group?(%__MODULE__{type: :group}), do: true
def group?(%__MODULE__{type: _}), do: false
def percentage_of_time?(%__MODULE__{type: :percentage_of_time}), do: true
def percentage_of_time?(%__MODULE__{type: _}), do: false
def percentage_of_actors?(%__MODULE__{type: :percentage_of_actors}), do: true
def percentage_of_actors?(%__MODULE__{type: _}), do: false
@spec enabled?(t, options) :: {:ok, boolean}
def enabled?(gate, options \\ [])
def enabled?(%__MODULE__{type: :boolean, enabled: enabled}, []) do
{:ok, enabled}
end
def enabled?(%__MODULE__{type: :boolean, enabled: enabled}, [for: _]) do
{:ok, enabled}
end
def enabled?(%__MODULE__{type: :actor, for: actor_id, enabled: enabled}, [for: actor]) do
case Actor.id(actor) do
^actor_id -> {:ok, enabled}
_ -> :ignore
end
end
def enabled?(%__MODULE__{type: :group, for: group, enabled: enabled}, [for: item]) do
if Group.in?(item, group) do
{:ok, enabled}
else
:ignore
end
end
def enabled?(%__MODULE__{type: :percentage_of_time, for: ratio}, _) do
roll = random_float()
enabled = roll <= ratio
{:ok, enabled}
end
def enabled?(%__MODULE__{type: :percentage_of_actors, for: ratio}, opts) do
actor = Keyword.fetch!(opts, :for)
flag_name = Keyword.fetch!(opts, :flag_name)
roll = Actor.score(actor, flag_name)
enabled = roll <= ratio
{:ok, enabled}
end
# Returns a float (4 digit precision) between 0.0 and 1.0
#
# Alternative:
# :crypto.rand_uniform(1, 10_000) / 10_000
#
defp random_float do
:rand.uniform(10_000) / 10_000
end
end