lib/shorthand.ex

defmodule Shorthand do
  @moduledoc """
  Convenience macros to eliminate laborious typing. Provides macros for short map, string keyed map, keyword lists, and structs (ES6 like style)

  ## Examples:
  These examples use variable arguments (default is 10, see configuration below)

  Instead of `%{one: one, two: two, three: three}`, you can type `m(one, two, three)`

  Instead of `my_func(one: one, two: two, three: three)`, you can type `my_func(kw(one, two, three))`

  Instead of `%MyStruct(one: one, two: two, three: three)`, you can type `st(MyStruct, one, two, three)`

  ### Without variable arguemnts,
  Instead of `%{one: one, two: two, three: three}`, you can type `m([one, two, three])`

  Instead of `my_func(one: one, two: two, three: three)`, you can type `my_func(kw([one, two, three]))`

  Instead of `%MyStruct(one: one, two: two, three: three)`, you can type `bulid_struct(MyStruct, [one, two, three])`

  ## Configuration

  For the convenience of `m(a, b, c, d, e, f, g, h, i, j)` instead of `m([a, b, c, d, e, f, g, h, i, j])` `Shorthand` generates multiple copies of each macro like m/1, m/2, m/3, …, m/10. You can configure how many of these "variable argument" macros are generated.

      config :shorthand,
        variable_args: 10 # false to remove variable arguemnts


  ## Usage

  You can import the `Shorthand` module and call each macro

      defmodule MyModule do
        import Shorthand

        def my_func(m(a, b)) do
          kw(a, b)
        end
      end

  or you can require the module and prefix calls with the module name (example uses a module alias to keep the typing low

      defmodule MyModule do
        require Shorthand, as: S

        def my_func(S.m(a, b)) do
          S.kw(a, b)
        end
      end

  ## Phoenix Examples
      defmodule MyController do
        # ...

        def index(conn, sm(id)) do
          model = Repo.get(MyModel, id)
          # ...
        end

        def create(conn, sm(my_model: sm(first_name, last_name) = params)) do
          changeset = MyModel.changeset(%MyModel{}, params) # params contains all form fields, not just first_name and last_name
          # ...
          conn
          |> put_flash(:notice, "User \#{first_name} \#{last_name} was created successfully")
          # ...
        end
      end
  """

  @doc ~S"""
  Builds a map where the keys and values have the same name

  ## Example:

      iex> a = 1
      iex> b = 2
      iex> m(a, b)
      %{a: 1, b: 2}

  ## Example:

      iex> a = 1
      iex> c = 2
      iex> m(a, b: m(c), d: nil)
      %{a: 1, b: %{c: 2}, d: nil}

  ## Example:

      iex> a = 1
      iex> m(^a, _b, c) = %{a: 1, b: 3, c: 2}
      iex> c
      2
      iex> match?(m(^a), %{a: 2})
      false

  ## Example:

      iex> m(model: m(a, b) = params) = %{model: %{a: 1, b: 2, c: 3, d: 4}}
      iex> params
      %{a: 1, b: 2, c: 3, d: 4}
      iex> a
      1
      iex> b
      2
  """
  defmacro m([_ | _] = args) do
    build_map(args, :atom)
  end

  @doc ~S"""
  Builds a map where the string keys and values have the same name

  ## Example:

      iex> a = 1
      iex> b = 2
      iex> sm(a, b)
      %{"a" => 1, "b" => 2}

  ## Example:

      iex> a = 1
      iex> b = 2
      iex> sm(a, other: sm(b))
      %{"a" => 1, "other" => %{"b" => 2}}

  ## Example:

      iex> a = 1
      iex> sm(^a, _b, c) = %{"a" => 1, "b" => 3, "c" => 2}
      iex> c
      2
      iex> match?(sm(^a), %{"a" => 2})
      false

  ## Example:

      iex> sm(model: sm(a, b) = params) = %{"model" => %{"a" => 1, "b" => 2, "c" => 3, "d" => 4}}
      iex> params
      %{"a" => 1, "b" => 2, "c" => 3, "d" => 4}
      iex> a
      1
      iex> b
      2
  """
  defmacro sm([_ | _] = args) do
    build_map(args, :string)
  end

  @doc ~S"""
  Builds a keyword list where the keys and value arguments are the same name

  ## Example:

      iex> a = 1
      iex> b = 2
      iex> c = 3
      iex> kw(a, b, c)
      [a: 1, b: 2, c: 3]

  ## Examples

      iex> c = 3
      iex> kw(a, _b, ^c) = [a: 1, b: 3, c: 3]
      iex> a
      1
      iex> match?(kw(^a), [a: 2])
      false
  """
  defmacro kw([_ | _] = args) do
    build_keywords(args)
  end

  @doc ~S"""
  Builds a struct where the field names are the same as the arguments supplied

  ## Example:

      iex> scheme = "https"
      iex> host = "elixir-lang.org"
      iex> path = "/docs.html"
      iex> st(URI, scheme, host, path)
      %URI{scheme: "https", host: "elixir-lang.org", path: "/docs.html"}
  """
  defmacro st(module, [_ | _] = args) do
    build_struct_from_list(module, args)
  end

  variable_args = Application.get_env(:shorthand, :variable_args, 10)

  if variable_args do
    1..variable_args
    |> Enum.each(fn i ->
      args = 1..i |> Enum.map(fn i -> {:"arg#{i}", [], nil} end)

      defmacro m(unquote_splicing(args)) do
        build_map(unquote(args), :atom)
      end

      defmacro sm(unquote_splicing(args)) do
        build_map(unquote(args), :string)
      end

      defmacro kw(unquote_splicing(args)) do
        build_keywords(unquote(args))
      end

      defmacro st(module, unquote_splicing(args)) do
        build_struct_from_list(module, unquote(args))
      end
    end)
  end

  defp build_map(args, type) do
    {:%{}, [], parse_args(args, type)}
  end

  defp build_keywords(args) do
    quote do
      unquote(parse_args(args, :atom))
    end
  end

  defp parse_args(args, type) do
    # IO.inspect(args, label: "args")

    args
    |> Enum.map(fn
      {name, value} ->
        {map_key(name, type), value}

      {:^, context1, [{name, context2, nil}]} ->
        {map_key(name, type), {:^, context1, [{name, context2, nil}]}}

      {name, context, nil} ->
        {map_key(variable_name(name), type), {name, context, nil}}

      keyword_list when is_list(keyword_list) ->
        keyword_list
        |> Enum.map(fn {key, value} -> {map_key(key, type), value} end)

        # other ->
        #   IO.inspect(other, label: "other")
    end)
    |> List.flatten()
  end

  defp build_struct_from_list(module, args) do
    quote do
      struct(unquote(module), unquote(build_keywords(args)))
    end
  end

  defp map_key(key, :atom) when is_atom(key), do: key
  defp map_key(key, :string) when is_atom(key), do: to_string(key)

  defp variable_name(name) when is_atom(name), do: variable_name(Atom.to_string(name))
  defp variable_name(<<"_", name::binary>>), do: variable_name(name)
  defp variable_name(name), do: String.to_atom(name)
end