Tweak protocols and add test coverage.

This commit is contained in:
James Harton 2020-11-15 14:46:13 +13:00
parent 2cc786cb28
commit 7dec0917f7
21 changed files with 975 additions and 278 deletions

View file

@ -2,68 +2,155 @@ defmodule Upshot.Option do
alias Upshot.{ExpectError, Option.Proto, Result, UnwrapError}
@moduledoc """
A protocol for interacting with optional types.
A standard interface for working with optional types.
Options are values which either contain a result (called a "some"), or a
non-value (called a "none"). A good example of an option is the result of
`Map.fetch/2` - either an ok tuple or error atom - in fact the option protocol
is implemented for `Tuple` and `Atom` for exactly this reason.
In order to use module with your own types, you need to implement the
`Upshot.Option.Proto` protocol.
"""
@type t :: Proto.t()
@type map_fn :: (any -> t)
@type predicate_fn :: (any -> boolean)
@callback some(any) :: t
@callback none() :: t
@doc """
Create a new option from value.
## Example
iex> some("Marty")
{:ok, "Marty"}
"""
@spec some(any) :: t
def some(value), do: {:ok, value}
@doc """
Create a new option.
## Example
iex> none()
:error
"""
@spec none :: t
def none, do: :error
@doc """
Does the option contain a value?
## Examples
iex> some("Marty")
...> |> some?()
true
iex> none()
...> |> some?()
false
"""
@spec some?(t) :: boolean
defdelegate some?(option), to: Proto
@spec some?(t) :: boolean | no_return
def some?(option) do
assert_valid!(option)
Proto.some?(option)
end
@doc """
Does the option contain no value?
Is the option a none?
## Examples
iex> none()
...> |> none?()
true
iex> some("Marty")
...> |> none?()
false
"""
@spec none?(t) :: boolean
defdelegate none?(option), to: Proto
@spec none?(t) :: boolean | no_return
def none?(option) do
assert_valid!(option)
Proto.none?(option)
end
@doc """
Convert an option into a result.
Convert an option into an `Upshot.Result`.
If the option is some then it will contain the some, otherwise it will use the
provided error value.
## Examples
iex> some("Marty")
...> |> ok_or("Doc")
{:ok, "Marty"}
iex> none()
...> |> ok_or("Doc")
{:error, "Doc"}
"""
@spec ok_or(t, any) :: Result.t()
def ok_or(option, error) do
if Proto.some?(option),
do: option,
else: {:error, error}
@spec ok_or(t, any) :: Result.t() | no_return
def ok_or(option, error_value) do
assert_valid!(option)
case Proto.unwrap(option) do
{:ok, value} -> {:ok, value}
:error -> {:error, error_value}
end
end
@doc """
Convert an option into a result mapping a some value into an ok value, and
none into an error using the provided function.
## Examples
iex> some("Marty")
...> |> ok_or_else(fn -> "Doc" end)
{:ok, "Marty"}
iex> none()
...> |> ok_or_else(fn -> "Doc" end)
{:error, "Doc"}
"""
@spec ok_or_else(t, (() -> any)) :: t
@spec ok_or_else(t, (() -> any)) :: t | no_return
def ok_or_else(option, err_callback) when is_function(err_callback, 0) do
if Proto.some?(option),
do: option,
else: {:error, safe_callback(err_callback, [])}
assert_valid!(option)
if Proto.some?(option) do
option
else
{:error, apply(err_callback, [])}
end
end
@doc """
A logical and.
Returns none if the option is none, otherwise the other option.
Returns none if the lhs option is none, otherwise the rhs option.
## Examples
iex> and_(some("Doc"), some("Marty"))
{:ok, "Marty"}
iex> and_(none(), some("Marty"))
:error
"""
@spec and_(t, t) :: t
@spec and_(t, t) :: t | no_return
def and_(lhs, rhs) do
assert_valid!(lhs)
assert_valid!(rhs)
if Proto.none?(lhs),
do: lhs,
else: rhs
@ -72,20 +159,58 @@ defmodule Upshot.Option do
@doc """
Returns none, if the option is none, otherwise ok_callback with the wrapped
value and returns the result.
Useful for building combinators.
## Example
iex> square = fn i -> {:ok, i * i} end
...> some(2)
...> |> and_then(square)
...> |> and_then(square)
...> |> and_then(square)
{:ok, 256}
iex> square = fn i -> {:ok, i * i} end
...> some(2)
...> |> and_then(fn _ -> :error end)
...> |> and_then(square)
:error
"""
@spec and_then(t, map_fn) :: t
@spec and_then(t, map_fn) :: t | no_return
def and_then(option, ok_callback) when is_function(ok_callback, 1) do
if Proto.none?(option),
do: option,
else: safe_callback(ok_callback, [unwrap(option)])
assert_valid!(option)
case Proto.unwrap(option) do
:error ->
option
{:ok, value} ->
ok_callback
|> apply([value])
|> assert_valid!()
end
end
@doc """
Returns none if the option is none, otherwise calls predicate with the wrapped
value returns the wrapped value if the predicate returns true, otherwise none.
## Examples
iex> some(4)
...> |> filter(&(rem(&1, 2) == 0))
{:ok, 4}
iex> some(3)
...> |> filter(&(rem(&1, 2) == 0))
:error
"""
@spec filter(t, predicate_fn) :: t
@spec filter(t, predicate_fn) :: t | no_return
def filter(option, predicate) do
assert_valid!(option)
case Proto.unwrap(option) do
{:ok, value} ->
if apply(predicate, [value]),
@ -100,9 +225,23 @@ defmodule Upshot.Option do
@doc """
Returns the option if it contains a value, otherwise returns the alternative
option.
## Examples
iex> or_(some("Marty"), some("Doc"))
{:ok, "Marty"}
iex> or_(none(), some("Doc"))
{:ok, "Doc"}
iex> or_(none(), none())
:error
"""
@spec or_(t, t) :: t
@spec or_(t, t) :: t | no_return
def or_(lhs, rhs) do
assert_valid!(lhs)
assert_valid!(rhs)
if Proto.some?(lhs),
do: lhs,
else: rhs
@ -111,21 +250,52 @@ defmodule Upshot.Option do
@doc """
Returns the option if it contains a value, otherwise calls err_callback and
returns the resulting option.
## Examples
iex> some("Marty")
...> |> or_else(fn -> some("Doc") end)
{:ok, "Marty"}
iex> none()
...> |> or_else(fn -> some("Doc") end)
{:ok, "Doc"}
"""
@spec or_else(t, (() -> t)) :: t
@spec or_else(t, (() -> t)) :: t | no_return
def or_else(option, err_callback) when is_function(err_callback, 0) do
assert_valid!(option)
if Proto.some?(option) do
option
else
apply(err_callback, [])
err_callback
|> apply([])
|> assert_valid!()
end
end
@doc """
Returns some if exactly one of the options are some, otherwise none.
## Examples
iex> xor(some("Marty"), none())
{:ok, "Marty"}
iex> xor(none(), some("Doc"))
{:ok, "Doc"}
iex> xor(some("Marty"), some("Doc"))
:error
iex> xor(none(), none())
:error
"""
@spec xor(t, t) :: t
@spec xor(t, t) :: t | no_return
def xor(lhs, rhs) do
assert_valid!(lhs)
assert_valid!(rhs)
cond do
Proto.some?(lhs) && Proto.none?(rhs) -> lhs
Proto.none?(lhs) && Proto.some?(rhs) -> rhs
@ -137,9 +307,23 @@ defmodule Upshot.Option do
Zips the two options together.
If both options are some, returns a some containing a tuple of both values, otherwise none.
## Examples
iex> zip(some("Marty"), some("Doc"))
{:ok, {"Marty", "Doc"}}
iex> zip(some("Marty"), none())
:error
iex> zip(none(), some("Doc"))
:error
"""
@spec zip(t, t) :: t
@spec zip(t, t) :: t | no_return
def zip(lhs, rhs) do
assert_valid!(lhs)
assert_valid!(rhs)
with {:ok, lhs} <- Proto.unwrap(lhs),
{:ok, rhs} <- Proto.unwrap(rhs) do
{:ok, {lhs, rhs}}
@ -152,9 +336,21 @@ defmodule Upshot.Option do
Unwraps the option and returns the value.
Raises an `ExpectError` on failure with `message`.
## Examples
iex> some("Marty")
...> |> expect!("the Delorean has no driver!")
"Marty"
iex> none()
...> |> expect!("the Delorean has no driver!")
** (Upshot.ExpectError) the Delorean has no driver!
"""
@spec expect(t, String.t()) :: any | no_return
def expect(option, message) when is_binary(message) do
@spec expect!(t, String.t()) :: any | no_return
def expect!(option, message) when is_binary(message) do
assert_valid!(option)
case Proto.unwrap(option) do
{:ok, value} -> value
:error -> raise ExpectError, message
@ -165,13 +361,25 @@ defmodule Upshot.Option do
Unwraps the option presuming it's none.
Raises an `ExpectError` when the option is some with `message`.
## Examples
iex> some("Marty")
...> |> expect_none!("you should be in 1955!")
** (Upshot.ExpectError) you should be in 1955!
iex> none()
...> |> expect_none!("you should be in 1955!")
:error
"""
@spec expect_none(t, String.t()) :: t | no_return
def expect_none(option, message) when is_binary(message) do
@spec expect_none!(t, String.t()) :: t | no_return
def expect_none!(option, message) when is_binary(message) do
assert_valid!(option)
if Proto.some?(option) do
raise ExpectError, message
else
none()
option
end
end
@ -179,12 +387,24 @@ defmodule Upshot.Option do
Unwraps the option and returns the value.
Raises an `UnwrapError` on failure.
## Examples
iex> some("Marty")
...> |> unwrap!()
"Marty"
iex> none()
...> |> unwrap!()
** (Upshot.UnwrapError) Attempt to unwrap a none.
"""
@spec unwrap(t) :: any | no_return
def unwrap(option) do
@spec unwrap!(t) :: any | no_return
def unwrap!(option) do
assert_valid!(option)
case Proto.unwrap(option) do
{:ok, value} -> value
:error -> raise UnwrapError, "Attempt to unwrap a none"
:error -> raise UnwrapError, "Attempt to unwrap a none."
end
end
@ -192,33 +412,50 @@ defmodule Upshot.Option do
Unwraps an option and returns a none.
Raises an `UnwrapError` on failure.
## Examples
iex> some("Marty")
...> |> unwrap_none!()
** (Upshot.UnwrapError) Expected none, found `{:ok, "Marty"}`.
"""
@spec unwrap_none(t) :: t | no_return
def unwrap_none(option) do
@spec unwrap_none!(t) :: t | no_return
def unwrap_none!(option) do
assert_valid!(option)
if Proto.none?(option) do
option
else
raise UnwrapError, "Attempt to unwrap a some"
raise UnwrapError, "Expected none, found `#{inspect(option)}`."
end
end
@doc """
Unwraps an option and returns it's contained value or a default.
## Examples
iex> some("Marty")
...> |> unwrap_or_default("Doc")
"Marty"
iex> none()
...> |> unwrap_or_default("Doc")
"Doc"
"""
@spec unwrap_or_default(t, any) :: any
@spec unwrap_or_default(t, any) :: any | no_return
def unwrap_or_default(option, default) do
assert_valid!(option)
case Proto.unwrap(option) do
{:ok, value} -> value
:error -> default
end
end
# Safely execute a mapping function, catching an exceptions and converting
# them into a result.
@spec safe_callback((... -> any()), [any()]) :: any()
defp safe_callback(callback, args) when is_function(callback) when is_list(args) do
apply(callback, args)
rescue
error -> {:error, error}
defp assert_valid!(option) do
if Proto.valid?(option),
do: option,
else: raise(ArgumentError, "Expected option, but got #{inspect(option)}")
end
end

View file

@ -3,14 +3,59 @@ defprotocol Upshot.Option.Proto do
A protocol for interacting with optional types.
"""
@fallback_to_any true
@doc """
Used to check if the option contains a value.
## Example
iex> some?({:ok, "Marty"})
true
"""
@spec some?(t) :: boolean
def some?(_option)
@doc """
Used to check if the option is empty.
## Example
iex> none?(:error)
true
"""
@spec none?(t) :: boolean
def none?(_option)
@spec unwrap(t) :: {:ok, any} | :error
@doc """
Converts the option type into an ok tuple, or error atom.
This is used internally by the `Option` module for combinators, etc.
## Example
iex> OptionExample.some("Marty")
...> |> unwrap()
{:ok, "Marty"}
"""
@spec unwrap(t) :: {:ok, any} | :error | no_return
def unwrap(_option)
@doc """
Validates that the provided value is a valid option.
Used internally by the `Option` module to ensure that arguments and callback
results are valid options.
Return true if the option is valid, false otherwise.
## Examples
iex> valid?({:ok, "Marty"})
true
iex> valid?({:error, "Doc Brown"})
false
"""
@spec valid?(t) :: boolean
def valid?(_option)
end

