diff --git a/README.md b/README.md index 8dffa24..c00d632 100644 --- a/README.md +++ b/README.md @@ -28,11 +28,17 @@ by adding `outstanding` to your list of dependencies in `mix.exs`: ```elixir def deps do [ - {:outstanding, "~> 0.1.0"} + {:outstanding, "~> 0.2.0"} ] end ``` +## Tutorial + +To get started you need a running instance of [Livebook](https://livebook.dev/) + +[![Run in Livebook](https://livebook.dev/badge/v1/blue.svg)](https://livebook.dev/run?url=https%3A%2F%2Fgithub.com%2Fdiffo-dev%2Foutstanding%2Fblob%2Fdev%2Foutstanding.livemd) + ## Outstanding? Outstanding.outstanding? simply calls Outstanding.outstanding, and is true if anything is outstanding, or false if nil outstanding. @@ -273,5 +279,5 @@ Kudos to the [Elixir Core Team](https://elixir-lang.org/) for [elixir](https://g [ash_outstanding](https://github.com/diffo-dev/ash_outstanding) -[documentation]() +[documentation]() diff --git a/VERSION b/VERSION index 8a9ecc2..341cf11 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.0.1 \ No newline at end of file +0.2.0 \ No newline at end of file diff --git a/mix.exs b/mix.exs index 18a2e09..88b4a50 100644 --- a/mix.exs +++ b/mix.exs @@ -4,7 +4,7 @@ defmodule Outstanding.MixProject do def project do [ app: :outstanding, - version: "0.1.0", + version: "0.2.0", elixir: "~> 1.18", consolidate_protocols: Mix.env() != :test, start_permanent: Mix.env() == :prod, diff --git a/mix.lock b/mix.lock index 3d1836d..f317d52 100644 --- a/mix.lock +++ b/mix.lock @@ -1,6 +1,6 @@ %{ - "earmark_parser": {:hex, :earmark_parser, "1.4.43", "34b2f401fe473080e39ff2b90feb8ddfeef7639f8ee0bbf71bb41911831d77c5", [:mix], [], "hexpm", "970a3cd19503f5e8e527a190662be2cee5d98eed1ff72ed9b3d1a3d466692de8"}, - "ex_doc": {:hex, :ex_doc, "0.37.2", "2a3aa7014094f0e4e286a82aa5194a34dd17057160988b8509b15aa6c292720c", [:mix], [{:earmark_parser, "~> 1.4.42", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "4dfa56075ce4887e4e8b1dcc121cd5fcb0f02b00391fd367ff5336d98fa49049"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.44", "f20830dd6b5c77afe2b063777ddbbff09f9759396500cdbe7523efd58d7a339c", [:mix], [], "hexpm", "4778ac752b4701a5599215f7030989c989ffdc4f6df457c5f36938cc2d2a2750"}, + "ex_doc": {:hex, :ex_doc, "0.38.1", "bae0a0bd5b5925b1caef4987e3470902d072d03347114ffe03a55dbe206dd4c2", [:mix], [{:earmark_parser, "~> 1.4.44", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_c, ">= 0.1.0", [hex: :makeup_c, repo: "hexpm", optional: true]}, {:makeup_elixir, "~> 0.14 or ~> 1.0", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1 or ~> 1.0", [hex: :makeup_erlang, repo: "hexpm", optional: false]}, {:makeup_html, ">= 0.1.0", [hex: :makeup_html, repo: "hexpm", optional: true]}], "hexpm", "754636236d191b895e1e4de2ebb504c057fe1995fdfdd92e9d75c4b05633008b"}, "makeup": {:hex, :makeup, "1.2.1", "e90ac1c65589ef354378def3ba19d401e739ee7ee06fb47f94c687016e3713d1", [:mix], [{:nimble_parsec, "~> 1.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "d36484867b0bae0fea568d10131197a4c2e47056a6fbe84922bf6ba71c8d17ce"}, "makeup_elixir": {:hex, :makeup_elixir, "1.0.1", "e928a4f984e795e41e3abd27bfc09f51db16ab8ba1aebdba2b3a575437efafc2", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.2.3 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "7284900d412a3e5cfd97fdaed4f5ed389b8f2b4cb49efc0eb3bd10e2febf9507"}, "makeup_erlang": {:hex, :makeup_erlang, "1.0.2", "03e1804074b3aa64d5fad7aa64601ed0fb395337b982d9bcf04029d68d51b6a7", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm", "af33ff7ef368d5893e4a267933e7744e46ce3cf1f61e2dccf53a111ed3aa3727"}, diff --git a/outstanding.livemd b/outstanding.livemd new file mode 100644 index 0000000..8fb6047 --- /dev/null +++ b/outstanding.livemd @@ -0,0 +1,312 @@ +# Outstanding Elixir Protocol + +```elixir +Mix.install([{:outstanding, "~> 0.2.0"}], 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 +Outstand.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) +``` + +## How to use Outstanding Protocol + +### Outstanding Map + +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. +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. + +```elixir +Outstanding.outstanding(%{state: :active, status: :working}, + %{id: 1, state: :inactive, status: :idle}) +``` + +Try and resolve outstanding by setting actual ```state: active``` + +
+ Show Solution +
+ + ```elixir + Outstanding.outstanding(%{state: :active, status: working}, + %{id: 1, state: :active, status: :idle}) + ``` + + Which should evaluate to + ```elixir + %{status: :working} + ``` + +
+
+ +Then start the actual service so that it has 'status: working' + +
+ Show Solution +
+ + ```elixir + Outstanding.outstanding(%{state: :active, status: working}, + %{id: 1, state: :active, status: :working}) + ``` + + Which should evaluate to + ```elixir + nil + ``` + +
+
+ +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? + +
+ Show Solution +
+ + ```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}} + ``` + +
+
+ +## 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 + +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. + + + +```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 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. + +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 +``` + +
+ Show Solution +
+ + ```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 + ``` + +
+
+ +```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 +``` + +
+ Show Solution +
+ + ```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 + ``` + +
+
+ +```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() +```