Current section

Files

Jump to
split_client lib boundary data_source split_stub.ex
Raw

lib/boundary/data_source/split_stub.ex

defmodule SplitClient.Boundary.DataSource.SplitStub do
# Common functions for matching split data for
# development and testing
# Expects to work with Split data that's formatted
# like this
# ```elixir
# [
# %{
# "my_feature" => %SplitStub{
# treatment: "on",
# keys: "mock_user_id",
# traffic_type: "customer"
# }
# },
# %{
# "my_feature" => %SplitStub{
# treatment: "off",
# traffic_type: "customer"
# }
# }
# ]
# ```
@moduledoc false
alias SplitClient.Treatment
defstruct [:treatment, :keys, :traffic_type, :config]
@doc """
Create a new SplitStub
"""
def new(%__MODULE__{} = split), do: split
def new(fields) do
struct!(__MODULE__, massage_fields(fields))
end
defp massage_fields(%{} = fields) do
Enum.reduce(fields, %{}, fn {key, value}, acc ->
Map.put(acc, massage_key(key), value)
end)
end
defp massage_key(key) when is_binary(key) do
String.to_existing_atom(key)
end
defp massage_key(key), do: key
@doc """
Convert a list of splits to a using the struct
"""
def from_list(splits) when is_list(splits) do
Enum.map(splits, fn split ->
Enum.reduce(split, %{}, fn {split_name, data}, acc ->
Map.put(acc, split_name, new(data))
end)
end)
end
@doc """
Get all treatments grouped by traffic type
"""
def find_all_treatments(splits, keys) do
Enum.reduce(keys, %{}, fn composite_key, acc ->
%{matching_key: key, traffic_type: traffic_type} = composite_key
Map.put(acc, traffic_type, group_by_traffic_type(splits, key, traffic_type))
end)
end
defp group_by_traffic_type(splits, key, traffic_type) do
splits =
Enum.filter(splits, fn split ->
[split_name] = Map.keys(split)
split[split_name].traffic_type == traffic_type
end)
split_names = find_split_names(splits)
Enum.reduce(split_names, %{}, fn split_name, acc ->
Map.put(acc, split_name, find_treatment(splits, key, split_name))
end)
end
defp find_split_names(splits) do
Enum.map(splits, fn split ->
Map.keys(split) |> Enum.at(0)
end)
|> Enum.uniq()
end
@doc """
Find treatments for multiple split names
"""
def find_treatments(splits, key, split_names) do
Enum.reduce(split_names, %{}, fn split_name, acc ->
Map.put(acc, split_name, find_treatment(splits, key, split_name))
end)
end
@doc """
Find single treatment
"""
def find_treatment(splits, key, split_name) do
splits
|> Enum.filter(&Map.has_key?(&1, split_name))
|> find_matching_keys(split_name, key)
|> to_treatment(split_name)
end
@doc """
Take two lists of splits and merge them together with the 2nd
taking precedent
"""
@spec merge_splits(splits1 :: [map()], splits2 :: [map()]) :: [map()]
def merge_splits(splits1, splits2) do
remove_same_splits(splits1, splits2)
|> Enum.concat(splits2)
end
defp remove_same_splits(splits1, splits2) do
new_split_names = Enum.flat_map(splits2, &Map.keys(&1)) |> MapSet.new()
Enum.filter(splits1, fn split ->
Map.keys(split) |> MapSet.new() |> MapSet.disjoint?(new_split_names)
end)
end
defp find_matching_keys(splits, split_name, key) do
Enum.find(splits, fn split ->
keys_match?(get_in(split, [split_name, Access.key!(:keys)]), key)
end)
|> find_default_split(splits, split_name)
end
defp keys_match?(nil, _key), do: nil
defp keys_match?(split_keys, key) when is_list(split_keys),
do: Enum.member?(split_keys, key)
defp keys_match?(split_key, key), do: split_key == key
defp find_default_split(nil, splits, split_name) do
Enum.find(splits, fn split ->
!split[split_name].keys
end)
end
defp find_default_split(found_split, _splits, _split_name), do: found_split
defp to_treatment(nil, split_name) do
Treatment.new(split_name: split_name, treatment: "control")
end
defp to_treatment(split_data, split_name) do
data = split_data[split_name]
Treatment.new(
split_name: split_name,
treatment: data.treatment,
config: format_config(data.config)
)
end
defp format_config(nil), do: nil
defp format_config(config), do: Jason.decode!(config)
end