Skip to content

Commit

Permalink
feat: Add next_state builtin change.
Browse files Browse the repository at this point in the history
When there is only a single possible next state that can be transitioned into, we can automatically transition into that state.
  • Loading branch information
jimsynz committed Sep 7, 2023
1 parent 4f1b402 commit 552c21b
Show file tree
Hide file tree
Showing 3 changed files with 105 additions and 0 deletions.
5 changes: 5 additions & 0 deletions lib/builtin_changes/builtin_changes.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,9 @@ defmodule AshStateMachine.BuiltinChanges do
def transition_state(target) do
{AshStateMachine.BuiltinChanges.TransitionState, target: target}
end

@doc """
Try and transition to the next state.
"""
def next_state, do: AshStateMachine.BuiltinChanges.NextState
end
39 changes: 39 additions & 0 deletions lib/builtin_changes/next_state.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
defmodule AshStateMachine.BuiltinChanges.NextState do
@moduledoc """
Given the action and the current state, attempt to find the next state to
transition into.
"""
use Ash.Resource.Change

def change(changeset, _opts, _) do
attribute = AshStateMachine.Info.state_machine_state_attribute!(changeset.resource)

current_state = Map.get(changeset.data, attribute)

changeset.resource
|> AshStateMachine.Info.state_machine_transitions(changeset.action.name)
|> Enum.filter(fn
%{from: from} when is_list(from) -> current_state in from
%{from: from} -> current_state == from
end)
|> Enum.flat_map(fn
%{to: to} when is_list(to) -> to
%{to: to} -> [to]
end)
|> Enum.uniq()
|> Enum.reject(&(&1 == :*))
|> case do
[to] ->
AshStateMachine.transition_state(changeset, to)

[] ->
Ash.Changeset.add_error(changeset, "Cannot determine next state: no next state available")

_ ->
Ash.Changeset.add_error(
changeset,
"Cannot determine next state: multiple next states available"
)
end
end
end
61 changes: 61 additions & 0 deletions test/ash_state_machine_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -166,4 +166,65 @@ defmodule AshStateMachineTest do
|> String.trim_trailing()
end
end

describe "next state" do
defmodule NextStateMachine do
@moduledoc false
use Ash.Resource,
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
constraints one_of: [:a, :b, :c, :d]
default :a
end
end

actions do
defaults [:create]

update :next do
change next_state()
end
end

code_interface do
define_for Api
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
end

0 comments on commit 552c21b

Please sign in to comment.