lib/helper/macro.ex

# Copyright (c) 2021 Anand Panchapakesan
#
# This software is released under the MIT License.
# https://opensource.org/licenses/MIT

defmodule Helper.Macro do
  @moduledoc """
  Common functions for manipulating ast / quoted code
  """

  @typedoc """
  AST type
  """
  @type t() :: Macro.t()

  @doc """
  Returns the module from the AST alias.

    ## Example
          iex> ast2mod!({:__aliases__, [], [Elixir, String]})
          Elixir.String

          iex> ast2mod!([{:__aliases__, [], [Elixir, String]}])
          Elixir.String

          iex> ast2mod!(String)
          String

          iex> ast2mod!("String")
          ** (ArgumentError) "String": Invalid value for module
  """
  @spec ast2mod!(module() | t()) :: module() | no_return()
  def ast2mod!([m]), do: ast2mod!(m)
  def ast2mod!(m) when is_atom(m), do: m
  def ast2mod!({:__aliases__, _, m}) when is_list(m), do: Module.concat(m)
  def ast2mod!(e), do: raise(ArgumentError, "#{inspect(e)}: Invalid value for module")

  @doc """
  Parses the AST and returns the node and value in a keyword list

    ## Example
          iex> ast2kw([{:primary_key, [], [:id]}, {:timestamps, [], [[type: :utc_datetime_usec]]}])
          [primary_key: :id, timestamps: [type: :utc_datetime_usec]]
  """
  @spec ast2kw(t()) :: keyword()
  def ast2kw(ast), do: for({key, _, [value]} <- ast, do: {key, value})

  @doc """
  Parses the AST and returns the node and value in a map

    ## Example
          iex> attr2map([{:source, [], ["embedded"]}])
          %{source: "embedded"}

          iex> attr2map([{:repo, [], [{Helper.Example.Repo, []}]}])
          %{repo: {Helper.Example.Repo, []}}

          iex> attr2map([{:name, [], [{:__aliases__, [], [Helper, Example, User]}]}])
          %{name: Helper.Example.User}
  """
  @spec attr2map(t()) :: map()
  def attr2map(ast), do: Enum.reduce(ast, %{}, &do_attr2map/2)

  defp do_attr2map({:name, _, [value]}, map), do: Map.put(map, :name, ast2mod!(value))

  defp do_attr2map({:repo, _, [{repo, options}]}, map),
    do: Map.put(map, :repo, {ast2mod!(repo), options})

  defp do_attr2map({:source, _, [value]}, map), do: Map.put(map, :source, value)

  @doc """
  Split the AST with nodes matching the values in the first value and the remain in the second value

    ## Example
          iex> split({:primary_key, [], []}, :primary_key)
          {[{:primary_key, [], []}], []}

          iex> split({:timestamps, [], []}, :primary_key)
          {[], [{:timestamps, [], []}]}

          iex> split([{:primary_key, [], []}], [:timestamps, :primary_key])
          {[{:primary_key, [], []}], []}
  """
  @spec split(t(), list(atom()) | atom()) :: {t(), t()}
  def split(ast, value) when is_list(ast), do: Enum.split_with(ast, &can_split?(&1, value))
  def split(ast, value), do: if(can_split?(ast, value), do: {[ast], []}, else: {[], [ast]})

  @spec can_split?(t(), list(atom()) | atom()) :: boolean()
  defp can_split?({node, _, _}, value) when is_atom(value), do: node == value
  defp can_split?({node, _, _}, values), do: Enum.member?(values, node)
end