AshOutstanding
Ash Extension for implementing Outstanding protocol on Ash.Resource and Ash.TypedStruct.
Implementing Outstanding on your Ash Resources and/or TypedStructs is useful when you have an expected / actual twin, and want to establish whether/which expectations are outstanding given actual.
This is a powerful concept, particularly as expectations are declarations of intent, and we want to separate the concerns of whether we've sufficiently met our expectations from how to deal with what is outstanding. Ash itself is highly declarative, creating and exploiting Spark DSL.
This extension employs Spark DSL to allow you to declare how your Ash Resources and TypedStructs should implement Outstanding.
Installation
Add to the deps:
def deps do
[
{:ash_outstanding, "~> 0.2.4"},
]
endTutorial
To get started you need a running instance of Livebook
Usage
Add AshOutstanding.Resource to extensions list within use Ash.Resource options:
defmodule Example.Resource do
use Ash.Resource,
extensions: [AshOutstanding.Resource]
endConfiguration
Generally you will want to configure your Ash Resource so that outstanding?(expected, actual) is true when the essentials of your Ash Resource are satisfied. What this really means will be specific to each resource in your domain.
By defaults this includes public fields which aren't sensitive. This includes attributes, calculations and aggregates.
defmodule Specification.Resource do
use Ash.Resource,
extensions: [AshOutstanding.Resource]
endYou have more control using the expect keyword.
- expect, provide list of Ash Record fields which can have have expectations, or a behaviour configuration map.
Here is an example outstanding dsl section, which configures a Specification resource so that we can set expectations on any or all of the values of keys :name, :major_version and :version while ignoring other fields in the expected/actual resource.
When nil_outstanding?(expected, actual) is true, outstanding(expected, actual) returns nil
When nil_outstanding?(expected, actual) is false, outstanding(expected, actual) returns a struct of your Ash Record with just the unmet expectations.
defmodule Specification.Resource do
use Ash.Resource,
extensions: [AshOutstanding.Resource]
outstanding do
expect [:name, :major_version, :version]
end
endThe behaviour configuration map options are:
- private? - Whenever to expect private fields (defaults false) - Ash.Resource only
- sensitive? - Whenever to pick sensitive fields (defaults false) - Ash.Resource only
- include - Fields to expect. In addition to public?: true && sensitive?: false fields.
- exclude - Fields not to expect.
outstanding do
# Expect only listed fields
expect [:included_field_a, :included_field_b]
# Expect all fields including public?: false and sensitive?: true
expect %{private?: true, sensitive?: true}
# Expect default fields with specific inclusions and exclusions
expect %{include: [:included_private_field], exclude: [:excluded_public_field]}
endUsing Outstanding on your resource
We don't need to set all of the expectations, by default a missing expectation or explict nil means we have no expectation (so nothing will be outstanding)
In the following example we require the access specification to be major version 2. We don't match version as we aren't concerned with minor or trival versions.
use Outstand
expected = %Specification.Resource{name: access, major_version: 2}
actual = %Specification.Resource{name: access, major_version: 1}
expected >>> actual
false
expected --- actual
%Specification.Resource{major_version: 2}If we are happy with either major_version: 1 or 2, we can use a range
use Outstand
expected = %Specification.Resource{name: access, major_version: 1..2}
actual = %Specification.Resource{name: access, major_version: 1}
expected >>> actual
true
expected --- actual
nilOutstanding supports regex, here we use a regex to expect version: is at least v1.1 We don't need an expectation on major version.
use Outstand
expected = %Specification.Resource{name: access, version: ~r/v1.1/}
actual = %Specification.Resource{name: access, version: v1.1.17}
expected >>> actual
true
expected --- actual
nilOutstanding supports expected functions. Arity 1 and 2 expected functions use actual as a parameter. Arity 2 expected functions take an argument list, and this can be a list of 'prototype' expected resources used in an Outstand or your own expected function.
You may need to ensure your expected/actual Ash Resources are appropriately loaded, depending on what key/values you expect.
The meta field is nil expectation.
Customize
A dsl option is provided which will insert a custom arity 3 function into the outstanding pipeline.
Below customize is used to ensure that outstanding is enriched with expected.id
defmodule Specification.Resource do
use Ash.Resource,
extensions: [AshOutstanding.Resource]
outstanding do
expect [:name, :major_version, :version]
customize fn outstanding, expected, _actual ->
case outstanding do
nil ->
outstanding
%_{} ->
outstanding
|> Map.put(:id, expected.id)
end
end
end
endUsing Outstanding in Ash Expressions
Ash Expressions can call outstanding(expected, actual) and is_outstanding(expected, actual) via custom expressions. This is particularly useful when combined with relationships, as a supervising resource can manage its expectations of a supervised resource.
AshOutstanding includes the Outstanding and IsOutstanding custom expressions for the Ash.DataLayer.Simple, Ash.DataLayer.ETS, AshCsv.DataLayer and AshNeo4j.DataLayer
To make these available add to your config.exs:
config :ash, :custom_expressions, [AshOutstanding.Expressions.Outstanding, AshOutstanding.Expressions.IsOutstanding]Typed Structs
AshOutstanding includes support for Ash.TypedStruct. The DSL for TypedStruct is identical to Resource, except that 'expect' is simpler since TypedStruct fields cannot be private or sensitive.
defmodule Specification.TypedStruct do
use Ash.TypedStruct,
extensions: [AshOutstanding.TypedStruct]
outstanding do
expect [:name, :major_version, :version]
end
endAcknowledgements
Thanks to Dmitry Maganov for ash_jason which was an exemplar.
Kudos to the Ash Core for ash 🚀
Links
Diffo.devOutstanding protocol docs
[Outstanding protocol livebook] (https://livebook.dev/run/?url=https%3A%2F%2Fgithub.com%2Fdiffo-dev%2Foutstanding%2Fblob%2Fdev%2Foutstanding.livemd)