View file

@ -1,26 +0,0 @@
defimpl Upshot.Option.Proto, for: Any do
@moduledoc """
Implements the option protocol for arbitrary terms.
"""
@doc """
Returns `true` for `{:ok, value}` tuples, otherwise `false`.
"""
@spec some?(any) :: boolean
def some?({:ok, _value}), do: true
def some?(_option), do: false
@doc """
Returns `false` for `{:ok, value}` tuples, otherwise `true`.
"""
@spec none?(any) :: boolean
def none?({:ok, _value}), do: false
def none?(_option), do: true
@doc """
Returns the internal value of `{:ok, value}` or `:error`.
"""
@spec unwrap(any) :: {:ok, any} | :error
def unwrap({:ok, value}), do: {:ok, value}
def unwrap(_option), do: :error
end

View file

@ -0,0 +1,55 @@
defimpl Upshot.Option.Proto, for: Atom do
@moduledoc """
Implements the option protocol for atoms.
"""
@doc """
Returns `false`.
"""
@spec some?(atom) :: false
def some?(_), do: false
@doc """
Returns `true` if the atom is `:error` or `nil`, otherwise `false`.
## Examples
iex> none?(:error)
true
iex> none?(nil)
true
iex> none?(:marty)
false
"""
@spec none?(atom) :: boolean
def none?(:error), do: true
def none?(nil), do: true
def none?(_), do: false
@doc """
Returns `:error`.
"""
@spec unwrap(atom) :: :error
def unwrap(_), do: :error
@doc """
Returns `true` for `:error` and `nil` atoms, otherwise `false`.
## Examples
iex> valid?(:error)
true
iex> valid?(nil)
true
iex> valid?(:marty)
false
"""
@spec valid?(atom) :: boolean
def valid?(:error), do: true
def valid?(nil), do: true
def valid?(_), do: false
end

