rails_state_machine 1.1.3 → 2.0.0

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
- SHA1:
3
- metadata.gz: 9600103349aae06d290487b42d571e2c3128068b
4
- data.tar.gz: 6599f3c3ae8aa457876917b082a9394d17bd3af3
2
+ SHA256:
3
+ metadata.gz: 12ccebe03922ae8add9f35e88e0b2d9759e8105b8278713242fc88df697a320a
4
+ data.tar.gz: 44c8d685840968e1e69ce0a225062a9ca84e2715fd515cc7cbada5923dff204a
5
5
  SHA512:
6
- metadata.gz: f824cc7e14c0a452dcec86193967825d8d98a83fffa18e1330ac18532d31b46a2121b509391b37e7594c0b9c44dce39ec73fe72a290aaa2ce9bea98b9dd1b149
7
- data.tar.gz: a39eef615eefffb383c1f9a0576de0f56022e49275a056ebef9f9b792305247968805f879fe7287c5a50ac51440c3a8bc479c1a9852734951d63962e73875ad4
6
+ metadata.gz: 6e1a075ea8c01a222e872672925ce96c884491f4deced754c21dc598f65dd5aecb9381f0097269c9efdd5da855a10a69e9a1da63b1decaf047990cc7e8e57487
7
+ data.tar.gz: 6337d7f34d934b8a62239dcef8ed45a88fe545311cbc0e0c7b0728a03f047362e237104802caf929ff3ea72e669ba445365dc8afa7e92737893904421f798ff4
@@ -1 +1 @@
1
- 2.4.4
1
+ 2.4.6
@@ -7,19 +7,31 @@ before_script:
7
7
  - mysql -e 'create database IF NOT EXISTS rails_state_machine_test;'
8
8
 
9
9
  install:
10
+ - gem install bundler:2.0.2
10
11
  # Replace default Travis CI bundler script with a version that doesn't
11
12
  # explode when lockfile doesn't match recently bumped version
12
13
  - bundle install --no-deployment --jobs=3 --retry=3 --path=${BUNDLE_PATH:-vendor/bundle}
13
14
 
14
15
  script: bundle exec rake current_rspec
15
16
 
16
- rvm:
17
- - 2.3.7
18
- - 2.4.4
19
- - 2.5.1
20
-
21
- gemfile:
22
- - Gemfile.5.1.pg
23
- - Gemfile.5.2.pg
17
+ matrix:
18
+ include:
19
+ - rvm: 2.3.8
20
+ gemfile: Gemfile.5.1.pg
21
+ - rvm: 2.4.6
22
+ gemfile: Gemfile.5.1.pg
23
+ - rvm: 2.3.8
24
+ gemfile: Gemfile.5.2.pg
25
+ - rvm: 2.4.6
26
+ gemfile: Gemfile.5.2.pg
27
+ - rvm: 2.5.6
28
+ gemfile: Gemfile.5.2.pg
29
+ - rvm: 2.5.6
30
+ gemfile: Gemfile.6.0.pg
31
+ - rvm: 2.6.4
32
+ gemfile: Gemfile.6.0.pg
24
33
 
25
34
  dist: trusty
35
+
36
+ addons:
37
+ postgresql: 9.3
@@ -5,13 +5,23 @@ This project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html
5
5
 
6
6
  ## Unreleased
7
7
 
8
+ ### Compatible changes
9
+
8
10
  ### Breaking changes
9
11
 
10
- -
12
+
13
+ ## 2.0.0 2019-09-30
11
14
 
12
15
  ### Compatible changes
13
16
 
14
- -
17
+ - Added: State machine can now use an attribute other than `state` to represent the machine's state.
18
+ - Added: It is now possible to define multiple state machines on the same model. States and event names
19
+ have to differ, though.
20
+
21
+ ### Breaking changes
22
+
23
+ - Removed: Dropped support for adding a state machine to a model without including `RailsStateMachine::Model`.
24
+
15
25
 
16
26
  ## 1.1.3 2019-08-12
17
27
 
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rails_state_machine (1.1.3)
4
+ rails_state_machine (2.0.0)
5
5
  activerecord
6
6
 
7
7
  GEM
@@ -53,7 +53,7 @@ GEM
53
53
  database_cleaner (1.6.2)
