state_gate 1.2.3 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/lib/state_gate/builder/conflict_detection_methods.rb +59 -18
- data/lib/state_gate/builder/dynamic_module_creation_methods.rb +36 -22
- data/lib/state_gate/builder/scope_methods.rb +30 -20
- data/lib/state_gate/builder/state_methods.rb +108 -72
- data/lib/state_gate/builder/transition_methods.rb +54 -34
- data/lib/state_gate/builder/transition_validation_methods.rb +76 -66
- data/lib/state_gate/builder.rb +53 -43
- data/lib/state_gate/engine/configurator.rb +77 -63
- data/lib/state_gate/engine/errator.rb +42 -9
- data/lib/state_gate/engine/fixer.rb +21 -12
- data/lib/state_gate/engine/scoper.rb +12 -4
- data/lib/state_gate/engine/sequencer.rb +18 -5
- data/lib/state_gate/engine/stator.rb +98 -53
- data/lib/state_gate/engine/transitioner.rb +28 -14
- data/lib/state_gate/engine.rb +19 -6
- data/lib/state_gate/type.rb +22 -2
- data/lib/state_gate.rb +88 -61
- metadata +3 -3
@@ -12,61 +12,61 @@ module StateGate
|
|
12
12
|
#
|
13
13
|
# Options include:
|
14
14
|
#
|
15
|
-
#
|
16
|
-
#
|
17
|
-
#
|
15
|
+
# [state]
|
16
|
+
# Required name for the new state, supplied as a Symbol. The +state-gate+ requires
|
17
|
+
# a minimum of two states to be defined.
|
18
18
|
# state :state_name
|
19
19
|
#
|
20
20
|
#
|
21
|
-
#
|
21
|
+
# **_:transitions_to_**
|
22
22
|
# An optional list of the other state that this state is allowed to change to.
|
23
23
|
# state :state_1, transtions_to: [:state_2, :state_3, :state_4]
|
24
24
|
# state :state_2, transtions_to: :state_4
|
25
25
|
# state :state_3, transtions_to: :any
|
26
26
|
# state :state_4
|
27
27
|
#
|
28
|
-
#
|
28
|
+
# **_:human_**
|
29
29
|
# An optional String name to used when displaying gthe state in a view. If no
|
30
30
|
# name is specified, it will default to +:state.titleized+.
|
31
31
|
# state :state_1, transtions_to: [:state_2, :state_3], human: "My State"
|
32
32
|
#
|
33
33
|
#
|
34
|
-
#
|
35
|
-
#
|
36
|
-
#
|
34
|
+
# [default]
|
35
|
+
# Optional setting to specify the default state for a new object. The state name
|
36
|
+
# is given as a Symbol.
|
37
37
|
# default :state_name
|
38
38
|
#
|
39
39
|
#
|
40
|
-
#
|
41
|
-
#
|
42
|
-
#
|
43
|
-
# prefix :before
|
40
|
+
# [prefix]
|
41
|
+
# Optional setting to add a given Symbol before each state name when using Class Scopes.
|
42
|
+
# This helps to differential between multiple attributes that have similar state names.
|
43
|
+
# prefix :before #=> Class.before_active
|
44
44
|
#
|
45
45
|
#
|
46
|
-
#
|
47
|
-
#
|
48
|
-
#
|
49
|
-
# suffix :after
|
46
|
+
# [suffix]
|
47
|
+
# Optional setting to add a given Symbol after each state name when using Class Scopes.
|
48
|
+
# This helps to differential between multiple attributes that have similar state names.
|
49
|
+
# suffix :after #=> Class.active_after
|
50
50
|
#
|
51
51
|
#
|
52
|
-
#
|
53
|
-
#
|
54
|
-
#
|
52
|
+
# [make_sequential]
|
53
|
+
# Optional setting to automatically add transitions from each state to both the
|
54
|
+
# preceeding and following states.
|
55
55
|
# make_sequential
|
56
56
|
#
|
57
|
-
#
|
58
|
-
#
|
59
|
-
#
|
60
|
-
#
|
57
|
+
# **_:one_way_**
|
58
|
+
# Option to restrict the generated transitions to one directtion only: from each
|
59
|
+
# state to the follow state.
|
60
|
+
# make_sequential :one_way
|
61
61
|
#
|
62
|
-
#
|
63
|
-
#
|
64
|
-
#
|
65
|
-
#
|
62
|
+
# **_:loop_**
|
63
|
+
# Option to add transitions from the last state to the first and, unless +:one_way+
|
64
|
+
# is specified, also from the first state to the last.
|
65
|
+
# make_sequential :one_way, :loop
|
66
66
|
#
|
67
67
|
#
|
68
|
-
#
|
69
|
-
#
|
68
|
+
# [no_scopes]
|
69
|
+
# Optional setting to disable the generation of Class Scope helpers methods.
|
70
70
|
# no_scopes
|
71
71
|
#
|
72
72
|
module Configurator
|
@@ -81,53 +81,61 @@ module StateGate
|
|
81
81
|
# ======================================================================
|
82
82
|
|
83
83
|
|
84
|
+
##
|
84
85
|
# Execute the provided configuration.
|
85
86
|
#
|
87
|
+
# @block config
|
88
|
+
# the given configuration
|
89
|
+
#
|
86
90
|
# ==== actions
|
87
91
|
#
|
88
|
-
#
|
89
|
-
#
|
90
|
-
#
|
92
|
+
# - create sequence links and transitions
|
93
|
+
# - create scope names
|
94
|
+
# - remove duplicate transitions for each state
|
91
95
|
#
|
92
|
-
#
|
93
|
-
#
|
94
|
-
#
|
96
|
+
# - verify there are multiple valid state names
|
97
|
+
# - verify all transitions lead to existing states
|
98
|
+
# - verify each state, except the default, can be reached from a transition
|
95
99
|
#
|
96
|
-
def
|
97
|
-
|
100
|
+
def _parse_configuration(&config)
|
101
|
+
_exec_configuration(&config)
|
98
102
|
|
99
103
|
generate_sequences
|
100
104
|
generate_scope_names
|
101
105
|
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
106
|
+
_assert_states_are_valid
|
107
|
+
_assert_transitions_exist
|
108
|
+
_assert_uniq_transitions
|
109
|
+
_assert_any_has_been_expanded
|
110
|
+
_assert_all_transitions_are_states
|
111
|
+
_assert_all_states_are_reachable
|
108
112
|
end
|
109
113
|
|
110
114
|
|
111
115
|
|
116
|
+
##
|
112
117
|
# Run the configuration commands.
|
113
118
|
#
|
119
|
+
# @block config
|
120
|
+
# the given configuration
|
121
|
+
#
|
114
122
|
# ==== actions
|
115
123
|
#
|
116
|
-
#
|
117
|
-
#
|
118
|
-
#
|
124
|
+
# - create sequence links and transitions
|
125
|
+
# - create scope names
|
126
|
+
# - remove duplicate transitions for each state
|
119
127
|
#
|
120
|
-
#
|
121
|
-
#
|
122
|
-
#
|
128
|
+
# - verify there are multiple valid state names
|
129
|
+
# - verify all transitions lead to existing states
|
130
|
+
# - verify each state, except the default, can be reached from a transition
|
123
131
|
#
|
124
|
-
def
|
132
|
+
def _exec_configuration(&config)
|
125
133
|
instance_exec(&config)
|
126
134
|
rescue NameError => e
|
127
135
|
err_command = e.to_s.gsub('undefined local variable or method `', '')
|
128
136
|
.split("'")
|
129
137
|
.first
|
130
|
-
|
138
|
+
_cerr :bad_command, cmd: err_command
|
131
139
|
end
|
132
140
|
|
133
141
|
|
@@ -136,21 +144,22 @@ module StateGate
|
|
136
144
|
# Assertions
|
137
145
|
# ======================================================================
|
138
146
|
|
147
|
+
##
|
139
148
|
# Ensure there are enough states and the default is a valid state, setting
|
140
149
|
# the default to the first state if required.
|
141
150
|
#
|
142
|
-
def
|
151
|
+
def _assert_states_are_valid
|
143
152
|
state_names = @states.keys
|
144
153
|
|
145
154
|
# are there states
|
146
|
-
|
155
|
+
_cerr(:states_missing_err) if state_names.blank?
|
147
156
|
|
148
157
|
# is there more than one state
|
149
|
-
|
158
|
+
_cerr(:single_state_err) if state_names.one?
|
150
159
|
|
151
160
|
# set the deafult state if needed, otherwise check it is a valid state
|
152
161
|
if @default
|
153
|
-
|
162
|
+
_cerr(:default_state_err) unless state_names.include?(@default)
|
154
163
|
else
|
155
164
|
@default = state_names.first
|
156
165
|
end
|
@@ -158,11 +167,12 @@ module StateGate
|
|
158
167
|
|
159
168
|
|
160
169
|
|
170
|
+
##
|
161
171
|
# Ensure that transitions have been specified. If not, then add the transitions
|
162
172
|
# to allow every stater to transition to another state and flag the engine as
|
163
173
|
# transitionless, so we don't add any validation methods.
|
164
174
|
#
|
165
|
-
def
|
175
|
+
def _assert_transitions_exist
|
166
176
|
return if @states.map { |_state, opts| opts[:transitions_to] }.uniq.flatten.any?
|
167
177
|
|
168
178
|
@transitionless = true
|
@@ -173,39 +183,42 @@ module StateGate
|
|
173
183
|
|
174
184
|
|
175
185
|
|
186
|
+
##
|
176
187
|
# Ensure that there is only one of reach transition
|
177
188
|
#
|
178
|
-
def
|
189
|
+
def _assert_uniq_transitions
|
179
190
|
@states.each { |_state, opts| opts[:transitions_to].uniq! }
|
180
191
|
end
|
181
192
|
|
182
193
|
|
183
194
|
|
195
|
+
##
|
184
196
|
# Ensure that the :any transition is expanded or raise an exception
|
185
197
|
# if it's included with other transitions
|
186
|
-
def
|
198
|
+
def _assert_any_has_been_expanded
|
187
199
|
@states.each do |state_name, opts|
|
188
200
|
if opts[:transitions_to] == [:any]
|
189
201
|
@states[state_name][:transitions_to] = @states.keys - [state_name]
|
190
202
|
|
191
203
|
elsif opts[:transitions_to].include?(:any)
|
192
|
-
|
204
|
+
_cerr(:any_transition_err, state: state_name, kattr: true)
|
193
205
|
end
|
194
206
|
end
|
195
207
|
end
|
196
208
|
|
197
209
|
|
198
210
|
|
211
|
+
##
|
199
212
|
# Ensure all transitions are to valid states.
|
200
213
|
#
|
201
214
|
# Replaces transition to :any with a list of all states
|
202
215
|
# Raises an exception if :any in included with a list of other transitions
|
203
216
|
#
|
204
|
-
def
|
217
|
+
def _assert_all_transitions_are_states
|
205
218
|
@states.each do |state_name, opts|
|
206
219
|
opts[:transitions_to].each do |transition|
|
207
220
|
unless @states.keys.include?(transition)
|
208
|
-
|
221
|
+
_cerr(:transition_state_err, state: state_name, transition: transition, kattr: true)
|
209
222
|
end
|
210
223
|
end
|
211
224
|
end
|
@@ -213,16 +226,17 @@ module StateGate
|
|
213
226
|
|
214
227
|
|
215
228
|
|
229
|
+
##
|
216
230
|
# Ensure there is a transition leading to every non-default state.
|
217
231
|
#
|
218
|
-
def
|
232
|
+
def _assert_all_states_are_reachable
|
219
233
|
# is there a transition to every state except the default.
|
220
234
|
transitions = @states.map { |_state, opts| opts[:transitions_to] }.flatten.uniq
|
221
235
|
adrift_states = (@states.keys - transitions - [@default])
|
222
236
|
return if adrift_states.blank?
|
223
237
|
|
224
238
|
states = adrift_states.map { |s| ':' + s.to_s }.to_sentence
|
225
|
-
|
239
|
+
_cerr(:transitionless_states_err, states: states, kattr: true)
|
226
240
|
end
|
227
241
|
|
228
242
|
end # ConfigurationMethods
|
@@ -6,8 +6,8 @@ module StateGate
|
|
6
6
|
# = Description
|
7
7
|
#
|
8
8
|
# Adds error reporting methods to a StateMachine::Engine
|
9
|
-
#
|
10
|
-
#
|
9
|
+
# - all error messages are I18n configured
|
10
|
+
# - method names are deliberately short to encourage code readability with
|
11
11
|
# if/unless one-liners
|
12
12
|
module Errator
|
13
13
|
|
@@ -15,24 +15,39 @@ module StateGate
|
|
15
15
|
# ======================================================================
|
16
16
|
private
|
17
17
|
|
18
|
+
##
|
18
19
|
# Format the given value and report an ArgumentError
|
19
20
|
#
|
20
|
-
|
21
|
+
# @param [String]
|
22
|
+
# the error message
|
23
|
+
#
|
24
|
+
# @raise [ArgumentError]
|
25
|
+
#
|
26
|
+
def _invalid_state_error(val)
|
21
27
|
case val
|
22
28
|
when NilClass
|
23
|
-
|
29
|
+
_aerr :invalid_state_err, val: "'nil'", kattr: true
|
24
30
|
when Symbol
|
25
|
-
|
31
|
+
_aerr :invalid_state_err, val: ":#{val}", kattr: true
|
26
32
|
else
|
27
|
-
|
33
|
+
_aerr :invalid_state_err, val: "'#{val&.to_s}'", kattr: true
|
28
34
|
end
|
29
35
|
end
|
30
36
|
|
31
37
|
|
32
38
|
|
39
|
+
##
|
33
40
|
# Report a ConfigurationError, including the Klass#attr variable.
|
34
41
|
#
|
35
|
-
|
42
|
+
# @param [Symbol] err
|
43
|
+
# the I18n error message key
|
44
|
+
#
|
45
|
+
# @param [Hash] args
|
46
|
+
# arguments to be included in the error
|
47
|
+
#
|
48
|
+
# @raise [ConfigurationError]
|
49
|
+
#
|
50
|
+
def _cerr(err, **args)
|
36
51
|
args[:kattr] = "#{@klass}##{@attribute}" if args[:kattr] == true
|
37
52
|
key = "state_gate.engine.config.#{err}"
|
38
53
|
fail ConfigurationError, I18n.t(key, **args)
|
@@ -40,9 +55,18 @@ module StateGate
|
|
40
55
|
|
41
56
|
|
42
57
|
|
58
|
+
##
|
43
59
|
# Report a RuntimeError, including the Klass#attr variable.
|
44
60
|
#
|
45
|
-
|
61
|
+
# @param [Symbol] err
|
62
|
+
# the I18n error message key
|
63
|
+
#
|
64
|
+
# @param [Hash] args
|
65
|
+
# arguments to be included in the error
|
66
|
+
#
|
67
|
+
# @raise [RuntimeError]
|
68
|
+
#
|
69
|
+
def _rerr(err, **args)
|
46
70
|
args[:kattr] = "#{@klass}##{@attribute}" if args[:kattr] == true
|
47
71
|
key = "state_gate.engine.#{err}"
|
48
72
|
fail I18n.t(key, **args)
|
@@ -50,9 +74,18 @@ module StateGate
|
|
50
74
|
|
51
75
|
|
52
76
|
|
77
|
+
##
|
53
78
|
# Report an ArgumentError, including the Klass#attr variable.
|
54
79
|
#
|
55
|
-
|
80
|
+
# @param [Symbol] err
|
81
|
+
# the I18n error message key
|
82
|
+
#
|
83
|
+
# @param [Hash] args
|
84
|
+
# arguments to be included in the error
|
85
|
+
#
|
86
|
+
# @raise [ArgumentError]
|
87
|
+
#
|
88
|
+
def _aerr(err, **args)
|
56
89
|
args[:kattr] = "#{@klass}##{@attribute}" if args[:kattr] == true
|
57
90
|
key = "state_gate.engine.#{err}"
|
58
91
|
fail ArgumentError, I18n.t(key, **args)
|
@@ -13,15 +13,19 @@ module StateGate
|
|
13
13
|
# Configuration Methods
|
14
14
|
# ======================================================================
|
15
15
|
|
16
|
+
##
|
16
17
|
# A phrase to add before state names when using Class Scopes.
|
17
18
|
# This helps differential attributes that have similar state names.
|
18
|
-
# (Symbol | optional)
|
19
19
|
#
|
20
|
-
#
|
20
|
+
# @param [Symbol] val
|
21
|
+
# the prefix to use
|
22
|
+
#
|
23
|
+
# @example
|
24
|
+
# prefix :before #=> Class.before_active
|
21
25
|
#
|
22
26
|
def prefix(val = nil)
|
23
|
-
|
24
|
-
|
27
|
+
_cerr(:prefix_type_err, kattr: true) unless val.is_a?(Symbol)
|
28
|
+
_cerr(:prefix_multiple_err, kattr: true) if @prefix
|
25
29
|
@prefix = "#{val.to_s.downcase}_"
|
26
30
|
end # prefix
|
27
31
|
|
@@ -29,13 +33,16 @@ module StateGate
|
|
29
33
|
|
30
34
|
# A phrase to add before state names when using Class Scopes.
|
31
35
|
# This helps differential attributes that have similar state names.
|
32
|
-
# (Symbol | optional)
|
33
36
|
#
|
34
|
-
#
|
37
|
+
# @param [Symbol] val
|
38
|
+
# the suffix to use
|
39
|
+
#
|
40
|
+
# @example
|
41
|
+
# suffix :after #=> Class.active_after
|
35
42
|
#
|
36
43
|
def suffix(val = nil)
|
37
|
-
|
38
|
-
|
44
|
+
_cerr(:suffix_type_err, kattr: true) unless val.is_a?(Symbol)
|
45
|
+
_cerr(:suffix_multiple_err, kattr: true) if @suffix
|
39
46
|
@suffix = "_#{val.to_s.downcase}"
|
40
47
|
end # suffix
|
41
48
|
|
@@ -49,8 +56,9 @@ module StateGate
|
|
49
56
|
# Returns the defined prefix for the state_gate, or an empty string if no
|
50
57
|
# prefix has been defined.
|
51
58
|
#
|
52
|
-
#
|
53
|
-
# .state_prefix
|
59
|
+
# @example
|
60
|
+
# .state_prefix #=> 'my_prefix'
|
61
|
+
# .state_prefix #=> ''
|
54
62
|
#
|
55
63
|
def state_prefix
|
56
64
|
@prefix
|
@@ -62,8 +70,9 @@ module StateGate
|
|
62
70
|
# Returns the defined suffix for the state_gate, or an empty string if no
|
63
71
|
# suffix has been defined.
|
64
72
|
#
|
65
|
-
#
|
66
|
-
# .state_suffix
|
73
|
+
# @example
|
74
|
+
# .state_suffix #=> 'my_suffix'
|
75
|
+
# .state_suffix #=> ''
|
67
76
|
#
|
68
77
|
def state_suffix
|
69
78
|
@suffix
|
@@ -13,8 +13,10 @@ module StateGate
|
|
13
13
|
# Configuration Methods
|
14
14
|
# ======================================================================
|
15
15
|
|
16
|
+
##
|
16
17
|
# Disables the generation of Class Scope helpers methods
|
17
18
|
#
|
19
|
+
# @example
|
18
20
|
# no_scopes
|
19
21
|
#
|
20
22
|
def no_scopes
|
@@ -23,6 +25,7 @@ module StateGate
|
|
23
25
|
|
24
26
|
|
25
27
|
|
28
|
+
##
|
26
29
|
# Generate the scope name to use for each state.
|
27
30
|
#
|
28
31
|
def generate_scope_names
|
@@ -40,7 +43,8 @@ module StateGate
|
|
40
43
|
##
|
41
44
|
# Returns TRUE if scope methods should be added to the model, otherwise FALSE.
|
42
45
|
#
|
43
|
-
#
|
46
|
+
# @example
|
47
|
+
# .include_scopes? #=> true
|
44
48
|
#
|
45
49
|
def include_scopes?
|
46
50
|
!!@scopes
|
@@ -52,9 +56,13 @@ module StateGate
|
|
52
56
|
# Returns the scope name for the given state. Scope names are generated by
|
53
57
|
# concatenating the prefix, state name and suffix
|
54
58
|
#
|
55
|
-
#
|
56
|
-
#
|
57
|
-
#
|
59
|
+
# @param [Symbol] state_name
|
60
|
+
# the state to generate the scope_name for
|
61
|
+
#
|
62
|
+
# Example
|
63
|
+
# .scope_name_for_state(:active) #=> 'active'
|
64
|
+
# .scope_name_for_state(:pending) #=> 'pending_status'
|
65
|
+
# .scope_name_for_state(:archived) #=> 'with_archived_status'
|
58
66
|
#
|
59
67
|
def scope_name_for_state(state_name = nil)
|
60
68
|
state = assert_valid_state!(state_name)
|
@@ -15,16 +15,23 @@ module StateGate
|
|
15
15
|
# Configuration Methods
|
16
16
|
# ======================================================================
|
17
17
|
|
18
|
+
##
|
18
19
|
# Automatically add transitions from each state to the preceeding and following states.
|
19
20
|
# make_sequential
|
20
21
|
#
|
21
|
-
# [
|
22
|
+
# @param [Array[Symbol]] args
|
23
|
+
# the optional configuration arguments
|
24
|
+
#
|
25
|
+
# @option args [Symbol] :one_way
|
22
26
|
# Only adds tranitions from each state to the follow state. (optional)
|
23
|
-
# make_sequential :one_way
|
24
27
|
#
|
25
|
-
# [:loop
|
28
|
+
# @option args [Symbol] :loop
|
26
29
|
# Adds transitions from the last state to the first and from the first to the last
|
27
30
|
# (unless also :one_way) (optional)
|
31
|
+
#
|
32
|
+
# @example
|
33
|
+
# make_sequential :one_way
|
34
|
+
# make_sequential :loop
|
28
35
|
# make_sequential :one_way, :loop
|
29
36
|
#
|
30
37
|
def make_sequential(*args)
|
@@ -35,6 +42,7 @@ module StateGate
|
|
35
42
|
|
36
43
|
|
37
44
|
|
45
|
+
##
|
38
46
|
# Add sequence hooks if sequential requested.
|
39
47
|
#
|
40
48
|
def generate_sequences
|
@@ -47,6 +55,7 @@ module StateGate
|
|
47
55
|
|
48
56
|
|
49
57
|
|
58
|
+
##
|
50
59
|
# Add the previous sequential state
|
51
60
|
#
|
52
61
|
def add_previous_sequential_state
|
@@ -64,6 +73,7 @@ module StateGate
|
|
64
73
|
|
65
74
|
|
66
75
|
|
76
|
+
##
|
67
77
|
# Add the next sequential state
|
68
78
|
#
|
69
79
|
def add_next_sequential_state
|
@@ -79,6 +89,7 @@ module StateGate
|
|
79
89
|
|
80
90
|
|
81
91
|
|
92
|
+
##
|
82
93
|
# Add the first and last transitions to complete the sequential loop.
|
83
94
|
#
|
84
95
|
def loop_sequence
|
@@ -103,9 +114,11 @@ module StateGate
|
|
103
114
|
# ======================================================================
|
104
115
|
|
105
116
|
##
|
106
|
-
# return
|
117
|
+
# @return [Boolean]
|
118
|
+
# true if the state_gate is sequential, otherwise false.
|
107
119
|
#
|
108
|
-
#
|
120
|
+
# @example
|
121
|
+
# .sequential? #=> TRUE
|
109
122
|
#
|
110
123
|
def sequential?
|
111
124
|
!!@sequential
|