-
Notifications
You must be signed in to change notification settings - Fork 53
/
transition.ex
187 lines (169 loc) · 5.54 KB
/
transition.ex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
defmodule Machinery.Transition do
@moduledoc """
Machinery module responsible for control transitions,
guard functions and callbacks (before and after).
This is meant to be for internal use only.
"""
@doc """
Function responsible for checking if the transition from a state to another
was specifically declared.
This is meant to be for internal use only.
"""
@spec declared_transition?(list, atom, atom) :: boolean
def declared_transition?(transitions, current_state, next_state) do
if matches_wildcard?(transitions, next_state) do
true
else
matches_transition?(transitions, current_state, next_state)
end
end
@doc """
Default guard transition fallback to make sure all transitions are permitted
unless another existing guard condition exists.
This is meant to be for internal use only.
"""
@spec guarded_transition?(module, struct, atom, map()) :: boolean
def guarded_transition?(module, struct, state, extra_metadata) do
function =
if extra_metadata == None, do: &module.guard_transition/2, else: &module.guard_transition/3
case run_or_fallback(
function,
&guard_transition_fallback/4,
struct,
state,
module._field(),
extra_metadata
) do
{:error, cause} -> {:error, cause}
_ -> false
end
end
@doc """
Function responsible to run all before_transitions callbacks or
fallback to a boilerplate behaviour.
This is meant to be for internal use only.
"""
@spec before_callbacks(struct, atom, module, map()) :: struct
def before_callbacks(struct, state, module, extra_metadata) do
function =
if extra_metadata == None,
do: &module.before_transition/2,
else: &module.before_transition/3
run_or_fallback(
function,
&callbacks_fallback/4,
struct,
state,
module._field(),
extra_metadata
)
end
@doc """
Function responsible to run all after_transitions callbacks or
fallback to a boilerplate behaviour.
This is meant to be for internal use only.
"""
@spec after_callbacks(struct, atom, module, map()) :: struct
def after_callbacks(struct, state, module, extra_metadata) do
function =
if extra_metadata == None, do: &module.after_transition/2, else: &module.after_transition/3
run_or_fallback(
function,
&callbacks_fallback/4,
struct,
state,
module._field(),
extra_metadata
)
end
@doc """
This function will try to trigger persistence, if declared, to the struct
changing state.
This is meant to be for internal use only.
"""
@spec persist_struct(struct, atom, module, map()) :: struct
def persist_struct(struct, state, module, extra_metadata) do
function = if extra_metadata == None, do: &module.persist/2, else: &module.persist/3
run_or_fallback(
function,
&persist_fallback/4,
struct,
state,
module._field(),
extra_metadata
)
end
@doc """
Function responsible for triggering transitions persistence.
This is meant to be for internal use only.
"""
@spec log_transition(struct, atom, module, map()) :: struct
def log_transition(struct, state, module, extra_metadata) do
function =
if extra_metadata == None, do: &module.log_transition/2, else: &module.log_transition/3
run_or_fallback(
function,
&log_transition_fallback/4,
struct,
state,
module._field(),
extra_metadata
)
end
defp matches_wildcard?(transitions, next_state) do
matches_transition?(transitions, "*", next_state)
end
defp matches_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
# This function looks at the arity of a function and calls it with
# the appropriate number of parameters, passing in the struct,
# state, and extra_metadata. If the function throws an error,
# the fallback function is called instead.
defp run_or_fallback(func, fallback, struct, state, field, extra_metadata) do
case :erlang.fun_info(func)[:arity] do
2 -> func.(struct, state)
3 -> func.(struct, state, extra_metadata)
_ -> raise "Invalid arity for #{inspect(func)}"
end
rescue
error in UndefinedFunctionError -> fallback.(struct, state, error, field)
error in FunctionClauseError -> fallback.(struct, state, error, field)
end
defp persist_fallback(struct, state, error, field) do
if error.function == :persist && Enum.member?([2, 3], error.arity) do
Map.put(struct, field, state)
else
raise error
end
end
defp log_transition_fallback(struct, _state, error, _field) do
if error.function == :log_transition && Enum.member?([2, 3], error.arity) do
struct
else
raise error
end
end
defp callbacks_fallback(struct, _state, error, _field) do
if error.function in [:after_transition, :before_transition] &&
Enum.member?([2, 3], error.arity) do
struct
else
raise error
end
end
# If the exception passed is related to a specific signature of
# guard_transition/2 it will fallback returning true and
# allowing the transition, otherwise it will raise the exception.
defp guard_transition_fallback(_struct, _state, error, _field) do
if error.function == :guard_transition && Enum.member?([2, 3], error.arity) do
true
else
raise error
end
end
end