MapRewire (map_rewire v0.3.0)

MapRewire makes it easier to rewire maps, such as might be done when translating from an external API result to an internal value or taking the output of one external API and transforming it the input shape of an entirely different external API.

To rewire a map, build transformation rules and call rewire/3, or if MapRewire has been imported, use the operator, <~>.

iex> map   = %{"id" => "234923409", "title" => "asdf"}
iex> rules = "title=>name id=>shopify_id"
iex> map <~> rules
{%{"id" => "234923409", "title" => "asdf"}, %{"shopify_id" => "234923409", "name" => "asdf"}}
iex> MapRewire.rewire(map, rules) == (map <~> rules)
true

Rewire Rules

The rewire rules have three basic forms.

  1. A string containing string rename rules separated by whitespace.

    iex> map   = %{"id" => "234923409", "title" => "asdf"}
    iex> rules = "title=>name id=>shopify_id"
    iex> map <~> rules
    {%{"id" => "234923409", "title" => "asdf"}, %{"shopify_id" => "234923409", "name" => "asdf"}}

    Here, rules normalizes to: [{"title", "name"}, {"id", "shopify_id"}].

  2. A list of strings with one string rename rule in each string.

    iex> map   = %{"id" => "234923409", "title" => "asdf"}
    iex> rules = ["title=>name", "id=>shopify_id"]
    iex> map <~> rules
    {%{"id" => "234923409", "title" => "asdf"}, %{"shopify_id" => "234923409", "name" => "asdf"}}

    Here, rules normalizes to: [{"title", "name"}, {"id", "shopify_id"}].

  3. Any enumerable value that iterates as key/value tuples (map, keyword list, or a list of 2-tuples). These may be either rename rules, or may be more complex key transform rules.

    iex> map   = %{id: "234923409", title: "asdf"}
    iex> rules = [title: :name, id: :shopify_id]
    iex> map <~> rules
    {%{id: "234923409", title: "asdf"}, %{shopify_id: "234923409", name: "asdf"}}
    iex> map   = %{"id" => "234923409", "title" => "asdf"}
    iex> rules = [{"title", "name"}, {"id", "shopify_id"}]
    iex> map <~> rules
    {%{"id" => "234923409", "title" => "asdf"}, %{"shopify_id" => "234923409", "name" => "asdf"}}
    iex> map   = %{"id" => "234923409", "title" => "asdf"}
    iex> rules = %{"title" => :name, "id" => :shopify_id}
    iex> map <~> rules
    {%{"id" => "234923409", "title" => "asdf"}, %{shopify_id: "234923409", name: "asdf"}}
    # This is legal, but really ugly. Don't do it.
    iex> map   = %{"id" => "234923409", "title" => "asdf"}
    iex> rules = ["title=>name", {"id", "shopify_id"}]
    iex> map <~> rules
    {%{"id" => "234923409", "title" => "asdf"}, %{"shopify_id" => "234923409", "name" => "asdf"}}

Rename Rules

Rename rules take the value of the old key from the source map and write it to the target map as the new key, like "title=>name", %{"title" => "name"}, and [title: :name] that normalize to {old_key, new_key}. Both old_key and new_key are typically atoms or strings, but may be any valid Map key value, except for the forms noted below.

Advanced Rules

There are two types of advanced rules (keys with options and producer functions), which can only be provided when the rules are in an enumerable format such as a keyword list, map, or list of tuples.

Keys with Options

The new key is provided as a tuple {new_key, options}. Supported options are :transform (expecting a transformer/0 function) and :default, expecting any normal map value. The :default will work as the third parameter of Map.get/3 and be used instead of key_missing/0.

iex> map   = %{"title" => "asdf"}
iex> rules = %{"title" => {:name, transform: &String.reverse/1}}
iex> map <~> rules
{%{"title" => "asdf"}, %{name: "fdsa"}}

# If "title" could be missing from the source map, the `transform` function
# should be written to handle `key_missing/0` values or have its own safe
# `default` value.
iex> map   = %{}
iex> rules = %{"title" => {:name, default: "unknown", transform: &String.reverse/1}}
iex> map <~> rules
{%{}, %{name: "nwonknu"}}

Producer Functions

Producer functions (producer/0) take in the value and return zero or more key/value tuples. It may be provided either as producer or {producer, options} as shown below.

iex> dcs = fn value -> ...> unless MapRewire.key_missing?(value) do ...> [dept, class, subclass] = ...> value ...> |> String.split("-", parts: 3) ...> |> Enum.map(&String.to_integer/1) ...> ...> Enum.to_list(%{"department" => dept, "class" => class, "subclass" => subclass}) ...> end ...> end iex> map = %{"title" => "asdf", "dcs" => "1-3-5"} iex> rules = %{"title" => "name", "dcs" => dcs} iex> map <~> rules {%{"title" => "asdf", "dcs" => "1-3-5"}, %{"name" => "asdf", "department" => 1, "class" => 3, "subclass" => 5}}

If "title" could be missing from the source map, the transform function

