2023-04-22 16:33:28 +12:00
|
|
|
defmodule AshStateMachineTest do
|
2023-04-22 05:43:36 +12:00
|
|
|
use ExUnit.Case
|
2023-04-22 16:33:28 +12:00
|
|
|
doctest AshStateMachine
|
2023-04-22 05:43:36 +12:00
|
|
|
|
2023-04-23 13:38:33 +12:00
|
|
|
defmodule Order do
|
|
|
|
# leaving out data layer configuration for brevity
|
|
|
|
use Ash.Resource,
|
2024-03-30 11:01:07 +13:00
|
|
|
domain: AshStateMachineTest.Domain,
|
2023-04-23 13:38:33 +12:00
|
|
|
extensions: [AshStateMachine]
|
|
|
|
|
|
|
|
state_machine do
|
|
|
|
initial_states [:pending]
|
|
|
|
default_initial_state :pending
|
|
|
|
|
|
|
|
transitions do
|
|
|
|
transition :confirm, from: :pending, to: :confirmed
|
|
|
|
transition :begin_delivery, from: :confirmed, to: :on_its_way
|
|
|
|
transition :package_arrived, from: :on_its_way, to: :arrived
|
|
|
|
transition :error, from: [:pending, :confirmed, :on_its_way], to: :error
|
2023-09-13 00:40:18 +12:00
|
|
|
transition :abort, from: :*, to: :aborted
|
|
|
|
transition :reroute, from: :*, to: :rerouted
|
2023-04-23 13:38:33 +12:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
actions do
|
2024-03-30 11:01:07 +13:00
|
|
|
default_accept :*
|
2023-04-23 13:38:33 +12:00
|
|
|
# create sets the st
|
|
|
|
defaults [:create, :read]
|
|
|
|
|
|
|
|
update :confirm do
|
|
|
|
# accept [...] you can change other attributes
|
|
|
|
# or do anything else an action can normally do
|
|
|
|
# this transition will be validated according to
|
|
|
|
# the state machine rules above
|
|
|
|
change transition_state(:confirmed)
|
|
|
|
end
|
|
|
|
|
|
|
|
update :begin_delivery do
|
|
|
|
# accept [...]
|
|
|
|
change transition_state(:on_its_way)
|
|
|
|
end
|
|
|
|
|
|
|
|
update :package_arrived do
|
|
|
|
# accept [...]
|
|
|
|
change transition_state(:arrived)
|
|
|
|
end
|
|
|
|
|
|
|
|
update :error do
|
|
|
|
accept [:error_state, :error]
|
|
|
|
change transition_state(:error)
|
|
|
|
end
|
2023-09-13 00:40:18 +12:00
|
|
|
|
|
|
|
update :abort do
|
|
|
|
# accept [...]
|
|
|
|
change transition_state(:aborted)
|
|
|
|
end
|
|
|
|
|
|
|
|
update :reroute do
|
|
|
|
# accept [...]
|
|
|
|
|
|
|
|
# The defined transition for this route contains a `from: :*` but does not include `to: :aborted`
|
|
|
|
# This should never succeed
|
|
|
|
change transition_state(:aborted)
|
|
|
|
end
|
2023-04-23 13:38:33 +12:00
|
|
|
end
|
|
|
|
|
|
|
|
changes do
|
|
|
|
# any failures should be captured and transitioned to the error state
|
2024-03-30 11:22:57 +13:00
|
|
|
change after_transaction(fn
|
|
|
|
changeset, {:ok, result}, _ ->
|
2023-04-23 13:38:33 +12:00
|
|
|
{:ok, result}
|
|
|
|
|
2024-03-30 11:01:07 +13:00
|
|
|
changeset, {:error, error}, _ ->
|
2023-04-23 13:38:33 +12:00
|
|
|
message = Exception.message(error)
|
|
|
|
|
|
|
|
changeset.data
|
|
|
|
|> Ash.Changeset.for_update(:error, %{
|
|
|
|
message: message,
|
|
|
|
error_state: changeset.data.state
|
|
|
|
})
|
|
|
|
end),
|
|
|
|
on: [:update]
|
|
|
|
end
|
|
|
|
|
2023-09-13 00:40:18 +12:00
|
|
|
code_interface do
|
|
|
|
define :abort
|
|
|
|
define :reroute
|
|
|
|
end
|
|
|
|
|
2023-04-23 13:38:33 +12:00
|
|
|
attributes do
|
|
|
|
uuid_primary_key :id
|
|
|
|
# ...attributes like address/delivery options would go here
|
2024-03-30 11:01:07 +13:00
|
|
|
attribute :error, :string, public?: true
|
|
|
|
attribute :error_state, :string, public?: true
|
2023-04-23 13:38:33 +12:00
|
|
|
# :state attribute is added for you by `state_machine`
|
|
|
|
# however, you can add it yourself, and you will be guided by
|
|
|
|
# compile errors on what states need to be allowed by your type.
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2023-04-22 07:25:39 +12:00
|
|
|
defmodule ThreeStates do
|
2023-04-22 05:43:36 +12:00
|
|
|
use Ash.Resource,
|
2024-03-30 11:01:07 +13:00
|
|
|
domain: AshStateMachineTest.Domain,
|
2023-04-22 05:43:36 +12:00
|
|
|
data_layer: Ash.DataLayer.Ets,
|
2023-04-22 16:33:28 +12:00
|
|
|
extensions: [AshStateMachine]
|
2023-04-22 05:43:36 +12:00
|
|
|
|
2023-04-22 16:33:28 +12:00
|
|
|
state_machine do
|
2023-04-23 12:39:29 +12:00
|
|
|
initial_states [:pending]
|
2023-04-22 07:25:39 +12:00
|
|
|
default_initial_state :pending
|
2023-04-22 05:43:36 +12:00
|
|
|
|
2023-04-22 16:33:28 +12:00
|
|
|
transitions do
|
|
|
|
transition(:begin, from: :pending, to: :executing)
|
|
|
|
transition(:complete, from: :executing, to: :complete)
|
2023-08-26 01:37:45 +12:00
|
|
|
transition(:*, from: :*, to: :pending)
|
2023-04-22 07:25:39 +12:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
actions do
|
2024-03-30 11:01:07 +13:00
|
|
|
default_accept :*
|
2023-04-22 07:25:39 +12:00
|
|
|
defaults [:create]
|
|
|
|
|
|
|
|
update :begin do
|
|
|
|
change transition_state(:executing)
|
|
|
|
end
|
|
|
|
|
|
|
|
update :complete do
|
|
|
|
change transition_state(:complete)
|
2023-04-22 05:43:36 +12:00
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
ets do
|
|
|
|
private? true
|
|
|
|
end
|
|
|
|
|
|
|
|
attributes do
|
|
|
|
uuid_primary_key :id
|
|
|
|
end
|
2023-04-22 07:25:39 +12:00
|
|
|
|
|
|
|
code_interface do
|
|
|
|
define :create
|
|
|
|
define :begin
|
|
|
|
define :complete
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
2024-03-30 11:01:07 +13:00
|
|
|
defmodule Domain do
|
|
|
|
use Ash.Domain
|
2023-04-22 07:25:39 +12:00
|
|
|
|
|
|
|
resources do
|
|
|
|
allow_unregistered? true
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
describe "transformers" do
|
2023-08-26 01:37:45 +12:00
|
|
|
test "infers all states, excluding star (:*)" do
|
2023-04-22 16:33:28 +12:00
|
|
|
assert Enum.sort(AshStateMachine.Info.state_machine_all_states(ThreeStates)) ==
|
2023-04-22 07:25:39 +12:00
|
|
|
Enum.sort([:executing, :pending, :complete])
|
|
|
|
end
|
2023-04-22 05:43:36 +12:00
|
|
|
end
|
|
|
|
|
2023-04-22 07:25:39 +12:00
|
|
|
describe "behavior" do
|
|
|
|
test "begins in the appropriate state" do
|
|
|
|
assert ThreeStates.create!().state == :pending
|
|
|
|
end
|
|
|
|
|
|
|
|
test "it transitions to the appropriate state" do
|
2023-04-22 16:33:28 +12:00
|
|
|
state_machine = ThreeStates.create!()
|
2023-04-22 07:25:39 +12:00
|
|
|
|
2023-04-22 16:33:28 +12:00
|
|
|
assert ThreeStates.begin!(state_machine).state == :executing
|
2023-04-22 07:25:39 +12:00
|
|
|
end
|
|
|
|
|
|
|
|
test "it transitions again to the appropriate state" do
|
2023-04-22 16:33:28 +12:00
|
|
|
state_machine = ThreeStates.create!() |> ThreeStates.begin!()
|
2023-04-22 07:25:39 +12:00
|
|
|
|
2023-04-22 16:33:28 +12:00
|
|
|
assert ThreeStates.complete!(state_machine).state == :complete
|
2023-04-22 07:25:39 +12:00
|
|
|
end
|
2023-09-13 00:40:18 +12:00
|
|
|
|
|
|
|
test "`from: :*` can transition from any state" do
|
|
|
|
for state <- [:pending, :confirmed, :on_its_way, :arrived, :error] do
|
|
|
|
assert {:ok, machine} = Order.abort(%Order{state: state})
|
|
|
|
assert machine.state == :aborted
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
test "`from: :*` cannot transition _to_ any state" do
|
|
|
|
for state <- [:pending, :confirmed, :on_its_way, :arrived, :error] do
|
|
|
|
assert {:error, reason} = Order.reroute(%Order{state: state})
|
|
|
|
assert Exception.message(reason) =~ ~r/no matching transition/i
|
|
|
|
end
|
|
|
|
end
|
2023-04-22 05:43:36 +12:00
|
|
|
end
|
2023-04-23 10:50:23 +12:00
|
|
|
|
|
|
|
describe "charts" do
|
|
|
|
test "it generates the appropriate chart" do
|
|
|
|
assert AshStateMachine.Charts.mermaid_flowchart(ThreeStates) ==
|
|
|
|
"""
|
|
|
|
flowchart TD
|
|
|
|
pending --> |begin| executing
|
|
|
|
executing --> |complete| complete
|
2023-09-13 00:40:18 +12:00
|
|
|
complete --> pending
|
|
|
|
executing --> pending
|
|
|
|
pending --> pending
|
2023-04-23 10:50:23 +12:00
|
|
|
"""
|
|
|
|
|> String.trim_trailing()
|
|
|
|
end
|
|
|
|
end
|
2023-09-08 13:35:51 +12:00
|
|
|
|
|
|
|
describe "next state" do
|
|
|
|
defmodule NextStateMachine do
|
|
|
|
@moduledoc false
|
|
|
|
use Ash.Resource,
|
2024-03-30 11:01:07 +13:00
|
|
|
domain: AshStateMachineTest.Domain,
|
2023-09-08 13:35:51 +12:00
|
|
|
extensions: [AshStateMachine]
|
|
|
|
|
|
|
|
state_machine do
|
|
|
|
initial_states [:a]
|
|
|
|
default_initial_state :a
|
|
|
|
|
|
|
|
transitions do
|
|
|
|
transition :next, from: :a, to: :b
|
|
|
|
transition :next, from: :b, to: :c
|
|
|
|
transition :next, from: :b, to: :d
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
attributes do
|
|
|
|
uuid_primary_key :id
|
|
|
|
|
|
|
|
attribute :state, :atom do
|
|
|
|
allow_nil? false
|
2024-03-30 11:01:07 +13:00
|
|
|
public? true
|
2023-09-08 13:35:51 +12:00
|
|
|
constraints one_of: [:a, :b, :c, :d]
|
|
|
|
default :a
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
actions do
|
2024-03-30 11:01:07 +13:00
|
|
|
default_accept :*
|
2023-09-08 13:35:51 +12:00
|
|
|
defaults [:create]
|
|
|
|
|
|
|
|
update :next do
|
|
|
|
change next_state()
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
code_interface do
|
|
|
|
define :create
|
|
|
|
define :next
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
test "when there is only one next state, it transitions into it" do
|
|
|
|
assert {:ok, nsm} = NextStateMachine.create(%{state: :a})
|
|
|
|
assert {:ok, nsm} = NextStateMachine.next(nsm)
|
|
|
|
assert nsm.state == :b
|
|
|
|
end
|
|
|
|
|
|
|
|
test "when there is more than one next state, it makes an oopsie" do
|
|
|
|
assert {:ok, nsm} = NextStateMachine.create(%{state: :b})
|
|
|
|
assert {:error, reason} = NextStateMachine.next(nsm)
|
|
|
|
assert Exception.message(reason) =~ ~r/multiple next states/i
|
|
|
|
end
|
|
|
|
|
|
|
|
test "when there are no next states available, it also makes an oopsie" do
|
|
|
|
assert {:ok, nsm} = NextStateMachine.create(%{state: :c})
|
|
|
|
assert {:error, reason} = NextStateMachine.next(nsm)
|
|
|
|
assert Exception.message(reason) =~ ~r/no next state/i
|
|
|
|
end
|
|
|
|
end
|
2023-09-15 15:15:29 +12:00
|
|
|
|
|
|
|
describe "possible_next_states/1" do
|
|
|
|
test "it correctly returns the next states" do
|
2024-03-30 11:01:07 +13:00
|
|
|
record = ThreeStates.create!()
|
2023-09-15 15:15:29 +12:00
|
|
|
assert [:executing, :pending] = AshStateMachine.possible_next_states(record)
|
|
|
|
end
|
|
|
|
end
|
|
|
|
|
|
|
|
describe "possible_next_states/2" do
|
|
|
|
test "it correctly returns the next states" do
|
2024-03-30 11:01:07 +13:00
|
|
|
record = ThreeStates.create!()
|
2023-09-15 15:15:29 +12:00
|
|
|
assert [:pending] = AshStateMachine.possible_next_states(record, :complete)
|
|
|
|
end
|
|
|
|
end
|
2023-04-22 05:43:36 +12:00
|
|
|
end
|