statesman 0.8.3 → 1.0.0.beta1

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: caf6d74f912f306c3431c382639a68d14eb70023
4
- data.tar.gz: 9e551630b7eaff3ba8b045394c0269d9a9536349
3
+ metadata.gz: 55f5eab4bdf1849056059ca216caa5412679967d
4
+ data.tar.gz: 56d537766abea0884d94e4a55a9051896bbfc902
5
5
  SHA512:
6
- metadata.gz: 2eb9f50f24c02c9932c58551d164fda32ec47a844f40291064854dcc0239bfbb058a1fb83e44e8f03c640f6ad77e42725830e4295dc8f0ebf42d24b1775c9b0a
7
- data.tar.gz: e62600a9070ef38c87677952506c96cda36009a53ac9c9a65997050586f6fcefb293b23f5f679604dbbf46f0da08832e5348581204e7f09e1e42937c40e23cb1
6
+ metadata.gz: b2bead7fc506334da98f4ab620cb6df1fd49012a3a77c3accb8e779e8f09212e3b9926c5ea145ea7d0d3df2f69db13808d05bb65ec55fdfbb39be3ea55d4e915
7
+ data.tar.gz: e9c8295a8eca68e22debdb594c4802bdd554c18b6a26b4e7d3940fed0673436bf5aa917b4b3a188191b867f6cb4532d08f9383e378d771752ca4b0e91eab1790
data/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ ## v1.0.0.rc1 9 October 2014
2
+ *Breaking changes*
3
+
4
+ - Classes which include `ActiveRecordModel` must define an `initial_state` class method.
5
+
6
+ *Fixes*
7
+
8
+ - `ActiveRecordModel.in_state` and `ActiveRecordModel.not_in_state` now handle inital states correctly (patch by [@isaacseymour](https://github.com/isaacseymour))
9
+
10
+ *Additions*
11
+
12
+ - Transition tables created by generated migrations have `NOT NULL` constraints on `to_state`, `sort_key` and foreign key columns (patch by [@greysteil](https://github.com/greysteil))
13
+ - `before_transition` and `after_transition` allow an array of to states (patch by [@isaacseymour](https://github.com/isaacseymour))
14
+
1
15
  ## v0.8.3 2 September 2014
2
16
  *Fixes*
3
17
 
data/README.md CHANGED
@@ -331,7 +331,7 @@ callback code throws an exception, it will not be caught.)
331
331
 
332
332
  A mixin is provided for the ActiveRecord adapter which adds scopes to easily
333
333
  find all models currently in (or not in) a given state. Include it into your
334
- model and define a `transition_class` method.
334
+ model and define `transition_class` and `initial_state` class methods:
335
335
 
336
336
  ```ruby
337
337
  class Order < ActiveRecord::Base
@@ -342,14 +342,122 @@ class Order < ActiveRecord::Base
342
342
  def self.transition_class
343
343
  OrderTransition
344
344
  end
345
+
346
+ def self.initial_state
347
+ OrderStateMachine.initial_state
348
+ end
345
349
  end
346
350
  ```
347
351
 
348
352
  #### `Model.in_state(:state_1, :state_2, etc)`
349
- Returns all models currently in any of the supplied states.
353
+ Returns all models currently in any of the supplied states. Prior to 1.0 this ignored all models in the initial state, and the `initial_state` class method was not required.
350
354
 
351
355
  #### `Model.not_in_state(:state_1, :state_2, etc)`
352
- Returns all models not currently in any of the supplied states.
356
+ Returns all models not currently in any of the supplied states. Prior to 1.0 this always excluded models in the initial state, and the `initial_state` class method was not required.
357
+
358
+ ## Frequently Asked Questions
359
+
360
+ #### `Model.in_state` does not work before first transition
361
+
362
+ This is expected. State is modelled as a series of tranisitions so initially where there has been no transition, there is no explict state. At GoCardless we get around this by transitioning immediately after creation:
363
+
364
+ ```ruby
365
+ after_create do
366
+ state_machine.transition_to! "pending"
367
+ end
368
+ ```
369
+
370
+ #### Storing the state on the model object
371
+
372
+ If you wish to store the model state on the model directly, you can keep it up to date using an `after_transition` hook:
373
+
374
+ ```ruby
375
+ after_transition do |model, transition|
376
+ model.state = transition.to_state
377
+ model.save!
378
+ end
379
+ ```
380
+
381
+ #### Accessing metadata from the last transition
382
+
383
+ Given a field `foo` that was stored in the metadata, you can access it like so:
384
+
385
+ ```ruby
386
+ model_instance.last_transition.metadata["foo"]
387
+ ```
388
+
389
+ ## Testing Statesman Implementations
390
+
391
+ This answer was abstracted from [this issue](https://github.com/gocardless/statesman/issues/77).
392
+
393
+ At GoCardless we focus on testing that:
394
+ - guards correctly prevent / allow transitions
395
+ - callbacks execute when expected and perform the expected actions
396
+
397
+ #### Testing Guards
398
+
399
+ Guards can be tested by asserting that `transition_to!` does or does not raise a `Statesman::GuardFailedError`:
400
+
401
+ ```ruby
402
+ describe "guards" do
403
+ it "cannot transition from state foo to state bar" do
404
+ expect { some_model.transition_to!(:bar) }.to raise_error(Statesman::GuardFailedError)
405
+ end
406
+
407
+ it "can transition from state foo to state baz" do
408
+ expect { some_model.transition_to!(:baz).to_not raise_error
409
+ end
410
+ end
411
+ ```
412
+
413
+ #### Testing Callbacks
414
+
415
+ Callbacks are tested by asserting that the action they perform occurs:
416
+
417
+ ```ruby
418
+ describe "some callback" do
419
+ it "adds one to the count property on the model" do
420
+ expect { some_model.transition_to!(:some_state) }.
421
+ to change {
422
+ some_model.reload.count
423
+ }.by(1)
424
+ end
425
+ end
426
+ ```
427
+
428
+ #### Creating models in certain states
429
+
430
+ Sometimes you'll want to test a guard/transition from one state to another, where the state you want to go from is not the initial state of the model. In this instance you'll need to construct a model instance in the state required. However, if you have strict guards, this can be a pain. One way to get around this in tests is to directly create the transitions in the database, hence avoiding the guards.
431
+
432
+ We use [FactoryGirl](https://github.com/thoughtbot/factory_girl) for creating our test objects. Given an `Order` model that is backed by Statesman, we can easily set it up to be in a particular state:
433
+
434
+ ```ruby
435
+ factory :order do
436
+ property "value"
437
+ ...
438
+
439
+ trait :shipped do
440
+ after(:create) do |order|
441
+ FactoryGirl.create(:order_transition, :shipped, order: order)
442
+ end
443
+ end
444
+ end
445
+
446
+ factory :order_transition do
447
+ order
448
+ ...
449
+
450
+ trait :shipped do
451
+ to_state "shipped"
452
+ end
453
+ end
454
+ ```
455
+
456
+ This means you can easily create an `Order` in the `shipped` state:
457
+
458
+ ```ruby
459
+ let(:shipped_order) { FactoryGirl.create(:order, :shipped) }
460
+ ```
353
461
 
354
462
  ---
355
463
 
@@ -1,10 +1,10 @@
1
1
  class Create<%= migration_class_name %> < ActiveRecord::Migration
2
2
  def change
3
3
  create_table :<%= table_name %> do |t|
4
- t.string :to_state
4
+ t.string :to_state, null: false
5
5
  t.text :metadata<%= ", default: \"{}\"" unless mysql? %>
6
- t.integer :sort_key
7
- t.integer :<%= parent_id %>
6
+ t.integer :sort_key, null: false
7
+ t.integer :<%= parent_id %>, null: false
8
8
  t.timestamps
9
9
  end
10
10
 
@@ -1,9 +1,9 @@
1
1
  class AddStatesmanTo<%= migration_class_name %> < ActiveRecord::Migration
2
2
  def change
3
- add_column :<%= table_name %>, :to_state, :string
3
+ add_column :<%= table_name %>, :to_state, :string, null: false
4
4
  add_column :<%= table_name %>, :metadata, :text<%= ", default: \"{}\"" unless mysql? %>
5
- add_column :<%= table_name %>, :sort_key, :integer
6
- add_column :<%= table_name %>, :<%= parent_id %>, :integer
5
+ add_column :<%= table_name %>, :sort_key, :integer, null: false
6
+ add_column :<%= table_name %>, :<%= parent_id %>, :integer, null: false
7
7
  add_column :<%= table_name %>, :created_at, :datetime
8
8
  add_column :<%= table_name %>, :updated_at, :datetime
9
9
 
@@ -23,6 +23,8 @@ module Statesman
23
23
  end
24
24
 
25
25
  def create(from, to, metadata = {})
26
+ from = from.to_s
27
+ to = to.to_s
26
28
  create_transition(from, to, metadata)
27
29
  rescue ::ActiveRecord::RecordNotUnique => e
28
30
  if e.message.include?('sort_key') &&
@@ -36,7 +38,9 @@ module Statesman
36
38
 
37
39
  def history
38
40
  if transitions_for_parent.loaded?
39
- transitions_for_parent.sort_by(&:sort_key)
41
+ # Workaround for Rails bug which causes infinite loop when sorting
42
+ # already loaded result set. Introduced in rails/rails@b097ebe
43
+ transitions_for_parent.to_a.sort_by(&:sort_key)
40
44
  else
41
45
  transitions_for_parent.order(:sort_key)
42
46
  end
@@ -7,16 +7,20 @@ module Statesman
7
7
 
8
8
  module ClassMethods
9
9
  def in_state(*states)
10
- joins(transition_name)
11
- .joins(transition_join)
12
- .where("#{transition_name}.to_state" => states.map(&:to_s))
10
+ states = states.map(&:to_s)
11
+
12
+ joins(transition1_join)
13
+ .joins(transition2_join)
14
+ .where(state_inclusion_where(states), states)
13
15
  .where("transition2.id" => nil)
14
16
  end
15
17
 
16
18
  def not_in_state(*states)
17
- joins(transition_name)
18
- .joins(transition_join)
19
- .where("#{transition_name}.to_state NOT IN (?)", states.map(&:to_s))
19
+ states = states.map(&:to_s)
20
+
21
+ joins(transition1_join)
22
+ .joins(transition2_join)
23
+ .where("NOT (#{state_inclusion_where(states)})", states)
20
24
  .where("transition2.id" => nil)
21
25
  end
22
26
 
@@ -27,6 +31,11 @@ module Statesman
27
31
  "defined on the model"
28
32
  end
29
33
 
34
+ def initial_state
35
+ raise NotImplementedError, "An initial_state method should be " \
36
+ "defined on the model"
37
+ end
38
+
30
39
  def transition_name
31
40
  transition_class.table_name.to_sym
32
41
  end
@@ -35,10 +44,25 @@ module Statesman
35
44
  reflections[transition_name].foreign_key
36
45
  end
37
46
 
38
- def transition_join
47
+ def transition1_join
48
+ "LEFT OUTER JOIN #{transition_name} transition1
49
+ ON transition1.#{model_foreign_key} = #{table_name}.id"
50
+ end
51
+
52
+ def transition2_join
39
53
  "LEFT OUTER JOIN #{transition_name} transition2
40
54
  ON transition2.#{model_foreign_key} = #{table_name}.id
41
- AND transition2.sort_key > #{transition_name}.sort_key"
55
+ AND transition2.sort_key > transition1.sort_key"
56
+ end
57
+
58
+ def state_inclusion_where(states)
59
+ if initial_state.in?(states)
60
+ 'transition1.to_state IN (?) OR ' \
61
+ 'transition1.to_state IS NULL'
62
+ else
63
+ 'transition1.to_state IN (?) AND ' \
64
+ 'transition1.to_state IS NOT NULL'
65
+ end
42
66
  end
43
67
  end
44
68
  end
@@ -17,6 +17,8 @@ module Statesman
17
17
  end
18
18
 
19
19
  def create(from, to, metadata = {})
20
+ from = from.to_s
21
+ to = to.to_s
20
22
  transition = transition_class.new(to, next_sort_key, metadata)
21
23
 
22
24
  @observer.execute(:before, from, to, transition)
@@ -16,6 +16,8 @@ module Statesman
16
16
  end
17
17
 
18
18
  def create(from, to, metadata = {})
19
+ from = from.to_s
20
+ to = to.to_s
19
21
  transition = transitions_for_parent.build(to_state: to,
20
22
  sort_key: next_sort_key,
21
23
  statesman_metadata: metadata)
@@ -11,8 +11,8 @@ module Statesman
11
11
  raise InvalidCallbackError, "No callback passed"
12
12
  end
13
13
 
14
- @from = options[:from]
15
- @to = options[:to]
14
+ @from = options[:from]
15
+ @to = Array(options[:to])
16
16
  @callback = options[:callback]
17
17
  end
18
18
 
@@ -34,19 +34,19 @@ module Statesman
34
34
  end
35
35
 
36
36
  def matches_all_transitions
37
- from.nil? && to.nil?
37
+ from.nil? && to.empty?
38
38
  end
39
39
 
40
40
  def matches_from_state(from, to)
41
- (from == self.from && (to.nil? || self.to.nil?))
41
+ (from == self.from && (to.nil? || self.to.empty?))
42
42
  end
43
43
 
44
44
  def matches_to_state(from, to)
45
- ((from.nil? || self.from.nil?) && to == self.to)
45
+ ((from.nil? || self.from.nil?) && self.to.include?(to))
46
46
  end
47
47
 
48
48
  def matches_both_states(from, to)
49
- from == self.from && to == self.to
49
+ from == self.from && self.to.include?(to)
50
50
  end
51
51
  end
52
52
  end
@@ -64,7 +64,7 @@ module Statesman
64
64
 
65
65
  def transition(options = { from: nil, to: nil }, event = nil)
66
66
  from = to_s_or_nil(options[:from])
67
- to = Array(options[:to]).map { |item| to_s_or_nil(item) }
67
+ to = array_to_s_or_nil(options[:to])
68
68
 
69
69
  raise InvalidStateError, "No to states provided." if to.empty?
70
70
 
@@ -82,44 +82,39 @@ module Statesman
82
82
  end
83
83
 
84
84
  def before_transition(options = { from: nil, to: nil }, &block)
85
- from = to_s_or_nil(options[:from])
86
- to = to_s_or_nil(options[:to])
85
+ add_callback(
86
+ options.merge(callback_class: Callback, callback_type: :before),
87
+ &block)
88
+ end
87
89
 
88
- validate_callback_condition(from: from, to: to)
89
- callbacks[:before] << Callback.new(from: from, to: to, callback: block)
90
+ def guard_transition(options = { from: nil, to: nil }, &block)
91
+ add_callback(
92
+ options.merge(callback_class: Guard, callback_type: :guards),
93
+ &block)
90
94
  end
91
95
 
92
96
  def after_transition(options = { from: nil, to: nil,
93
97
  after_commit: false }, &block)
94
- from = to_s_or_nil(options[:from])
95
- to = to_s_or_nil(options[:to])
98
+ callback_type = options[:after_commit] ? :after_commit : :after
96
99
 
97
- validate_callback_condition(from: from, to: to)
98
- phase = options[:after_commit] ? :after_commit : :after
99
- callbacks[phase] << Callback.new(from: from, to: to, callback: block)
100
- end
101
-
102
- def guard_transition(options = { from: nil, to: nil }, &block)
103
- from = to_s_or_nil(options[:from])
104
- to = to_s_or_nil(options[:to])
105
-
106
- validate_callback_condition(from: from, to: to)
107
- callbacks[:guards] << Guard.new(from: from, to: to, callback: block)
100
+ add_callback(
101
+ options.merge(callback_class: Callback, callback_type: callback_type),
102
+ &block)
108
103
  end
109
104
 
110
105
  def validate_callback_condition(options = { from: nil, to: nil })
111
106
  from = to_s_or_nil(options[:from])
112
- to = to_s_or_nil(options[:to])
107
+ to = array_to_s_or_nil(options[:to])
113
108
 
114
- [from, to].compact.each { |state| validate_state(state) }
115
- return if from.nil? && to.nil?
109
+ ([from] + to).compact.each { |state| validate_state(state) }
110
+ return if from.nil? && to.empty?
116
111
 
117
112
  validate_not_from_terminal_state(from)
118
- validate_not_to_initial_state(to)
113
+ to.each { |state| validate_not_to_initial_state(state) }
119
114
 
120
- return if from.nil? || to.nil?
115
+ return if from.nil? || to.empty?
121
116
 
122
- validate_from_and_to_state(from, to)
117
+ to.each { |state| validate_from_and_to_state(from, state) }
123
118
  end
124
119
 
125
120
  # Check that the 'from' state is not terminal
@@ -148,6 +143,17 @@ module Statesman
148
143
 
149
144
  private
150
145
 
146
+ def add_callback(options, &block)
147
+ from = to_s_or_nil(options[:from])
148
+ to = array_to_s_or_nil(options[:to])
149
+ callback_klass = options.fetch(:callback_class)
150
+ callback_type = options.fetch(:callback_type)
151
+
152
+ validate_callback_condition(from: from, to: to)
153
+ callbacks[callback_type] <<
154
+ callback_klass.new(from: from, to: to, callback: block)
155
+ end
156
+
151
157
  def validate_state(state)
152
158
  unless states.include?(state.to_s)
153
159
  raise InvalidStateError, "Invalid state '#{state}'"
@@ -164,6 +170,10 @@ module Statesman
164
170
  def to_s_or_nil(input)
165
171
  input.nil? ? input : input.to_s
166
172
  end
173
+
174
+ def array_to_s_or_nil(input)
175
+ Array(input).map { |item| to_s_or_nil(item) }
176
+ end
167
177
  end
168
178
 
169
179
  def initialize(object,
@@ -1,3 +1,3 @@
1
1
  module Statesman
2
- VERSION = "0.8.3"
2
+ VERSION = "1.0.0.beta1"
3
3
  end
@@ -28,5 +28,6 @@ describe Statesman::MigrationGenerator, type: :generator do
28
28
 
29
29
  it { is_expected.to contain(/:bacon_transition/) }
30
30
  it { is_expected.not_to contain(/:yummy\/bacon/) }
31
+ it { is_expected.to contain(/null: false/) }
31
32
  end
32
33
  end
@@ -12,46 +12,69 @@ describe Statesman::Adapters::ActiveRecordModel do
12
12
  def self.transition_class
13
13
  MyActiveRecordModelTransition
14
14
  end
15
+
16
+ def self.initial_state
17
+ MyStateMachine.initial_state
18
+ end
15
19
  end
16
20
  end
17
21
 
18
22
  let!(:model) do
19
23
  model = MyActiveRecordModel.create
20
- model.my_active_record_model_transitions.create(to_state: :state_a)
24
+ model.my_active_record_model_transitions.create(to_state: :succeeded)
21
25
  model
22
26
  end
23
27
 
24
28
  let!(:other_model) do
25
29
  model = MyActiveRecordModel.create
26
- model.my_active_record_model_transitions.create(to_state: :state_b)
30
+ model.my_active_record_model_transitions.create(to_state: :failed)
31
+ model
32
+ end
33
+
34
+ let!(:initial_state_model) { MyActiveRecordModel.create }
35
+
36
+ let!(:returned_to_initial_model) do
37
+ model = MyActiveRecordModel.create
38
+ model.my_active_record_model_transitions.create(to_state: :failed)
39
+ model.my_active_record_model_transitions.create(to_state: :initial)
27
40
  model
28
41
  end
29
42
 
30
43
  describe ".in_state" do
31
44
  context "given a single state" do
32
- subject { MyActiveRecordModel.in_state(:state_a) }
45
+ subject { MyActiveRecordModel.in_state(:succeeded) }
33
46
 
34
47
  it { is_expected.to include model }
35
48
  end
36
49
 
37
50
  context "given multiple states" do
38
- subject { MyActiveRecordModel.in_state(:state_a, :state_b) }
51
+ subject { MyActiveRecordModel.in_state(:succeeded, :failed) }
39
52
 
40
53
  it { is_expected.to include model }
41
54
  it { is_expected.to include other_model }
42
55
  end
56
+
57
+ context "given the initial state" do
58
+ subject { MyActiveRecordModel.in_state(:initial) }
59
+
60
+ it { is_expected.to include initial_state_model }
61
+ it { is_expected.to include returned_to_initial_model }
62
+ end
43
63
  end
44
64
 
45
65
  describe ".not_in_state" do
46
66
  context "given a single state" do
47
- subject { MyActiveRecordModel.not_in_state(:state_b) }
67
+ subject { MyActiveRecordModel.not_in_state(:failed) }
48
68
  it { is_expected.to include model }
49
69
  it { is_expected.not_to include other_model }
50
70
  end
51
71
 
52
72
  context "given multiple states" do
53
- subject { MyActiveRecordModel.not_in_state(:state_a, :state_b) }
54
- it { is_expected.to eq([]) }
73
+ subject { MyActiveRecordModel.not_in_state(:succeeded, :failed) }
74
+ it do
75
+ is_expected.to match_array([initial_state_model,
76
+ returned_to_initial_model])
77
+ end
55
78
  end
56
79
  end
57
80
  end
@@ -56,7 +56,7 @@ describe Statesman::Adapters::ActiveRecord do
56
56
  end
57
57
 
58
58
  describe "#create" do
59
- let(:adapter) do
59
+ let!(:adapter) do
60
60
  described_class.new(MyActiveRecordModelTransition, model, observer)
61
61
  end
62
62
  let(:from) { :x }
@@ -123,8 +123,8 @@ describe Statesman::Adapters::ActiveRecord do
123
123
 
124
124
  context "with a pre-fetched transition history" do
125
125
  before do
126
- # inspect the transitions to coerce a [pre-]load
127
- model.my_active_record_model_transitions.inspect
126
+ adapter.create(:x, :y)
127
+ model.my_active_record_model_transitions.load_target
128
128
  end
129
129
 
130
130
  it "doesn't query the database" do
@@ -72,7 +72,7 @@ describe Statesman::Callback do
72
72
 
73
73
  context "with any from value on the callback" do
74
74
  let(:callback) do
75
- Statesman::Callback.new(to: :y, callback: cb_lambda)
75
+ Statesman::Callback.new(to: [:y, :z], callback: cb_lambda)
76
76
  end
77
77
  let(:from) { :x }
78
78
 
@@ -81,6 +81,11 @@ describe Statesman::Callback do
81
81
  it { is_expected.to be_truthy }
82
82
  end
83
83
 
84
+ context "and another allowed to value" do
85
+ let(:to) { :z }
86
+ it { is_expected.to be_truthy }
87
+ end
88
+
84
89
  context "and a disallowed to value" do
85
90
  let(:to) { :a }
86
91
  it { is_expected.to be_falsey }
@@ -218,54 +218,94 @@ describe Statesman::Machine do
218
218
  machine.class_eval do
219
219
  state :x, initial: true
220
220
  state :y
221
- transition from: :x, to: :y
221
+ state :z
222
+ transition from: :x, to: [:y, :z]
222
223
  end
223
224
  end
224
225
 
225
- it "stores callbacks" do
226
- expect do
227
- machine.send(assignment_method) {}
228
- end.to change(machine.callbacks[callback_store], :count).by(1)
226
+ let(:options) { { from: nil, to: [] } }
227
+ let(:set_callback) { machine.send(assignment_method, options) {} }
228
+
229
+ shared_examples "fails" do |error_type|
230
+ it "raises an exception" do
231
+ expect { set_callback }.to raise_error(error_type)
232
+ end
233
+
234
+ it "does not add a callback" do
235
+ expect do
236
+ begin
237
+ set_callback
238
+ rescue error_type
239
+ nil
240
+ end
241
+ end.to_not change(machine.callbacks[callback_store], :count)
242
+ end
229
243
  end
230
244
 
231
- it "stores callback instances" do
232
- machine.send(assignment_method) {}
233
- machine.callbacks[callback_store].each do |callback|
234
- expect(callback).to be_a(Statesman::Callback)
245
+ shared_examples "adds callback" do
246
+ it "does not raise" do
247
+ expect { set_callback }.to_not raise_error
248
+ end
249
+
250
+ it "stores callbacks" do
251
+ expect { set_callback }.to change(
252
+ machine.callbacks[callback_store], :count).by(1)
253
+ end
254
+
255
+ it "stores callback instances" do
256
+ set_callback
257
+ machine.callbacks[callback_store].each do |callback|
258
+ expect(callback).to be_a(Statesman::Callback)
259
+ end
235
260
  end
236
261
  end
237
262
 
238
263
  context "with invalid states" do
239
- it "raises an exception when both are invalid" do
240
- expect do
241
- machine.send(assignment_method, from: :foo, to: :bar) {}
242
- end.to raise_error(Statesman::InvalidStateError)
264
+ context "when both are invalid" do
265
+ let(:options) { { from: :foo, to: :bar } }
266
+ it_behaves_like "fails", Statesman::InvalidStateError
243
267
  end
244
268
 
245
- it "raises an exception with a terminal from state and nil to state" do
246
- expect do
247
- machine.send(assignment_method, from: :y) {}
248
- end.to raise_error(Statesman::InvalidTransitionError)
269
+ context "from a terminal state to anything" do
270
+ let(:options) { { from: :y, to: [] } }
271
+ it_behaves_like "fails", Statesman::InvalidTransitionError
249
272
  end
250
273
 
251
- it "raises an exception with an initial to state and nil from state" do
252
- expect do
253
- machine.send(assignment_method, to: :x) {}
254
- end.to raise_error(Statesman::InvalidTransitionError)
274
+ context "to an initial state and from anything" do
275
+ let(:options) { { from: nil, to: :x } }
276
+ it_behaves_like "fails", Statesman::InvalidTransitionError
277
+ end
278
+
279
+ context "from a terminal state and to multiple states" do
280
+ let(:options) { { from: :y, to: [:x, :z] } }
281
+ it_behaves_like "fails", Statesman::InvalidTransitionError
282
+ end
283
+
284
+ context "to an initial state and other states" do
285
+ let(:options) { { from: nil, to: [:y, :x, :z] } }
286
+ it_behaves_like "fails", Statesman::InvalidTransitionError
255
287
  end
256
288
  end
257
289
 
258
290
  context "with validate_states" do
259
- it "allows a nil from state" do
260
- expect do
261
- machine.send(assignment_method, to: :y) {}
262
- end.to_not raise_error
291
+ context "from anything" do
292
+ let(:options) { { from: nil, to: :y } }
293
+ it_behaves_like "adds callback"
263
294
  end
264
295
 
265
- it "allows a nil to state" do
266
- expect do
267
- machine.send(assignment_method, from: :x) {}
268
- end.to_not raise_error
296
+ context "to anything" do
297
+ let(:options) { { from: :x, to: [] } }
298
+ it_behaves_like "adds callback"
299
+ end
300
+
301
+ context "to several" do
302
+ let(:options) { { from: :x, to: [:y, :z] } }
303
+ it_behaves_like "adds callback"
304
+ end
305
+
306
+ context "from any to several" do
307
+ let(:options) { { from: nil, to: [:y, :z] } }
308
+ it_behaves_like "adds callback"
269
309
  end
270
310
  end
271
311
  end
@@ -3,8 +3,24 @@ require "json"
3
3
 
4
4
  DB = Pathname.new("test.sqlite3")
5
5
 
6
+ class MyStateMachine
7
+ include Statesman::Machine
8
+
9
+ state :initial, initial: true
10
+ state :succeeded
11
+ state :failed
12
+
13
+ transition from: :initial, to: [:succeeded, :failed]
14
+ transition from: :failed, to: :initial
15
+ end
16
+
6
17
  class MyActiveRecordModel < ActiveRecord::Base
7
18
  has_many :my_active_record_model_transitions
19
+
20
+ def state_machine
21
+ @state_machine ||= MyStateMachine
22
+ .new(self, transition_class: MyActiveRecordModelTransition)
23
+ end
8
24
  end
9
25
 
10
26
  class MyActiveRecordModelTransition < ActiveRecord::Base
@@ -7,6 +7,8 @@ end
7
7
  class MyMongoidModel
8
8
  include Mongoid::Document
9
9
 
10
+ field :current_state, type: String
11
+
10
12
  has_many :my_mongoid_model_transitions
11
13
  end
12
14
 
data/statesman.gemspec CHANGED
@@ -20,13 +20,13 @@ Gem::Specification.new do |spec|
20
20
 
21
21
  spec.add_development_dependency "bundler", "~> 1.3"
22
22
  spec.add_development_dependency "rake"
23
- spec.add_development_dependency "rspec", "~> 3.0"
23
+ spec.add_development_dependency "rspec", "~> 3.1"
24
24
  spec.add_development_dependency "rspec-its", "~> 1.0"
25
25
  spec.add_development_dependency "guard-rspec", "~> 4.3"
26
- spec.add_development_dependency "rubocop", "~> 0.24.1"
26
+ spec.add_development_dependency "rubocop", "~> 0.26"
27
27
  spec.add_development_dependency "guard-rubocop", "~> 1.1"
28
- spec.add_development_dependency "sqlite3", "~> 1.3.8"
29
- spec.add_development_dependency "mongoid", "~> 3.1.5"
30
- spec.add_development_dependency "rails", "~> 3.2"
28
+ spec.add_development_dependency "sqlite3", "~> 1.3"
29
+ spec.add_development_dependency "mongoid", "~> 4.0"
30
+ spec.add_development_dependency "activerecord", "~> 4.1"
31
31
  spec.add_development_dependency "ammeter", "~> 1.1"
32
32
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: statesman
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.3
4
+ version: 1.0.0.beta1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Harry Marr
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2014-09-02 00:00:00.000000000 Z
12
+ date: 2014-10-09 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bundler
@@ -45,14 +45,14 @@ dependencies:
45
45
  requirements:
46
46
  - - ~>
47
47
  - !ruby/object:Gem::Version
48
- version: '3.0'
48
+ version: '3.1'
49
49
  type: :development
50
50
  prerelease: false
51
51
  version_requirements: !ruby/object:Gem::Requirement
52
52
  requirements:
53
53
  - - ~>
54
54
  - !ruby/object:Gem::Version
55
- version: '3.0'
55
+ version: '3.1'
56
56
  - !ruby/object:Gem::Dependency
57
57
  name: rspec-its
58
58
  requirement: !ruby/object:Gem::Requirement
@@ -87,14 +87,14 @@ dependencies:
87
87
  requirements:
88
88
  - - ~>
89
89
  - !ruby/object:Gem::Version
90
- version: 0.24.1
90
+ version: '0.26'
91
91
  type: :development
92
92
  prerelease: false
93
93
  version_requirements: !ruby/object:Gem::Requirement
94
94
  requirements:
95
95
  - - ~>
96
96
  - !ruby/object:Gem::Version
97
- version: 0.24.1
97
+ version: '0.26'
98
98
  - !ruby/object:Gem::Dependency
99
99
  name: guard-rubocop
100
100
  requirement: !ruby/object:Gem::Requirement
@@ -115,42 +115,42 @@ dependencies:
115
115
  requirements:
116
116
  - - ~>
117
117
  - !ruby/object:Gem::Version
118
- version: 1.3.8
118
+ version: '1.3'
119
119
  type: :development
120
120
  prerelease: false
121
121
  version_requirements: !ruby/object:Gem::Requirement
122
122
  requirements:
123
123
  - - ~>
124
124
  - !ruby/object:Gem::Version
125
- version: 1.3.8
125
+ version: '1.3'
126
126
  - !ruby/object:Gem::Dependency
127
127
  name: mongoid
128
128
  requirement: !ruby/object:Gem::Requirement
129
129
  requirements:
130
130
  - - ~>
131
131
  - !ruby/object:Gem::Version
132
- version: 3.1.5
132
+ version: '4.0'
133
133
  type: :development
134
134
  prerelease: false
135
135
  version_requirements: !ruby/object:Gem::Requirement
136
136
  requirements:
137
137
  - - ~>
138
138
  - !ruby/object:Gem::Version
139
- version: 3.1.5
139
+ version: '4.0'
140
140
  - !ruby/object:Gem::Dependency
141
- name: rails
141
+ name: activerecord
142
142
  requirement: !ruby/object:Gem::Requirement
143
143
  requirements:
144
144
  - - ~>
145
145
  - !ruby/object:Gem::Version
146
- version: '3.2'
146
+ version: '4.1'
147
147
  type: :development
148
148
  prerelease: false
149
149
  version_requirements: !ruby/object:Gem::Requirement
150
150
  requirements:
151
151
  - - ~>
152
152
  - !ruby/object:Gem::Version
153
- version: '3.2'
153
+ version: '4.1'
154
154
  - !ruby/object:Gem::Dependency
155
155
  name: ammeter
156
156
  requirement: !ruby/object:Gem::Requirement
@@ -240,9 +240,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
240
240
  version: '0'
241
241
  required_rubygems_version: !ruby/object:Gem::Requirement
242
242
  requirements:
243
- - - '>='
243
+ - - '>'
244
244
  - !ruby/object:Gem::Version
245
- version: '0'
245
+ version: 1.3.1
246
246
  requirements: []
247
247
  rubyforge_project:
248
248
  rubygems_version: 2.4.1