Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ Of course you can easily implement the outstanding protocol for your own type (e
## Expected Functions
Sometimes our expectation is a bit vague, for instance in the example above we initially did not know the id. We can supply a function as an expectation, when not met this supplies a corresponding atom.

An expected function of arity 1 implicitly has actual as the argument.

```elixir
iex> import Outstanding
Outstanding
Expand All @@ -115,6 +117,17 @@ iex> outstanding(&Outstand.any_integer/1, nil)
end
```

There are are number of included expected functions, see the table in Supported Types for the types they relate to.

Expected functions of arity 2 are also supported. These have the form of a tuple of function and term, where term is an argument list.

| Expected Function | Expected Type | Resolving Types | Behaviour |
|-------------------|-------------------------|-------------------------|---------------------------------------------------------------------------------------|
| all_of | List, Map, Keyword List | List, Map, Keyword List | expects all expected elements to be resolved by any actual element |
| any_of | List, Map, Keyword List | Any | expects at least one expected element to be resolved by actual |
| none_of | List, Map, Keyword List | List, Map, Keyword List | expects no expected elements to be resolved by any actual element |
| one_of | List, Map, Keyword List | List, Map, Keyword List | expects exactly one expected element to be resolved by any actual element |

You can supply your own functions where needed.

## Infix Shortcuts
Expand Down
257 changes: 207 additions & 50 deletions lib/outstand.ex
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ defmodule Outstand do
gen_something_outstanding_test: 3,
gen_result_outstanding_test: 4,
outstanding?: 2,
nil_outstanding?: 2,
outstanding?: 1,
nil_outstanding?: 1,
any_atom: 1,
any_bitstring: 1,
any_boolean: 1,
Expand Down Expand Up @@ -55,7 +57,10 @@ defmodule Outstand do
past_date_time: 1,
past_naive_date_time: 1,
past_time: 1,
suppress: 1,
all_of: 2,
any_of: 2,
none_of: 2,
one_of: 2
]
end
end
Expand Down Expand Up @@ -235,7 +240,27 @@ defmodule Outstand do
end

@doc """
Is anything oustanding given expected and actual term?
Checks whether a result has nothing outstanding

## Examples

```
iex> Outstand.nil_outstanding?(1)
false
iex> Outstand.nil_outstanding?(nil)
true
iex> Outstand.nil_outstanding?(%{})
false
iex> Outstand.nil_outstanding?([])
false
```
"""
def nil_outstanding?(outstanding) do
not outstanding?(outstanding)
end

@doc """
Is anything outstanding given expected and actual term?

## Examples
```
Expand All @@ -252,38 +277,22 @@ defmodule Outstand do
outstanding?(Outstanding.outstanding(expected, actual))
end


@doc """
Suppress outstanding result when empty map or list
Is nothing outstanding given expected and actual term?

## Examples

```
iex> Outstand.suppress(%{})
nil
iex> Outstand.suppress(%{x: :a})
%{x: :a}
iex> Outstand.suppress(MapSet.new())
nil
iex> Outstand.suppress(MapSet.new([:a]))
MapSet.new([:a])
iex> Outstand.suppress([])
nil
iex> Outstand.suppress([:a])
[:a]
iex> Outstand.nil_outstanding?(1, 1)
true
iex> Outstand.nil_outstanding?(1, nil)
false
iex> Outstand.nil_outstanding?(1, 2)
false
```
"""
def suppress(enum) when is_map(enum) or is_list(enum) do
if (Enum.empty?(enum)) do
nil
else
enum
end
end


def suppress(term) when is_nil(term) do
nil
@spec nil_outstanding?(Outstanding.t, any) :: boolean()
def nil_outstanding?(expected, actual) do
not outstanding?(expected, actual)
end

@doc """
Expand Down Expand Up @@ -1178,6 +1187,176 @@ defmodule Outstand do
end
end

@spec all_of(maybe_improper_list(), any()) :: nil | :all_of
@doc """
Function which expects all elements in expected list to be resolved by an element from actual list

## Examples
```
iex> Outstand.all_of([1, 2, 3], [3, 1, 2])
nil
iex> Outstand.all_of([1, 2, 3], [1])
:all_of
iex> Outstand.all_of([1, 2, 3], nil)
:all_of
```
"""
def all_of(expected, actual) when is_list(expected) do
case is_list(actual) and Enum.all?(expected, &Outstand.nil_outstanding?(Outstand.any_of_actual_list(&1, actual))) do
true -> nil
_ -> :all_of
end
end

