state_machines 0.6.0 → 0.20.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.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +93 -13
  3. data/lib/state_machines/branch.rb +8 -4
  4. data/lib/state_machines/callback.rb +2 -0
  5. data/lib/state_machines/core.rb +3 -2
  6. data/lib/state_machines/core_ext/class/state_machine.rb +2 -0
  7. data/lib/state_machines/core_ext.rb +2 -0
  8. data/lib/state_machines/error.rb +2 -0
  9. data/lib/state_machines/eval_helpers.rb +38 -9
  10. data/lib/state_machines/event.rb +22 -7
  11. data/lib/state_machines/event_collection.rb +2 -0
  12. data/lib/state_machines/extensions.rb +2 -0
  13. data/lib/state_machines/helper_module.rb +2 -0
  14. data/lib/state_machines/integrations/base.rb +2 -0
  15. data/lib/state_machines/integrations.rb +2 -0
  16. data/lib/state_machines/machine/class_methods.rb +79 -0
  17. data/lib/state_machines/machine.rb +21 -67
  18. data/lib/state_machines/machine_collection.rb +5 -1
  19. data/lib/state_machines/macro_methods.rb +2 -0
  20. data/lib/state_machines/matcher.rb +3 -0
  21. data/lib/state_machines/matcher_helpers.rb +2 -0
  22. data/lib/state_machines/node_collection.rb +5 -1
  23. data/lib/state_machines/options_validator.rb +72 -0
  24. data/lib/state_machines/path.rb +5 -1
  25. data/lib/state_machines/path_collection.rb +5 -1
  26. data/lib/state_machines/state.rb +71 -43
  27. data/lib/state_machines/state_collection.rb +2 -0
  28. data/lib/state_machines/state_context.rb +5 -1
  29. data/lib/state_machines/stdio_renderer.rb +74 -0
  30. data/lib/state_machines/test_helper.rb +305 -0
  31. data/lib/state_machines/transition.rb +2 -0
  32. data/lib/state_machines/transition_collection.rb +5 -1
  33. data/lib/state_machines/version.rb +3 -1
  34. data/lib/state_machines.rb +4 -1
  35. metadata +11 -9
  36. data/lib/state_machines/assertions.rb +0 -40
