defmodule ConfigCat.User do
@moduledoc """
Represents a user in your system; used for ConfigCat's Targeting feature.
The User Object is an optional parameter when getting a feature flag or
setting value from ConfigCat. It allows you to pass potential [Targeting
rule](https://configcat.com/docs/advanced/targeting) variables to the
ConfigCat SDK.
Has the following properties:
- `identifier`: **REQUIRED** We recommend using a primary key, email address,
or session ID. Enables ConfigCat to differentiate your users from each other
and to evaluate the setting values for percentage-based targeting.
- `country`: **OPTIONAL** Fill this for location or country-based targeting.
e.g: Turn on a feature for users in Canada only.
- `email`: **OPTIONAL** By adding this parameter you will be able to create
Email address-based targeting. e.g: Only turn on a feature for users with
@example.com addresses.
- `custom`: **OPTIONAL** This parameter will let you create targeting based on
any user data you like. e.g: age, subscription type, user role, device type,
app version number, etc. `custom` is a map containing string or atom keys.
When evaluating targeting rules, keys are case-sensitive, so make sure you
specify your keys with the same capitalization as you use when defining your
targeting rules.
All comparators support string values as User Object attributes (in some cases
they need to be provided in a specific format though, see below), but some of
them also support other types of values. It depends on the comparator how the
values will be handled. The following rules apply:
Text-based comparators (EQUALS, IS_ONE_OF, etc.)
- accept string values,
- all other values are automatically converted to string (a warning will be
logged but evaluation will continue as normal).
SemVer-based comparators (IS_ONE_OF_SEMVER, LESS_THAN_SEMVER,
GREATER_THAN_SEMVER, etc.)
- accept string values containing a properly formatted, valid semver value
- all other values are considered invalid (a warning will be logged and the
currently evaluated targeting rule will be skipped).
Number-based comparators (EQUALS_NUMBER, LESS_THAN_NUMBER,
GREATER_THAN_OR_EQUAL_NUMBER, etc.)
- accept float values and all other numeric values which can safely be
converted to float,
- accept string values containing a properly formatted, valid float value,
- all other values are considered invalid (a warning will be logged and the
currently evaluated targeting rule will be skipped).
Date time-based comparators (BEFORE_DATETIME / AFTER_DATETIME)
- accept `DateTime` and `NaiveDateTime` values, which are automatically
converted to a second-based fractional Unix timestamp (`NaiveDateTime`
values are considered to be in UTC),
- accept float values representing a fractional second-based Unix timestamp
and all other numeric values which can safely be converted to float,
- accept string values containing a properly formatted, valid float value,
- all other values are considered invalid (a warning will be logged and the
currently evaluated targeting rule will be skipped).
String array-based comparators (ARRAY_CONTAINS_ANY_OF /
ARRAY_NOT_CONTAINS_ANY_OF)
- accept arrays of strings,
- accept string values containing a valid JSON string which can be
deserialized to an array of strings,
- all other values are considered invalid (a warning will be logged and the
currently evaluated targeting rule will be skipped).
While `ConfigCat.User` is a struct, we also provide the `new/2` function to
make it easier to create a new user object. Pass it the `identifier` and then
either a keyword list or map containing the other properties you want to
specify.
e.g. `ConfigCat.User.new("IDENTIFIER", email: "user@example.com")`
"""
use TypedStruct
alias ConfigCat.Config
typedstruct do
@typedoc "The ConfigCat user object."
field :country, String.t()
field :custom, custom(), default: %{}
field :email, String.t()
field :identifier, String.t(), enforce: true
end
@typedoc """
Custom properties for additional targeting options.
Can use either atoms or strings as keys; values must be strings.
Keys are case-sensitive and must match the targeting rule exactly.
"""
@type custom :: %{optional(String.t() | atom()) => String.t()}
@typedoc """
Additional values for creating a `User` struct.
Can be either a keyword list or a maps, but any keys that don't
match the field names of `t:t()` will be ignored.
"""
@type options :: keyword() | map()
@doc """
Creates a new ConfigCat.User Object.
This is provided as a convenience to make it easier to create a
new user object.
Pass it the `identifier` and then either a keyword list or map
containing the other properties you want to specify.
e.g. `ConfigCat.User.new("IDENTIFIER", email: "user@example.com")`
"""
@spec new(String.t(), options()) :: t()
def new(identifier, other_props \\ []) do
struct!(%__MODULE__{identifier: identifier}, other_props)
end
@doc false
@spec get_attribute(t(), String.t()) :: Config.value() | nil
def get_attribute(user, "Identifier"), do: user.identifier
def get_attribute(user, "Country"), do: user.country
def get_attribute(user, "Email"), do: user.email
def get_attribute(user, attribute), do: custom_attribute(user.custom, attribute)
defp custom_attribute(custom, attribute) do
case Enum.find(custom, fn {key, _value} ->
to_string(key) == attribute
end) do
{_key, value} -> value
_ -> nil
end
end
defimpl String.Chars do
@moduledoc false
alias ConfigCat.User
@spec to_string(User.t()) :: String.t()
def to_string(%User{} = user) do
%{
"Identifier" => user.identifier,
"Email" => user.email,
"Country" => user.country
}
|> Map.merge(user.custom)
|> Enum.reject(fn {_k, v} -> is_nil(v) end)
|> Map.new()
|> Jason.encode!()
end
end
end