Current section
Files
Jump to
Current section
Files
lib/pit.ex
defmodule Pit do
defmodule PipedValueMismatch do
defexception [:message, :pattern, :value]
end
@doc ~S"""
The `pit` macro lets you pipe value transformations by pattern matching
on data as it is passed down the pipe.
The syntax for transforming values is `expression |> pit(value <- pattern)`.
## Examples
iex> # The following will ensure there are no errors on
iex> # the response and double the count value from data.
iex> import Pit
...> response = {:ok, %{data: %{"count" => 10}, errors: []}}
...> response
...> |> pit(data <- {:ok, %{errors: [], data: data}})
...> |> pit(count * 2 <- %{"count" => count})
20
iex> # By using the ! operator, you can pipe values
iex> # only if they dont match some pattern
iex> # This example only pipes anything that aint an error
iex> import Pit
...> response = {:cool, 22}
...> response
...> |> pit(! {:error, _})
...> |> pit(n <- {_, n})
22
iex> # if the piped value does not match an error is raised.
iex> import Pit
...> response = {:error, :not_found}
...> response
...> |> pit(! {:error, _})
...> |> pit(n <- {:ok, n})
** (Pit.PipedValueMismatch) did not expect piped value to match `{:error, _}` but got `{:error, :not_found}`
iex> # also, when a guard fails an error is raised
iex> import Pit
...> response = {:ok, 22}
...> response
...> |> pit({:ok, n} when n > 30)
...> |> pit(n <- {:ok, n})
** (Pit.PipedValueMismatch) expected piped value to match `{:ok, n} when n > 30` but got `{:ok, 22}`
iex> # You can provide a default value in case of mismatch
iex> import Pit
...> response = {:error, :not_found}
...> response
...> |> pit({:ok, _}, else_value: {:ok, :default})
...> |> pit(n <- {:ok, n})
:default
iex> # Or you can pipe the mismatch value to other pipe using `else:` option
iex> # and get the value down a more interesting transformation flow.
iex> import Pit
...> response = {:ok, "hello"}
...> response
...> |> pit({:ok, n} when is_integer(n),
...> do_value: {:ok, :was_integer, n},
...> else: pit(s <- {:ok, s} when is_binary(s)) |> String.length |> pit({:ok, :was_string, len} <- len))
...> |> pit(x * 2 <- {:ok, _, x})
10
iex> # Both `do` and `else` if given the `:it` atom just pass the value down
iex> import Pit
...> {:error, 22} |> pit({:ok, _}, else: :it)
{:error, 22}
iex> import Pit
...> {:ok, 22} |> pit({:ok, _}, do: :it)
{:ok, 22}
iex> # The do form can take a block using bound variables.
iex> import Pit
...> {:ok, 22} |> (pit {:ok, n} do
...> x = n / 11
...> x * 2
...> end)
4.0
"""
defmacro pit(pipe, expr, options \\ []) do
pit_pipe(pipe, expr, options)
end
def pit_pipe(piped, expr, options) do
options = [
do: do_pipe(Keyword.take(options, [:do, :do_value])),
else: else_pipe(expr, Keyword.take(options, [:else, :else_value]))
]
quote do
unquote(piped) |> unquote(pit_fn(expr, options)).()
end
end
defp pit_fn(expr, options) do
it = Macro.var(:it, __MODULE__)
quote do
fn unquote(it) ->
case unquote(it) do
unquote(pit_branches(it, expr, options))
end
end
end
end
defp pit_branches(_it, {:<-, _, [expr, pattern = {v, _, s}]}, [do: do_pipe, else: _else_pipe]) when is_atom(v) and is_atom(s) do
quote do
unquote(pattern) -> unquote(expr) |> unquote(do_pipe)
end
end
defp pit_branches(it, {:<-, _, [expr, pattern]}, [do: do_pipe, else: else_pipe]) do
quote do
unquote(pattern) -> unquote(expr) |> unquote(do_pipe)
_ -> unquote(it) |> unquote(else_pipe)
end
end
defp pit_branches(it, {:!, _, [pattern]}, [do: do_pipe, else: else_pipe]) do
quote do
unquote(pattern) -> unquote(it) |> unquote(else_pipe)
_ -> unquote(it) |> unquote(do_pipe)
end
end
defp pit_branches(it, pattern = {v, _, s}, [do: do_pipe, else: _else_pipe]) when is_atom(v) and is_atom(s) do
quote do
unquote(pattern) -> unquote(it) |> unquote(do_pipe)
end
end
defp pit_branches(it, pattern, [do: do_pipe, else: else_pipe]) do
quote do
unquote(pattern) -> unquote(it) |> unquote(do_pipe)
_ -> unquote(it) |> unquote(else_pipe)
end
end
defp do_pipe(do: :it), do: do_pipe([])
defp do_pipe(do: body = {:__block__, _, _}) do
quote do
(fn _ -> unquote(body) end).()
end
end
defp do_pipe(do: pipe), do: pipe
defp do_pipe(do_value: expr) do
quote do
(fn _ -> unquote(expr) end).()
end
end
defp do_pipe([]) do
quote do
(fn it -> it end).()
end
end
defp else_pipe(_expr, else: :it) do
quote do
(fn it -> it end).()
end
end
defp else_pipe(_expr, body = {:__block__, _, _}) do
quote do
(fn _ -> unquote(body) end).()
end
end
defp else_pipe(_expr, else: pipe), do: pipe
defp else_pipe(_expr, else_value: expr) do
quote do
(fn _ -> unquote(expr) end).()
end
end
defp else_pipe({:<-, _, [_, pattern]}, []) do
mismatch({"expected piped value to match", pattern})
end
defp else_pipe({:!, _, [pattern]}, []) do
mismatch({"did not expect piped value to match", pattern})
end
defp else_pipe(pattern, []) do
mismatch({"expected piped value to match", pattern})
end
defp mismatch({message, pattern}) do
quote do
(fn it ->
raise PipedValueMismatch,
message: "#{unquote(message)} `#{unquote(Macro.to_string(pattern))}` but got `#{inspect(it)}`",
pattern: unquote(Macro.escape(pattern)),
value: it
end).()
end
end
end