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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,9 @@ Out of the box we have outstanding protocol implementations for the following ty

Maps call outstanding on each element is expected, but allow extra elements in actual.

Keyword Lists are Lists of Tuples but are handled like Maps.
Keywords are Lists of Tuples but are handled like Maps.

Lists (other than Keyword Lists) are strict in that they must be in order, so lists must have the same number of elements. Elements in the list have outstanding called on them. The entire list is returned when there is no match.
Lists (other than Keywords) are strict in that they must be in order, so lists must have the same number of elements for outstanding to be nil. Outstanding is attempted on each pair of expected/actual elements, even when they have unequal number, in order to return a list of resolved (nil) or outstanding elements. If all expected elements are resolved however there are extra actual elements a list of nils is returned, of length expected.

MapSets are not strict in that actual may contain additional elements, however MapSet.difference is used on the elements (which uses equals not outstanding).

Expand Down
19 changes: 15 additions & 4 deletions lib/outstand.ex
Original file line number Diff line number Diff line change
Expand Up @@ -1441,11 +1441,13 @@ defmodule Outstand do
end

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

## Examples

```
iex> Outstand.suppress([nil, nil])
nil
iex> Outstand.suppress([])
nil
iex> Outstand.suppress([:a])
Expand All @@ -1464,11 +1466,20 @@ defmodule Outstand do
{:a}
```
"""
def suppress(enum) when is_map(enum) or is_list(enum) do
if Enum.empty?(enum) do
def suppress(map) when is_map(map) do
if Enum.empty?(map) do
nil
else
map
end
end

def suppress(list) when is_list(list) do
nils_removed = Enum.reject(list, &is_nil(&1))
if Enum.empty?(nils_removed) do
nil
else
enum
list
end
end

Expand Down
57 changes: 39 additions & 18 deletions lib/outstanding/list.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,25 +3,46 @@ use Outstand
defoutstanding expected :: List, actual :: Any do
case Outstand.type_of(actual) do
List ->
if (expected != [] and Keyword.keyword?(expected)) do
# keyword lists are treated like maps, in that extra elements are tolerated and outstanding is called on 'matching' elements
Keyword.keys(expected)
|> Enum.filter(fn key ->
(key not in Keyword.keys(actual) and key == :no_value) or
Outstanding.outstanding(expected[key], actual[key]) != nil
end)
|> Enum.into([], &{&1, Outstanding.outstanding(expected[&1], actual[&1])})
|> Outstand.suppress()
else
if Enum.count(expected) == Enum.count(actual) do
{outstanding, _} =
Enum.zip(expected, actual)
|> Enum.filter(&Outstanding.outstanding(elem(&1, 0), elem(&1, 1)))
|> Enum.unzip()
Outstand.suppress(outstanding)
else
cond do
# both empty
expected == [] and actual == [] ->
nil

# keyword list
expected != [] and Keyword.keyword?(expected) ->
# keyword lists are treated like maps, in that extra elements are tolerated and outstanding is called on 'matching' elements
Keyword.keys(expected)
|> Enum.filter(fn key ->
(key not in Keyword.keys(actual) and key == :no_value) or
Outstanding.outstanding(expected[key], actual[key]) != nil
end)
|> Enum.into([], &{&1, Outstanding.outstanding(expected[&1], actual[&1])})
|> Outstand.suppress()

# actual not a list
!is_list(actual) ->
expected

# equal size lists (may resolve outstanding)
Enum.count(expected) == Enum.count(actual) ->
expected
|> Enum.zip(actual)
|> Enum.map(&Outstanding.outstanding(elem(&1, 0), elem(&1, 1)))
|> Outstand.suppress()

# actual longer than expected, cannot resolve outstanding
Enum.count(expected) <= Enum.count(actual) ->
expected
|> Enum.take(Enum.count(actual))
|> Enum.zip(actual)
|> Enum.map(&Outstanding.outstanding(elem(&1, 0), elem(&1, 1)))

# expected longer than actual, cannot resolve outstanding
true ->
padded_actual = actual ++ List.duplicate(nil, Enum.count(expected) - Enum.count(actual))
expected
end
|> Enum.zip(padded_actual)
|> Enum.map(&Outstanding.outstanding(elem(&1, 0), elem(&1, 1)))
end
_ ->
expected
Expand Down
Binary file added logos/diffo.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
82 changes: 76 additions & 6 deletions outstanding.livemd
Original file line number Diff line number Diff line change
Expand Up @@ -51,21 +51,39 @@ Outstanding(:thing, :other_thing) is :thing, since, since expected :thing is not
Outstanding.outstanding(:thing, :other_thing)
```

### Exceeds and Difference Operators

For convenient use in expressions we've implemented operators.

The 'exceeds' operator tells us whether our expectations exceed our actual. ```expected >>> actual``` is equivalent to ```Outstanding.outstanding?(expected, actual)```

```elixir
use Outstand
:thing >>> :thing
```

The 'difference' operator tells us what expectations remain unmet. ```expected --- actual``` is equivalent to ```Outstanding.outstanding(expected, actual)```

```elixir
use Outstand
:thing --- :other_thing
```

## How to use Outstanding Protocol

### Outstanding Map
### Outstanding on Maps

Outstanding is implemented for Map, which is where is gets more useful, as if actual is also a map, we call Outstanding value in the expected map, using the corresponding value if any from the actual map.
Outstanding is implemented for Map, where values can be of any type, providing they implement Outstanding. This is where Outstanding protocol gets more useful, as if actual is also a map, we call Outstanding value in the expected map, using the corresponding value if any from the actual map.
This allows actual to have additional keys/values and still potentially resolve expected, resulting in nil outstanding.

We expect a mapped service to be active, however it is currently inactive.
We expect a mapped service to be active/working, however it is currently inactive/idle.

```elixir
Outstanding.outstanding(%{state: :active, status: :working},
%{id: 1, state: :inactive, status: :idle})
```

Try and resolve outstanding by setting actual ```state: active```
Try and resolve outstanding by setting actual ```state: :active``` and ```status: :working```:

<details class="rounded-lg my-4" style="background-color: #96ef86; color: #040604;">
<summary class="cursor-pointer font-bold p-4"></i>Show Solution</summary>
Expand Down Expand Up @@ -100,6 +118,8 @@ Then start the actual service so that it has 'status: working'
nil
```

What happens when expected and actual lists are different lengths? Try adding a third actual service after the other two. If the first two resolve then outstanding will be ```[nil, nil]``` indicating that our list expectation is unmet, however there is nothing outstanding with the first two elements.

</div>
</details>

Expand All @@ -123,12 +143,62 @@ What remains outstanding?
</div>
</details>

## Outstanding on Lists

Outstanding is implemented for Lists, where Lists contain elements implementing Outstanding.
For an actual list to resolve the expected list, the lists must be the same length, and each expected element must be resolved by the corresponding actual element.

We expect a list containing exactly two active/working child services, which are maps.

```elixir
Outstanding.outstanding([%{state: :active, status: :working}, %{state: :active, status: :working}],
[%{id: 1, state: :active, status: :idle}, %{id: 2, state: :suspended, status: :restricted}])
```

Try resolving the first child by setting its actual ```status: :working```:

<details class="rounded-lg my-4" style="background-color: #96ef86; color: #040604;">
<summary class="cursor-pointer font-bold p-4"></i>Show Solution</summary>
<div class="p-4">

```elixir
Outstanding.outstanding([%{state: :active, status: :working}, %{state: :active, status: :working}],
[%{id: 1, state: :active, status: :idle}, %{id: 2, state: :suspended, status: :restricted}])
```

Which should evaluate to
```elixir
[nil, %{state: :active, status: :working}]
```

</div>
</details>

Now resolve the second child by setting its actual ```state: :active``` and ```status: :working```:

<details class="rounded-lg my-4" style="background-color: #96ef86; color: #040604;">
<summary class="cursor-pointer font-bold p-4"></i>Show Solution</summary>
<div class="p-4">

```elixir
Outstanding.outstanding([%{state: :active, status: :working}, %{state: :active, status: :working}],
[%{id: 1, state: :active, status: :working}, %{id: 2, state: :active, status: :working}])
```

Which should evaluate to
```elixir
nil
```

</div>
</details>

## How to implement Outstanding for your Types And Structs

You can implement outstanding for any Type or Struct using the Outstand defoutstanding macro.
Expected is of whatever type you are implementing the protocol for, and actual must be of Any type.

### Outstanding Types
### Outstanding on any Type

The following is the Outstanding implementation for Regex, which expects actual to match the (evaluated) regex.
We won't evaluate this as it is already defined.
Expand Down Expand Up @@ -168,7 +238,7 @@ These tests can be executed
ExUnit.run()
```

### Outstanding Structs
### Outstanding on Structs

When implementing Outstanding for a struct, you need to think about what fields you wish to run outstanding on,
what your expectation is for each field, and whether you require actual to have the same struct name, or even be a struct at all.
Expand Down
5 changes: 4 additions & 1 deletion test/list_of_map_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,13 @@ defmodule Outstanding.ListOfMapTest do
gen_something_outstanding_test("order outstanding", [%{a: "a"}, %{b: "b"}], [%{b: "b"}, %{a: "a"}])
gen_something_outstanding_test("empty outstanding", [], [%{a: "a"}])
gen_something_outstanding_test("extra outstanding", [%{a: "a"}, %{b: "b"}], [%{a: "a"}, %{b: "b"}, %{c: "c"}])
gen_something_outstanding_test("missing outstanding", [%{a: "a"}, %{b: "b"}], [%{a: "a"}])
gen_nothing_outstanding_test("realized", [%{a: "a"}, %{b: "b"}], [%{a: "a"}, %{b: "b"}])
gen_nothing_outstanding_test("realised, extra elements in map", [%{a: "a"}, %{b: "b"}], [%{a: "a", c: "c"}, %{b: "b", d: "d"}])
gen_nothing_outstanding_test("realized, empty", [], [])
gen_result_outstanding_test("element result", [%{a: "a"}, %{b: "b"}], [%{b: "b"}, %{c: "c"}], [%{a: "a"}, %{b: "b"}])
gen_result_outstanding_test("order result", [%{a: "a"}, %{b: "b"}], [%{b: "b"}, %{a: "a"}], [%{a: "a"}, %{b: "b"}])
gen_result_outstanding_test("empty result", [], [%{a: "a"}], [])
gen_result_outstanding_test("extra result", [%{a: "a"}, %{b: "b"}], [%{a: "a"}, %{b: "b"}, %{c: "c"}], [%{a: "a"}, %{b: "b"}])
gen_result_outstanding_test("extra result", [%{a: "a"}, %{b: "b"}], [%{a: "a"}, %{b: "b"}, %{c: "c"}], [nil, nil])
gen_result_outstanding_test("missing result", [%{a: "a"}, %{b: "b"}], [%{a: "a"}], [nil, %{b: "b"}])
end
5 changes: 4 additions & 1 deletion test/list_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@ defmodule Outstanding.ListTest do
use ExUnit.Case
use Outstand


gen_something_outstanding_test("element outstanding", [:a, :b], [:b, :c])
gen_something_outstanding_test("order outstanding", [:a, :b], [:b, :a])
gen_something_outstanding_test("empty outstanding", [], [:a])
gen_something_outstanding_test("extra outstanding", [:a, :b], [:a, :b, :c])
gen_something_outstanding_test("list outstanding, nil", [:a, :b], nil)
gen_nothing_outstanding_test("realized", [:a, :b], [:a, :b])
gen_nothing_outstanding_test("empty realized", [], [])
gen_result_outstanding_test("element result", [:a, :b], [:b, :c], [:a, :b])
gen_result_outstanding_test("order result", [:a, :b], [:b, :a], [:a, :b])
gen_result_outstanding_test("empty result", [], [:a], [])
gen_result_outstanding_test("extra result", [:a, :b], [:a, :b, :c], [:a, :b])
gen_result_outstanding_test("extra result", [:a, :b], [:a, :b, :c], [nil, nil])
gen_result_outstanding_test("list result, nil", [:a, :b], nil, [:a, :b])
end