Started adding test coverage.

This commit is contained in:
James Harton 2020-11-14 21:08:28 +13:00
parent 576ff84ca5
commit 2cc786cb28
18 changed files with 388 additions and 23 deletions

16
LICENSE Normal file
View file

@ -0,0 +1,16 @@
Copyright 2020 James Harton
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
* The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
* No Harm: The software may not be used by anyone for systems or activities that actively and knowingly endanger, harm, or otherwise threaten the physical, mental, economic, or general well-being of other individuals or groups, in violation of the United Nations Universal Declaration of Human Rights (https://www.un.org/en/universal-declaration-human-rights/).
* Services: If the Software is used to provide a service to others, the licensee shall, as a condition of use, require those others not to use the service in any way that violates the No Harm clause above.
* Enforceability: If any portion or provision of this License shall to any extent be declared illegal or unenforceable by a court of competent jurisdiction, then the remainder of this License, or the application of such portion or provision in circumstances other than those as to which it is so declared illegal or unenforceable, shall not be affected thereby, and each portion and provision of this Agreement shall be valid and enforceable to the fullest extent permitted by law.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
This Hippocratic License is an Ethical Source license (https://ethicalsource.dev) derived from the MIT License, amended to limit the impact of the unethical use of open source software.

View file

@ -1,2 +1,8 @@
defmodule Upshot do
@moduledoc """
Upshot is a library for working with result types; either your own or
conventional ok/error tuples.
Currently experimental.
"""
end

View file

@ -50,11 +50,11 @@ defmodule Upshot.Option do
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
@spec ok_or_else(t, (() -> any)) :: t
def ok_or_else(option, err_callback) when is_function(err_callback, 0) do
if Proto.some?(option),
do: option,
else: safe_callback(option, err_callback)
else: {:error, safe_callback(err_callback, [])}
end
@doc """
@ -77,7 +77,7 @@ defmodule Upshot.Option do
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)
else: safe_callback(ok_callback, [unwrap(option)])
end
@doc """
@ -95,8 +95,6 @@ defmodule Upshot.Option do
:error ->
option
end
rescue
_error -> none()
end
@doc """
@ -112,7 +110,7 @@ defmodule Upshot.Option do
@doc """
Returns the option if it contains a value, otherwise calls err_callback and
returns the result.
returns the resulting option.
"""
@spec or_else(t, (() -> t)) :: t
def or_else(option, err_callback) when is_function(err_callback, 0) do
@ -121,15 +119,13 @@ defmodule Upshot.Option do
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
@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
@ -219,9 +215,9 @@ defmodule Upshot.Option do
# 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])
@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}
end

View file

@ -21,6 +21,6 @@ defimpl Upshot.Option.Proto, for: Any do
Returns the internal value of `{:ok, value}` or `:error`.
"""
@spec unwrap(any) :: {:ok, any} | :error
def unwrap({:ok, value}), do: value
def unwrap({:ok, value}), do: {:ok, value}
def unwrap(_option), do: :error
end

View file

@ -4,11 +4,10 @@ defimpl Upshot.Result.Proto, for: Any do
"""
@doc """
Returns `true` for `{:ok, value}` and `:ok`, otherwise `false`.
Returns `true` for `{:ok, value}`, otherwise `false`.
"""
@spec ok?(any) :: boolean
def ok?({:ok, _value}), do: true
def ok?(:ok), do: true
def ok?(_result), do: false
@doc """
@ -17,7 +16,7 @@ defimpl Upshot.Result.Proto, for: Any do
@spec error?(any) :: boolean
def error?({:error, _reason}), do: true
def error?(%{:__exception__ => true}), do: true
def error?(_result_), do: true
def error?(_result_), do: false
@doc """
Unwrap a result, if possible.

View file

@ -40,7 +40,8 @@ defmodule Upshot.MixProject do
[
{:credo, "~> 1.5.0-rc.2", only: [:dev, :test], runtime: false},
{:earmark, ">= 0.0.0", only: [:dev, :test], runtime: false},
{:ex_doc, ">= 0.0.0", only: [:dev, :test], runtime: false}
{:ex_doc, ">= 0.0.0", only: [:dev, :test], runtime: false},
{:mimic, "~> 1.3", only: [:dev, :test]}
]
end
end

View file

@ -8,5 +8,6 @@
"jason": {:hex, :jason, "1.2.2", "ba43e3f2709fd1aa1dce90aaabfd039d000469c05c56f0b8e31978e03fa39052", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "18a228f5f0058ee183f29f9eae0805c6e59d61c3b006760668d8d18ff0d12179"},
"makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"},
"makeup_elixir": {:hex, :makeup_elixir, "0.15.0", "98312c9f0d3730fde4049985a1105da5155bfe5c11e47bdc7406d88e01e4219b", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "75ffa34ab1056b7e24844c90bfc62aaf6f3a37a15faa76b07bc5eba27e4a8b4a"},
"mimic": {:hex, :mimic, "1.3.1", "ba1c90b851de9065db81b1e3a16f659e53dffb89d2f4d404db06f67d0b990deb", [:mix], [], "hexpm", "51b74ab8009e0673bf68beba6abfce0e5500c8bec3ad27033c4a8dacbc44d55e"},
"nimble_parsec": {:hex, :nimble_parsec, "1.1.0", "3a6fca1550363552e54c216debb6a9e95bd8d32348938e13de5eda962c0d7f89", [:mix], [], "hexpm", "08eb32d66b706e913ff748f11694b17981c0b04a33ef470e33e11b3d3ac8f54b"},
}

View file

@ -0,0 +1,13 @@
defmodule OptionExample do
defstruct optional_value: nil
@moduledoc false
defimpl Upshot.Option.Proto do
def some?(%OptionExample{optional_value: nil}), do: false
def some?(%OptionExample{}), do: true
def none?(%OptionExample{optional_value: nil}), do: true
def none?(%OptionExample{}), do: false
def unwrap(%OptionExample{optional_value: nil}), do: :error
def unwrap(%OptionExample{optional_value: value}), do: {:ok, value}
end
end

View file

@ -0,0 +1,12 @@
defmodule ResultExample do
defstruct state: nil, value: nil
@moduledoc false
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}
end
end

View file

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

View file

@ -0,0 +1,17 @@
defmodule Upshot.ExpectErrorTest do
use ExUnit.Case, async: true
alias Upshot.ExpectError
@moduledoc false
describe "exception/1" do
test "it returns an expect error" do
assert %ExpectError{message: "Great Scott!"} = ExpectError.exception("Great Scott!")
end
end
describe "message/1" do
test "it converts the error into a string" do
assert "Great Scott!" = ExpectError.exception("Great Scott!") |> ExpectError.message()
end
end
end

View file

@ -0,0 +1,17 @@
defmodule Upshot.UnwrapErrorTest do
use ExUnit.Case, async: true
alias Upshot.UnwrapError
@moduledoc false
describe "exception/1" do
test "it returns an unwrap error" do
assert %UnwrapError{message: "Great Scott!"} = UnwrapError.exception("Great Scott!")
end
end
describe "message/1" do
test "it converts the error into a string" do
assert "Great Scott!" = UnwrapError.exception("Great Scott!") |> UnwrapError.message()
end
end
end

View file

@ -0,0 +1,35 @@
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,34 @@
defmodule Upshot.Option.ProtoTest do
use ExUnit.Case, async: true
alias 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"})
end
test "when the option has no value, it returns false" do
refute Proto.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"})
end
test "when the option has no value, it returns true" do
assert Proto.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"})
end
test "when the option has no value, it returns an error" do
assert :error = Proto.unwrap(%OptionExample{optional_value: nil})
end
end
end

142
test/upshot/option_test.exs Normal file
View file

@ -0,0 +1,142 @@
defmodule Upshot.OptionTest do
use ExUnit.Case, async: true
use Mimic
alias Upshot.{Option, Option.Proto}
@moduledoc false
describe "some/1" do
test "it returns an ok tuple" do
assert {:ok, "Marty"} = Option.some("Marty")
end
end
describe "none/1" do
test "it returns an error atom" do
assert :error = Option.none()
end
end
describe "some?/1" do
test "it delegates to Proto" do
Proto
|> expect(:some?, &assert(&1 == "Marty"))
Option.some?("Marty")
end
end
describe "none?/1" do
test "it delegates to Proto" do
Proto
|> expect(:none?, &assert(&1 == "Doc Brown"))
Option.none?("Doc Brown")
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")
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")
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")
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)
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"))
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"})
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))
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))
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)
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)
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)
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"))
end
test "when the lhs option is none, it returns the rhs option" do
assert {:ok, "Doc"} = Option.or_(Option.none(), Option.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)
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)
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())
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"))
end
test "when both options are some, it returns none" do
assert Option.none() == Option.xor(Option.some("Marty"), Option.some("Doc"))
end
test "when both options are none, it returns none" do
assert Option.none() == Option.xor(Option.none(), Option.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"))
end
test "when the lhs option is none, it returns none" do
assert Option.none() == Option.zip(Option.none(), Option.some("Doc"))
end
test "when the rhs option is none, it returns none" do
assert Option.none() == Option.zip(Option.some("Marty"), Option.none())
end
end
end

View file

@ -0,0 +1,43 @@
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

@ -0,0 +1,35 @@
defmodule Upshot.Result.ProtoTest do
use ExUnit.Case, async: true
alias 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})
end
test "when the result is not ok, it returns false" do
refute Proto.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})
end
test "when the result is not error, it returns false" do
refute Proto.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"})
end
test "when the result is error, it returns an error tuple" do
assert {:error, "Marty"} = Proto.unwrap(%ResultExample{state: :error, value: "Marty"})
end
end
end

View file

@ -1,8 +1,5 @@
defmodule UpshotTest do
use ExUnit.Case
doctest Upshot
test "greets the world" do
assert Upshot.hello() == :world
end
@moduledoc false
end