defmodule Dreamy do @moduledoc """ Dreamy provides useful macros, functions, types & operators to make elixir even more dreamy 😴 """ defmacro __using__(_) do quote do import IO, only: [inspect: 1, inspect: 2, inspect: 3] alias Dreamy.Types import Dreamy, only: [ fallthrough: 2, otherwise: 3, const: 2, through: 2, unwrap: 1, unwrap_error: 1, flip: 1, ~>: 2, ~>>: 2, >>>: 2, |||: 2 ] end end defmodule Types do @moduledoc """ Useful Type definitions """ @typedoc "Type union of `t` & `nil`" @type nullable(t) :: t | nil @typedoc "Type for documenting the value within an enumerable" @type enumerable(_t) :: Enumerable.t() @typedoc "Wrapper for :ok, and :error tuple" @type result(ok, error) :: {:ok, ok} | {:error, error} end @doc """ Macro for adding a default catchall -> clause to case statements, that returns the input value ## Examples ``` iex> use Dreamy ...> fallthrough 1 do ...> 2 -> 2 ...> end 1 iex> use Dreamy ...> fallthrough 2 do ...> 2 -> 2 ...> end 2 ``` """ 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 1, nil do ...> 2 -> 2 ...> end nil iex> use Dreamy ...> otherwise 2, nil do ...> 2 -> 2 ...> end 2 ``` """ 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 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)) def unquote(name)(), do: unquote(code) end ) end @doc """ function for applying anonymous functions ## Examples ``` iex> use Dreamy iex> 2 |> iex> through(fn x -> x - 1 end) |> iex> through(fn x -> x - 1 end) 0 ``` """ @spec through(any(), (any() -> any())) :: any() def through(v, fun), do: fun.(v) @doc """ Function for extracting values from :ok records ## Examples ``` iex> use Dreamy ...> {:ok, "hello"} |> unwrap "hello" iex> use Dreamy ...> {:error, "world"} |> unwrap {:error, "world"} ``` """ @spec unwrap(Types.result(x, y)) :: x | {:error, y} when x: var, y: var def unwrap({:ok, v}), do: v def unwrap({:error, v}), do: {:error, v} @doc """ Function for extracting values from :error tuples ## Examples ``` iex> use Dreamy ...> {:ok, "hello"} |> unwrap_error {:ok, "hello"} iex> use Dreamy ...> {:error, "world"} |> unwrap_error "world" ``` """ @spec unwrap_error(Types.result(x, y)) :: {:ok, x} | y when x: var, y: var def unwrap_error({:ok, v}), do: {:ok, v} def unwrap_error({:error, v}), do: v @doc """ Function for applying result tuples on success only ## Examples ``` iex> use Dreamy ...> {:ok, 1} ~> ...> fn x -> {:ok, x + 1} end ~> ...> fn y -> y + 1 end 3 iex> use Dreamy ...> {:error, 1} ~> fn x -> x + 1 end {:error, 1} ``` """ @spec Types.result(res, err) ~> (res -> x) :: x | {:error, err} when res: var, err: var, x: var def {:ok, l} ~> r when is_function(r), do: r.(l) def {:error, l} ~> r when is_function(r), do: {:error, l} @doc """ Operator short hand 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] ``` """ defmacro enumerable >>> func do quote do Enum.map(unquote(enumerable), unquote(func)) end end @doc """ Function for applying result tuples on success only ## Examples ``` iex> use Dreamy ...> {:ok, 1} ~>> ...> fn {:ok, x} -> {:ok, x + 1} end ~>> ...> fn {:ok, x} -> {:ok, x + 1} end {:ok, 3} iex> use Dreamy ...> {:error, 1} ~>> fn {:ok, x} -> {:ok, x + 1} end {:error, 1} ``` """ @spec Types.result(res, err) ~>> (Types.result(res, err) -> x) :: x | {:error, err} when res: var, err: var, x: var def ({:ok, _} = l) ~>> r when is_function(r), do: r.(l) def ({:error, _} = l) ~>> r when is_function(r), do: l @doc """ Function for flipping results ## Examples ``` iex> use Dreamy ...> flip({:ok, 3}) {:error, 3} iex> use Dreamy ...> flip({:error, 3}) {:ok, 3} ``` """ @spec flip(Types.result(res, err)) :: Types.result(err, res) when res: var, err: var def flip({:ok, v}), do: {:error, v} def flip({:error, v}), do: {:ok, v} @doc """ Function getting the first :ok result if one exists ## Examples ``` iex> use Dreamy ...> {:ok, "success"} ||| {:ok, "success 2"} {:ok, "success"} iex> use Dreamy ...> {:error, "oops"} ||| {:ok, "success 2"} {:ok, "success 2"} iex> use Dreamy ...> {:error, "oops"} ||| {:error, "darn"} {:error, "oops"} iex> use Dreamy ...> {:ok, "first try"} ||| {:error, "darn"} ||| {:ok, "finally"} {:ok, "first try"} iex> use Dreamy ...> {:error, "oops"} ||| {:error, "darn"} ||| {:ok, "finally"} {:ok, "finally"} ``` """ @spec Types.result(a, err_a) ||| Types.result(b, any()) :: {:ok, a} | {:ok, b} | {:error, err_a} when a: var, b: var, err_a: var def ({:ok, _} = l) ||| _, do: l def {:error, _} ||| ({:ok, _} = r), do: r def ({:error, _} = l) ||| {:error, _}, do: l end