statesman 0.8.3 → 1.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
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