Packages
unleash_fresha
2.0.0-git-67b2
5.1.4
5.1.3
5.1.2
5.1.1
5.1.0
5.0.0
4.0.0
4.0.0-git-bda3
3.0.1
3.0.0
2.1.0
2.0.1
2.0.0-git-75d8
2.0.0-git-67b2
2.0.0-git-4b61
2.0.0-git-49f4
1.16.0
1.15.0
1.14.1-git-e754
1.14.0
1.14.0-git-71e9
1.13.0
1.13.0-git-4ba6
1.12.0
1.11.0
1.11.0-git-1b62
1.10.2
1.10.2-git-b835
1.10.1
1.10.0
1.9.3-alpha0
1.9.2
1.9.1
1.9.0
An Unleash Feature Flag client for Elixir, forked from [unleash](https://gitlab.com/afontaine/unleash_ex)
Current section
Files
Jump to
Current section
Files
lib/unleash/context.ex
defmodule Unleash.Context do
@moduledoc """
The Unleash Context, mostly used to evaluate conditional activation strategies.
https://docs.getunleash.io/reference/unleash-context.
The Context has a predefined set of fields,
but custom context properties can be set in the `:properties` field.
Note that, for hystorical / backwards-compatibility reasons,
both atom and string keys are allowed in the properties map.
You are encouraged to use `find/2` to lookup context values in a way
that handles fallback to properties and the different style of keys correctly.
"""
# Note that some context keys are never referenced in the SDK,
# as the bundled in strategies only use a subset of them.
# We still support the full context for propagation purposes and
# because users of the library might define custom strategies using any context key.
@type t :: %{
optional(:app_name) => String.t(),
optional(:environment) => String.t(),
optional(:user_id) => String.t(),
optional(:session_id) => String.t(),
optional(:remote_address) => String.t(),
# Having to allow both atom and string keys in the properties map
# is an unfortunate consequence of the evolution of the SDK:
# it started out supporting only atom keys, but when we introduced
# the propagation mechanism, properties are read from request HTTP headers.
# and building atom at runtime from uncontrolled user-data is a big no-no.
optional(:properties) => %{(atom() | String.t()) => String.t()},
optional(:current_time) => String.t()
}
@context_fields [:app_name, :environment, :user_id, :session_id, :remote_address, :properties, :current_time]
@string_context_fields ["appName", "environment", "userId", "sessionId", "remoteAddress", "properties", "currentTime"]
@doc """
Looks for a context value, taking care of some of the nasty details
like fallback to `:properties` (a.k.a. custom context values)
with support for both atom and string keys.
Specifically:
1. If the key is a standard context key (either in its atom or string form),
it looks for it in the standard context fields, without fallbacking to
properties.
2. Otherwise, it looks for it in properties, trying both the atomized and the stringified
version of the key, starting from the supplied one.
NOTE that this can create atom at runtimes, which is dangerous.
It is highly recommended you do not call this function with values
from untrusted / unbounded sources.
"""
@spec find(t(), atom() | String.t()) :: term()
def find(context, key)
# Do not fallback to properties for known context fields
def find(context, key) when key in @context_fields, do: Map.get(context, key)
def find(context, key) when key in @string_context_fields, do: Map.get(context, atomize_context_key(key))
def find(%{properties: %{} = props}, key) when is_binary(key) do
Map.get_lazy(props, key, fn -> Map.get(props, atomize_context_key(key)) end)
end
def find(%{properties: %{} = props}, key) when is_atom(key) do
Map.get_lazy(props, key, fn -> Map.get(props, stringify_context_key(key)) end)
end
def find(_context_with_no_props, _nonstandard_context_key), do: nil
@spec atomize_context_key(String.t()) :: atom()
def atomize_context_key(string_key) when is_binary(string_key) do
# NOTE: potentially dangerous. Can create new atoms.
string_key |> Recase.to_snake() |> String.to_atom()
end
@spec stringify_context_key(atom()) :: String.t()
def stringify_context_key(atom_key) when is_atom(atom_key) do
atom_key |> Recase.to_camel() |> to_string()
end
end