state_gate 1.2.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,230 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StateGate
4
+ class Engine
5
+ ##
6
+ # = Description
7
+ #
8
+ # Parses the configuration for StateGate::Engine.
9
+ #
10
+ # The configuration defines the state names, allowed transitions and a number of
11
+ # options to help customise the _state-gate_ to your exact preference.
12
+ #
13
+ # Options include:
14
+ #
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
+ # state :state_name
19
+ #
20
+ #
21
+ # [:transitions_to]
22
+ # An optional list of the other state that this state is allowed to change to.
23
+ # state :state_1, transtions_to: [:state_2, :state_3, :state_4]
24
+ # state :state_2, transtions_to: :state_4
25
+ # state :state_3, transtions_to: :any
26
+ # state :state_4
27
+ #
28
+ # [:human]
29
+ # An optional String name to used when displaying gthe state in a view. If no
30
+ # name is specified, it will default to +:state.titleized+.
31
+ # state :state_1, transtions_to: [:state_2, :state_3], human: "My State"
32
+ #
33
+ #
34
+ # === | default
35
+ # Optional setting to specify the default state for a new object. The state name
36
+ # is given as a Symbol.
37
+ # default :state_name
38
+ #
39
+ #
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
+ #
45
+ #
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
+ #
51
+ #
52
+ # === | make_sequential
53
+ # Optional setting to automatically add transitions from each state to both the
54
+ # preceeding and following states.
55
+ # make_sequential
56
+ #
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
+ #
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
+ #
67
+ #
68
+ # === | no_scopes
69
+ # Optional setting to disable the generation of Class Scope helpers methods.
70
+ # no_scopes
71
+ #
72
+ module Configurator
73
+
74
+ # = Private
75
+ # ======================================================================
76
+ private
77
+
78
+
79
+ # ======================================================================
80
+ # Configuration Commands
81
+ # ======================================================================
82
+
83
+
84
+ # Execute the provided configuration.
85
+ #
86
+ # ==== actions
87
+ #
88
+ # + create sequence links and transitions
89
+ # + create scope names
90
+ # + remove duplicate transitions for each state
91
+ #
92
+ # + verify there are multiple valid state names
93
+ # + verify all transitions lead to existing states
94
+ # + verify each state, except the default, can be reached from a transition
95
+ #
96
+ def parse_configuration(&config)
97
+ exec_configuration(&config)
98
+
99
+ generate_sequences
100
+ generate_scope_names
101
+
102
+ assert_states_are_valid
103
+ assert_transitions_exist
104
+ assert_uniq_transitions
105
+ assert_any_has_been_expanded
106
+ assert_all_transitions_are_states
107
+ assert_all_states_are_reachable
108
+ end
109
+
110
+
111
+
112
+ # Run the configuration commands.
113
+ #
114
+ # ==== actions
115
+ #
116
+ # + create sequence links and transitions
117
+ # + create scope names
118
+ # + remove duplicate transitions for each state
119
+ #
120
+ # + verify there are multiple valid state names
121
+ # + verify all transitions lead to existing states
122
+ # + verify each state, except the default, can be reached from a transition
123
+ #
124
+ def exec_configuration(&config)
125
+ instance_exec(&config)
126
+ rescue NameError => e
127
+ err_command = e.to_s.gsub('undefined local variable or method `', '')
128
+ .split("'")
129
+ .first
130
+ cerr :bad_command, cmd: err_command
131
+ end
132
+
133
+
134
+
135
+ # ======================================================================
136
+ # Assertions
137
+ # ======================================================================
138
+
139
+ # Ensure there are enough states and the default is a valid state, setting
140
+ # the default to the first state if required.
141
+ #
142
+ def assert_states_are_valid
143
+ state_names = @states.keys
144
+
145
+ # are there states
146
+ cerr(:states_missing_err) if state_names.blank?
147
+
148
+ # is there more than one state
149
+ cerr(:single_state_err) if state_names.one?
150
+
151
+ # set the deafult state if needed, otherwise check it is a valid state
152
+ if @default
153
+ cerr(:default_state_err) unless state_names.include?(@default)
154
+ else
155
+ @default = state_names.first
156
+ end
157
+ end
158
+
159
+
160
+
161
+ # Ensure that transitions have been specified. If not, then add the transitions
162
+ # to allow every stater to transition to another state and flag the engine as
163
+ # transitionless, so we don't add any validation methods.
164
+ #
165
+ def assert_transitions_exist
166
+ return if @states.map { |_state, opts| opts[:transitions_to] }.uniq.flatten.any?
167
+
168
+ @transitionless = true
169
+ @states.keys.each do |key|
170
+ @states[key][:transitions_to] = @states.keys - [key]
171
+ end
172
+ end
173
+
174
+
175
+
176
+ # Ensure that there is only one of reach transition
177
+ #
178
+ def assert_uniq_transitions
179
+ @states.each { |_state, opts| opts[:transitions_to].uniq! }
180
+ end
181
+
182
+
183
+
184
+ # Ensure that the :any transition is expanded or raise an exception
185
+ # if it's included with other transitions
186
+ def assert_any_has_been_expanded
187
+ @states.each do |state_name, opts|
188
+ if opts[:transitions_to] == [:any]
189
+ @states[state_name][:transitions_to] = @states.keys - [state_name]
190
+
191
+ elsif opts[:transitions_to].include?(:any)
192
+ cerr(:any_transition_err, state: state_name, kattr: true)
193
+ end
194
+ end
195
+ end
196
+
197
+
198
+
199
+ # Ensure all transitions are to valid states.
200
+ #
201
+ # Replaces transition to :any with a list of all states
202
+ # Raises an exception if :any in included with a list of other transitions
203
+ #
204
+ def assert_all_transitions_are_states
205
+ @states.each do |state_name, opts|
206
+ opts[:transitions_to].each do |transition|
207
+ unless @states.keys.include?(transition)
208
+ cerr(:transition_state_err, state: state_name, transition: transition, kattr: true)
209
+ end
210
+ end
211
+ end
212
+ end
213
+
214
+
215
+
216
+ # Ensure there is a transition leading to every non-default state.
217
+ #
218
+ def assert_all_states_are_reachable
219
+ # is there a transition to every state except the default.
220
+ transitions = @states.map { |_state, opts| opts[:transitions_to] }.flatten.uniq
221
+ adrift_states = (@states.keys - transitions - [@default])
222
+ return if adrift_states.blank?
223
+
224
+ states = adrift_states.map { |s| ':' + s.to_s }.to_sentence
225
+ cerr(:transitionless_states_err, states: states, kattr: true)
226
+ end
227
+
228
+ end # ConfigurationMethods
229
+ end # Engine
230
+ end # StateGate
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StateGate
4
+ class Engine
5
+ ##
6
+ # = Description
7
+ #
8
+ # Adds error reporting methods to a StateMachine::Engine
9
+ # * all error messages are I18n configured
10
+ # * method names are deliberately short to encourage code readability with
11
+ # if/unless one-liners
12
+ module Errator
13
+
14
+ # Private
15
+ # ======================================================================
16
+ private
17
+
18
+ # Format the given value and report an ArgumentError
19
+ #
20
+ def invalid_state_error(val)
21
+ case val
22
+ when NilClass
23
+ aerr :invalid_state_err, val: "'nil'", kattr: true
24
+ when Symbol
25
+ aerr :invalid_state_err, val: ":#{val}", kattr: true
26
+ else
27
+ aerr :invalid_state_err, val: "'#{val&.to_s}'", kattr: true
28
+ end
29
+ end
30
+
31
+
32
+
33
+ # Report a ConfigurationError, including the Klass#attr variable.
34
+ #
35
+ def cerr(err, **args)
36
+ args[:kattr] = "#{@klass}##{@attribute}" if args[:kattr] == true
37
+ key = "state_gate.engine.config.#{err}"
38
+ fail ConfigurationError, I18n.t(key, **args)
39
+ end
40
+
41
+
42
+
43
+ # Report a RuntimeError, including the Klass#attr variable.
44
+ #
45
+ def rerr(err, **args)
46
+ args[:kattr] = "#{@klass}##{@attribute}" if args[:kattr] == true
47
+ key = "state_gate.engine.#{err}"
48
+ fail I18n.t(key, **args)
49
+ end
50
+
51
+
52
+
53
+ # Report an ArgumentError, including the Klass#attr variable.
54
+ #
55
+ def aerr(err, **args)
56
+ args[:kattr] = "#{@klass}##{@attribute}" if args[:kattr] == true
57
+ key = "state_gate.engine.#{err}"
58
+ fail ArgumentError, I18n.t(key, **args)
59
+ end
60
+
61
+ end # Errator
62
+ end # Engine
63
+ end # StateGate
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StateGate
4
+ class Engine
5
+ ##
6
+ # = Description
7
+ #
8
+ # Provides prefix and suffix helper methods for StateGate::Engine.
9
+ #
10
+ module Fixer
11
+
12
+ # ======================================================================
13
+ # Configuration Methods
14
+ # ======================================================================
15
+
16
+ # A phrase to add before state names when using Class Scopes.
17
+ # This helps differential attributes that have similar state names.
18
+ # (Symbol | optional)
19
+ #
20
+ # prefix :before # => Class.before_active
21
+ #
22
+ def prefix(val = nil)
23
+ cerr(:prefix_type_err, kattr: true) unless val.is_a?(Symbol)
24
+ cerr(:prefix_multiple_err, kattr: true) if @prefix
25
+ @prefix = "#{val.to_s.downcase}_"
26
+ end # prefix
27
+
28
+
29
+
30
+ # A phrase to add before state names when using Class Scopes.
31
+ # This helps differential attributes that have similar state names.
32
+ # (Symbol | optional)
33
+ #
34
+ # suffix :after # => Class.active_after
35
+ #
36
+ def suffix(val = nil)
37
+ cerr(:suffix_type_err, kattr: true) unless val.is_a?(Symbol)
38
+ cerr(:suffix_multiple_err, kattr: true) if @suffix
39
+ @suffix = "_#{val.to_s.downcase}"
40
+ end # suffix
41
+
42
+
43
+
44
+ # ======================================================================
45
+ # Helper Methods
46
+ # ======================================================================
47
+
48
+ ##
49
+ # Returns the defined prefix for the state_gate, or an empty string if no
50
+ # prefix has been defined.
51
+ #
52
+ # .state_prefix # => 'my_prefix'
53
+ # .state_prefix # => ''
54
+ #
55
+ def state_prefix
56
+ @prefix
57
+ end
58
+
59
+
60
+
61
+ ##
62
+ # Returns the defined suffix for the state_gate, or an empty string if no
63
+ # suffix has been defined.
64
+ #
65
+ # .state_suffix # => 'my_suffix'
66
+ # .state_suffix # => ''
67
+ #
68
+ def state_suffix
69
+ @suffix
70
+ end
71
+
72
+
73
+ end # Sequencer
74
+ end # Engine
75
+ end # StateGate
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StateGate
4
+ class Engine
5
+ ##
6
+ # = Description
7
+ #
8
+ # Provides scope helper methods for StateGate::Engine.
9
+ #
10
+ module Scoper
11
+
12
+ # ======================================================================
13
+ # Configuration Methods
14
+ # ======================================================================
15
+
16
+ # Disables the generation of Class Scope helpers methods
17
+ #
18
+ # no_scopes
19
+ #
20
+ def no_scopes
21
+ @scopes = false
22
+ end
23
+
24
+
25
+
26
+ # Generate the scope name to use for each state.
27
+ #
28
+ def generate_scope_names
29
+ @states.each do |state, opts|
30
+ opts[:scope_name] = "#{@prefix}#{state}#{@suffix}"
31
+ end # each state
32
+ end # generate_sequences
33
+
34
+
35
+
36
+ # ======================================================================
37
+ # Helper Methods
38
+ # ======================================================================
39
+
40
+ ##
41
+ # Returns TRUE if scope methods should be added to the model, otherwise FALSE.
42
+ #
43
+ # .include_scopes? # => true
44
+ #
45
+ def include_scopes?
46
+ !!@scopes
47
+ end
48
+
49
+
50
+
51
+ ##
52
+ # Returns the scope name for the given state. Scope names are generated by
53
+ # concatenating the prefix, state name and suffix
54
+ #
55
+ # .scope_name_for_state(:active) # => 'active'
56
+ # .scope_name_for_state(:pending) # => 'pending_status'
57
+ # .scope_name_for_state(:archived) # => 'with_archived_status'
58
+ #
59
+ def scope_name_for_state(state_name = nil)
60
+ state = assert_valid_state!(state_name)
61
+ @states[state][:scope_name]
62
+ end
63
+
64
+ end # Sequencer
65
+ end # Engine
66
+ end # StateGate
@@ -0,0 +1,116 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StateGate
4
+ class Engine
5
+ ##
6
+ # = Description
7
+ #
8
+ # Provides state sequence helper methods for StateGate::Engine.
9
+ #
10
+ # All methods raise an error if +sequential?+ is FALSE
11
+ #
12
+ module Sequencer
13
+
14
+ # ======================================================================
15
+ # Configuration Methods
16
+ # ======================================================================
17
+
18
+ # Automatically add transitions from each state to the preceeding and following states.
19
+ # make_sequential
20
+ #
21
+ # [:one_way]
22
+ # Only adds tranitions from each state to the follow state. (optional)
23
+ # make_sequential :one_way
24
+ #
25
+ # [:loop]
26
+ # Adds transitions from the last state to the first and from the first to the last
27
+ # (unless also :one_way) (optional)
28
+ # make_sequential :one_way, :loop
29
+ #
30
+ def make_sequential(*args)
31
+ @sequential = true
32
+ @sequential_loop = true if args.include?(:loop)
33
+ @sequential_one_way = true if args.include?(:one_way)
34
+ end
35
+
36
+
37
+
38
+ # Add sequence hooks if sequential requested.
39
+ #
40
+ def generate_sequences
41
+ return unless sequential?
42
+
43
+ add_previous_sequential_state
44
+ add_next_sequential_state
45
+ loop_sequence
46
+ end # generate_sequences
47
+
48
+
49
+
50
+ # Add the previous sequential state
51
+ #
52
+ def add_previous_sequential_state
53
+ return if @sequential_one_way
54
+
55
+ previous_state = nil
56
+ @states.keys.each do |state|
57
+ if previous_state
58
+ @states[state][:previous_state] = previous_state
59
+ @states[state][:transitions_to] << previous_state
60
+ end
61
+ previous_state = state
62
+ end
63
+ end
64
+
65
+
66
+
67
+ # Add the next sequential state
68
+ #
69
+ def add_next_sequential_state
70
+ next_state = nil
71
+ @states.keys.reverse.each do |state|
72
+ if next_state
73
+ @states[state][:next_state] = next_state
74
+ @states[state][:transitions_to] << next_state
75
+ end
76
+ next_state = state
77
+ end
78
+ end
79
+
80
+
81
+
82
+ # Add the first and last transitions to complete the sequential loop.
83
+ #
84
+ def loop_sequence
85
+ return unless @sequential_loop
86
+
87
+ first_state = @states.keys.first
88
+ last_state = @states.keys.last
89
+
90
+ @states[last_state][:next_state] = first_state
91
+ @states[last_state][:transitions_to] << first_state
92
+
93
+ return if @sequential_one_way
94
+
95
+ @states[first_state][:previous_state] = last_state
96
+ @states[first_state][:transitions_to] << last_state
97
+ end # loop_sequence
98
+
99
+
100
+
101
+ # ======================================================================
102
+ # Helper Methods
103
+ # ======================================================================
104
+
105
+ ##
106
+ # return TRUE if the state_gate is sequential, otherwise FALSE.
107
+ #
108
+ # .sequential? # => TRUE
109
+ #
110
+ def sequential?
111
+ !!@sequential
112
+ end
113
+
114
+ end # Sequencer
115
+ end # Engine
116
+ end # StateGate