View file

@ -0,0 +1,57 @@
defimpl Upshot.Option.Proto, for: Tuple do
@moduledoc """
Implements the option protocol for tuples allowing `{:ok, any}` to be a valid
option.
"""
@doc """
Returns `true` for `{:ok, any}`, otherwise `false`.
## Examples
iex> some?({:ok, "Marty"})
true
iex> some?({"Marty", "McFly"})
false
"""
@spec some?(tuple) :: boolean
def some?({:ok, _}), do: true
def some?(_), do: false
@doc """
Returns `false` for all tuples.
"""
@spec none?(tuple) :: false
def none?(_), do: false
@doc """
Passes through `{:ok, any}` tuples, otherwise `:error`.
## Examples
iex> unwrap({:ok, "Doc"})
{:ok, "Doc"}
iex> unwrap({"Marty", "McFly"})
:error
"""
@spec unwrap(tuple) :: {:ok, any} | :error
def unwrap({:ok, _} = value), do: value
def unwrap(_), do: :error
@doc """
Validates that the tuple is in the form `{:ok, any}`.
## Examples
iex> valid?({:ok, "Doc"})
true
iex> valid?({"Marty", "McFly"})
false
"""
@spec valid?(tuple) :: boolean
def valid?({:ok, _}), do: true
def valid?(_), do: false
end

View file

