Packages
ConfigCat SDK for Elixir. Feature Flags created by developers for developers with ❤️. ConfigCat lets you manage feature flags across frontend, backend, mobile, and desktop apps without (re)deploying code. % rollouts, user targeting, segmentation. Feature toggle SDKs for all main languages.
Current section
Files
Jump to
Current section
Files
lib/config_cat/client.ex
defmodule ConfigCat.Client do
@moduledoc false
use GenServer
alias ConfigCat.{
CachePolicy,
Config,
Constants,
OverrideDataSource,
Rollout,
User
}
require Constants
require Logger
@type client :: ConfigCat.instance_id()
@type option ::
{:cache_policy, module()}
| {:cache_policy_id, CachePolicy.id()}
| {:flag_overrides, OverrideDataSource.t()}
| {:name, client()}
@type options :: [option]
@type refresh_result :: CachePolicy.refresh_result()
@spec start_link(options()) :: GenServer.on_start()
def start_link(options) do
with {name, options} <- Keyword.pop!(options, :name) do
GenServer.start_link(__MODULE__, Map.new(options), name: name)
end
end
@spec get_all_keys(client()) :: [Config.key()]
def get_all_keys(client) do
GenServer.call(client, :get_all_keys, Constants.fetch_timeout())
end
@spec get_value(client(), Config.key(), Config.value(), User.t() | nil) :: Config.value()
def get_value(client, key, default_value, user \\ nil) do
GenServer.call(client, {:get_value, key, default_value, user}, Constants.fetch_timeout())
end
@spec get_variation_id(client(), Config.key(), Config.variation_id(), User.t() | nil) ::
Config.variation_id()
def get_variation_id(client, key, default_variation_id, user \\ nil) do
GenServer.call(
client,
{:get_variation_id, key, default_variation_id, user},
Constants.fetch_timeout()
)
end
@spec get_all_variation_ids(client(), User.t() | nil) :: [Config.variation_id()]
def get_all_variation_ids(client, user \\ nil) do
GenServer.call(client, {:get_all_variation_ids, user}, Constants.fetch_timeout())
end
@spec get_key_and_value(client(), Config.variation_id()) :: {Config.key(), Config.value()} | nil
def get_key_and_value(client, variation_id) do
GenServer.call(client, {:get_key_and_value, variation_id}, Constants.fetch_timeout())
end
@spec get_all_values(client(), User.t() | nil) :: %{Config.key() => Config.value()}
def get_all_values(client, user \\ nil) do
GenServer.call(client, {:get_all_values, user}, Constants.fetch_timeout())
end
@spec force_refresh(client()) :: refresh_result()
def force_refresh(client) do
GenServer.call(client, :force_refresh, Constants.fetch_timeout())
end
@impl GenServer
def init(state) do
{:ok, state}
end
@impl GenServer
def handle_call(:get_all_keys, _from, state) do
result = do_get_all_keys(state)
{:reply, result, state}
end
@impl GenServer
def handle_call({:get_value, key, default_value, user}, _from, state) do
result = do_get_value(key, default_value, user, state)
{:reply, result, state}
end
@impl GenServer
def handle_call({:get_variation_id, key, default_variation_id, user}, _from, state) do
result = do_get_variation_id(key, default_variation_id, user, state)
{:reply, result, state}
end
@impl GenServer
def handle_call({:get_all_variation_ids, user}, _from, state) do
result =
state
|> do_get_all_keys()
|> Enum.map(&do_get_variation_id(&1, nil, user, state))
|> Enum.reject(&is_nil/1)
{:reply, result, state}
end
@impl GenServer
def handle_call({:get_key_and_value, variation_id}, _from, state) do
with {:ok, config} <- cached_config(state),
{:ok, feature_flags} <- Map.fetch(config, Constants.feature_flags()),
result <- Enum.find_value(feature_flags, nil, &entry_matching(&1, variation_id)) do
{:reply, result, state}
else
_ ->
Logger.warn(
"Evaluating get_key_and_value(#{variation_id}) failed. Cache is empty. Returning nil."
)
{:reply, nil, state}
end
end
@impl GenServer
def handle_call({:get_all_values, user}, _from, state) do
result =
state
|> do_get_all_keys()
|> Map.new(fn key -> {key, do_get_value(key, nil, user, state)} end)
{:reply, result, state}
end
@impl GenServer
def handle_call(:force_refresh, _from, state) do
%{cache_policy: policy, cache_policy_id: policy_id} = state
result = policy.force_refresh(policy_id)
{:reply, result, state}
end
defp do_get_value(key, default_value, user, state) do
with {:ok, result} <- evaluate(key, user, default_value, nil, state),
{value, _variation} = result do
value
else
error -> error
end
end
defp do_get_all_keys(state) do
with {:ok, config} <- cached_config(state) do
feature_flags = Map.get(config, Constants.feature_flags(), %{})
Map.keys(feature_flags)
else
{:error, :not_found} -> []
error -> error
end
end
defp do_get_variation_id(key, default_variation_id, user, state) do
with {:ok, result} <- evaluate(key, user, nil, default_variation_id, state),
{_value, variation} = result do
variation
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())}
else
nil
end
end
defp evaluate(key, user, default_value, default_variation_id, state) do
with {:ok, config} <- cached_config(state) do
{:ok, Rollout.evaluate(key, user, default_value, default_variation_id, config)}
else
{:error, :not_found} -> {:ok, {default_value, default_variation_id}}
error -> error
end
end
defp cached_config(%{
cache_policy: policy,
cache_policy_id: policy_id,
flag_overrides: override_data_source
}) do
with {:ok, local_settings} <- OverrideDataSource.overrides(override_data_source) do
case OverrideDataSource.behaviour(override_data_source) do
:local_only ->
{:ok, local_settings}
:local_over_remote ->
with {:ok, remote_settings} <- policy.get(policy_id) do
{:ok, merge_settings(remote_settings, local_settings)}
end
:remote_over_local ->
with {:ok, remote_settings} <- policy.get(policy_id) do
{:ok, merge_settings(local_settings, remote_settings)}
end
end
end
end
defp merge_settings(%{Constants.feature_flags() => left_flags} = target, %{
Constants.feature_flags() => right_flags
}) do
Map.put(target, Constants.feature_flags(), Map.merge(left_flags, right_flags))
end
defp merge_settings(target, _overrides), do: target
end