@@ -0,0 +1,305 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StateMachines
4
+ # Test helper module providing assertion methods for state machine testing
5
+ # Designed to work with Minitest, RSpec, and future testing frameworks
6
+ #
7
+ # @example Basic usage with Minitest
8
+ # class MyModelTest < Minitest::Test
9
+ # include StateMachines::TestHelper
10
+ #
11
+ # def test_initial_state
12
+ # model = MyModel.new
13
+ # assert_state(model, :state_machine_name, :initial_state)
14
+ # end
15
+ # end
16
+ #
17
+ # @example Usage with RSpec
18
+ # RSpec.describe MyModel do
19
+ # include StateMachines::TestHelper
20
+ #
21
+ # it "starts in initial state" do
22
+ # model = MyModel.new
23
+ # assert_state(model, :state_machine_name, :initial_state)
24
+ # end
25
+ # end
26
+ #
27
+ # @since 0.10.0
28
+ module TestHelper
29
+ # Assert that an object is in a specific state for a given state machine
30
+ #
31
+ # @param object [Object] The object with state machines
32
+ # @param machine_name [Symbol] The name of the state machine
33
+ # @param expected_state [Symbol] The expected state
34
+ # @param message [String, nil] Custom failure message
35
+ # @return [void]
36
+ # @raise [AssertionError] If the state doesn't match
37
+ #
38
+ # @example
39
+ # user = User.new
40
+ # assert_state(user, :status, :active)
41
+ def assert_state(object, machine_name, expected_state, message = nil)
42
+ actual = object.send("#{machine_name}_name")
43
+ default_message = "Expected #{object.class}##{machine_name} to be #{expected_state}, but was #{actual}"
44
+
45
+ if defined?(::Minitest)
46
+ assert_equal expected_state.to_s, actual.to_s, message || default_message
47
+ elsif defined?(::RSpec)
48
+ expect(actual.to_s).to eq(expected_state.to_s), message || default_message
49
+ else
50
+ raise "Expected #{expected_state}, but got #{actual}" unless expected_state.to_s == actual.to_s
51
+ end
52
+ end
53
+
54
+ # Assert that an object can transition via a specific event
55
+ #
56
+ # @param object [Object] The object with state machines
57
+ # @param event [Symbol] The event name
58
+ # @param message [String, nil] Custom failure message
59
+ # @return [void]
60
+ # @raise [AssertionError] If the transition is not available
61
+ #
62
+ # @example
63
+ # user = User.new
64
+ # assert_can_transition(user, :activate)
65
+ def assert_can_transition(object, event, message = nil)
66
+ can_method = "can_#{event}?"
67
+ default_message = "Expected to be able to trigger event :#{event}, but #{can_method} returned false"
68
+
69
+ if defined?(::Minitest)
70
+ assert object.send(can_method), message || default_message
71
+ elsif defined?(::RSpec)
72
+ expect(object.send(can_method)).to be_truthy, message || default_message
73
+ else
74
+ raise default_message unless object.send(can_method)
75
+ end
76
+ end
77
+
78
+ # Assert that an object cannot transition via a specific event
79
+ #
80
+ # @param object [Object] The object with state machines
81
+ # @param event [Symbol] The event name
82
+ # @param message [String, nil] Custom failure message
83
+ # @return [void]
84
+ # @raise [AssertionError] If the transition is available
85
+ #
86
+ # @example
87
+ # user = User.new
88
+ # assert_cannot_transition(user, :delete)
89
+ def assert_cannot_transition(object, event, message = nil)
90
+ can_method = "can_#{event}?"
91
+ default_message = "Expected not to be able to trigger event :#{event}, but #{can_method} returned true"
92
+
93
+ if defined?(::Minitest)
94
+ refute object.send(can_method), message || default_message
95
+ elsif defined?(::RSpec)
96
+ expect(object.send(can_method)).to be_falsy, message || default_message
97
+ elsif object.send(can_method)
98
+ raise default_message
99
+ end
100
+ end
101
+
102
+ # Assert that triggering an event changes the object to the expected state
103
+ #
104
+ # @param object [Object] The object with state machines
105
+ # @param event [Symbol] The event to trigger
106
+ # @param machine_name [Symbol] The name of the state machine
107
+ # @param expected_state [Symbol] The expected state after transition
108
+ # @param message [String, nil] Custom failure message
109
+ # @return [void]
110
+ # @raise [AssertionError] If the transition fails or results in wrong state
111
+ #
112
+ # @example
113
+ # user = User.new
114
+ # assert_transition(user, :activate, :status, :active)
115
+ def assert_transition(object, event, machine_name, expected_state, message = nil)
116
+ object.send("#{event}!")
117
+ assert_state(object, machine_name, expected_state, message)
118
+ end
119
+
120
+ # === Extended State Machine Assertions ===
121
+
122
+ def assert_sm_states_list(machine, expected_states, message = nil)
123
+ actual_states = machine.states.map(&:name).compact
124
+ default_message = "Expected states #{expected_states} but got #{actual_states}"
125
+
126
+ if defined?(::Minitest)
127
+ assert_equal expected_states.sort, actual_states.sort, message || default_message
128
+ elsif defined?(::RSpec)
129
+ expect(actual_states.sort).to eq(expected_states.sort), message || default_message
130
+ else
131
+ raise default_message unless expected_states.sort == actual_states.sort
132
+ end
133
+ end
134
+
135
+ def refute_sm_state_defined(machine, state, message = nil)
136
+ state_exists = machine.states.any? { |s| s.name == state }
137
+ default_message = "Expected state #{state} to not be defined in machine"
138
+
139
+ if defined?(::Minitest)
140
+ refute state_exists, message || default_message
141
+ elsif defined?(::RSpec)
142
+ expect(state_exists).to be_falsy, message || default_message
143
+ elsif state_exists
144
+ raise default_message
145
+ end
146
+ end
147
+ alias assert_sm_state_not_defined refute_sm_state_defined
148
+
149
+ def assert_sm_initial_state(machine, expected_state, message = nil)
150
+ state_obj = machine.state(expected_state)
151
+ is_initial = state_obj&.initial?
152
+ default_message = "Expected state #{expected_state} to be the initial state"
153
+
154
+ if defined?(::Minitest)
155
+ assert is_initial, message || default_message
156
+ elsif defined?(::RSpec)
157
+ expect(is_initial).to be_truthy, message || default_message
158
+ else
159
+ raise default_message unless is_initial
160
+ end
161
+ end
162
+
163
+ def assert_sm_final_state(machine, state, message = nil)
164
+ state_obj = machine.states[state]
165
+ is_final = state_obj&.final?
166
+ default_message = "Expected state #{state} to be final"
167
+
168
+ if defined?(::Minitest)
169
+ assert is_final, message || default_message
170
+ elsif defined?(::RSpec)
171
+ expect(is_final).to be_truthy, message || default_message
172
+ else
173
+ raise default_message unless is_final
174
+ end
175
+ end
176
+
177
+ def assert_sm_possible_transitions(machine, from:, expected_to_states:, message: nil)
178
+ actual_transitions = machine.events.flat_map do |event|
179
+ event.branches.select { |branch| branch.known_states.include?(from) }
180
+ .map(&:to)
181
+ end.uniq
182
+ default_message = "Expected transitions from #{from} to #{expected_to_states} but got #{actual_transitions}"
183
+
184
+ if defined?(::Minitest)
185
+ assert_equal expected_to_states.sort, actual_transitions.sort, message || default_message
186
+ elsif defined?(::RSpec)
187
+ expect(actual_transitions.sort).to eq(expected_to_states.sort), message || default_message
188
+ else
189
+ raise default_message unless expected_to_states.sort == actual_transitions.sort
190
+ end
191
+ end
192
+
193
+ def refute_sm_transition_allowed(machine, from:, to:, on:, message: nil)
194
+ event = machine.events[on]
195
+ is_allowed = event&.branches&.any? { |branch| branch.known_states.include?(from) && branch.to == to }
196
+ default_message = "Expected transition from #{from} to #{to} on #{on} to not be allowed"
197
+
198
+ if defined?(::Minitest)
199
+ refute is_allowed, message || default_message
200
+ elsif defined?(::RSpec)
201
+ expect(is_allowed).to be_falsy, message || default_message
202
+ elsif is_allowed
203
+ raise default_message
204
+ end
205
+ end
206
+ alias assert_sm_transition_not_allowed refute_sm_transition_allowed
207
+
208
+ def assert_sm_event_triggers(object, event, message = nil)
209
+ initial_state = object.state
210
+ object.send("#{event}!")
211
+ state_changed = initial_state != object.state
212
+ default_message = "Expected event #{event} to trigger state change"
213
+
214
+ if defined?(::Minitest)
215
+ assert state_changed, message || default_message
216
+ elsif defined?(::RSpec)
217
+ expect(state_changed).to be_truthy, message || default_message
218
+ else
219
+ raise default_message unless state_changed
220
+ end
221
+ end
222
+
223
+ def refute_sm_event_triggers(object, event, message = nil)
224
+ initial_state = object.state
225
+ begin
226
+ object.send("#{event}!")
227
+ state_unchanged = initial_state == object.state
228
+ default_message = "Expected event #{event} to not trigger state change"
229
+
230
+ if defined?(::Minitest)
231
+ assert state_unchanged, message || default_message
232
+ elsif defined?(::RSpec)
233
+ expect(state_unchanged).to be_truthy, message || default_message
234
+ else
235
+ raise default_message unless state_unchanged
236
+ end
237
+ rescue StateMachines::InvalidTransition
238
+ # Expected behavior - transition was blocked
239
+ end
240
+ end
241
+ alias assert_sm_event_not_triggers refute_sm_event_triggers
242
+
243
+ def assert_sm_event_raises_error(object, event, error_class, message = nil)
244
+ default_message = "Expected event #{event} to raise #{error_class}"
245
+
246
+ if defined?(::Minitest)
247
+ assert_raises(error_class, message || default_message) do
248
+ object.send("#{event}!")
249
+ end
250
+ elsif defined?(::RSpec)
251
+ expect { object.send("#{event}!") }.to raise_error(error_class), message || default_message
252
+ else
253
+ begin
254
+ object.send("#{event}!")
255
+ raise default_message
256
+ rescue error_class
257
+ # Expected behavior
258
+ end
259
+ end
260
+ end
261
+
262
+ def assert_sm_callback_executed(object, callback_name, message = nil)
263
+ callbacks_executed = object.instance_variable_get(:@_sm_callbacks_executed) || []
264
+ callback_was_executed = callbacks_executed.include?(callback_name)
265
+ default_message = "Expected callback #{callback_name} to be executed"
266
+
267
+ if defined?(::Minitest)
268
+ assert callback_was_executed, message || default_message
269
+ elsif defined?(::RSpec)
270
+ expect(callback_was_executed).to be_truthy, message || default_message
271
+ else
272
+ raise default_message unless callback_was_executed
273
+ end
274
+ end
275
+
276
+ def refute_sm_callback_executed(object, callback_name, message = nil)
277
+ callbacks_executed = object.instance_variable_get(:@_sm_callbacks_executed) || []
278
+ callback_was_executed = callbacks_executed.include?(callback_name)
279
+ default_message = "Expected callback #{callback_name} to not be executed"
280
+
281
+ if defined?(::Minitest)
282
+ refute callback_was_executed, message || default_message
283
+ elsif defined?(::RSpec)
284
+ expect(callback_was_executed).to be_falsy, message || default_message
285
+ elsif callback_was_executed
286
+ raise default_message
287
+ end
288
+ end
289
+ alias assert_sm_callback_not_executed refute_sm_callback_executed
290
+
291
+ def assert_sm_state_persisted(record, expected:, message: nil)
292
+ record.reload if record.respond_to?(:reload)
293
+ actual_state = record.state
294
+ default_message = "Expected persisted state #{expected} but got #{actual_state}"
295
+
296
+ if defined?(::Minitest)
297
+ assert_equal expected, actual_state, message || default_message
298
+ elsif defined?(::RSpec)
299
+ expect(actual_state).to eq(expected), message || default_message
300
+ else
301
+ raise default_message unless expected == actual_state
302
+ end
303
+ end
304
+ end
305
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module StateMachines
2
4
  # A transition represents a state change for a specific attribute.
