Skip to main content

lib/rulestead/targeting/audience_dependencies.ex

# credo:disable-for-this-file
defmodule Rulestead.Targeting.AudienceDependencies do
  @moduledoc false

  @segment_match "segment_match"
  @unavailable %{available?: false}

  @spec summarize(String.t() | atom() | nil, list()) :: [map()]
  def summarize(audience_key, authored_flags_or_payloads)
      when is_list(authored_flags_or_payloads) do
    target_audience_key = normalize_string(audience_key)

    authored_flags_or_payloads
    |> Enum.flat_map(&references_for_payload(target_audience_key, &1))
    |> Enum.sort_by(&sort_key/1)
  end

  def summarize(_audience_key, _authored_flags_or_payloads), do: []

  @spec reference_keys(list()) :: [String.t()]
  def reference_keys(references) when is_list(references) do
    references
    |> Enum.map(&(fetch(&1, :reference_key) |> normalize_string()))
    |> Enum.reject(&is_nil/1)
    |> Enum.uniq()
    |> Enum.sort()
  end

  def reference_keys(_references), do: []

  defp references_for_payload(nil, _payload), do: []

  defp references_for_payload(target_audience_key, payload) when is_map(payload) do
    flag = fetch(payload, :flag) || %{}
    ruleset = fetch(payload, :active_ruleset) || %{}
    flag_key = normalize_string(fetch(flag, :key) || fetch(payload, :flag_key))
    rules = fetch(ruleset, :rules) || []

    rules
    |> Enum.filter(&segment_match_for_audience?(&1, target_audience_key))
    |> Enum.map(fn rule ->
      build_reference(payload, flag_key, ruleset, rule)
    end)
  end

  defp references_for_payload(_target_audience_key, _payload), do: []

  defp segment_match_for_audience?(rule, target_audience_key) when is_map(rule) do
    rule_strategy(rule) == @segment_match and
      normalize_string(fetch(rule, :audience_key)) == target_audience_key
  end

  defp segment_match_for_audience?(_rule, _target_audience_key), do: false

  defp build_reference(payload, flag_key, ruleset, rule) do
    ruleset_version = fetch(ruleset, :version)
    rule_key = normalize_string(fetch(rule, :key))

    %{
      reference_key: reference_key(flag_key, ruleset_version, rule_key),
      resource_type: "flag",
      flag_key: flag_key,
      ruleset_version: ruleset_version,
      ruleset_status: normalize_string(fetch(ruleset, :status)),
      rule_key: rule_key,
      rule_strategy: @segment_match,
      rollout_context: context_or_unavailable(fetch(rule, :rollout)),
      lifecycle_context:
        context_or_unavailable(fetch(rule, :lifecycle) || fetch(payload, :lifecycle)),
      environment_key: normalize_string(fetch(payload, :environment_key)),
      tenant_key: normalize_string(fetch(payload, :tenant_key))
    }
  end

  defp reference_key(flag_key, ruleset_version, rule_key) do
    "flag:#{flag_key}:ruleset:#{ruleset_version}:rule:#{rule_key}"
  end

  defp sort_key(reference) do
    {
      reference.environment_key || "",
      reference.tenant_key || "",
      reference.flag_key || "",
      reference.ruleset_version || 0,
      reference.rule_key || ""
    }
  end

  defp rule_strategy(rule) do
    rule
    |> fetch(:strategy)
    |> normalize_string()
  end

  defp context_or_unavailable(nil), do: @unavailable

  defp context_or_unavailable(context) when is_map(context) do
    if map_size(context) == 0 do
      @unavailable
    else
      normalize_context(context)
    end
  end

  defp context_or_unavailable(_context), do: @unavailable

  defp normalize_context(context) do
    Map.new(context, fn {key, value} ->
      {normalize_context_key(key), normalize_context_value(value)}
    end)
  end

  defp normalize_context_key(key) when is_atom(key), do: key
  defp normalize_context_key(key), do: to_string(key)

  defp normalize_context_value(value) when is_map(value), do: normalize_context(value)

  defp normalize_context_value(value) when is_list(value),
    do: Enum.map(value, &normalize_context_value/1)

  defp normalize_context_value(value), do: value

  defp fetch(map, key) when is_map(map),
    do: Map.get(map, key) || Map.get(map, Atom.to_string(key))

  defp fetch(_map, _key), do: nil

  defp normalize_string(value) when is_binary(value) do
    value
    |> String.trim()
    |> case do
      "" -> nil
      normalized -> normalized
    end
  end

  defp normalize_string(value) when is_atom(value) and not is_nil(value),
    do: Atom.to_string(value)

  defp normalize_string(value), do: value
end