defmodule Confeature do
@moduledoc """
Defines a macro for implementing your own feature settings store on top of
Ecto and the cache store of your choice.
You can simply declare your module like this:
defmodule MyApp.Confeature do
use Confeature,
ecto_repo: MyApp.Repo,
cache: MyApp.Cache.Feature
end
Providing an ecto_repo is mandatory, but by default, Confeature will use a
placeholder cache module. Please refer to the `Confeature.Cache` module doc for detailed instructions
on how to implement your cache.
The functions docs below assume that you have the following feature modules
declared, as references:
defmodule MyApp.Features.UsageAlert do
defstruct [:threshold]
@type t :: %__MODULE__{
threshold: float()
}
end
defmodule MyApp.Features.HiddenPixel do
defstruct [:enabled, :x, :y]
@type t :: %__MODULE__{
enabled: boolean(),
x: integer(),
y: integer()
}
end
"""
@doc false
defmacro __using__(opts) do
quote bind_quoted: [opts: opts] do
@behaviour Confeature
@repo Keyword.fetch!(opts, :ecto_repo)
@cache Keyword.get(opts, :cache, Confeature.Cache.Default)
@table_name Keyword.get(opts, :table_name)
def __repo__, do: @repo
def __cache__, do: @cache
def __table_name__, do: @table_name
import Ecto.{
Changeset,
Query
}
alias Confeature.{
Schema,
Type,
SQL
}
def get(name) when is_atom(name) do
case apply(__cache__(), :get, [name]) do
%Schema{} = feature ->
{:ok, feature} = feature |> Type.load()
feature
nil ->
case SQL.get(__MODULE__, name) do
nil ->
nil
%Confeature.Schema{} = record ->
{:ok, _result} = apply(__cache__(), :set, [name, record])
{:ok, feature} = record |> Type.load()
feature
end
end
end
def set(%{__struct__: name} = feature_struct) do
{:ok, result} = SQL.upsert(__MODULE__, feature_struct)
{:ok, _result} = apply(__cache__(), :set, [name, result])
{:ok, _} = result |> Type.load()
end
def delete!(name) do
feature = SQL.get(__MODULE__, name)
{:ok, _result} = apply(__repo__(), :delete, [feature])
{:ok, _result} = apply(__cache__(), :delete, [name])
end
def enabled?(name) do
name
|> get()
|> Map.fetch!(:enabled)
end
def enable(name) do
name
|> get()
|> Map.put(:enabled, true)
|> set()
end
def disable(name) do
name
|> get()
|> Map.put(:enabled, false)
|> set()
end
end
end
@doc """
Returns a feature struct based on the module name.
MyApp.Confeature.get(MyApp.Features.UsageAlert)
# If you never set it:
=> nil
# After setting it:
=> %MyApp.Features.UsageAlert{threshold: 4.5}
"""
@callback get(name :: atom()) :: struct() | nil
@doc """
Writes new settings using a feature struct. It'll write to the database
and invalidate cache right after.
This function allows incremental updates; parameters you don't provide
won't get erased.
MyApp.Confeature.set(%MyApp.Features.HiddenPixel{x: 43, y: 219})
=> {:ok, *your_updated_struct*}
"""
@callback set(struct :: struct()) :: {:ok, struct()}
@doc """
Deletes the feature row from your database and invalidates the cache.
You may want to call this function once you're completely done with a
feature (eg. in a post-release Ecto migration).
"""
@callback delete!(name :: atom()) :: {:ok, any()}
@doc """
Returns true if your feature is enabled. This is a helper function, and
it requires you to declare the enabled boolean field on the feature
struct.
MyApp.Confeature.enabled?(MyApp.Features.UsageAlert)
=> *will throw an error*
MyApp.Confeature.enabled?(MyApp.Features.HiddenPixel)
=> false
"""
@callback enabled?(name :: atom()) :: boolean()
@doc """
Enables a feature. This is a helper function, and it requires you to
declare the enabled boolean field on the feature struct.
MyApp.Confeature.enable(MyApp.Features.UsageAlert)
=> *will throw an error*
MyApp.Confeature.enable(MyApp.Features.HiddenPixel)
=> {:ok, *your_updated_struct*}
"""
@callback enable(name :: atom()) :: {:ok, struct()}
@doc """
Disables a feature. This is a helper function, and it requires you to
declare the enabled boolean field on the feature struct.
MyApp.Confeature.disable(MyApp.Features.UsageAlert)
=> *will throw an error*
MyApp.Confeature.disable(MyApp.Features.HiddenPixel)
=> {:ok, *your_updated_struct*}
"""
@callback disable(name :: atom()) :: {:ok, struct()}
end