54
54
  diff-lcs (1.3)
55
55
  erubi (1.7.0)
56
- gemika (0.3.2)
56
+ gemika (0.4.0)
57
57
  globalid (0.4.1)
58
58
  activesupport (>= 4.2.0)
59
59
  i18n (0.9.1)
@@ -146,4 +146,4 @@ DEPENDENCIES
146
146
  rspec (~> 3.5)
147
147
 
148
148
  BUNDLED WITH
149
- 1.17.1
149
+ 2.0.2
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- rails_state_machine (1.1.3)
4
+ rails_state_machine (2.0.0)
5
5
  activerecord
6
6
 
7
7
  GEM
@@ -57,7 +57,7 @@ GEM
57
57
  database_cleaner (1.7.0)
58
58
  diff-lcs (1.3)
59
59
  erubi (1.7.1)
60
- gemika (0.3.4)
60
+ gemika (0.4.0)
61
61
  globalid (0.4.1)
62
62
  activesupport (>= 4.2.0)
63
63
  i18n (1.1.0)
@@ -154,4 +154,4 @@ DEPENDENCIES
154
154
  rspec (~> 3.5)
155
155
 
156
156
  BUNDLED WITH
157
- 1.17.1
157
+ 2.0.2
@@ -0,0 +1,15 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Runtime dependencies
4
+ gem 'rails', '~>6.0.0'
5
+ gem 'pg'
6
+
7
+ # Development dependencies
8
+ gem 'rspec', '~>3.5'
9
+ gem 'rake'
10
+ gem 'pry-byebug'
11
+ gem 'gemika'
12
+ gem 'database_cleaner'
13
+
14
+ # Gem under test
15
+ gem 'rails_state_machine', :path => '.'
@@ -0,0 +1,173 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ rails_state_machine (2.0.0)
5
+ activerecord
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ actioncable (6.0.0)
11
+ actionpack (= 6.0.0)
12
+ nio4r (~> 2.0)
13
+ websocket-driver (>= 0.6.1)
14
+ actionmailbox (6.0.0)
15
+ actionpack (= 6.0.0)
16
+ activejob (= 6.0.0)
17
+ activerecord (= 6.0.0)
18
+ activestorage (= 6.0.0)
19
+ activesupport (= 6.0.0)
20
+ mail (>= 2.7.1)
21
+ actionmailer (6.0.0)
22
+ actionpack (= 6.0.0)
23
+ actionview (= 6.0.0)
24
+ activejob (= 6.0.0)
25
+ mail (~> 2.5, >= 2.5.4)
26
+ rails-dom-testing (~> 2.0)
27
+ actionpack (6.0.0)
28
+ actionview (= 6.0.0)
29
+ activesupport (= 6.0.0)
30
+ rack (~> 2.0)
31
+ rack-test (>= 0.6.3)
32
+ rails-dom-testing (~> 2.0)
33
+ rails-html-sanitizer (~> 1.0, >= 1.2.0)
34
+ actiontext (6.0.0)
35
+ actionpack (= 6.0.0)
36
+ activerecord (= 6.0.0)
37
+ activestorage (= 6.0.0)
38
+ activesupport (= 6.0.0)
39
+ nokogiri (>= 1.8.5)
40
+ actionview (6.0.0)
41
+ activesupport (= 6.0.0)
42
+ builder (~> 3.1)
43
+ erubi (~> 1.4)
44
+ rails-dom-testing (~> 2.0)
45
+ rails-html-sanitizer (~> 1.1, >= 1.2.0)
46
+ activejob (6.0.0)
47
+ activesupport (= 6.0.0)
48
+ globalid (>= 0.3.6)
49
+ activemodel (6.0.0)
50
+ activesupport (= 6.0.0)
51
+ activerecord (6.0.0)
52
+ activemodel (= 6.0.0)
53
+ activesupport (= 6.0.0)
54
+ activestorage (6.0.0)
55
+ actionpack (= 6.0.0)
56
+ activejob (= 6.0.0)
57
+ activerecord (= 6.0.0)
58
+ marcel (~> 0.3.1)
59
+ activesupport (6.0.0)
60
+ concurrent-ruby (~> 1.0, >= 1.0.2)
61
+ i18n (>= 0.7, < 2)
62
+ minitest (~> 5.1)
63
+ tzinfo (~> 1.1)
64
+ zeitwerk (~> 2.1, >= 2.1.8)
65
+ builder (3.2.3)
66
+ byebug (10.0.2)
67
+ coderay (1.1.2)
68
+ concurrent-ruby (1.1.5)
69
+ crass (1.0.4)
70
+ database_cleaner (1.7.0)
71
+ diff-lcs (1.3)
72
+ erubi (1.9.0)
73
+ gemika (0.4.0)
74
+ globalid (0.4.2)
75
+ activesupport (>= 4.2.0)
76
+ i18n (1.6.0)
77
+ concurrent-ruby (~> 1.0)
78
+ loofah (2.3.0)
79
+ crass (~> 1.0.2)
80
+ nokogiri (>= 1.5.9)
81
+ mail (2.7.1)
82
+ mini_mime (>= 0.1.1)
83
+ marcel (0.3.3)
84
+ mimemagic (~> 0.3.2)
85
+ method_source (0.9.2)
86
+ mimemagic (0.3.3)
87
+ mini_mime (1.0.2)
88
+ mini_portile2 (2.4.0)
89
+ minitest (5.12.2)
90
+ nio4r (2.5.2)
91
+ nokogiri (1.10.4)
92
+ mini_portile2 (~> 2.4.0)
93
+ pg (1.1.2)
94
+ pry (0.11.3)
95
+ coderay (~> 1.1.0)
96
+ method_source (~> 0.9.0)
97
+ pry-byebug (3.6.0)
98
+ byebug (~> 10.0)
99
+ pry (~> 0.10)
100
+ rack (2.0.7)
101
+ rack-test (1.1.0)
102
+ rack (>= 1.0, < 3)
103
+ rails (6.0.0)
104
+ actioncable (= 6.0.0)
105
+ actionmailbox (= 6.0.0)
106
+ actionmailer (= 6.0.0)
107
+ actionpack (= 6.0.0)
108
+ actiontext (= 6.0.0)
109
+ actionview (= 6.0.0)
110
+ activejob (= 6.0.0)
111
+ activemodel (= 6.0.0)
112
+ activerecord (= 6.0.0)
113
+ activestorage (= 6.0.0)
114
+ activesupport (= 6.0.0)
115
+ bundler (>= 1.3.0)
116
+ railties (= 6.0.0)
117
+ sprockets-rails (>= 2.0.0)
118
+ rails-dom-testing (2.0.3)
119
+ activesupport (>= 4.2.0)
120
+ nokogiri (>= 1.6)
121
+ rails-html-sanitizer (1.2.0)
122
+ loofah (~> 2.2, >= 2.2.2)
123
+ railties (6.0.0)
124
+ actionpack (= 6.0.0)
125
+ activesupport (= 6.0.0)
126
+ method_source
127
+ rake (>= 0.8.7)
128
+ thor (>= 0.20.3, < 2.0)
129
+ rake (13.0.0)
130
+ rspec (3.8.0)
131
+ rspec-core (~> 3.8.0)
132
+ rspec-expectations (~> 3.8.0)
133
+ rspec-mocks (~> 3.8.0)
134
+ rspec-core (3.8.0)
135
+ rspec-support (~> 3.8.0)
136
+ rspec-expectations (3.8.1)
137
+ diff-lcs (>= 1.2.0, < 2.0)
138
+ rspec-support (~> 3.8.0)
139
+ rspec-mocks (3.8.0)
140
+ diff-lcs (>= 1.2.0, < 2.0)
141
+ rspec-support (~> 3.8.0)
142
+ rspec-support (3.8.0)
143
+ sprockets (3.7.2)
144
+ concurrent-ruby (~> 1.0)
145
+ rack (> 1, < 3)
146
+ sprockets-rails (3.2.1)
147
+ actionpack (>= 4.0)
148
+ activesupport (>= 4.0)
149
+ sprockets (>= 3.0.0)
150
+ thor (0.20.3)
151
+ thread_safe (0.3.6)
152
+ tzinfo (1.2.5)
153
+ thread_safe (~> 0.1)
154
+ websocket-driver (0.7.1)
155
+ websocket-extensions (>= 0.1.0)
156
+ websocket-extensions (0.1.4)
157
+ zeitwerk (2.1.10)
158
+
159
+ PLATFORMS
160
+ ruby
161
+
162
+ DEPENDENCIES
163
+ database_cleaner
164
+ gemika
165
+ pg
166
+ pry-byebug
167
+ rails (~> 6.0.0)
168
+ rails_state_machine!
169
+ rake
170
+ rspec (~> 3.5)
171
+
172
+ BUNDLED WITH
173
+ 2.0.2
data/README.md CHANGED
@@ -71,39 +71,6 @@ event :request_feedback do
71
71
  end
