Current section

Files

Jump to
split_client lib split_client.ex
Raw

lib/split_client.ex

defmodule SplitClient do
@moduledoc """
A Split.io client for Elixir.
See [Glossary](README.md#split-io-glossary) for explanations about
function arguments and return values
"""
alias SplitClient.Boundary.Treatments
@doc """
Get the treatment for a specific split.
## Args
* `key`
* `split_name`
## Options
* `:attributes`
* `:bucketing_key`
## Examples
iex> SplitClient.get_treatment("unique-customer-id", "shiny-feature")
%Treatment{split_name: "shiny-feature", treatment: "on"}
"""
@spec get_treatment(key :: String.t(), split_name :: String.t(), options :: keyword()) ::
Treatment.t()
def get_treatment(key, split_name, opts \\ []) do
validate_params([
&validate_key(&1, key, "key"),
&validate_key(&1, split_name, "split_name"),
&validate_optional(&1, opts, :bucketing_key),
&validate_optional(&1, opts, :attributes)
])
treatments_server().get_treatment(key, split_name, opts)
end
@doc """
Get the treatments for multiple splits.
## Args
* `key`
* `split_name`
## Options
* `:attributes`
* `:bucketing_key`
## Examples
iex> SplitClient.get_treatments("unique-customer-id", ["shiny-feature", "my-experiment"])
%{"shiny-feature" => %Treatment{treatment: "on"}, "my-experiment" => %Treatment{split_name: "my-experiment", treatment: "off"}}
"""
@spec get_treatments(key :: String.t(), split_names :: [String.t()], options :: keyword()) ::
map()
def get_treatments(key, split_names, opts \\ []) do
validate_params([
&validate_key(&1, key, "key"),
&validate_keys(&1, split_names, "split_name"),
&validate_optional(&1, opts, :bucketing_key),
&validate_optional(&1, opts, :attributes)
])
treatments_server().get_treatments(key, split_names, opts)
end
@doc """
Get all the treatments for one or more traffic types.
## Args
* `keys` - A list of maps containing the following:
- `:matching_key`
- `:traffic_type`
- `:bucketing_key` - Optional
## Options
* `:attributes`
## Examples
iex> SplitClient.get_all_treatments(%{matching_key: "unique-customer-id", target_type: "customer"}, %{matching_key: "unique-user-id", target_type: "user"})
%{"customer" => %{"shiny-feature" => %Treatment{treatment: "on"}}, "user" => %{"my-experiment" => %Treatment{treatment: "off"}}}
"""
@spec get_all_treatments(keys :: [map()], options :: keyword()) :: map()
def get_all_treatments(keys, opts \\ []) do
validate_params([
&validate_composite_keys(&1, keys),
&validate_optional(&1, opts, :attributes)
])
treatments_server().get_all_treatments(keys, opts)
end
@doc """
Determine if a particular split is turned on. This is syntax sugar around the
`get_treatment/3` function. If your treatment has a meaningful config that
could be returned then use `get_treatment/3` instead
## Args
* `key`
* `split_name`
## Options
* `:attributes`
* `:bucketing_key`
## Examples
iex> SplitClient.on?("unique-customer-id", "shiny-feature")
true
"""
@spec on?(key :: String.t(), split_name :: String.t(), options :: keyword()) ::
boolean()
def on?(key, split_name, opts \\ []) do
treatment = get_treatment(key, split_name, opts)
treatment.treatment == "on"
end
@doc """
Determine if a particular split is turned off. This is syntax sugar around the
`get_treatment/3` function. If your treatment has a meaningful config that
could be returned then use `get_treatment/3` instead
## Args
* `key`
* `split_name`
## Options
* `:attributes`
* `:bucketing_key`
## Examples
iex> SplitClient.off?("unique-customer-id", "shiny-feature")
true
"""
@spec off?(key :: String.t(), split_name :: String.t(), options :: keyword()) ::
boolean()
def off?(key, split_name, opts \\ []) do
treatment = get_treatment(key, split_name, opts)
treatment.treatment != "on"
end
defp validate_params(validators) do
errors =
Enum.reduce(validators, [], fn validator, acc ->
validator.(acc)
end)
if !Enum.empty?(errors) do
raise __MODULE__.InvalidParamsException, errors
end
end
defp validate_optional(errors, opts, option) do
case Keyword.has_key?(opts, option) do
true ->
validate_option(errors, Keyword.get(opts, option), option)
false ->
errors
end
end
defp validate_option(errors, bucketing_key, :bucketing_key) do
validate_key(errors, bucketing_key, "bucketing_key")
end
defp validate_option(errors, attributes, :attributes) when is_list(attributes), do: errors
defp validate_option(errors, attributes, :attributes) when is_map(attributes), do: errors
defp validate_option(errors, attributes, :attributes) do
add_error(errors, "`attributes` must be a map or keyword list, got `#{inspect(attributes)}`")
end
defp validate_composite_keys(errors, keys) when is_list(keys) and length(keys) > 0 do
Enum.reduce(keys, errors, fn key, acc ->
validate_composite_key(acc, key)
end)
end
defp validate_composite_keys(errors, keys) do
add_error(errors, "`keys` must be a list with at least 1 item, got #{inspect(keys)}")
end
defp validate_composite_key(errors, %{matching_key: matching, traffic_type: traffic_type} = key) do
validate_optional(errors, Enum.into(key, Keyword.new()), :bucketing_key)
|> validate_key(matching, "matching_key")
|> validate_key(traffic_type, "traffic_type")
end
defp validate_composite_key(errors, key) do
add_error(
errors,
"`key` must be a map with a `matching_key` and `traffic_type`, got #{inspect(key)}"
)
end
defp validate_keys(errors, keys, key_name) when is_list(keys) do
Enum.reduce(keys, errors, fn key, acc ->
validate_key(acc, key, key_name)
end)
end
defp validate_key(errors, nil, key_name),
do: [non_empty_string_msg(key_name, nil) | errors]
defp validate_key(errors, key, key_name) when is_binary(key) do
errors =
case String.trim(key) do
"" -> add_error(errors, non_empty_string_msg(key_name, key))
_key -> errors
end
case String.length(key) > 250 do
true ->
add_error(errors, "`#{key_name}` must be less than 250 characters, got `#{inspect(key)}`")
false ->
errors
end
end
defp validate_key(errors, key, key_name) when is_atom(key) do
validate_key(errors, Atom.to_string(key), key_name)
end
defp validate_key(errors, key, key_name) when is_integer(key) do
validate_key(errors, Integer.to_string(key), key_name)
end
defp validate_key(errors, key, key_name) do
add_error(errors, non_empty_string_msg(key_name, key))
end
defp add_error(errors, error) do
[error | errors]
end
defp non_empty_string_msg(key_name, key) do
"`#{key_name}` must be a non-empty string, got `#{inspect(key)}`"
end
defp treatments_server do
Application.get_env(:split_client, :treatments_server, Treatments)
end
defmodule InvalidParamsException do
@moduledoc false
defexception [:message]
@impl true
def exception(errors) do
msg = Enum.join(errors, "\n")
%__MODULE__{message: msg}
end
end
end