Skip to content

Commit

Permalink
enforce valid upsert transition targets at runtime
Browse files Browse the repository at this point in the history
  • Loading branch information
enoonan committed Nov 25, 2024
1 parent 6411657 commit 5fe0325
Show file tree
Hide file tree
Showing 3 changed files with 52 additions and 17 deletions.
56 changes: 39 additions & 17 deletions lib/ash_state_machine.ex
Original file line number Diff line number Diff line change
Expand Up @@ -137,27 +137,45 @@ defmodule AshStateMachine do
end
end

def transition_state(%{action_type: :create, action: %{upsert?: true}} = changeset, target) do
attribute = AshStateMachine.Info.state_machine_state_attribute!(changeset.resource)
old_state = Map.get(changeset.data, attribute)

cond do
target not in AshStateMachine.Info.state_machine_initial_states!(changeset.resource) ->
invalid_initial_state(changeset, target)

target not in available_upsert_targets(changeset) -> no_matching_transition(changeset, target, old_state)

true -> 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
changeset
|> Ash.Changeset.set_context(%{state_machine: %{attempted_change: target}})
|> Ash.Changeset.add_error(
AshStateMachine.Errors.InvalidInitialState.exception(
target: target,
action: changeset.action.name
)
)
invalid_initial_state(changeset, target)
end
end

def transition_state(other, _target) do
Ash.Changeset.add_error(other, "Can't transition states on destroy actions")
end

defp invalid_initial_state(changeset, target) do
changeset
|> Ash.Changeset.set_context(%{state_machine: %{attempted_change: target}})
|> Ash.Changeset.add_error(
AshStateMachine.Errors.InvalidInitialState.exception(
target: target,
action: changeset.action.name
)
)
end

defp find_and_perform_transition(changeset, old_state, attribute, target) do
changeset.resource
|> AshStateMachine.Info.state_machine_transitions(changeset.action.name)
Expand All @@ -166,15 +184,7 @@ defmodule AshStateMachine do
end)
|> case do
nil ->
changeset
|> Ash.Changeset.set_context(%{state_machine: %{attempted_change: target}})
|> Ash.Changeset.add_error(
AshStateMachine.Errors.NoMatchingTransition.exception(
old_state: old_state,
target: target,
action: changeset.action.name
)
)
no_matching_transition(changeset, target, old_state)

_transition ->
Ash.Changeset.force_change_attribute(changeset, attribute, target)
Expand All @@ -197,6 +207,11 @@ defmodule AshStateMachine do
To remediate this, add the `extra_states` option and include the state #{inspect(target)}
""")

no_matching_transition(changeset, target, old_state)
end

@doc false
defp no_matching_transition(changeset, target, old_state) do
changeset
|> Ash.Changeset.set_context(%{state_machine: %{attempted_change: target}})
|> Ash.Changeset.add_error(
Expand Down Expand Up @@ -243,4 +258,11 @@ defmodule AshStateMachine do
|> Enum.reject(&(&1 == :*))
|> Enum.uniq()
end

defp available_upsert_targets(changeset) do
AshStateMachine.Info.state_machine_transitions(changeset.resource, changeset.action.name)
|> Enum.map(& &1.to)
|> List.flatten()
|> Enum.uniq()
end
end
6 changes: 6 additions & 0 deletions test/ash_state_machine_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ defmodule AshStateMachineTest do
assert Verification.reset!(%{id: state_machine.id}).state == :pending
end

test "create upsert? actions do not allow invalid states" do
state_machine = Verification.create!() |> Verification.begin!()
assert {:error, reason} = Verification.broken_upsert(%{id: state_machine.id})
assert Exception.message(reason) =~ ~r/no matching transition/i
end

test "create actions without `upsert? true` do not compile" do
assert_raise Spark.Error.DslError, ~r/non-upsert create action/, fn ->
defmodule CreateWithoutUpsert do
Expand Down
7 changes: 7 additions & 0 deletions test/support/verification.ex
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ defmodule Verification do
transitions do
transition(:begin, from: :pending, to: :executing)
transition(:reset, from: :*, to: :pending)
transition(:broken_upsert, from: :*, to: [:foo, :bar])
end
end

Expand All @@ -26,6 +27,11 @@ defmodule Verification do
upsert? true
change transition_state(:pending)
end

create :broken_upsert do
upsert? true
change transition_state(:pending)
end
end

attributes do
Expand All @@ -36,5 +42,6 @@ defmodule Verification do
define :create
define :begin
define :reset
define :broken_upsert
end
end

0 comments on commit 5fe0325

Please sign in to comment.