chore: fsm -> state_machine, event -> transition

This commit is contained in:
Zach Daniel 2023-04-21 22:33:28 -06:00
parent 6d0fa817a2
commit 06871a9977
15 changed files with 120 additions and 112 deletions

View file

@ -1,8 +1,8 @@
spark_locals_without_parens = [
default_initial_state: 1,
deprecated_states: 1,
event: 1,
event: 2,
transition: 1,
transition: 2,
from: 1,
initial_states: 1,
state_attribute: 1,

2
.gitignore vendored
View file

@ -20,7 +20,7 @@ erl_crash.dump
*.ez
# Ignore package tarball (built via "mix hex.build").
ash_fsm-*.tar
ash_state_machine-*.tar
# Temporary files, for example, from tests.
/tmp/

View file

@ -1,21 +1,21 @@
# AshFsm
# AshStateMachine
**TODO: Add description**
## Installation
If [available in Hex](https://hex.pm/docs/publish), the package can be installed
by adding `ash_fsm` to your list of dependencies in `mix.exs`:
by adding `ash_state_machine` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:ash_fsm, "~> 0.1.0"}
{:ash_state_machine, "~> 0.1.0"}
]
end
```
Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
be found at <https://hexdocs.pm/ash_fsm>.
be found at <https://hexdocs.pm/ash_state_machine>.

View file

@ -1,11 +1,11 @@
defmodule AshFsm do
defmodule AshStateMachine do
@moduledoc """
Documentation for `AshFsm`.
Documentation for `AshStateMachine`.
"""
defmodule Event do
defmodule Transition do
@moduledoc """
The configuration for an event.
The configuration for an transition.
"""
@type t :: %__MODULE__{
action: atom,
@ -16,15 +16,15 @@ defmodule AshFsm do
defstruct [:action, :from, :to]
end
@event %Spark.Dsl.Entity{
name: :event,
target: Event,
@transition %Spark.Dsl.Entity{
name: :transition,
target: Transition,
args: [:action],
identifier: :action,
schema: [
action: [
type: :atom,
doc: "The corresponding action that is invoked for the event."
doc: "The corresponding action that is invoked for the transition."
],
from: [
type: {:or, [{:list, :atom}, :atom]},
@ -39,23 +39,23 @@ defmodule AshFsm do
]
}
@events %Spark.Dsl.Section{
name: :events,
@transitions %Spark.Dsl.Section{
name: :transitions,
entities: [
@event
@transition
]
}
@fsm %Spark.Dsl.Section{
name: :fsm,
@state_machine %Spark.Dsl.Section{
name: :state_machine,
schema: [
deprecated_states: [
type: {:list, :atom},
doc: """
A list of states that have been deprecated.
The list of states is derived from the events normally.
The list of states is derived from the transitions normally.
Use this option to express that certain types should still
be included even though no events go to/from that state anymore.
be included even though no transitions go to/from that state anymore.
"""
],
state_attribute: [
@ -74,44 +74,46 @@ defmodule AshFsm do
]
],
sections: [
@events
@transitions
]
}
use Spark.Dsl.Extension,
sections: [@fsm],
sections: [@state_machine],
transformers: [
AshFsm.Transformers.FillInEventDefaults,
AshFsm.Transformers.AddState,
AshFsm.Transformers.EnsureStateSelected
AshStateMachine.Transformers.FillInTransitionDefaults,
AshStateMachine.Transformers.AddState,
AshStateMachine.Transformers.EnsureStateSelected
],
verifiers: [
AshFsm.Verifiers.VerifyEventActions,
AshFsm.Verifiers.VerifyDefaultInitialState
AshStateMachine.Verifiers.VerifyTransitionActions,
AshStateMachine.Verifiers.VerifyDefaultInitialState
],
imports: [
AshFsm.BuiltinChanges
AshStateMachine.BuiltinChanges
]
def transition_state(%{action_type: :update} = changeset, target) do
events = AshFsm.Info.fsm_events(changeset.resource, changeset.action.name)
attribute = AshFsm.Info.fsm_state_attribute!(changeset.resource)
transitions =
AshStateMachine.Info.state_machine_transitions(changeset.resource, changeset.action.name)
attribute = AshStateMachine.Info.state_machine_state_attribute!(changeset.resource)
old_state = Map.get(changeset.data, attribute)
case Enum.find(events, fn event ->
old_state in List.wrap(event.from) and target in List.wrap(event.to)
case Enum.find(transitions, fn transition ->
old_state in List.wrap(transition.from) and target in List.wrap(transition.to)
end) do
nil ->
Ash.Changeset.add_error(
changeset,
AshFsm.Errors.NoMatchingEvent.exception(
AshStateMachine.Errors.NoMatchingTransition.exception(
from: old_state,
target: target,
action: changeset.action.name
)
)
_event ->
_transition ->
Ash.Changeset.force_change_attribute(changeset, attribute, target)
end
end

View file

@ -1,12 +1,12 @@
defmodule AshFsm.BuiltinChanges do
defmodule AshStateMachine.BuiltinChanges do
@moduledoc """
Changes for working with AshFsm resources.
Changes for working with AshStateMachine resources.
"""
@doc """
Changes the state to the target state, validating the transition
"""
def transition_state(target) do
{AshFsm.BuiltinChanges.TransitionState, target: target}
{AshStateMachine.BuiltinChanges.TransitionState, target: target}
end
end

View file

@ -1,7 +1,7 @@
defmodule AshFsm.BuiltinChanges.TransitionState do
defmodule AshStateMachine.BuiltinChanges.TransitionState do
use Ash.Resource.Change
def change(changeset, opts, _) do
AshFsm.transition_state(changeset, opts[:target])
AshStateMachine.transition_state(changeset, opts[:target])
end
end

View file

@ -1,5 +1,5 @@
defmodule AshFsm.Errors.NoMatchingEvent do
@moduledoc "Used when a state change occurs in an action with no matching event"
defmodule AshStateMachine.Errors.NoMatchingTransition do
@moduledoc "Used when a state change occurs in an action with no matching transition"
use Ash.Error.Exception
def_ash_error([:action, :target, :old_state], class: :invalid)
@ -7,11 +7,11 @@ defmodule AshFsm.Errors.NoMatchingEvent do
defimpl Ash.ErrorKind do
def id(_), do: Ash.UUID.generate()
def code(_), do: "no_matching_event"
def code(_), do: "no_matching_transition"
def message(error) do
"""
Attempted to change state from #{error.old_state} to #{error.target} in action #{error.action}, but no matching event was configured.
Attempted to change state from #{error.old_state} to #{error.target} in action #{error.action}, but no matching transition was configured.
"""
end
end

View file

@ -1,15 +1,16 @@
defmodule AshFsm.Info do
use Spark.InfoGenerator, extension: AshFsm, sections: [:fsm]
defmodule AshStateMachine.Info do
use Spark.InfoGenerator, extension: AshStateMachine, sections: [:state_machine]
@spec fsm_events(Ash.Resource.record() | map(), name :: atom) :: list(AshFsm.Event.t())
def fsm_events(resource_or_dsl, name) do
@spec state_machine_transitions(Ash.Resource.record() | map(), name :: atom) ::
list(AshStateMachine.Transition.t())
def state_machine_transitions(resource_or_dsl, name) do
resource_or_dsl
|> fsm_events()
|> state_machine_transitions()
|> Enum.filter(&(&1.action == name))
end
@spec fsm_all_states(Ash.Resource.record() | map()) :: list(atom)
def fsm_all_states(resource_or_dsl) do
Spark.Dsl.Extension.get_persisted(resource_or_dsl, :all_fsm_states, [])
@spec state_machine_all_states(Ash.Resource.record() | map()) :: list(atom)
def state_machine_all_states(resource_or_dsl) do
Spark.Dsl.Extension.get_persisted(resource_or_dsl, :all_state_machine_states, [])
end
end

View file

@ -1,4 +1,4 @@
defmodule AshFsm.Transformers.AddState do
defmodule AshStateMachine.Transformers.AddState do
use Spark.Dsl.Transformer
alias Spark.Dsl.Transformer
@ -6,19 +6,21 @@ defmodule AshFsm.Transformers.AddState do
def transform(dsl_state) do
deprecated_states =
case AshFsm.Info.fsm_deprecated_states(dsl_state) do
case AshStateMachine.Info.state_machine_deprecated_states(dsl_state) do
{:ok, value} -> value || []
_ -> []
end
all_states = Enum.uniq(AshFsm.Info.fsm_all_states(dsl_state) ++ deprecated_states)
attribute_name = AshFsm.Info.fsm_state_attribute!(dsl_state)
all_states =
Enum.uniq(AshStateMachine.Info.state_machine_all_states(dsl_state) ++ deprecated_states)
attribute_name = AshStateMachine.Info.state_machine_state_attribute!(dsl_state)
module = Transformer.get_persisted(dsl_state, :module)
case Ash.Resource.Info.attribute(dsl_state, attribute_name) do
nil ->
default =
case AshFsm.Info.fsm_default_initial_state(dsl_state) do
case AshStateMachine.Info.state_machine_default_initial_state(dsl_state) do
{:ok, value} ->
value
@ -42,7 +44,7 @@ defmodule AshFsm.Transformers.AddState do
{attribute.type, attribute.constraints}
end
case AshFsm.Info.fsm_default_initial_state(dsl_state) do
case AshStateMachine.Info.state_machine_default_initial_state(dsl_state) do
{:ok, default} ->
if attribute.default != default do
raise Spark.Error.DslError,

View file

@ -1,4 +1,4 @@
defmodule AshFsm.Transformers.EnsureStateSelected do
defmodule AshStateMachine.Transformers.EnsureStateSelected do
use Spark.Dsl.Transformer
def transform(dsl_state) do
@ -6,7 +6,7 @@ defmodule AshFsm.Transformers.EnsureStateSelected do
dsl_state,
{Ash.Resource.Preparation.Build,
ensure_selected: [
AshFsm.Info.fsm_state_attribute(dsl_state)
AshStateMachine.Info.state_machine_state_attribute(dsl_state)
]}
)
end

View file

@ -1,11 +1,11 @@
defmodule AshFsm.Transformers.FillInEventDefaults do
defmodule AshStateMachine.Transformers.FillInTransitionDefaults do
use Spark.Dsl.Transformer
alias Spark.Dsl.Transformer
@moduledoc false
def transform(dsl_state) do
initial_states =
case AshFsm.Info.fsm_initial_states(dsl_state) do
case AshStateMachine.Info.state_machine_initial_states(dsl_state) do
{:ok, value} -> List.wrap(value)
_ -> []
end
@ -13,7 +13,7 @@ defmodule AshFsm.Transformers.FillInEventDefaults do
initial_states =
case initial_states do
[] ->
case AshFsm.Info.fsm_default_initial_state(dsl_state) do
case AshStateMachine.Info.state_machine_default_initial_state(dsl_state) do
{:ok, value} when not is_nil(value) ->
[value]
@ -25,36 +25,36 @@ defmodule AshFsm.Transformers.FillInEventDefaults do
initial_states
end
events =
transitions =
dsl_state
|> AshFsm.Info.fsm_events()
|> AshStateMachine.Info.state_machine_transitions()
all_states =
events
|> Enum.flat_map(fn event ->
List.wrap(event.from) ++ List.wrap(event.to)
transitions
|> Enum.flat_map(fn transition ->
List.wrap(transition.from) ++ List.wrap(transition.to)
end)
|> Enum.concat(List.wrap(initial_states))
|> Enum.uniq()
dsl_state =
case AshFsm.Info.fsm_initial_states(dsl_state) do
case AshStateMachine.Info.state_machine_initial_states(dsl_state) do
{:ok, value} when not is_nil(value) and value != [] ->
dsl_state
_ ->
Transformer.set_option(dsl_state, [:fsm], :initial_states, all_states)
Transformer.set_option(dsl_state, [:state_machine], :initial_states, all_states)
end
{:ok,
events
|> Enum.reduce(dsl_state, fn event, dsl_state ->
Transformer.replace_entity(dsl_state, [:fsm], %{
event
| from: event.from || all_states,
to: event.to || all_states
transitions
|> Enum.reduce(dsl_state, fn transition, dsl_state ->
Transformer.replace_entity(dsl_state, [:state_machine], %{
transition
| from: transition.from || all_states,
to: transition.to || all_states
})
end)
|> Transformer.persist(:all_fsm_states, all_states)}
|> Transformer.persist(:all_state_machine_states, all_states)}
end
end

View file

@ -1,15 +1,18 @@
defmodule AshFsm.Verifiers.VerifyDefaultInitialState do
defmodule AshStateMachine.Verifiers.VerifyDefaultInitialState do
use Spark.Dsl.Verifier
def verify(dsl_state) do
module = Spark.Dsl.Verifier.get_persisted(dsl_state, :module)
attribute =
Ash.Resource.Info.attribute(dsl_state, AshFsm.Info.fsm_state_attribute!(dsl_state))
Ash.Resource.Info.attribute(
dsl_state,
AshStateMachine.Info.state_machine_state_attribute!(dsl_state)
)
case AshFsm.Info.fsm_default_initial_state(dsl_state) do
case AshStateMachine.Info.state_machine_default_initial_state(dsl_state) do
{:ok, initial} when not is_nil(initial) ->
initial_states = AshFsm.Info.fsm_initial_states!(dsl_state)
initial_states = AshStateMachine.Info.state_machine_initial_states!(dsl_state)
unless initial in initial_states do
raise Spark.Error.DslError,

View file

@ -1,18 +1,18 @@
defmodule AshFsm.Verifiers.VerifyEventActions do
defmodule AshStateMachine.Verifiers.VerifyTransitionActions do
use Spark.Dsl.Verifier
def verify(dsl_state) do
dsl_state
|> AshFsm.Info.fsm_events()
|> Enum.each(fn event ->
action = Ash.Resource.Info.action(dsl_state, event.action)
|> AshStateMachine.Info.state_machine_transitions()
|> Enum.each(fn transition ->
action = Ash.Resource.Info.action(dsl_state, transition.action)
unless action && action.type == :update do
raise Spark.Error.DslError,
module: Spark.Dsl.Verifier.get_persisted(dsl_state, :module),
path: [:fsm, :events, :event, event.action],
path: [:state_machine, :transitions, :transition, transition.action],
message: """
Event configured with action `:#{event.action}` but no such update action is defined.
Transition configured with action `:#{transition.action}` but no such update action is defined.
"""
end
end)

22
mix.exs
View file

@ -1,4 +1,4 @@
defmodule AshFsm.MixProject do
defmodule AshStateMachine.MixProject do
use Mix.Project
@version "0.1.0"
@ -9,7 +9,7 @@ defmodule AshFsm.MixProject do
def project do
[
app: :ash_fsm,
app: :ash_state_machine,
version: @version,
elixir: "~> 1.14",
start_permanent: Mix.env() == :prod,
@ -20,19 +20,19 @@ defmodule AshFsm.MixProject do
dialyzer: [plt_add_apps: [:ash]],
docs: docs(),
description: @description,
source_url: "https://github.com/ash-project/ash_fsm",
homepage_url: "https://github.com/ash-project/ash_fsm"
source_url: "https://github.com/ash-project/ash_state_machine",
homepage_url: "https://github.com/ash-project/ash_state_machine"
]
end
defp package do
[
name: :ash_fsm,
name: :ash_state_machine,
licenses: ["MIT"],
files: ~w(lib .formatter.exs mix.exs README* LICENSE*
CHANGELOG* documentation),
links: %{
GitHub: "https://github.com/ash-project/ash_fsm"
GitHub: "https://github.com/ash-project/ash_state_machine"
}
]
end
@ -87,17 +87,17 @@ defmodule AshFsm.MixProject do
defp docs do
[
main: "AshFsm",
main: "AshStateMachine",
source_ref: "v#{@version}",
logo: "logos/small-logo.png",
extra_section: "GUIDES",
spark: [
extensions: [
%{
module: AshFsm,
name: "AshFsm",
module: AshStateMachine,
name: "AshStateMachine",
target: "Ash.Resource",
type: "Fsm Resource"
type: "StateMachine Resource"
},
%{
module: AshGraphql.Api,
@ -147,7 +147,7 @@ defmodule AshFsm.MixProject do
sobelow: "sobelow --skip",
credo: "credo --strict",
docs: ["docs", "ash.replace_doc_links"],
"spark.formatter": "spark.formatter --extensions AshFsm"
"spark.formatter": "spark.formatter --extensions AshStateMachine"
]
end
end

View file

@ -1,18 +1,18 @@
defmodule AshFsmTest do
defmodule AshStateMachineTest do
use ExUnit.Case
doctest AshFsm
doctest AshStateMachine
defmodule ThreeStates do
use Ash.Resource,
data_layer: Ash.DataLayer.Ets,
extensions: [AshFsm]
extensions: [AshStateMachine]
fsm do
state_machine do
default_initial_state :pending
events do
event :begin, from: :pending, to: :executing
event :complete, from: :executing, to: :complete
transitions do
transition(:begin, from: :pending, to: :executing)
transition(:complete, from: :executing, to: :complete)
end
end
@ -37,7 +37,7 @@ defmodule AshFsmTest do
end
code_interface do
define_for AshFsmTest.Api
define_for AshStateMachineTest.Api
define :create
define :begin
define :complete
@ -54,7 +54,7 @@ defmodule AshFsmTest do
describe "transformers" do
test "infers all states" do
assert Enum.sort(AshFsm.Info.fsm_all_states(ThreeStates)) ==
assert Enum.sort(AshStateMachine.Info.state_machine_all_states(ThreeStates)) ==
Enum.sort([:executing, :pending, :complete])
end
end
@ -65,15 +65,15 @@ defmodule AshFsmTest do
end
test "it transitions to the appropriate state" do
fsm = ThreeStates.create!()
state_machine = ThreeStates.create!()
assert ThreeStates.begin!(fsm).state == :executing
assert ThreeStates.begin!(state_machine).state == :executing
end
test "it transitions again to the appropriate state" do
fsm = ThreeStates.create!() |> ThreeStates.begin!()
state_machine = ThreeStates.create!() |> ThreeStates.begin!()
assert ThreeStates.complete!(fsm).state == :complete
assert ThreeStates.complete!(state_machine).state == :complete
end
end
end