Run this notebook

Use Livebook to open this notebook and explore new ideas.

It is easy to get started, on your machine or the cloud.

Click below to open and run it in your Livebook at .

(or change your Livebook location)

# Outstanding Elixir Protocol ```elixir Mix.install([{:outstanding, "~> 0.2.3"}], consolidate_protocols: false) ``` ## Overview In this livebook tutorial you will learn * Why Outstanding? * What Outstanding does? * How to use Outstanding Protocol * How to implement Outstanding for your Types And Structs * How to use Outstand Expected Functions * How to create Expected Functions ## Why Outstanding? Outstanding is a protocol for checking whether our expectations have been met or exceeded, and/or seeing which expectations are still outstanding. ## What Outstanding does? Outstanding defines an Elixir protocol for comparing expected and actual, where these can be of any type. Outstanding protocol defines two functions: * outstanding?(expected, actual), returning a boolean which is true if anything is outstanding, otherwise false * outstanding(expected, actual), returning what is outstanding, otherwise nil ### Outstanding.outstanding?(expected, actual) Try out the ```outstanding?``` function with different arguments, and with nil for expected or actual. Outstanding.outstanding?(:thing, :thing) is false, since expected :thing is resolved by the actual :thing and nothing is outstanding ```elixir Outstanding.outstanding?(:thing, :thing) ``` Outstanding?(nil, :anything) is always false, as nil expectations are always met, so there is nothing outstanding Outstanding?(:anything, nil) is always true, as nil cannot meet the :anything expectation. ### Outstanding.outstanding(expected, actual) Try out the ```outstanding``` function with different arguments, and with nil for expected or actual. Outstanding(:thing, :other_thing) is :thing, since, since expected :thing is not resolved by the actual :other-thing, so expected :thing is outstanding ```elixir 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 on Maps 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/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``` 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}, %{id: 1, state: :active, status: :idle}) ``` Which should evaluate to ```elixir %{status: :working} ``` </div> </details> Then start the actual service so that it has '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}, %{id: 1, state: :active, status: :working}) ``` Which should evaluate to ```elixir 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> Try expecting an access 'child' service with ```access: %{status: working}```, with an actual access 'child, which has an actual ```:access``` value of ```%{id: 3, state: :active, status: :degraded}```. What remains outstanding? <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, access: %{status: :working}}, %{id: 1, state: :active, status: :working, access: %{id: 3, state: :active, status: :degraded}}) ``` Which should evaluate to ```elixir %{access: %{status: :working}} ``` </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 easily implement Outstanding on Structs and other Types ### Derive Outstanding on your Structs When you define your struct you can simply @derive the Outstanding protocol. By default this expects all fields, and actual must be a struct of the same type. ```elixir defmodule ABC do @derive Outstanding defstruct [:a, :b, :c] end ``` Then we can evaluate outstanding on our new struct: ```elixir Outstanding.outstanding(%ABC{a: "apple", b: "banana", c: "carrot"}, %ABC{a: "apple", b: "bagel", c: "cake"}) ``` We can also use the ```except``` option to exclude fields: ```elixir defmodule AB do @derive {Outstanding, except: [:c]} defstruct [:a, :b, :c] end ``` ```elixir Outstanding.outstanding(%AB{a: "apple", b: "banana", c: "carrot"}, %AB{a: "apple", b: "bagel", c: "cake"}) ``` ### Outstanding on any Type 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. 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. <!-- livebook:{"force_markdown":true} --> ```elixir use Outstand defoutstanding expected :: Regex, actual :: Any do case Regex.match?(expected, String.Chars.to_string(actual)) do true -> nil false -> expected end end ``` Additionally macros are provided to allow you to easily test your outstanding implementation. ```elixir ExUnit.start() defmodule Outstanding.RegexTest do use ExUnit.Case use Outstand gen_something_outstanding_test("value outstanding", ~r/foo/, "bar") gen_nothing_outstanding_test("realized", ~r/foo/, "foo") gen_nothing_outstanding_test("realized, match within string", ~r/foo/, "barfoobar") gen_nothing_outstanding_test("realized, match within String.Chars implementation", ~r/foo/, :barfoobar) gen_result_outstanding_test("value result", ~r/foo/, "bar", ~r/foo/) end ``` These tests can be executed ```elixir ExUnit.run() ``` ### Outstanding on your Struct using defoutstanding 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. Given the struct ```elixir defmodule Service do defstruct [:id, :state, :status, :access] end ``` Try writing defoutstanding for yourself, requiring actual to also be a service struct, and to perform outstanding on all fields except id. ```elixir use Outstand defoutstanding expected :: Service, actual :: Any do case {expected, actual} do {nil, nil} -> nil {_, ^expected} -> nil {%name{}, %name{}} -> expected # your code here |> Outstand.map_to_struct(name) {_, _} -> # not an exact match so default to outstanding expected end end ``` <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 defoutstanding expected :: Service, actual :: Any do case {expected, actual} do {nil, nil} -> nil {_, ^expected} -> nil {%name{}, %name{}} -> expected |> Map.from_struct() |> Map.delete(:id) |> Outstanding.outstanding(Map.from_struct(actual)) |> Outstand.map_to_struct(name) {_, _} -> # not an exact match so default to outstanding expected end end ``` </div> </details> ```elixir ExUnit.start() defmodule Outstanding.ServiceTest do use ExUnit.Case use Outstand gen_something_outstanding_test("service state outstanding", %Service{state: :active}, %Service{state: :inactive}) gen_nothing_outstanding_test("service state realised", %Service{state: :active}, %Service{state: :active}) gen_result_outstanding_test("service state outstanding result", %Service{state: :active}, %Service{state: :inactive}, %Service{state: :active}) end ExUnit.run() ``` ## How to use Outstand Expected Functions Outstand also has a number of arity/1 and arity/2 expected functions. Arity/1 functions simply work on the actual value, whereas arity/2 functions take a expected argument list and actual. By convention Outstanding returns an atom with the function name if anything is outstanding. We can use the expected function &any_of/2 for the value of state ```elixir Outstanding.outstanding({&Outstand.any_of/2, [:active, :inactive, :suspended]}, :cancelled) ``` ## How to create Expected Functions You can easily create your own expected functions, they simply need to return nil or outstanding after evaluating their expected argument list and actual. Try creating an expected function ```non_terminal_state``` which expects the value to be any of [:active, :inactive, :suspended] ```elixir defmodule ExpectedFunction do def non_terminal_state(actual) do # your code here end end ``` <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 defmodule ExpectedFunction do def non_terminal_state(actual) do if (actual in [:active, :inactive, :suspended]) do nil else :non_terminal_state end end end ``` </div> </details> ```elixir ExUnit.start() defmodule Outstanding.ExpectedFunctionTest do use ExUnit.Case use Outstand gen_something_outstanding_test("non_terminal_state value outstanding", &ExpectedFunction.non_terminal_state/1, :cancelled) gen_nothing_outstanding_test("non_terminal_state :active realized", &ExpectedFunction.non_terminal_state/1, :active) gen_nothing_outstanding_test("non_terminal_state :inactive realized", &ExpectedFunction.non_terminal_state/1, :inactive) gen_nothing_outstanding_test("non_terminal_state :suspended realized", &ExpectedFunction.non_terminal_state/1, :suspended) gen_result_outstanding_test("non_terminal_state value result", &ExpectedFunction.non_terminal_state/1, :cancelled, :non_terminal_state) end ExUnit.run() ```
See source

Have you already installed Livebook?

If you already installed Livebook, you can configure the default Livebook location where you want to open notebooks.
Livebook up Checking status We can't reach this Livebook (but we saved your preference anyway)
Run notebook

Not yet? Install Livebook in just a minute

Livebook is open source, free, and ready to run anywhere.

Run on your machine

with Livebook Desktop

Run in the cloud

on select platforms

To run on Linux, Docker, embedded devices, or Elixir’s Mix, check our README.

PLATINUM SPONSORS
SPONSORS
Code navigation with go to definition of modules and functions Read More ×