lib/fun_with_flags.ex

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

  """
  @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