LemonCore.MapHelpers (lemon_core v0.1.0)

View Source

Helpers for accessing map keys that may be stored as either atoms or strings.

Many parts of the codebase deal with maps that may have atom keys or string keys (e.g. from JSON decoding). get_key/2 unifies access by trying both representations, eliminating the repeated Map.get(m, k) || Map.get(m, Atom.to_string(k)) pattern found across the codebase.

Summary

Functions

Recursively merges override into base, with the override side winning.

Gets a value from a map, trying both the given key and its atom/string counterpart.

Merges an optional overrides value into a base config map.

Recursively converts all map keys to strings.

Functions

deep_merge(base, override)

@spec deep_merge(term(), term()) :: term()

Recursively merges override into base, with the override side winning.

At any key present in both maps, two map values are merged recursively; every other pair — including a map facing a non-map, on either side — is replaced by the override value. When either top-level argument is not a map, override is returned as-is. Merging an empty map on either side is therefore the identity on the other.

This is the shared implementation behind config layering (global ← project ← overrides); it is intentionally not associative in general, because nesting vs. replacement depends on the shape at each level.

Examples

iex> LemonCore.MapHelpers.deep_merge(%{a: %{x: 1, y: 2}}, %{a: %{y: 3, z: 4}})
%{a: %{x: 1, y: 3, z: 4}}

iex> LemonCore.MapHelpers.deep_merge(%{a: 1}, %{})
%{a: 1}

iex> LemonCore.MapHelpers.deep_merge(%{}, %{a: 1})
%{a: 1}

iex> LemonCore.MapHelpers.deep_merge(%{a: %{x: 1}}, %{a: 2})
%{a: 2}

iex> LemonCore.MapHelpers.deep_merge(%{a: 1}, "scalar")
"scalar"

get_key(map, key)

@spec get_key(map(), atom()) :: any()
@spec get_key(map(), String.t()) :: any()

Gets a value from a map, trying both the given key and its atom/string counterpart.

When key is an atom, tries the atom first then its string representation. When key is a string, tries the string first then its existing atom representation (via String.to_existing_atom/1 to avoid atom table pollution).

Returns nil when the key is not found under either representation.

Examples

iex> LemonCore.MapHelpers.get_key(%{name: "Alice"}, :name)
"Alice"

iex> LemonCore.MapHelpers.get_key(%{"name" => "Alice"}, :name)
"Alice"

iex> LemonCore.MapHelpers.get_key(%{name: "Alice"}, "name")
"Alice"

iex> LemonCore.MapHelpers.get_key(%{"age" => 30}, "age")
30

iex> LemonCore.MapHelpers.get_key(%{}, :missing)
nil

merge_config(base, opts)

@spec merge_config(map(), term()) :: map()

Merges an optional overrides value into a base config map.

Handles nil (no-op), maps (direct merge), keyword lists (converted to map then merged), and ignores anything else.

Examples

iex> LemonCore.MapHelpers.merge_config(%{a: 1}, %{b: 2})
%{a: 1, b: 2}

iex> LemonCore.MapHelpers.merge_config(%{a: 1}, nil)
%{a: 1}

iex> LemonCore.MapHelpers.merge_config(%{a: 1}, [b: 2])
%{a: 1, b: 2}

stringify_keys(map)

@spec stringify_keys(map()) :: map()
@spec stringify_keys(list()) :: list()
@spec stringify_keys(term()) :: term()

Recursively converts all map keys to strings.

Traverses nested maps and lists, converting atom keys (and any other key types) to their string representation via to_string/1.

Examples

iex> LemonCore.MapHelpers.stringify_keys(%{foo: %{bar: 1}})
%{"foo" => %{"bar" => 1}}

iex> LemonCore.MapHelpers.stringify_keys(%{"already" => "string"})
%{"already" => "string"}

iex> LemonCore.MapHelpers.stringify_keys([%{a: 1}, %{b: 2}])
[%{"a" => 1}, %{"b" => 2}]