@spec any_of(maybe_improper_list(), any()) :: nil | :any_of
@doc """
Function which expects at least one element in expected list to be resolved by actual

## Examples
```
iex> Outstand.any_of([1, 2, 3], 1)
nil
iex> Outstand.any_of([1, 2, 3], 0)
:any_of
iex> Outstand.any_of([1, 2, 3], nil)
:any_of
```
"""
def any_of(expected, actual) when is_list(expected) do
case Enum.any?(expected, &Outstand.nil_outstanding?(&1, actual)) do
true -> nil
_ -> :any_of
end
end

@spec none_of(maybe_improper_list(), any()) :: nil | :none_of
@doc """
Function which expects no element in expected list to be resolved by actual

## Examples
```
iex> Outstand.none_of([1, 2, 3], 0)
nil
iex> Outstand.none_of([1, 2, 3], 1)
:none_of
iex> Outstand.none_of([1, 2, 3], nil)
nil
```
"""
def none_of(expected, actual) when is_list(expected) do
case Enum.count(Enum.map(expected, &Outstand.nil_outstanding?(&1, actual)), fn x -> x end) do
0 -> nil
_ -> :none_of
end
end

@spec one_of(maybe_improper_list(), any()) :: nil | :one_of
@doc """
Function which expects exactly one element in expected list to be resolved by actual

## Examples
```
iex> Outstand.one_of([1, 2, 3], 1)
nil
iex> Outstand.one_of([1, 1, 3], 1)
:one_of
iex> Outstand.one_of([1, 2, 3], nil)
:one_of
```
"""
def one_of(expected, actual) when is_list(expected) do
case Enum.count(Enum.map(expected, &Outstand.nil_outstanding?(&1, actual)), fn x -> x end) do
1 -> nil
_ -> :one_of
end
end

@spec any_of_actual_list(any(), any()) :: any()
@doc """
Function which expects at least one element from actual list to resolve expected

## Examples
```
iex> Outstand.any_of_actual_list(1, [1,2,3])
nil
iex> Outstand.any_of_actual_list(1, [2, 3])
1
iex> Outstand.any_of_actual_list(1, nil)
1
```
"""
def any_of_actual_list(expected, actual) do
case is_list(actual) && Enum.any?(actual, &Outstand.nil_outstanding?(expected, &1)) do
true -> nil
_ -> expected
end
end

@doc """
Converts term to struct if a map

## Examples
```
iex> today = DateTime.utc_now() |> DateTime.to_date()
iex> today_map = Map.delete(today, :__struct__)
iex> assert today == Outstand.map_to_struct(today_map, Date)
iex> Outstand.map_to_struct(nil, Date)
nil
```
"""
@spec map_to_struct(any() | nil, bitstring()) :: any()
def map_to_struct(term, name) do
if (is_map(term)) do
struct(name, term)
else
term
end
end

@doc """
Suppress outstanding result when empty list, map, map set or tuple

## Examples

```
iex> Outstand.suppress([])
nil
iex> Outstand.suppress([:a])
[:a]
iex> Outstand.suppress(%{})
nil
iex> Outstand.suppress(%{x: :a})
%{x: :a}
iex> Outstand.suppress(MapSet.new())
nil
iex> Outstand.suppress(MapSet.new([:a]))
MapSet.new([:a])
iex> Outstand.suppress({})
nil
iex> Outstand.suppress({:a})
{:a}
```
"""
def suppress(enum) when is_map(enum) or is_list(enum) do
if Enum.empty?(enum) do
nil
else
enum
end
end

def suppress(tuple) when is_tuple(tuple) do
if tuple_size(tuple) == 0 do
nil
else
tuple
end
end

def suppress(atom) when is_nil(atom) do
nil
end

@doc """
Types the argument, similar to Typable

Expand Down Expand Up @@ -1239,26 +1418,4 @@ defmodule Outstand do
end
end
end

@doc """
Converts term to struct if a map