@ -6,7 +6,10 @@ defmodule Upshot.Result do
"""
@type t :: Proto.t()
@type map_fn :: (any -> {:ok, any} | {:error, any})
@type map_fn :: (any -> t)
@callback ok(any) :: t
@callback error(any) :: t
@doc """
Create a new result with an ok value.
@ -21,10 +24,22 @@ defmodule Upshot.Result do
def error(reason), do: {:error, reason}
@doc """
Convert a result into an option.
Convert a result into an option by discarding any error values.
## Example
iex> ok("Marty")
...> |> to_option()
{:ok, "Marty"}
iex> error("Doc Brown")
...> |> to_option()
:error
"""
@spec to_option(t) :: Option.t()
@spec to_option(t) :: Option.t() | no_return
def to_option(result) do
assert_valid!(result)
case Proto.unwrap(result) do
{:ok, value} -> {:ok, value}
_error -> :error
@ -32,78 +47,204 @@ defmodule Upshot.Result do
end
@doc """
Is the result okay?
Does the result contain an ok?
## Examples
iex> ok("Marty") |> ok?()
true
iex> error("Doc") |> ok?()
false
"""
@spec ok?(t) :: boolean
defdelegate ok?(result), to: Proto
@spec ok?(t) :: boolean | no_return
def ok?(result) do
assert_valid!(result)
Proto.ok?(result)
end
@doc """
Is the result an error?
## Examples
iex> ok("Marty") |> error?()
false
iex> error("Doc") |> error?()
true
"""
@spec error?(t) :: boolean
defdelegate error?(result), to: Proto
@spec error?(t) :: boolean | no_return
def error?(result) do
assert_valid!(result)
Proto.error?(result)
end
@doc """
Returns true if the result is ok and it's value equals the test value.
## Examples
iex> ok("Marty")
...> |> contains?("Marty")
true
iex> ok("Doc")
...> |> contains?("Marty")
false
iex> error("Marty")
...> |> contains?("Marty")
false
"""
@spec contains?(t, any) :: boolean
def contains?(result, test_value), do: Proto.unwrap(result) == {:ok, test_value}
def contains?(result, test_value) do
assert_valid!(result)
Proto.unwrap(result) == {:ok, test_value}
end
@doc """
Returns true if the result is an error and it's value equals the test value.
## Examples
iex> error("Marty")
...> |> contains_error?("Marty")
true
iex> ok("Doc")
...> |> contains_error?("Marty")
false
iex> ok("Marty")
...> |> contains_error?("Marty")
false
"""
@spec contains_err?(t, any) :: boolean
def contains_err?(result, test_value), do: Proto.unwrap(result) == {:error, test_value}
@spec contains_error?(t, any) :: boolean
def contains_error?(result, test_value) do
assert_valid!(result)
Proto.unwrap(result) == {:error, test_value}
end
@doc """
Maps an ok result into a new result using the provided function, leaving an
error result untouched.
## Examples
iex> ok(2)
...> |> map(&({:ok, &1 * 2}))
{:ok, 4}
iex> error(2)
...> |> map(&({:ok, &1 * 2}))
{:error, 2}
"""
@spec map(t, map_fn) :: t
def map(result, ok_callback) when is_function(ok_callback, 1) do
with {:ok, value} <- Proto.unwrap(result),
{:ok, value} <- safe_callback(value, ok_callback) do
{:ok, value}
else
_error -> result
assert_valid!(result)
case Proto.unwrap(result) do
{:ok, value} ->
ok_callback
|> apply([value])
|> assert_valid!()
{:error, _} ->
result
end
end
@doc """
Maps an ok result into a new result using the provided function, replacing an
error value with the default.
## Examples
iex> ok(2)
...> |> map_or(&({:ok, &1 * 2}), 8)
{:ok, 4}
iex> error(2)
...> |> map_or(&({:ok, &1 * 2}), 8)
{:ok, 8}
"""
@spec map_or(t, map_fn, any) :: t
def map_or(result, ok_callback, default) when is_function(ok_callback, 1) do
with {:ok, value} <- Proto.unwrap(result),
{:ok, value} <- safe_callback(value, ok_callback) do
{:ok, value}
else
_error -> {:ok, default}
assert_valid!(result)
case Proto.unwrap(result) do
{:ok, value} ->
ok_callback
|> apply([value])
|> assert_valid!()
{:error, _} ->
{:ok, default}
end
end
@doc """
Maps a result to another result by applying `ok_callback` or `err_callback`
depending on whether the result is successful or not.
## Examples
iex> ok(4)
...> |> map_or_else(&({:ok, &1 * 2}), &({:ok, &1 * 3}))
{:ok, 8}
iex> error(4)
...> |> map_or_else(&({:ok, &1 * 2}), &({:ok, &1 * 3}))
{:ok, 12}
"""
@spec map_or_else(t, map_fn, map_fn) :: t
def map_or_else(result, ok_callback, err_callback) do
assert_valid!(result)
case Proto.unwrap(result) do
{:ok, value} -> safe_callback(value, ok_callback)
{:error, value} -> safe_callback(value, err_callback)
{:ok, value} ->
ok_callback
|> apply([value])
|> assert_valid!()
{:error, value} ->
err_callback
|> apply([value])
|> assert_valid!()
end
end
@doc """
Maps an error result into another result by applying the function to an error
value and leaving an ok value untouched.
## Examples
iex> ok(2)
...> |> map_err(&({:ok, &1 * 2}))
{:ok, 2}
iex> error(3)
...> |> map_err(&({:ok, &1 * 2}))
{:ok, 6}
"""
@spec map_err(t, map_fn) :: t
def map_err(result, err_callback) do
assert_valid!(result)
case Proto.unwrap(result) do
{:error, value} -> safe_callback(value, err_callback)
_other -> result
{:error, value} ->
err_callback
|> apply([value])
|> assert_valid!()
{:ok, _} ->
result
end
end
@ -111,23 +252,57 @@ defmodule Upshot.Result do
A logical and.
If `lhs` is ok, returns `rhs`, otherwise returns `lhs`.
## Examples
iex> and_(ok(1), ok(2))
{:ok, 2}
iex> and_(error(0), ok(2))
{:error, 0}
"""
@spec and_(t, t) :: t
def and_(lhs, rhs) do
assert_valid!(lhs)
assert_valid!(rhs)
case Proto.unwrap(lhs) do
{:ok, _value} -> rhs
_other -> lhs
{:ok, _} -> rhs
{:error, _} -> lhs
end
end
@doc """
Calls `ok_callback` if the result is ok, otherwise returns the error value.
## Examples
iex> square = &({:ok, &1 * &1})
...> ok(2)
...> |> and_then(square)
...> |> and_then(square)
...> |> and_then(square)
{:ok, 256}
iex> square = &({:ok, &1 * &1})
...> error(2)
...> |> and_then(square)
...> |> and_then(square)
...> |> and_then(square)
{:error, 2}
"""
@spec and_then(t, map_fn) :: t
def and_then(result, ok_callback) when is_function(ok_callback, 1) do
assert_valid!(result)
case Proto.unwrap(result) do
{:ok, value} -> safe_callback(value, ok_callback)
_other -> result
{:ok, value} ->
ok_callback
|> apply([value])
|> assert_valid!()
{:error, _} ->
result
end
end
@ -135,12 +310,23 @@ defmodule Upshot.Result do
A logical or.
If `lhs` is an error, returns `rhs`, otherwise returns `lhs`.
## Examples
iex> or_(ok(1), ok(2))
{:ok, 1}
iex> or_(error(0), ok(2))
{:ok, 2}
"""
@spec or_(t, t) :: t
def or_(lhs, rhs) do
assert_valid!(lhs)
assert_valid!(rhs)
case Proto.unwrap(lhs) do
{:error, _value} -> rhs
_ -> lhs
{:ok, _} -> lhs
end
end
@ -148,9 +334,21 @@ defmodule Upshot.Result do
Unwraps the result and returns the value.
Raises an `ExpectError` on failure with `message`.
## Examples
iex> ok("Marty")
...> |> expect!("the Delorean has no driver!")
"Marty"
iex> error("Einstein")
...> |> expect!("the Delorean has no driver!")
** (Upshot.ExpectError) the Delorean has no driver!
"""
@spec expect(t, String.t()) :: any | no_return
def expect(result, message) when is_binary(message) do
@spec expect!(t, String.t()) :: any | no_return
def expect!(result, message) when is_binary(message) do
assert_valid!(result)
case Proto.unwrap(result) do
{:ok, value} ->
value
@ -164,47 +362,79 @@ defmodule Upshot.Result do
Unwraps the result and returns the value.
Raises an `UnwrapError` on failure.
## Examples
iex> ok("Marty")
...> |> unwrap!()
"Marty"
iex> error("Doc")
...> |> unwrap!()
** (Upshot.UnwrapError) Attempt to unwrap an error result.
"""
@spec unwrap(t) :: any | no_return
def unwrap(result) do
@spec unwrap!(t) :: any | no_return
def unwrap!(result) do
assert_valid!(result)
case Proto.unwrap(result) do
{:ok, value} -> value
{:error, _} -> raise UnwrapError, "Attempt to unwrap error result"
{:error, _} -> raise UnwrapError, "Attempt to unwrap an error result."
end
end
@doc """
Returns the contained ok value, or a provided default.
## Examples
iex> ok("Marty")
...> |> unwrap_or("Doc")
"Marty"
iex> error("Einsteim")
...> |> unwrap_or("Doc")
"Doc"
"""
@spec unwrap_or(t, any) :: any
@spec unwrap_or(t, any) :: any | no_return
def unwrap_or(result, default) do
case Proto.unwrap(result) do
{:ok, value} -> value
_other -> default
{:error, _} -> default
end
end
@doc """
Returns the contained ok value, or computes it with the provided function.
## Examples
iex> ok("Marty")
...> |> unwrap_or_else(fn _ -> {:ok, "Doc"} end)
"Marty"
iex> error("Einstein")
...> |> unwrap_or_else(fn _ -> {:ok, "Doc"} end)
"Doc"
"""
@spec unwrap_or_else(t, map_fn) :: any
@spec unwrap_or_else(t, map_fn) :: any | no_return
def unwrap_or_else(result, err_callback) when is_function(err_callback, 1) do
assert_valid!(result)
case Proto.unwrap(result) do
{:ok, value} ->
value
{:error, value} ->
value
|> safe_callback(err_callback)
|> unwrap()
err_callback
|> apply([value])
|> unwrap!()
end
end
# Calls a callback, but rescues exceptions and converts them results.
@spec safe_callback(any, map_fn) :: t
defp safe_callback(value, callback) do
apply(callback, [value])
rescue
exception -> {:error, exception}
defp assert_valid!(result) do
if Proto.valid?(result),
do: result,
else: raise(ArgumentError, "Expected result, but got #{inspect(result)}")
end
end

