NiceMaps provides a single function parse
to convert maps into the desired format.
It can build camelcase/snake_case keys, convert string keys to atom keys and vice versa, or convert structs to maps.
NiceMaps
uses String.to_existing_atom/1
for conversions from string keys to atom keys and camelcase-snake_case, so please make sure your atoms exists before attempting something like
%{this_does_not_exist_as_camelcase: "abc"} |> NiceMaps.parse(keys: :camelcase)
# ** (ArgumentError) argument error
# :erlang.binary_to_existing_atom("thisDoesNotExistAsCamelcase", :utf8)
If you need to convert a map with unknown atoms, please use string keys instead:
%{this_does_not_exist_as_camelcase: "abc"} |> NiceMaps.parse(keys: :camelcase, key_type: :string)
# %{"thisDoesNotExistAsCamelcase" => "abc"}
# or
%{"this_does_not_exist_as_camelcase" => "abc"} |> NiceMaps.parse(keys: :camelcase)
NiceMaps
does not provide a key_type: :atom
option for the same reason explained later, but you can convert keys to existing atoms using key_type: :existing_atom
. If you absolutely insist on creating unknown atoms, there is a way to do it, but I will leave it to you to figure it out from the code (because I think it is a bad idea, and you should really know what you are doing before using it.)
Many people prefer working with atom keys over string keys, because you get some nice syntactic sugar like map.key
and the JSON like notation %{key: "value"}
, but because atoms are not garbage collected, web frameworks provide parameters as string key maps (otherwise an attacker could flood your memory with atoms until your server crashes.)
So, lets say you have parameters in a Phoenix controller and you want to convert the keys into atoms:
# We only allow these keys, you could call it the "strong parameters approach"
@allowed_keys ["a", "b", "c"]
def my_controller(conn, params) do
params
|> Map.take(@allowed_keys)
|> NiceMaps.parse(key_type: :existing_atom)
|> MyContext.create_a_thing()
end
Another possible use case is JSON parsing. If you have a map that could or could not have struct values, libraries like Jason will explode on you, and you have to implement the Jason.Encoder
protocol for your struct - which is not possible if you do not have control over the structs.
NiceMaps
to the rescue:
converted = %MyStruct{a: "a", b: "b", a_struct: %MyOtherStruct{c: "c"}} |> NiceMaps.parse(convert_structs: true)
# {a: "a", b: "b", a_struct: %{c: "c"}}
Jason.encode!(converted)
Last but not least, converting keys from snake case (with_underscore
) to camelcase (likeThis
) and vice versa. Different protocols/frameworks/programing languages use different conventions, snake case vs camelcase is one of those where there is no right or wrong, but you might want to convert - for example - a graphql response to a more "elixiry" map (using Neuron for this example):
{:ok, %{body: response}} = Neuron.query("""
{
aThing {
aField
}
}
""")
NiceMaps.parse(response, keys: :snake_case)
# %{a_thing: %{a_field: "whatever"}}
:keys
one of:camelcase
or:snake_case
:convert_structs
one oftrue
orfalse
, default:false
:key_type
, one of:string
or:existing_atom
iex> NiceMaps.parse(%MyStruct{id: 1, my_key: "bar"})
%{id: 1, my_key: "bar"}
iex> NiceMaps.parse([%MyStruct{id: 1, my_key: "bar"}, %{value: "a"}])
[%{id: 1, my_key: "bar"}, %{value: "a"}]
iex> NiceMaps.parse([%MyStruct{id: 1, my_key: "bar"}, "String"])
[%{id: 1, my_key: "bar"}, "String"]
iex> NiceMaps.parse(%{0 => "0", 1 => "1"})
%{0 => "0", 1 => "1"}
iex> NiceMaps.parse([%MyStruct{id: 1, my_key: "bar"}, %{value: "a"}], keys: :camelcase)
[%{id: 1, myKey: "bar"}, %{value: "a"}]
iex> NiceMaps.parse(%MyStruct{id: 1, my_key: "foo"}, keys: :camelcase)
%{id: 1, myKey: "foo"}
iex> NiceMaps.parse(%{"string" => "value", "another_string" => "value"}, keys: :camelcase)
%{"string" => "value", "anotherString" => "value"}
# Keys to snake case:
iex> NiceMaps.parse(%MyCamelStruct{id: 1, myKey: "foo"}, keys: :snake_case)
%{id: 1, my_key: "foo"}
iex> NiceMaps.parse(%MyCamelStruct{id: 1, myKey: "foo"}, keys: :snake_case)
%{id: 1, my_key: "foo"}
iex> NiceMaps.parse(%{"string" => "value", "another_string" => "value"}, keys: :camelcase)
%{"string" => "value", "anotherString" => "value"}
iex> map = %{
...> list: [
...> %MyStruct{id: 1, my_key: "foo"}
...> ],
...> struct: %MyStruct{id: 2, my_key: "bar"},
...> other_struct: %MyStruct{id: 3, my_key: %MyStruct{id: 4, my_key: nil}}
...> }
...> NiceMaps.parse(map, convert_structs: true)
%{
list: [
%{id: 1, my_key: "foo"}
],
struct: %{id: 2, my_key: "bar"},
other_struct: %{id: 3, my_key: %{id: 4, my_key: nil}}
}
iex> map = %{
...> "key1" => "value 1",
...> "nested" => %{"key2" => "value 2"},
...> "list" => [%{"key3" => "value 3", "key4" => "value 4"}],
...> 1 => "an integer key",
...> %MyStruct{} => "a struct key"
...> }
iex> [:key1, :key2, :key3, :key4, :nested, :list] # Make sure atoms exist
iex> NiceMaps.parse(map, key_type: :existing_atom)
%{
:key1 => "value 1",
:nested => %{key2: "value 2"},
:list => [%{key3: "value 3", key4: "value 4"}],
1 => "an integer key",
%MyStruct{} => "a struct key"
}
iex> map = %{
...> "hello_there" => [%{"aA" => "asdf"}, %{"a_a" => "bhjk"}, "a string", 1],
...> thingA: "thing A",
...> thing_b: "thing B"
...> }
iex> NiceMaps.parse(map, keys: :camelcase, key_type: :string)
%{"helloThere" => [%{"aA" => "asdf"}, %{"aA" => "bhjk"}, "a string", 1], "thingA" => "thing A", "thingB" => "thing B"}
iex> map = %{
...> "helloThere" => [%{"aA" => "asdf"}, %{"a_a" => "bhjk"}, "a string", 1],
...> thingA: "thing A",
...> thing_b: "thing B"
...> }
iex> [:hello_there, :thing_a, :thing_b] # make sure atoms exist
iex> NiceMaps.parse(map, keys: :snake_case, key_type: :existing_atom)
%{:hello_there => [%{:a_a => "asdf"}, %{:a_a => "bhjk"}, "a string", 1], :thing_a => "thing A", :thing_b => "thing B"}
If available in Hex, the package can be installed
by adding nice_maps
to your list of dependencies in mix.exs
:
def deps do
[
{:nice_maps, "~> 0.1.0"}
]
end
Documentation can be generated with ExDoc and published on HexDocs. Once published, the docs can be found at https://hexdocs.pm/nice_maps.