defmodule Flagsmith.Configuration do
require Logger
@default_url "https://edge.api.flagsmith.com/api/v1/"
@environment_header "X-Environment-Key"
@api_paths %{
flags: "/flags/",
identities: "/identities/",
traits: "/traits/",
analytics: "/analytics/flags/",
environment: "/environment-document/"
}
@min_refresh_interval if(Mix.env() == :test, do: 1, else: 60_000)
@enforce_keys [:environment_key]
defstruct [
:environment_key,
:default_flag_handler,
api_url: @default_url,
custom_headers: [],
request_timeout_milliseconds: 5000,
enable_local_evaluation: false,
environment_refresh_interval_milliseconds: 60_000,
retries: 0,
enable_analytics: false
]
@default_keys [
:api_url,
:default_flag_handler,
:custom_headers,
:request_timeout_milliseconds,
:enable_local_evaluation,
:environment_refresh_interval_milliseconds,
:retries,
:enable_analytics
]
@type t() :: %__MODULE__{
environment_key: String.t(),
default_flag_handler: function(),
api_url: String.t(),
custom_headers: list({String.t(), String.t()}),
request_timeout_milliseconds: non_neg_integer(),
enable_local_evaluation: boolean(),
environment_refresh_interval_milliseconds: non_neg_integer(),
retries: non_neg_integer(),
enable_analytics: boolean()
}
@doc false
def default_url(), do: @default_url
@doc false
def environment_header(), do: @environment_header
@doc false
@spec api_paths() :: map()
def api_paths(), do: @api_paths
@doc false
@spec api_paths(what :: atom()) :: String.t() | no_return
def api_paths(what), do: Map.fetch!(@api_paths, what)
@doc false
def build(opts \\ []) do
with key <- get_environment_key(opts),
config <- %__MODULE__{environment_key: key} do
Enum.reduce(@default_keys, config, fn key, config_acc ->
maybe_add_key(config_acc, key, opts)
end)
|> validate!()
|> warn_if_unknown_opts(opts)
end
end
defp validate!(%__MODULE__{} = config) do
Enum.all?([:environment_key | @default_keys], fn k ->
value = Map.fetch!(config, k)
case validate(k, value) do
true -> true
{:error, msg} -> raise "#{k} #{msg}"
end
end)
config
end
defp warn_if_unknown_opts(config, opts) do
Enum.map(opts, fn {k, _} ->
case is_map_key(config, k) do
true ->
:ok
false ->
Logger.warn("unknown option #{inspect(k)} passed as configuration to Flagsmith.Client")
end
end)
config
end
defp validate(key, value) do
case key do
:environment_key ->
is_binary(value) || {:error, "needs to be a String.t()"}
:api_url ->
is_binary(value) || {:error, "needs to be a String.t()"}
:default_flag_handler ->
is_function(value, 1) or is_nil(value) ||
{:error, "needs to be a 1 arity function or nil"}
:custom_headers ->
(is_list(value) and
Enum.all?(value, fn pair ->
with true <- is_tuple(pair) and tuple_size(pair) == 2,
{k, v} <- pair do
is_binary(k) and is_binary(v)
else
_ -> false
end
end)) ||
{:error, "needs to be a list of 2 sized tuples, with both elements String.t()"}
:request_timeout_milliseconds ->
(is_integer(value) and value >= 5000) ||
{:error, "needs to be an integer equal or bigger than 5000"}
:enable_local_evaluation ->
is_boolean(value) || {:error, "needs to be true or false"}
:environment_refresh_interval_milliseconds ->
(is_integer(value) and value >= @min_refresh_interval) ||
{:error, "needs to be an integer equal or bigger than #{@min_refresh_interval}"}
:retries ->
(is_integer(value) and value >= 0) ||
{:error, "needs to be an integer equal or bigger than 0"}
:enable_analytics ->
is_boolean(value) || {:error, "needs to be true or false"}
end
end
defp get_environment_key(opts) do
case Keyword.get(opts, :environment_key) do
nil -> get!(:environment_key)
key -> key
end
end
defp maybe_add_key(config_acc, key, opts) do
case Keyword.get(opts, key) do
nil ->
case get(key) do
nil -> config_acc
val -> Map.put(config_acc, key, val)
end
val ->
Map.put(config_acc, key, val)
end
end
defp get!(key) do
Application.get_env(:flagsmith_engine, :configuration, [])
|> Keyword.fetch!(key)
end
defp get(key) do
Application.get_env(:flagsmith_engine, :configuration, [])
|> Keyword.get(key)
end
end