flow_machine 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,253 @@
1
+ RSpec.describe FlowMachine::WorkflowState do
2
+ class StateTestClass < described_class
3
+ def self.state_name; :test; end
4
+ def guard1?; end
5
+ def guard2?; end
6
+ def after_hook; end
7
+ end
8
+
9
+ class WorkflowTestClass
10
+ include FlowMachine::Workflow
11
+ def workflow_guard?; end
12
+ def workflow_hook; end
13
+ end
14
+
15
+ let(:state_class) { Class.new(StateTestClass) }
16
+ let(:workflow_class) { Class.new WorkflowTestClass }
17
+
18
+ before :each do
19
+ workflow_class.state state_class
20
+ end
21
+
22
+ let(:object) { double state: :test, changes: {}, save: true, new_record?: true }
23
+ let(:workflow) { workflow_class.new(object) }
24
+
25
+ describe 'defining events' do
26
+ context 'a basic event' do
27
+
28
+ before { state_class.event :event1 }
29
+ subject(:state) { state_class.new(workflow) }
30
+
31
+ it 'defines the event on the object' do
32
+ expect(state).to respond_to :event1
33
+ end
34
+
35
+ it 'defines may_event1?' do
36
+ expect(state).to respond_to :may_event1?
37
+ end
38
+
39
+ it 'defines the bang event' do
40
+ expect(state).to respond_to :event1!
41
+ end
42
+ end
43
+ end
44
+
45
+ describe 'events' do
46
+ let(:state_class2) do
47
+ Class.new(described_class) do
48
+ def self.state_name; :test2; end
49
+ end
50
+ end
51
+ before :each do
52
+ state_class.event(:event1) { transition to: :test2 }
53
+ workflow_class.refresh_state_methods!
54
+ workflow_class.state state_class2
55
+ end
56
+
57
+ it 'returns false when trying to transition to the current state' do
58
+ expect(object).to receive(:state).and_return :test2
59
+ expect(workflow.event1).to be false
60
+ end
61
+ end
62
+
63
+ describe 'guards' do
64
+ subject(:state) { workflow.current_state }
65
+
66
+ context 'a single guard' do
67
+ before { state_class.event(:event1, guard: :guard1?) {} }
68
+ it 'calls the guard' do
69
+ expect(state).to receive(:guard1?).and_return true
70
+ state.event1
71
+ end
72
+
73
+ context 'may?' do
74
+ it 'is able to transition if the guard returns true' do
75
+ expect(state).to receive(:guard1?).and_return true
76
+ expect(state.may_event1?).to be true
77
+ end
78
+
79
+ it 'is not able to transition if the guard returns false' do
80
+ expect(state).to receive(:guard1?).and_return false
81
+ expect(state.may_event1?).to be false
82
+ end
83
+ end
84
+ end
85
+
86
+ context 'the guard method is on the workflow instead' do
87
+ before { state_class.event(:event1, guard: :workflow_guard?) {} }
88
+ it 'calls the guard' do
89
+ expect(workflow).to receive(:workflow_guard?).and_return true
90
+ state.event1
91
+ end
92
+ end
93
+
94
+ context 'the guard method is on the object' do
95
+ before { state_class.event(:event1, guard: :object_guard?) {} }
96
+ it 'calls the guard' do
97
+ expect(object).to receive(:object_guard?).and_return true
98
+ state.event1
99
+ end
100
+ end
101
+
102
+ context 'multiple guards' do
103
+ before do
104
+ state_class.event(:event1, guard: [:guard1?, :guard2?]) {}
105
+ workflow_class.refresh_state_methods!
106
+ end
107
+ it 'calls all the guards' do
108
+ expect(state).to receive(:guard1?).and_return true
109
+ expect(state).to receive(:guard2?).and_return true
110
+ state.event1
111
+ end
112
+
113
+ context 'one guard returns false' do
114
+ before :each do
115
+ expect(state).to receive(:guard1?).and_return false
116
+ expect(state).to receive(:guard2?).and_return true
117
+ end
118
+
119
+ it 'gets the guard errors on may_event' do
120
+ workflow.may_event1?
121
+ expect(workflow.guard_errors).to eq([:guard1?])
122
+ end
123
+
124
+ it 'gets the guard errors on transition' do
125
+ workflow.event1
126
+ expect(workflow.guard_errors).to eq([:guard1?])
127
+ end
128
+ end
129
+ end
130
+
131
+ it 'does not call the event block if the guard fails' do
132
+ state_class.event(:event1, guard: [:guard1?]) { raise 'Should not call block' }
133
+ expect(state).to receive(:guard1?).and_return false
134
+ state.event1
135
+ end
136
+ end
137
+
138
+ describe 'triggering workflow after_transition hook' do
139
+ let(:state) { workflow.current_state }
140
+ before do
141
+ workflow_class.after_transition :workflow_hook
142
+
143
+ state_class.event :event1, guard: :guard1? do
144
+ transition to: :state2
145
+ end
146
+
147
+ workflow_class.refresh_state_methods!
148
+ end
149
+
150
+ it 'calls the hook on transition' do
151
+ # allow the transition, and make it think it's a different state
152
+ expect(workflow).to receive(:current_state_name=).with('state2')
153
+ expect(state).to receive(:==).and_return false
154
+
155
+ expect(state).to receive(:guard1?).and_return(true)
156
+ expect(workflow).to receive(:workflow_hook)
157
+ workflow.event1
158
+ end
159
+
160
+ it 'does not call the hook on failure' do
161
+ expect(state).to receive(:guard1?).and_return(false)
162
+ expect(workflow).not_to receive(:workflow_hook)
163
+ workflow.event1
164
+ end
165
+ end
166
+
167
+ describe 'after transition hooks' do
168
+ let(:state) { workflow.current_state }
169
+ before :each do
170
+ state_class.event :event1 do
171
+ transition to: :state2, after: :after_hook
172
+ end
173
+
174
+ workflow_class.refresh_state_methods!
175
+
176
+ expect(workflow).to receive(:current_state_name=).with('state2')
177
+ end
178
+
179
+ it 'does not call the hook before saving' do
180
+ expect(state).to receive(:after_hook).never
181
+ workflow.event1
182
+ end
183
+
184
+ it 'calls the hook after saving the transition' do
185
+ expect(state).to receive(:after_hook).once
186
+ workflow.event1
187
+ workflow.persist
188
+ end
189
+ end
190
+
191
+ describe 'after_enter' do
192
+ let(:state) { state_class.new(workflow) }
193
+ context 'the method is on the workflow' do
194
+ before { state_class.after_enter :workflow_hook }
195
+ it "can call the method" do
196
+ expect(workflow).to receive(:workflow_hook)
197
+ state.fire_callbacks(:after_enter, {})
198
+ end
199
+ end
200
+
201
+ context 'the method is on the object' do
202
+ before { state_class.after_enter :object_hook }
203
+
204
+ it "can call the method" do
205
+ expect(object).to receive(:object_hook)
206
+ state.fire_callbacks(:after_enter, {})
207
+ end
208
+ end
209
+ end
210
+
211
+ describe '#run_workflow_method' do
212
+ let(:state) { state_class.new(workflow) }
213
+
214
+ context 'nothing in the chain has the method' do
215
+ it 'raises a NoMethodError' do
216
+ expect { state.run_workflow_method :some_method }.to raise_error(NoMethodError)
217
+ end
218
+ end
219
+
220
+ context 'the object defines the method' do
221
+ before :each do
222
+ allow(object).to receive(:some_method)
223
+ end
224
+
225
+ it 'calls the method on object' do
226
+ expect(object).to receive(:some_method)
227
+ state.run_workflow_method :some_method
228
+ end
229
+
230
+ context 'and the workflow defines the method' do
231
+ before :each do
232
+ workflow.singleton_class.send(:define_method, :some_method) {}
233
+ end
234
+
235
+ it 'calls the method on workflow' do
236
+ expect(workflow).to receive(:some_method)
237
+ state.run_workflow_method :some_method
238
+ end
239
+
240
+ context 'and the state defines the method' do
241
+ before :each do
242
+ state.singleton_class.send(:define_method, :some_method) {}
243
+ end
244
+
245
+ it 'calls the method on the state' do
246
+ expect(state).to receive(:some_method)
247
+ state.run_workflow_method :some_method
248
+ end
249
+ end
250
+ end
251
+ end
252
+ end
253
+ end
@@ -0,0 +1,94 @@
1
+ GEM_ROOT = File.expand_path('../../', __FILE__)
2
+ $LOAD_PATH.unshift File.join(GEM_ROOT, 'lib')
3
+
4
+ require 'rspec/its'
5
+ require 'flow_machine'
6
+
7
+ # This file was generated by the `rspec --init` command. Conventionally, all
8
+ # specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
9
+ # The generated `.rspec` file contains `--require spec_helper` which will cause this
10
+ # file to always be loaded, without a need to explicitly require it in any files.
11
+ #
12
+ # Given that it is always loaded, you are encouraged to keep this file as
13
+ # light-weight as possible. Requiring heavyweight dependencies from this file
14
+ # will add to the boot time of your test suite on EVERY test run, even for an
15
+ # individual file that may not need all of that loaded. Instead, consider making
16
+ # a separate helper file that requires the additional dependencies and performs
17
+ # the additional setup, and require it from the spec files that actually need it.
18
+ #
19
+ # The `.rspec` file also contains a few flags that are not defaults but that
20
+ # users commonly want.
21
+ #
22
+ # See http://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration
23
+ RSpec.configure do |config|
24
+ # rspec-expectations config goes here. You can use an alternate
25
+ # assertion/expectation library such as wrong or the stdlib/minitest
26
+ # assertions if you prefer.
27
+ config.expect_with :rspec do |expectations|
28
+ # This option will default to `true` in RSpec 4. It makes the `description`
29
+ # and `failure_message` of custom matchers include text for helper methods
30
+ # defined using `chain`, e.g.:
31
+ # be_bigger_than(2).and_smaller_than(4).description
32
+ # # => "be bigger than 2 and smaller than 4"
33
+ # ...rather than:
34
+ # # => "be bigger than 2"
35
+ expectations.include_chain_clauses_in_custom_matcher_descriptions = true
36
+ end
37
+
38
+ # rspec-mocks config goes here. You can use an alternate test double
39
+ # library (such as bogus or mocha) by changing the `mock_with` option here.
40
+ config.mock_with :rspec do |mocks|
41
+ # Prevents you from mocking or stubbing a method that does not exist on
42
+ # a real object. This is generally recommended, and will default to
43
+ # `true` in RSpec 4.
44
+ mocks.verify_partial_doubles = true
45
+ end
46
+
47
+ # The settings below are suggested to provide a good initial experience
48
+ # with RSpec, but feel free to customize to your heart's content.
49
+
50
+ # These two settings work together to allow you to limit a spec run
51
+ # to individual examples or groups you care about by tagging them with
52
+ # `:focus` metadata. When nothing is tagged with `:focus`, all examples
53
+ # get run.
54
+ config.filter_run :focus
55
+ config.run_all_when_everything_filtered = true
56
+
57
+ # Limits the available syntax to the non-monkey patched syntax that is recommended.
58
+ # For more details, see:
59
+ # - http://myronmars.to/n/dev-blog/2012/06/rspecs-new-expectation-syntax
60
+ # - http://teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
61
+ # - http://myronmars.to/n/dev-blog/2014/05/notable-changes-in-rspec-3#new__config_option_to_disable_rspeccore_monkey_patching
62
+ config.disable_monkey_patching!
63
+
64
+ # This setting enables warnings. It's recommended, but in some cases may
65
+ # be too noisy due to issues in dependencies.
66
+ # config.warnings = true
67
+
68
+ # Many RSpec users commonly either run the entire suite or an individual
69
+ # file, and it's useful to allow more verbose output when running an
70
+ # individual spec file.
71
+ if config.files_to_run.one?
72
+ # Use the documentation formatter for detailed output,
73
+ # unless a formatter has already been configured
74
+ # (e.g. via a command-line flag).
75
+ config.default_formatter = 'doc'
76
+ end
77
+
78
+ # Print the 10 slowest examples and example groups at the
79
+ # end of the spec run, to help surface which specs are running
80
+ # particularly slow.
81
+ # config.profile_examples = 10
82
+
83
+ # Run specs in random order to surface order dependencies. If you find an
84
+ # order dependency and want to debug it, you can fix the order by providing
85
+ # the seed, which is printed after each run.
86
+ # --seed 1234
87
+ config.order = :random
88
+
89
+ # Seed global randomization in this process using the `--seed` CLI option.
90
+ # Setting this allows you to use `--seed` to deterministically reproduce
91
+ # test failures related to randomization by passing the same `--seed` value
92
+ # as the one that triggered the failure.
93
+ Kernel.srand config.seed
94
+ end
metadata ADDED
@@ -0,0 +1,112 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: flow_machine
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Jason Hanggi
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-02-24 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '3.2'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '3.2'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 3.1.0
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 3.1.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec-its
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description: Build finite state machines in a backend-agnostic, class-centric way.
56
+ email:
57
+ - jason@tablexi.com
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - MIT-LICENSE
63
+ - README.md
64
+ - Rakefile
65
+ - lib/flow_machine.rb
66
+ - lib/flow_machine/callback.rb
67
+ - lib/flow_machine/change_callback.rb
68
+ - lib/flow_machine/factory.rb
69
+ - lib/flow_machine/state_callback.rb
70
+ - lib/flow_machine/version.rb
71
+ - lib/flow_machine/workflow.rb
72
+ - lib/flow_machine/workflow_state.rb
73
+ - lib/tasks/workflow_tasks.rake
74
+ - spec/flow_machine/factory_spec.rb
75
+ - spec/flow_machine/multiple_workflow_spec.rb
76
+ - spec/flow_machine/workflow_callback_spec.rb
77
+ - spec/flow_machine/workflow_change_callback_spec.rb
78
+ - spec/flow_machine/workflow_spec.rb
79
+ - spec/flow_machine/workflow_state_spec.rb
80
+ - spec/spec_helper.rb
81
+ homepage: http://www.github.com/tablexi/flow_machine
82
+ licenses:
83
+ - MIT
84
+ metadata: {}
85
+ post_install_message:
86
+ rdoc_options: []
87
+ require_paths:
88
+ - lib
89
+ required_ruby_version: !ruby/object:Gem::Requirement
90
+ requirements:
91
+ - - ">="
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ required_rubygems_version: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - ">="
97
+ - !ruby/object:Gem::Version
98
+ version: '0'
99
+ requirements: []
100
+ rubyforge_project:
101
+ rubygems_version: 2.2.2
102
+ signing_key:
103
+ specification_version: 4
104
+ summary: A class-based state machine.
105
+ test_files:
106
+ - spec/flow_machine/factory_spec.rb
107
+ - spec/flow_machine/multiple_workflow_spec.rb
108
+ - spec/flow_machine/workflow_callback_spec.rb
109
+ - spec/flow_machine/workflow_change_callback_spec.rb
110
+ - spec/flow_machine/workflow_spec.rb
111
+ - spec/flow_machine/workflow_state_spec.rb
112
+ - spec/spec_helper.rb