Skip to content

Commit

Permalink
♻️ Encapsulate config format (#114)
Browse files Browse the repository at this point in the history
* ♻️ Rename settings -> feature flags

This is a start at encapsulating the format of the Config map.

* ♻️ Introduce Config.Preferences module

Refactor existing use of preferences to use new module.

* ♻️ Encapsulate EvaluationFormula and rules

* ♻️ Move entry matching to config modules
  • Loading branch information
randycoulman authored Jan 9, 2024
1 parent fc129c9 commit ead514d
Show file tree
Hide file tree
Showing 31 changed files with 417 additions and 228 deletions.
4 changes: 2 additions & 2 deletions lib/config_cat/cache.ex
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,8 @@ defmodule ConfigCat.Cache do
def handle_call(:get, _from, %State{latest_entry: nil} = state) do
with {:ok, serialized} <- state.cache.get(state.cache_key),
{:ok, entry} <- deserialize(serialized),
{:ok, settings} <- Config.fetch_settings(entry.config) do
Hooks.invoke_on_config_changed(state.instance_id, settings)
{:ok, feature_flags} <- Config.fetch_feature_flags(entry.config) do
Hooks.invoke_on_config_changed(state.instance_id, feature_flags)
{:reply, {:ok, entry}, State.with_entry(state, entry)}
else
error ->
Expand Down
6 changes: 3 additions & 3 deletions lib/config_cat/cache_policy/auto.ex
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ defmodule ConfigCat.CachePolicy.Auto do

@impl GenServer
def handle_call(:get, _from, %State{} = state) do
{:reply, Helpers.cached_settings(state), state}
{:reply, Helpers.cached_feature_flags(state), state}
end

@impl GenServer
Expand Down Expand Up @@ -204,10 +204,10 @@ defmodule ConfigCat.CachePolicy.Auto do
defp be_initialized(%State{} = state) when initialized?(state), do: state

defp be_initialized(%State{} = state) do
settings = Helpers.cached_settings(state)
feature_flags = Helpers.cached_feature_flags(state)

for caller <- state.policy_state.callers do
GenServer.reply(caller, settings)
GenServer.reply(caller, feature_flags)
end

Helpers.on_client_ready(state)
Expand Down
2 changes: 1 addition & 1 deletion lib/config_cat/cache_policy/behaviour.ex
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ defmodule ConfigCat.CachePolicy.Behaviour do
alias ConfigCat.FetchTime

@callback get(ConfigCat.instance_id()) ::
{:ok, Config.settings(), FetchTime.t()} | {:error, :not_found}
{:ok, Config.feature_flags(), FetchTime.t()} | {:error, :not_found}
@callback offline?(ConfigCat.instance_id()) :: boolean()
@callback set_offline(ConfigCat.instance_id()) :: :ok
@callback set_online(ConfigCat.instance_id()) :: :ok
Expand Down
20 changes: 7 additions & 13 deletions lib/config_cat/cache_policy/helpers.ex
Original file line number Diff line number Diff line change
Expand Up @@ -66,18 +66,12 @@ defmodule ConfigCat.CachePolicy.Helpers do
Hooks.invoke_on_client_ready(state.instance_id)
end

@spec cached_settings(State.t()) ::
{:ok, Config.settings(), FetchTime.t()} | {:error, :not_found}
def cached_settings(%State{} = state) do
@spec cached_feature_flags(State.t()) ::
{:ok, Config.feature_flags(), FetchTime.t()} | {:error, :not_found}
def cached_feature_flags(%State{} = state) do
with {:ok, %ConfigEntry{} = entry} <- cached_entry(state),
{:ok, settings} <- Config.fetch_settings(entry.config) do
{:ok, settings, entry.fetch_time_ms}
else
:error ->
{:error, :not_found}

error ->
error
{:ok, feature_flags} <- Config.fetch_feature_flags(entry.config) do
{:ok, feature_flags, entry.fetch_time_ms}
end
end

Expand Down Expand Up @@ -111,8 +105,8 @@ defmodule ConfigCat.CachePolicy.Helpers do
{:ok, %ConfigEntry{} = entry} ->
update_cache(state, entry)

with {:ok, settings} <- Config.fetch_settings(entry.config) do
Hooks.invoke_on_config_changed(state.instance_id, settings)
with {:ok, feature_flags} <- Config.fetch_feature_flags(entry.config) do
Hooks.invoke_on_config_changed(state.instance_id, feature_flags)
end

:ok
Expand Down
2 changes: 1 addition & 1 deletion lib/config_cat/cache_policy/lazy.ex
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ defmodule ConfigCat.CachePolicy.Lazy do
@impl GenServer
def handle_call(:get, _from, %State{} = state) do
with {:ok, new_state} <- maybe_refresh(state) do
{:reply, Helpers.cached_settings(new_state), new_state}
{:reply, Helpers.cached_feature_flags(new_state), new_state}
end
end

Expand Down
2 changes: 1 addition & 1 deletion lib/config_cat/cache_policy/manual.ex
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ defmodule ConfigCat.CachePolicy.Manual do

@impl GenServer
def handle_call(:get, _from, %State{} = state) do
{:reply, Helpers.cached_settings(state), state}
{:reply, Helpers.cached_feature_flags(state), state}
end

@impl GenServer
Expand Down
52 changes: 22 additions & 30 deletions lib/config_cat/client.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ defmodule ConfigCat.Client do
use GenServer

alias ConfigCat.CachePolicy
alias ConfigCat.Config.EvaluationFormula
alias ConfigCat.EvaluationDetails
alias ConfigCat.FetchTime
alias ConfigCat.Hooks
Expand All @@ -12,7 +13,6 @@ defmodule ConfigCat.Client do
alias ConfigCat.User

require ConfigCat.ConfigCatLogger, as: ConfigCatLogger
require ConfigCat.Constants, as: Constants

defmodule State do
@moduledoc false
Expand Down Expand Up @@ -96,9 +96,9 @@ defmodule ConfigCat.Client do

@impl GenServer
def handle_call({:get_key_and_value, variation_id}, _from, %State{} = state) do
case cached_settings(state) do
{:ok, settings, _fetch_time_ms} ->
result = Enum.find_value(settings, nil, &entry_matching(&1, variation_id))
case cached_feature_flags(state) do
{:ok, feature_flags, _fetch_time_ms} ->
result = Enum.find_value(feature_flags, nil, &entry_matching(&1, variation_id))

if is_nil(result) do
ConfigCatLogger.error(
Expand Down Expand Up @@ -183,9 +183,9 @@ defmodule ConfigCat.Client do
end

defp do_get_all_keys(%State{} = state) do
case cached_settings(state) do
{:ok, settings, _fetch_time_ms} ->
Map.keys(settings)
case cached_feature_flags(state) do
{:ok, feature_flags, _fetch_time_ms} ->
Map.keys(feature_flags)

_ ->
ConfigCatLogger.error("Config JSON is not present. Returning empty result.",
Expand All @@ -196,30 +196,22 @@ defmodule ConfigCat.Client do
end
end

defp entry_matching({key, setting}, variation_id) do
value_matching(key, setting, variation_id) ||
value_matching(key, Map.get(setting, Constants.rollout_rules()), variation_id) ||
value_matching(key, Map.get(setting, Constants.percentage_rules()), variation_id)
end

defp value_matching(key, value, variation_id) when is_list(value) do
Enum.find_value(value, nil, &value_matching(key, &1, variation_id))
end

defp value_matching(key, value, variation_id) do
if Map.get(value, Constants.variation_id(), nil) == variation_id do
{key, Map.get(value, Constants.value())}
defp entry_matching({key, formula}, variation_id) do
case EvaluationFormula.variation_value(formula, variation_id) do
nil -> nil
value -> {key, value}
end
end

defp evaluate(key, user, default_value, default_variation_id, %State{} = state) do
user = if user != nil, do: user, else: state.default_user

details =
case cached_settings(state) do
{:ok, settings, fetch_time_ms} ->
case cached_feature_flags(state) do
{:ok, feature_flags, fetch_time_ms} ->
%EvaluationDetails{} =
details = Rollout.evaluate(key, user, default_value, default_variation_id, settings)
details =
Rollout.evaluate(key, user, default_value, default_variation_id, feature_flags)

fetch_time =
case FetchTime.to_datetime(fetch_time_ms) do
Expand Down Expand Up @@ -249,22 +241,22 @@ defmodule ConfigCat.Client do
details
end

defp cached_settings(%State{} = state) do
defp cached_feature_flags(%State{} = state) do
%{cache_policy: policy, flag_overrides: flag_overrides, instance_id: instance_id} = state
local_settings = OverrideDataSource.overrides(flag_overrides)
local_feature_flags = OverrideDataSource.overrides(flag_overrides)

case OverrideDataSource.behaviour(flag_overrides) do
:local_only ->
{:ok, local_settings, 0}
{:ok, local_feature_flags, 0}

:local_over_remote ->
with {:ok, remote_settings, fetch_time_ms} <- policy.get(instance_id) do
{:ok, Map.merge(remote_settings, local_settings), fetch_time_ms}
with {:ok, remote_feature_flags, fetch_time_ms} <- policy.get(instance_id) do
{:ok, Map.merge(remote_feature_flags, local_feature_flags), fetch_time_ms}
end

:remote_over_local ->
with {:ok, remote_settings, fetch_time_ms} <- policy.get(instance_id) do
{:ok, Map.merge(local_settings, remote_settings), fetch_time_ms}
with {:ok, remote_feature_flags, fetch_time_ms} <- policy.get(instance_id) do
{:ok, Map.merge(local_feature_flags, remote_feature_flags), fetch_time_ms}
end
end
end
Expand Down
49 changes: 23 additions & 26 deletions lib/config_cat/config.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,23 @@ defmodule ConfigCat.Config do
@moduledoc """
Defines configuration-related types used in the rest of the library.
"""
alias ConfigCat.RedirectMode
alias ConfigCat.Config.EvaluationFormula
alias ConfigCat.Config.Preferences

@typedoc false
@type comparator :: non_neg_integer()

@typedoc false
@type feature_flags :: %{String.t() => EvaluationFormula.t()}

@typedoc "The name of a configuration setting."
@type key :: String.t()

@typedoc "The configuration settings within a Config."
@type settings :: map()
@typedoc false
@type opt :: {:feature_flags, feature_flags()} | {:preferences, Preferences.t()}

@typedoc "A collection of configuration settings and preferences."
@type t :: map()
@typedoc "A collection of feature flags and preferences."
@type t :: %{String.t() => map()}

@typedoc false
@type url :: String.t()
Expand All @@ -27,41 +31,34 @@ defmodule ConfigCat.Config do

@feature_flags "f"
@preferences "p"
@preferences_base_url "u"
@redirect_mode "r"

@doc false
@spec new_with_preferences(url(), RedirectMode.t()) :: t()
def new_with_preferences(base_url, redirect_mode) do
%{
@preferences => %{
@preferences_base_url => base_url,
@redirect_mode => redirect_mode
}
}
@spec new([opt]) :: t()
def new(opts \\ []) do
feature_flags = Keyword.get(opts, :feature_flags, %{})
preferences = Keyword.get_lazy(opts, :preferences, &Preferences.new/0)

%{@feature_flags => feature_flags, @preferences => preferences}
end

@doc false
@spec new_with_settings(settings()) :: t()
def new_with_settings(settings) do
%{@feature_flags => settings}
@spec feature_flags(t()) :: feature_flags()
def feature_flags(config) do
Map.get(config, @feature_flags, %{})
end

@doc false
@spec fetch_settings(t()) :: {:ok, settings()} | {:error, :not_found}
def fetch_settings(config) do
@spec fetch_feature_flags(t()) :: {:ok, feature_flags()} | {:error, :not_found}
def fetch_feature_flags(config) do
case Map.fetch(config, @feature_flags) do
{:ok, settings} -> {:ok, settings}
{:ok, feature_flags} -> {:ok, feature_flags}
:error -> {:error, :not_found}
end
end

@doc false
@spec preferences(t()) :: {url() | nil, RedirectMode.t() | nil}
@spec preferences(t()) :: Preferences.t()
def preferences(config) do
case config[@preferences] do
nil -> {nil, nil}
preferences -> {preferences[@preferences_base_url], preferences[@redirect_mode]}
end
Map.get_lazy(config, @preferences, &Preferences.new/0)
end
end
70 changes: 70 additions & 0 deletions lib/config_cat/config/evaluation_formula.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
defmodule ConfigCat.Config.EvaluationFormula do
@moduledoc false
alias ConfigCat.Config
alias ConfigCat.Config.PercentageRule
alias ConfigCat.Config.RolloutRule

@type opt :: {:value, Config.value()}
@type t :: %{String.t() => term()}

@percentage_rules "p"
@rollout_rules "r"
@value "v"
@variation_id "i"

@spec new([opt]) :: t()
def new(opts \\ []) do
%{
@value => opts[:value]
}
end

@spec percentage_rules(t()) :: [PercentageRule.t()]
def percentage_rules(formula) do
Map.get(formula, @percentage_rules, [])
end

@spec rollout_rules(t()) :: [RolloutRule.t()]
def rollout_rules(formula) do
Map.get(formula, @rollout_rules, [])
end

@spec value(t()) :: Config.value()
@spec value(t(), Config.value() | nil) :: Config.value() | nil
def value(formula, default \\ nil) do
Map.get(formula, @value, default)
end

@spec variation_id(t()) :: Config.variation_id() | nil
@spec variation_id(t(), Config.variation_id() | nil) :: Config.variation_id() | nil
def variation_id(formula, default \\ nil) do
Map.get(formula, @variation_id, default)
end

@spec variation_value(t(), Config.variation_id()) :: Config.value() | nil
def variation_value(formula, variation_id) do
if variation_id(formula) == variation_id do
value(formula)
else
rollout_value = rollout_rule_variation_value(formula, variation_id)

if is_nil(rollout_value) do
percentage_rule_variation_value(formula, variation_id)
else
rollout_value
end
end
end

defp rollout_rule_variation_value(formula, variation_id) do
formula
|> rollout_rules()
|> Enum.find_value(nil, &RolloutRule.variation_value(&1, variation_id))
end

defp percentage_rule_variation_value(formula, variation_id) do
formula
|> percentage_rules()
|> Enum.find_value(nil, &PercentageRule.variation_value(&1, variation_id))
end
end
32 changes: 32 additions & 0 deletions lib/config_cat/config/percentage_rule.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
defmodule ConfigCat.Config.PercentageRule do
@moduledoc false
alias ConfigCat.Config

@type t :: %{String.t() => term()}

@percentage "p"
@value "v"
@variation_id "i"

@spec percentage(t()) :: non_neg_integer()
def percentage(rule) do
Map.get(rule, @percentage, 0)
end

@spec value(t()) :: Config.value()
def value(rule) do
Map.get(rule, @value)
end

@spec variation_id(t()) :: Config.variation_id() | nil
def variation_id(rule) do
Map.get(rule, @variation_id)
end

@spec variation_value(t(), Config.variation_id()) :: Config.value() | nil
def variation_value(rule, variation_id) do
if variation_id(rule) == variation_id do
value(rule)
end
end
end
Loading

0 comments on commit ead514d

Please sign in to comment.