Packages

A collection of helpers to operate on nested maps. See README for the complete list of helpers. The test suite is also helpful.

Current section

Files

Jump to
digger lib protocols digger_protocol.ex
Raw

lib/protocols/digger_protocol.ex

defprotocol Digger do
@moduledoc """
Documentation for Digger Protocol
"""
@fallback_to_any true
alias Digger.Types
@doc """
'Atomize' a valid Types.data_type according to the protocol implementation
By default, atomizing a string that has no corresponding existing atom
creates a new one via `String.to_atom/1`, which is safe for trusted input
but can exhaust the atom table if used on untrusted/external data (atoms
are never garbage collected).
Pass `existing: true` to atomize via `String.to_existing_atom/1` instead:
strings that already have a matching atom are converted as usual, and
strings that don't are left as the original string rather than raising or
creating a new atom.
## Examples
iex> Digger.atomize(%{"user" => %{"name" => "Ada"}})
%{user: %{name: "Ada"}}
iex> Digger.atomize(%{"ok" => 1, "no_such_atom_xyz" => 2}, existing: true)
%{:ok => 1, "no_such_atom_xyz" => 2}
"""
@spec atomize(Types.data_type(), keyword()) :: Types.valid_return_type()
def atomize(
data_type,
opts \\ [type: :key, key_transform: :atomize, value_transform: :none]
)
@doc """
Camel case a valid Types.data_type according to the protocol implementation
The default is UpperCamelCase; pass `key_transform: :lower` for the
lowerCamelCase convention used in JavaScript APIs.
## Examples
iex> Digger.camel_case(%{user_id: 1})
%{UserId: 1}
iex> Digger.camel_case(
...> %{user_id: 1},
...> type: :key, key_transform: :lower, value_transform: :none
...> )
%{userId: 1}
"""
@spec camel_case(Types.data_type(), keyword()) :: Types.valid_return_type()
def camel_case(
data_type,
opts \\ [type: :key, key_transform: :upper, value_transform: :none]
)
@doc """
'Dasherize' a valid Types.data_type according to the protocol implementation
## Examples
iex> Digger.dasherize(%{"foo_bar" => 1})
%{"foo-bar" => 1}
"""
@spec dasherize(Types.data_type(), keyword()) :: Types.valid_return_type()
def dasherize(
data_type,
opts \\ [type: :key, key_transform: :dasherize, value_transform: :none]
)
@doc """
Flatten a nested map into a single-level map whose keys are the
separator-joined paths (default separator `"."`) to each leaf value
Path keys are converted to strings with `to_string/1`, so keys must
implement `String.Chars` (atoms, strings, and numbers all do).
Structs (including calendar types like `Date`), lists, and empty maps
are leaf values — they are never descended into. Non-map input passes
through unchanged.
## Examples
iex> Digger.flatten(%{a: %{b: 1}})
%{"a.b" => 1}
iex> Digger.flatten(%{"user" => %{"id" => 7}}, separator: "/")
%{"user/id" => 7}
"""
@spec flatten(Types.data_type(), keyword()) :: Types.valid_return_type()
def flatten(data_type, opts \\ [separator: "."])
@doc """
Lower case first letter of a valid Types.data_type according to the protocol implementation
## Examples
iex> Digger.lowercase_first(%{"FooBar" => 1})
%{"fooBar" => 1}
"""
@spec lowercase_first(Types.data_type(), keyword()) :: Types.valid_return_type()
def lowercase_first(
data_type,
opts \\ [type: :key, key_transform: :lower, value_transform: :none]
)
@doc """
snake_case a valid Types.data_type according to the protocol implementation
Only keys are transformed by default — string values pass through
unchanged.
## Examples
iex> Digger.snake_case(%{"userId" => 1, "displayName" => "johnDoe"})
%{"user_id" => 1, "display_name" => "johnDoe"}
"""
@spec snake_case(Types.data_type(), keyword()) :: Types.valid_return_type()
def snake_case(data_type, opts \\ [type: :key, key_transform: :snake, value_transform: :none])
@doc """
'Stringify' a valid Types.data_type according to the protocol implementation
## Examples
iex> Digger.stringify(%{user: %{name: "Ada"}})
%{"user" => %{"name" => "Ada"}}
"""
@spec stringify(Types.data_type(), keyword()) :: Types.valid_return_type()
def stringify(
data_type,
opts \\ [type: :key, key_transform: :stringify, value_transform: :none]
)
@doc """
Rebuild a nested map from a flattened one by splitting each string key
on the separator (default `"."`)
The exact inverse of `Digger.flatten/2` for the maps it produces:
`unflatten(flatten(map)) == map` whenever `map`'s keys are strings
that don't contain the separator. Maps with atom or number keys
round-trip to their string-keyed equivalent, since `flatten/2`
converts path keys to strings. Non-string keys are kept in place,
never split. Non-map input passes through unchanged.
## Examples
iex> Digger.unflatten(%{"a.b" => 1, "a.c" => 2})
%{"a" => %{"b" => 1, "c" => 2}}
iex> Digger.unflatten(%{"user/id" => 7}, separator: "/")
%{"user" => %{"id" => 7}}
"""
@spec unflatten(Types.data_type(), keyword()) :: Types.valid_return_type()
def unflatten(data_type, opts \\ [separator: "."])
@doc """
Upper case the first letter of a valid Types.data_type according to
the protocol implementation
## Examples
iex> Digger.upcase_first(%{"fooBar" => 1})
%{"FooBar" => 1}
"""
@spec upcase_first(Types.data_type(), keyword()) :: Types.valid_return_type()
def upcase_first(
data_type,
opts \\ [type: :key, key_transform: :upper, value_transform: :none]
)
end