3
5
  #
@@ -1,3 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'options_validator'
4
+
1
5
  module StateMachines
2
6
  # Represents a collection of transitions in a state machine
3
7
  class TransitionCollection < Array
@@ -28,7 +32,7 @@ module StateMachines
28
32
  attributes = map { |transition| transition.attribute }.uniq
29
33
  fail ArgumentError, 'Cannot perform multiple transitions in parallel for the same state machine attribute' if attributes.length != length
30
34
 
31
- options.assert_valid_keys(:actions, :after, :use_transactions)
35
+ StateMachines::OptionsValidator.assert_valid_keys!(options, :actions, :after, :use_transactions)
32
36
  options = {actions: true, after: true, use_transactions: true}.merge(options)
33
37
  @skip_actions = !options[:actions]
34
38
  @skip_after = !options[:after]
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module StateMachines
2
- VERSION = '0.6.0'
4
+ VERSION = '0.20.0'
3
5
  end
@@ -1,3 +1,6 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'state_machines/version'
2
4
  require 'state_machines/core'
3
- require 'state_machines/core_ext'
5
+ require 'state_machines/core_ext'
6
+ require 'state_machines/stdio_renderer'
metadata CHANGED
@@ -1,15 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: state_machines
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.20.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Abdelkader Boudih
8
8
  - Aaron Pfeifer
