mirror of
https://github.com/ash-project/ash_state_machine.git
synced 2024-09-19 21:03:33 +12:00
feat: Add next_state
builtin change. (#6)
When there is only a single possible next state that can be transitioned into, we can automatically transition into that state.
This commit is contained in:
parent
4f1b40281d
commit
cf2bef0e38
4 changed files with 107 additions and 2 deletions
|
@ -9,4 +9,9 @@ defmodule AshStateMachine.BuiltinChanges do
|
||||||
def transition_state(target) do
|
def transition_state(target) do
|
||||||
{AshStateMachine.BuiltinChanges.TransitionState, target: target}
|
{AshStateMachine.BuiltinChanges.TransitionState, target: target}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@doc """
|
||||||
|
Try and transition to the next state.
|
||||||
|
"""
|
||||||
|
def next_state, do: AshStateMachine.BuiltinChanges.NextState
|
||||||
end
|
end
|
||||||
|
|
39
lib/builtin_changes/next_state.ex
Normal file
39
lib/builtin_changes/next_state.ex
Normal file
|
@ -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
|
|
@ -2,7 +2,7 @@ defmodule AshStateMachine.Info do
|
||||||
@moduledoc "Introspection helpers for `AshStateMachine`"
|
@moduledoc "Introspection helpers for `AshStateMachine`"
|
||||||
use Spark.InfoGenerator, extension: AshStateMachine, sections: [:state_machine]
|
use Spark.InfoGenerator, extension: AshStateMachine, sections: [:state_machine]
|
||||||
|
|
||||||
@spec state_machine_transitions(Ash.Resource.record() | map(), name :: atom) ::
|
@spec state_machine_transitions(Ash.Resource.t() | map(), name :: atom) ::
|
||||||
list(AshStateMachine.Transition.t())
|
list(AshStateMachine.Transition.t())
|
||||||
def state_machine_transitions(resource_or_dsl, name) do
|
def state_machine_transitions(resource_or_dsl, name) do
|
||||||
resource_or_dsl
|
resource_or_dsl
|
||||||
|
@ -10,7 +10,7 @@ defmodule AshStateMachine.Info do
|
||||||
|> Enum.filter(&(&1.action == :* || &1.action == name))
|
|> Enum.filter(&(&1.action == :* || &1.action == name))
|
||||||
end
|
end
|
||||||
|
|
||||||
@spec state_machine_all_states(Ash.Resource.record() | map()) :: list(atom)
|
@spec state_machine_all_states(Ash.Resource.t() | map()) :: list(atom)
|
||||||
def state_machine_all_states(resource_or_dsl) do
|
def state_machine_all_states(resource_or_dsl) do
|
||||||
Spark.Dsl.Extension.get_persisted(resource_or_dsl, :all_state_machine_states, [])
|
Spark.Dsl.Extension.get_persisted(resource_or_dsl, :all_state_machine_states, [])
|
||||||
end
|
end
|
||||||
|
|
|
@ -166,4 +166,65 @@ defmodule AshStateMachineTest do
|
||||||
|> String.trim_trailing()
|
|> String.trim_trailing()
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|
Loading…
Reference in a new issue