Current section
Files
Jump to
Current section
Files
lib/cased/context.ex
defmodule Cased.Context do
@moduledoc """
Used to set contextual data when publishing audit events with `Cased.publish/2`.
Stored using the process dictionary, the context is tied to the current process.
"""
@typedoc false
@type stack :: [map()]
@process_dict_key :cased_context
@doc """
Get the context as a map.
## Example
Setting a value and retrieving the context as a map:
```
iex> Cased.Context.put(:location, "https://example.com")
:ok
iex> Cased.Context.to_map()
%{location: "https://example.com"}
"""
@spec to_map() :: map()
def to_map() do
get_stack()
|> List.foldl(%{}, &DeepMerge.deep_merge/2)
end
@doc """
Add a value to the context.
## Example
Adding a `:location` to the context:
```
iex> Cased.Context.put(:location, "https://example.com")
:ok
```
Note that the return value of `put/2` is `:ok` rather than what you might
expect for, e.g., `Map.put/2`, since we don't incur the cost of recalculating
the full context when a value is added.
"""
@spec put(key :: atom(), value :: any()) :: :ok
def put(key, value) do
merge(%{key => value})
end
@doc """
Add a value to the context for the duration of a function execution.
## Example
Adding a `:location` to the context, temporarily:
```
iex> Cased.Context.put(:location, "https://example.com", fn ->
iex> # Use Cased.publish/2
iex> :your_return_value
iex> end)
:your_return_value
```
Note that, unlike `put/2`, the return value of `put/3` is the value of the
last statement in your function.
"""
@spec put(key :: atom(), value :: any(), scope :: function()) :: any()
def put(key, value, scope) do
merge(%{key => value}, scope)
end
@doc """
Add multiple values value to the context.
## Example
Adding a `:location` and `:http_method` to the context:
```
iex> Cased.Context.merge(%{
iex> location: "https://example.com",
iex> http_method: :patch
iex> })
:ok
```
Note that the return value of `merge/2` is `:ok` rather than what you might
expect for, e.g., `Map.merge/2`, since we don't incur the cost of
recalculating the full context when a value is added.
"""
@spec merge(data :: map() | Keyword.t()) :: :ok
def merge(data) when is_list(data) do
data
|> Map.new()
|> merge()
end
def merge(data) when is_map(data) do
[data | get_stack()]
|> put_stack()
:ok
end
@doc """
Add multiple values to the context for the duration of a function execution.
## Example
Adding a `:location` and `:http_method` to the context, temporarily:
```
iex> tmp_context = %{
iex> location: "https://example.com",
iex> http_method: :patch
iex> }
iex> Cased.Context.merge(tmp_context, fn ->
iex> # Use Cased.publish/2
iex> :your_return_value
iex> end)
:your_return_value
```
Note that, unlike `merge/1`, the return value of `merge/2` is the value of the
last statement in your function.
"""
@spec merge(data :: map(), function()) :: any()
def merge(data, scope) do
merge(data)
result = scope.()
pop_stack()
result
end
@doc """
Reset the context data.
## Examples
Setting a value in the context and then resetting it (note `reset/0` returns
`:ok` as context data was deleted):
```
iex> Cased.Context.put(:item, "value")
iex> Cased.Context.to_map()
%{item: "value"}
iex> Cased.Context.reset()
:ok
iex> Cased.Context.to_map()
%{}
```
Resetting an empty context returns `nil`:
```
iex> Cased.Context.reset()
nil
```
"""
@spec reset() :: nil | :ok
def reset() do
case Process.delete(@process_dict_key) do
nil ->
nil
[] ->
nil
_other ->
:ok
end
end
##
# Utilities
@doc false
@spec get(key :: atom(), default :: any()) :: any()
def get(key, default \\ nil) do
to_map()
|> Map.get(key, default)
end
@doc false
@spec has_key?(atom()) :: boolean()
def has_key?(key) do
get_stack()
|> Enum.any?(&Map.has_key?(&1, key))
end
@doc false
@spec stack_size() :: non_neg_integer()
def stack_size() do
get_stack()
|> length()
end
@doc false
@spec has_stack?() :: boolean()
def has_stack? do
!is_nil(Process.get(@process_dict_key))
end
@spec get_stack() :: stack()
defp get_stack() do
Process.get(@process_dict_key, [])
end
@spec put_stack(stack :: stack()) :: any() | nil
defp put_stack(stack) do
Process.put(@process_dict_key, stack)
end
@spec pop_stack() :: nil | map()
defp pop_stack() do
get_stack()
|> tl()
|> put_stack()
end
end