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.
@@ -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