Current section

Files

Jump to
trogon_error lib trogon error metadata.ex
Raw

lib/trogon/error/metadata.ex

defmodule Trogon.Error.Metadata do
@moduledoc """
A structured container for error metadata with support for visibility controls.
This module provides a wrapper around metadata entries, where each entry
is a `MetadataValue` struct containing both the value and its visibility level.
## Examples
iex> metadata = Trogon.Error.Metadata.new(%{"user_id" => "123", "action" => "login"})
iex> metadata["user_id"].value
"123"
iex> metadata["user_id"].visibility
:INTERNAL
"""
alias Trogon.Error.MetadataValue
@enforce_keys [:entries]
defstruct [:entries]
@type t :: %__MODULE__{entries: %{String.t() => MetadataValue.t()}}
@typedoc """
Raw input format for creating Metadata via `new/1`.
Values without explicit visibility default to `:INTERNAL`.
See `t:Trogon.Error.MetadataValue.raw/0` for value formats.
%{
"user_id" => "123", # visibility: :INTERNAL (default)
"api_key" => {"secret", :PRIVATE} # visibility: :PRIVATE (explicit)
}
"""
@type raw :: %{String.t() => MetadataValue.raw()}
@doc """
Guard that checks if metadata is empty (has no entries).
## Examples
iex> import Trogon.Error.Metadata, only: [is_empty_metadata: 1]
iex> empty = Trogon.Error.Metadata.new()
iex> is_empty_metadata(empty)
true
iex> import Trogon.Error.Metadata, only: [is_empty_metadata: 1]
iex> with_data = Trogon.Error.Metadata.new(%{"key" => "value"})
iex> is_empty_metadata(with_data)
false
"""
defguard is_empty_metadata(metadata) when is_struct(metadata, __MODULE__) and map_size(metadata.entries) == 0
@behaviour Access
@doc """
Fetches a metadata entry by key.
## Examples
iex> metadata = Trogon.Error.Metadata.new(%{"user_id" => "123"})
iex> Trogon.Error.Metadata.fetch(metadata, "user_id")
{:ok, %Trogon.Error.MetadataValue{value: "123", visibility: :INTERNAL}}
iex> metadata = Trogon.Error.Metadata.new(%{})
iex> Trogon.Error.Metadata.fetch(metadata, "missing")
:error
"""
@impl Access
def fetch(%__MODULE__{entries: entries}, key) do
Map.fetch(entries, key)
end
@doc """
Gets and updates a metadata entry.
## Examples
iex> metadata = Trogon.Error.Metadata.new(%{"count" => "1"})
iex> {old_value, new_metadata} = Trogon.Error.Metadata.get_and_update(metadata, "count", fn old ->
...> {old, %Trogon.Error.MetadataValue{value: "2", visibility: :INTERNAL}}
...> end)
iex> old_value.value
"1"
iex> new_metadata["count"].value
"2"
"""
@impl Access
def get_and_update(%__MODULE__{entries: entries} = metadata, key, fun) do
{value, new_entries} = Map.get_and_update(entries, key, fun)
{value, %{metadata | entries: new_entries}}
end
@doc """
Removes and returns a metadata entry by key.
## Examples
iex> metadata = Trogon.Error.Metadata.new(%{"user_id" => "123", "action" => "login"})
iex> {value, new_metadata} = Trogon.Error.Metadata.pop(metadata, "user_id")
iex> value.value
"123"
iex> map_size(new_metadata.entries)
1
"""
@impl Access
def pop(%__MODULE__{entries: entries} = metadata, key) do
{value, new_entries} = Map.pop(entries, key)
{value, %{metadata | entries: new_entries}}
end
@doc """
Creates an empty Metadata struct.
## Examples
iex> Trogon.Error.Metadata.new()
%Trogon.Error.Metadata{entries: %{}}
"""
@spec new() :: t()
def new do
%__MODULE__{entries: %{}}
end
@doc """
Creates a Metadata struct from a map. See `t:raw/0` for accepted input formats.
## Examples
iex> metadata = Trogon.Error.Metadata.new(%{"user_id" => "123", "secret" => {"api-key", :PRIVATE}})
iex> metadata["user_id"].visibility
:INTERNAL
iex> metadata["secret"].visibility
:PRIVATE
"""
@spec new(%{term() => MetadataValue.t() | {term(), MetadataValue.visibility()} | term()}) :: t()
def new(entries) do
%__MODULE__{entries: Map.new(entries, &to_entry/1)}
end
@doc """
Merges two Metadata structs. The second argument takes precedence for duplicate keys.
## Examples
iex> metadata1 = Trogon.Error.Metadata.new(%{"user_id" => "123", "action" => "login"})
iex> metadata2 = Trogon.Error.Metadata.new(%{"user_id" => "456", "session" => "abc"})
iex> merged = Trogon.Error.Metadata.merge(metadata1, metadata2)
iex> merged["user_id"].value
"456"
iex> map_size(merged.entries)
3
"""
@spec merge(t(), t()) :: t()
def merge(%__MODULE__{entries: entries1}, %__MODULE__{entries: entries2}) do
%__MODULE__{entries: Map.merge(entries1, entries2)}
end
defp to_entry({key, %MetadataValue{} = value}) do
{entry_key(key), value}
end
defp to_entry({key, {value, visibility}}) do
{entry_key(key), MetadataValue.new(value, visibility)}
end
defp to_entry({key, value}) do
{entry_key(key), MetadataValue.new(value)}
end
defp entry_key(key) when is_binary(key), do: key
defp entry_key(key), do: to_string(key)
end
defimpl Enumerable, for: Trogon.Error.Metadata do
def count(%Trogon.Error.Metadata{entries: entries}) do
{:ok, map_size(entries)}
end
def member?(%Trogon.Error.Metadata{entries: entries}, {key, value}) do
{:ok, Map.has_key?(entries, key) and Map.get(entries, key) == value}
end
def member?(%Trogon.Error.Metadata{entries: entries}, key) when is_binary(key) do
{:ok, Map.has_key?(entries, key)}
end
def member?(_, _) do
{:ok, false}
end
def reduce(%Trogon.Error.Metadata{entries: entries}, acc, fun) do
Enumerable.Map.reduce(entries, acc, fun)
end
def slice(%Trogon.Error.Metadata{entries: entries}) do
entries_list = Map.to_list(entries)
{:ok, length(entries_list), &Enum.slice(entries_list, &1, &2)}
end
end