First pass.
This commit is contained in:
parent
cd1691fa51
commit
576ff84ca5
9 changed files with 567 additions and 16 deletions
|
@ -1,18 +1,2 @@
|
|||
defmodule Upshot do
|
||||
@moduledoc """
|
||||
Documentation for `Upshot`.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Hello world.
|
||||
|
||||
## Examples
|
||||
|
||||
iex> Upshot.hello()
|
||||
:world
|
||||
|
||||
"""
|
||||
def hello do
|
||||
:world
|
||||
end
|
||||
end
|
||||
|
|
19
lib/upshot/errors/expect_error.ex
Normal file
19
lib/upshot/errors/expect_error.ex
Normal file
|
@ -0,0 +1,19 @@
|
|||
defmodule Upshot.ExpectError do
|
||||
defexception message: ""
|
||||
alias Upshot.ExpectError
|
||||
|
||||
@moduledoc """
|
||||
Raised when a call to `Result.expect/2` or `Option.expect/2` fails.
|
||||
"""
|
||||
|
||||
@type t :: %ExpectError{
|
||||
:__exception__ => true,
|
||||
message: String.t()
|
||||
}
|
||||
|
||||
@impl Exception
|
||||
def exception(message) when is_binary(message), do: %ExpectError{message: message}
|
||||
|
||||
@impl Exception
|
||||
def message(%ExpectError{message: message}), do: message
|
||||
end
|
19
lib/upshot/errors/unwrap_error.ex
Normal file
19
lib/upshot/errors/unwrap_error.ex
Normal file
|
@ -0,0 +1,19 @@
|
|||
defmodule Upshot.UnwrapError do
|
||||
defexception message: nil
|
||||
alias Upshot.UnwrapError
|
||||
|
||||
@moduledoc """
|
||||
Raised when a call to `Result.unwrap/1` or `Option.unwrap/1` fails.
|
||||
"""
|
||||
|
||||
@type t :: %UnwrapError{
|
||||
:__exception__ => true,
|
||||
message: String.t()
|
||||
}
|
||||
|
||||
@impl Exception
|
||||
def exception(message), do: %UnwrapError{message: message}
|
||||
|
||||
@impl Exception
|
||||
def message(%UnwrapError{message: message}), do: message
|
||||
end
|
228
lib/upshot/option.ex
Normal file
228
lib/upshot/option.ex
Normal file
|
@ -0,0 +1,228 @@
|
|||
defmodule Upshot.Option do
|
||||
alias Upshot.{ExpectError, Option.Proto, Result, UnwrapError}
|
||||
|
||||
@moduledoc """
|
||||
A protocol for interacting with optional types.
|
||||
"""
|
||||
|
||||
@type t :: Proto.t()
|
||||
@type map_fn :: (any -> t)
|
||||
@type predicate_fn :: (any -> boolean)
|
||||
|
||||
@doc """
|
||||
Create a new option from value.
|
||||
"""
|
||||
@spec some(any) :: t
|
||||
def some(value), do: {:ok, value}
|
||||
|
||||
@doc """
|
||||
Create a new option.
|
||||
"""
|
||||
@spec none :: t
|
||||
def none, do: :error
|
||||
|
||||
@doc """
|
||||
Does the option contain a value?
|
||||
"""
|
||||
@spec some?(t) :: boolean
|
||||
defdelegate some?(option), to: Proto
|
||||
|
||||
@doc """
|
||||
Does the option contain no value?
|
||||
"""
|
||||
@spec none?(t) :: boolean
|
||||
defdelegate none?(option), to: Proto
|
||||
|
||||
@doc """
|
||||
Convert an option into a result.
|
||||
|
||||
If the option is some then it will contain the some, otherwise it will use the
|
||||
provided error value.
|
||||
"""
|
||||
@spec ok_or(t, any) :: Result.t()
|
||||
def ok_or(option, error) do
|
||||
if Proto.some?(option),
|
||||
do: option,
|
||||
else: {:error, error}
|
||||
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.
|
||||
"""
|
||||
@spec ok_or_else(t, map_fn) :: t
|
||||
def ok_or_else(option, err_callback) when is_function(err_callback, 1) do
|
||||
if Proto.some?(option),
|
||||
do: option,
|
||||
else: safe_callback(option, err_callback)
|
||||
end
|
||||
|
||||
@doc """
|
||||
A logical and.
|
||||
|
||||
Returns none if the option is none, otherwise the other option.
|
||||
"""
|
||||
@spec and_(t, t) :: t
|
||||
def and_(lhs, rhs) do
|
||||
if Proto.none?(lhs),
|
||||
do: lhs,
|
||||
else: rhs
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns none, if the option is none, otherwise ok_callback with the wrapped
|
||||
value and returns the result.
|
||||
"""
|
||||
@spec and_then(t, map_fn) :: t
|
||||
def and_then(option, ok_callback) when is_function(ok_callback, 1) do
|
||||
if Proto.none?(option),
|
||||
do: option,
|
||||
else: safe_callback(option, ok_callback)
|
||||
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.
|
||||
"""
|
||||
@spec filter(t, predicate_fn) :: t
|
||||
def filter(option, predicate) do
|
||||
case Proto.unwrap(option) do
|
||||
{:ok, value} ->
|
||||
if apply(predicate, [value]),
|
||||
do: {:ok, value},
|
||||
else: none()
|
||||
|
||||
:error ->
|
||||
option
|
||||
end
|
||||
rescue
|
||||
_error -> none()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the option if it contains a value, otherwise returns the alternative
|
||||
option.
|
||||
"""
|
||||
@spec or_(t, t) :: t
|
||||
def or_(lhs, rhs) do
|
||||
if Proto.some?(lhs),
|
||||
do: lhs,
|
||||
else: rhs
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the option if it contains a value, otherwise calls err_callback and
|
||||
returns the result.
|
||||
"""
|
||||
@spec or_else(t, (() -> t)) :: t
|
||||
def or_else(option, err_callback) when is_function(err_callback, 0) do
|
||||
if Proto.some?(option) do
|
||||
option
|
||||
else
|
||||
apply(err_callback, [])
|
||||
end
|
||||
rescue
|
||||
_error -> none()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns some if exactly one of the options are some, otherwise none.
|
||||
"""
|
||||
@spec xor_(t, t) :: t
|
||||
def xor_(lhs, rhs) do
|
||||
cond do
|
||||
Proto.some?(lhs) && Proto.none?(rhs) -> lhs
|
||||
Proto.none?(lhs) && Proto.some?(rhs) -> rhs
|
||||
true -> none()
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Zips the two options together.
|
||||
|
||||
If both options are some, returns a some containing a tuple of both values, otherwise none.
|
||||
"""
|
||||
@spec zip(t, t) :: t
|
||||
def zip(lhs, rhs) do
|
||||
with {:ok, lhs} <- Proto.unwrap(lhs),
|
||||
{:ok, rhs} <- Proto.unwrap(rhs) do
|
||||
{:ok, {lhs, rhs}}
|
||||
else
|
||||
_error -> none()
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Unwraps the option and returns the value.
|
||||
|
||||
Raises an `ExpectError` on failure with `message`.
|
||||
"""
|
||||
@spec expect(t, String.t()) :: any | no_return
|
||||
def expect(option, message) when is_binary(message) do
|
||||
case Proto.unwrap(option) do
|
||||
{:ok, value} -> value
|
||||
:error -> raise ExpectError, message
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Unwraps the option presuming it's none.
|
||||
|
||||
Raises an `ExpectError` when the option is some with `message`.
|
||||
"""
|
||||
@spec expect_none(t, String.t()) :: t | no_return
|
||||
def expect_none(option, message) when is_binary(message) do
|
||||
if Proto.some?(option) do
|
||||
raise ExpectError, message
|
||||
else
|
||||
none()
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Unwraps the option and returns the value.
|
||||
|
||||
Raises an `UnwrapError` on failure.
|
||||
"""
|
||||
@spec unwrap(t) :: any | no_return
|
||||
def unwrap(option) do
|
||||
case Proto.unwrap(option) do
|
||||
{:ok, value} -> value
|
||||
:error -> raise UnwrapError, "Attempt to unwrap a none"
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Unwraps an option and returns a none.
|
||||
|
||||
Raises an `UnwrapError` on failure.
|
||||
"""
|
||||
@spec unwrap_none(t) :: t | no_return
|
||||
def unwrap_none(option) do
|
||||
if Proto.none?(option) do
|
||||
option
|
||||
else
|
||||
raise UnwrapError, "Attempt to unwrap a some"
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Unwraps an option and returns it's contained value or a default.
|
||||
"""
|
||||
@spec unwrap_or_default(t, any) :: any
|
||||
def unwrap_or_default(option, default) do
|
||||
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(t, map_fn) :: Result.t()
|
||||
defp safe_callback(option, callback) when is_function(callback, 1) do
|
||||
apply(callback, [option])
|
||||
rescue
|
||||
error -> {:error, error}
|
||||
end
|
||||
end
|
16
lib/upshot/option/proto.ex
Normal file
16
lib/upshot/option/proto.ex
Normal file
|
@ -0,0 +1,16 @@
|
|||
defprotocol Upshot.Option.Proto do
|
||||
@moduledoc """
|
||||
A protocol for interacting with optional types.
|
||||
"""
|
||||
|
||||
@fallback_to_any true
|
||||
|
||||
@spec some?(t) :: boolean
|
||||
def some?(_option)
|
||||
|
||||
@spec none?(t) :: boolean
|
||||
def none?(_option)
|
||||
|
||||
@spec unwrap(t) :: {:ok, any} | :error
|
||||
def unwrap(_option)
|
||||
end
|
26
lib/upshot/option/proto_any.ex
Normal file
26
lib/upshot/option/proto_any.ex
Normal file
|
@ -0,0 +1,26 @@
|
|||
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: value
|
||||
def unwrap(_option), do: :error
|
||||
end
|
210
lib/upshot/result.ex
Normal file
210
lib/upshot/result.ex
Normal file
|
@ -0,0 +1,210 @@
|
|||
defmodule Upshot.Result do
|
||||
alias Upshot.{ExpectError, Option, Result.Proto, UnwrapError}
|
||||
|
||||
@moduledoc """
|
||||
Functions for working with types which implement the result protocol.
|
||||
"""
|
||||
|
||||
@type t :: Proto.t()
|
||||
@type map_fn :: (any -> {:ok, any} | {:error, any})
|
||||
|
||||
@doc """
|
||||
Create a new result with an ok value.
|
||||
"""
|
||||
@spec ok(any) :: t
|
||||
def ok(value), do: {:ok, value}
|
||||
|
||||
@doc """
|
||||
Create a new result with an error value.
|
||||
"""
|
||||
@spec error(any) :: t
|
||||
def error(reason), do: {:error, reason}
|
||||
|
||||
@doc """
|
||||
Convert a result into an option.
|
||||
"""
|
||||
@spec to_option(t) :: Option.t()
|
||||
def to_option(result) do
|
||||
case Proto.unwrap(result) do
|
||||
{:ok, value} -> {:ok, value}
|
||||
_error -> :error
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Is the result okay?
|
||||
"""
|
||||
@spec ok?(t) :: boolean
|
||||
defdelegate ok?(result), to: Proto
|
||||
|
||||
@doc """
|
||||
Is the result an error?
|
||||
"""
|
||||
@spec error?(t) :: boolean
|
||||
defdelegate error?(result), to: Proto
|
||||
|
||||
@doc """
|
||||
Returns true if the result is ok and it's value equals the test value.
|
||||
"""
|
||||
@spec contains?(t, any) :: boolean
|
||||
def contains?(result, test_value), do: Proto.unwrap(result) == {:ok, test_value}
|
||||
|
||||
@doc """
|
||||
Returns true if the result is an error and it's value equals the test value.
|
||||
"""
|
||||
@spec contains_err?(t, any) :: boolean
|
||||
def contains_err?(result, test_value), do: Proto.unwrap(result) == {:error, test_value}
|
||||
|
||||
@doc """
|
||||
Maps an ok result into a new result using the provided function, leaving an
|
||||
error result untouched.
|
||||
"""
|
||||
@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
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Maps an ok result into a new result using the provided function, replacing an
|
||||
error value with the default.
|
||||
"""
|
||||
@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}
|
||||
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.
|
||||
"""
|
||||
@spec map_or_else(t, map_fn, map_fn) :: t
|
||||
def map_or_else(result, ok_callback, err_callback) do
|
||||
case Proto.unwrap(result) do
|
||||
{:ok, value} -> safe_callback(value, ok_callback)
|
||||
{:error, value} -> safe_callback(value, err_callback)
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Maps an error result into another result by applying the function to an error
|
||||
value and leaving an ok value untouched.
|
||||
"""
|
||||
@spec map_err(t, map_fn) :: t
|
||||
def map_err(result, err_callback) do
|
||||
case Proto.unwrap(result) do
|
||||
{:error, value} -> safe_callback(value, err_callback)
|
||||
_other -> result
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
A logical and.
|
||||
|
||||
If `lhs` is ok, returns `rhs`, otherwise returns `lhs`.
|
||||
"""
|
||||
@spec and_(t, t) :: t
|
||||
def and_(lhs, rhs) do
|
||||
case Proto.unwrap(lhs) do
|
||||
{:ok, _value} -> rhs
|
||||
_other -> lhs
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Calls `ok_callback` if the result is ok, otherwise returns the error value.
|
||||
"""
|
||||
@spec and_then(t, map_fn) :: t
|
||||
def and_then(result, ok_callback) when is_function(ok_callback, 1) do
|
||||
case Proto.unwrap(result) do
|
||||
{:ok, value} -> safe_callback(value, ok_callback)
|
||||
_other -> result
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
A logical or.
|
||||
|
||||
If `lhs` is an error, returns `rhs`, otherwise returns `lhs`.
|
||||
"""
|
||||
@spec or_(t, t) :: t
|
||||
def or_(lhs, rhs) do
|
||||
case Proto.unwrap(lhs) do
|
||||
{:error, _value} -> rhs
|
||||
_ -> lhs
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Unwraps the result and returns the value.
|
||||
|
||||
Raises an `ExpectError` on failure with `message`.
|
||||
"""
|
||||
@spec expect(t, String.t()) :: any | no_return
|
||||
def expect(result, message) when is_binary(message) do
|
||||
case Proto.unwrap(result) do
|
||||
{:ok, value} ->
|
||||
value
|
||||
|
||||
{:error, _} ->
|
||||
raise ExpectError, message
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Unwraps the result and returns the value.
|
||||
|
||||
Raises an `UnwrapError` on failure.
|
||||
"""
|
||||
@spec unwrap(t) :: any | no_return
|
||||
def unwrap(result) do
|
||||
case Proto.unwrap(result) do
|
||||
{:ok, value} -> value
|
||||
{:error, _} -> raise UnwrapError, "Attempt to unwrap error result"
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the contained ok value, or a provided default.
|
||||
"""
|
||||
@spec unwrap_or(t, any) :: any
|
||||
def unwrap_or(result, default) do
|
||||
case Proto.unwrap(result) do
|
||||
{:ok, value} -> value
|
||||
_other -> default
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the contained ok value, or computes it with the provided function.
|
||||
"""
|
||||
@spec unwrap_or_else(t, map_fn) :: any
|
||||
def unwrap_or_else(result, err_callback) when is_function(err_callback, 1) do
|
||||
case Proto.unwrap(result) do
|
||||
{:ok, value} ->
|
||||
value
|
||||
|
||||
{:error, value} ->
|
||||
value
|
||||
|> safe_callback(err_callback)
|
||||
|> 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}
|
||||
end
|
||||
end
|
18
lib/upshot/result/proto.ex
Normal file
18
lib/upshot/result/proto.ex
Normal file
|
@ -0,0 +1,18 @@
|
|||
defprotocol Upshot.Result.Proto do
|
||||
@moduledoc """
|
||||
The result protocol.
|
||||
|
||||
Implement this for your own result types, as needed.
|
||||
"""
|
||||
|
||||
@fallback_to_any true
|
||||
|
||||
@spec ok?(t) :: boolean
|
||||
def ok?(_result)
|
||||
|
||||
@spec error?(t) :: boolean
|
||||
def error?(_result)
|
||||
|
||||
@spec unwrap(t) :: {:ok, any} | {:error, any}
|
||||
def unwrap(_result)
|
||||
end
|
31
lib/upshot/result/proto_any.ex
Normal file
31
lib/upshot/result/proto_any.ex
Normal file
|
@ -0,0 +1,31 @@
|
|||
defimpl Upshot.Result.Proto, for: Any do
|
||||
@moduledoc """
|
||||
Implements the result protocol for arbitrary terms.
|
||||
"""
|
||||
|
||||
@doc """
|
||||
Returns `true` for `{:ok, value}` and `:ok`, otherwise `false`.
|
||||
"""
|
||||
@spec ok?(any) :: boolean
|
||||
def ok?({:ok, _value}), do: true
|
||||
def ok?(:ok), 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: true
|
||||
|
||||
@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
|
Reference in a new issue