should be written to handle key_missing/0 values or have its own safe

default value.

iex> dcs = fn value -> ...> [dept, class, subclass] = ...> value ...> |> String.split("-", parts: 3) ...> |> Enum.map(&String.to_integer/1) ...> ...> Enum.to_list(%{"department" => dept, "class" => class, "subclass" => subclass}) ...> end iex> map = %{"title" => "asdf"} iex> rules = %{"title" => "name", "dcs" => {dcs, default: "0-0-0"}} iex> map <~> rules {%{"title" => "asdf"}, %{"name" => "asdf", "department" => 0, "class" => 0, "subclass" => 0}}

Link to this section Summary

Types

A function that, given a map value, produces zero or more key/value tuples.

A normalized rewire rule.

Advanced rewire rule options

Rewire rule target values.

The shape of MapRewire transformation rules.

A function that, given a map value, transforms it before insertion into the target map.

Functions

The operator form of rewire/3, which remaps the map content and replaces the key if it matches with an item in rewire_rules.

The value used in rewire operations if an old key does not exist in the source map.

Returns true if value is the same as key_missing/0.

Remaps the map content and replaces the key if it matches with an item in rules.

Link to this section Types

Specs

producer() ::
  (Map.value() -> nil | {Map.key(), Map.value()} | [{Map.key(), Map.value()}])

A function that, given a map value, produces zero or more key/value tuples.

The value provided may be key_missing/0, so key_missing?/1 should be used to compare before blindly operating on value.

If no keys are to be produced (possibly because value is key_missing/0), either nil or an empty list ([]) should be returned.

fn value ->
  unless MapRewire.key_missing?(value) do
    [dept, class, subclass] =
      value
      |> String.split("-", parts: 3)
      |> Enum.map(&String.to_integer/1)

    Enum.to_list(%{"department" => dept, "class" => class, "subclass" => subclass})
  end
end
Link to this type

rewire_rule()

Specs

rewire_rule() :: {old :: Map.key(), rewire_rule_target()}

A normalized rewire rule.

Link to this type

rewire_rule_options()

Specs

rewire_rule_options() :: [transform: transformer(), default: Map.value()]

Advanced rewire rule options

Link to this type

rewire_rule_target()

Specs

rewire_rule_target() ::
  Map.key()
  | producer()
  | {producer(), [{:default, Map.value()}]}
  | {Map.key(), rewire_rule_options()}

Rewire rule target values.

Link to this type

rewire_rules()

Specs

rewire_rules() ::
  String.t() | [String.t()] | keyword() | map() | [rewire_rule()]

The shape of MapRewire transformation rules.

Note that although keyword lists and maps may be used, the values must be rewire_rule_target/0 values.

Link to this type

transformer()

Specs

transformer() :: (Map.value() -> Map.value())

A function that, given a map value, transforms it before insertion into the target map.

The value may be key_missing/0, so key_missing?/1 should be used to compare before blindly operating on value.

If the key should be omitted when rewire/3 is called, key_missing/0 should be returned.

fn value ->
  cond do
    MapRewire.key_missing?(value) ->
      value

    is_binary(value) ->
      String.reverse(value)

    true ->
      String.reverse(to_string(value))
  end
end

Link to this section Functions

Link to this function

content <~> rewire_rules

The operator form of rewire/3, which remaps the map content and replaces the key if it matches with an item in rewire_rules.

Specs

key_missing() :: binary()

The value used in rewire operations if an old key does not exist in the source map.

Normally, when the rewired map is produced, keys containing this value will be removed from the rewired map, but providing the option compact: false to rewire/3 will replace this value with nil.

The value in key_missing/0 may be provided to producer/0 and transformer/0 functions, so key_missing?/1 should be used to determine the correct response if this value is received (see the documentation for these function types).

Note that the value of key_missing/0 is a 45-byte binary value with a 13-byte fixed head ("<~>NoMatch<~>") and a random value that changes whenever MapRewire is recompiled.

Link to this function

key_missing?(value)

Specs

key_missing?(Map.value()) :: boolean()

Returns true if value is the same as key_missing/0.

Link to this function

rewire(content, rules, options \\ [])

Specs

rewire(map(), rewire_rules(), keyword()) :: {old :: map(), new :: map()}

Remaps the map content and replaces the key if it matches with an item in rules.

Accepts two options:

  • :debug controls the logging of the steps taken to transform content using rules. The default is Application.get_env(:map_rewire, :debug?).

  • :compact which controls the removal of values from the result map for keys missing in the content map. The default is true.

    iex> map   = %{"title" => "asdf"}
    iex> rules = %{"title" => :name, "missing" => :missing}
    iex> rewire(map, rules, compact: true) # the default
    {%{"title" => "asdf"}, %{name: "asdf"}}
    iex> map   = %{"title" => "asdf"}
    iex> rules = %{"title" => :name, "missing" => :missing}
    iex> rewire(map, rules, compact: false)
    {%{"title" => "asdf"}, %{name: "asdf", missing: nil}}