state_gate 1.2.3
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 +7 -0
- data/lib/state_gate/builder/conflict_detection_methods.rb +134 -0
- data/lib/state_gate/builder/dynamic_module_creation_methods.rb +120 -0
- data/lib/state_gate/builder/scope_methods.rb +96 -0
- data/lib/state_gate/builder/state_methods.rb +289 -0
- data/lib/state_gate/builder/transition_methods.rb +142 -0
- data/lib/state_gate/builder/transition_validation_methods.rb +247 -0
- data/lib/state_gate/builder.rb +244 -0
- data/lib/state_gate/engine/configurator.rb +230 -0
- data/lib/state_gate/engine/errator.rb +63 -0
- data/lib/state_gate/engine/fixer.rb +75 -0
- data/lib/state_gate/engine/scoper.rb +66 -0
- data/lib/state_gate/engine/sequencer.rb +116 -0
- data/lib/state_gate/engine/stator.rb +225 -0
- data/lib/state_gate/engine/transitioner.rb +73 -0
- data/lib/state_gate/engine.rb +65 -0
- data/lib/state_gate/locale/builder_en.yml +14 -0
- data/lib/state_gate/locale/engine_en.yml +58 -0
- data/lib/state_gate/locale/state_gate_en.yml +5 -0
- data/lib/state_gate/rspec/allow_transitions_on.rb +259 -0
- data/lib/state_gate/rspec/have_states.rb +140 -0
- data/lib/state_gate/rspec.rb +6 -0
- data/lib/state_gate/type.rb +112 -0
- data/lib/state_gate.rb +193 -0
- metadata +83 -0
@@ -0,0 +1,142 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module StateGate
|
4
|
+
class Builder
|
5
|
+
##
|
6
|
+
# = Description
|
7
|
+
#
|
8
|
+
# Multiple private methods allowing StateGate::Builder to generate
|
9
|
+
# transition methods.
|
10
|
+
#
|
11
|
+
# * query the class for all allowed transitions:
|
12
|
+
# Klass.status_transitions # => { pending: [:active],
|
13
|
+
# # active: [:suspended, :archived],
|
14
|
+
# # suspended: [:active, :archived],
|
15
|
+
# # archived: [] }
|
16
|
+
#
|
17
|
+
# * query the class for the allowed transitions for the given state:
|
18
|
+
# Klass.status_transitions_for(:pending) # => [:active]
|
19
|
+
# Klass.status_transitions_for(:active) # => [:suspended, :archived]
|
20
|
+
#
|
21
|
+
# * list the allowed transitions from the current state:
|
22
|
+
# .status_transitions # => [:suspended, :archived]
|
23
|
+
#
|
24
|
+
# * query if a given transition is allowed from the current state:
|
25
|
+
# .status_transitions_to?(:active) # => true
|
26
|
+
#
|
27
|
+
module TransitionMethods
|
28
|
+
|
29
|
+
# Private
|
30
|
+
# ======================================================================
|
31
|
+
private
|
32
|
+
|
33
|
+
|
34
|
+
|
35
|
+
# Add instance methods to the klass that query the allowed transitions
|
36
|
+
#
|
37
|
+
def generate_transition_methods
|
38
|
+
_add__klass__attr_transitions
|
39
|
+
_add__klass__attr_transitions_for
|
40
|
+
|
41
|
+
_add__instance__attr_transitions
|
42
|
+
_add__instance__attr_transitions_to
|
43
|
+
|
44
|
+
return unless @alias
|
45
|
+
|
46
|
+
_add__klass__attr_transitions(@alias)
|
47
|
+
_add__klass__attr_transitions_for(@alias)
|
48
|
+
|
49
|
+
_add__instance__attr_transitions(@alias)
|
50
|
+
_add__instance__attr_transitions_to(@alias)
|
51
|
+
end
|
52
|
+
|
53
|
+
|
54
|
+
|
55
|
+
# ======================================================================
|
56
|
+
# Class methods
|
57
|
+
# ======================================================================
|
58
|
+
|
59
|
+
# Adds a Class method to return a Hash of the allowed transitions for the attribte
|
60
|
+
# eg:
|
61
|
+
# Klass.status_transitions # => { pending: [:active],
|
62
|
+
# active: [:suspended, :archived],
|
63
|
+
# suspended: [:active, :archived],
|
64
|
+
# archived: [] }
|
65
|
+
#
|
66
|
+
def _add__klass__attr_transitions(method_name = @attribute)
|
67
|
+
method_name = "#{method_name}_transitions"
|
68
|
+
|
69
|
+
add__klass__helper_method(method_name, __FILE__, __LINE__ - 2, %(
|
70
|
+
def #{method_name}
|
71
|
+
stateables[:#{@attribute}].transitions
|
72
|
+
end
|
73
|
+
))
|
74
|
+
end
|
75
|
+
|
76
|
+
|
77
|
+
|
78
|
+
# Adds a Class method to return an Array of the allowed attribute transitions for
|
79
|
+
# the provided state.
|
80
|
+
# eg:
|
81
|
+
# Klass.status_transitions_for(:pending) # => [:active]
|
82
|
+
# Klass.status_transitions_for(:active) # => [:suspended, :archived]
|
83
|
+
# Klass.status_transitions_for(:dummy) # => ArgumentError
|
84
|
+
#
|
85
|
+
def _add__klass__attr_transitions_for(method_name = @attribute)
|
86
|
+
method_name = "#{method_name}_transitions_for"
|
87
|
+
|
88
|
+
add__klass__helper_method(method_name, __FILE__, __LINE__ - 2, %(
|
89
|
+
def #{method_name}(state)
|
90
|
+
stateables[:#{@attribute}].transitions_for_state(state)
|
91
|
+
end
|
92
|
+
))
|
93
|
+
end
|
94
|
+
|
95
|
+
|
96
|
+
|
97
|
+
# ======================================================================
|
98
|
+
# Instance methods
|
99
|
+
# ======================================================================
|
100
|
+
|
101
|
+
|
102
|
+
# Adds an instance method to return an Array of the allowed transitions from
|
103
|
+
# the current attribute state.
|
104
|
+
# eg:
|
105
|
+
# .status_transitions # => [:active]
|
106
|
+
# .status_transitions # => [:suspended, :archived]
|
107
|
+
# .status_transitions # => []
|
108
|
+
#
|
109
|
+
def _add__instance__attr_transitions(method_name = @attribute)
|
110
|
+
method_name = "#{method_name}_transitions"
|
111
|
+
|
112
|
+
add__instance__helper_method(method_name, __FILE__, __LINE__ - 2, %(
|
113
|
+
def #{method_name}
|
114
|
+
stateables[:#{@attribute}].transitions_for_state(self[:#{@attribute}])
|
115
|
+
end
|
116
|
+
))
|
117
|
+
end
|
118
|
+
|
119
|
+
|
120
|
+
|
121
|
+
|
122
|
+
# Adds an instance method to return TRUE if the current attribute state can
|
123
|
+
# transition to the queries status.
|
124
|
+
# eg:
|
125
|
+
# .status_transitions_to?(:active) # => true
|
126
|
+
# .status_transitions_to?(:archived) # => false
|
127
|
+
#
|
128
|
+
def _add__instance__attr_transitions_to(method_name = @attribute)
|
129
|
+
method_name = "#{method_name}_transitions_to?"
|
130
|
+
|
131
|
+
add__instance__helper_method(method_name, __FILE__, __LINE__ - 2, %(
|
132
|
+
def #{method_name}(query_state)
|
133
|
+
test_state = StateGate.symbolize(query_state)
|
134
|
+
stateables[:#{@attribute}].transitions_for_state(self[:#{@attribute}])
|
135
|
+
.include?(test_state)
|
136
|
+
end
|
137
|
+
))
|
138
|
+
end
|
139
|
+
|
140
|
+
end # TransitionMethods
|
141
|
+
end # Builder
|
142
|
+
end # StateGate
|
@@ -0,0 +1,247 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module StateGate
|
4
|
+
class Builder
|
5
|
+
##
|
6
|
+
# = Description
|
7
|
+
#
|
8
|
+
# Multiple private methods allowing StateGate::Builder to generate
|
9
|
+
# attribute setter methods for transition validation.
|
10
|
+
#
|
11
|
+
# * initializing the attribute with +Class.new+ :
|
12
|
+
# Klass.new(status: :active) # => ArgumentError
|
13
|
+
#
|
14
|
+
# * initializing the attribute with +Class.create+ :
|
15
|
+
# Klass.create(status: :active) # => ArgumentError
|
16
|
+
#
|
17
|
+
# * initializing the attribute with <tt>Class.create!</tt> :
|
18
|
+
# Klass.create!(status: :active) # => ArgumentError
|
19
|
+
#
|
20
|
+
# * setting the attribute with +attr=+ :
|
21
|
+
# .status = :active # => :active
|
22
|
+
# .status = :archived # => ArgumentError
|
23
|
+
#
|
24
|
+
# * setting the attribute with <tt>[:attr]=</tt> :
|
25
|
+
# [:status] = :active # => :active
|
26
|
+
# [:status] = :archived # => ArgumentError
|
27
|
+
#
|
28
|
+
# * setting the attribute with <tt>attributes=</tt> :
|
29
|
+
# .attrubutes = {status: :active} # => :active
|
30
|
+
# .attributes = {status: :archived } # => ArgumentError
|
31
|
+
#
|
32
|
+
# * setting the attribute with <tt>assign_attributes</tt> :
|
33
|
+
# .assign_attrubutes(status: :active) # => :active
|
34
|
+
# .assign_attributes(status: :archived) # => ArgumentError
|
35
|
+
#
|
36
|
+
# * updating the attribute with <tt>Class.update</tt> :
|
37
|
+
# Klass.update(instance.id, status: :active) # => :active
|
38
|
+
# Klass.update(instance.id, status: :archived) # => ArgumentError
|
39
|
+
#
|
40
|
+
# * updating the attribute with <tt>.update</tt> :
|
41
|
+
# .update(status: :active) # => :active
|
42
|
+
# .update(status: :archived) # => ArgumentError
|
43
|
+
#
|
44
|
+
# * updating the attribute with <tt>.update_column</tt> :
|
45
|
+
# .update_column(:status, :active) # => :active
|
46
|
+
# .update_column(:status, :archived) # => ArgumentError
|
47
|
+
#
|
48
|
+
# * updating the attribute with <tt>.update_columns</tt> :
|
49
|
+
# .update_columns(status: :active) # => :active
|
50
|
+
# .update_columns(status: :archived) # => ArgumentError
|
51
|
+
#
|
52
|
+
# * updating the attribute with <tt>.write_attribute</tt> :
|
53
|
+
# .write_attribute(:status, :active) # => :active
|
54
|
+
# .write_attribute(:status, :archived) # => ArgumentError
|
55
|
+
#
|
56
|
+
#
|
57
|
+
# === | Forcing a change
|
58
|
+
#
|
59
|
+
# To force a status change that would otherwise be prohibited, preceed the
|
60
|
+
# new state with +force_+ :
|
61
|
+
# .status = :archived # => ArgumentError
|
62
|
+
# .status = :force_archived # => :archived
|
63
|
+
#
|
64
|
+
module TransitionValidationMethods
|
65
|
+
|
66
|
+
# Private
|
67
|
+
# ======================================================================
|
68
|
+
private
|
69
|
+
|
70
|
+
|
71
|
+
|
72
|
+
# Add prepended instance methods to the klass that catch all methods for
|
73
|
+
# updating the attribute and validated the new value is an allowed transition
|
74
|
+
#
|
75
|
+
# Note:
|
76
|
+
# These methods are only added if the engine has an
|
77
|
+
# include_transition_validations? status on initialisation
|
78
|
+
#
|
79
|
+
# Note:
|
80
|
+
# The three methods "<atrr>=(val)", "write_attribute(<attr>, val)" and
|
81
|
+
# "update_columns(<attr>: val)" cover all the possibilities of setting the
|
82
|
+
# attribute through ActiveRecord.
|
83
|
+
#
|
84
|
+
def generate_transition_validation_methods
|
85
|
+
return if @engine.transitionless?
|
86
|
+
|
87
|
+
_prepend__attribute_equals
|
88
|
+
_prepend__write_attribute
|
89
|
+
_prepend__update_columns
|
90
|
+
_prepend__initialize
|
91
|
+
end
|
92
|
+
|
93
|
+
|
94
|
+
|
95
|
+
# ======================================================================
|
96
|
+
# Prepend Module
|
97
|
+
# ======================================================================
|
98
|
+
|
99
|
+
# Dynamically generated module to hold the validation setter methods
|
100
|
+
# and is pre-pended to the class.
|
101
|
+
#
|
102
|
+
# A new module is create if it doesn't already exist.
|
103
|
+
#
|
104
|
+
# Note:
|
105
|
+
# the module is named "StateGate::<klass>TranstionValidationMethods"
|
106
|
+
#
|
107
|
+
def _transition_validation_module # rubocop:disable Metrics/MethodLength
|
108
|
+
@_transition_validation_module ||= begin
|
109
|
+
mod_name = 'StateGate_ValidationMethods'
|
110
|
+
|
111
|
+
if @klass.const_defined?(mod_name)
|
112
|
+
"#{@klass}::#{mod_name}".constantize
|
113
|
+
else
|
114
|
+
@klass.const_set(mod_name, Module.new)
|
115
|
+
mod = "#{@klass}::#{mod_name}".constantize
|
116
|
+
@klass.prepend mod
|
117
|
+
mod
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
|
123
|
+
|
124
|
+
# ======================================================================
|
125
|
+
# Instance methods
|
126
|
+
# ======================================================================
|
127
|
+
|
128
|
+
# Adds a method to overwrite the attribute :<attr>=(val) setter, raising an error
|
129
|
+
# if the supplied value is not a valid transition
|
130
|
+
# eg:
|
131
|
+
# .status = :archived # => ArgumentError
|
132
|
+
# .status - :active # => :active
|
133
|
+
#
|
134
|
+
# ==== actions
|
135
|
+
#
|
136
|
+
# + assert it's a valid transition
|
137
|
+
# + call super
|
138
|
+
#
|
139
|
+
def _prepend__attribute_equals
|
140
|
+
attr_name = @attribute
|
141
|
+
|
142
|
+
_transition_validation_module.module_eval(%(
|
143
|
+
def #{attr_name}=(new_val)
|
144
|
+
stateables[StateGate.symbolize(:#{attr_name})] \
|
145
|
+
&.assert_valid_transition!(self[:#{attr_name}], new_val)
|
146
|
+
super(new_val)
|
147
|
+
end
|
148
|
+
), __FILE__, __LINE__ - 6)
|
149
|
+
end
|
150
|
+
|
151
|
+
|
152
|
+
|
153
|
+
# Adds a method to overwrite the instance :write_attribute(attr, val) setter,
|
154
|
+
# raising an error if the supplied value is not a valid transition
|
155
|
+
# eg:
|
156
|
+
# .write_attribute(:status, :archived) # => ArgumentError
|
157
|
+
# .write_attribute(:status, :active) # => :active
|
158
|
+
#
|
159
|
+
# ==== actions
|
160
|
+
#
|
161
|
+
# + loop through each attribute
|
162
|
+
# + get the base attribute name from any alias used
|
163
|
+
# + assert it's a valid transition
|
164
|
+
# + call super
|
165
|
+
#
|
166
|
+
def _prepend__write_attribute
|
167
|
+
return if _transition_validation_module.method_defined?(:write_attribute)
|
168
|
+
|
169
|
+
_transition_validation_module.module_eval(%(
|
170
|
+
def write_attribute(attrribute_name, new_val = nil)
|
171
|
+
name = attrribute_name.to_s.downcase
|
172
|
+
name = self.class.attribute_aliases[name] || name
|
173
|
+
|
174
|
+
stateables[StateGate.symbolize(name)] \
|
175
|
+
&.assert_valid_transition!(self[name], new_val)
|
176
|
+
super(attrribute_name, new_val)
|
177
|
+
end
|
178
|
+
), __FILE__, __LINE__ - 9)
|
179
|
+
end
|
180
|
+
|
181
|
+
|
182
|
+
|
183
|
+
# Adds a method to overwrite the instance :update_columns(attr: val) setter,
|
184
|
+
# raising an error if the supplied value is not a valid transition
|
185
|
+
# eg:
|
186
|
+
# .update_columns(status: :archived) # => ArgumentError
|
187
|
+
# .update_columns(status: :active) # => :active
|
188
|
+
#
|
189
|
+
# ==== actions
|
190
|
+
#
|
191
|
+
# + loop through each attribute
|
192
|
+
# + get the base attribute name from any alias used
|
193
|
+
# + assert it's a valid transition
|
194
|
+
# + call super
|
195
|
+
#
|
196
|
+
def _prepend__update_columns # rubocop:disable Metrics/MethodLength
|
197
|
+
return if _transition_validation_module.method_defined?(:update_columns)
|
198
|
+
|
199
|
+
_transition_validation_module.module_eval(%(
|
200
|
+
def update_columns(args)
|
201
|
+
super(args) and return if (new_record? || destroyed?)
|
202
|
+
|
203
|
+
args.each do |key, value|
|
204
|
+
name = key.to_s.downcase
|
205
|
+
name = self.class.attribute_aliases[name] || name
|
206
|
+
|
207
|
+
stateables[StateGate.symbolize(name)] \
|
208
|
+
&.assert_valid_transition!(self[name], value)
|
209
|
+
end
|
210
|
+
|
211
|
+
super
|
212
|
+
end
|
213
|
+
), __FILE__, __LINE__ - 14)
|
214
|
+
end
|
215
|
+
|
216
|
+
|
217
|
+
|
218
|
+
# Prepends an :itialize method to ensure the attribute is not set on initializing
|
219
|
+
# a new instance unless :forced.
|
220
|
+
#
|
221
|
+
# Klass.new(status: :archived) # => ArgumentError
|
222
|
+
#
|
223
|
+
def _prepend__initialize # rubocop:disable Metrics/MethodLength
|
224
|
+
return if _transition_validation_module.method_defined?(:initialize)
|
225
|
+
|
226
|
+
_transition_validation_module.module_eval(%(
|
227
|
+
def initialize(attributes = nil, &block)
|
228
|
+
attributes&.each do |attr_name, value|
|
229
|
+
key = self.class.attribute_aliases[attr_name.to_s] || attr_name
|
230
|
+
if self.stateables.keys.include?(key.to_sym)
|
231
|
+
unless value.to_s.start_with?('force_')
|
232
|
+
msg = ":\#{attr_name} may not be included in the parameters for a new" \
|
233
|
+
" \#{self.class.name}. Create the new instance first, then transition" \
|
234
|
+
" :\#{attr_name} as required."
|
235
|
+
fail ArgumentError, msg
|
236
|
+
end
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
super
|
241
|
+
end
|
242
|
+
), __FILE__, __LINE__ - 13)
|
243
|
+
end
|
244
|
+
|
245
|
+
end # LockingMethods
|
246
|
+
end # Builder
|
247
|
+
end # StateGate
|
@@ -0,0 +1,244 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'engine'
|
4
|
+
require_relative 'type'
|
5
|
+
Dir[File.join(__dir__, 'builder', '*.rb')].sort.each { |file| require file }
|
6
|
+
|
7
|
+
module StateGate
|
8
|
+
##
|
9
|
+
# = Description
|
10
|
+
#
|
11
|
+
# Responsible for generating the state gate engine, along
|
12
|
+
# with the Class and Instance helper methods for the submitted Klass. Everything is
|
13
|
+
# generated from +#initialize+ when a new instance is created.
|
14
|
+
#
|
15
|
+
# Both Class and Instance methods are generated for:
|
16
|
+
#
|
17
|
+
# * state interaction
|
18
|
+
# * state sequences
|
19
|
+
# * state scopes
|
20
|
+
# * transition interaction
|
21
|
+
# * transition validation
|
22
|
+
#
|
23
|
+
class Builder
|
24
|
+
|
25
|
+
include StateMethods
|
26
|
+
include ScopeMethods
|
27
|
+
include TransitionMethods
|
28
|
+
include ConflictDetectionMethods
|
29
|
+
include TransitionValidationMethods
|
30
|
+
include DynamicModuleCreationMethods
|
31
|
+
|
32
|
+
|
33
|
+
|
34
|
+
# Private
|
35
|
+
# ======================================================================
|
36
|
+
private
|
37
|
+
|
38
|
+
# Initialize the Builder, creating the state gate and generating all the
|
39
|
+
# Class and Instace helper methods on the sumbitted klass.
|
40
|
+
#
|
41
|
+
# [:klass]
|
42
|
+
# The class containing the attribute to be cast as a state gate
|
43
|
+
# (Class | required)
|
44
|
+
#
|
45
|
+
# [:attribute_name]
|
46
|
+
# The name of the klass attribute to use for the state gate
|
47
|
+
# (Symbol | required)
|
48
|
+
#
|
49
|
+
# [:config]
|
50
|
+
# The configuration block for the state gate.
|
51
|
+
# (Block | required)
|
52
|
+
#
|
53
|
+
# StateGate::Builder.new(Klass, :attribute_name) do
|
54
|
+
# ... configuration ...
|
55
|
+
# end
|
56
|
+
#
|
57
|
+
def initialize(klass = nil, attribute_name = nil, &config)
|
58
|
+
@klass = klass
|
59
|
+
@attribute = attribute_name
|
60
|
+
@alias = nil
|
61
|
+
|
62
|
+
# assert the input is valid
|
63
|
+
_assert_klass_is_valid_for_state_gate
|
64
|
+
_parse_atttribute_name_for_alias
|
65
|
+
_assert_no_existing_state_gate_for_attribute
|
66
|
+
_assert_attribute_name_is_a_database_string_column
|
67
|
+
|
68
|
+
# build the engine and cast the attribute
|
69
|
+
_build_state_gate_engine(&config)
|
70
|
+
_cast_attribute_as_state_gate
|
71
|
+
|
72
|
+
# generate the helper methods
|
73
|
+
generate_helper_methods
|
74
|
+
end
|
75
|
+
|
76
|
+
|
77
|
+
|
78
|
+
# Generate the helper methods
|
79
|
+
#
|
80
|
+
def generate_helper_methods
|
81
|
+
# add the helper methods
|
82
|
+
generate_scope_methods
|
83
|
+
generate_state_methods
|
84
|
+
generate_transition_methods
|
85
|
+
generate_transition_validation_methods
|
86
|
+
|
87
|
+
# warn if any state gate attribute methods are redefined
|
88
|
+
_generate_method_redefine_detection
|
89
|
+
end
|
90
|
+
|
91
|
+
|
92
|
+
|
93
|
+
# Validate the klass to ensure it is a 'Class' and derived from ActiveRecord,
|
94
|
+
# raising an error if not.
|
95
|
+
#
|
96
|
+
def _assert_klass_is_valid_for_state_gate
|
97
|
+
err(:non_class_err, klass: true) unless @klass.is_a?(Class)
|
98
|
+
err(:non_ar_err, klass: true) unless @klass.ancestors.include?(::ActiveRecord::Base)
|
99
|
+
end
|
100
|
+
|
101
|
+
|
102
|
+
|
103
|
+
# Parse the attribute name to ensure it is a valid input value and
|
104
|
+
# detect if it's an attribute_alias.
|
105
|
+
#
|
106
|
+
# = meta
|
107
|
+
#
|
108
|
+
# * ensure it exists
|
109
|
+
# * ensure it's a Symbol, to avoid string whitespace issues
|
110
|
+
# * check is it's a registere attribute
|
111
|
+
# * update @attribute & @alias
|
112
|
+
#
|
113
|
+
def _parse_atttribute_name_for_alias
|
114
|
+
if @attribute.nil?
|
115
|
+
err :missing_attribute_err, klass: true
|
116
|
+
|
117
|
+
elsif !@attribute.is_a?(Symbol)
|
118
|
+
err :attribute_type_err
|
119
|
+
|
120
|
+
elsif @klass.attribute_aliases[@attribute.to_s]
|
121
|
+
@alias = @attribute
|
122
|
+
@attribute = @klass.attribute_aliases[@attribute.to_s].to_sym
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
|
127
|
+
|
128
|
+
# Validate we don't already have a state gate defined for the attribute,
|
129
|
+
# raising an error if not.
|
130
|
+
#
|
131
|
+
def _assert_no_existing_state_gate_for_attribute
|
132
|
+
return unless @klass.methods(false).include?(:stateables)
|
133
|
+
return unless @klass.stateables.keys.include?(@attribute)
|
134
|
+
|
135
|
+
err :existing_state_gate_err, kattr: true
|
136
|
+
end # parse_attribute_name
|
137
|
+
|
138
|
+
|
139
|
+
|
140
|
+
# Validate the attribute is a database String attribute,
|
141
|
+
# raising an error if not.
|
142
|
+
#
|
143
|
+
# = meta
|
144
|
+
#
|
145
|
+
# * ensure it's mapped to a database column
|
146
|
+
# * ensure it's a :string databse type
|
147
|
+
#
|
148
|
+
def _assert_attribute_name_is_a_database_string_column
|
149
|
+
if @klass.column_names.exclude?(@attribute.to_s)
|
150
|
+
err :non_db_attr_err, kattr: true
|
151
|
+
|
152
|
+
elsif @klass.columns_hash[@attribute.to_s].type != :string
|
153
|
+
err :non_string_column_err, kattr: true,
|
154
|
+
attr_type: @klass.columns_hash[@attribute.to_s].type
|
155
|
+
end
|
156
|
+
end # parse_attribute_name
|
157
|
+
|
158
|
+
|
159
|
+
|
160
|
+
# Builds a StateGate::Engine for the given attribute and add it to
|
161
|
+
# the :stateables repository.
|
162
|
+
#
|
163
|
+
# config - the user generated configuration for the engine, including states,
|
164
|
+
# transitions and optional settings
|
165
|
+
#
|
166
|
+
def _build_state_gate_engine(&config)
|
167
|
+
_initialize_state_gate_repository
|
168
|
+
@engine = StateGate::Engine.new(@klass.name, @attribute, &config)
|
169
|
+
@klass.stateables[@attribute] = @engine # rubocop:disable Layout/ExtraSpacing
|
170
|
+
end
|
171
|
+
|
172
|
+
|
173
|
+
|
174
|
+
# Adds a :stateables class_attribute if it doesn't already exist and
|
175
|
+
# initializes it to an empty Hash.
|
176
|
+
#
|
177
|
+
# :stateables contains is a repository for the state gate engines
|
178
|
+
# created when generating state gates. The state gate attribute name is used
|
179
|
+
# as the key.
|
180
|
+
#
|
181
|
+
# Example
|
182
|
+
# Klass.stateables # => {
|
183
|
+
# status: <StateGate::Engine>,
|
184
|
+
# account: <StateGate::Engine>
|
185
|
+
# }
|
186
|
+
#
|
187
|
+
# Note
|
188
|
+
#
|
189
|
+
# The default empty Hash is set after the attribute is created to accommodate
|
190
|
+
# ActiveRecord 5.0, even though ActiveRecord 6.0 allows it to be set within
|
191
|
+
# the .class_attribute method.
|
192
|
+
#
|
193
|
+
def _initialize_state_gate_repository
|
194
|
+
return if @klass.methods(false).include?(:stateables)
|
195
|
+
|
196
|
+
@klass.class_attribute(:stateables, instance_writer: false)
|
197
|
+
@klass.stateables = {}
|
198
|
+
end
|
199
|
+
|
200
|
+
|
201
|
+
|
202
|
+
# Builds a StateGate::Type with the custom states for the attribute,
|
203
|
+
# then casts the attribute.
|
204
|
+
#
|
205
|
+
# This ensures that regardless of the setter method used to set a new state, even
|
206
|
+
# if transition validations are disabled, an invalid state should never reach the
|
207
|
+
# database.
|
208
|
+
#
|
209
|
+
# meta
|
210
|
+
#
|
211
|
+
# * retrieve the root attribute name if the supplied attribute is an alias.
|
212
|
+
# * create a StateGate::Type with attributes states.
|
213
|
+
# * overwrite the attribute, casting the type as the new StateGate::Type
|
214
|
+
#
|
215
|
+
def _cast_attribute_as_state_gate
|
216
|
+
states = @engine.states
|
217
|
+
attr_type = StateGate::Type.new(@klass.name, @attribute, states)
|
218
|
+
@klass.attribute(@attribute, attr_type, default: @engine.default_state)
|
219
|
+
end
|
220
|
+
|
221
|
+
|
222
|
+
|
223
|
+
# Raise an ArgumentError for the given error, using I18n for the message.
|
224
|
+
#
|
225
|
+
# err - Symbol key for the I18n message
|
226
|
+
#
|
227
|
+
# args - Hash of attributes to pass to the message string.
|
228
|
+
#
|
229
|
+
# [:klass] When true, args[:klass] will be updated with the 'KlassName'.
|
230
|
+
# [:kattr] When true, args[:kattr] will be updated with 'KlassName#attribute'.
|
231
|
+
#
|
232
|
+
# Example
|
233
|
+
#
|
234
|
+
# err(:invalid_attribute_type_err, kattr: true)
|
235
|
+
#
|
236
|
+
def err(err, **args)
|
237
|
+
args[:klass] = @klass if args.dig(:klass) == true
|
238
|
+
args[:kattr] = "#{@klass}##{@attribute}" if args.dig(:kattr) == true
|
239
|
+
|
240
|
+
fail ArgumentError, I18n.t("state_gate.builder.#{err}", **args)
|
241
|
+
end
|
242
|
+
|
243
|
+
end # class Builder
|
244
|
+
end # module StateGate
|