Skip to content

Commit

Permalink
Adding first version of before and after callbacks
Browse files Browse the repository at this point in the history
This commit adds the initial tests and implementation for before and after
callbacks. It follows a similar implementation to the guard fucntions,
relying on rescuing the specific exceptions.
  • Loading branch information
joaomdmoura committed Dec 12, 2017
1 parent b1a7759 commit c548f6c
Show file tree
Hide file tree
Showing 4 changed files with 91 additions and 18 deletions.
5 changes: 4 additions & 1 deletion lib/machinery.ex
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,10 @@ defmodule Machinery do
{:error, @guarded_error}

true ->
struct = Map.put(struct, :state, next_state)
struct = struct
|> Transition.run_before_callbacks(next_state, module)
|> Map.put(:state, next_state)
|> Transition.run_after_callbacks(next_state, module)
{:ok, struct}
end
end
Expand Down
45 changes: 34 additions & 11 deletions lib/machinery/transition.ex
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,19 @@ defmodule Machinery.Transition do
It's meant to be for internal use only.
"""

@doc """
Function responsible for checking if the transition from a state to another
was specifically declared.
"""
@spec declared_transition?(list, atom, atom) :: boolean
def declared_transition?(transitions, current_state, next_state) do
case Map.fetch(transitions, current_state) do
{:ok, [_|_] = declared_states} -> Enum.member?(declared_states, next_state)
{:ok, declared_state} -> declared_state == next_state
:error -> false
end
end

@doc """
Default guard transition fallback to make sure all transitions are permitted
unless another existing guard condition exists.
Expand All @@ -17,16 +30,25 @@ defmodule Machinery.Transition do
error in FunctionClauseError -> guard_transition_fallback?(error)
end

@doc """
Function responsible for checking if the transition from a state to another
was specifically declared.
"""
@spec declared_transition?(list, atom, atom) :: boolean
def declared_transition?(transitions, current_state, next_state) do
case Map.fetch(transitions, current_state) do
{:ok, [_|_] = declared_states} -> Enum.member?(declared_states, next_state)
{:ok, declared_state} -> declared_state == next_state
:error -> false
def run_before_callbacks(struct, state, module) do
module.before_transition(struct, state)
rescue
error in UndefinedFunctionError -> callbacks_fallback(struct, error)
error in FunctionClauseError -> callbacks_fallback(struct, error)
end

def run_after_callbacks(struct, state, module) do
module.after_transition(struct, state)
rescue
error in UndefinedFunctionError -> callbacks_fallback(struct, error)
error in FunctionClauseError -> callbacks_fallback(struct, error)
end

defp callbacks_fallback(struct, error) do
if error.function in [:after_transition, :before_transition] && error.arity == 2 do
struct
else
raise error
end
end

Expand All @@ -40,4 +62,5 @@ defmodule Machinery.Transition do
raise error
end
end
end
end

16 changes: 16 additions & 0 deletions test/machinery/transition_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
defmodule MachineryTest.TransitionTest do
use ExUnit.Case
doctest Machinery.Transition
alias Machinery.Transition

test "declared_transition?/3 based on a map of transitions, current and next state" do
transitions = %{
created: [:partial, :completed],
partial: :completed
}
assert Transition.declared_transition?(transitions, :created, :partial)
assert Transition.declared_transition?(transitions, :created, :completed)
assert Transition.declared_transition?(transitions, :partial, :completed)
refute Transition.declared_transition?(transitions, :partial, :created)
end
end
43 changes: 37 additions & 6 deletions test/machinery_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,8 @@ defmodule MachineryTest do
}

def guard_transition(struct, :completed) do

# Code to unquote code into this AST that
# will force and exception.
# Code to simulate and force an exception inside a
# guard function.
if Map.get(struct, :force_exception) do
Machinery.non_existing_function_should_raise_error()
end
Expand All @@ -28,14 +27,28 @@ defmodule MachineryTest do
end

defmodule TestModule do
defstruct state: nil, missing_fields: nil
defstruct state: nil, missing_fields: nil, force_exception: false

use Machinery,
states: [:created, :partial, :completed],
transitions: %{
created: [:partial, :completed],
partial: :completed
}

def before_transition(struct, :partial) do
# Code to simulate and force an exception inside a
# guard function.
if Map.get(struct, :force_exception) do
Machinery.non_existing_function_should_raise_error()
end

Map.put(struct, :missing_fields, true)
end

def after_transition(struct, :completed) do
Map.put(struct, :missing_fields, false)
end
end

test "All internal functions should be injected into AST" do
Expand Down Expand Up @@ -76,8 +89,8 @@ defmodule MachineryTest do
end

test "Modules without guard conditions should allow transitions by default" do
struct = %TestModule{state: :created, missing_fields: true}
assert {:ok, %TestModule{state: :completed, missing_fields: true}} = Machinery.transition_to(struct, :completed)
struct = %TestModule{state: :created}
assert {:ok, %TestModule{state: :completed}} = Machinery.transition_to(struct, :completed)
end

test "Implict rescue on the guard clause internals should raise any other excepetion not strictly related to missing guard_tranistion/2 existence" do
Expand All @@ -86,4 +99,22 @@ defmodule MachineryTest do
Machinery.transition_to(wrong_struct, :completed)
end
end

test "after_transition/2 and before_transition/2 callbacks should be automatically executed" do
struct = %TestModule{}
assert struct.missing_fields == nil

{:ok, partial_struct} = Machinery.transition_to(struct, :partial)
assert partial_struct.missing_fields == true

{:ok, completed_struct} = Machinery.transition_to(struct, :completed)
assert completed_struct.missing_fields == false
end

test "Implict rescue on the callbacks internals should raise any other excepetion not strictly related to missing callbacks_fallback/2 existence" do
wrong_struct = %TestModule{state: :created, force_exception: true}
assert_raise UndefinedFunctionError, fn() ->
Machinery.transition_to(wrong_struct, :partial)
end
end
end

0 comments on commit c548f6c

Please sign in to comment.