Packages
unleash_fresha
2.0.0-git-75d8
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/propagation/serialization.ex
defmodule Unleash.Propagation.Serialization do
@moduledoc """
Module handling the (de)serialization of Unleash objects for propagation.
See `Unleash.Propagation`.
"""
defmodule ContextDeserializationError do
@moduledoc "An error representing context deserialization failure."
defexception [:message, :details]
@type t :: %__MODULE__{message: String.t(), details: term()}
end
defmodule OverridesDeserializationError do
@moduledoc "An error representing overrides deserialization failure."
defexception [:message, :details]
@type t :: %__MODULE__{message: String.t(), details: term()}
end
defmodule ContextSerializationError do
@moduledoc "An error representing context serialization failure."
defexception [:message, :details]
@type t :: %__MODULE__{message: String.t(), details: term()}
end
defmodule OverridesSerializationError do
@moduledoc "An error representing overrides serialization failure."
defexception [:message, :details]
@type t :: %__MODULE__{message: String.t(), details: term()}
end
defmodule ImpressionsDeserializationError do
@moduledoc "An error representing impressions deserialization failure."
defexception [:message, :details]
@type t :: %__MODULE__{message: String.t(), details: term()}
end
@propagated_context_schema %{
"userId" => :string,
"sessionId" => :string,
"properties" => {:custom, &__MODULE__.validate_context_properties/1}
}
@spec deserialize_context(binary()) :: {:ok, Unleash.context()} | {:error, ContextDeserializationError.t()}
def deserialize_context(serialized_context) do
with {:ok, payload} <- Jason.decode(serialized_context),
{:ok, validated} <- Peri.validate(@propagated_context_schema, payload) do
context =
Map.reject(
%{
user_id: validated["userId"],
session_id: validated["sessionId"],
properties: validated["properties"]
},
fn {_key, val} -> is_nil(val) end
)
{:ok, context}
else
{:error, %Jason.DecodeError{} = jason_error} ->
{:error, %ContextDeserializationError{message: "invalid JSON", details: jason_error}}
{:error, [%Peri.Error{} | _]} = peri_errors ->
{:error, %ContextDeserializationError{message: "invalid context object", details: peri_errors}}
end
end
@spec deserialize_context!(binary()) :: Unleash.context()
def deserialize_context!(serialized_context) do
case deserialize_context(serialized_context) do
{:ok, context} -> context
{:error, exception} -> raise exception
end
end
@doc """
Serializes context for further propagation to downstream services.
Note that only propagation-friendly context fields are serialized.
"""
@spec serialize_context!(Unleash.context()) :: String.t()
def serialize_context!(context) do
%{
userId: context[:user_id],
sessionId: context[:session_id],
properties: context[:properties]
}
|> Map.update(:properties, nil, &stringify_properties/1)
|> Map.reject(fn {_key, val} -> is_nil(val) end)
|> Jason.encode!()
rescue
err ->
reraise(
ContextSerializationError,
[message: "failed to serialize context", details: err],
__STACKTRACE__
)
end
@propagated_overrides_schema %{
"name" => {:required, :string},
"enabled" => {:required, {:either, {:boolean, :string}}}
}
@spec deserialize_overrides(binary()) :: {:ok, Unleash.context()} | {:error, OverridesDeserializationError.t()}
def deserialize_overrides(serialized_overrides) do
with {:ok, payload} <- Jason.decode(serialized_overrides),
{_tag, payload} when is_list(payload) <- {:check_list, payload},
{:ok, overrides} <- parse_overrides(payload) do
{:ok, overrides}
else
{:error, %Jason.DecodeError{} = jason_error} ->
{:error, %OverridesDeserializationError{message: "invalid JSON", details: jason_error}}
{:check_list, _not_a_list} ->
{:error, %OverridesDeserializationError{message: "not a list of overrides objects"}}
{:error, [%Peri.Error{} | _] = peri_errors} ->
{:error, %OverridesDeserializationError{message: "invalid override object", details: peri_errors}}
end
end
@spec deserialize_overrides!(binary()) :: Unleash.overrides()
def deserialize_overrides!(serialized_overrides) do
case deserialize_overrides(serialized_overrides) do
{:ok, overrides} -> overrides
{:error, exception} -> raise exception
end
end
@doc """
Serializes overrides for further propagation to downstream services.
"""
@spec serialize_overrides!(Unleash.overrides()) :: String.t()
def serialize_overrides!(overrides) do
overrides
|> Enum.map(&objectify_override_entry/1)
|> Jason.encode!()
rescue
err ->
reraise(
OverridesSerializationError,
[message: "failed to serialize overrides", details: err],
__STACKTRACE__
)
end
@spec serialize_impressions!([Unleash.impression()]) :: String.t()
def serialize_impressions!(impressions) do
impressions
|> Enum.map(fn %{feature_name: feature, enabled: enabled} -> %{name: feature, enabled: enabled} end)
|> Jason.encode!()
end
@spec deserialize_impressions(binary()) :: {:ok, [Unleash.impression()]} | {:error, ImpressionsDeserializationError.t()}
def deserialize_impressions(serialized_impressions) do
case Jason.decode(serialized_impressions) do
{:ok, decoded} when is_list(decoded) ->
parse_impressions(decoded)
{:ok, _not_a_list} ->
{:error, %ImpressionsDeserializationError{message: "expected a list"}}
{:error, err} ->
{:error, %ImpressionsDeserializationError{message: "invalid JSON", details: err}}
end
end
@spec deserialize_impressions!(binary()) :: [Unleash.impression()]
def deserialize_impressions!(serialized_impressions) do
case deserialize_impressions(serialized_impressions) do
{:ok, impressions} -> impressions
{:error, exception} -> raise exception
end
end
@spec parse_impressions(list(term())) :: {:ok, [Unleash.impression()]} | {:error, ImpressionsDeserializationError.t()}
defp parse_impressions(unparsed_impressions) do
unparsed_impressions
|> Enum.reduce_while([], fn unparsed_impression, acc ->
case parse_impression(unparsed_impression) do
{:ok, impression} -> {:cont, [impression | acc]}
{:error, err} -> {:halt, {:error, err}}
end
end)
|> case do
{:error, err} -> {:error, err}
impressions -> {:ok, impressions}
end
end
@spec parse_impression(term()) :: {:ok, Unleash.impression()} | {:error}
defp parse_impression(%{"name" => name, "enabled" => enabled}) when is_binary(name) and is_boolean(enabled) do
{:ok,
%{
feature_name: name,
enabled: enabled
}}
end
defp parse_impression(_val), do: {:error, %ImpressionsDeserializationError{message: "invalid impression object"}}
defp stringify_properties(nil), do: nil
defp stringify_properties(%{} = props) do
Map.new(
props,
fn
{key, val} when is_atom(key) -> {Unleash.stringify_context_key(key), val}
{key, val} when is_binary(key) -> {key, val}
end
)
end
defp objectify_override_entry({feature_name, state}) do
state =
case state do
:off -> false
:on -> true
variant when is_binary(variant) -> variant
end
%{"name" => feature_name, "enabled" => state}
end
@spec parse_overrides(list(term())) :: {:ok, Unleash.overrides()} | {:error, Peri.Error.t()}
defp parse_overrides(overrides_payload) do
overrides_payload
|> Enum.reduce_while(
%{},
fn override, acc ->
case Peri.validate(@propagated_overrides_schema, override) do
{:ok, %{"name" => name, "enabled" => enabled}} ->
state =
case enabled do
true -> :on
false -> :off
variant when is_binary(variant) -> variant
end
{:cont, Map.put(acc, name, state)}
{:error, error} ->
{:halt, {:error, error}}
end
end
)
|> case do
{:error, error} -> {:error, error}
overrides -> {:ok, overrides}
end
end
def validate_context_properties(properties) when is_map(properties) do
properties
|> Enum.all?(fn {key, val} -> is_binary(key) and is_binary(val) end)
|> case do
true -> :ok
false -> {:error, "properties should be a flat object with string values", []}
end
end
def validate_context_properties(_properties), do: {:error, "invalid properties", []}
end