Skip to content

Instantly share code, notes, and snippets.

@kipcole9
Last active October 24, 2023 22:13
Show Gist options
  • Save kipcole9/0bd4c6fb6109bfec9955f785087f53fb to your computer and use it in GitHub Desktop.
Save kipcole9/0bd4c6fb6109bfec9955f785087f53fb to your computer and use it in GitHub Desktop.
Helpers for Elixir Maps: underscore, atomise and stringify map keys
defmodule Map.Helpers do
@moduledoc """
Functions to transform maps
"""
@doc """
Convert map string camelCase keys to underscore_keys
"""
def underscore_keys(nil), do: nil
def underscore_keys(map = %{}) do
map
|> Enum.map(fn {k, v} -> {Macro.underscore(k), underscore_keys(v)} end)
|> Enum.map(fn {k, v} -> {String.replace(k, "-", "_"), v} end)
|> Enum.into(%{})
end
# Walk the list and atomize the keys of
# of any map members
def underscore_keys([head | rest]) do
[underscore_keys(head) | underscore_keys(rest)]
end
def underscore_keys(not_a_map) do
not_a_map
end
@doc """
Convert map string keys to :atom keys
"""
def atomize_keys(nil), do: nil
# Structs don't do enumerable and anyway the keys are already
# atoms
def atomize_keys(struct = %{__struct__: _}) do
struct
end
def atomize_keys(map = %{}) do
map
|> Enum.map(fn {k, v} -> {String.to_atom(k), atomize_keys(v)} end)
|> Enum.into(%{})
end
# Walk the list and atomize the keys of
# of any map members
def atomize_keys([head | rest]) do
[atomize_keys(head) | atomize_keys(rest)]
end
def atomize_keys(not_a_map) do
not_a_map
end
@doc """
Convert map atom keys to strings
"""
def stringify_keys(nil), do: nil
def stringify_keys(map = %{}) do
map
|> Enum.map(fn {k, v} -> {Atom.to_string(k), stringify_keys(v)} end)
|> Enum.into(%{})
end
# Walk the list and stringify the keys of
# of any map members
def stringify_keys([head | rest]) do
[stringify_keys(head) | stringify_keys(rest)]
end
def stringify_keys(not_a_map) do
not_a_map
end
@doc """
Deep merge two maps
"""
def deep_merge(left, right) do
Map.merge(left, right, &deep_resolve/3)
end
# Key exists in both maps, and both values are maps as well.
# These can be merged recursively.
defp deep_resolve(_key, left = %{}, right = %{}) do
deep_merge(left, right)
end
# Key exists in both maps, but at least one of the values is
# NOT a map. We fall back to standard merge behavior, preferring
# the value on the right.
defp deep_resolve(_key, _left, right) do
right
end
end
@mmendez512
Copy link

You can also use String.to_existing_atom\1 to prevent atom size from reaching the limit. If the key is not an existing atom then use the String.to_atom first (otherwise it'll throw an Argument Exception). Here's a simple function to handle both cases:

def safe_to_atom(k) do try do String.to_existing_atom(k) rescue ArgumentError -> String.to_atom(k) end end

@mmendez512
Copy link

But you still want to make sure the data you're converting comes from a trusted source

@onixus74
Copy link

Thank you!

@jeremyjh
Copy link

jeremyjh commented Dec 2, 2019

@mmendez512 the whole point of String.to_existing_atom is for when you cannot not trust the input data, such as when it is sent from a web browser. Catching the exception and falling back to to_atom is redundant, you may as well just use to_atom when you trust the input data, and you can just let it fail when you do not trust the input data and get non-matching keys.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment