Skip to content

Commit

Permalink
Convert to supervisor (#42)
Browse files Browse the repository at this point in the history
Converts Flippant to use a supervisor structure instead of being an application.

Moving to a supervised structure allows us to completely move off of application config, and to application state using persistent term.

Moving to persistent term forces an elixir version bump.
  • Loading branch information
TylerWitt authored Apr 22, 2020
1 parent 93c58d8 commit 04212cc
Show file tree
Hide file tree
Showing 8 changed files with 163 additions and 82 deletions.
4 changes: 1 addition & 3 deletions bench/adapters.exs
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,7 @@ defmodule Bench.Adapters do
end

def configure(adapter) do
Application.stop(:flippant)
Application.put_env(:flippant, :adapter, adapter)
Application.ensure_started(:flippant)
start_supervised!({Flippant, adapter: adapter})

Flippant.setup()
Flippant.clear()
Expand Down
157 changes: 118 additions & 39 deletions lib/flippant.ex
Original file line number Diff line number Diff line change
Expand Up @@ -191,18 +191,40 @@ defmodule Flippant do
Flippant.load("flippant.dump")
"""

use Supervisor

alias Flippant.Config

@doc """
Start a Flippant process linked to the current process.
"""
@spec start_link([{:name, module()}]) :: Supervisor.on_start()
def start_link(opts \\ []) do
opts = Keyword.put_new(opts, :name, __MODULE__)
conf = Config.new(opts)

:ok = Config.put(opts[:name], conf)

Supervisor.start_link(__MODULE__, conf, name: opts[:name])
end

@impl true
def init(conf) do
Supervisor.init([{conf.adapter, conf.adapter_opts}], strategy: :one_for_one)
end

# Adapter

@doc """
Retrieve the `pid` of the configured adapter process.
This will return `nil` if the adapter hasn't been started.
"""
@spec adapter() :: pid | nil
def adapter do
:flippant
|> Application.fetch_env!(:adapter)
|> Process.whereis()
@spec adapter(name :: atom()) :: pid | nil
def adapter(name \\ __MODULE__) do
%{adapter: adapter} = Config.get(name)

Process.whereis(adapter)
end

@doc """
Expand All @@ -216,9 +238,11 @@ defmodule Flippant do
Flippant.add("search")
#=> :ok
"""
@spec add(binary) :: :ok
def add(feature) when is_binary(feature) do
GenServer.cast(adapter(), {:add, normalize(feature)})
@spec add(name :: atom(), feature :: binary()) :: :ok
def add(name \\ __MODULE__, feature) when is_binary(feature) do
name
|> adapter()
|> GenServer.cast({:add, normalize(feature)})
end

@doc """
Expand Down Expand Up @@ -249,9 +273,14 @@ defmodule Flippant do
Flippant.breakdown(actor)
#=> %{"delete" => true, "search" => false}
"""
@spec breakdown(map | struct | :all) :: map
def breakdown(actor \\ :all) do
GenServer.call(adapter(), {:breakdown, actor})
@spec breakdown(actor :: map | struct | :all) :: map
def breakdown(actor \\ :all), do: breakdown(__MODULE__, actor)

@spec breakdown(name :: atom(), actor :: map | struct | :all) :: map
def breakdown(name, actor) do
name
|> adapter()
|> GenServer.call({:breakdown, actor})
end

@doc """
Expand All @@ -265,9 +294,11 @@ defmodule Flippant do
Flippant.clear()
#=> :ok
"""
@spec clear() :: :ok
def clear do
GenServer.cast(adapter(), :clear)
@spec clear(name :: atom()) :: :ok
def clear(name \\ __MODULE__) do
name
|> adapter()
|> GenServer.cast(:clear)
end

@doc """
Expand All @@ -294,7 +325,15 @@ defmodule Flippant do
@spec disable(binary, binary) :: :ok
def disable(feature, group, values \\ [])
when is_binary(feature) and is_binary(group) and is_list(values) do
GenServer.cast(adapter(), {:remove, normalize(feature), group, values})
disable(__MODULE__, feature, group, values)
end

@spec disable(name :: atom(), binary, binary) :: :ok
def disable(name, feature, group, values)
when is_binary(feature) and is_binary(group) and is_list(values) do
name
|> adapter()
|> GenServer.cast({:remove, normalize(feature), group, values})
end

@doc """
Expand All @@ -313,10 +352,11 @@ defmodule Flippant do
Flippant.dump((Date.utc_today() |> Date.to_string()) <> ".dump")
#=> :ok
"""
@spec dump(binary()) :: :ok | {:error, File.posix()}
def dump(path) when is_binary(path) do
@spec dump(name :: atom(), binary()) :: :ok | {:error, File.posix()}
def dump(name \\ __MODULE__, path) when is_binary(path) do
dumped =
adapter()
name
|> adapter()
|> GenServer.call({:breakdown, :all})
|> Jason.encode!()

Expand Down Expand Up @@ -350,9 +390,16 @@ defmodule Flippant do
#=> :ok
"""
@spec enable(binary, binary, list(any)) :: :ok
def enable(feature, group, values \\ [])
def enable(feature, group, values \\ []) do
enable(__MODULE__, feature, group, values)
end

@spec enable(name :: atom(), binary, binary, list(any)) :: :ok
def enable(name, feature, group, values)
when is_binary(feature) and is_binary(group) do
GenServer.cast(adapter(), {:add, normalize(feature), {group, values}})
name
|> adapter()
|> GenServer.cast({:add, normalize(feature), {group, values}})
end

@doc """
Expand All @@ -366,9 +413,11 @@ defmodule Flippant do
Flippant.enabled?("search", actor)
#=> false
"""
@spec enabled?(binary, map | struct) :: boolean
def enabled?(feature, actor) when is_binary(feature) do
GenServer.call(adapter(), {:enabled?, normalize(feature), actor})
@spec enabled?(name :: atom(), binary, map | struct) :: boolean
def enabled?(name \\ __MODULE__, feature, actor) when is_binary(feature) do
name
|> adapter()
|> GenServer.call({:enabled?, normalize(feature), actor})
end

@doc """
Expand All @@ -388,7 +437,14 @@ defmodule Flippant do
"""
@spec exists?(binary(), binary() | :any) :: boolean()
def exists?(feature, group \\ :any) when is_binary(feature) do
GenServer.call(adapter(), {:exists?, normalize(feature), group})
exists?(__MODULE__, feature, group)
end

@spec exists?(name :: atom(), binary(), binary() | :any) :: boolean()
def exists?(name, feature, group) when is_binary(feature) do
name
|> adapter()
|> GenServer.call({:exists?, normalize(feature), group})
end

@doc """
Expand All @@ -411,7 +467,14 @@ defmodule Flippant do
"""
@spec features(:all | binary()) :: list(binary())
def features(group \\ :all) do
GenServer.call(adapter(), {:features, group})
features(__MODULE__, group)
end

@spec features(name :: atom(), :all | binary()) :: list(binary())
def features(name, group) do
name
|> adapter()
|> GenServer.call({:features, group})
end

@doc """
Expand All @@ -430,12 +493,12 @@ defmodule Flippant do
Flippant.clear(:features) #=> :ok
Flippant.load("backup.dump") #=> :ok
"""
@spec load(binary()) :: :ok | {:error, File.posix() | binary()}
def load(path) when is_binary(path) do
@spec load(name :: atom(), binary()) :: :ok | {:error, File.posix() | binary()}
def load(name \\ __MODULE__, path) when is_binary(path) do
with {:ok, data} <- File.read(path) do
loaded = Jason.decode!(data)

GenServer.cast(adapter(), {:restore, loaded})
name
|> adapter()
|> GenServer.cast({:restore, Jason.decode!(data)})
end
end

Expand All @@ -450,10 +513,12 @@ defmodule Flippant do
Flippant.rename("search", "super-search")
:ok
"""
@spec rename(binary, binary) :: :ok
def rename(old_name, new_name)
@spec rename(name :: atom(), binary, binary) :: :ok
def rename(name \\ __MODULE__, old_name, new_name)
when is_binary(old_name) and is_binary(new_name) do
GenServer.cast(adapter(), {:rename, normalize(old_name), normalize(new_name)})
name
|> adapter()
|> GenServer.cast({:rename, normalize(old_name), normalize(new_name)})
end

@doc """
Expand All @@ -464,9 +529,11 @@ defmodule Flippant do
Flippant.remove("search")
:ok
"""
@spec remove(binary) :: :ok
def remove(feature) when is_binary(feature) do
GenServer.cast(adapter(), {:remove, normalize(feature)})
@spec remove(name :: atom(), binary) :: :ok
def remove(name \\ __MODULE__, feature) when is_binary(feature) do
name
|> adapter()
|> GenServer.cast({:remove, normalize(feature)})
end

@doc """
Expand All @@ -481,9 +548,21 @@ defmodule Flippant do
Flippant.setup()
:ok
"""
@spec setup() :: :ok
def setup do
GenServer.cast(adapter(), :setup)
@spec setup(name :: atom()) :: :ok
def setup(name \\ __MODULE__) do
name
|> adapter()
|> GenServer.cast(:setup)
end

@doc false
def update_config(name \\ __MODULE__, key, value) do
conf =
name
|> Config.get()
|> Map.put(key, value)

Config.put(name, conf)
end

defp normalize(value) when is_binary(value) do
Expand Down
26 changes: 0 additions & 26 deletions lib/flippant/application.ex

This file was deleted.

32 changes: 32 additions & 0 deletions lib/flippant/config.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
defmodule Flippant.Config do
@moduledoc false

@type t :: %__MODULE__{
adapter: module(),
adapter_opts: Keyword.t(),
name: module(),
rules: module()
}

defstruct adapter: Flippant.Adapter.Memory,
adapter_opts: [],
name: Flippant,
rules: Flippant.Rules.Default

@spec new(Keyword.t()) :: t()
def new(opts) when is_list(opts) do
config = struct!(__MODULE__, opts)

Map.update!(config, :adapter_opts, &default_adapter_opts(&1, config))
end

@spec get(name :: atom()) :: t()
def get(name), do: :persistent_term.get(name)

@spec put(name :: atom(), config :: t()) :: :ok
def put(name, %__MODULE__{} = conf) when is_atom(name), do: :persistent_term.put(name, conf)

defp default_adapter_opts(opts, %{adapter: adapter}) do
Keyword.put_new(opts, :name, adapter)
end
end
8 changes: 5 additions & 3 deletions lib/flippant/rules.ex
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ defmodule Flippant.Rules do
@type rules :: Enumerable.t()
@type actor :: term()

alias Flippant.Config

@doc """
Validate that an actor is both a member of `group` and in the `enabled_for` list. For example,
if we had a `staff` group, and a rule containing `%{"staff" => ids}`, we could have a function
Expand All @@ -28,9 +30,9 @@ defmodule Flippant.Rules do
Check whether any rules are enabled for a particular actor. The function
accepts a list of names/value pairs and an actor.
"""
@spec enabled_for_actor?(rules(), actor()) :: boolean()
def enabled_for_actor?(rules, actor) do
ruleset = Application.fetch_env!(:flippant, :rules)
@spec enabled_for_actor?(name :: atom(), rules(), actor()) :: boolean()
def enabled_for_actor?(name \\ Flippant, rules, actor) do
%{rules: ruleset} = Config.get(name)

Enum.any?(rules, fn {group, enabled_for} ->
ruleset.enabled?(group, enabled_for, actor)
Expand Down
6 changes: 2 additions & 4 deletions mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ defmodule Flippant.Mixfile do
[
app: :flippant,
version: @version,
elixir: "~> 1.6",
elixir: "~> 1.10",
build_embedded: Mix.env() == :prod,
start_permanent: Mix.env() == :prod,
test_coverage: [tool: ExCoveralls],
Expand All @@ -25,9 +25,7 @@ defmodule Flippant.Mixfile do

def application do
[
extra_applications: [:logger],
env: [adapter: Flippant.Adapter.Memory, rules: Flippant.Rules.Default],
mod: {Flippant.Application, []}
extra_applications: [:logger]
]
end

Expand Down
4 changes: 2 additions & 2 deletions test/flippant/rules_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,9 @@ defmodule Flippant.RulesTest do

describe "enabled_for_actor?/2 with custom rules" do
setup do
Application.put_env(:flippant, :rules, TestRules)
Flippant.update_config(:rules, TestRules)

on_exit(fn -> Application.put_env(:flippant, :rules, Default) end)
on_exit(fn -> Flippant.update_config(:rules, Default) end)
end

test "groups and membership are asserted through custom rules" do
Expand Down
Loading

0 comments on commit 04212cc

Please sign in to comment.