View file

@ -1,18 +1,68 @@
defprotocol Upshot.Result.Proto do
@moduledoc """
The result protocol.
Implement this for your own result types, as needed.
A protocol for interactint with optional types.
"""
@fallback_to_any true
@doc """
Used to check if the result contains a value.
## Examples
iex> ok?({:ok, "Marty"})
true
iex> ok?({:error, "Doc"})
false
"""
@spec ok?(t) :: boolean
def ok?(_result)
@doc """
Used to check if the result contains an error.
## Examples
iex> error?({:ok, "Marty"})
false
iex> error?({:error, "Doc"})
true
"""
@spec error?(t) :: boolean
def error?(_result)
@spec unwrap(t) :: {:ok, any} | {:error, any}
@doc """
Converts the result into an ok or error tuple.
This is used internally by the `Result` module for combinators, etc.
## Example
iex> ResultExample.ok("Marty")
...> |> unwrap()
{:ok, "Marty"}
iex> ResultExample.error("Doc")
...> |> unwrap()
{:error, "Doc"}
"""
@spec unwrap(t) :: {:ok, any} | {:error, any} | :invalid
def unwrap(_result)
@doc """
Validates that the provided value is a valid result.
Used internally by the `Result` module to ensure that arguments and callback
results are valid result.
## Examples
iex> valid?({:ok, "Marty"})
true
iex valid?({"Marty", "Doc", "Einstein"})
false
"""
@spec valid?(t) :: boolean
def valid?(_result)
end

