state_gate 1.2.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|