72
72
  ```
73
73
 
74
- As an alternative to using `RailsStateMachine::Model` and `state_machine do`, configure the state machine manually. This only adds the `state_machine` to your model, but no `states` or `state_events`.
75
-
76
- ```ruby
77
- class YourModel < ApplicationRecord
78
- RailsStateMachine::StateMachine.new(self).configure do
79
- state :draft, initial: true
80
- state :review_pending
81
- state :approved
82
- state :rejected
83
-
84
- event :request_review do
85
- transitions from: [:draft, :rejected], to: :review_pending
86
- end
87
-
88
- event :approve do
89
- transitions from: :review_pending, to: :approved
90
- end
91
-
92
- event :reject do
93
- transitions from: :review_pending, to: :rejected
94
- end
95
- end
96
-
97
- def self.states
98
- state_machine.state_names
99
- end
100
-
101
- def self.state_events
102
- state_machine.event_names
103
- end
104
- end
105
- ```
106
-
107
74
  ## Event callbacks
108
75
 
109
76
  Here is a list with all the available callbacks, listed in the same order in which they will get called during the respective operations. The callbacks are chained with the existing active record callbacks on the model.
@@ -137,6 +104,47 @@ event :request_review do
137
104
  end
138
105
  ```
139
106
 
107
+ ## Other state attributes and multiple state machines on the same model
108
+
109
+ To use a state attribute other than the default `state`, pass it to the `.state_machine` method:
110
+
111
+ ```
112
+ state_machine :review_state do
113
+ # ...
114
+ end
115
+ ```
116
+
117
+ This also allows you to define multiple state machines on the same model. Note that event
118
+ and state names still have to be unique for the whole model.
119
+
120
+
121
+ ## Taking multiple transitions
122
+
123
+ You can safely take a second transition inside an after_save callback. All relevant
124
+ callbacks will be run.
125
+
126
+ ```
127
+ state_machine do
128
+ state :draft, initial: true
129
+ state :review_pending
130
+ state :approved
131
+
132
+ event :request_review do
133
+ transitions from: [:draft, :rejected], to: :review_pending
134
+
135
+ after_save do
136
+ if auto_approve?
137
+ approve
138
+ end
139
+ end
140
+ end
141
+
142
+ event :approve do
143
+ transitions from: :review_pending, to: :approved
144
+ end
145
+ end
146
+ ```
147
+
140
148
  ## Development
