Ash Outstanding Extension
Mix.install(
[{:ash_outstanding, "~> 0.2.0"}],
consolidate_protocols: false,
config: [
ash: [custom_expressions: [AshOutstanding.Expressions.Outstanding, AshOutstanding.Expressions.IsOutstanding]]
]
)
Overview
If already familiar with the Elixir Outstanding Protocol and Ash Framework in this livebook tutorial you will learn
- Brief Recap of Outstanding Protocol
- Why have an Ash Outstanding Extension?
- How to declare Outstanding on your Ash Resources
- How to use Outstanding with Ash Resources
- How to use Outstanding in Ash Expressions
Outstanding protocol has its own livebook and you should explore this first.
Brief Recap of Outstanding
Outstanding is a protocol for checking whether our expectations have been met or exceeded, and/or seeing which expectations are still outstanding. It compares 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
Why have an Ash Outstanding Extension?
Outstanding is about expectations and actuality and is intentionally not implemented on structs (although it can be done easily). As Ash is used to model your domain declaratively, we want to declare what Outstanding should be concerned about, without having to deal with the details of how. The Ash Outstanding extension allows you to declare which attributes outstanding should be calculated on
How to declare Outstanding on your Ash Resources
First we define a minimal Domain to hold our Ash Resources:
defmodule Domain do
@moduledoc false
use Ash.Domain
resources do
allow_unregistered? true
end
end
Then we define an Ash Resource for Service:
defmodule Service do
use Ash.Resource,
domain: Domain,
data_layer: Ash.DataLayer.Ets,
extensions: [AshOutstanding.Resource]
outstanding do
expect [:state, :status]
end
actions do
default_accept :*
defaults [:read, :update, :destroy, create: :*]
end
attributes do
uuid_primary_key :id, writable?: true
attribute :which, :atom do
public? true
allow_nil? false
default :actual
constraints one_of: [:expected, :actual]
end
attribute :name, :string, public?: true
attribute :state, :atom, public?: true
attribute :status, :atom, public?: true
end
end
Now we create an expected Service instance, holding our generic goals for actual instances of this service. The goal is that the state is active and the status is working.
{:ok, expected} = Service
|> Ash.Changeset.for_create(:create, %{which: :expected, state: :active, status: :working})
|> Ash.create()
Now we create an actual instance with a state of initial.
{:ok, actual} = Service
|> Ash.Changeset.for_create(:create, %{state: :initial})
|> Ash.create()
Now we can run outstanding? and see what happens? Hint: we need to use Outstand to bring in the Outstanding ‘exceeds’ >>> and ‘difference’ — operators
use Outstand
# is anything outstanding?
expected >>> actual
# what is outstanding?
expected --- actual
We can see that that Service struct created by Ash contains meta data and shows nil values where either i) there is nothing outstanding or ii) we have excluded the field from the calculation.
Now imagine you are the orchestrator and have determined and executed the workflow to activate your service, but you don’t know if it is working yet. Update the actual service to activate it:
{:ok, actual} = actual
|> Ash.Changeset.for_update(:update, %{state: :active})
|> Ash.update()
We now recalcuate outstanding, and we find that the state is resolved and the status is outstanding.
expected --- actual
We now ensure the service is working, and mark it as such:
{:ok, actual} = actual
|> Ash.Changeset.for_update(:update, %{status: :working})
|> Ash.update()
Now we recalculate outstanding and we can see that nothing is outstanding:
expected --- actual
How to use Outstanding in Ash Expressions
We saw how we can use Outstanding on Ash Resources from Elixir, but we can also use Outstanding from within an Ash Resource using Ash Expressions.
Firstly we’ve got to configure ash to use our calculations, when you compile your ash dependencies. We did this configuration at the start:
config :ash, :custom_expressions,
[AshOutstanding.Expressions.Outstanding, AshOutstanding.Expressions.IsOutstanding]
We define a Specification resource which uses the is_outstanding
and outstanding
Ash Expressions in calculations to ensure that the expectation on major version is exactly 2.
defmodule Specification do
use Ash.Resource,
domain: Domain,
data_layer: Ash.DataLayer.Ets,
extensions: [AshOutstanding.Resource]
outstanding do
expect [:name, :major_version]
end
actions do
defaults [:read, :destroy, create: :*, update: :*]
end
attributes do
uuid_primary_key :id, writable?: true
attribute :href, :string, public?: true
attribute :name, :string, public?: true
attribute :major_version, :integer, public?: true
attribute :version, :string, public?: true
attribute :category, :union do
constraints types: [
string: [type: :string],
atom: [type: :atom],
function: [type: :function]
]
end
end
calculations do
calculate :is_outstanding_major_version, :boolean, expr(is_outstanding(2, major_version))
calculate :outstanding_major_version, :term, expr(outstanding(2, major_version))
end
end
Now we create a Specification instance and load its calculation:
{:ok, specification} = Specification
|> Ash.Changeset.for_create(:create, %{major_version: 1})
|> Ash.create()
We now load the calculations and can see is_outstanding_major_version: true
and outstanding_major_version: 2
{:ok, specification} = specification
|> Ash.load([:outstanding_major_version, :is_outstanding_major_version])
What Next?
In this tutorial you’ve used the AshOutstanding extension to declare what the Outstanding protocol does on your Ash Resources. Where this gets useful is when resources have expectations on other resources. There is a lot more to Outstanding protocol, including the use of expected functions.
Outstanding is great for managing expectations, particularly when used via a delegation pattern where the details of derived expectations are handled by the delegate.
The Outstanding Protocol is the core of the ‘difference engine’ powering the diffo TMF service and resource management platform, which is powered by Ash.
What is next is up to you…