Current section
Files
Jump to
Current section
Files
lib/nice_maps.ex
defmodule NiceMaps do
@moduledoc """
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
"""
@doc """
The main interface - this is where the magic happens.
## Options
* `:keys` one of `:camelcase` or `:snake_case`
* `:convert_structs` one of `true` or `false`, default: `false`
* `:key_type`, one of `:string`, `:existing_atom`, or `:unsave_atom` (please use `:existing_atom` whenever possible)
## Examples
### Without Options:
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"}
### Keys to camelcase:
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"}
### Convert all structs into maps
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}}
}
### Convert string keys to existing atom
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"
}
### Mix it all together
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"}
"""
def parse(map_or_struct, opts \\ [])
def parse(map_or_struct, opts) when is_list(map_or_struct), do: parse_list(map_or_struct, opts)
def parse(%{__struct__: _} = struct, opts),
do: struct |> Map.from_struct() |> parse_keys(opts)
def parse(map_or_struct, opts) when is_map(map_or_struct), do: parse_keys(map_or_struct, opts)
def parse(obj, _opts), do: obj
@doc false
def parse_list(list, opts, result \\ [])
def parse_list([], _opts, result), do: Enum.reverse(result)
def parse_list([obj | rest], opts, result),
do: parse_list(rest, opts, [parse(obj, opts) | result])
@doc false
def parse_keys(map, opts) do
case Keyword.get(opts, :keys) do
:camelcase ->
parse_camelcase_keys(map, opts)
:snake_case ->
parse_snake_case(map, opts)
nil ->
key_type = Keyword.get(opts, :key_type)
convert_structs = Keyword.get(opts, :convert_structs)
if key_type || convert_structs do
for {key, val} <- map, do: {parse_key_type(key, key_type), parse(val, opts)}, into: %{}
else
map
end
end
end
defp parse_key_type(key, :existing_atom) when is_atom(key), do: key
defp parse_key_type(key, :existing_atom) when is_bitstring(key),
do: String.to_existing_atom(key)
defp parse_key_type(key, :unsave_atom) when is_atom(key), do: key
defp parse_key_type(key, :unsave_atom) when is_bitstring(key),
do: String.to_atom(key)
defp parse_key_type(key, :string) when is_bitstring(key), do: key
defp parse_key_type(key, :string), do: to_string(key)
defp parse_key_type(key, _), do: key
defp parse_snake_case(map, opts) do
Enum.map(map, fn
{key, val} -> {convert_to_snake_case(key, opts), parse(val, opts)}
end)
|> Enum.into(%{})
end
defp convert_to_snake_case(key, opts) when is_bitstring(key) do
key_type = Keyword.get(opts, :key_type, :string)
key |> Macro.underscore() |> parse_key_type(key_type)
end
defp convert_to_snake_case(key, opts) when is_atom(key) do
key_type = Keyword.get(opts, :key_type)
new_key = key |> to_string() |> Macro.underscore()
if key_type do
parse_key_type(new_key, key_type)
else
String.to_existing_atom(new_key)
end
end
defp convert_to_snake_case(key, opts) do
key_type = Keyword.get(opts, :key_type)
key |> parse_key_type(key_type)
end
defp parse_camelcase_keys(map, opts) do
Enum.map(map, fn
{key, val} -> {convert_to_camelcase(key, opts), parse(val, opts)}
end)
|> Enum.into(%{})
end
defp convert_to_camelcase(key, opts) when is_bitstring(key) do
first_char = String.first(key)
key_type = Keyword.get(opts, :key_type, :string)
key
|> Macro.camelize()
|> String.replace_prefix(String.upcase(first_char), first_char)
|> parse_key_type(key_type)
end
defp convert_to_camelcase(key, opts) when is_atom(key) do
key_type = Keyword.get(opts, :key_type)
new_key = key |> to_string() |> convert_to_camelcase(opts)
if key_type do
parse_key_type(new_key, key_type)
else
String.to_existing_atom(new_key)
end
end
defp convert_to_camelcase(key, opts) do
key_type = Keyword.get(opts, :key_type)
parse_key_type(key, key_type)
end
end