9
- autorequire:
10
9
  bindir: bin
11
10
  cert_chain: []
12
- date: 2023-06-30 00:00:00.000000000 Z
11
+ date: 1980-01-02 00:00:00.000000000 Z
13
12
  dependencies:
14
13
  - !ruby/object:Gem::Dependency
15
14
  name: bundler
@@ -56,7 +55,6 @@ dependencies:
56
55
  description: Adds support for creating state machines for attributes on any Ruby class
57
56
  email:
58
57
  - terminale@gmail.com
59
- - aaron@pluginaweek.org
60
58
  executables: []
61
59
  extensions: []
62
60
  extra_rdoc_files: []
@@ -64,7 +62,6 @@ files:
64
62
  - LICENSE.txt
65
63
  - README.md
66
64
  - lib/state_machines.rb
67
- - lib/state_machines/assertions.rb
68
65
  - lib/state_machines/branch.rb
69
66
  - lib/state_machines/callback.rb
70
67
  - lib/state_machines/core.rb
@@ -79,24 +76,30 @@ files:
79
76
  - lib/state_machines/integrations.rb
80
77
  - lib/state_machines/integrations/base.rb
81
78
  - lib/state_machines/machine.rb
79
+ - lib/state_machines/machine/class_methods.rb
82
80
  - lib/state_machines/machine_collection.rb
