Created
June 12, 2019 18:32
-
-
Save erikreedstrom/59cd0903452437fc2119bfd39b0f272c to your computer and use it in GitHub Desktop.
FunWithFlags toggle overlay for when having fun is not an option.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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