diff --git a/README.md b/README.md index 79dcfa5..ff0f6ec 100644 --- a/README.md +++ b/README.md @@ -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). diff --git a/lib/outstand.ex b/lib/outstand.ex index 14b4478..d92384c 100644 --- a/lib/outstand.ex +++ b/lib/outstand.ex @@ -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]) @@ -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 diff --git a/lib/outstanding/list.ex b/lib/outstanding/list.ex index 2f1973a..c3f6804 100644 --- a/lib/outstanding/list.ex +++ b/lib/outstanding/list.ex @@ -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 diff --git a/logos/diffo.jpg b/logos/diffo.jpg new file mode 100644 index 0000000..a962441 Binary files /dev/null and b/logos/diffo.jpg differ diff --git a/outstanding.livemd b/outstanding.livemd index 82211f0..6040a62 100644 --- a/outstanding.livemd +++ b/outstanding.livemd @@ -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```:
Show Solution @@ -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. +
@@ -123,12 +143,62 @@ What remains outstanding? +## 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```: + +
+ Show Solution +
+ + ```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}] + ``` + +
+
+ +Now resolve the second child by setting its actual ```state: :active``` and ```status: :working```: + +
+ Show Solution +
+ + ```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 + ``` + +
+
+ ## 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. @@ -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. diff --git a/test/list_of_map_test.exs b/test/list_of_map_test.exs index f539729..81000fb 100644 --- a/test/list_of_map_test.exs +++ b/test/list_of_map_test.exs @@ -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 diff --git a/test/list_test.exs b/test/list_test.exs index 5d247cf..03b52bb 100644 --- a/test/list_test.exs +++ b/test/list_test.exs @@ -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