83
81
  - lib/state_machines/macro_methods.rb
84
82
  - lib/state_machines/matcher.rb
85
83
  - lib/state_machines/matcher_helpers.rb
86
84
  - lib/state_machines/node_collection.rb
85
+ - lib/state_machines/options_validator.rb
87
86
  - lib/state_machines/path.rb
88
87
  - lib/state_machines/path_collection.rb
89
88
  - lib/state_machines/state.rb
90
89
  - lib/state_machines/state_collection.rb
91
90
  - lib/state_machines/state_context.rb
91
+ - lib/state_machines/stdio_renderer.rb
92
+ - lib/state_machines/test_helper.rb
92
93
  - lib/state_machines/transition.rb
93
94
  - lib/state_machines/transition_collection.rb
94
95
  - lib/state_machines/version.rb
95
96
  homepage: https://github.com/state-machines/state_machines
96
97
  licenses:
97
98
  - MIT
98
- metadata: {}
99
- post_install_message:
99
+ metadata:
100
+ changelog_uri: https://github.com/state-machines/state_machines/blob/master/CHANGELOG.md
101
+ homepage_uri: https://github.com/state-machines/state_machines
102
+ source_code_uri: https://github.com/state-machines/state_machines
100
103
  rdoc_options: []
101
104
  require_paths:
102
105
  - lib
@@ -111,8 +114,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
111
114
  - !ruby/object:Gem::Version
112
115
  version: '0'
113
116
  requirements: []
114
- rubygems_version: 3.4.10
115
- signing_key:
117
+ rubygems_version: 3.6.7
116
118
  specification_version: 4
117
119
  summary: State machines for attributes
118
120
  test_files: []
@@ -1,40 +0,0 @@
1
- class Hash
2
- # Provides a set of helper methods for making assertions about the content
3
- # of various objects
4
-
5
- unless respond_to?(:assert_valid_keys)
6
- # Validate all keys in a hash match <tt>*valid_keys</tt>, raising ArgumentError
7
- # on a mismatch. Note that keys are NOT treated indifferently, meaning if you
8
- # use strings for keys but assert symbols as keys, this will fail.
9
- #
10
- # { name: 'Rob', years: '28' }.assert_valid_keys(:name, :age) # => raises "ArgumentError: Unknown key: :years. Valid keys are: :name, :age"
11
- # { name: 'Rob', age: '28' }.assert_valid_keys('name', 'age') # => raises "ArgumentError: Unknown key: :name. Valid keys are: 'name', 'age'"
12
- # { name: 'Rob', age: '28' }.assert_valid_keys(:name, :age) # => passes, raises nothing
13
- # Code from ActiveSupport
14
- def assert_valid_keys(*valid_keys)
15
- valid_keys.flatten!
16
- each_key do |k|
17
- unless valid_keys.include?(k)
18
- raise ArgumentError.new("Unknown key: #{k.inspect}. Valid keys are: #{valid_keys.map(&:inspect).join(', ')}")
19
- end
20
- end
21
- end
22
- end
23
-
24
- # Validates that the given hash only includes at *most* one of a set of
25
- # exclusive keys. If more than one key is found, an ArgumentError will be
26
- # raised.
27
- #
28
- # == Examples
29
- #
30
- # options = {:only => :on, :except => :off}
31
- # options.assert_exclusive_keys(:only) # => nil
32
- # options.assert_exclusive_keys(:except) # => nil
33
- # options.assert_exclusive_keys(:only, :except) # => ArgumentError: Conflicting keys: only, except
34
- # options.assert_exclusive_keys(:only, :except, :with) # => ArgumentError: Conflicting keys: only, except
35
- def assert_exclusive_keys(*exclusive_keys)
36
- conflicting_keys = exclusive_keys & keys
37
- raise ArgumentError, "Conflicting keys: #{conflicting_keys.join(', ')}" unless conflicting_keys.length <= 1
38
- end
39
- end
40
-