lib/r_map/active_support.ex

defmodule RMap.ActiveSupport do
  @moduledoc """
  Summarized all of Hash functions in Rails.ActiveSupport.
  If a function with the same name already exists in Elixir, that is not implemented.
  Defines all of here functions when `use RMap.ActiveSupport`.
  """
  @spec __using__(any) :: list
  defmacro __using__(_opts) do
    RUtils.define_all_functions!(__MODULE__)
  end

  import RMap.Ruby

  # https://www.rubydoc.info/gems/activesupport/Hash
  # [:as_json, :assert_valid_keys, :compact_blank, :compact_blank!, :deep_dup, :deep_merge, :deep_merge!, :deep_stringify_keys, :deep_stringify_keys!, :deep_symbolize_keys, :deep_symbolize_keys!, :deep_transform_keys, :deep_transform_keys!, :deep_transform_values, :deep_transform_values!, :except, :except!, :extract!, :extractable_options?, :reverse_merge, :reverse_merge!, :slice!, :stringify_keys, :stringify_keys!, :symbolize_keys, :symbolize_keys!, :to_query, :to_xml, :with_indifferent_access]
  # |> RUtils.required_functions([List, RMap.Ruby, REnum])
  # × as_json
  # ✔ assert_valid_keys
  # × deep_dup
  # × deep_merge
  # ✔ deep_stringify_keys
  # ✔ deep_symbolize_keys
  # ✔ deep_transform_keys
  # ✔ deep_transform_values
  # × extractable_options?
  # × reverse_merge
  # ✔ stringify_keys
  # ✔ symbolize_keys
  # to_query
  # to_xml
  # with_indifferent_access TODO: Low priority

  @doc """
  Validates all keys in a map match given keys, raising ArgumentError on a mismatch.
  ## Examples
      iex> RMap.assert_valid_keys(%{name: "Rob", years: "28"}, [:name, :age])
      ** (ArgumentError) Unknown key: years. Valid keys are: name, age

      iex> RMap.assert_valid_keys(%{name: "Rob", age: "28"}, ["age"])
      ** (ArgumentError) Unknown key: age. Valid keys are: age

      iex> RMap.assert_valid_keys(%{name: "Rob", age: "28"}, [:name, :age])
      :ok
  """
  @spec assert_valid_keys(map(), list()) :: :ok
  def assert_valid_keys(map, keys) do
    valid_keys_str = keys |> Enum.map(&IO.inspect(&1)) |> Enum.join(", ")

    each_key(map, fn key ->
      if(key not in keys) do
        raise ArgumentError, "Unknown key: #{key}. Valid keys are: #{valid_keys_str}"
      end
    end)
  end

  @doc """
  Returns a map with all keys converted to strings.
  ## Examples
      iex> RMap.stringify_keys(%{name: "Rob", years: "28", nested: %{ a: 1 }})
      %{"name" => "Rob", "nested" => %{a: 1}, "years" => "28"}
  """
  @spec stringify_keys(map()) :: map()
  def stringify_keys(map) do
    transform_keys(map, &to_string(&1))
  end

  @doc """
  Returns a list with all keys converted to strings.
  This includes the keys from the root map and from all nested maps and arrays.
  ## Examples
      iex> RMap.deep_stringify_keys(%{name: "Rob", years: "28", nested: %{ a: 1 }})
      %{"name" => "Rob", "nested" => %{"a" => 1}, "years" => "28"}

      iex> RMap.deep_stringify_keys(%{a: %{b: %{c: 1}, d: [%{a: 1, b: %{c: 2}}]}})
      %{"a" => %{"b" => %{"c" => 1}, "d" => [%{"a" => 1, "b" => %{"c" => 2}}]}}
  """
  @spec deep_stringify_keys(map()) :: map()
  def deep_stringify_keys(map) do
    deep_transform_keys(map, &to_string(&1))
  end

  @doc """
  Returns a map with all keys converted to atom.
  ## Examples
      iex> RMap.symbolize_keys(%{"name" => "Rob", "years" => "28", "nested" => %{ "a" => 1 }})
      %{name: "Rob", nested: %{"a" => 1}, years: "28"}
  """
  @spec symbolize_keys(map()) :: map()
  def symbolize_keys(map) do
    transform_keys(map, &String.to_atom(&1))
  end

  @doc """
  Returns a list with all keys converted to atom.
  This includes the keys from the root map and from all nested maps and arrays.
  ## Examples
      iex> RMap.deep_symbolize_keys(%{"name" => "Rob", "years" => "28", "nested" => %{ "a" => 1 }})
      %{name: "Rob", nested: %{a: 1}, years: "28"}

      iex> RMap.deep_symbolize_keys(%{"a" => %{"b" => %{"c" => 1}, "d" => [%{"a" => 1, "b" => %{"c" => 2}}]}})
      %{a: %{b: %{c: 1}, d: [%{a: 1, b: %{c: 2}}]}}
  """
  @spec deep_symbolize_keys(map()) :: map()
  def deep_symbolize_keys(map) do
    deep_transform_keys(map, &String.to_atom(&1))
  end

  @doc """
  Returns a map with all keys converted by the function.
  This includes the keys from the root map and from all nested maps and arrays.
  ## Examples
      iex> RMap.deep_transform_keys(%{a: %{b: %{c: 1}}}, &to_string(&1))
      %{"a" => %{"b" => %{"c" => 1}}}

      iex> RMap.deep_transform_keys(%{a: %{b: %{c: 1}, d: [%{a: 1, b: %{c: 2}}]}}, &inspect(&1))
      %{":a" => %{":b" => %{":c" => 1}, ":d" => [%{":a" => 1, ":b" => %{":c" => 2}}]}}
  """
  @spec deep_transform_keys(map(), function()) :: map()
  def deep_transform_keys(map, func) do
    map
    |> Enum.map(fn {k, v} ->
      cond do
        is_map(v) -> {func.(k), deep_transform_keys(v, func)}
        is_list(v) -> {func.(k), Enum.map(v, fn el -> deep_transform_keys(el, func) end)}
        true -> {func.(k), v}
      end
    end)
    |> Map.new()
  end

  @doc """
  Returns a map with all values converted by the function.
  This includes the keys from the root map and from all nested maps and arrays.
  ## Examples
      iex> RMap.deep_transform_values(%{a: %{b: %{c: 1}}, d: 2}, &inspect(&1))
      %{a: %{b: %{c: "1"}}, d: "2"}

      iex> RMap.deep_transform_values(%{a: %{b: %{c: 1}, d: [%{a: 1, b: %{c: 2}}]}}, &inspect(&1))
      %{a: %{b: %{c: "1"}, d: [%{a: "1", b: %{c: "2"}}]}}
  """
  @spec deep_transform_values(map(), function()) :: map()
  def deep_transform_values(map, func) do
    Enum.map(map, fn {k, v} ->
      cond do
        is_map(v) -> {k, deep_transform_values(v, func)}
        is_list(v) -> {k, Enum.map(v, fn el -> deep_transform_values(el, func) end)}
        true -> {k, func.(v)}
      end
    end)
    |> Map.new()
  end

  defdelegate atomize_keys(map), to: __MODULE__, as: :symbolize_keys
  defdelegate deep_atomize_keys(map), to: __MODULE__, as: :deep_symbolize_keys
end