Current section

Files

Jump to
transmap lib transmap.ex
Raw

lib/transmap.ex

defmodule Transmap do
@default_key :_default
@spread_key :_spread
@doc """
Transforms the given map.
`rule_map` is a map has elements `key => rule`.
case {key, rule} do
{:_default, rule} -> # Change the default rule for the map from false to the given rule
{:_spread, targets} -> # Pop the maps for specific keys and merge it into the result map
{key, true} -> # Put the value for a specific key to the result map.
{key, false} -> # Don't put the value for a specific key to the result map.
{key, rule_map} when is_map(rule_map) -> # Transform the map for a specific key with the given rule
{key, {new_key, rule}} -> # Rename the key to new_key
{key, new_key} -> # Shorthand for {new_key, true}
end
If the `:diff` option is `true`, removes empty maps in the result.
## Examples
Basic transformation:
iex> map = %{a: 1, b: 2, c: 3}
iex> rule = %{a: true}
iex> Transmap.transform(map, rule)
%{a: 1}
iex> map = %{a: %{b: 1, c: 2}, d: %{e: 3, f: 4}, g: %{h: 5}}
iex> rule = %{a: %{c: true}, d: true, g: %{i: true}}
iex> Transmap.transform(map, rule)
%{a: %{c: 2}, d: %{e: 3, f: 4}, g: %{}}
iex> Transmap.transform(map, rule, diff: true)
%{a: %{c: 2}, d: %{e: 3, f: 4}}
Transfomation with `_default`:
iex> map = %{a: 1, b: 2, c: %{d: 3, e: 4}}
iex> rule = %{_default: true, b: false, c: %{_default: true}}
iex> Transmap.transform(map, rule)
%{a: 1, c: %{d: 3, e: 4}}
Transformation with `_spread`:
iex> map = %{a: %{b: 1, c: 2}, d: %{e: %{f: 3, g: 4}, h: 5}}
iex> rule = %{_spread: [[:a], [:d, :e]], a: true, d: %{e: %{f: true}}}
iex> Transmap.transform(map, rule)
%{b: 1, c: 2, d: %{}, f: 3}
iex> Transmap.transform(map, rule, diff: true)
%{b: 1, c: 2, f: 3}
iex> map = %{a: %{b: %{c: 1, d: 2}}}
iex> rule = %{_spread: [[:a, :b]], a: %{b: %{_default: true, c: :C}}}
iex> Transmap.transform(map, rule)
%{C: 1, d: 2, a: %{}}
iex> Transmap.transform(map, rule, diff: true)
%{C: 1, d: 2}
Transformation with renaming:
iex> map = %{a: 1, b: 2, c: %{d: 3, e: 4}, f: %{g: 5}}
iex> rule = %{_spread: [[:f]], a: {"A", true}, b: "B", c: {:C, %{d: 6}}, f: %{g: "G"}}
iex> Transmap.transform(map, rule)
%{"A" => 1, "B" => 2, :C => %{6 => 3}, "G" => 5}
iex> map = %{a: 1, b: %{a: 2}, c: %{a: 3}}
iex> rule = %{_spread: [[:b], [:c]], a: true, b: %{a: :b}, c: %{a: {:c, true}}}
iex> Transmap.transform(map, rule)
%{a: 1, b: 2, c: 3}
"""
@spec transform(data :: any, rule :: any) :: any
def transform(data, rule, opts \\ [])
def transform(data, _, opts) when not is_map(data), do: data
def transform(data, true, opts), do: data
def transform(data, rule, opts) when is_map(data) do
{default, rule} = Map.pop(rule, @default_key, false)
{spread, rule} = Map.pop(rule, @spread_key, [])
{data, rule} = apply_spread(data, rule, spread)
diff = Keyword.get(opts, :diff, false)
Enum.reduce(data, %{}, fn {key, value}, result ->
{key, rule_value} = with_key(key, Map.get(rule, key, default))
if rule_value != false do
filtered = transform(value, rule_value, opts)
case {filtered, diff} do
{map, true} when is_map(map) and map_size(map) == 0 -> result
{other, _} -> Map.put(result, key, other)
end
else
result
end
end)
end
defp apply_spread(data, rule, spread) do
Enum.reduce(spread, {data, rule}, fn keys, {data, rule} ->
{data_map, data} = pop_in(data, keys)
{rule_map, rule} = pop_in(rule, keys)
if is_map(data_map) do
if is_map(rule_map) do
{default, rule_map} = Map.pop(rule_map, @default_key, false)
rule_map = Enum.reduce(data_map, rule_map, fn {key, _}, rule_map ->
Map.put_new(rule_map, key, default)
end)
{data_map, rule_map} = apply_rename(data_map, rule_map)
data = Map.merge(data, data_map)
rule = Map.merge(rule, rule_map)
{data, rule}
else
rule_map = for {key, _} <- data_map, into: %{}, do: {key, true}
data = Map.merge(data, data_map)
rule = Map.merge(rule, rule_map)
{data, rule}
end
else
{data, rule}
end
end)
end
defp apply_rename(data_map, rule_map) do
reducer = fn {key, rule}, {data_map, rule_map} ->
{new_key, rule} = with_key(key, rule)
if key == new_key do
{data_map, rule_map}
else
rule_map = rule_map |> Map.delete(key) |> Map.put(new_key, rule)
data_map = if Map.has_key?(data_map, key) do
{data, data_map} = Map.pop(data_map, key)
Map.put(data_map, new_key, data)
else
data_map
end
{data_map, rule_map}
end
end
Enum.reduce(rule_map, {data_map, rule_map}, reducer)
end
defp with_key(key, rule) do
case rule do
boolean when is_boolean(boolean) -> {key, boolean}
{key, rule_value} -> {key, rule_value}
rule_value when is_map(rule_value) -> {key, rule_value}
new_key -> {new_key, true}
end
end
end