Tweak protocols and add test coverage.
This commit is contained in:
parent
2cc786cb28
commit
7dec0917f7
21 changed files with 975 additions and 278 deletions
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
55
lib/upshot/option/proto_atom.ex
Normal file
55
lib/upshot/option/proto_atom.ex
Normal 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
|
57
lib/upshot/option/proto_tuple.ex
Normal file
57
lib/upshot/option/proto_tuple.ex
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
74
lib/upshot/result/proto_tuple.ex
Normal file
74
lib/upshot/result/proto_tuple.ex
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
Mimic.copy(Upshot.Option.Proto)
|
||||
Mimic.copy(Upshot.Result.Proto)
|
||||
ExUnit.start()
|
||||
|
|
|
@ -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
|
6
test/upshot/option/proto_atom_test.exs
Normal file
6
test/upshot/option/proto_atom_test.exs
Normal 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
|
|
@ -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
|
||||
|
|
6
test/upshot/option/proto_tuple_test.exs
Normal file
6
test/upshot/option/proto_tuple_test.exs
Normal 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
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
5
test/upshot/result/proto_tuple_test.exs
Normal file
5
test/upshot/result/proto_tuple_test.exs
Normal 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
|
26
test/upshot/result_test.exs
Normal file
26
test/upshot/result_test.exs
Normal 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
|
Reference in a new issue