View file

@ -1,30 +0,0 @@
defimpl Upshot.Result.Proto, for: Any do
@moduledoc """
Implements the result protocol for arbitrary terms.
"""
@doc """
Returns `true` for `{:ok, value}`, otherwise `false`.
"""
@spec ok?(any) :: boolean
def ok?({:ok, _value}), do: true
def ok?(_result), do: false
@doc """
Returns `true` for `{:error, reason}` and `:error`.
"""
@spec error?(any) :: boolean
def error?({:error, _reason}), do: true
def error?(%{:__exception__ => true}), do: true
def error?(_result_), do: false
@doc """
Unwrap a result, if possible.
Returns the value of an `{:ok, value}` tuple, otherwise `{:error, reason}`.
"""
@spec unwrap(any) :: {:ok, any} | {:error, any}
def unwrap({:ok, value}), do: {:ok, value}
def unwrap({:error, value}), do: {:error, value}
def unwrap(%{:__exception__ => true} = error), do: {:error, error}
end

View file

@ -0,0 +1,74 @@
defimpl Upshot.Result.Proto, for: Tuple do
@moduledoc """
Implements the option protocol for tuples, allowing `{:ok, any}` and `{:error,
any}` to be valid results.
"""
@doc """
Returns `true` for `{:ok, any}` tuples, `false` otherwise.
## Examples
iex> ok?({:ok, "Marty"})
true
iex> ok?({:error, "Doc"})
false
"""
@spec ok?(tuple) :: boolean
def ok?({:ok, _}), do: true
def ok?(_), do: false
@doc """
Returns `true` for `{:error, any}` tuples, `false` otherwise.
## Examples
iex> error?({:ok, "Marty"})
false
iex> error?({:error, "Doc"})
true
"""
@spec error?(tuple) :: boolean
def error?({:error, _}), do: true
def error?(_), do: false
@doc """
Passes ok and error tuples, converts other values to error tuples.
### Examples
iex> unwrap({:ok, "Marty"})
{:ok, "Marty"}
iex> unwrap({:error, "Doc"})
{:error, "Doc"}
iex> unwrap({:einstein})
{:error, :invalid_result}
"""
@spec unwrap(tuple) :: {:ok, any} | {:error, any}
def unwrap({:ok, _} = result), do: result
def unwrap({:error, _} = result), do: result
def unwrap(_), do: {:error, :invalid_result}
@doc """
Returns `true` for ok/error tuples, otherwise `false`.
## Examples
iex> valid?({:ok, "Marty"})
true
iex> valid?({:error, "Doc"})
true
iex> valid?({})
false
"""
@spec valid?(tuple) :: boolean
def valid?({:ok, _}), do: true
def valid?({:error, _}), do: true
def valid?(_), do: false
end

View file

@ -1,6 +1,13 @@
defmodule OptionExample do
defstruct optional_value: nil
@moduledoc false
@behaviour Upshot.Option
@impl true
def some(value), do: %OptionExample{optional_value: value}
@impl true
def none, do: %OptionExample{optional_value: nil}
defimpl Upshot.Option.Proto do
def some?(%OptionExample{optional_value: nil}), do: false
@ -9,5 +16,7 @@ defmodule OptionExample do
def none?(%OptionExample{}), do: false
def unwrap(%OptionExample{optional_value: nil}), do: :error
def unwrap(%OptionExample{optional_value: value}), do: {:ok, value}
def valid?(%OptionExample{}), do: true
end
end

View file

@ -1,12 +1,23 @@
defmodule ResultExample do
defstruct state: nil, value: nil
@behaviour Upshot.Result
@moduledoc false
@impl true
def ok(value), do: %ResultExample{state: :ok, value: value}
@impl true
def error(value), do: %ResultExample{state: :error, value: value}
defimpl Upshot.Result.Proto do
def ok?(%ResultExample{state: :ok}), do: true
def ok?(%ResultExample{}), do: false
def error?(%ResultExample{state: :error}), do: true
def error?(%ResultExample{}), do: false
def unwrap(%ResultExample{state: state, value: value}), do: {state, value}
def valid?(%ResultExample{state: :ok}), do: true
def valid?(%ResultExample{state: :error}), do: true
def valid?(_), do: false
end
end

View file

@ -1,2 +1,3 @@
Mimic.copy(Upshot.Option.Proto)
Mimic.copy(Upshot.Result.Proto)
ExUnit.start()

View file

@ -1,35 +0,0 @@
defmodule Upshot.Option.Proto.AnyTest do
use ExUnit.Case, async: true
alias Upshot.Option.Proto
@moduledoc false
describe "some?/1" do
test "it is true for ok tuples" do
assert Proto.some?({:ok, "Marty"})
end
test "it is false for anything else" do
refute Proto.some?("Doc Brown")
end
end
describe "none?/1" do
test "it is false for ok tuples" do
refute Proto.none?({:ok, "Marty"})
end
test "it is true for anything else" do
assert Proto.none?("Doc Brown")
end
end
describe "unwrap/1" do
test "it passes through an ok tuple" do
assert {:ok, "Marty"} = Proto.unwrap({:ok, "Marty"})
end
test "it returns error for anything else" do
assert :error = Proto.unwrap("Doc Brown")
end
end
end

View file

@ -0,0 +1,6 @@
defmodule Upshot.Option.Proto.AtomTest do
use ExUnit.Case, async: true
import Upshot.Option.Proto
doctest Upshot.Option.Proto.Atom
@moduledoc false
end

View file

