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