lib/useful.ex

defmodule Useful do
  @moduledoc """
  A Library of `Useful` functions for building `Elixir` Apps.
  """

  @doc """
  `atomize_map_keys/1` converts a `Map` with different keys
  to a map with just atom keys. Works recursively for nested maps.
  Inspired by stackoverflow.com/questions/31990134

  ## Examples

      iex> Useful.atomize_map_keys(%{"name" => "alex", id: 1})
      %{id: 1, name: "alex"}

      iex> Useful.atomize_map_keys(%{"name" => "alex", data: %{ "age" => 17}})
      %{name: "alex", data: %{age: 17}}

  """
  def atomize_map_keys(%Date{} = value), do: value
  def atomize_map_keys(%Time{} = value), do: value
  def atomize_map_keys(%DateTime{} = value), do: value
  def atomize_map_keys(%NaiveDateTime{} = value), do: value
  # Avoid Plug.Upload.__struct__/0 is undefined compilation error useful/issues#52
  # alias Plug.Upload
  def atomize_map_keys(%Plug.Upload{} = value), do: value

  # handle lists in maps: github.com/dwyl/useful/issues/46
  def atomize_map_keys(items) when is_list(items) do
    for i <- items do
      atomize_map_keys(i)
    end
  end

  def atomize_map_keys(map) when is_map(map) do
    for {key, val} <- map, into: %{} do
      cond do
        is_atom(key) -> {key, atomize_map_keys(val)}
        true -> {String.to_atom(key), atomize_map_keys(val)}
      end
    end
  end

  def atomize_map_keys(value), do: value

  @doc """
  `empty_dir_contents/1` delete all files including any directories
  recursively in a dir but not the dir itself.

  Very happy for anyone to refactor this function to something pretty.
  """
  def empty_dir_contents(dir) do
    cond do
      is_nil(dir) ->
        {:error, "dir supplied to Useful.empty_dir_contents/1 is nil"}

      not File.dir?(dir) ->
        {:error, "#{dir} arg to Useful.empty_dir_contents/1 not a directory"}

      true ->
        # read contents of directory:
        {:ok, list} = File.ls(dir)

        Enum.each(list, fn f ->
          path = Path.join([dir, f]) |> Path.expand()

          cond do
            # delete all dirs recursively:
            File.dir?(path) ->
              File.rm_rf(path)

            # delete any files in the dir:
            File.exists?(path) ->
              File.rm(path)
          end
        end)

        {:ok, dir}
    end
  end

  @doc """
  `flatten_map/1` flattens a `Map` of any depth/nesting for easier processing.
  Deeply nested maps are denoted by "__" (double underscore) e.g:
  `%{name: Alex, detail: %{age: 17}}` becomes `%{name: Alex, detail__age: 17}`
  this makes it easy to see what the data structure was before flattening.
  Map keys are converted to Atom for simpler access and consistency.
  Inspired by: https://stackoverflow.com/questions/39401947/flatten-nested-map

  ## Examples

      iex> map = %{name: "alex", data: %{age: 17, height: 185}}
      iex> Useful.flatten_map(map)
      %{data__age: 17, data__height: 185, name: "alex"}

  """
  def flatten_map(map) when is_map(map) do
    map
    |> to_list_of_tuples
    |> Enum.map(&key_to_atom/1)
    |> Enum.into(%{})
  end

  @doc """
  `get_in_default/3` Proxies `Kernel.get_in/2`
  but allows setting a `default` value as the 3rd argument.

  ## Examples

      iex> map = %{name: "alex", data: %{age: 17, height: 185}}
      iex> Useful.get_in_default(map, [:data, :age])
      17
      iex> Useful.get_in_default(map, [:data, :iq], 180)
      180
      iex> Useful.get_in_default(nil, [:unhappy, :path], "Happy!")
      "Happy!"
  """
  def get_in_default(map, keys, default \\ nil) do
    # https://hexdocs.pm/elixir/1.14/Kernel.html#get_in/2
    # Enum.each(keys, )
    try do
      case get_in(map, keys) do
        nil -> default
        result -> result
      end
    rescue
      _ ->
        default
    end
  end

  @doc """
  `remove_item_from_list/2` removes a given `item` from a `list` in any position.

  ## Examples

      iex> list = ["They'll", "never", "take", "our", "freedom!"]
      iex> Useful.remove_item_from_list(list, "never")
      ["They'll", "take", "our", "freedom!"]

  """
  def remove_item_from_list(list, item) do
    if Enum.member?(list, item) do
      i = Enum.find_index(list, fn it -> it == item end)
      List.delete_at(list, i)
    else
      list
    end
  end

  @doc """
  `stringy_map/1` converts a `Map` of any depth/nesting into a string.
  Deeply nested maps are denoted by "__" (double underscore). See flatten_map/1
  for more details.
  Alphabetizes the keys for consistency. See: github.com/dwyl/useful/issues/56

  ## Examples

      iex> map = %{name: "alex", data: %{age: 17, height: 185}}
      iex> Useful.stringify_map(map)
      "data__age: 17, data__height: 185, name: alex"

  """
  def stringify_map(map) when is_nil(map), do: "nil"

  def stringify_map(map) when is_map(map) do
    map
    |> flatten_map()
    |> Enum.sort()
    |> Enum.map(&stringify_tuple/1)
    |> Enum.join(", ")
  end

  @doc """
  `stringify_tuple/1` stringifies a `Tuple` with arbitrary values.
  Handy when you want to print out a tuple during debugging.

  ## Examples

      iex> tuple = {:name, "alex"}
      iex> Useful.stringify_tuple(tuple)
      "name: alex"
  """
  def stringify_tuple({key, values}) when is_list(values) do
    text = Enum.join(values, ", ")
    stringify_tuple({key, "\"#{text}\""})
  end

  def stringify_tuple({key, value}) do
    "#{key}: #{value}"
  end

  # Recap: https://elixir-lang.org/getting-started/basic-types.html#tuples
  defp to_list_of_tuples(map) do
    map
    |> Enum.map(&process/1)
    |> List.flatten()
  end

  # avoids the error "** (Protocol.UndefinedError) protocol Enumerable
  #   not implemented for ~U[2017-08-05 16:34:08.003Z] of type DateTime"
  defp process({key, %Date{} = date}), do: {key, date}
  defp process({key, %DateTime{} = datetime}), do: {key, datetime}

  # process nested maps
  defp process({key, sub_map}) when is_map(sub_map) do
    for {sub_key, value} <- flatten_map(sub_map) do
      {"#{key}__#{sub_key}", value}
    end
  end

  # catch-all for any type of key/value
  defp process({key, value}), do: {key, value}

  # Converts the {key: value} with Atom key for simpler access
  defp key_to_atom({key, value}) do
    {String.to_atom("#{key}"), value}
  end

  @doc """
  `typeof/1` returns the type of a vairable.
  Inspired by stackoverflow.com/questions/28377135/check-typeof-variable-in-elixir

  ## Examples

      iex> Useful.typeof(:atom)
      "atom"

      iex> bin = "hello"
      iex> Useful.typeof(bin)
      "binary"

      iex> bitstr = <<1::3>>
      iex> Useful.typeof(bitstr)
      "bitstring"

      iex> Useful.typeof(:true)
      "boolean"

      iex> pi = 3.14159
      iex> Useful.typeof(pi)
      "float"

      iex> fun = fn (a, b) -> a + b end
      iex> Useful.typeof(fun)
      "function"

      iex> Useful.typeof(&Useful.typeof/1)
      "function"

      iex> int = 42
      iex> Useful.typeof(int)
      "integer"

      iex> list = [1,2,3,4]
      iex> Useful.typeof(list)
      "list"

      iex> map = %{:foo => "bar", "hello" => :world}
      iex> Useful.typeof(map)
      "map"

      iex> Useful.typeof(nil)
      "nil"

      iex> pid = spawn(fn -> 1 + 2 end)
      iex> Useful.typeof(pid)
      "pid"

      iex> port = Port.open({:spawn, "cat"}, [:binary])
      iex> Useful.typeof(port)
      "port"

      iex> ref = :erlang.make_ref
      iex> Useful.typeof(ref)
      "reference"

      iex> tuple = {:name, "alex"}
      iex> Useful.typeof(tuple)
      "tuple"
  """
  types =
    ~w[boolean binary bitstring float function integer list map nil pid port reference tuple atom]

  for type <- types do
    def typeof(x) when unquote(:"is_#{type}")(x), do: unquote(type)
  end

  # No idea how to test this. Do you? ¯\_(ツ)_/¯
  # coveralls-ignore-start
  def typeof(_) do
    "unknown"
  end

  # coveralls-ignore-stop
end