@ -1,34 +1,36 @@
defmodule Upshot.Option.ProtoTest do
use ExUnit.Case, async: true
alias Upshot.Option.Proto
import Upshot.Option.Proto
doctest Upshot.Option.Proto
@moduledoc false
describe "some?/1" do
test "when the option has a value, it returns true" do
assert Proto.some?(%OptionExample{optional_value: "Marty"})
assert some?(%OptionExample{optional_value: "Marty"})
end
test "when the option has no value, it returns false" do
refute Proto.some?(%OptionExample{optional_value: nil})
refute some?(%OptionExample{optional_value: nil})
end
end
describe "none?/1" do
test "when the option has a value, it returns false" do
refute Proto.none?(%OptionExample{optional_value: "Marty"})
refute none?(%OptionExample{optional_value: "Marty"})
end
test "when the option has no value, it returns true" do
assert Proto.none?(%OptionExample{optional_value: nil})
assert none?(%OptionExample{optional_value: nil})
end
end
describe "unwrap/1" do
test "when the option has a value, it returns an ok tuple" do
assert {:ok, "Marty"} = Proto.unwrap(%OptionExample{optional_value: "Marty"})
assert {:ok, "Marty"} = unwrap(%OptionExample{optional_value: "Marty"})
end
test "when the option has no value, it returns an error" do
assert :error = Proto.unwrap(%OptionExample{optional_value: nil})
assert :error = unwrap(%OptionExample{optional_value: nil})
end
end
end

View file

@ -0,0 +1,6 @@
defmodule Upshot.Option.Proto.TupleTest do
use ExUnit.Case, async: true
import Upshot.Option.Proto
doctest Upshot.Option.Proto.Tuple
@moduledoc false
end

View file

@ -1,142 +1,144 @@
defmodule Upshot.OptionTest do
use ExUnit.Case, async: true
use Mimic
alias Upshot.{Option, Option.Proto}
alias Upshot.Option.Proto
import Upshot.Option
doctest Upshot.Option
@moduledoc false
describe "some/1" do
test "it returns an ok tuple" do
assert {:ok, "Marty"} = Option.some("Marty")
assert {:ok, "Marty"} = some("Marty")
end
end
describe "none/1" do
test "it returns an error atom" do
assert :error = Option.none()
assert :error = none()
end
end
describe "some?/1" do
test "it delegates to Proto" do
Proto
|> expect(:some?, &assert(&1 == "Marty"))
|> expect(:some?, &assert(&1 == {:ok, "Marty"}))
Option.some?("Marty")
some?({:ok, "Marty"})
end
end
describe "none?/1" do
test "it delegates to Proto" do
Proto
|> expect(:none?, &assert(&1 == "Doc Brown"))
|> expect(:none?, &assert(&1 == :error))
Option.none?("Doc Brown")
none?(:error)
end
end
describe "ok_or/2" do
test "when the option contains a value it returns it" do
assert {:ok, "Marty"} = Option.ok_or({:ok, "Marty"}, "Doc Brown")
assert {:ok, "Marty"} = ok_or({:ok, "Marty"}, "Doc Brown")
end
test "when the option does not contain a value it returns the error value" do
assert {:error, "Doc Brown"} = Option.ok_or(Option.none(), "Doc Brown")
assert {:error, "Doc Brown"} = ok_or(none(), "Doc Brown")
end
end
describe "ok_or_else/2" do
test "when the option contains a value it returns it" do
assert {:ok, "Marty"} = Option.ok_or({:ok, "Marty"}, "Doc Brown")
assert {:ok, "Marty"} = ok_or({:ok, "Marty"}, "Doc Brown")
end
test "when the option does not contain a value it converts the result of the callback into an error" do
assert {:error, "Doc Brown"} = Option.ok_or_else(Option.none(), fn -> "Doc Brown" end)
assert {:error, "Doc Brown"} = ok_or_else(none(), fn -> "Doc Brown" end)
end
end
describe "and_/2" do
test "when the lhs option is none, it returns it" do
assert Option.none() == Option.and_(Option.none(), Option.some("Marty"))
assert none() == and_(none(), some("Marty"))
end
test "when the lhs option is some, it returns the rhs option" do
assert {:ok, "Doc Brown"} = Option.and_({:ok, "Marty"}, {:ok, "Doc Brown"})
assert {:ok, "Doc Brown"} = and_({:ok, "Marty"}, {:ok, "Doc Brown"})
end
end
describe "and_then/2" do
test "when the option is none, it returns it" do
assert Option.none() == Option.and_then(Option.none(), &Option.some(&1 * 2))
assert none() == and_then(none(), &some(&1 * 2))
end
test "when the option is some, it maps the value into a new option" do
assert Option.some(4) == Option.and_then(Option.some(2), &Option.some(&1 * 2))
assert some(4) == and_then(some(2), &some(&1 * 2))
end
end
describe "filter/2" do
test "when the option is none, it returns none" do
assert Option.none() == Option.filter(Option.none(), fn _ -> false end)
assert none() == filter(none(), fn _ -> false end)
end
test "when the option is some and the predicate is false, it returns none" do
assert Option.none() == Option.filter(Option.some("Marty"), fn _ -> false end)
assert none() == filter(some("Marty"), fn _ -> false end)
end
test "when the option is some and the predicate is true, it returns it" do
assert Option.some("Marty") == Option.filter(Option.some("Marty"), fn _ -> true end)
assert some("Marty") == filter(some("Marty"), fn _ -> true end)
end
end
describe "or_/2" do
test "when the lhs option is some, it returns it" do
assert {:ok, "Marty"} = Option.or_(Option.some("Marty"), Option.some("Doc"))
assert {:ok, "Marty"} = or_(some("Marty"), some("Doc"))
end
test "when the lhs option is none, it returns the rhs option" do
assert {:ok, "Doc"} = Option.or_(Option.none(), Option.some("Doc"))
assert {:ok, "Doc"} = or_(none(), some("Doc"))
end
end
describe "or_else/2" do
test "when the option is some, it returns it" do
assert {:ok, "Marty"} = Option.or_else(Option.some("Marty"), fn -> Option.some("Doc") end)
assert {:ok, "Marty"} = or_else(some("Marty"), fn -> some("Doc") end)
end
test "when the option is none, calls the callback and returns it's option" do
assert {:ok, "Doc"} = Option.or_else(Option.none(), fn -> Option.some("Doc") end)
assert {:ok, "Doc"} = or_else(none(), fn -> some("Doc") end)
end
end
describe "xor/2" do
test "when the lhs option is some and the rhs option is none, it returns the lhs option" do
assert {:ok, "Marty"} = Option.xor(Option.some("Marty"), Option.none())
assert {:ok, "Marty"} = xor(some("Marty"), none())
end
test "when the lhs option is none and the rhs is some, it returns the rhs option" do
assert {:ok, "Doc"} = Option.xor(Option.none(), Option.some("Doc"))
assert {:ok, "Doc"} = xor(none(), some("Doc"))
end
test "when both options are some, it returns none" do
assert Option.none() == Option.xor(Option.some("Marty"), Option.some("Doc"))
assert none() == xor(some("Marty"), some("Doc"))
end
test "when both options are none, it returns none" do
assert Option.none() == Option.xor(Option.none(), Option.none())
assert none() == xor(none(), none())
end
end
describe "zip/2" do
test "when both options are some, it zips them together" do
assert Option.some({"Marty", "Doc"}) == Option.zip(Option.some("Marty"), Option.some("Doc"))
assert some({"Marty", "Doc"}) == zip(some("Marty"), some("Doc"))
end
test "when the lhs option is none, it returns none" do
assert Option.none() == Option.zip(Option.none(), Option.some("Doc"))
assert none() == zip(none(), some("Doc"))
end
test "when the rhs option is none, it returns none" do
assert Option.none() == Option.zip(Option.some("Marty"), Option.none())
assert none() == zip(some("Marty"), none())
end
end
end