## Examples
```
iex> today = DateTime.utc_now() |> DateTime.to_date()
iex> today_map = Map.delete(today, :__struct__)
iex> Outstand.map_to_struct(today_map, Date)
~D[2025-03-04]
iex> Outstand.map_to_struct(nil, Date)
nil

"""
@spec map_to_struct(any() | nil, bitstring()) :: any()
def map_to_struct(term, name) do
if (is_map(term)) do
struct(name, term)
else
term
end
end
end
1 change: 1 addition & 0 deletions lib/outstanding/function.ex
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use Outstand

defoutstanding expected :: Function, actual :: Any do
# arity/1 expected function
expected.(actual)
end
28 changes: 28 additions & 0 deletions lib/outstanding/tuple.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
use Outstand

defoutstanding expected :: Tuple, actual :: Any do
if is_function(elem(expected, 0)) and tuple_size(expected) == 2 do
# tuple contains an arity/2 expected function, where first element is function and second is term
elem(expected, 0).(elem(expected, 1), actual)
else
# regular tuple
case Outstand.type_of(actual) do
Tuple ->
if tuple_size(expected) == tuple_size(actual) do
{outstanding, _} =
Enum.zip(Tuple.to_list(expected), Tuple.to_list(actual))
|> Enum.filter(&Outstanding.outstanding(elem(&1, 0), elem(&1, 1)))
|> Enum.unzip()
if (outstanding == []) do
nil
else
expected
end
else
expected
end
_ ->
expected
end
end
end
2 changes: 1 addition & 1 deletion test/function_test.exs → test/function_arity_1_test.exs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
defmodule Outstanding.FunctionTest do
defmodule Outstanding.ExpectedFunctionArity1Test do
use ExUnit.Case
use Outstand

Expand Down
26 changes: 26 additions & 0 deletions test/function_arity_2_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
defmodule Outstanding.ExpectedFunctionArity2Test do
use ExUnit.Case
use Outstand

gen_something_outstanding_test("all_of value outstanding", {&Outstand.all_of/2, [1, 2, 3]}, [2, 3])
gen_nothing_outstanding_test("all_of value realized", {&Outstand.all_of/2, [1, 2, 3]}, [1, 2, 3])
gen_nothing_outstanding_test("all_of value realized, extra element", {&Outstand.all_of/2, [1, 2, 3]}, [1, 2, 3, 4])
gen_result_outstanding_test("all_of value result", {&Outstand.all_of/2, [1, 2, 3]}, [2, 3], :all_of)

gen_something_outstanding_test("any_of value outstanding", {&Outstand.any_of/2, [1, 2, 3]}, 4)
gen_nothing_outstanding_test("any_of value realized", {&Outstand.any_of/2, [1, 2, 3]}, 3)
gen_nothing_outstanding_test("any_of value realized, duplicates", {&Outstand.any_of/2, [1, 2, 2]}, 2)
gen_result_outstanding_test("any_of value result", {&Outstand.any_of/2, [1, 2, 3]}, 4, :any_of)

gen_something_outstanding_test("none_of value outstanding", {&Outstand.none_of/2, [1, 2, 3]}, 3)
gen_something_outstanding_test("none_of value outstanding, duplicates", {&Outstand.none_of/2, [1, 2, 2]}, 2)
gen_nothing_outstanding_test("none_of value realized", {&Outstand.none_of/2, [1, 2, 3]}, 4)
gen_result_outstanding_test("none_of value result", {&Outstand.none_of/2, [1, 2, 3]}, 3, :none_of)
gen_result_outstanding_test("none_of value result, duplicates", {&Outstand.none_of/2, [1, 2, 2]}, 2, :none_of)

gen_something_outstanding_test("one_of value outstanding", {&Outstand.one_of/2, [1, 2, 3]}, 4)
gen_something_outstanding_test("one_of value outstanding, duplicates", {&Outstand.one_of/2, [1, 2, 2]}, 2)
gen_nothing_outstanding_test("one_of value realized", {&Outstand.one_of/2, [1, 2, 3]}, 3)
gen_result_outstanding_test("one_of value result", {&Outstand.one_of/2, [1, 2, 3]}, 4, :one_of)
gen_result_outstanding_test("one_of value result, duplicates", {&Outstand.one_of/2, [1, 2, 2]}, 2, :one_of)
end