From 089fc2307e2b01864cbb313a567379e32acce8f9 Mon Sep 17 00:00:00 2001 From: Matt Beanland Date: Mon, 19 May 2025 09:13:28 +1000 Subject: [PATCH 1/2] deriving/2 callback --- CHANGELOG.md | 7 ++++++- README.md | 16 ++++++++++++++-- VERSION | 2 +- lib/outstanding.ex | 31 +++++++++++++++++++++++++++++++ mix.exs | 2 +- outstanding.livemd | 21 ++++++++++++++++++--- test/struct_derive_test.exs | 22 ++++++++++++++++++++++ 7 files changed, 93 insertions(+), 8 deletions(-) create mode 100644 test/struct_derive_test.exs diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a9b41d..ba6af66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,4 +21,9 @@ See [Conventional Commits](Https://conventionalcommits.org) for commit guideline ### Features * enhanced List implementation -* fixed Duration and related expected functions not to use to_timeout \ No newline at end of file +* fixed Duration and related expected functions not to use to_timeout + +## [v0.2.2](https://github.com/diffo-dev/outstanding/compare/v0.2.1...v0.2.2) (2025-05-19) + +### Features +* deriving callback for Structs \ No newline at end of file diff --git a/README.md b/README.md index 27b09f5..fc0c984 100644 --- a/README.md +++ b/README.md @@ -206,9 +206,21 @@ iex> Outstanding.outstanding(:no_value, "a") ## Implementing Outstanding for other types -The defoutstanding macro can be used to implement outstanding on other types, including your own structs. +This requires some though as to what it means to 'resolve' your expected type with actual. -This requires some though as to what it means to 'resolve' your expected struct with actual. The following XYZ struct uses Outstanding on the map to resolve :x, :y, :z, and also expects that actual is also an XYZ struct, although you may want to allow matching using straight maps or other struct with equivalent fields. +## derive for Structs +Outstanding implements the ```__deriving__/2``` callback so you can simply derive an Outstanding implementation when you define your struct. By default this performs outstanding on all fields, and requires the actual struct to be of the same type. + +```elixir +defmodule ABC do + @derive Outstanding + defstruct [:a, :b, :c] +end +``` + +## defoutstanding macro + +More flexibly, the defoutstanding macro can be used to implement outstanding on other types, including your own structs. ```elixir use Outstand diff --git a/VERSION b/VERSION index 7dff5b8..f477849 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.2.1 \ No newline at end of file +0.2.2 \ No newline at end of file diff --git a/lib/outstanding.ex b/lib/outstanding.ex index b675e05..d6d2201 100644 --- a/lib/outstanding.ex +++ b/lib/outstanding.ex @@ -17,4 +17,35 @@ defprotocol Outstanding do """ @spec outstanding?(t, any()) :: boolean() def outstanding?(expected, actual) + + @impl true + defmacro __deriving__(module, _options) do + quote do + defimpl Outstanding, for: unquote(module) do + import Outstand, only: [map_to_struct: 2, outstanding?: 1] + def outstanding(expected, actual) do + case {expected, actual} do + {nil, nil} -> + nil + + {_, ^expected} -> + nil + + {%name{}, %name{}} -> + expected + |> Map.from_struct() + |> Outstanding.outstanding(Map.from_struct(actual)) + |> Outstand.map_to_struct(name) + + {_, _} -> + # not an exact match so default to outstanding + expected + end + end + def outstanding?(expected, actual) do + Outstand.outstanding?(outstanding(expected, actual)) + end + end + end + end end diff --git a/mix.exs b/mix.exs index fb656f1..cc69efd 100644 --- a/mix.exs +++ b/mix.exs @@ -2,7 +2,7 @@ defmodule Outstanding.MixProject do use Mix.Project @name :outstanding - @version "0.2.1" + @version "0.2.2" @description "Elixir protocol calculating outstanding from expected and actual" @github_url "https://github.com/diffo-dev/outstanding" diff --git a/outstanding.livemd b/outstanding.livemd index 4723155..9a8e459 100644 --- a/outstanding.livemd +++ b/outstanding.livemd @@ -1,7 +1,7 @@ # Outstanding Elixir Protocol ```elixir -Mix.install([{:outstanding, "~> 0.2.1"}], consolidate_protocols: false) +Mix.install([{:outstanding, "~> 0.2.2"}], consolidate_protocols: false) ``` ## Overview @@ -195,11 +195,26 @@ Now resolve the second child by setting its actual ```state: :active``` and ```s ## 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. +You can easily implement Outstandign 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 + + outstanding(%ABC{a: "apple", b: "banana", c: "carrot"}, %ABC{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. diff --git a/test/struct_derive_test.exs b/test/struct_derive_test.exs new file mode 100644 index 0000000..5377614 --- /dev/null +++ b/test/struct_derive_test.exs @@ -0,0 +1,22 @@ +defmodule Outstanding.StructDeriveTest do + use ExUnit.Case + use Outstand + + @v0 :value0 + @v1 :value1 + @v2 :value2 + + defmodule ABC do + @derive Outstanding + defstruct [:a, :b, :c] + end + + gen_something_outstanding_test("key outstanding", %ABC{a: @v0, b: @v1}, %ABC{a: @v0, c: @v1}) + gen_something_outstanding_test("value outstanding", %ABC{a: @v0, b: @v1, c: @v2}, %ABC{a: @v1, b: @v1, c: @v2}) + gen_nothing_outstanding_test("realized", %ABC{a: @v0, b: @v1, c: @v2}, %ABC{a: @v0, b: @v1, c: @v2}) + gen_nothing_outstanding_test("realized, no c expectation", %ABC{a: @v0, b: @v1}, %ABC{a: @v0, b: @v1}) + gen_nothing_outstanding_test("realized, nil c expectation", %ABC{a: @v0, b: @v1, c: nil}, %ABC{a: @v0, b: @v1}) + gen_nothing_outstanding_test("realized, extra item", %ABC{a: @v0, b: @v1}, %ABC{a: @v0, b: @v1, c: @v1}) + gen_result_outstanding_test("key result", %ABC{a: @v0, b: @v1}, %ABC{a: @v0, c: @v1}, %ABC{b: @v1}) + gen_result_outstanding_test("value result", %ABC{a: @v0, b: @v1}, %ABC{a: @v1, b: @v1}, %ABC{a: @v0}) +end From 4ec2e5fe6a38765cedc0a89833320642b981dfd7 Mon Sep 17 00:00:00 2001 From: Matt Beanland Date: Mon, 19 May 2025 09:57:26 +1000 Subject: [PATCH 2/2] option except --- README.md | 11 ++++++++++- lib/outstanding.ex | 9 ++++++--- test/struct_derive_except_test.exs | 19 +++++++++++++++++++ 3 files changed, 35 insertions(+), 4 deletions(-) create mode 100644 test/struct_derive_except_test.exs diff --git a/README.md b/README.md index fc0c984..7b5b11d 100644 --- a/README.md +++ b/README.md @@ -209,7 +209,7 @@ iex> Outstanding.outstanding(:no_value, "a") This requires some though as to what it means to 'resolve' your expected type with actual. ## derive for Structs -Outstanding implements the ```__deriving__/2``` callback so you can simply derive an Outstanding implementation when you define your struct. By default this performs outstanding on all fields, and requires the actual struct to be of the same type. +Outstanding implements the ```__deriving__/3``` callback so you can simply derive an Outstanding implementation when you define your struct. By default this performs outstanding on all fields, and requires the actual struct to be of the same type. ```elixir defmodule ABC do @@ -218,6 +218,15 @@ defmodule ABC do end ``` +You can also exclude fields with the ```except``` option + +```elixir + defmodule AB do + @derive {Outstanding, except: [:c]} + defstruct [:a, :b, :c] + end +``` + ## defoutstanding macro More flexibly, the defoutstanding macro can be used to implement outstanding on other types, including your own structs. diff --git a/lib/outstanding.ex b/lib/outstanding.ex index d6d2201..0503b30 100644 --- a/lib/outstanding.ex +++ b/lib/outstanding.ex @@ -19,7 +19,7 @@ defprotocol Outstanding do def outstanding?(expected, actual) @impl true - defmacro __deriving__(module, _options) do + defmacro __deriving__(module, options) do quote do defimpl Outstanding, for: unquote(module) do import Outstand, only: [map_to_struct: 2, outstanding?: 1] @@ -34,12 +34,15 @@ defprotocol Outstanding do {%name{}, %name{}} -> expected |> Map.from_struct() + |> Map.drop(Keyword.get(unquote(options), :except, [])) |> Outstanding.outstanding(Map.from_struct(actual)) |> Outstand.map_to_struct(name) - {_, _} -> - # not an exact match so default to outstanding + {%name{}, _} -> expected + |> Map.from_struct() + |> Map.drop(Keyword.get(unquote(options), :except, [])) + |> Outstand.map_to_struct(name) end end def outstanding?(expected, actual) do diff --git a/test/struct_derive_except_test.exs b/test/struct_derive_except_test.exs new file mode 100644 index 0000000..1eb605f --- /dev/null +++ b/test/struct_derive_except_test.exs @@ -0,0 +1,19 @@ +defmodule Outstanding.StructDeriveExceptTest do + use ExUnit.Case + use Outstand + + @v0 :value0 + @v1 :value1 + + defmodule AB do + @derive {Outstanding, except: [:c]} + defstruct [:a, :b, :c] + end + + gen_something_outstanding_test("key outstanding", %AB{a: @v0, b: @v1}, %AB{a: @v0}) + gen_something_outstanding_test("value outstanding", %AB{a: @v0, b: @v1}, %AB{a: @v1, b: @v1}) + gen_nothing_outstanding_test("realized", %AB{a: @v0, b: @v1}, %AB{a: @v0, b: @v1}) + gen_nothing_outstanding_test("realized, extra item", %AB{a: @v0, b: @v1}, %AB{a: @v0, b: @v1, c: @v1}) + gen_result_outstanding_test("key result", %AB{a: @v0, b: @v1}, %AB{a: @v0}, %AB{b: @v1}) + gen_result_outstanding_test("value result", %AB{a: @v0, b: @v1}, %AB{a: @v1, b: @v1}, %AB{a: @v0}) +end