View file

@ -1,43 +0,0 @@
defmodule Upshot.Result.Proto.AnyTest do
use ExUnit.Case, async: true
alias Upshot.{Result.Proto, UnwrapError}
describe "ok?/1" do
test "when the result is an ok tuple, it returns true" do
assert Proto.ok?({:ok, "Marty"})
end
test "when the result is anything else, it returns false" do
refute Proto.ok?("Doc Brown")
end
end
describe "error?/1" do
test "when the result is an error tuple, it returns true" do
assert Proto.error?({:error, "Marty"})
end
test "when the result is an exception, it returns true" do
assert Proto.error?(UnwrapError.exception("Marty"))
end
test "when the result is anything else, it returns false" do
refute Proto.error?("Doc Brown")
end
end
describe "unwrap/1" do
test "it passes through an ok tuple untouched" do
assert {:ok, "Marty"} = Proto.unwrap({:ok, "Marty"})
end
test "it passes through an error tuple untouched" do
assert {:error, "Marty"} = Proto.unwrap({:error, "Marty"})
end
test "it converts an exception into an error tuple" do
exception = UnwrapError.exception("Marty")
assert {:error, ^exception} = Proto.unwrap(exception)
end
end
end

View file

@ -1,35 +1,50 @@
defmodule Upshot.Result.ProtoTest do
use ExUnit.Case, async: true
alias Upshot.Result.Proto
import Upshot.Result.Proto
doctest Upshot.Result.Proto
@moduledoc false
describe "ok?/1" do
test "when the result is ok, it returns true" do
assert Proto.ok?(%ResultExample{state: :ok})
assert ok?(%ResultExample{state: :ok})
end
test "when the result is not ok, it returns false" do
refute Proto.ok?(%ResultExample{state: :error})
refute ok?(%ResultExample{state: :error})
end
end
describe "error?/1" do
test "when the result is error, it returns true" do
assert Proto.error?(%ResultExample{state: :error})
assert error?(%ResultExample{state: :error})
end
test "when the result is not error, it returns false" do
refute Proto.error?(%ResultExample{state: :ok})
refute error?(%ResultExample{state: :ok})
end
end
describe "unwrap/1" do
test "when the result is ok, it returns an ok tuple" do
assert {:ok, "Marty"} = Proto.unwrap(%ResultExample{state: :ok, value: "Marty"})
assert {:ok, "Marty"} = unwrap(ResultExample.ok("Marty"))
end
test "when the result is error, it returns an error tuple" do
assert {:error, "Marty"} = Proto.unwrap(%ResultExample{state: :error, value: "Marty"})
assert {:error, "Doc"} = unwrap(ResultExample.error("Doc"))
end
end
describe "valid?/1" do
test "when the result is ok, it returns true" do
assert valid?(ResultExample.ok("Marty"))
end
test "when the result is error, it returns true" do
assert valid?(ResultExample.error("Doc"))
end
test "when the result is invalid, it returns false" do
refute valid?(%ResultExample{})
end
end
end

View file

@ -0,0 +1,5 @@
defmodule Upshot.Result.Proto.TupleTest do
use ExUnit.Case, async: true
import Upshot.Result.Proto
doctest Upshot.Result.Proto.Tuple
end

View file

@ -0,0 +1,26 @@
defmodule Upshot.ResultTest do
use ExUnit.Case, async: true
use Mimic
alias Upshot.Result.Proto
import Upshot.Result
doctest Upshot.Result
@moduledoc false
describe "ok?/1" do
test "it delegates to Proto" do
Proto
|> expect(:ok?, &assert(&1 == {:ok, "Marty"}))
ok?({:ok, "Marty"})
end
end
describe "error?/1" do
test "it delegates to Proto" do
Proto
|> expect(:error?, &assert(&1 == {:ok, "Marty"}))
error?({:ok, "Marty"})
end
end
end