141
149
 
142
150
  There are tests in `spec`. We only accept PRs with tests. To run tests:
@@ -175,4 +183,4 @@ The gem is available as open source under the terms of the [MIT License](https:/
175
183
 
176
184
  ## Credits
177
185
 
178
- Arne Hartherz and Emanuel Denzel from [makandra](https://makandra.de/).
186
+ Arne Hartherz, Emanuel Denzel, Tobias Kraze from [makandra](https://makandra.de/).
@@ -1,5 +1,7 @@
1
1
  require 'rails_state_machine/version'
2
2
  require 'rails_state_machine/state'
3
3
  require 'rails_state_machine/event'
4
+ require 'rails_state_machine/callbacks'
4
5
  require 'rails_state_machine/state_machine'
6
+ require 'rails_state_machine/state_manager'
5
7
  require 'rails_state_machine/model'
@@ -0,0 +1,68 @@
1
+ module RailsStateMachine
2
+ module Callbacks
3
+ class << self
4
+ def included(model)
5
+ register_callbacks(model)
6
+ register_validations(model)
7
+ end
8
+
9
+ private
10
+
11
+ def register_callbacks(model)
12
+ model.class_eval do
13
+ before_validation :run_state_events_before_validation
14
+ before_save :register_state_events_for_callbacks
15
+ before_save { flush_state_event_callbacks(:before_save) }
16
+ after_save { flush_state_event_callbacks(:after_save) }
17
+ after_commit { flush_state_event_callbacks(:after_commit) }
18
+ end
19
+ end
20
+
21
+ def register_validations(model)
22
+ model.class_eval do
23
+ after_validation :revert_states, if: -> { errors.any? }
24
+ end
25
+ end
26
+ end
27
+
28
+ def run_state_events_before_validation
29
+ # Since validations may be skipped, we will not register validation callbacks in @state_event_callbacks,
30
+ # but call them explicitly when before_validation callbacks are triggered.
31
+ state_machine_state_managers.each do |state_manager|
32
+ state_manager.next_event&.run_before_validation(self)
33
+ end
34
+ end
35
+
36
+ def register_state_events_for_callbacks
37
+ @state_event_callbacks ||= {
38
+ before_save: [],
39
+ after_save: [],
40
+ after_commit: []
41
+ }
42
+ state_machine_state_managers.each do |state_manager|
43
+ if (next_event = state_manager.next_event)
44
+ @state_event_callbacks[:before_save] << next_event
45
+ @state_event_callbacks[:after_save] << next_event
46
+ @state_event_callbacks[:after_commit] << next_event
47
+ state_manager.next_event = nil
48
+ end
49
+ end
50
+
51
+ true
52
+ end
53
+
54
+ def flush_state_event_callbacks(name)
55
+ if @state_event_callbacks
56
+ while (event = @state_event_callbacks[name].shift)
57
+ event.public_send("run_#{name}", self)
58
+ end
59
+ end
60
+ end
61
+
62
+ def revert_states
63
+ state_machine_state_managers.each do |state_manager|
64
+ state_manager.revert
65
+ end
66
+ end
67
+ end
68
+ end
@@ -1,21 +1,71 @@
1
1
  module RailsStateMachine
2
+ DEFAULT_STATE_ATTRIBUTE = :state
3
+
2
4
  module Model
3
5
  def self.included(base)
4
- base.extend ClassMethods
6
+ base.class_eval do
7
+ extend ClassMethods
8
+
9
+ cattr_accessor :state_machines
10
+ self.state_machines = {}
11
+
12
+ delegate :state_machine, to: :class
13
+ end
5
14
  end
6
15
 
7
16
  module ClassMethods
8
- def state_machine(&block)
9
- StateMachine.new(self).configure(&block)
17
+ def state_machine(state_attribute = DEFAULT_STATE_ATTRIBUTE, &block)
18
+ state_machine = state_machines[state_attribute] ||= StateMachine.new(self, state_attribute)
19
+ if block
20
+ include(Callbacks) unless self < Callbacks
21
+ state_machine.configure(&block)
22
+ end
23
+ state_machine
24
+ end
25
+
26
+ def states(state_attribute = DEFAULT_STATE_ATTRIBUTE)
27
+ state_machine(state_attribute).state_names
28
+ end
29
+
30
+ def state_events(state_attribute = DEFAULT_STATE_ATTRIBUTE)
31
+ state_machine(state_attribute).event_names
10
32
  end
33
+ end
34
+
35
+
36
+ private
11
37
 
12
- def states
13
- state_machine.state_names
38
+ def state_machine_state_manager(state_attribute)
39
+ @state_machine_state_managers ||= {}
40
+ @state_machine_state_managers[state_attribute] ||= StateManager.new(self, state_machine(state_attribute), state_attribute)
41
+ end
42
+
43
+ def state_machine_state_managers
44
+ self.state_machines.keys.collect do |state_attribute|
45
+ state_machine_state_manager(state_attribute)
14
46
  end
47
+ end
15
48
 
16
- def state_events
17
- state_machine.event_names
49
+ def prepare_state_event_change(attributes)
50
+ if ActiveRecord::VERSION::STRING < '5.2' && saved_changes?
51
+ # After calling `save`, ActiveRecord 5.1 will flag the changes that it just stored as saved.
52
+ # https://github.com/rails/rails/blob/v5.1.4/activerecord/lib/active_record/attribute_methods/dirty.rb#L33-L46
53
+ #
54
+ # When taking multiple state events (e.g. a second event called inside an `after_save` callback) and thus
55
+ # saving after other changes were just saved, we need to mimic that behavior. Otherwise, ActiveRecord will
56
+ # print deprecation warnings like these:
57
+ #
58
+ # DEPRECATION WARNING: The behavior of `attribute_was` inside of after callbacks will be changing in the
59
+ # next version of Rails. The new return value will reflect the behavior of calling the method after
60
+ # `save` returned (e.g. the opposite of what it returns now). To maintain the current behavior, use
61
+ # `attribute_before_last_save` instead.
62
+ #
63
+ # These actually originate from ActiveRecord internals which try to determine the changes that should be
64
+ # stored for the second save. It is probably a shortcoming of ActiveRecord 5.1.x that will be fixed, but
65
+ # since the current/previous save was already successful, the right action is to just call `changes_applied`.
66
+ changes_applied
18
67
  end
68
+ self.attributes = attributes
19
69
  end
20
70
  end
21
71
  end
@@ -1,13 +1,13 @@
1
1
  module RailsStateMachine
2
2
  class StateMachine
3
- def initialize(model)
4
- @model = model
5
-
6
- model_constant('StateMachineMethods', Module.new)
7
- @model.include(@model::StateMachineMethods)
3
+ attr_reader :model
8
4
 
5
+ def initialize(model, state_attribute)
6
+ @model = model
7
+ @state_attribute = state_attribute
9
8
  @states_by_name = {}
10
9
  @events_by_name = {}
10
+ build_model_module
11
11
  end
12
12
 
13
13
  def configure(&block)
@@ -18,11 +18,6 @@ module RailsStateMachine
18
18
  register_initial_state
19
19
 
20
20
  define_event_methods
21
-
22
- register_callbacks
23
- register_validations
24
- register_state_machine
25
-
26
21
  define_model_methods
27
22
  end
28
23
 
@@ -60,18 +55,6 @@ module RailsStateMachine
60
55
  event = Event.new(name, self)
61
56
  event.configure(&block)
62
57
 
63
- model_methods do
64
- define_method "#{event.name}" do |**attributes|
65
- prepare_state_event_change(attributes.merge(state_event: event.name))
66
- save
67
- end
68
-
69
- define_method "#{event.name}!" do |**attributes|
70
- prepare_state_event_change(attributes.merge(state_event: event.name))
71
- save!
72
- end
73
- end
74
-
75
58
  @events_by_name[name] = event
76
59
  end
77
60
 
@@ -79,18 +62,24 @@ module RailsStateMachine
79
62
  @model.const_set(name, value)
80
63
  end
81
64
 
82
- def model_methods(&block)
65
+ def build_model_module
83
66
  # Using a state machine defines several methods on the model.
84
67
  # The model should be able to re-define them and `super` into the original method, if necessary.
85
68
  # For that, we use a module to store all methods. The module is loaded into the model class.
86
- @model::StateMachineMethods.module_eval(&block)
69
+ @model_module = Module.new
70
+ @model.include(@model_module)
71
+ end
72
+
73
+ def model_module_eval(&block)
74
+ @model_module.module_eval(&block)
87
75
  end
88
76
 
89
77
  def define_state_methods
78
+ state_attribute = @state_attribute
90
79
  state_names.each do |state_name|
91
- model_methods do
80
+ model_module_eval do
92
81
  define_method "#{state_name}?" do
93
- self.state.to_s == state_name.to_s
82
+ state_machine_state_manager(state_attribute).state == state_name.to_s
94
83
  end
95
84
  end
96
85
  end
@@ -103,131 +92,49 @@ module RailsStateMachine
103
92
  end
104
93
 
105
94
  def register_initial_state
106
- initial_state = states.detect(&:initial?)
107
- return unless initial_state
95
+ state_attribute = @state_attribute
96
+ initial_state_name = states.detect(&:initial?)&.name
97
+ return unless initial_state_name
108
98
 
109
99
  @model.after_initialize do
110
- self.state ||= initial_state.name if new_record?
100
+ manager = state_machine_state_manager(state_attribute)
101
+ if new_record? && !manager.state
102
+ manager.state = initial_state_name
103
+ end
111
104
  end
112
105
  end
113
106
 
114
107
  def define_event_methods
108
+ state_attribute = @state_attribute
115
109
  event_names.each do |event_name, event|
116
- model_methods do
117
- define_method "may_#{event_name}?" do
118
- state_machine.find_event(event_name).allowed_from?(source_state)
110
+ model_module_eval do
111
+ define_method "#{event_name}" do |**attributes|
112
+ prepare_state_event_change(attributes.merge("#{state_attribute}_event": event_name))
113
+ save
119
114
  end
120
- end
121
- end
122
- end
123
-
124
- def register_callbacks
125
- @model.class_eval do
126
- before_validation :run_state_event_before_validation
127
- before_save :register_state_events_for_callbacks
128
- before_save { flush_state_event_callbacks(:before_save) }
129
- after_save { flush_state_event_callbacks(:after_save) }
130
- after_save :unset_next_state_machine_event
131
- after_commit { flush_state_event_callbacks(:after_commit) }
132
- end
133
- end
134
-
135
- def register_validations
136
- @model.class_eval do
137
- after_validation :revert_state, if: -> { errors.any? }
138
- end
139
- end
140
115
 
141
- def register_state_machine
142
- @model.class_eval do
143
- cattr_accessor :state_machine
144
- delegate :state_machine, to: :class
145
- end
146
-
147
- @model.state_machine = self
148
- end
149
-
150
- def define_model_methods
151
- model_methods do
152
- def state_event=(event_name)
153
- @next_state_machine_event = state_machine.find_event(event_name)
154
- @state_before_state_event = source_state
155
-
156
- # If the event can not transition from source_state, a TransitionNotFoundError will be raised
157
- self.state = @next_state_machine_event.future_state_name(source_state).to_s
158
- end
159
-
160
- def state_event
161
- @next_state_machine_event&.name
162
- end
163
-
164
- def source_state
165
- if new_record?
166
- state
167
- else
168
- state_in_database
116
+ define_method "#{event_name}!" do |**attributes|
117
+ prepare_state_event_change(attributes.merge("#{state_attribute}_event": event_name))
118
+ save!
169
119
  end
170
- end
171
-
172
- private
173
120
 
174
- def run_state_event_before_validation
175
- # Since validations may be skipped, we will not register validation callbacks in @state_event_callbacks,
176
- # but call them explicitly when before_validation callbacks are triggered.
177
- @next_state_machine_event&.run_before_validation(self)
178
- end
179
-
180
- def register_state_events_for_callbacks
181
- @state_event_callbacks ||= {
182
- before_save: [],
183
- after_save: [],
184
- after_commit: []
185
- }
186
- if @next_state_machine_event
187
- @state_event_callbacks[:before_save] << @next_state_machine_event
188
- @state_event_callbacks[:after_save] << @next_state_machine_event
189
- @state_event_callbacks[:after_commit] << @next_state_machine_event
190
- end
191
-
192
- true
193
- end
194
-
195
- def flush_state_event_callbacks(name)
196
- if @state_event_callbacks
197
- while (event = @state_event_callbacks[name].shift)
198
- event.public_send("run_#{name}", self)
199
- end
121
+ define_method "may_#{event_name}?" do
122
+ state_machine_state_manager(state_attribute).transition_allowed_for?(event_name)
200
123
  end
201
124
  end
125
+ end
126
+ end
202
127
 
203
- def unset_next_state_machine_event
204
- @next_state_machine_event = nil
205
- end
128
+ def define_model_methods
129
+ state_attribute = @state_attribute
206
130
 
207
- def revert_state
208
- self.state = @state_before_state_event if @next_state_machine_event
131
+ model_module_eval do
132
+ define_method :"#{state_attribute}_event=" do |event_name|
133
+ state_machine_state_manager(state_attribute).transition_to(event_name)
209
134
  end
210
135
 
211
- def prepare_state_event_change(attributes)
212
- if ActiveRecord::VERSION::STRING <= '5.2' && saved_changes?
213
- # After calling `save`, ActiveRecord 5.1 will flag the changes that it just stored as saved.
214
- # https://github.com/rails/rails/blob/v5.1.4/activerecord/lib/active_record/attribute_methods/dirty.rb#L33-L46
215
- #
216
- # When taking multiple state events (e.g. a second event called inside an `after_save` callback) and thus
217
- # saving after other changes were just saved, we need to mimic that behavior. Otherwise, ActiveRecord will
218
- # print deprecation warnings like these:
219
- #
220
- # DEPRECATION WARNING: The behavior of `attribute_was` inside of after callbacks will be changing in the
221
- # next version of Rails. The new return value will reflect the behavior of calling the method after
222
- # `save` returned (e.g. the opposite of what it returns now). To maintain the current behavior, use
223
- # `attribute_before_last_save` instead.
224
- #
225
- # These actually originate from ActiveRecord internals which try to determine the changes that should be
226
- # stored for the second save. It is probably a shortcoming of ActiveRecord 5.1.x that will be fixed, but
227
- # since the current/previous save was already successful, the right action is to just call `changes_applied`.
228
- changes_applied
229
- end
230
- self.attributes = attributes
136
+ define_method :"#{state_attribute}_event" do
137
+ state_machine_state_manager(state_attribute).next_event&.name
231
138
  end
232
139
  end
233
140
  end
@@ -0,0 +1,47 @@
1
+ module RailsStateMachine
2
+ class StateManager
3
+ attr_accessor :next_event, :state_before_state_event
4
+
5
+ def initialize(record, state_machine, state_attribute)
6
+ @record = record
7
+ @state_machine = state_machine
8
+ @state_attribute = state_attribute
9
+ end
10
+
11
+ def state
12
+ @record.public_send(@state_attribute)
13
+ end
14
+
15
+ def state_in_database
16
+ @record.public_send(:"#{@state_attribute}_in_database").to_s
17
+ end
18
+
19
+ def state=(value)
20
+ @record.public_send(:"#{@state_attribute}=", value)
21
+ end
22
+
23
+ def revert
24
+ self.state = @state_before_state_event if @next_event
25
+ end
26
+
27
+ def source_state
28
+ if @record.new_record?
29
+ state
30
+ else
31
+ state_in_database
32
+ end
33
+ end
34
+
35
+ def transition_to(event_name)
36
+ @next_event = @state_machine.find_event(event_name)
37
+ @state_before_state_event = source_state
38
+
39
+ # If the event can not transition from source_state, a TransitionNotFoundError will be raised
40
+ self.state = @next_event.future_state_name(source_state).to_s
41
+ end
42
+
43
+ def transition_allowed_for?(event_name)
44
+ @state_machine.find_event(event_name).allowed_from?(source_state)
45
+ end
46
+ end
47
+ end
@@ -1,3 +1,3 @@
1
1
  module RailsStateMachine
2
- VERSION = '1.1.3'
2
+ VERSION = '2.0.0'
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rails_state_machine
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.3
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Arne Hartherz
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2019-08-12 00:00:00.000000000 Z
12
+ date: 2019-09-30 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activerecord
@@ -84,6 +84,8 @@ files:
84
84
  - Gemfile.5.1.pg.lock
85
85
  - Gemfile.5.2.pg
86
86
  - Gemfile.5.2.pg.lock
87
+ - Gemfile.6.0.pg
88
+ - Gemfile.6.0.pg.lock
87
89
  - Gemfile.lock
88
90
  - LICENSE
89
91
  - LICENSE.txt
@@ -92,10 +94,12 @@ files:
92
94
  - bin/console
93
95
  - bin/setup
94
96
  - lib/rails_state_machine.rb
97
+ - lib/rails_state_machine/callbacks.rb
95
98
  - lib/rails_state_machine/event.rb
96
99
  - lib/rails_state_machine/model.rb
97
100
  - lib/rails_state_machine/state.rb
98
101
  - lib/rails_state_machine/state_machine.rb
102
+ - lib/rails_state_machine/state_manager.rb
99
103
  - lib/rails_state_machine/version.rb
100
104
  - rails_state_machine.gemspec
101
105
  homepage: https://github.com/makandra/rails_state_machine
@@ -117,8 +121,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
117
121
  - !ruby/object:Gem::Version
118
122
  version: '0'
119
123
  requirements: []
120
- rubyforge_project:
121
- rubygems_version: 2.6.14.1
124
+ rubygems_version: 3.0.3
122
125
  signing_key:
123
126
  specification_version: 4
124
127
  summary: ActiveRecord-bound state machine