diff --git a/.credo.exs b/.credo.exs index 6f868f8..4b72f90 100644 --- a/.credo.exs +++ b/.credo.exs @@ -125,7 +125,7 @@ {Credo.Check.Refactor.FilterCount, []}, {Credo.Check.Refactor.FilterFilter, []}, {Credo.Check.Refactor.FunctionArity, []}, - {Credo.Check.Refactor.LongQuoteBlocks, []}, + # {Credo.Check.Refactor.LongQuoteBlocks, []}, {Credo.Check.Refactor.MapJoin, []}, {Credo.Check.Refactor.MatchInCondition, []}, {Credo.Check.Refactor.NegatedConditionsInUnless, []}, diff --git a/lib/fun_with_flags.ex b/lib/fun_with_flags.ex index 7987875..da64c85 100644 --- a/lib/fun_with_flags.ex +++ b/lib/fun_with_flags.ex @@ -2,526 +2,10 @@ defmodule FunWithFlags do @moduledoc """ FunWithFlags, the Elixir feature flag library. - This module provides the public interface to the library and its API is - made of three simple methods to enable, disable and query feature flags. - - In their simplest form, flags can be toggled on and off globally. - - More advanced rules or "gates" are available, and they can be set and queried - for any term that implements these protocols: - - * The `FunWithFlags.Actor` protocol can be - implemented for types and structs that should have specific rules. For - example, in web applications it's common to use a `%User{}` struct or - equivalent as an actor, or perhaps the current country of the request. - - * The `FunWithFlags.Group` protocol can be - implemented for types and structs that should belong to groups for which - one wants to enable and disable some flags. For example, one could implement - the protocol for a `%User{}` struct to identify administrators. - - - See the [Usage](/fun_with_flags/readme.html#usage) notes for a more detailed - explanation. - """ - - alias FunWithFlags.{Config, Flag, Gate} - - @store FunWithFlags.Config.store_module_determined_at_compile_time() - - @type options :: Keyword.t - - - @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. - - ## Examples - - This example relies on the [reference implementation](https://github.com/tompave/fun_with_flags/blob/master/test/support/test_user.ex) - used in the tests. - - iex> alias FunWithFlags.TestUser, as: User - iex> harry = %User{id: 1, name: "Harry Potter", groups: ["wizards", "gryffindor"]} - iex> FunWithFlags.disable(:elder_wand) - iex> FunWithFlags.enable(:elder_wand, for_actor: harry) - iex> FunWithFlags.enabled?(:elder_wand) - false - iex> FunWithFlags.enabled?(:elder_wand, for: harry) - true - iex> voldemort = %User{id: 7, name: "Tom Riddle", groups: ["wizards", "slytherin"]} - iex> FunWithFlags.enabled?(:elder_wand, for: voldemort) - false - iex> filch = %User{id: 88, name: "Argus Filch", groups: ["staff"]} - iex> FunWithFlags.enable(:magic_wands, for_group: "wizards") - iex> FunWithFlags.enabled?(:magic_wands, for: harry) - true - iex> FunWithFlags.enabled?(:magic_wands, for: voldemort) - true - iex> FunWithFlags.enabled?(:magic_wands, for: filch) - false + This module is the legacy interface from v1.x. + See the [Usage](/fun_with_flags/readme.html#usage). """ - @spec enabled?(atom, options) :: boolean - def enabled?(flag_name, options \\ []) - - def enabled?(flag_name, []) when is_atom(flag_name) do - {:ok, flag} = @store.lookup(flag_name) - Flag.enabled?(flag) - end - - def enabled?(flag_name, [for: nil]) do - enabled?(flag_name) - end - - def enabled?(flag_name, [for: item]) when is_atom(flag_name) do - {:ok, flag} = @store.lookup(flag_name) - Flag.enabled?(flag, for: item) - end - - - @doc """ - Enables a feature flag. - - ## Options - - * `:for_actor` - used to enable the flag for a specific term only. - The value can be any term that implements the `Actor` protocol. - * `:for_group` - used to enable the flag for a specific group only. - The value should be a binary or an atom (It's internally converted - to a binary and it's stored and retrieved as a binary. Atoms are - supported for retro-compatibility with versions <= 0.9) - * `:for_percentage_of` - used to enable the flag for a percentage - of time or actors, expressed as `{:time, float}` or `{:actors, float}`, - where float is in the range `0.0 < x < 1.0`. - - ## Examples - - ### Enable globally - - iex> FunWithFlags.enabled?(:super_shrink_ray) - false - iex> FunWithFlags.enable(:super_shrink_ray) - {:ok, true} - iex> FunWithFlags.enabled?(:super_shrink_ray) - true - - ### Enable for an actor - - iex> FunWithFlags.disable(:warp_drive) - {:ok, false} - iex> FunWithFlags.enable(:warp_drive, for_actor: "Scotty") - {:ok, true} - iex> FunWithFlags.enabled?(:warp_drive) - false - iex> FunWithFlags.enabled?(:warp_drive, for: "Scotty") - true - - ### Enable for a group - - This example relies on the [reference implementation](https://github.com/tompave/fun_with_flags/blob/master/test/support/test_user.ex) - used in the tests. - - iex> alias FunWithFlags.TestUser, as: User - iex> marty = %User{name: "Marty McFly", groups: ["students", "time_travelers"]} - iex> doc = %User{name: "Emmet Brown", groups: ["scientists", "time_travelers"]} - iex> buford = %User{name: "Buford Tannen", groups: ["gunmen", "bandits"]} - iex> FunWithFlags.enable(:delorean, for_group: "time_travelers") - {:ok, true} - iex> FunWithFlags.enabled?(:delorean) - false - iex> FunWithFlags.enabled?(:delorean, for: buford) - false - iex> FunWithFlags.enabled?(:delorean, for: marty) - true - iex> FunWithFlags.enabled?(:delorean, for: doc) - true - - - ### Enable for a percentage of the time - - iex> FunWithFlags.disable(:random_glitch) - iex> FunWithFlags.enable(:random_glitch, for_percentage_of: {:time, 0.999999999}) - iex> FunWithFlags.enabled?(:random_glitch) - true - iex> FunWithFlags.enable(:random_glitch, for_percentage_of: {:time, 0.000000001}) - iex> FunWithFlags.enabled?(:random_glitch) - false - - ### Enable for a percentage of the actors - - This example is based on the fact that the actor score for the actor-flag pair - `marty + :new_ui` is lower than 50%, and for the `buford + :new_ui` is higher. - - iex> FunWithFlags.disable(:new_ui) - iex> FunWithFlags.enable(:new_ui, for_percentage_of: {:actors, 0.5}) - iex> FunWithFlags.enabled?(:new_ui) - false - iex> alias FunWithFlags.TestUser, as: User - iex> marty = %User{id: 42, name: "Marty McFly"} - iex> buford = %User{id: 2, name: "Buford Tannen"} - iex> FunWithFlags.enabled?(:new_ui, for: marty) - true - iex> FunWithFlags.enabled?(:new_ui, for: buford) - false - - """ - @spec enable(atom, options) :: {:ok, true} | {:error, any} - def enable(flag_name, options \\ []) - - def enable(flag_name, []) when is_atom(flag_name) do - gate = Gate.new(:boolean, true) - case @store.put(flag_name, gate) do - {:ok, flag} -> verify(flag) - error -> error - end - end - - def enable(flag_name, [for_actor: nil]) do - enable(flag_name) - end - - def enable(flag_name, [for_actor: actor]) when is_atom(flag_name) do - gate = Gate.new(:actor, actor, true) - case @store.put(flag_name, gate) do - {:ok, flag} -> verify(flag, for: actor) - error -> error - end - end - - - def enable(flag_name, [for_group: nil]) do - enable(flag_name) - end - - def enable(flag_name, [for_group: group_name]) when is_atom(flag_name) do - gate = Gate.new(:group, group_name, true) - case @store.put(flag_name, gate) do - {:ok, _flag} -> {:ok, true} - error -> error - end - end - - - def enable(flag_name, [for_percentage_of: {:time, ratio}]) when is_atom(flag_name) do - gate = Gate.new(:percentage_of_time, ratio) - case @store.put(flag_name, gate) do - {:ok, _flag} -> {:ok, true} - error -> error - end - end - - def enable(flag_name, [for_percentage_of: {:actors, ratio}]) when is_atom(flag_name) do - gate = Gate.new(:percentage_of_actors, ratio) - case @store.put(flag_name, gate) do - {:ok, _flag} -> {:ok, true} - error -> error - end - end - - - @doc """ - Disables a feature flag. - - ## Options - - * `:for_actor` - used to disable the flag for a specific term only. - The value can be any term that implements the `Actor` protocol. - * `:for_group` - used to disable the flag for a specific group only. - The value should be a binary or an atom (It's internally converted - to a binary and it's stored and retrieved as a binary. Atoms are - supported for retro-compatibility with versions <= 0.9) - * `:for_percentage_of` - used to disable the flag for a percentage - of time or actors, expressed as `{:time, float}` or `{:actors, float}`, - where float is in the range `0.0 < x < 1.0`. - - ## Examples - - ### Disable globally - - iex> FunWithFlags.enable(:random_koala_gifs) - iex> FunWithFlags.enabled?(:random_koala_gifs) - true - iex> FunWithFlags.disable(:random_koala_gifs) - {:ok, false} - iex> FunWithFlags.enabled?(:random_koala_gifs) - false - - - ## Disable for an actor - - iex> FunWithFlags.enable(:spider_sense) - {:ok, true} - iex> villain = %{name: "Venom"} - iex> FunWithFlags.disable(:spider_sense, for_actor: villain) - {:ok, false} - iex> FunWithFlags.enabled?(:spider_sense) - true - iex> FunWithFlags.enabled?(:spider_sense, for: villain) - false - - ### Disable for a group - - This example relies on the [reference implementation](https://github.com/tompave/fun_with_flags/blob/master/test/support/test_user.ex) - used in the tests. - - iex> alias FunWithFlags.TestUser, as: User - iex> harry = %User{name: "Harry Potter", groups: ["wizards", "gryffindor"]} - iex> dudley = %User{name: "Dudley Dursley", groups: ["muggles"]} - iex> FunWithFlags.enable(:hogwarts) - {:ok, true} - iex> FunWithFlags.disable(:hogwarts, for_group: "muggles") - {:ok, false} - iex> FunWithFlags.enabled?(:hogwarts) - true - iex> FunWithFlags.enabled?(:hogwarts, for: harry) - true - iex> FunWithFlags.enabled?(:hogwarts, for: dudley) - false - - ### Disable for a percentage of the time - - iex> FunWithFlags.clear(:random_glitch) - :ok - iex> FunWithFlags.disable(:random_glitch, for_percentage_of: {:time, 0.999999999}) - {:ok, false} - iex> FunWithFlags.enabled?(:random_glitch) - false - iex> FunWithFlags.disable(:random_glitch, for_percentage_of: {:time, 0.000000001}) - {:ok, false} - iex> FunWithFlags.enabled?(:random_glitch) - true - - ### Disable for a percentage of the actors - - iex> FunWithFlags.disable(:new_ui, for_percentage_of: {:actors, 0.3}) - {:ok, false} - - """ - @spec disable(atom, options) :: {:ok, boolean()} | {:error, any} - def disable(flag_name, options \\ []) - - def disable(flag_name, []) when is_atom(flag_name) do - gate = Gate.new(:boolean, false) - case @store.put(flag_name, gate) do - {:ok, flag} -> verify(flag) - error -> error - end - end - - def disable(flag_name, [for_actor: nil]) do - disable(flag_name) - end - - def disable(flag_name, [for_actor: actor]) when is_atom(flag_name) do - gate = Gate.new(:actor, actor, false) - case @store.put(flag_name, gate) do - {:ok, flag} -> verify(flag, for: actor) - error -> error - end - end - - def disable(flag_name, [for_group: nil]) do - disable(flag_name) - end - - def disable(flag_name, [for_group: group_name]) when is_atom(flag_name) do - gate = Gate.new(:group, group_name, false) - case @store.put(flag_name, gate) do - {:ok, _flag} -> {:ok, false} - error -> error - end - end - - - def disable(flag_name, [for_percentage_of: {type, ratio}]) - when is_atom(flag_name) and is_float(ratio) do - inverted_ratio = 1.0 - ratio - case enable(flag_name, [for_percentage_of: {type, inverted_ratio}]) do - {:ok, true} -> {:ok, false} - error -> error - end - end - - - @doc """ - Clears the data of a feature flag. - - Clears the data for an entire feature flag or for a specific - Actor or Group gate. Clearing a boolean gate is not supported - because a missing boolean gate is equivalent to a disabled boolean - gate. - - Sometimes enabling or disabling a gate is not what you want, and you - need to remove that gate's rules instead. For example, if you don't need - anymore to explicitly enable or disable a flag for an actor, and the - default state should be used instead, you'll want to clear the gate. - - It's also possible to clear the entire flag, by not passing any option. - - ## Options - - * `for_actor: an_actor` - used to clear the flag for a specific term only. - The value can be any term that implements the `Actor` protocol. - * `for_group: a_group_name` - used to clear the flag for a specific group only. - The value should be a binary or an atom (It's internally converted - to a binary and it's stored and retrieved as a binary. Atoms are - supported for retro-compatibility with versions <= 0.9) - * `boolean: true` - used to clear the boolean gate. - * `for_percentage: true` - used to clear any percentage gate. - - ## Examples - - iex> alias FunWithFlags.TestUser, as: User - iex> harry = %User{id: 1, name: "Harry Potter", groups: ["wizards", "gryffindor"]} - iex> hagrid = %User{id: 2, name: "Rubeus Hagrid", groups: ["wizards", "gamekeeper"]} - iex> dudley = %User{id: 3, name: "Dudley Dursley", groups: ["muggles"]} - iex> FunWithFlags.disable(:wands) - iex> FunWithFlags.enable(:wands, for_group: "wizards") - iex> FunWithFlags.disable(:wands, for_actor: hagrid) - iex> - iex> FunWithFlags.enabled?(:wands) - false - iex> FunWithFlags.enabled?(:wands, for: harry) - true - iex> FunWithFlags.enabled?(:wands, for: hagrid) - false - iex> FunWithFlags.enabled?(:wands, for: dudley) - false - iex> - iex> FunWithFlags.clear(:wands, for_actor: hagrid) - :ok - iex> FunWithFlags.enabled?(:wands, for: hagrid) - true - iex> - iex> FunWithFlags.clear(:wands) - :ok - iex> FunWithFlags.enabled?(:wands) - false - iex> FunWithFlags.enabled?(:wands, for: harry) - false - iex> FunWithFlags.enabled?(:wands, for: hagrid) - false - iex> FunWithFlags.enabled?(:wands, for: dudley) - false - - """ - @spec clear(atom, options) :: :ok | {:error, any} - def clear(flag_name, options \\ []) - - def clear(flag_name, []) when is_atom(flag_name) do - case @store.delete(flag_name) do - {:ok, _flag} -> :ok - error -> error - end - end - - def clear(flag_name, [boolean: true]) do - gate = Gate.new(:boolean, false) # we only care about the gate id - _clear_gate(flag_name, gate) - end - - def clear(flag_name, [for_actor: nil]) do - clear(flag_name) - end - - def clear(flag_name, [for_actor: actor]) when is_atom(flag_name) do - gate = Gate.new(:actor, actor, false) # we only care about the gate id - _clear_gate(flag_name, gate) - end - - def clear(flag_name, [for_group: nil]) do - clear(flag_name) - end - - def clear(flag_name, [for_group: group_name]) when is_atom(flag_name) do - gate = Gate.new(:group, group_name, false) # we only care about the gate id - _clear_gate(flag_name, gate) - end - - def clear(flag_name, [for_percentage: true]) do - gate = Gate.new(:percentage_of_time, 0.5) # we only care about the gate id - _clear_gate(flag_name, gate) - end - - defp _clear_gate(flag_name, gate) do - case @store.delete(flag_name, gate) do - {:ok, _flag} -> :ok - error -> error - end - end - - - @doc """ - Returns a list of all flag names currently configured, as atoms. - - This can be useful for debugging or for display purposes, - but it's not meant to be used at runtime. Undefined flags, - for example, will be considered disabled. - """ - @spec all_flag_names() :: {:ok, [atom]} | {:error, any} - def all_flag_names do - Config.persistence_adapter().all_flag_names() - end - - @doc """ - Returns a list of all the flags currently configured, as data structures. - - This function is provided for debugging and to build more complex - functionality (e.g. it's used in the web GUI), but it is not meant to be - used at runtime to check if a flag is enabled. - - To query the value of a flag, please use the `enabled?2` function instead. - """ - @spec all_flags() :: {:ok, [FunWithFlags.Flag.t]} | {:error, any} - def all_flags do - Config.persistence_adapter().all_flags() - end - - - @doc """ - Returns a `FunWithFlags.Flag` struct for the given name, or `nil` if - no flag is found. - - Useful for debugging. - """ - @spec get_flag(atom) :: FunWithFlags.Flag.t | nil | {:error, any} - def get_flag(name) do - case all_flag_names() do - {:ok, names} -> - if name in names do - case Config.persistence_adapter().get(name) do - {:ok, flag} -> flag - error -> error - end - else - nil - end - error -> error - end - end - - - defp verify(flag) do - {:ok, Flag.enabled?(flag)} - end - defp verify(flag, [for: data]) do - {:ok, Flag.enabled?(flag, for: data)} - end - # Used in some tests and to debug. - # - # Apparently calling `Config.store_module_determined_at_compile_time` even - # just once in a test causes all sorts of weird test failures everywhere. - # - @doc false - def compiled_store, do: @store + use FunWithFlags.EntryPoint end diff --git a/lib/fun_with_flags/entry_point.ex b/lib/fun_with_flags/entry_point.ex new file mode 100644 index 0000000..d4e3196 --- /dev/null +++ b/lib/fun_with_flags/entry_point.ex @@ -0,0 +1,544 @@ +defmodule FunWithFlags.EntryPoint do + @moduledoc """ + Defines an entry point to query feature flags. + + An entry point provides the public interface to the library. Its API + consists of simple functions to enable, disable and query feature flags. + + In order to define an entry point, use this module inside your custom module: + + ## Example + + defmodule MyApp.Flags do + use FunWithFlags.EntryPoint + + @impl true + def config do + {:ok, [...]} + end + end + + {:ok, true} = MyApp.Flags.enabled?(:my_named_flag) + + + In their simplest form, flags can be toggled on and off globally. + + More advanced rules or "gates" are available, and they can be set and queried + for any term that implements these protocols: + + * The `FunWithFlags.Actor` protocol can be + implemented for types and structs that should have specific rules. For + example, in web applications it's common to use a `%User{}` struct or + equivalent as an actor, or perhaps the current country of the request. + + * The `FunWithFlags.Group` protocol can be + implemented for types and structs that should belong to groups for which + one wants to enable and disable some flags. For example, one could implement + the protocol for a `%User{}` struct to identify administrators. + + See the [Usage](/fun_with_flags/readme.html#usage) notes for a more detailed + explanation. + """ + + @doc false + defmacro __using__(opts) do + quote bind_quoted: [opts: opts] do + alias FunWithFlags.{Config, Flag, Gate} + + @store FunWithFlags.Config.store_module_determined_at_compile_time() + + @type options :: Keyword.t + + @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. + + ## Examples + + This example relies on the [reference implementation](https://github.com/tompave/fun_with_flags/blob/master/test/support/test_user.ex) + used in the tests. + + iex> alias FunWithFlags.TestUser, as: User + iex> harry = %User{id: 1, name: "Harry Potter", groups: ["wizards", "gryffindor"]} + iex> FunWithFlags.disable(:elder_wand) + iex> FunWithFlags.enable(:elder_wand, for_actor: harry) + iex> FunWithFlags.enabled?(:elder_wand) + false + iex> FunWithFlags.enabled?(:elder_wand, for: harry) + true + iex> voldemort = %User{id: 7, name: "Tom Riddle", groups: ["wizards", "slytherin"]} + iex> FunWithFlags.enabled?(:elder_wand, for: voldemort) + false + iex> filch = %User{id: 88, name: "Argus Filch", groups: ["staff"]} + iex> FunWithFlags.enable(:magic_wands, for_group: "wizards") + iex> FunWithFlags.enabled?(:magic_wands, for: harry) + true + iex> FunWithFlags.enabled?(:magic_wands, for: voldemort) + true + iex> FunWithFlags.enabled?(:magic_wands, for: filch) + false + + """ + @spec enabled?(atom, options) :: boolean + def enabled?(flag_name, options \\ []) + + def enabled?(flag_name, []) when is_atom(flag_name) do + {:ok, flag} = @store.lookup(flag_name) + Flag.enabled?(flag) + end + + def enabled?(flag_name, [for: nil]) do + enabled?(flag_name) + end + + def enabled?(flag_name, [for: item]) when is_atom(flag_name) do + {:ok, flag} = @store.lookup(flag_name) + Flag.enabled?(flag, for: item) + end + + @doc """ + Enables a feature flag. + + ## Options + + * `:for_actor` - used to enable the flag for a specific term only. + The value can be any term that implements the `Actor` protocol. + * `:for_group` - used to enable the flag for a specific group only. + The value should be a binary or an atom (It's internally converted + to a binary and it's stored and retrieved as a binary. Atoms are + supported for retro-compatibility with versions <= 0.9) + * `:for_percentage_of` - used to enable the flag for a percentage + of time or actors, expressed as `{:time, float}` or `{:actors, float}`, + where float is in the range `0.0 < x < 1.0`. + + ## Examples + + ### Enable globally + + iex> FunWithFlags.enabled?(:super_shrink_ray) + false + iex> FunWithFlags.enable(:super_shrink_ray) + {:ok, true} + iex> FunWithFlags.enabled?(:super_shrink_ray) + true + + ### Enable for an actor + + iex> FunWithFlags.disable(:warp_drive) + {:ok, false} + iex> FunWithFlags.enable(:warp_drive, for_actor: "Scotty") + {:ok, true} + iex> FunWithFlags.enabled?(:warp_drive) + false + iex> FunWithFlags.enabled?(:warp_drive, for: "Scotty") + true + + ### Enable for a group + + This example relies on the [reference implementation](https://github.com/tompave/fun_with_flags/blob/master/test/support/test_user.ex) + used in the tests. + + iex> alias FunWithFlags.TestUser, as: User + iex> marty = %User{name: "Marty McFly", groups: ["students", "time_travelers"]} + iex> doc = %User{name: "Emmet Brown", groups: ["scientists", "time_travelers"]} + iex> buford = %User{name: "Buford Tannen", groups: ["gunmen", "bandits"]} + iex> FunWithFlags.enable(:delorean, for_group: "time_travelers") + {:ok, true} + iex> FunWithFlags.enabled?(:delorean) + false + iex> FunWithFlags.enabled?(:delorean, for: buford) + false + iex> FunWithFlags.enabled?(:delorean, for: marty) + true + iex> FunWithFlags.enabled?(:delorean, for: doc) + true + + + ### Enable for a percentage of the time + + iex> FunWithFlags.disable(:random_glitch) + iex> FunWithFlags.enable(:random_glitch, for_percentage_of: {:time, 0.999999999}) + iex> FunWithFlags.enabled?(:random_glitch) + true + iex> FunWithFlags.enable(:random_glitch, for_percentage_of: {:time, 0.000000001}) + iex> FunWithFlags.enabled?(:random_glitch) + false + + ### Enable for a percentage of the actors + + This example is based on the fact that the actor score for the actor-flag pair + `marty + :new_ui` is lower than 50%, and for the `buford + :new_ui` is higher. + + iex> FunWithFlags.disable(:new_ui) + iex> FunWithFlags.enable(:new_ui, for_percentage_of: {:actors, 0.5}) + iex> FunWithFlags.enabled?(:new_ui) + false + iex> alias FunWithFlags.TestUser, as: User + iex> marty = %User{id: 42, name: "Marty McFly"} + iex> buford = %User{id: 2, name: "Buford Tannen"} + iex> FunWithFlags.enabled?(:new_ui, for: marty) + true + iex> FunWithFlags.enabled?(:new_ui, for: buford) + false + + """ + @spec enable(atom, options) :: {:ok, true} | {:error, any} + def enable(flag_name, options \\ []) + + def enable(flag_name, []) when is_atom(flag_name) do + gate = Gate.new(:boolean, true) + case @store.put(flag_name, gate) do + {:ok, flag} -> verify(flag) + error -> error + end + end + + def enable(flag_name, [for_actor: nil]) do + enable(flag_name) + end + + def enable(flag_name, [for_actor: actor]) when is_atom(flag_name) do + gate = Gate.new(:actor, actor, true) + case @store.put(flag_name, gate) do + {:ok, flag} -> verify(flag, for: actor) + error -> error + end + end + + + def enable(flag_name, [for_group: nil]) do + enable(flag_name) + end + + def enable(flag_name, [for_group: group_name]) when is_atom(flag_name) do + gate = Gate.new(:group, group_name, true) + case @store.put(flag_name, gate) do + {:ok, _flag} -> {:ok, true} + error -> error + end + end + + + def enable(flag_name, [for_percentage_of: {:time, ratio}]) when is_atom(flag_name) do + gate = Gate.new(:percentage_of_time, ratio) + case @store.put(flag_name, gate) do + {:ok, _flag} -> {:ok, true} + error -> error + end + end + + def enable(flag_name, [for_percentage_of: {:actors, ratio}]) when is_atom(flag_name) do + gate = Gate.new(:percentage_of_actors, ratio) + case @store.put(flag_name, gate) do + {:ok, _flag} -> {:ok, true} + error -> error + end + end + + @doc """ + Disables a feature flag. + + ## Options + + * `:for_actor` - used to disable the flag for a specific term only. + The value can be any term that implements the `Actor` protocol. + * `:for_group` - used to disable the flag for a specific group only. + The value should be a binary or an atom (It's internally converted + to a binary and it's stored and retrieved as a binary. Atoms are + supported for retro-compatibility with versions <= 0.9) + * `:for_percentage_of` - used to disable the flag for a percentage + of time or actors, expressed as `{:time, float}` or `{:actors, float}`, + where float is in the range `0.0 < x < 1.0`. + + ## Examples + + ### Disable globally + + iex> FunWithFlags.enable(:random_koala_gifs) + iex> FunWithFlags.enabled?(:random_koala_gifs) + true + iex> FunWithFlags.disable(:random_koala_gifs) + {:ok, false} + iex> FunWithFlags.enabled?(:random_koala_gifs) + false + + + ## Disable for an actor + + iex> FunWithFlags.enable(:spider_sense) + {:ok, true} + iex> villain = %{name: "Venom"} + iex> FunWithFlags.disable(:spider_sense, for_actor: villain) + {:ok, false} + iex> FunWithFlags.enabled?(:spider_sense) + true + iex> FunWithFlags.enabled?(:spider_sense, for: villain) + false + + ### Disable for a group + + This example relies on the [reference implementation](https://github.com/tompave/fun_with_flags/blob/master/test/support/test_user.ex) + used in the tests. + + iex> alias FunWithFlags.TestUser, as: User + iex> harry = %User{name: "Harry Potter", groups: ["wizards", "gryffindor"]} + iex> dudley = %User{name: "Dudley Dursley", groups: ["muggles"]} + iex> FunWithFlags.enable(:hogwarts) + {:ok, true} + iex> FunWithFlags.disable(:hogwarts, for_group: "muggles") + {:ok, false} + iex> FunWithFlags.enabled?(:hogwarts) + true + iex> FunWithFlags.enabled?(:hogwarts, for: harry) + true + iex> FunWithFlags.enabled?(:hogwarts, for: dudley) + false + + ### Disable for a percentage of the time + + iex> FunWithFlags.clear(:random_glitch) + :ok + iex> FunWithFlags.disable(:random_glitch, for_percentage_of: {:time, 0.999999999}) + {:ok, false} + iex> FunWithFlags.enabled?(:random_glitch) + false + iex> FunWithFlags.disable(:random_glitch, for_percentage_of: {:time, 0.000000001}) + {:ok, false} + iex> FunWithFlags.enabled?(:random_glitch) + true + + ### Disable for a percentage of the actors + + iex> FunWithFlags.disable(:new_ui, for_percentage_of: {:actors, 0.3}) + {:ok, false} + + """ + @spec disable(atom, options) :: {:ok, false} | {:error, any} + def disable(flag_name, options \\ []) + + def disable(flag_name, []) when is_atom(flag_name) do + gate = Gate.new(:boolean, false) + case @store.put(flag_name, gate) do + {:ok, flag} -> verify(flag) + error -> error + end + end + + def disable(flag_name, [for_actor: nil]) do + disable(flag_name) + end + + def disable(flag_name, [for_actor: actor]) when is_atom(flag_name) do + gate = Gate.new(:actor, actor, false) + case @store.put(flag_name, gate) do + {:ok, flag} -> verify(flag, for: actor) + error -> error + end + end + + def disable(flag_name, [for_group: nil]) do + disable(flag_name) + end + + def disable(flag_name, [for_group: group_name]) when is_atom(flag_name) do + gate = Gate.new(:group, group_name, false) + case @store.put(flag_name, gate) do + {:ok, _flag} -> {:ok, false} + error -> error + end + end + + + def disable(flag_name, [for_percentage_of: {type, ratio}]) + when is_atom(flag_name) and is_float(ratio) do + inverted_ratio = 1.0 - ratio + case enable(flag_name, [for_percentage_of: {type, inverted_ratio}]) do + {:ok, true} -> {:ok, false} + error -> error + end + end + + + @doc """ + Clears the data of a feature flag. + + Clears the data for an entire feature flag or for a specific + Actor or Group gate. Clearing a boolean gate is not supported + because a missing boolean gate is equivalent to a disabled boolean + gate. + + Sometimes enabling or disabling a gate is not what you want, and you + need to remove that gate's rules instead. For example, if you don't need + anymore to explicitly enable or disable a flag for an actor, and the + default state should be used instead, you'll want to clear the gate. + + It's also possible to clear the entire flag, by not passing any option. + + ## Options + + * `for_actor: an_actor` - used to clear the flag for a specific term only. + The value can be any term that implements the `Actor` protocol. + * `for_group: a_group_name` - used to clear the flag for a specific group only. + The value should be a binary or an atom (It's internally converted + to a binary and it's stored and retrieved as a binary. Atoms are + supported for retro-compatibility with versions <= 0.9) + * `boolean: true` - used to clear the boolean gate. + * `for_percentage: true` - used to clear any percentage gate. + + ## Examples + + iex> alias FunWithFlags.TestUser, as: User + iex> harry = %User{id: 1, name: "Harry Potter", groups: ["wizards", "gryffindor"]} + iex> hagrid = %User{id: 2, name: "Rubeus Hagrid", groups: ["wizards", "gamekeeper"]} + iex> dudley = %User{id: 3, name: "Dudley Dursley", groups: ["muggles"]} + iex> FunWithFlags.disable(:wands) + iex> FunWithFlags.enable(:wands, for_group: "wizards") + iex> FunWithFlags.disable(:wands, for_actor: hagrid) + iex> + iex> FunWithFlags.enabled?(:wands) + false + iex> FunWithFlags.enabled?(:wands, for: harry) + true + iex> FunWithFlags.enabled?(:wands, for: hagrid) + false + iex> FunWithFlags.enabled?(:wands, for: dudley) + false + iex> + iex> FunWithFlags.clear(:wands, for_actor: hagrid) + :ok + iex> FunWithFlags.enabled?(:wands, for: hagrid) + true + iex> + iex> FunWithFlags.clear(:wands) + :ok + iex> FunWithFlags.enabled?(:wands) + false + iex> FunWithFlags.enabled?(:wands, for: harry) + false + iex> FunWithFlags.enabled?(:wands, for: hagrid) + false + iex> FunWithFlags.enabled?(:wands, for: dudley) + false + + """ + @spec clear(atom, options) :: :ok | {:error, any} + def clear(flag_name, options \\ []) + + def clear(flag_name, []) when is_atom(flag_name) do + case @store.delete(flag_name) do + {:ok, _flag} -> :ok + error -> error + end + end + + def clear(flag_name, [boolean: true]) do + gate = Gate.new(:boolean, false) # we only care about the gate id + _clear_gate(flag_name, gate) + end + + def clear(flag_name, [for_actor: nil]) do + clear(flag_name) + end + + def clear(flag_name, [for_actor: actor]) when is_atom(flag_name) do + gate = Gate.new(:actor, actor, false) # we only care about the gate id + _clear_gate(flag_name, gate) + end + + def clear(flag_name, [for_group: nil]) do + clear(flag_name) + end + + def clear(flag_name, [for_group: group_name]) when is_atom(flag_name) do + gate = Gate.new(:group, group_name, false) # we only care about the gate id + _clear_gate(flag_name, gate) + end + + def clear(flag_name, [for_percentage: true]) do + gate = Gate.new(:percentage_of_time, 0.5) # we only care about the gate id + _clear_gate(flag_name, gate) + end + + defp _clear_gate(flag_name, gate) do + case @store.delete(flag_name, gate) do + {:ok, _flag} -> :ok + error -> error + end + end + + @doc """ + Returns a list of all flag names currently configured, as atoms. + + This can be useful for debugging or for display purposes, + but it's not meant to be used at runtime. Undefined flags, + for example, will be considered disabled. + """ + @spec all_flag_names() :: {:ok, [atom]} | {:error, any} + def all_flag_names do + Config.persistence_adapter().all_flag_names() + end + + @doc """ + Returns a list of all the flags currently configured, as data structures. + + This function is provided for debugging and to build more complex + functionality (e.g. it's used in the web GUI), but it is not meant to be + used at runtime to check if a flag is enabled. + + To query the value of a flag, please use the `enabled?2` function instead. + """ + @spec all_flags() :: {:ok, [FunWithFlags.Flag.t]} | {:error, any} + def all_flags do + Config.persistence_adapter().all_flags() + end + + + @doc """ + Returns a `FunWithFlags.Flag` struct for the given name, or `nil` if + no flag is found. + + Useful for debugging. + """ + @spec get_flag(atom) :: FunWithFlags.Flag.t | nil | {:error, any} + def get_flag(name) do + case all_flag_names() do + {:ok, names} -> + if name in names do + case Config.persistence_adapter().get(name) do + {:ok, flag} -> flag + error -> error + end + else + nil + end + error -> error + end + end + + + defp verify(flag) do + {:ok, Flag.enabled?(flag)} + end + defp verify(flag, [for: data]) do + {:ok, Flag.enabled?(flag, for: data)} + end + + # Used in some tests and to debug. + # + # Apparently calling `Config.store_module_determined_at_compile_time` even + # just once in a test causes all sorts of weird test failures everywhere. + # + @doc false + def compiled_store, do: @store + + end + end +end