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} alias Upshot.{ExpectError, Option.Proto, Result, UnwrapError}
@moduledoc """ @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 t :: Proto.t()
@type map_fn :: (any -> t) @type map_fn :: (any -> t)
@type predicate_fn :: (any -> boolean) @type predicate_fn :: (any -> boolean)
@callback some(any) :: t
@callback none() :: t
@doc """ @doc """
Create a new option from value. Create a new option from value.
## Example
iex> some("Marty")
{:ok, "Marty"}
""" """
@spec some(any) :: t @spec some(any) :: t
def some(value), do: {:ok, value} def some(value), do: {:ok, value}
@doc """ @doc """
Create a new option. Create a new option.
## Example
iex> none()
:error
""" """
@spec none :: t @spec none :: t
def none, do: :error def none, do: :error
@doc """ @doc """
Does the option contain a value? Does the option contain a value?
## Examples
iex> some("Marty")
...> |> some?()
true
iex> none()
...> |> some?()
false
""" """
@spec some?(t) :: boolean @spec some?(t) :: boolean | no_return
defdelegate some?(option), to: Proto def some?(option) do
assert_valid!(option)
Proto.some?(option)
end
@doc """ @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 @spec none?(t) :: boolean | no_return
defdelegate none?(option), to: Proto def none?(option) do
assert_valid!(option)
Proto.none?(option)
end
@doc """ @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 If the option is some then it will contain the some, otherwise it will use the
provided error value. 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() @spec ok_or(t, any) :: Result.t() | no_return
def ok_or(option, error) do def ok_or(option, error_value) do
if Proto.some?(option), assert_valid!(option)
do: option,
else: {:error, error} case Proto.unwrap(option) do
{:ok, value} -> {:ok, value}
:error -> {:error, error_value}
end
end end
@doc """ @doc """
Convert an option into a result mapping a some value into an ok value, and Convert an option into a result mapping a some value into an ok value, and
none into an error using the provided function. 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 def ok_or_else(option, err_callback) when is_function(err_callback, 0) do
if Proto.some?(option), assert_valid!(option)
do: option,
else: {:error, safe_callback(err_callback, [])} if Proto.some?(option) do
option
else
{:error, apply(err_callback, [])}
end
end end
@doc """ @doc """
A logical and. 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 def and_(lhs, rhs) do
assert_valid!(lhs)
assert_valid!(rhs)
if Proto.none?(lhs), if Proto.none?(lhs),
do: lhs, do: lhs,
else: rhs else: rhs
@ -72,20 +159,58 @@ defmodule Upshot.Option do
@doc """ @doc """
Returns none, if the option is none, otherwise ok_callback with the wrapped Returns none, if the option is none, otherwise ok_callback with the wrapped
value and returns the result. 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 def and_then(option, ok_callback) when is_function(ok_callback, 1) do
if Proto.none?(option), assert_valid!(option)
do: option,
else: safe_callback(ok_callback, [unwrap(option)]) case Proto.unwrap(option) do
:error ->
option
{:ok, value} ->
ok_callback
|> apply([value])
|> assert_valid!()
end
end end
@doc """ @doc """
Returns none if the option is none, otherwise calls predicate with the wrapped 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. 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 def filter(option, predicate) do
assert_valid!(option)
case Proto.unwrap(option) do case Proto.unwrap(option) do
{:ok, value} -> {:ok, value} ->
if apply(predicate, [value]), if apply(predicate, [value]),
@ -100,9 +225,23 @@ defmodule Upshot.Option do
@doc """ @doc """
Returns the option if it contains a value, otherwise returns the alternative Returns the option if it contains a value, otherwise returns the alternative
option. 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 def or_(lhs, rhs) do
assert_valid!(lhs)
assert_valid!(rhs)
if Proto.some?(lhs), if Proto.some?(lhs),
do: lhs, do: lhs,
else: rhs else: rhs
@ -111,21 +250,52 @@ defmodule Upshot.Option do
@doc """ @doc """
Returns the option if it contains a value, otherwise calls err_callback and Returns the option if it contains a value, otherwise calls err_callback and
returns the resulting option. 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 def or_else(option, err_callback) when is_function(err_callback, 0) do
assert_valid!(option)
if Proto.some?(option) do if Proto.some?(option) do
option option
else else
apply(err_callback, []) err_callback
|> apply([])
|> assert_valid!()
end end
end end
@doc """ @doc """
Returns some if exactly one of the options are some, otherwise none. 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 def xor(lhs, rhs) do
assert_valid!(lhs)
assert_valid!(rhs)
cond do cond do
Proto.some?(lhs) && Proto.none?(rhs) -> lhs Proto.some?(lhs) && Proto.none?(rhs) -> lhs
Proto.none?(lhs) && Proto.some?(rhs) -> rhs Proto.none?(lhs) && Proto.some?(rhs) -> rhs
@ -137,9 +307,23 @@ defmodule Upshot.Option do
Zips the two options together. Zips the two options together.
If both options are some, returns a some containing a tuple of both values, otherwise none. 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 def zip(lhs, rhs) do
assert_valid!(lhs)
assert_valid!(rhs)
with {:ok, lhs} <- Proto.unwrap(lhs), with {:ok, lhs} <- Proto.unwrap(lhs),
{:ok, rhs} <- Proto.unwrap(rhs) do {:ok, rhs} <- Proto.unwrap(rhs) do
{:ok, {lhs, rhs}} {:ok, {lhs, rhs}}
@ -152,9 +336,21 @@ defmodule Upshot.Option do
Unwraps the option and returns the value. Unwraps the option and returns the value.
Raises an `ExpectError` on failure with `message`. 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 @spec expect!(t, String.t()) :: any | no_return
def expect(option, message) when is_binary(message) do def expect!(option, message) when is_binary(message) do
assert_valid!(option)
case Proto.unwrap(option) do case Proto.unwrap(option) do
{:ok, value} -> value {:ok, value} -> value
:error -> raise ExpectError, message :error -> raise ExpectError, message
@ -165,13 +361,25 @@ defmodule Upshot.Option do
Unwraps the option presuming it's none. Unwraps the option presuming it's none.
Raises an `ExpectError` when the option is some with `message`. 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 @spec expect_none!(t, String.t()) :: t | no_return
def expect_none(option, message) when is_binary(message) do def expect_none!(option, message) when is_binary(message) do
assert_valid!(option)
if Proto.some?(option) do if Proto.some?(option) do
raise ExpectError, message raise ExpectError, message
else else
none() option
end end
end end
@ -179,12 +387,24 @@ defmodule Upshot.Option do
Unwraps the option and returns the value. Unwraps the option and returns the value.
Raises an `UnwrapError` on failure. 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 @spec unwrap!(t) :: any | no_return
def unwrap(option) do def unwrap!(option) do
assert_valid!(option)
case Proto.unwrap(option) do case Proto.unwrap(option) do
{:ok, value} -> value {:ok, value} -> value
:error -> raise UnwrapError, "Attempt to unwrap a none" :error -> raise UnwrapError, "Attempt to unwrap a none."
end end
end end
@ -192,33 +412,50 @@ defmodule Upshot.Option do
Unwraps an option and returns a none. Unwraps an option and returns a none.
Raises an `UnwrapError` on failure. 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 @spec unwrap_none!(t) :: t | no_return
def unwrap_none(option) do def unwrap_none!(option) do
assert_valid!(option)
if Proto.none?(option) do if Proto.none?(option) do
option option
else else
raise UnwrapError, "Attempt to unwrap a some" raise UnwrapError, "Expected none, found `#{inspect(option)}`."
end end
end end
@doc """ @doc """
Unwraps an option and returns it's contained value or a default. 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 def unwrap_or_default(option, default) do
assert_valid!(option)
case Proto.unwrap(option) do case Proto.unwrap(option) do
{:ok, value} -> value {:ok, value} -> value
:error -> default :error -> default
end end
end end
# Safely execute a mapping function, catching an exceptions and converting defp assert_valid!(option) do
# them into a result. if Proto.valid?(option),
@spec safe_callback((... -> any()), [any()]) :: any() do: option,
defp safe_callback(callback, args) when is_function(callback) when is_list(args) do else: raise(ArgumentError, "Expected option, but got #{inspect(option)}")
apply(callback, args)
rescue
error -> {:error, error}
end end
end end

View file

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

View file

@ -1,18 +1,68 @@
defprotocol Upshot.Result.Proto do defprotocol Upshot.Result.Proto do
@moduledoc """ @moduledoc """
The result protocol. A protocol for interactint with optional types.
Implement this for your own result types, as needed.
""" """
@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 @spec ok?(t) :: boolean
def ok?(_result) 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 @spec error?(t) :: boolean
def error?(_result) 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) 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 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 defmodule OptionExample do
defstruct optional_value: nil defstruct optional_value: nil
@moduledoc false @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 defimpl Upshot.Option.Proto do
def some?(%OptionExample{optional_value: nil}), do: false def some?(%OptionExample{optional_value: nil}), do: false
@ -9,5 +16,7 @@ defmodule OptionExample do
def none?(%OptionExample{}), do: false def none?(%OptionExample{}), do: false
def unwrap(%OptionExample{optional_value: nil}), do: :error def unwrap(%OptionExample{optional_value: nil}), do: :error
def unwrap(%OptionExample{optional_value: value}), do: {:ok, value} def unwrap(%OptionExample{optional_value: value}), do: {:ok, value}
def valid?(%OptionExample{}), do: true
end end
end end

View file

@ -1,12 +1,23 @@
defmodule ResultExample do defmodule ResultExample do
defstruct state: nil, value: nil defstruct state: nil, value: nil
@behaviour Upshot.Result
@moduledoc false @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 defimpl Upshot.Result.Proto do
def ok?(%ResultExample{state: :ok}), do: true def ok?(%ResultExample{state: :ok}), do: true
def ok?(%ResultExample{}), do: false def ok?(%ResultExample{}), do: false
def error?(%ResultExample{state: :error}), do: true def error?(%ResultExample{state: :error}), do: true
def error?(%ResultExample{}), do: false def error?(%ResultExample{}), do: false
def unwrap(%ResultExample{state: state, value: value}), do: {state, value} 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
end end

View file

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