Skip to content

Commit b4110dd

Browse files
authored
✨ Config v6 (#133)
* 🎨 Adopt Styler Adopt the [Styler](https://github.com/adobe/elixir-styler) library, which is an Elixir formatter plugin that imposes some opinionated formatting on the code in place of a number of Credo rules. While I don't always prefer its opinions, I really like have it auto-fix a number of issues, and I find that it's worth the tradeoff to adopt it. * ♻️ Encapsulate config format (#114) * ♻️ 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
1 parent cf572a3 commit b4110dd

File tree

153 files changed

+8215
-1179
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

153 files changed

+8215
-1179
lines changed

.credo.exs

+1-1
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,7 @@
119119
{Credo.Check.Refactor.IoPuts, []},
120120
{Credo.Check.Refactor.LongQuoteBlocks, []},
121121
{Credo.Check.Refactor.MatchInCondition, []},
122-
{Credo.Check.Refactor.Nesting, []},
122+
{Credo.Check.Refactor.Nesting, [max_nesting: 3]},
123123
{Credo.Check.Refactor.PassAsyncInTestCases, []},
124124
{Credo.Check.Refactor.FilterFilter, []},
125125
{Credo.Check.Refactor.RejectReject, []},

config/config.exs

+19
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import Config
2+
3+
if config_env() == :dev do
4+
config :mix_test_interactive, clear: true
5+
end
6+
7+
if config_env() == :test do
8+
config :logger, level: :warning
9+
10+
if Version.compare(System.version(), "1.15.0") == :lt do
11+
config :logger, :console,
12+
colors: [enabled: false],
13+
format: "$level $message\n"
14+
else
15+
config :logger, :default_formatter,
16+
colors: [enabled: false],
17+
format: "$level $message\n"
18+
end
19+
end

lib/config_cat/cache_policy/auto.ex

+3-3
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,7 @@ defmodule ConfigCat.CachePolicy.Auto do
140140

141141
@impl GenServer
142142
def handle_call(:get, _from, %State{} = state) do
143-
{:reply, Helpers.cached_settings(state), state}
143+
{:reply, Helpers.cached_config(state), state}
144144
end
145145

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

206206
defp be_initialized(%State{} = state) do
207-
settings = Helpers.cached_settings(state)
207+
config = Helpers.cached_config(state)
208208

209209
for caller <- state.policy_state.callers do
210-
GenServer.reply(caller, settings)
210+
GenServer.reply(caller, config)
211211
end
212212

213213
Helpers.on_client_ready(state)

lib/config_cat/cache_policy/behaviour.ex

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ defmodule ConfigCat.CachePolicy.Behaviour do
77
alias ConfigCat.FetchTime
88

99
@callback get(ConfigCat.instance_id()) ::
10-
{:ok, Config.settings(), FetchTime.t()} | {:error, :not_found}
10+
{:ok, Config.t(), FetchTime.t()} | {:error, :not_found}
1111
@callback offline?(ConfigCat.instance_id()) :: boolean()
1212
@callback set_offline(ConfigCat.instance_id()) :: :ok
1313
@callback set_online(ConfigCat.instance_id()) :: :ok

lib/config_cat/cache_policy/helpers.ex

+3-18
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ defmodule ConfigCat.CachePolicy.Helpers do
44
alias ConfigCat.Cache
55
alias ConfigCat.CachePolicy
66
alias ConfigCat.Config
7-
alias ConfigCat.ConfigCache
87
alias ConfigCat.ConfigEntry
98
alias ConfigCat.ConfigFetcher.FetchError
109
alias ConfigCat.FetchTime
@@ -66,25 +65,11 @@ defmodule ConfigCat.CachePolicy.Helpers do
6665
Hooks.invoke_on_client_ready(state.instance_id)
6766
end
6867

69-
@spec cached_settings(State.t()) ::
70-
{:ok, Config.settings(), FetchTime.t()} | {:error, :not_found}
71-
def cached_settings(%State{} = state) do
72-
with {:ok, %ConfigEntry{} = entry} <- cached_entry(state),
73-
{:ok, settings} <- Config.fetch_settings(entry.config) do
74-
{:ok, settings, entry.fetch_time_ms}
75-
else
76-
:error ->
77-
{:error, :not_found}
78-
79-
error ->
80-
error
81-
end
82-
end
83-
84-
@spec cached_config(State.t()) :: ConfigCache.result()
68+
@spec cached_config(State.t()) ::
69+
{:ok, Config.t(), FetchTime.t()} | {:error, :not_found}
8570
def cached_config(%State{} = state) do
8671
with {:ok, %ConfigEntry{} = entry} <- cached_entry(state) do
87-
{:ok, entry.config}
72+
{:ok, entry.config, entry.fetch_time_ms}
8873
end
8974
end
9075

lib/config_cat/cache_policy/lazy.ex

+1-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ defmodule ConfigCat.CachePolicy.Lazy do
4141
@impl GenServer
4242
def handle_call(:get, _from, %State{} = state) do
4343
with {:ok, new_state} <- maybe_refresh(state) do
44-
{:reply, Helpers.cached_settings(new_state), new_state}
44+
{:reply, Helpers.cached_config(new_state), new_state}
4545
end
4646
end
4747

lib/config_cat/cache_policy/manual.ex

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ defmodule ConfigCat.CachePolicy.Manual do
3333

3434
@impl GenServer
3535
def handle_call(:get, _from, %State{} = state) do
36-
{:reply, Helpers.cached_settings(state), state}
36+
{:reply, Helpers.cached_config(state), state}
3737
end
3838

3939
@impl GenServer

lib/config_cat/client.ex

+65-30
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,18 @@ defmodule ConfigCat.Client do
44
use GenServer
55

66
alias ConfigCat.CachePolicy
7+
alias ConfigCat.Config
8+
alias ConfigCat.Config.Setting
79
alias ConfigCat.EvaluationDetails
10+
alias ConfigCat.EvaluationLogger
811
alias ConfigCat.FetchTime
912
alias ConfigCat.Hooks
1013
alias ConfigCat.OverrideDataSource
1114
alias ConfigCat.Rollout
1215
alias ConfigCat.User
1316

17+
require ConfigCat.Config.SettingType, as: SettingType
1418
require ConfigCat.ConfigCatLogger, as: ConfigCatLogger
15-
require ConfigCat.Constants, as: Constants
1619

1720
defmodule State do
1821
@moduledoc false
@@ -96,9 +99,12 @@ defmodule ConfigCat.Client do
9699

97100
@impl GenServer
98101
def handle_call({:get_key_and_value, variation_id}, _from, %State{} = state) do
99-
case cached_settings(state) do
100-
{:ok, settings, _fetch_time_ms} ->
101-
result = Enum.find_value(settings, nil, &entry_matching(&1, variation_id))
102+
case cached_config(state) do
103+
{:ok, config, _fetch_time_ms} ->
104+
result =
105+
config
106+
|> Config.settings()
107+
|> Enum.find_value(nil, &entry_matching(&1, variation_id))
102108

103109
if is_nil(result) do
104110
ConfigCatLogger.error(
@@ -183,9 +189,9 @@ defmodule ConfigCat.Client do
183189
end
184190

185191
defp do_get_all_keys(%State{} = state) do
186-
case cached_settings(state) do
187-
{:ok, settings, _fetch_time_ms} ->
188-
Map.keys(settings)
192+
case cached_config(state) do
193+
{:ok, config, _fetch_time_ms} ->
194+
config |> Config.settings() |> Map.keys()
189195

190196
_ ->
191197
ConfigCatLogger.error("Config JSON is not present. Returning empty result.",
@@ -197,29 +203,26 @@ defmodule ConfigCat.Client do
197203
end
198204

199205
defp entry_matching({key, setting}, variation_id) do
200-
value_matching(key, setting, variation_id) ||
201-
value_matching(key, Map.get(setting, Constants.rollout_rules()), variation_id) ||
202-
value_matching(key, Map.get(setting, Constants.percentage_rules()), variation_id)
203-
end
204-
205-
defp value_matching(key, value, variation_id) when is_list(value) do
206-
Enum.find_value(value, nil, &value_matching(key, &1, variation_id))
207-
end
208-
209-
defp value_matching(key, value, variation_id) do
210-
if Map.get(value, Constants.variation_id(), nil) == variation_id do
211-
{key, Map.get(value, Constants.value())}
206+
case Setting.variation_value(setting, variation_id) do
207+
nil -> nil
208+
value -> {key, value}
212209
end
213210
end
214211

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

218-
details =
219-
case cached_settings(state) do
220-
{:ok, settings, fetch_time_ms} ->
215+
%EvaluationDetails{} =
216+
details =
217+
with {:ok, config, fetch_time_ms} <- cached_config(state),
218+
{:ok, _settings} <- Config.fetch_settings(config),
219+
{:ok, logger} <- EvaluationLogger.start() do
220+
try do
221221
%EvaluationDetails{} =
222-
details = Rollout.evaluate(key, user, default_value, default_variation_id, settings)
222+
details =
223+
Rollout.evaluate(key, user, default_value, default_variation_id, config, logger)
224+
225+
check_type_mismatch(details.value, default_value)
223226

224227
fetch_time =
225228
case FetchTime.to_datetime(fetch_time_ms) do
@@ -228,7 +231,14 @@ defmodule ConfigCat.Client do
228231
end
229232

230233
%{details | fetch_time: fetch_time}
234+
after
235+
logger
236+
|> EvaluationLogger.result()
237+
|> ConfigCatLogger.debug(event_id: 5000)
231238

239+
EvaluationLogger.stop(logger)
240+
end
241+
else
232242
_ ->
233243
message =
234244
"Config JSON is not present when evaluating setting '#{key}'. Returning the `default_value` parameter that you specified in your application: '#{default_value}'."
@@ -249,23 +259,48 @@ defmodule ConfigCat.Client do
249259
details
250260
end
251261

252-
defp cached_settings(%State{} = state) do
262+
defp cached_config(%State{} = state) do
253263
%{cache_policy: policy, flag_overrides: flag_overrides, instance_id: instance_id} = state
254-
local_settings = OverrideDataSource.overrides(flag_overrides)
264+
local_config = OverrideDataSource.overrides(flag_overrides)
255265

256266
case OverrideDataSource.behaviour(flag_overrides) do
257267
:local_only ->
258-
{:ok, local_settings, 0}
268+
{:ok, local_config, 0}
259269

260270
:local_over_remote ->
261-
with {:ok, remote_settings, fetch_time_ms} <- policy.get(instance_id) do
262-
{:ok, Map.merge(remote_settings, local_settings), fetch_time_ms}
271+
with {:ok, remote_config, fetch_time_ms} <- policy.get(instance_id) do
272+
{:ok, Config.merge(remote_config, local_config), fetch_time_ms}
263273
end
264274

265275
:remote_over_local ->
266-
with {:ok, remote_settings, fetch_time_ms} <- policy.get(instance_id) do
267-
{:ok, Map.merge(local_settings, remote_settings), fetch_time_ms}
276+
with {:ok, remote_config, fetch_time_ms} <- policy.get(instance_id) do
277+
merged = Config.merge(local_config, remote_config)
278+
{:ok, merged, fetch_time_ms}
268279
end
269280
end
270281
end
282+
283+
defp check_type_mismatch(_value, nil), do: :ok
284+
285+
defp check_type_mismatch(value, default_value) do
286+
value_type = SettingType.from_value(value)
287+
default_type = SettingType.from_value(default_value)
288+
number_types = [SettingType.double(), SettingType.int()]
289+
290+
cond do
291+
value_type == default_type ->
292+
:ok
293+
294+
value_type in number_types and default_type in number_types ->
295+
:ok
296+
297+
true ->
298+
ConfigCatLogger.warning(
299+
"The type of a setting does not match the type of the specified default value (#{default_value}). " <>
300+
"Setting's type was #{value_type} but the default value's type was #{default_type}. " <>
301+
"Please make sure that using a default value not matching the setting's type was intended.",
302+
event_id: 4002
303+
)
304+
end
305+
end
271306
end

lib/config_cat/config.ex

+56-25
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,27 @@ defmodule ConfigCat.Config do
22
@moduledoc """
33
Defines configuration-related types used in the rest of the library.
44
"""
5-
alias ConfigCat.RedirectMode
5+
alias ConfigCat.Config.Preferences
6+
alias ConfigCat.Config.Segment
7+
alias ConfigCat.Config.Setting
68

79
@typedoc false
810
@type comparator :: non_neg_integer()
911

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

13-
@typedoc "The configuration settings within a Config."
14-
@type settings :: map()
15+
@typedoc false
16+
@type opt :: {:preferences, Preferences.t()} | {:settings, settings()}
17+
18+
@typedoc false
19+
@type salt :: String.t()
20+
21+
@typedoc false
22+
@type settings :: %{String.t() => Setting.t()}
1523

1624
@typedoc "A collection of configuration settings and preferences."
17-
@type t :: map()
25+
@type t :: %{String.t() => map()}
1826

1927
@typedoc false
2028
@type url :: String.t()
@@ -25,43 +33,66 @@ defmodule ConfigCat.Config do
2533
@typedoc "The name of a variation being tested."
2634
@type variation_id :: String.t()
2735

28-
@feature_flags "f"
36+
@settings "f"
2937
@preferences "p"
30-
@preferences_base_url "u"
31-
@redirect_mode "r"
38+
@segments "s"
39+
40+
@doc false
41+
@spec new([opt]) :: t()
42+
def new(opts \\ []) do
43+
settings = Keyword.get(opts, :settings, %{})
44+
preferences = Keyword.get_lazy(opts, :preferences, &Preferences.new/0)
45+
46+
%{@settings => settings, @preferences => preferences}
47+
end
48+
49+
@doc false
50+
@spec preferences(t()) :: Preferences.t()
51+
def preferences(config) do
52+
Map.get_lazy(config, @preferences, &Preferences.new/0)
53+
end
3254

3355
@doc false
34-
@spec new_with_preferences(url(), RedirectMode.t()) :: t()
35-
def new_with_preferences(base_url, redirect_mode) do
36-
%{
37-
@preferences => %{
38-
@preferences_base_url => base_url,
39-
@redirect_mode => redirect_mode
40-
}
41-
}
56+
@spec segments(t()) :: [Segment.t()]
57+
def segments(config) do
58+
Map.get(config, @segments, [])
4259
end
4360

4461
@doc false
45-
@spec new_with_settings(settings()) :: t()
46-
def new_with_settings(settings) do
47-
%{@feature_flags => settings}
62+
@spec settings(t()) :: settings()
63+
def settings(config) do
64+
Map.get(config, @settings, %{})
4865
end
4966

5067
@doc false
5168
@spec fetch_settings(t()) :: {:ok, settings()} | {:error, :not_found}
5269
def fetch_settings(config) do
53-
case Map.fetch(config, @feature_flags) do
70+
case Map.fetch(config, @settings) do
5471
{:ok, settings} -> {:ok, settings}
5572
:error -> {:error, :not_found}
5673
end
5774
end
5875

5976
@doc false
60-
@spec preferences(t()) :: {url() | nil, RedirectMode.t() | nil}
61-
def preferences(config) do
62-
case config[@preferences] do
63-
nil -> {nil, nil}
64-
preferences -> {preferences[@preferences_base_url], preferences[@redirect_mode]}
65-
end
77+
@spec merge(left :: t(), right :: t()) :: t()
78+
def merge(left, right) do
79+
left_flags = settings(left)
80+
right_flags = settings(right)
81+
82+
Map.put(left, @settings, Map.merge(left_flags, right_flags))
83+
end
84+
85+
@doc false
86+
@spec inline_salt_and_segments(t()) :: t()
87+
def inline_salt_and_segments(config) do
88+
salt = config |> preferences() |> Preferences.salt()
89+
segments = segments(config)
90+
91+
Map.update(
92+
config,
93+
@settings,
94+
%{},
95+
&Map.new(&1, fn {key, setting} -> {key, Setting.inline_salt_and_segments(setting, salt, segments)} end)
96+
)
6697
end
6798
end

0 commit comments

Comments
 (0)