Packages

Cased provides user and employee activity audit trails to companies that need to monitor access to information or demonstrate regulatory compliance.

Current section

Files

Jump to
cased lib cased context.ex
Raw

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