defmodule Morphix do @moduledoc """ Morphix provides convenience methods for dealing with Maps, Lists, and Tuples. `morphiflat` and `morphiflat!` flatten maps, discarding top level keys. ### Examples: ``` iex> Morphix.morphiflat %{flatten: %{this: "map"}, if: "you please"} {:ok, %{this: "map", if: "you please"}} iex> Morphix.morphiflat! %{flatten: %{this: "map"}, o: "k"} %{this: "map", o: "k"} ``` `morphify!` and `morphify` will take either a List or a Tuple as the first argument, and a function as the second. Returns a map, with the keys of the map being the function applied to each member of the input. ### Examples: ``` iex> Morphix.morphify!({[1,2,3], [12], [1,2,3,4]}, &length/1) %{1 => [12], 3 => [1,2,3], 4 => [1,2,3,4]} ``` `atomorphify` and `atomorphiform` take a map as an input and return the map with all string keys converted to atoms. `atomorphiform` is recursive. ### Examples: ``` iex> Morphix.atomorphify(%{"a" => "2", :a => 2, 'a' => :two}) {:ok, %{:a => 2, 'a' => :two }} ``` """ @spec morphiflat(Map.t) :: {:ok | :error, Map.t | String} @spec morphiflat!(Map.t) :: Map.t @spec morphify(List.t, Function) :: {:ok|:error, Map.t | String} @spec morphify(Tuple.t, Function) :: {:ok|:error, Map.t | String} @spec morphify!(List.t, Function) :: Map.t @spec morphify!(Tuple.t, Function) :: Map.t @spec atomorphify(Map.t, :safe) :: {:ok, Map.t} @spec atomorphify(Map.t) :: {:ok, Map.t} @spec atomorphiform(Map.t, :safe) :: {:ok, Map.t} @spec atomorphiform(Map.t) :: {:ok, Map.t} @doc """ Takes a map and returns a flattend version of that map, discarding any nested keys. ### Examples: ``` iex> Morphix.morphiflat! %{you: "will", youwill: %{be: "discarded"}} %{you: "will", be: "discarded"} ``` """ def morphiflat! map do flattn map end @doc """ Takes a map and returns a flattened version of that map. If the map has nested maps (or the maps nested maps have nested maps, etc.) morphiflat moves all nested key/value pairs to the top level, discarding the original keys. ### Examples: ``` iex> Morphix.morphiflat %{this: %{nested: :map, inner: %{twonested: :map, is: "now flat"}}} {:ok, %{nested: :map, twonested: :map, is: "now flat"}} ``` In the example, the key `:this` is discarded, along with the key `inner`, because they both point to map values. Will return `{:error, is not a Map}` if the input is not a map. ### Examples: ``` iex> Morphix.morphiflat({1,2,3}) {:error, "{1, 2, 3} is not a Map"} ``` """ def morphiflat(map) when is_map map do try do {:ok, flattn map} rescue exception -> {:error, Exception.message(exception)} end end def morphiflat(not_map), do: {:error, "#{inspect(not_map)} is not a Map"} defp flattn map do not_maps = fn({k, v}, acc) -> case is_map v do false -> Map.put_new(acc, k, v) true -> Map.merge(acc, flattn(v)) end end Enum.reduce(map, %{}, not_maps) end @doc """ Takes a map as an argument and returns the same map with string keys converted to atom keys. Does not examine nested maps. ### Examples ``` iex> Morphix.atomorphify(%{"this" => "map", "has" => %{"string" => "keys"}}) {:ok, %{this: "map", has: %{"string" => "keys"}}} iex> Morphix.atomorphify(%{1 => "2", "1" => 2, "one" => :two}) {:ok, %{1 => "2", "1": 2, one: :two}} ``` """ def atomorphify(map) when is_map map do {:ok, atomog(map, &atomize_binary/1)} end @doc """ Takes a map and the `:safe` flag, returns the same map, with string keys converted to existing atoms if possible, and ignored otherwise. Ignores nested maps. ### Examples: ``` iex> :existing_atom iex> Morphix.atomorphify(%{"existing_atom" => "exists", "non_existent_atom" => "does_not", 1 => "is_ignored"}, :safe) {:ok, %{ "non_existent_atom" => "does_not", 1 => "is_ignored", existing_atom: "exists"}} ``` """ def atomorphify(map, :safe) when is_map map do {:ok, (atomog map, &safe_atomize_binary/1)} end @doc """ Takes a map as an argument and returns the same map, with all string keys (including keys in nested maps) converted to atom keys. ### Examples: ``` iex> Morphix.atomorphiform(%{:this => %{map: %{"has" => "a", :nested => "string", :for => %{a: :key}}}, "the" => %{"other" => %{map: :does}}, as: "well"}) {:ok,%{this: %{map: %{has: "a", nested: "string", for: %{a: :key}}}, the: %{other: %{map: :does}}, as: "well"} } iex> Morphix.atomorphiform(%{"this" => ["map", %{"has" => ["a", "list"]}], "inside" => "it"}) {:ok, %{this: ["map", %{has: ["a", "list"]}], inside: "it"}} ``` """ def atomorphiform(map) when is_map map do {:ok, depth_atomog(map, &atomize_binary/1)} end @doc """ Takes a map and the `:safe` flag as arguments and returns `{:ok, map}`, with any strings that are existing atoms converted to atoms, and any strings that are not existing atoms left as strings. Works recursively on embedded maps. ### Examples: ``` iex> [:allowed, :values] iex> map = %{"allowed" => "atoms", "embed" => %{"will" => "convert", "values" => "to atoms"}} iex> Morphix.atomorphiform(map, :safe) {:ok, %{"embed" => %{"will" => "convert", values: "to atoms"}, allowed: "atoms"}} ``` """ def atomorphiform(map, :safe) when is_map map do {:ok, depth_atomog(map, &safe_atomize_binary/1)} end defp process_list_item(item, safe_or_atomize) do cond do is_map item -> depth_atomog(item, safe_or_atomize) is_list item -> Enum.map(item, fn(x) -> process_list_item(x, safe_or_atomize) end) true -> item end end defp depth_atomog(map, safe_or_atomize) do atomkeys = fn({k, v}, acc) -> cond do is_map v -> Map.put_new(acc, safe_or_atomize.(k), depth_atomog(v, safe_or_atomize)) is_list v -> Map.put_new(acc, safe_or_atomize.(k), process_list_item(v, safe_or_atomize)) true -> Map.put_new(acc, safe_or_atomize.(k), v) end end Enum.reduce(map, %{}, atomkeys) end defp atomog(map, safe_or_atomize) do atomkeys = fn({k, v}, acc) -> Map.put_new(acc, safe_or_atomize.(k), v) end Enum.reduce(map, %{}, atomkeys) end defp atomize_binary(value) do if is_binary(value) do String.to_atom(value) else value end end defp safe_atomize_binary(value) do if is_binary(value) do try do String.to_existing_atom(value) rescue _ -> value end else value end end @doc """ Takes a List and a function as arguments and returns `{:ok, Map}`, with the keys of the map the result of applying the function to each item in the list. If the function cannot be applied, will return `{:error, message}` ### Examples ``` iex> Morphix.morphify([[1,2,3], [12], [1,2,3,4]], &Enum.count/1) {:ok, %{1 => [12], 3 => [1,2,3], 4 => [1,2,3,4]}} iex> Morphix.morphify({[1,2,3], [12], [1,2,3,4]}, &length/1) {:ok, %{1 => [12], 3 => [1,2,3], 4 => [1,2,3,4]}} iex> Morphix.morphify([1,2], &String.length/1) {:error, "Unable to apply &String.length/1 to each of [1, 2]"} ``` """ def morphify(enum, funct) when is_tuple(enum), do: morphify(Tuple.to_list(enum), funct) def morphify(enum, funct) do try do {:ok, morphify!(enum, funct)} rescue _ -> {:error, "Unable to apply #{inspect(funct)} to each of #{inspect(enum)}"} end end @doc """ Takes a list and a function as arguments and returns a Map, with the keys of the map the result of applying the function to each item in the list. ### Examples ``` iex> Morphix.morphify!([[1,2,3], [12], [1,2,3,4]], &Enum.count/1) %{1 => [12], 3 => [1,2,3], 4 => [1,2,3,4]} ``` """ def morphify!(enum, funct) when is_tuple(enum), do: morphify!(Tuple.to_list(enum), funct) def morphify!(enum, funct) do Enum.reduce(enum, %{}, fn(x, acc) -> Map.put(acc, funct.(x), x) end) end end