defmodule SplitClient.Boundary.Evaluator do # Split.io has an Evaluator component that can be deployed in your # infrastructure that sits between your applications and Split.io # servers. It provides a REST API for accessing Split.io # data and is meant to be used in cases where Split.io has not # created an SDK for a specific language. As of April 2022, there # is no Elixir SDK for Split.io which necessitates relying on # a deployed Evaluator instance. # @see https://help.split.io/hc/en-us/articles/360020037072-Split-Evaluator @moduledoc false @behaviour SplitClient.Boundary.DataSourceBehaviour alias SplitClient.Boundary.OrderedAttributes alias SplitClient.Treatment @impl true def get_treatment(key, split_name, opts \\ []) do make_treatment_request( "client/get-treatment-with-config", [ {"key", key}, {"split-name", split_name} ], &format_single_treatment_response/1, opts ) end @impl true def get_treatments(key, split_names, opts \\ []) when is_list(split_names) do make_treatment_request( "client/get-treatments-with-config", [ {"key", key}, {"split-names", Enum.join(split_names, ",")} ], &format_map_response/1, opts ) end @impl true def get_all_treatments(keys, opts \\ []) when is_list(keys) do make_treatment_request( "client/get-all-treatments-with-config", [ {"keys", format_keys(keys)} ], &format_traffic_type_response/1, opts ) end defp format_keys(keys) when is_map(keys) do Enum.reduce(keys, %{}, fn {key, value}, acc -> Map.put(acc, camelize(Atom.to_string(key)), value) end) |> Jason.encode!() end defp format_keys(composite_keys) do keys = Enum.map_join(composite_keys, ",", fn keys -> format_keys(keys) end) "[#{keys}]" end defp camelize(value) do value = Macro.camelize(value) {first, rest} = String.split_at(value, 1) String.downcase(first) <> rest end defp make_treatment_request(path, params, response_formatter, opts) do params = params |> include_optional_params(opts) |> URI.encode_query() url = "#{evaluator_base_url()}/#{path}?#{params}" headers = [auth_header()] case http().get(url, headers) do {:ok, %{body: body}} -> format_response(Jason.decode!(body), response_formatter) error -> error end end defp include_optional_params(params, opts) do Enum.reduce(opts, params, fn {key, value}, acc -> format_param(acc, key, value) end) end defp format_param(params, :attributes, attributes) do attributes = OrderedAttributes.new(attributes) Enum.concat(params, [{"attributes", Jason.encode!(attributes)}]) end defp format_param(params, :bucketing_key, key) do Enum.concat(params, [{"bucketing-key", key}]) end defp format_param(params, _key, _value), do: params defp evaluator_base_url do Application.fetch_env!(:split_client, :evaluator_url) |> trim_trailing_slash() end defp trim_trailing_slash(url) do String.trim_trailing(url, "/") end defp auth_header do {"Authorization", evaluator_auth_token()} end defp evaluator_auth_token do Application.fetch_env!(:split_client, :evaluator_auth_token) end defp http do Application.get_env(:split_client, :http, SplitClient.Boundary.HTTP) end defp format_response(%{"error" => message}, _formatter) do {:error, message} end defp format_response(data, formatter) do {:ok, formatter.(data)} end defp format_single_treatment_response(data) do Treatment.new( split_name: data["splitName"], treatment: data["treatment"], config: Map.get(data, "config") ) end defp format_map_response(data) do Enum.reduce(data, %{}, fn {key, value}, acc -> Map.put( acc, key, Treatment.new( split_name: key, treatment: value["treatment"], config: Map.get(value, "config") ) ) end) end defp format_traffic_type_response(data) do Enum.reduce(data, %{}, fn {traffic_type, value}, acc -> Map.put(acc, traffic_type, format_map_response(value)) end) end end