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)`.
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
iex> # and get its value down a more interesting transformation flow.
iex> import Pit
...> response = {:ok, "hello"}
...> response
...> |> pit({:ok, n} when is_integer(n),
...> else: pit(s <- {:ok, s} when is_binary(s)) |> String.length |> pit({:ok, len} <- len))
...> |> pit(x * 2 <- {:ok, x})
10
"""
defmacro pit(pipe, expr, options \\ []) do
hole = down_the_pit(expr, fallback(expr, options))
quote do
unquote(pipe) |> unquote(hole).()
end
end
defp down_the_pit({:<-, _, [value, pattern = {v, _, l}]}, _fallback) when is_atom(v) and is_atom(l) do
quote do
fn unquote(pattern) -> unquote(value) end
end
end
defp down_the_pit({:<-, _, [value, pattern]}, fallback) do
quote do
fn it ->
case it do
unquote(pattern) -> unquote(value)
_ -> it |> unquote(fallback)
end
end
end
end
defp down_the_pit({:!, _, [pattern]}, fallback) do
quote do
fn it ->
case it do
unquote(pattern) -> it |> unquote(fallback)
_ -> it
end
end
end
end
defp down_the_pit({v, _, l}, _fallback) when is_atom(v) and is_atom(l) do
quote do
fn it -> it end
end
end
defp down_the_pit(pattern, fallback) do
quote do
fn it ->
case it do
unquote(pattern) -> it
_ -> it |> unquote(fallback)
end
end
end
end
defp fallback(_, [else: pipe]), do: pipe
defp fallback(_, [else_value: code]) do
quote do
(fn _ -> unquote(code) end).()
end
end
defp fallback({:<-, _, [_, pattern]}, _) do
mismatch({"expected piped value to match", pattern})
end
defp fallback({:!, _, [pattern]}, _) do
mismatch({"did not expect piped value to match", pattern})
end
defp fallback(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