lib/dreamy.ex

defmodule Dreamy do
  @moduledoc """
  Dreamy provides useful macros, functions, types & operators to make elixir even dreamier 😴
  """

  defmacro __using__(_) do
    quote do
      alias Dreamy.{Defaults, Either, Option, Result, Types}

      import Dreamy
      import Dreamy.{Defaults, Either, Monodic, Option, Result}
    end
  end

  defmodule Types do
    @moduledoc "Useful Type definitions"

    @typedoc "Type union of `t` & `nil`"
    @type nullable(v) :: v | nil

    @typedoc "Type for documenting the value within an enumerable"
    @type enumerable(_v) :: Enumerable.t()

    @typedoc "Monodic type that can hold a value"
    @type option(v) :: Dreamy.Option.t(v)

    @typedoc "Monodic type for :ok, :error tuples"
    @type result(ok, err) :: Dreamy.Result.t(ok, err)

    @typedoc "Monodic type representing a left or right tuple"
    @type either(l, r) :: Dreamy.Either.t(l, r)
  end

  @doc """
  Macro for adding a default catchall -> clause to case statements, that returns the input value

  ## Examples
  ```
  iex> use Dreamy
  ...> fallthrough {:error, "error"} do
  ...> {:ok, v} -> v
  ...> end
  {:error, "error"}

  iex> use Dreamy
  ...> fallthrough {:ok, "OK"} do
  ...> {:ok, "OK"} -> "OK"
  ...> end
  "OK"
  ```
  """
  defmacro fallthrough(val, do: code) do
    code =
      code ++
        [
          {:->, [], [[{:other, [], nil}], {:other, [], nil}]}
        ]

    quote do
      case unquote(val) do
        unquote(code)
      end
    end
  end

  @doc """
  Macro for adding a default catchall -> clause to case statements, that returns the default value

  ## Examples
  ```
  iex> use Dreamy
  ...> otherwise nil, {:error, :not_found} do
  ...> {:ok, _} -> :ok
  ...> end
  {:error, :not_found}

  iex> use Dreamy
  ...> otherwise {:ok, nil}, {:error, :not_found} do
  ...> {:ok, _} -> :ok
  ...> end
  :ok
  ```
  """
  defmacro otherwise(val, default, do: code) do
    code =
      code ++
        [
          {:->, [], [[{:_, [], nil}], default]}
        ]

    quote do
      case unquote(val) do
        unquote(code)
      end
    end
  end

  @doc """
  Macro for adding a default catchall `true` clause to cond statements, that returns the default value

  ## Examples
  ```
  iex> use Dreamy
  ...> x = 5
  ...> or_else "Less than 10" do
  ...> x == 10 -> "10"
  ...> x > 10 -> "Greater than 10"
  ...> end
  "Less than 10"

  iex> use Dreamy
  ...> x = 11
  ...> or_else "Less than 10" do
  ...> x == 10 -> "10"
  ...> x > 10 -> "Greater than 10"
  ...> end
  "Greater than 10"
  ```
  """
  defmacro or_else(default, do: code) do
    code =
      code ++
        [
          {:->, [], [[true], default]}
        ]

    quote do
      cond do
        unquote(code)
      end
    end
  end

  @doc """
  Macro for defining a constant attribute and function

  ## Examples
  ```
  iex> use Dreamy
  iex> const :example, "XYZ"
  iex> @example
  "XYZ"

  iex> example()
  "XYZ"
  ```
  """
  defmacro const(name, code) do
    caller = __CALLER__.module

    Module.register_attribute(caller, name, accumulate: false)

    Module.eval_quoted(
      caller,
      quote do
        Module.put_attribute(__MODULE__, unquote(name), unquote(code))
        # credo:disable-for-next-line
        def unquote(name)(), do: unquote(code)
      end
    )
  end

  @doc """
  Operator for Enum.map

  ## Examples
  ```
  iex> use Dreamy
  ...> x = fn y -> y + 1 end
  ...> y = fn z -> z * 2 end
  ...> [1, 2]
  ...> >>> x
  ...> >>> y
  [4, 6]

  iex> use Dreamy
  ...> x = fn y -> y - 1 end
  ...> [5, 7]
  ...> >>> x
  ...> >>> (&div(&1, 2))
  ...> >>> x
  [1, 2]
  ```
  """
  defmacro enumerable >>> func do
    quote do
      Enum.map(unquote(enumerable), unquote(func))
    end
  end

  @doc """
  conditionally apply a function to the input value,
  otherwise letting it pass through unchanged

  ## Examples
  ```
  iex> use Dreamy
  ...> x = fn y -> y - 1 end
  ...> 2
  ...> |> conditional_apply(true, x)
  ...> |> conditional_apply(false, x)
  ...> |> conditional_apply(true, x)
  0

  iex> use Dreamy
  ...> x = fn y -> y <> "!" end
  ...> "Hello"
  ...> |> conditional_apply(false, x)
  ...> |> conditional_apply(false, x)
  ...> |> conditional_apply(true, x)
  ...> |> conditional_apply(false, x)
  "Hello!"
  ```
  """
  @spec conditional_apply(v, false, function()) :: v when v: var
  @spec conditional_apply(v, true, (v -> res)) :: res when v: var, res: var
  def conditional_apply(val, false, _), do: val
  def conditional_apply(val, true, fun), do: fun.(val)
end