ash_state_machine/lib/ash_state_machine.ex

161 lines
4.3 KiB
Elixir
Raw Normal View History

defmodule AshStateMachine do
defmodule Transition do
2023-04-22 05:43:36 +12:00
@moduledoc """
The configuration for an transition.
2023-04-22 05:43:36 +12:00
"""
2023-04-22 07:25:39 +12:00
@type t :: %__MODULE__{
action: atom,
from: [atom],
to: [atom]
}
2023-04-22 05:43:36 +12:00
defstruct [:action, :from, :to]
end
@transition %Spark.Dsl.Entity{
name: :transition,
target: Transition,
2023-04-22 05:43:36 +12:00
args: [:action],
schema: [
action: [
type: :atom,
2023-04-23 10:28:19 +12:00
doc:
"The corresponding action that is invoked for the transition. Use `:*` to allow any update action to perform this transition."
2023-04-22 05:43:36 +12:00
],
from: [
type: {:or, [{:list, :atom}, :atom]},
2023-04-23 12:12:11 +12:00
required: true,
2023-04-22 05:43:36 +12:00
doc:
"The states in which this action may be called. If not specified, then any state is accepted."
],
to: [
type: {:or, [{:list, :atom}, :atom]},
2023-04-23 12:12:11 +12:00
required: true,
2023-04-22 05:43:36 +12:00
doc:
"The states that this action may move to. If not specified, then any state is accepted."
]
]
}
@transitions %Spark.Dsl.Section{
name: :transitions,
2023-04-22 05:43:36 +12:00
entities: [
@transition
2023-04-22 05:43:36 +12:00
]
}
@state_machine %Spark.Dsl.Section{
name: :state_machine,
2023-04-22 05:43:36 +12:00
schema: [
2023-04-22 07:25:39 +12:00
deprecated_states: [
type: {:list, :atom},
default: [],
2023-04-22 07:25:39 +12:00
doc: """
A list of states that have been deprecated.
The list of states is derived from the transitions normally.
2023-04-22 07:25:39 +12:00
Use this option to express that certain types should still
be included even though no transitions go to/from that state anymore.
2023-04-22 07:25:39 +12:00
"""
],
2023-04-22 05:43:36 +12:00
state_attribute: [
type: :atom,
doc: "The attribute to store the state in.",
default: :state
],
initial_states: [
2023-04-23 12:39:29 +12:00
type: {:list, :atom},
required: true,
doc: "The allowed starting states of this state machine."
2023-04-22 07:25:39 +12:00
],
default_initial_state: [
type: :atom,
doc: "The default initial state"
2023-04-22 05:43:36 +12:00
]
],
sections: [
@transitions
2023-04-22 05:43:36 +12:00
]
}
2023-04-23 18:46:09 +12:00
@sections [@state_machine]
@moduledoc """
Functions for working with AshStateMachine.
2023-04-23 18:46:09 +12:00
<!--- ash-hq-hide-start --> <!--- -->
## DSL Documentation
### Index
#{Spark.Dsl.Extension.doc_index(@sections)}
### Docs
#{Spark.Dsl.Extension.doc(@sections)}
<!--- ash-hq-hide-stop --> <!--- -->
"""
2023-04-22 07:25:39 +12:00
use Spark.Dsl.Extension,
2023-04-23 18:46:09 +12:00
sections: @sections,
2023-04-22 07:25:39 +12:00
transformers: [
AshStateMachine.Transformers.FillInTransitionDefaults,
AshStateMachine.Transformers.AddState,
AshStateMachine.Transformers.EnsureStateSelected
2023-04-22 07:25:39 +12:00
],
verifiers: [
AshStateMachine.Verifiers.VerifyTransitionActions,
AshStateMachine.Verifiers.VerifyDefaultInitialState
2023-04-22 07:25:39 +12:00
],
imports: [
AshStateMachine.BuiltinChanges
2023-04-22 07:25:39 +12:00
]
@doc """
A utility to transition the state of a changeset, honoring the rules of the resource.
"""
2023-04-22 07:25:39 +12:00
def transition_state(%{action_type: :update} = changeset, target) do
transitions =
AshStateMachine.Info.state_machine_transitions(changeset.resource, changeset.action.name)
attribute = AshStateMachine.Info.state_machine_state_attribute!(changeset.resource)
2023-04-22 07:25:39 +12:00
old_state = Map.get(changeset.data, attribute)
case Enum.find(transitions, fn transition ->
old_state in List.wrap(transition.from) and target in List.wrap(transition.to)
2023-04-22 07:25:39 +12:00
end) do
nil ->
Ash.Changeset.add_error(
changeset,
AshStateMachine.Errors.NoMatchingTransition.exception(
old_state: old_state,
2023-04-22 07:25:39 +12:00
target: target,
action: changeset.action.name
)
)
_transition ->
2023-04-22 07:25:39 +12:00
Ash.Changeset.force_change_attribute(changeset, attribute, target)
end
end
def transition_state(%{action_type: :create} = changeset, target) do
attribute = AshStateMachine.Info.state_machine_state_attribute!(changeset.resource)
if target in AshStateMachine.Info.state_machine_initial_states!(changeset.resource) do
Ash.Changeset.force_change_attribute(changeset, attribute, target)
else
Ash.Changeset.add_error(
changeset,
AshStateMachine.Errors.InvalidInitialState.exception(
target: target,
action: changeset.action.name
)
)
end
end
def transition_state(other, _target) do
Ash.Changeset.add_error(other, "Can't transition states on destroy actions")
end
2023-04-22 05:43:36 +12:00
end