-
-
Save kipcole9/0bd4c6fb6109bfec9955f785087f53fb to your computer and use it in GitHub Desktop.
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 |
Nice work. The only change I made to this was to allow stringify_keys
to work when the key is already a string. I did this with the following changes:
def stringify_keys(map = %{}) do
map
|> Enum.map(fn {k, v} -> {stringify_key(k), stringify_keys(v)} end)
|> Enum.into(%{})
end
defp stringify_key(key) when is_atom(key), do: Atom.to_string(key)
defp stringify_key(key), do: key
I added extra clausules for stringify_keys
to ignore ecto schemas and structs:
def stringify_keys(schema = %{__meta__: _}), do: schema
def stringify_keys(struct = %{struct: _}), do: struct
helpful resource, thanks
Thanks for the code! By replacing Atom.to_string(k)
with to_string(k)
, you'll cover cases where some keys are already strings. to_string/1
is part of the Kernel
module.
Just want to point out as well, it's generally a bad idea to blindly convert strings to atoms. It's "extremely discouraged", according to José Valim (creator of Elixir). This is because atoms are not garbage collected, and there's the risk of the VM crashing because of increasing number of atoms generated.
...in general this pattern is extremely discouraged in Elixir... – José Valim
https://stackoverflow.com/questions/31990134/how-to-convert-map-keys-from-strings-to-atoms-in-elixir
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
But you still want to make sure the data you're converting comes from a trusted source
Thank you!
@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.
Hello @kipcole9, your key atomization implementation was far more elegant than mine. Thanks for providing some code for me to learn from.
There's a minor bug:
ArgumentError
is thrown whenString.to_atom/1
is called with an atom key.We need to add a clause to check for that.
Thanks.