lib/iteraptor.ex

defmodule Iteraptor do
  @moduledoc """
  `Iteraptor` makes complicated nested structures (currently `Map`s, `List`s
    and `Keyword`s) iteration easier.

  ## Usage

  #### Iterating, Mapping, Reducing

  * [`Iteraptor.each/3`](https://hexdocs.pm/iteraptor/Iteraptor.html#each/3)
    to iterate a deeply nested map/list/keyword;
  * [`Iteraptor.map/3`](https://hexdocs.pm/iteraptor/Iteraptor.html#map/3)
    to map a deeply nested map/list/keyword;
  * [`Iteraptor.reduce/4`](https://hexdocs.pm/iteraptor/Iteraptor.html#reduce/4)
    to reduce a deeply nested map/list/keyword;
  * [`Iteraptor.map_reduce/4`](https://hexdocs.pm/iteraptor/Iteraptor.html#map_reduce/4)
    to map and reduce a deeply nested map/list/keyword;

  #### Flattening

  * [`Iteraptor.to_flatmap/2`](https://hexdocs.pm/iteraptor/Iteraptor.html#to_flatmap/2)
    to flatten a deeply nested map/list/keyword into
    flatten map with concatenated keys;
  * [`Iteraptor.from_flatmap/3`](https://hexdocs.pm/iteraptor/Iteraptor.html#from_flatmap/3)
    to “unveil”/“unflatten” the previously flattened map into nested structure;

  #### Filtering

  * [`Iteraptor.filter/3`](https://hexdocs.pm/iteraptor/Iteraptor.html#filter/3)
    to filter the structure according to the value returned from each iteration
    (`true` to leave the element, `false` to discard.)
  """

  import Iteraptor.Utils

  @doc """
  Build a flatmap out of nested structure, concatenating the names of keys.

      %{a: %{b: %{c: 42, d: [nil, 42]}, e: [:f, 42]}} |> Iteraptor.to_flatmap
      %{"a.b.c": 42, "a.b.d.0": nil, "a.b.d.1": 42, "a.e.0": :f, "a.e.1": 42}

  Lists are handled gracefully, index is used as a key in resulting map.

  ## Parameters

  - `input`: nested map/list/keyword/struct to be flattened,  
  - `opts`: the additional options to be passed through:  
    — `delimiter` (_default:_ `"."`,) might be passed explicitly or
    configured with `:iteraptor, :delimiter` application setting.

  ## Examples

      iex> [:a, 42] |> Iteraptor.to_flatmap
      %{0 => :a, 1 => 42}

      iex> %{a: 42} |> Iteraptor.to_flatmap
      %{a: 42}

      iex> %{a: 42, b: 42} |> Iteraptor.to_flatmap
      %{a: 42, b: 42}

      iex> %{a: %{b: 42}, d: 42} |> Iteraptor.to_flatmap
      %{"a.b" => 42, d: 42}

      iex> %{a: [:b, 42], d: 42} |> Iteraptor.to_flatmap
      %{"a.0" => :b, "a.1" => 42, d: 42}

      iex> %{a: %{b: [:c, 42]}, d: 42} |> Iteraptor.to_flatmap
      %{"a.b.0" => :c, "a.b.1" => 42, d: 42}

      iex> %{a: %{b: 42}} |> Iteraptor.to_flatmap
      %{"a.b" => 42}

      iex> %{a: %{b: %{c: 42}}} |> Iteraptor.to_flatmap
      %{"a.b.c" => 42}

      iex> %{a: %{b: %{c: 42}}, d: 42} |> Iteraptor.to_flatmap
      %{"a.b.c" => 42, d: 42}

      iex> [a: [b: [c: 42]], d: 42] |> Iteraptor.to_flatmap
      %{"a.b.c" => 42, d: 42}

      iex> [a: [[:b], 42], d: 42] |> Iteraptor.to_flatmap
      %{"a.0.0" => :b, "a.1" => 42, d: 42}

      iex> %{a: %{b: %{c: 42, d: [nil, 42]}, e: [:f, 42]}} |> Iteraptor.to_flatmap
      %{"a.b.c" => 42, "a.b.d.0" => nil, "a.b.d.1" => 42, "a.e.0" => :f, "a.e.1" => 42}

      iex> %{a: %{b: %{c: 42, d: [nil, 42]}, e: [:f, 42]}}
      ...> |> Iteraptor.to_flatmap(delimiter: "_")
      %{"a_b_c" => 42, "a_b_d_0" => nil, "a_b_d_1" => 42, "a_e_0" => :f, "a_e_1" => 42}
  """

  @type option ::
          {:keys, :reverse}
          | {:yield, :all | :none | :maps | :lists}
          | {:structs, :values | :keep}
  @type options :: [option()]

  @typedoc """
  The function that might be passed to all the traversion functions.

  When it’s a function or arity `1`, it receives `{key, value}` tuple when the key
    is the list of keys down the nesting levels.

  When its arity is `2`, it receives `key` and `value` as separated arguments.
  """
  @type traverse_fun :: ({any(), any()} -> any()) | (any(), any() -> any())

  @spec to_flatmap(Access.t(), options()) :: %{}

  def to_flatmap(input, opts \\ []) when is_map(input) or is_list(input) do
    reducer = fn {k, v}, acc ->
      key =
        case k do
          [key] -> key
          _ -> Enum.join(k, delimiter(opts))
        end

      Map.put(acc, key, v)
    end

    reduce(input, %{}, reducer, opts)
  end

  @doc """
  Build a nested structure out of a flatmap given, decomposing the names of keys
  and handling lists carefully.

      %{"a.b.c": 42, "a.b.d.0": nil, "a.b.d.1": 42, "a.e.0": :f, "a.e.1": 42}
      |> Iteraptor.from_flatmap
      #⇒ %{a: %{b: %{c: 42, d: [nil, 42]}, e: [:f, 42]}}

  ## Parameters

  - `input`: flat map to be “expanded” to nested maps/lists,  
  - `transformer`: the transformer function to be called on all the elements,  
  - `opts`: additional options to be passed through.

  ## Examples

      iex> %{"a.b.c": 42} |> Iteraptor.from_flatmap
      %{a: %{b: %{c: 42}}}

      iex> %{"a.b.c": 42, "a.b.d": 42} |> Iteraptor.from_flatmap
      %{a: %{b: %{c: 42, d: 42}}}

      iex> %{"a.b.c": 42, "a.b.d": 42, "a.e": 42} |> Iteraptor.from_flatmap
      %{a: %{b: %{c: 42, d: 42}, e: 42}}

      iex> %{"0": 42, "1": 42} |> Iteraptor.from_flatmap
      [42, 42]

      iex> %{"1": :a1, "0": :a0, "2": :a2, "3": :a3, "4": :a4, "5": :a5,
      ...>   "6": :a6, "7": :a7, "8": :a8, "9": :a9, "10": :a10, "11": :a11}
      ...> |> Iteraptor.from_flatmap
      [:a0, :a1, :a2, :a3, :a4, :a5, :a6, :a7, :a8, :a9, :a10, :a11]

      iex> %{"0.a": 42, "0.b": 42} |> Iteraptor.from_flatmap
      [%{a: 42, b: 42}]

      iex> %{"a.0.0" => :b, "a.1" => 42, d: 42} |> Iteraptor.from_flatmap
      %{a: [[:b], 42], d: 42}

      iex> %{"a.b.c": 42, "a.b.d.0": nil, "a.b.d.1": 42, "a.e.0": :f, "a.e.1": 42}
      ...> |> Iteraptor.from_flatmap
      %{a: %{b: %{c: 42, d: [nil, 42]}, e: [:f, 42]}}

      iex> %{"0.a": 42, "0.b": 42} |> Iteraptor.from_flatmap(&IO.inspect/1)
      {[0, :a], 42}
      {[0, :b], 42}
      [%{a: 42, b: 42}]
  """
  @spec from_flatmap(%{}, traverse_fun(), options()) :: Access.t()

  def from_flatmap(input, transformer \\ & &1, opts \\ []) when is_map(input) do
    reducer = fn {k, v}, acc ->
      key =
        case k |> Enum.join(delimiter(opts)) |> String.split(delimiter(opts)) do
          [k] -> [smart_convert(k)]
          list -> Enum.map(list, &smart_convert/1)
        end

      transformer_key = if opts[:keys] == :reverse, do: Enum.reverse(key), else: key

      value =
        case transformer.({transformer_key, v}) do
          {^key, any} -> any
          any -> any
        end

      deep_put_in(acc, {key, value}, opts)
    end

    input
    |> reduce(%{}, reducer, opts)
    |> squeeze()
  end

  @doc """
  Iterates the given nested structure, calling the callback provided on each
    value. The key returned is an array of all the parent keys (and/or indices
    in a case of an array.)

  The return value is `self`.

  ## Parameters

  - `input`: nested map/list/keyword to be walked through.
  - `fun`: callback to be called on each **`{key, value}`** pair, where `key`
    is an array or deeply nested keys;
  e.g. on `%{a: {b: 42}}` will be called once, with tuple `{[:a, :b], 42}`;
  - `opts`: the options to be passed to the iteration
    - `yield`: `[:all | :none | :maps | :lists]` what to yield; _default:_ `:all`
    for yielding _values only_
    - `keys`: `[:reverse]` reverse keys list to ease pattern matching; _default:_ `nil`
    - `structs`: `[:values | :keep]` how to handle structs;  _default:_ `:values`
    for treating them as `map`s. When `:values`, the nested structs
    are considered leaves and returned to the iterator instead of being iterated
    through; when `:keep` it returns a struct back after iteration

  ## Examples

      iex> %{a: %{b: %{c: 42}}} |> Iteraptor.each(&IO.inspect/1)
      {[:a, :b, :c], 42}
      %{a: %{b: %{c: 42}}}

      iex> %{a: %{b: %{c: 42}}} |> Iteraptor.each(&IO.inspect/1, yield: :all)
      {[:a], %{b: %{c: 42}}}
      {[:a, :b], %{c: 42}}
      {[:a, :b, :c], 42}
      %{a: %{b: %{c: 42}}}
  """

  @spec each(Access.t(), traverse_fun(), options()) :: Access.t()

  def each(input, fun, opts \\ []) do
    map(input, fun, opts)
    input
  end

  @doc """
  Maps the given nested structure, calling the callback provided on each value.
    The key returned is a concatenated names of all the parent keys
  (and/or indices in a case of an array.)

  The return value is the result of subsequent calls to the transformer given.

  ## Parameters

  - `input`: nested map/list/keyword to be mapped.
  - `fun`: callback to be called on each **`{key, value}`** pair, where `key`
    is an array or deeply nested keys;
  e.g. on `%{a: {b: 42}}` will be called once, with tuple `{[:a, :b], 42}`;
  - `opts`: the options to be passed to the iteration (see `Iteraptpr.each/3`)

  ## Examples

      iex> %{a: %{b: %{c: 42}}} |> Iteraptor.map(fn {_, v} -> v * 2 end)
      %{a: %{b: %{c: 84}}}

      iex> %{a: %{b: %{c: 42}}} |> Iteraptor.map(fn {k, _} -> Enum.join(k) end)
      %{a: %{b: %{c: "abc"}}}

      iex> %{a: %{b: %{c: 42}}}
      ...> |> Iteraptor.map(fn
      ...>      {[_], _} = self -> self
      ...>      {[_, _], _} -> "YAY"
      ...>    end, yield: :all)
      %{a: %{b: "YAY"}}
  """

  @spec map(Access.t(), traverse_fun(), options()) :: Access.t()

  def map(input, fun, opts \\ []) do
    unless is_function(fun, 1), do: raise("Function or arity fun/1 is required")

    {type, _, into} = type(input)
    {result, _} = traverse(input, fun, opts, {[], into})

    maybe_struct(opts[:structs], result, type)
  end

  @doc """
  Iteration with reducing. The function of arity `2`, called back on each
    iteration with `{k, v}` pair _and_ an accumulator is accepted.

  The return value is the result of the last call to the passed reducer function.

  ## Parameters

  - `input`: nested map/list/keyword to be mapped.
  - `fun`: callback to be called on each **`{key, value}, acc`** pair,
    where `key` is an array or deeply nested keys, `value` is the value and
    `acc` is the accumulator;
  - `opts`: the options to be passed to the iteration (see `Iteraptpr.each/3`)

  ## Examples

      iex> %{a: %{b: %{c: 42}}}
      ...> |> Iteraptor.reduce([], fn {k, _}, acc ->
      ...>      [Enum.join(k, "_") | acc]
      ...>    end, yield: :all)
      ...> |> :lists.reverse()
      ["a", "a_b", "a_b_c"]
  """

  @spec reduce(Access.t(), Access.t(), traverse_fun(), options()) :: Access.t()

  def reduce(input, acc \\ nil, fun, opts \\ []) do
    unless is_function(fun, 2), do: raise("Function or arity fun/2 is required")

    {type, _, into} = type(input)
    acc = if is_nil(acc), do: into, else: acc
    fun_wrapper = fn kv, acc -> {kv, fun.(kv, acc)} end
    {_, result} = traverse(input, fun_wrapper, opts, {[], acc})

    maybe_struct(opts[:structs], result, type)
  end

  @doc """
  Iteration with mapping and reducing. The function of arity `2`, called back on each
    iteration with `{k, v}` pair _and_ an accumulator is accepted.

  The return value is the tuple, consisting of mapped input _and_ the
    accumulator from the last call to the passed map-reducer.

  ## Parameters

  - `input`: nested map/list/keyword to be mapped.
  - `fun`: callback to be called on each **`{key, value}, acc`** pair,
    where `key` is an array or deeply nested keys, `value` is the value and
    `acc` is the accumulator;
  - `opts`: the options to be passed to the iteration  (see `Iteraptpr.each/3`)

  ## Examples

      iex> %{a: %{b: %{c: 42}}}
      ...> |> Iteraptor.map_reduce([], fn
      ...>      {k, %{} = v}, acc -> {{k, v}, [Enum.join(k, ".") | acc]}
      ...>      {k, v}, acc -> {{k, v * 2}, [Enum.join(k, ".") <> "=" | acc]}
      ...>    end, yield: :all)
      {%{a: %{b: %{c: 84}}}, ["a.b.c=", "a.b", "a"]}
  """

  @spec map_reduce(Access.t(), Access.t(), traverse_fun(), options()) :: {Access.t(), any()}

  def map_reduce(input, acc \\ %{}, fun, opts \\ []) do
    unless is_function(fun, 2), do: raise("Function or arity fun/2 is required")

    {type, _, into} = type(input)
    acc = if is_nil(acc), do: into, else: acc
    {map_result, result} = traverse(input, fun, opts, {[], acc})

    {map_result, maybe_struct(opts[:structs], result, type)}
  end

  @doc """
  Filters the deeply nested term, optionally calling the function on
  filtered entries.

  The return value is the filtered term.

  ## Parameters

  - `input`: nested map/list/keyword to be filtered.
  - `fun`: callback to be called on each **`{key, value}`** to filter entries.
  - `opts`: the options to be passed to the iteration (see `Iteraptpr.each/3`)

  ## Examples

      iex> %{a: %{b: 42, e: %{f: 3.14, c: 42}, d: %{c: 42}}, c: 42, d: 3.14}
      ...> |> Iteraptor.filter(fn {key, _} -> :c in key end, yield: :none)
      %{a: %{e: %{c: 42}, d: %{c: 42}}, c: 42}
  """

  @spec filter(Access.t(), traverse_fun(), options()) :: Access.t()

  def filter(input, fun, opts \\ []) do
    unless is_function(fun, 1), do: raise("Function or arity fun/1 is required")
    {type, _, acc} = type(input)

    fun_wrapper = fn {k, v}, acc ->
      if fun.({k, v}), do: {{k, v}, deep_put_in(acc, {k, v}, opts)}, else: {{k, v}, acc}
    end

    {_, result} = traverse(input, fun_wrapper, opts, {[], acc})

    maybe_struct(opts[:structs], result, type)
  end

  @doc """
  Produces a term ready-to-use with JSON interchange. Stringifies all keys
  and converts keywords to maps.

  If the option `keys: false` is given, leaves keys intact.

  ## Examples

      iex> Iteraptor.jsonify([foo: [:zzz], bar: :baz], values: true)
      %{"foo" => ["zzz"], "bar" => "baz"}

      iex> Iteraptor.jsonify(%{foo: [1, [bar: 2], 3], bar: [baz: 42]})
      %{"foo" => [1, %{"bar" => 2}, 3], "bar" => %{"baz" => 42}}

      iex> Iteraptor.jsonify([foo: [bar: [baz: :zoo], boo: 42]], values: true)
      %{"foo" => %{"bar" => %{"baz" => "zoo"}, "boo" => 42}}

      iex> Iteraptor.jsonify([foo: [bar: [baz: :zoo], boo: 42]], keys: false)
      %{foo: %{bar: %{baz: :zoo}, boo: 42}}
  """
  @spec jsonify(Access.container() | any(), keyword()) :: map()
  def jsonify(input, opts \\ [])
  def jsonify([{_, _} | _] = input, opts), do: input |> Map.new() |> jsonify(opts)
  def jsonify(input, opts) when is_list(input), do: Enum.map(input, &jsonify(&1, opts))

  def jsonify(input, opts) when not (is_map(input) or is_list(input)),
    do: if(opts[:values] && is_atom(input), do: to_string(input), else: input)

  def jsonify(input, opts) do
    stringify_keys = Keyword.get(opts, :keys, true)

    Iteraptor.map(
      input,
      fn
        {k, [{_, _} | _] = kw} when is_list(k) ->
          {k |> List.last() |> do_stringify(stringify_keys), jsonify(kw, opts)}

        {k, v} when is_list(k) ->
          {k |> List.last() |> do_stringify(stringify_keys), jsonify(v, opts)}
      end,
      yield: :all
    )
  end

  @spec maybe_struct(:keep | :values, result :: any(), type :: module()) :: Access.t()
  defp maybe_struct(:keep, %{} = result, type) when type != Map, do: struct(type, result)
  defp maybe_struct(_, result, _), do: result

  @spec do_stringify(any(), boolean()) :: any() | binary()
  defp do_stringify(k, false), do: k
  defp do_stringify(k, _) when is_atom(k), do: Atom.to_string(k)
  defp do_stringify(k, _), do: to_string(k)

  ##############################################################################

  @spec traverse_callback(nil | traverse_fun(), {any(), any()}, nil | :reverse) :: {any(), any()}

  defp traverse_callback(nil, {value, acc}, _), do: {value, acc}

  defp traverse_callback(fun, {{keys, value}, acc}, :reverse),
    do: traverse_callback(fun, {{Enum.reverse(keys), value}, acc}, nil)

  defp traverse_callback(fun, {value, acc}, nil) when is_function(fun, 1),
    do: {fun.(value), acc}

  defp traverse_callback(fun, {value, acc}, nil) when is_function(fun, 2),
    do: fun.(value, acc)

  defmacrop traverse_value({k, v}, fun, opts, {deep, acc}) do
    quote do
      {value, acc} = traverse(unquote(v), unquote(fun), unquote(opts), unquote({deep, acc}))
      {{unquote(k), value}, acc}
    end
  end

  @spec traverse(Access.t(), traverse_fun(), options(), {[any()], any()}) :: {Access.t(), any()}

  defp traverse(input, fun, opts, key_acc)

  defp traverse(input, fun, opts, {key, acc}) when is_list(input) or is_map(input) do
    {type, from, into} = type(input)

    s_as_v = opts[:structs] == :values

    if is_map(from) and type != Map and s_as_v do
      {input, acc}
    else
      {value, acc} =
        from
        |> Enum.with_index()
        |> Enum.map_reduce(acc, fn {kv, idx}, acc ->
          {k, v} =
            case kv do
              {k, v} -> {k, v}
              v -> {idx, v}
            end

          deep = key ++ [k]

          {value, acc} =
            case {opts[:yield], is_map(v) and not s_as_v, is_list(v)} do
              {_, false, false} -> traverse_callback(fun, {{deep, v}, acc}, opts[:keys])
              {:all, _, _} -> traverse_callback(fun, {{deep, v}, acc}, opts[:keys])
              {:none, _, _} -> traverse_callback(nil, {{deep, v}, acc}, opts[:keys])
              {:lists, _, true} -> traverse_callback(fun, {{deep, v}, acc}, opts[:keys])
              {:maps, true, _} -> traverse_callback(fun, {{deep, v}, acc}, opts[:keys])
              _ -> {{deep, v}, acc}
            end

          case value do
            ^v -> traverse_value({k, v}, fun, opts, {deep, acc})
            {^deep, v} -> traverse_value({k, v}, fun, opts, {deep, acc})
            {^k, v} -> traverse_value({k, v}, fun, opts, {deep, acc})
            {k, v} -> traverse_value({k, v}, fun, opts, {deep, acc})
            v -> traverse_value({k, v}, fun, opts, {deep, acc})
          end
        end)

      result = maybe_struct(opts[:structs], Enum.into(value, into), type)

      {squeeze(result, opts), acc}
    end
  end

  defp traverse(input, _fun, _opts, {_key, acc}), do: {input, acc}
end