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
@@ -15,27 +15,28 @@ module StateGate
|
|
15
15
|
|
16
16
|
# Add a new +state+.
|
17
17
|
#
|
18
|
-
# [
|
19
|
-
# The name for the new state.
|
18
|
+
# @param [Symbol, String] name
|
19
|
+
# The name for the new state. It must be converatbel to a Symbol
|
20
20
|
# state :state_name
|
21
21
|
#
|
22
|
+
# @param [Hash] opts
|
23
|
+
# configuration options for the given state
|
22
24
|
#
|
23
|
-
# [:transitions_to
|
25
|
+
# @option opts [Hash] :transitions_to
|
24
26
|
# A list of other state that this state may change to. (Array | optional)
|
25
27
|
# state :state_name, transtions_to: [:state_1, :state_4, :state_5]
|
26
28
|
#
|
27
|
-
# [:human
|
28
|
-
# A display name for the state used in views.
|
29
|
-
# (String | optoinal | default: state.titleized)
|
29
|
+
# @option opts [Hash] :human
|
30
|
+
# A display name for the state used in views, defaulting to state.titleized.
|
30
31
|
# state :state_name, transtions_to: [:state_1, :state_4, :state_5], human: "My State"
|
31
32
|
#
|
32
33
|
def state(name, opts = {})
|
33
34
|
name = StateGate.symbolize(name)
|
34
|
-
|
35
|
-
|
35
|
+
_assert_valid_state_name(name)
|
36
|
+
_assert_valid_opts(opts, name)
|
36
37
|
|
37
38
|
# add the state
|
38
|
-
@states[name] =
|
39
|
+
@states[name] = _state_template.merge({ human: opts[:human] || name.to_s.titleize })
|
39
40
|
|
40
41
|
# add the state transitions
|
41
42
|
Array(opts[:transitions_to]).each do |transition|
|
@@ -45,14 +46,19 @@ module StateGate
|
|
45
46
|
|
46
47
|
|
47
48
|
|
49
|
+
##
|
48
50
|
# The name for the default state for a new object. (String | required)
|
49
51
|
#
|
52
|
+
# @param [Symbol] default_state
|
53
|
+
# the state name to set as default
|
54
|
+
#
|
55
|
+
# @example
|
50
56
|
# default :state_name
|
51
57
|
#
|
52
|
-
def default(
|
53
|
-
|
54
|
-
|
55
|
-
@default = StateGate.symbolize(
|
58
|
+
def default(default_state = nil)
|
59
|
+
_cerr(:default_type_err, kattr: true) unless default_state.is_a?(Symbol)
|
60
|
+
_cerr(:default_repeat_err, kattr: true) if @default
|
61
|
+
@default = StateGate.symbolize(default_state)
|
56
62
|
end
|
57
63
|
|
58
64
|
|
@@ -64,7 +70,8 @@ module StateGate
|
|
64
70
|
##
|
65
71
|
# Returns an Array defined states.
|
66
72
|
#
|
67
|
-
#
|
73
|
+
# @example
|
74
|
+
# .states #=> [:pending, :active, :suspended, :archived]
|
68
75
|
#
|
69
76
|
def states
|
70
77
|
@states.keys
|
@@ -75,26 +82,30 @@ module StateGate
|
|
75
82
|
##
|
76
83
|
# Ensures the given value is a valid state name.
|
77
84
|
#
|
78
|
-
# [value
|
79
|
-
#
|
85
|
+
# @param [String, Symbol] value
|
86
|
+
# the state name.
|
80
87
|
#
|
81
|
-
#
|
82
|
-
#
|
83
|
-
#
|
88
|
+
# @example
|
89
|
+
# .assert_valid_state!(:active) #=> :active
|
90
|
+
# .assert_valid_state!('PENDING') #=> :pending
|
91
|
+
# .assert_valid_state!(:dummy) #=> ArgumentError
|
84
92
|
#
|
85
93
|
#
|
86
|
-
#
|
94
|
+
# @note
|
87
95
|
# Valid state names preceeded with +force_+ are also allowed.
|
88
96
|
#
|
89
|
-
# .assert_valid_state!(:force_active)
|
97
|
+
# .assert_valid_state!(:force_active) #=> :force_active
|
90
98
|
#
|
91
|
-
#
|
92
|
-
#
|
99
|
+
# @return [Symbol]
|
100
|
+
# the Symbol state name
|
101
|
+
#
|
102
|
+
# @raise [ArgumentError]
|
103
|
+
# if the value is not a valid state name.
|
93
104
|
#
|
94
105
|
def assert_valid_state!(value)
|
95
106
|
state_name = StateGate.symbolize(value)
|
96
107
|
unforced_state = state_name.to_s.remove(/^force_/).to_sym
|
97
|
-
|
108
|
+
_invalid_state_error(value) unless @states.keys.include?(unforced_state)
|
98
109
|
state_name
|
99
110
|
end
|
100
111
|
|
@@ -103,7 +114,8 @@ module StateGate
|
|
103
114
|
##
|
104
115
|
# Returns an Array of the human display names for each defined state.
|
105
116
|
#
|
106
|
-
#
|
117
|
+
# @example
|
118
|
+
# human_states #=> ['Pending Activation', 'Active', 'Suspended By Admin']
|
107
119
|
#
|
108
120
|
def human_states
|
109
121
|
@states.map { |_k, v| v[:human] }
|
@@ -114,8 +126,12 @@ module StateGate
|
|
114
126
|
##
|
115
127
|
# Returns the human display name for a given state.
|
116
128
|
#
|
117
|
-
#
|
118
|
-
#
|
129
|
+
# @param [Symbol] state
|
130
|
+
# the state name
|
131
|
+
#
|
132
|
+
# @example
|
133
|
+
# .human_state_for(:pending) #=> 'Panding Activation'
|
134
|
+
# .human_state_for(:active) #=> 'Active'
|
119
135
|
#
|
120
136
|
def human_state_for(state)
|
121
137
|
state_name = assert_valid_state!(state)
|
@@ -128,15 +144,20 @@ module StateGate
|
|
128
144
|
# Return an Array of states, with their human display names, ready for
|
129
145
|
# use in a form select.
|
130
146
|
#
|
131
|
-
#
|
147
|
+
# @param [Boolean] sorted
|
148
|
+
# true is the states should be sorted by human name, defaults to false
|
132
149
|
#
|
133
|
-
#
|
134
|
-
#
|
135
|
-
# # ['Suspended by Admin', 'suspended']]
|
150
|
+
# @return [Array[Array[Strings]]]
|
151
|
+
# Array of state names with their human names
|
136
152
|
#
|
137
|
-
#
|
138
|
-
#
|
139
|
-
#
|
153
|
+
# @example
|
154
|
+
# .states_for_select #=> [['Pending Activation', 'pending'],
|
155
|
+
# ['Active', 'active'],
|
156
|
+
# ['Suspended by Admin', 'suspended']]
|
157
|
+
#
|
158
|
+
# .states_for_select(true) #=> [['Active', 'active'],
|
159
|
+
# ['Pending Activation', 'pending'],
|
160
|
+
# ['Suspended by Admin', 'suspended']]
|
140
161
|
#
|
141
162
|
def states_for_select(sorted = false)
|
142
163
|
result = []
|
@@ -152,12 +173,14 @@ module StateGate
|
|
152
173
|
|
153
174
|
|
154
175
|
##
|
155
|
-
#
|
176
|
+
# @return [Hash]
|
177
|
+
# a hash of states and their transitions & human names
|
156
178
|
#
|
157
|
-
#
|
158
|
-
#
|
159
|
-
#
|
160
|
-
#
|
179
|
+
# @example
|
180
|
+
# .raw_states #=> { pending: { transitions_to: [:active],
|
181
|
+
# human: 'Pending Activation'},
|
182
|
+
# active: { transitions_to: [:pending],
|
183
|
+
# human: 'Active'}}
|
161
184
|
#
|
162
185
|
def raw_states
|
163
186
|
@states
|
@@ -166,9 +189,11 @@ module StateGate
|
|
166
189
|
|
167
190
|
|
168
191
|
##
|
169
|
-
#
|
192
|
+
# @return [String]
|
193
|
+
# the state_gate default state
|
170
194
|
#
|
171
|
-
#
|
195
|
+
# @example
|
196
|
+
# .default_state #=> :pending
|
172
197
|
#
|
173
198
|
def default_state
|
174
199
|
@default
|
@@ -183,8 +208,11 @@ module StateGate
|
|
183
208
|
|
184
209
|
|
185
210
|
|
186
|
-
|
187
|
-
|
211
|
+
##
|
212
|
+
# @return [Hash]
|
213
|
+
# of the state keys defined.
|
214
|
+
#
|
215
|
+
def _state_template
|
188
216
|
{
|
189
217
|
transitions_to: [],
|
190
218
|
previous_state: nil,
|
@@ -196,28 +224,45 @@ module StateGate
|
|
196
224
|
|
197
225
|
|
198
226
|
|
199
|
-
|
227
|
+
##
|
228
|
+
# Raises an error fail if the supplied name is not a symbol, already added
|
200
229
|
# or starts with 'not_' or 'force'.
|
201
230
|
#
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
231
|
+
# @param [Symbol] name
|
232
|
+
# the state name to validate
|
233
|
+
#
|
234
|
+
# @raise [AygumentError]
|
235
|
+
# if the state name is invalid
|
236
|
+
#
|
237
|
+
def _assert_valid_state_name(name)
|
238
|
+
_cerr(:state_type_err, kattr: true) unless name.is_a?(Symbol)
|
239
|
+
_cerr(:state_repeat_err, state: name, kattr: true) if @states.key?(name)
|
240
|
+
_cerr(:state_not_name_err, state: name, kattr: true) if name.to_s.start_with?('not_')
|
241
|
+
_cerr(:state_force_name_err, state: name, kattr: true) if name.to_s.start_with?('force_')
|
207
242
|
end
|
208
243
|
|
209
244
|
|
210
245
|
|
211
|
-
|
246
|
+
##
|
247
|
+
# raises an error if opts is not a hash, has non-symbol keys or non-symbol transitions.
|
248
|
+
#
|
249
|
+
# @param [Hash] opts
|
250
|
+
# an options hash
|
251
|
+
#
|
252
|
+
# @param [Symbol] name
|
253
|
+
# the state name
|
254
|
+
#
|
255
|
+
# @raise [ArgumentError]
|
256
|
+
# if any invalid options
|
212
257
|
#
|
213
|
-
def
|
258
|
+
def _assert_valid_opts(opts, state_name)
|
214
259
|
unless opts.keys.reject { |k| k.is_a?(Symbol) }.blank?
|
215
|
-
|
260
|
+
_cerr(:state_opts_key_type_err, state: state_name, kattr: true)
|
216
261
|
end
|
217
262
|
|
218
263
|
return if Array(opts[:transitions_to]).reject { |k| k.is_a?(Symbol) }.blank?
|
219
264
|
|
220
|
-
|
265
|
+
_cerr(:transition_key_type_err, state: state_name, kattr: true)
|
221
266
|
end
|
222
267
|
|
223
268
|
end # Stater
|
@@ -10,10 +10,12 @@ module StateGate
|
|
10
10
|
module Transitioner
|
11
11
|
|
12
12
|
##
|
13
|
-
#
|
14
|
-
#
|
13
|
+
# @return [Boolean]
|
14
|
+
# true if every state can transition to every other state, rendering
|
15
|
+
# transitions pointless.
|
15
16
|
#
|
16
|
-
#
|
17
|
+
# @example
|
18
|
+
# .transitionless? #=> true
|
17
19
|
#
|
18
20
|
def transitionless?
|
19
21
|
!!@transitionless
|
@@ -22,12 +24,14 @@ module StateGate
|
|
22
24
|
|
23
25
|
|
24
26
|
##
|
25
|
-
#
|
27
|
+
# @return [Hash]
|
28
|
+
# of states and allowed transitions.
|
26
29
|
#
|
27
|
-
#
|
28
|
-
#
|
29
|
-
#
|
30
|
-
#
|
30
|
+
# @example
|
31
|
+
# .transitions #=> { pending: [:active],
|
32
|
+
# ativive: [:suspended, :archived],
|
33
|
+
# suspended: [:active, :archived],
|
34
|
+
# archived: [] }
|
31
35
|
#
|
32
36
|
def transitions
|
33
37
|
@transitions ||= begin
|
@@ -40,9 +44,14 @@ module StateGate
|
|
40
44
|
|
41
45
|
|
42
46
|
##
|
43
|
-
#
|
47
|
+
# @param [Symbol, String] state
|
48
|
+
# the state name
|
44
49
|
#
|
45
|
-
#
|
50
|
+
# @return [Array]
|
51
|
+
# of allowed transitions for the given state
|
52
|
+
#
|
53
|
+
# @example
|
54
|
+
# .transitions_for_state(:active) #=> [:suspended, :archived]
|
46
55
|
#
|
47
56
|
def transitions_for_state(state)
|
48
57
|
state_name = assert_valid_state!(state)
|
@@ -52,10 +61,15 @@ module StateGate
|
|
52
61
|
|
53
62
|
|
54
63
|
##
|
55
|
-
#
|
64
|
+
# @return [true]
|
65
|
+
# if a transition is allowed
|
66
|
+
#
|
67
|
+
# @raise [ArgumentError]
|
68
|
+
# if a transition is invalid
|
56
69
|
#
|
57
|
-
#
|
58
|
-
# .assert_valid_transition!(:
|
70
|
+
# @example
|
71
|
+
# .assert_valid_transition!(:pending, :active) #=> true
|
72
|
+
# .assert_valid_transition!(:active, :pending) #=> ArgumentError
|
59
73
|
#
|
60
74
|
def assert_valid_transition!(current_state = nil, new_state = nil)
|
61
75
|
from_state = assert_valid_state!(current_state)
|
@@ -65,7 +79,7 @@ module StateGate
|
|
65
79
|
return true if to_state.to_s.start_with?('force_')
|
66
80
|
return true if @states[from_state][:transitions_to].include?(to_state)
|
67
81
|
|
68
|
-
|
82
|
+
_aerr(:invalid_state_transition_err, from: from_state, to: to_state, kattr: true)
|
69
83
|
end
|
70
84
|
|
71
85
|
end # Sequencer
|
data/lib/state_gate/engine.rb
CHANGED
@@ -28,30 +28,43 @@ module StateGate
|
|
28
28
|
# ======================================================================
|
29
29
|
private
|
30
30
|
|
31
|
+
##
|
31
32
|
# Initialize the engine, setting the Class and attribute for the new engine
|
32
33
|
# and parsing the provided configuration.
|
33
34
|
#
|
35
|
+
# @param [Class] klass
|
36
|
+
# The class containing the attribute to be cast as a state gate
|
37
|
+
#
|
38
|
+
# @param [Symbol] attribute
|
39
|
+
# The name of the database attribute to use for the state gate
|
40
|
+
#
|
41
|
+
# @block config
|
42
|
+
# The configuration block for the state gate.
|
43
|
+
#
|
44
|
+
# @example
|
34
45
|
# StateGate::Engine.new(MyKlass, :status) do
|
35
46
|
# ... configuration ...
|
36
47
|
# end
|
37
48
|
#
|
38
49
|
def initialize(klass, attribute, &config)
|
39
|
-
|
40
|
-
|
41
|
-
|
50
|
+
_aerror(:klass_type_err) unless klass.respond_to?(:to_s)
|
51
|
+
_aerror(:attribute_type_err) unless attribute.is_a?(Symbol)
|
52
|
+
_aerror(:missing_config_block_err) unless block_given?
|
42
53
|
|
43
54
|
@klass = klass
|
44
55
|
@attribute = StateGate.symbolize(attribute)
|
45
56
|
|
46
|
-
|
57
|
+
_set_defaults
|
47
58
|
|
48
|
-
|
59
|
+
_parse_configuration(&config)
|
49
60
|
end
|
50
61
|
|
51
62
|
|
52
63
|
|
64
|
+
##
|
53
65
|
# Set the class variables with default values
|
54
|
-
|
66
|
+
#
|
67
|
+
def _set_defaults
|
55
68
|
@states = {}
|
56
69
|
@default = nil
|
57
70
|
@prefix = nil
|
data/lib/state_gate/type.rb
CHANGED
@@ -12,6 +12,8 @@ module StateGate
|
|
12
12
|
#
|
13
13
|
# This class is has an internal API for ActiveRecord and is not intended for public use.
|
14
14
|
#
|
15
|
+
# @private
|
16
|
+
#
|
15
17
|
class Type < ::ActiveModel::Type::String
|
16
18
|
|
17
19
|
##
|
@@ -19,7 +21,7 @@ module StateGate
|
|
19
21
|
#
|
20
22
|
def cast(value) # :nodoc:
|
21
23
|
assert_valid_value(value)
|
22
|
-
value
|
24
|
+
cast_value(value)
|
23
25
|
end
|
24
26
|
|
25
27
|
|
@@ -47,6 +49,17 @@ module StateGate
|
|
47
49
|
|
48
50
|
|
49
51
|
|
52
|
+
##
|
53
|
+
# Convert a nil DB value to the default state.
|
54
|
+
#
|
55
|
+
def deserialize(value) # :nodoc:
|
56
|
+
return value if value
|
57
|
+
|
58
|
+
klass.constantize.stateables[name].default_state
|
59
|
+
end
|
60
|
+
|
61
|
+
|
62
|
+
|
50
63
|
##
|
51
64
|
# Raise an exception unless the value is both serializable and a legitimate state
|
52
65
|
#
|
@@ -99,7 +112,7 @@ module StateGate
|
|
99
112
|
attr_reader :klass, :name, :states
|
100
113
|
|
101
114
|
|
102
|
-
|
115
|
+
##
|
103
116
|
# initialize and set the class variables
|
104
117
|
#
|
105
118
|
def initialize(klass, name, states) # :nodoc:
|
@@ -108,5 +121,12 @@ module StateGate
|
|
108
121
|
@states = states.map(&:to_s)
|
109
122
|
end
|
110
123
|
|
124
|
+
|
125
|
+
##
|
126
|
+
# convert the value to lowercase and remove and 'force_' prefix
|
127
|
+
#
|
128
|
+
def cast_value(value)
|
129
|
+
value.to_s.downcase.remove(/^force_/)
|
130
|
+
end
|
111
131
|
end # class Type
|
112
132
|
end # StateGate
|