defmodule PipeHelpers do @moduledoc """ Helpers for piping data. The provided helpers can be grouped into two categories: 1. generate tuples that fit the return type of the containing function 2. deal with previous function tuple output without "breaking the pipe" ## 1. Return tuples builders You can use only the first category to improve readability in your code. For instance, lets take a typical `Phoenix.LiveView` code: def mount(_params, _session, socket) do socket = assign(socket, my_bool_assign: true) {:ok, socket} end With `ok/1`, it can be rewritten as: def mount(_params, _session, socket) do socket |> assign( my_bool_assign: true) |> ok() end In addition to the typical `{:ok, value}` return tuple, the library provides the most common return types helpers `noreply/1` `reply/2`. Generic helpers are also provided to build whatever tuple you need with `pair/2` and `rpair/2`. ## 2. Tuple deconstructors In some situations, return type incompatibilities prevent you from using an uninterrupted pipe sequence. In this case, the following helpers may help: ### 2a Generic unpair The generic `unpair/1` allows you to ignore the first element of the pair and forward the second for further piping. ### 2b Only when "ok" execution The `then_ok/2` provides a promise-like solution to handle 'maybe ok' results. If the result is an ok-tuple, it executes the function on the second argument and passes the result forward (i.e. for further piping). If the result is not an ok-tuple (typically an `{:error, message}` tuple), it is forwarded directly. For example, let's take the following (very powerful translator) function: def translate(source) do case source do "ola" -> {:ok, "hello"} _ -> {:error, "can't translate, sorry"} end end And the quoting function that you want to pipe into: def quote(text) do text end The `translate/1` output doesn't fit the `quote/1` input, so the following pipe is not possible: my_source |> translate() |> quote() With the `then_ok/2` you can handle do: my_source |> translate() |> then_ok("e/1) This pipe will either return the properly quoted translation or the error tuple generated by the `translate/1`. > If you intend to successively pipe into more than a single functions after an > `{:ok, result}` returning function, you need to only have `{:ok, result}` functions > except the last one and always use the `then_ok/2` construct until the end. Doing so > you can safely do the error management at the end of the pipe using a `|> case do` > irrespective of the function that failed to provide an `{:ok, result}` output. The `then_on/3` provides similar behaviour with a more generic/powerful pattern matching on the previous function output. The functions above have their `tap` counterpart if the idea is to apply a conditional side effect without modifying the piped value. """ @doc """ Wrap into standard `{:ok, result}` tuple. ## Example iex> socket = "socket" ...> socket |> ok() {:ok, "socket"} """ def ok(val) do {:ok, val} end @doc """ Wrap into `{:noreply, state}` tuple (genserver and phoenix socket format). ## Example iex> state = "gen server state" ...> state |> noreply() {:noreply, "gen server state"} """ def noreply(val) do {:noreply, val} end @doc """ Wrap into `{:reply, reply, state}` tuple (genserver and phoenix socket format). ## Example iex> state = "gen server state" ...> r = "reply" ...> state |> reply(r) {:reply, "reply", "gen server state"} """ def reply(state, reply) do {:reply, reply, state} end @doc """ Wrap into generic tuple pair. ## Example iex> 1 |> pair(2) {2, 1} """ def pair(val, res) do {res, val} end @doc """ Wrap into tuple rpair. ## Example iex> 1 |> rpair(2) {1, 2} """ def rpair(val, res) do {val, res} end @doc """ Unwrap from a tuple pair. ## Example iex> {:ok, 1} |> unpair() 1 """ def unpair({_val, res}) do res end @doc """ Execute `fun` function only if `result` is an ok-tuple. Returns the input in any case. The `fun` argument is a function taking the right part of the ok-tuple as an optional argument. ## Example iex> {:ok, "somedata"} |> tap_ok(fn -> "only executed when {:ok, ...}" end) {:ok, "somedata"} iex> {:error, "failed"} |> tap_ok(fn -> "not executed when not {:ok, ...}" end) {:error, "failed"} """ def tap_ok(result, fun) do then_ok(result, fun) result end @doc """ Execute `fun` function only if the first arument matches the second one (`value = result` check). Returns the input in any case. ## Example iex> true |> tap_on(true, fn -> "only executed when matching 'true'" end) true iex> false |> tap_on(true, fn -> "not executed when not matchin 'true'" end) false """ def tap_on(result, value, fun) do then_on(result, value, fun) result end @doc """ If `result` is an ok tuple, execute `fun` with the right part of `result` tuple as single argument and return `fun` output. Otherwise, return `result` as-is. ## Example iex> {:ok, "somedata"} |> then_ok(fn -> "only returned when result argument is {:ok, ...}" end) "only returned when result argument is {:ok, ...}" iex> {:error, "failed"} |> then_ok(fn -> "only returned when result argument is {:ok, ...}" end) {:error, "failed"} """ def then_ok({:ok, ok_val} = _result, fun) do fun |> Function.info() |> Keyword.get(:arity) |> case do 0 -> fun.() 1 -> fun.(ok_val) _ -> raise "then_ok function arity can only be 0 or 1" end end def then_ok(result, _fun), do: result @doc """ Then only if value match. See `then_ok/2` and `tap_ok/3` """ def then_on(value, value, fun) do fun |> Function.info() |> Keyword.get(:arity) |> case do 0 -> fun.() 1 -> fun.(value) _ -> raise "then_on function arity can only be 0 or 1" end end def then_on(result, _value, _fun), do: result end