Skip to content

Instantly share code, notes, and snippets.

@erikreedstrom
Created June 12, 2019 18:32
Show Gist options
  • Save erikreedstrom/59cd0903452437fc2119bfd39b0f272c to your computer and use it in GitHub Desktop.
Save erikreedstrom/59cd0903452437fc2119bfd39b0f272c to your computer and use it in GitHub Desktop.
FunWithFlags toggle overlay for when having fun is not an option.
defmodule WorkWithFlags do
@moduledoc """
Feature Toggle overlay for when having fun is not an option.
The main difference from the main lib is the precedence of gates
and how they combine.
In this implementation, we define Boolean > Actor > (Group & Percent).
The Boolean gate works as a master switch; requiring the flag be expressly
enabled for other gates to pass. Any Actor gate that is found will halt the
evaluation and resolve the returned value. Provided Boolean and Actor passed,
or Actor is ignored, Group and Percent gates are then evaluated together.
The same logic persists from the library: all Groups must pass to ensure
conflicts resolve properly, and % of Actor takes precedence over % of Time.
Granted, the logic after Actor is subtractive rather than additive. If
Group and Percent are defined, both must evaluate to true for the flag to enable.
"""
@default_store FunWithFlags.Config.store_module()
alias FunWithFlags.Flag
alias FunWithFlags.Gate
@store Application.get_env(:fun_with_flags, :store) || @default_store
@doc """
Checks if a flag is enabled.
It can be invoked with just the flag name, as an atom,
to check the general status of a flag (i.e. the boolean gate).
## Options
* `:for` - used to provide a term for which the flag could
have a specific value. The passed term should implement the
`Actor` or `Group` protocol, or both.
"""
@spec enabled?(atom, Flag.options()) :: boolean
def enabled?(flag_name, options \\ [])
def enabled?(flag_name, []) when is_atom(flag_name) do
case apply(@store, :lookup, [flag_name]) do
{:ok, flag} -> flag_enabled?(flag)
_ -> false
end
end
def enabled?(flag_name, for: nil) do
enabled?(flag_name)
end
def enabled?(flag_name, for: item) when is_atom(flag_name) do
case apply(@store, :lookup, [flag_name]) do
{:ok, flag} -> flag_enabled?(flag, for: item)
_ -> false
end
end
## PRIVATE FUNCTIONS
@spec flag_enabled?(Flag.t(), Flag.options()) :: boolean
defp flag_enabled?(flag, options \\ [])
defp flag_enabled?(%Flag{gates: []}, _), do: false
defp flag_enabled?(%Flag{gates: gates, name: flag_name}, opts) do
item = Keyword.get(opts, :for)
gates = gates_by_type(gates)
with {:flag_active, true} <- {:flag_active, check_boolean_gate(gates)},
{:actor_override, :cont} <- {:actor_override, check_actor_gates(gates, item)},
{:group_enabled, group_enabled} <- {:group_enabled, check_group_gates(gates, item)},
{:pct_enabled, pct_enabled} <- {:pct_enabled, check_percentage_gate(gates, item, flag_name)} do
group_enabled and pct_enabled
else
{:flag_active, false} -> false
{:actor_override, {:halt, is_enabled}} -> is_enabled
end
end
# Boolean gates override all, acting as a master switch
defp check_boolean_gate(%{boolean: gate}) do
{:ok, is_enabled} = Gate.enabled?(gate)
is_enabled
end
defp check_boolean_gate(_), do: false
# Actors override, so if actor is defined we break and return
defp check_actor_gates(%{actor: gates}, item) when not is_nil(item) do
Enum.reduce_while(gates, :cont, fn gate, acc ->
case Gate.enabled?(gate, for: item) do
{:ok, is_enabled} -> {:halt, {:halt, is_enabled}}
_ -> {:cont, acc}
end
end)
end
defp check_actor_gates(_, _), do: :cont
# If groups are defined and an item is passed, then the item must pass based on explicit perms.
defp check_group_gates(%{group: gates}, item) when not is_nil(item) do
by_type = Enum.group_by(gates, & &1.enabled)
enabled_gates = Map.get(by_type, true, [])
enabled =
Enum.reduce_while(enabled_gates, true, fn gate, acc ->
case Gate.enabled?(gate, for: item) do
{:ok, true} -> {:cont, acc}
_ -> {:halt, false}
end
end)
disabled_gates = Map.get(by_type, false, [])
not_disabled =
if Enum.empty?(disabled_gates) do
true
else
Enum.reduce_while(disabled_gates, true, fn gate, acc ->
case Gate.enabled?(gate, for: item) do
{:ok, false} -> {:halt, false}
_ -> {:cont, acc}
end
end)
end
enabled and not_disabled
end
# If there is no group defined, or no item, the group is ignored and the gate passes.
defp check_group_gates(_, _), do: true
# If % of Actors gate is defined, evaluate.
defp check_percentage_gate(%{pct_actor: gate}, item, flag_name) when not is_nil(item) do
{:ok, is_enabled} = Gate.enabled?(gate, for: item, flag_name: flag_name)
is_enabled
end
# If % of Time is defined, and no other patterns match, evaluate.
defp check_percentage_gate(%{pct_time: gate}, _, _) do
{:ok, is_enabled} = Gate.enabled?(gate)
is_enabled
end
# If no matches are found, ignore and pass the gate
defp check_percentage_gate(_, _, _), do: true
defp gates_by_type(gates) do
gates =
Enum.group_by(
gates,
fn gate ->
cond do
Gate.boolean?(gate) -> :boolean
Gate.actor?(gate) -> :actor
Gate.group?(gate) -> :group
Gate.percentage_of_time?(gate) -> :pct_time
Gate.percentage_of_actors?(gate) -> :pct_actor
end
end
)
gates = if gates[:boolean], do: Map.put(gates, :boolean, List.first(gates.boolean)), else: gates
gates = if gates[:pct_time], do: Map.put(gates, :pct_time, List.first(gates.pct_time)), else: gates
if gates[:pct_actor], do: Map.put(gates, :pct_actor, List.first(gates.pct_actor)), else: gates
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment