statesman 8.0.0 → 9.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
2
  SHA256:
3
- metadata.gz: aeab084afcfb068e82eae390dbac202fb8167884b3a58610429ca2efb30a8b46
4
- data.tar.gz: 65f10904ea38a716a26f7acd6d6809a484f4cb51eb5458b98f94d2b29acfd746
3
+ metadata.gz: 33b89c9b5c5c0a2e31706aedd4861e3c8967d82ec50a1571715d0b2029ed14d2
4
+ data.tar.gz: 8b201ea428387579869649c7739b393a4beaf6fb2b27070cd54202d7260fd6b4
5
5
  SHA512:
6
- metadata.gz: 36a6a95abb071113a8a17e59982db8c0c1e82cd189d2b5c0d59e4825f894dfde59a0051357a935ff9c3070520541461a69177a08003c7734252c4c1ae42946b6
7
- data.tar.gz: 96887197a7551e0fe65269b727cff3cba0318c459e895836d05969120cd22d6b17f1f9052935ce768f91ff4af9a0d33aff43af025e41bba28f24a1948e188056
6
+ metadata.gz: 0c29e6cf1d191c99d4867f5461ee859241873a00f7ee71730bed266a0e0ee07180acb16ff879f275cf789a33a7c67de73d5eafe04f5981b24ff1bb05ad876cab
7
+ data.tar.gz: 108e0c8b7c56cbac445e8c99708bd57b830325be91a40827a87ed51773c81165971cdf76ea446bdf6eb1b2fd19afe7accd6fba0b885824a30998212aa0492d85
data/.circleci/config.yml CHANGED
@@ -1,187 +1,127 @@
1
1
  ---
2
- version: 2
2
+ version: 2.1
3
3
 
4
4
  references:
5
- steps: &steps
6
- - checkout
7
-
8
- - type: shell
9
- name: Write RAILS_VERSION to a file so we can use it for caching purposes
10
- command: echo "$RAILS_VERSION" > ~/RAILS_VERSION.txt
11
-
12
- - type: cache-restore
13
- key: statesman-{{ checksum "Gemfile" }}-{{ checksum "~/RAILS_VERSION.txt" }}
14
-
15
- - run: gem install bundler -v 2.1.4
16
-
17
- - run: bundle install --path vendor/bundle
5
+ bundle_install: &bundle_install
6
+ run:
7
+ name: Bundle
8
+ command: |
9
+ gem install bundler --no-document && \
10
+ bundle config set no-cache 'true' && \
11
+ bundle config set jobs '4' && \
12
+ bundle config set retry '3' && \
13
+ bundle install
18
14
 
19
- - type: cache-save
20
- key: statesman-{{ checksum "Gemfile" }}-{{ checksum "~/RAILS_VERSION.txt" }}
15
+ cache_bundle: &cache_bundle
16
+ save_cache:
17
+ key: bundle-<< parameters.ruby_version >>-<< parameters.rails_version >>-{{ checksum "statesman.gemspec" }}-{{ checksum "Gemfile" }}
21
18
  paths:
22
19
  - vendor/bundle
23
20
 
24
- - run: bundle exec rubocop
21
+ restore_bundle: &restore_bundle
22
+ restore_cache:
23
+ key: bundle-<< parameters.ruby_version >>-<< parameters.rails_version >>-{{ checksum "statesman.gemspec" }}-{{ checksum "Gemfile" }}
25
24
 
25
+ steps: &steps
26
+ - add_ssh_keys
27
+ - checkout
28
+ - run:
29
+ name: "Add dependencies"
30
+ command: |
31
+ sudo apt-get update && sudo apt-get install -y sqlite3 libsqlite3-dev
32
+ - *restore_bundle
33
+ - *bundle_install
34
+ - *cache_bundle
26
35
  - run: dockerize -wait tcp://localhost:$DATABASE_DEPENDENCY_PORT -timeout 1m
36
+ - run:
37
+ name: Run specs
38
+ command: |
39
+ bundle exec rspec $(circleci tests glob "spec/**/*_spec.rb" | circleci tests split --split-by=timings) --profile --format progress --format RspecJunitFormatter -o /tmp/circle_artifacts/rspec.xml
40
+ - run:
41
+ name: "Rubocop"
42
+ command: bundle exec rubocop --extra-details --display-style-guide --parallel --force-exclusion
43
+ - store_artifacts:
44
+ path: /tmp/circle_artifacts/
45
+ - store_test_results:
46
+ path: /tmp/circle_artifacts/
27
47
 
28
- - run: bundle exec rake
48
+ ruby_versions: &ruby_versions
49
+ - "2.5"
50
+ - "2.6"
51
+ - "2.7"
52
+ - "3.0"
29
53
 
30
- - type: store_test_results
31
- path: /tmp/test-results
54
+ rails_versions: &rails_versions
55
+ - "5.2.6"
56
+ - "6.0.4"
57
+ - "6.1.4"
58
+ - "main"
32
59
 
33
- jobs:
34
- build-ruby249-rails-524-mysql:
35
- docker:
36
- - image: circleci/ruby:2.4.9-node
37
- environment:
38
- - RAILS_VERSION=5.2.4
39
- - DATABASE_URL=mysql2://root@127.0.0.1/statesman_test
40
- - DATABASE_DEPENDENCY_PORT=3306
41
- - image: circleci/mysql:5.7.18
42
- environment:
43
- - MYSQL_ALLOW_EMPTY_PASSWORD=true
44
- - MYSQL_USER=root
45
- - MYSQL_PASSWORD=
46
- - MYSQL_DATABASE=statesman_test
47
- steps: *steps
48
- build-ruby249-rails-524-postgres:
49
- docker:
50
- - image: circleci/ruby:2.4.9-node
51
- environment:
52
- - RAILS_VERSION=5.2.4
53
- - DATABASE_URL=postgres://postgres@localhost/statesman_test
54
- - DATABASE_DEPENDENCY_PORT=5432
55
- - image: circleci/postgres:9.6
56
- environment:
57
- - POSTGRES_USER=postgres
58
- - POSTGRES_DB=statesman_test
59
- - POSTGRES_PASSWORD=statesman
60
- steps: *steps
60
+ mysql_versions: &mysql_versions
61
+ - "5.7.18"
61
62
 
62
- build-ruby265-rails-602-mysql:
63
- docker:
64
- - image: circleci/ruby:2.6.5-node
65
- environment:
66
- - RAILS_VERSION=6.0.2
67
- - DATABASE_URL=mysql2://root@127.0.0.1/statesman_test
68
- - DATABASE_DEPENDENCY_PORT=3306
69
- - image: circleci/mysql:5.7.18
70
- environment:
71
- - MYSQL_ALLOW_EMPTY_PASSWORD=true
72
- - MYSQL_USER=root
73
- - MYSQL_PASSWORD=
74
- - MYSQL_DATABASE=statesman_test
75
- steps: *steps
76
- build-ruby265-rails-602-postgres:
77
- docker:
78
- - image: circleci/ruby:2.6.5-node
79
- environment:
80
- - RAILS_VERSION=6.0.2
81
- - DATABASE_URL=postgres://postgres@localhost/statesman_test
82
- - DATABASE_DEPENDENCY_PORT=5432
83
- - image: circleci/postgres:9.6
84
- environment:
85
- - POSTGRES_USER=postgres
86
- - POSTGRES_DB=statesman_test
87
- - POSTGRES_PASSWORD=statesman
88
- steps: *steps
89
- build-ruby265-rails-master-mysql:
90
- docker:
91
- - image: circleci/ruby:2.6.5-node
92
- environment:
93
- - RAILS_VERSION=master
94
- - DATABASE_URL=mysql2://root@127.0.0.1/statesman_test
95
- - DATABASE_DEPENDENCY_PORT=3306
96
- - image: circleci/mysql:5.7.18
97
- environment:
98
- - MYSQL_ALLOW_EMPTY_PASSWORD=true
99
- - MYSQL_USER=root
100
- - MYSQL_PASSWORD=
101
- - MYSQL_DATABASE=statesman_test
102
- steps: *steps
103
- build-ruby265-rails-master-postgres:
104
- docker:
105
- - image: circleci/ruby:2.6.5-node
106
- environment:
107
- - RAILS_VERSION=master
108
- - DATABASE_URL=postgres://postgres@localhost/statesman_test
109
- - EXCLUDE_MONGOID=true
110
- - DATABASE_DEPENDENCY_PORT=5432
111
- - image: circleci/postgres:9.6
112
- environment:
113
- - POSTGRES_USER=postgres
114
- - POSTGRES_DB=statesman_test
115
- - POSTGRES_PASSWORD=statesman
116
- steps: *steps
63
+ psql_versions: &psql_versions
64
+ - "9.6"
117
65
 
118
- build-ruby270-rails-602-mysql:
119
- docker:
120
- - image: circleci/ruby:2.7.0-node
121
- environment:
122
- - RAILS_VERSION=6.0.2
123
- - DATABASE_URL=mysql2://root@127.0.0.1/statesman_test
124
- - DATABASE_DEPENDENCY_PORT=3306
125
- - image: circleci/mysql:5.7.18
126
- environment:
127
- - MYSQL_ALLOW_EMPTY_PASSWORD=true
128
- - MYSQL_USER=root
129
- - MYSQL_PASSWORD=
130
- - MYSQL_DATABASE=statesman_test
131
- steps: *steps
132
- build-ruby270-rails-602-postgres:
133
- docker:
134
- - image: circleci/ruby:2.7.0-node
135
- environment:
136
- - RAILS_VERSION=6.0.2
137
- - DATABASE_URL=postgres://postgres@localhost/statesman_test
138
- - DATABASE_DEPENDENCY_PORT=5432
139
- - image: circleci/postgres:9.6
140
- environment:
141
- - POSTGRES_USER=postgres
142
- - POSTGRES_DB=statesman_test
143
- - POSTGRES_PASSWORD=statesman
144
- steps: *steps
145
- build-ruby270-rails-master-mysql:
66
+ jobs:
67
+ rspec_mysql:
68
+ working_directory: /mnt/ramdisk
69
+ parameters:
70
+ ruby_version:
71
+ type: string
72
+ rails_version:
73
+ type: string
74
+ mysql_version:
75
+ type: string
146
76
  docker:
147
- - image: circleci/ruby:2.7.0-node
77
+ - image: cimg/ruby:<< parameters.ruby_version >>
148
78
  environment:
149
- - RAILS_VERSION=master
150
- - DATABASE_URL=mysql2://root@127.0.0.1/statesman_test
151
- - DATABASE_DEPENDENCY_PORT=3306
152
- - image: circleci/mysql:5.7.18
79
+ CIRCLE_TEST_REPORTS: /tmp/circle_artifacts/
80
+ DATABASE_URL: mysql2://foobar:password@127.0.0.1/statesman_test
81
+ DATABASE_DEPENDENCY_PORT: "3306"
82
+ - image: circleci/mysql:<< parameters.mysql_version >>
153
83
  environment:
154
- - MYSQL_ALLOW_EMPTY_PASSWORD=true
155
- - MYSQL_USER=root
156
- - MYSQL_PASSWORD=
157
- - MYSQL_DATABASE=statesman_test
84
+ MYSQL_ROOT_PASSWORD: password
85
+ MYSQL_USER: foobar
86
+ MYSQL_PASSWORD: password
87
+ MYSQL_DATABASE: statesman_test
158
88
  steps: *steps
159
- build-ruby270-rails-master-postgres:
89
+
90
+ rspec_postgres:
91
+ working_directory: /mnt/ramdisk
92
+ parameters:
93
+ ruby_version:
94
+ type: string
95
+ rails_version:
96
+ type: string
97
+ psql_version:
98
+ type: string
160
99
  docker:
161
- - image: circleci/ruby:2.7.0-node
100
+ - image: cimg/ruby:<< parameters.ruby_version >>
162
101
  environment:
163
- - RAILS_VERSION=master
164
- - DATABASE_URL=postgres://postgres@localhost/statesman_test
165
- - EXCLUDE_MONGOID=true
166
- - DATABASE_DEPENDENCY_PORT=5432
167
- - image: circleci/postgres:9.6
102
+ CIRCLE_TEST_REPORTS: /tmp/circle_artifacts/
103
+ DATABASE_URL: postgres://postgres@localhost/statesman_test
104
+ DATABASE_DEPENDENCY_PORT: "5432"
105
+ - image: circleci/postgres:<< parameters.psql_version >>
168
106
  environment:
169
- - POSTGRES_USER=postgres
170
- - POSTGRES_DB=statesman_test
171
- - POSTGRES_PASSWORD=statesman
107
+ POSTGRES_USER: postgres
108
+ POSTGRES_DB: statesman_test
109
+ POSTGRES_PASSWORD: statesman
172
110
  steps: *steps
173
111
 
174
112
  workflows:
175
113
  version: 2
176
114
  tests:
177
115
  jobs:
178
- - build-ruby249-rails-524-mysql
179
- - build-ruby249-rails-524-postgres
180
- - build-ruby265-rails-602-mysql
181
- - build-ruby265-rails-602-postgres
182
- - build-ruby265-rails-master-mysql
183
- - build-ruby265-rails-master-postgres
184
- - build-ruby270-rails-602-mysql
185
- - build-ruby270-rails-602-postgres
186
- - build-ruby270-rails-master-mysql
187
- - build-ruby270-rails-master-postgres
116
+ - rspec_mysql:
117
+ matrix:
118
+ parameters:
119
+ mysql_version: *mysql_versions
120
+ ruby_version: *ruby_versions
121
+ rails_version: *rails_versions
122
+ - rspec_postgres:
123
+ matrix:
124
+ parameters:
125
+ psql_version: *psql_versions
126
+ ruby_version: *ruby_versions
127
+ rails_version: *rails_versions
@@ -0,0 +1,7 @@
1
+ version: 2
2
+
3
+ updates:
4
+ - package-ecosystem: bundler
5
+ directory: "/"
6
+ schedule:
7
+ interval: "daily"
data/.rubocop.yml CHANGED
@@ -4,4 +4,4 @@ inherit_gem:
4
4
  gc_ruboconfig: rubocop.yml
5
5
 
6
6
  AllCops:
7
- TargetRubyVersion: 2.4
7
+ TargetRubyVersion: 3.0
data/.rubocop_todo.yml CHANGED
@@ -1,38 +1,42 @@
1
1
  # This configuration was generated by
2
2
  # `rubocop --auto-gen-config`
3
- # on 2019-08-17 15:19:58 +0100 using RuboCop version 0.61.1.
3
+ # on 2021-08-09 15:32:40 UTC using RuboCop version 1.18.4.
4
4
  # The point is for the user to remove these configuration records
5
5
  # one by one as the offenses are removed from the code base.
6
6
  # Note that changes in the inspected code, or installation of new
7
7
  # versions of RuboCop, may require this file to be generated again.
8
8
 
9
+ # Offense count: 1
10
+ # Configuration parameters: Include.
11
+ # Include: **/*.gemspec
9
12
  Gemspec/RequiredRubyVersion:
10
- Enabled: false # We want to allow Ruby 2.2 even though we don't test on it
13
+ Exclude:
14
+ - 'statesman.gemspec'
15
+
16
+ # Offense count: 1
17
+ Lint/MissingSuper:
18
+ Exclude:
19
+ - 'lib/statesman/adapters/active_record_queries.rb'
11
20
 
12
21
  # Offense count: 5
22
+ # Configuration parameters: IgnoredMethods, CountRepeatedAttributes.
13
23
  Metrics/AbcSize:
14
- Max: 18
24
+ Max: 20
25
+
26
+ # Offense count: 1
27
+ # Configuration parameters: IgnoredMethods.
28
+ Metrics/CyclomaticComplexity:
29
+ Max: 8
15
30
 
16
- # Offense count: 4
17
- # Configuration parameters: CountComments, ExcludedMethods.
31
+ # Offense count: 3
32
+ # Configuration parameters: CountComments, CountAsOne, ExcludedMethods, IgnoredMethods.
18
33
  Metrics/MethodLength:
19
34
  Max: 14
20
35
 
21
- # Offense count: 2
22
- # Cop supports --auto-correct.
23
- # Configuration parameters: SkipBlocks, EnforcedStyle.
24
- # SupportedStyles: described_class, explicit
25
- RSpec/DescribedClass:
26
- Exclude:
27
- - 'spec/statesman/adapters/active_record_queries_spec.rb'
28
-
29
- # Offense count: 7
30
- # Configuration parameters: Max.
36
+ # Offense count: 11
37
+ # Configuration parameters: CountAsOne.
31
38
  RSpec/ExampleLength:
32
- Exclude:
33
- - 'spec/statesman/adapters/active_record_spec.rb'
34
- - 'spec/statesman/adapters/shared_examples.rb'
35
- - 'spec/statesman/machine_spec.rb'
39
+ Max: 14
36
40
 
37
41
  # Offense count: 7
38
42
  RSpec/ExpectInHook:
@@ -40,11 +44,12 @@ RSpec/ExpectInHook:
40
44
  - 'spec/statesman/adapters/active_record_spec.rb'
41
45
  - 'spec/statesman/machine_spec.rb'
42
46
 
43
- # Offense count: 3
44
- RSpec/ImplicitBlockExpectation:
47
+ # Offense count: 1
48
+ # Configuration parameters: Include, CustomTransform, IgnoreMethods, SpecSuffixOnly.
49
+ # Include: **/*_spec*rb*, **/spec/**/*
50
+ RSpec/FilePath:
45
51
  Exclude:
46
- - 'spec/statesman/adapters/active_record_spec.rb'
47
- - 'spec/statesman/adapters/shared_examples.rb'
52
+ - 'spec/statesman/exceptions_spec.rb'
48
53
 
49
54
  # Offense count: 1
50
55
  # Configuration parameters: AssignmentOnly.
@@ -75,23 +80,27 @@ RSpec/MessageSpies:
75
80
  Exclude:
76
81
  - 'spec/statesman/callback_spec.rb'
77
82
 
78
- # Offense count: 9
79
- # Configuration parameters: AggregateFailuresByDefault.
83
+ # Offense count: 14
80
84
  RSpec/MultipleExpectations:
81
85
  Max: 3
82
86
 
83
- # Offense count: 50
87
+ # Offense count: 49
84
88
  RSpec/NestedGroups:
85
89
  Max: 6
86
90
 
87
- # Offense count: 16
91
+ # Offense count: 2
92
+ RSpec/RepeatedExampleGroupBody:
93
+ Exclude:
94
+ - 'spec/statesman/exceptions_spec.rb'
95
+
96
+ # Offense count: 12
88
97
  RSpec/ScatteredSetup:
89
98
  Exclude:
90
99
  - 'spec/statesman/adapters/active_record_spec.rb'
91
100
  - 'spec/statesman/adapters/shared_examples.rb'
92
101
  - 'spec/statesman/machine_spec.rb'
93
102
 
94
- # Offense count: 8
103
+ # Offense count: 7
95
104
  # Configuration parameters: IgnoreNameless, IgnoreSymbolicNames.
96
105
  RSpec/VerifiedDoubles:
97
106
  Exclude:
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.0.2
data/CHANGELOG.md CHANGED
@@ -1,3 +1,32 @@
1
+ ## v9.0.0 9th August 2021
2
+
3
+ ### Added
4
+ - Added Ruby 3.0 support
5
+
6
+ ### Breaking changes
7
+
8
+ - Removed Ruby 2.4
9
+
10
+ ## v8.0.3 8th June 2021
11
+
12
+ ### Added
13
+ - Implement `Machine#last_transition_to`, to find the last transition to a given state
14
+ [#438](https://github.com/gocardless/statesman/pull/438)
15
+
16
+ ## v8.0.2 30th March 2021
17
+
18
+ ### Changed
19
+
20
+ - Fixed a bug where the `history` of a model was left in an incorrect state after a transition
21
+ conflict [#433](https://github.com/gocardless/statesman/pull/433)
22
+
23
+ ## v8.0.1 20th January 2021
24
+
25
+ ### Changed
26
+
27
+ - Fixed `no implicit conversion of nil into String` error when quoting null values
28
+ [#427](https://github.com/gocardless/statesman/pull/427)
29
+
1
30
  ## v8.0.0 6th January 2021
2
31
 
3
32
  ### Added
@@ -24,16 +53,16 @@
24
53
 
25
54
  ### Changed
26
55
 
27
- - Use correct Arel for null [#409](https://github.com/gocardless/statesman/pull/#409)
56
+ - Use correct Arel for null [#409](https://github.com/gocardless/statesman/pull/409)
28
57
 
29
58
  ## v7.2.0, 19th May 2020
30
59
 
31
60
  ### Changed
32
61
 
33
- - Set non-empty password for postgres tests [#398](https://github.com/gocardless/statesman/pull/#398)
34
- - Handle transitions differently for MySQL [#399](https://github.com/gocardless/statesman/pull/#399)
35
- - pg requirement from >= 0.18, <= 1.1 to >= 0.18, <= 1.3 [#400](https://github.com/gocardless/statesman/pull/#400)
36
- - Lazily enable mysql gaplock protection [#402](https://github.com/gocardless/statesman/pull/#402)
62
+ - Set non-empty password for postgres tests [#398](https://github.com/gocardless/statesman/pull/398)
63
+ - Handle transitions differently for MySQL [#399](https://github.com/gocardless/statesman/pull/399)
64
+ - pg requirement from >= 0.18, <= 1.1 to >= 0.18, <= 1.3 [#400](https://github.com/gocardless/statesman/pull/400)
65
+ - Lazily enable mysql gaplock protection [#402](https://github.com/gocardless/statesman/pull/402)
37
66
 
38
67
  ## v7.1.0, 10th Feb 2020
39
68
 
@@ -86,7 +115,7 @@
86
115
  to
87
116
  ```ruby
88
117
  include Statesman::Adapters::ActiveRecordQueries[
89
- initial_state: :inital,
118
+ initial_state: :initial,
90
119
  transition_class: MyTransition
91
120
  ]
92
121
  ```
data/CONTRIBUTING.md CHANGED
@@ -19,3 +19,21 @@ request passes by running `rubocop`.
19
19
 
20
20
  Please add a section to the readme for any new feature additions or behaviour
21
21
  changes.
22
+
23
+ ## Releasing
24
+
25
+ We publish new versions of Stateman using [RubyGems](https://guides.rubygems.org/publishing/). Once
26
+ the relevant changes have been merged and `VERSION` has been appropriately bumped to the new
27
+ version, we run the following command.
28
+ ```
29
+ $ gem build statesman.gemspec
30
+ ```
31
+ This builds a `.gem` file locally that will be named something like `statesman-X` where `X` is the
32
+ new version. For example, if we are releasing version 9.0.0, the file would be
33
+ `statesman-9.0.0.gem`.
34
+
35
+ To publish, run `gem push` with the new `.gem` file we just generated. This requires a OTP that is currently only available
36
+ to GoCardless engineers. For example, if we were to continue to publish version 9.0.0, we would run:
37
+ ```
38
+ $ gem push statesman-9.0.0.gem
39
+ ```
data/Gemfile CHANGED
@@ -4,14 +4,11 @@ source 'https://rubygems.org'
4
4
 
5
5
  gemspec
6
6
 
7
- # rubocop:disable Bundler/DuplicatedGem
8
- if ENV['RAILS_VERSION'] == 'master'
9
- gem "rails", git: "https://github.com/rails/rails"
7
+ if ENV['RAILS_VERSION'] == 'main'
8
+ gem "rails", git: "https://github.com/rails/rails", branch: "main"
10
9
  elsif ENV['RAILS_VERSION']
11
10
  gem "rails", "~> #{ENV['RAILS_VERSION']}"
12
11
  end
13
- # rubocop:enable Bundler/DuplicatedGem
14
-
15
12
  group :development do
16
13
  # test/unit is no longer bundled with Ruby 2.2, but required by Rails
17
14
  gem "test-unit", "~> 3.3" if Gem::Version.new(RUBY_VERSION) >= Gem::Version.new("2.2.0")
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- <p align="center"><img src="http://f.cl.ly/items/410n2A0S3l1W0i3i0o2K/statesman.png" alt="Statesman"></p>
1
+ <p align="center"><img src="https://user-images.githubusercontent.com/110275/106792848-96e4ee80-664e-11eb-8fd1-16ff24b41eb2.png" alt="Statesman" width="512"></p>
2
2
 
3
3
  A statesmanlike state machine library.
4
4
 
@@ -30,7 +30,7 @@ protection.
30
30
  To get started, just add Statesman to your `Gemfile`, and then run `bundle`:
31
31
 
32
32
  ```ruby
33
- gem 'statesman', '~> 7.1.0'
33
+ gem 'statesman', '~> 8.0.3'
34
34
  ```
35
35
 
36
36
  ## Usage
@@ -109,6 +109,8 @@ Order.first.state_machine.allowed_transitions # => ["checking_out", "cancelled"]
109
109
  Order.first.state_machine.can_transition_to?(:cancelled) # => true/false
110
110
  Order.first.state_machine.transition_to(:cancelled, optional: :metadata) # => true/false
111
111
  Order.first.state_machine.transition_to!(:cancelled) # => true/exception
112
+ Order.first.state_machine.last_transition # => transition model or nil
113
+ Order.first.state_machine.last_transition_to(:pending) # => transition model or nil
112
114
 
113
115
  Order.in_state(:cancelled) # => [#<Order id: "123">]
114
116
  Order.not_in_state(:checking_out) # => [#<Order id: "123">]
@@ -159,7 +161,8 @@ class Order < ActiveRecord::Base
159
161
 
160
162
  # Optionally delegate some methods
161
163
 
162
- delegate :can_transition_to?, :current_state, :history, :last_transition,
164
+ delegate :can_transition_to?,
165
+ :current_state, :history, :last_transition, :last_transition_to,
163
166
  :transition_to!, :transition_to, :in_state?, to: :state_machine
164
167
  end
165
168
  ```
@@ -322,6 +325,10 @@ Machine.successors
322
325
  #### `Machine#current_state`
323
326
  Returns the current state based on existing transition objects.
324
327
 
328
+ Takes an optional keyword argument to force a reload of data from the
329
+ database.
330
+ e.g `current_state(force_reload: true)`
331
+
325
332
  #### `Machine#in_state?(:state_1, :state_2, ...)`
326
333
  Returns true if the machine is in any of the given states.
327
334
 
@@ -331,6 +338,9 @@ Returns a sorted array of all transition objects.
331
338
  #### `Machine#last_transition`
332
339
  Returns the most recent transition object.
333
340
 
341
+ #### `Machine#last_transition_to(:state)`
342
+ Returns the most recent transition object to a given state.
343
+
334
344
  #### `Machine#allowed_transitions`
335
345
  Returns an array of states you can `transition_to` from current state.
336
346
 
@@ -347,6 +357,66 @@ Transition to the passed state, returning `true` on success. Swallows all
347
357
  Statesman exceptions and returns false on failure. (NB. if your guard or
348
358
  callback code throws an exception, it will not be caught.)
349
359
 
360
+
361
+ ## Errors
362
+
363
+ ### Initialization errors
364
+ These errors are raised when the Machine and/or Model is initialized. A simple spec like
365
+ ```ruby
366
+ expect { OrderStateMachine.new(Order.new, transition_class: OrderTransition) }.to_not raise_error
367
+ ```
368
+ will expose these errors as part of your test suite
369
+
370
+ #### InvalidStateError
371
+ Raised if:
372
+ * Attempting to define a transition without a `to` state.
373
+ * Attempting to define a transition with a non-existent state.
374
+ * Attempting to define multiple states as `initial`.
375
+
376
+ #### InvalidTransitionError
377
+ Raised if:
378
+ * Attempting to define a callback `from` a state that has no valid transitions (A terminal state).
379
+ * Attempting to define a callback `to` the `initial` state if that state has no transitions to it.
380
+ * Attempting to define a callback with `from` and `to` where any of the pairs have no transition between them.
381
+
382
+ #### InvalidCallbackError
383
+ Raised if:
384
+ * Attempting to define a callback without a block.
385
+
386
+ #### UnserializedMetadataError
387
+ Raised if:
388
+ * ActiveRecord is configured to not serialize the `metadata` attribute into
389
+ to Database column backing it. See the `Using PostgreSQL JSON column` section.
390
+
391
+ #### IncompatibleSerializationError
392
+ Raised if:
393
+ * There is a mismatch between the column type of the `metadata` in the
394
+ Database and the model. See the `Using PostgreSQL JSON column` section.
395
+
396
+ #### MissingTransitionAssociation
397
+ Raised if:
398
+ * The model that `Statesman::Adapters::ActiveRecordQueries` is included in
399
+ does not have a `has_many` association to the `transition_class`.
400
+
401
+ ### Runtime errors
402
+ These errors are raised by `transition_to!`. Using `transition_to` will
403
+ supress `GuardFailedError` and `TransitionFailedError` and return `false` instead.
404
+
405
+ #### GuardFailedError
406
+ Raised if:
407
+ * A guard callback between `from` and `to` state returned a falsey value.
408
+
409
+ #### TransitionFailedError
410
+ Raised if:
411
+ * A transition is attempted but `current_state -> new_state` is not a valid pair.
412
+
413
+ #### TransitionConflictError
414
+ Raised if:
415
+ * A database conflict affecting the `sort_key` or `most_recent` columns occurs
416
+ when attempting a transition.
417
+ Retried automatically if it occurs wrapped in `retry_conflicts`.
418
+
419
+
350
420
  ## Model scopes
351
421
 
352
422
  A mixin is provided for the ActiveRecord adapter which adds scopes to easily
@@ -42,7 +42,13 @@ module Statesman
42
42
  def create(from, to, metadata = {})
43
43
  create_transition(from.to_s, to.to_s, metadata)
44
44
  rescue ::ActiveRecord::RecordNotUnique => e
45
- raise TransitionConflictError, e.message if transition_conflict_error? e
45
+ if transition_conflict_error? e
46
+ # The history has the invalid transition on the end of it, which means
47
+ # `current_state` would then be incorrect. We force a reload of the history to
48
+ # avoid this.
49
+ transitions_for_parent.reload
50
+ raise TransitionConflictError, e.message
51
+ end
46
52
 
47
53
  raise
48
54
  ensure
@@ -59,6 +65,7 @@ module Statesman
59
65
  end
60
66
  end
61
67
 
68
+ # rubocop:disable Naming/MemoizedInstanceVariableName
62
69
  def last(force_reload: false)
63
70
  if force_reload
64
71
  @last_transition = history(force_reload: true).last
@@ -66,6 +73,7 @@ module Statesman
66
73
  @last_transition ||= history.last
67
74
  end
68
75
  end
76
+ # rubocop:enable Naming/MemoizedInstanceVariableName
69
77
 
70
78
  def reset
71
79
  @last_transition = nil
@@ -313,7 +321,7 @@ module Statesman
313
321
  end
314
322
 
315
323
  def db_null
316
- type_cast(nil)
324
+ Arel::Nodes::SqlLiteral.new("NULL")
317
325
  end
318
326
 
319
327
  # Type casting against a column is deprecated and will be removed in Rails 6.2.
@@ -2,9 +2,13 @@
2
2
 
3
3
  module Statesman
4
4
  class InvalidStateError < StandardError; end
5
+
5
6
  class InvalidTransitionError < StandardError; end
7
+
6
8
  class InvalidCallbackError < StandardError; end
9
+
7
10
  class TransitionConflictError < StandardError; end
11
+
8
12
  class MissingTransitionAssociation < StandardError; end
9
13
 
10
14
  class TransitionFailedError < StandardError
@@ -209,6 +209,10 @@ module Statesman
209
209
  @storage_adapter.last(force_reload: force_reload)
210
210
  end
211
211
 
212
+ def last_transition_to(state)
213
+ history.reverse.find { |transition| transition.to_state.to_sym == state.to_sym }
214
+ end
215
+
212
216
  def can_transition_to?(new_state, metadata = {})
213
217
  validate_transition(from: current_state,
214
218
  to: new_state,
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Statesman
4
- VERSION = "8.0.0"
4
+ VERSION = "9.0.0"
5
5
  end
@@ -50,10 +50,11 @@ describe Statesman::Adapters::ActiveRecordQueries, active_record: true do
50
50
 
51
51
  shared_examples "testing methods" do
52
52
  before do
53
- if config_type == :old
53
+ case config_type
54
+ when :old
54
55
  configure_old(MyActiveRecordModel, MyActiveRecordModelTransition)
55
56
  configure_old(OtherActiveRecordModel, OtherActiveRecordModelTransition)
56
- elsif config_type == :new
57
+ when :new
57
58
  configure_new(MyActiveRecordModel, MyActiveRecordModelTransition)
58
59
  configure_new(OtherActiveRecordModel, OtherActiveRecordModelTransition)
59
60
  else
@@ -201,7 +202,6 @@ describe Statesman::Adapters::ActiveRecordQueries, active_record: true do
201
202
  MyActiveRecordModel.create
202
203
  end
203
204
 
204
- # rubocop:disable RSpec/ExampleLength
205
205
  it do
206
206
  expect do
207
207
  ActiveRecord::Base.transaction do
@@ -210,7 +210,6 @@ describe Statesman::Adapters::ActiveRecordQueries, active_record: true do
210
210
  end
211
211
  end.to_not change(MyStateMachine, :after_commit_callback_executed)
212
212
  end
213
- # rubocop:enable RSpec/ExampleLength
214
213
  end
215
214
  end
216
215
 
@@ -130,6 +130,31 @@ describe Statesman::Adapters::ActiveRecord, active_record: true do
130
130
  expect { adapter.create(:y, :z) }.
131
131
  to raise_exception(Statesman::TransitionConflictError)
132
132
  end
133
+
134
+ it "does not pollute the state when the transition fails" do
135
+ # this increments the sort_key in the database
136
+ adapter.create(:x, :y)
137
+
138
+ # we then pre-load the transitions for efficiency
139
+ preloaded_model = MyActiveRecordModel.
140
+ includes(:my_active_record_model_transitions).
141
+ find(model.id)
142
+
143
+ adapter2 = described_class.
144
+ new(MyActiveRecordModelTransition, preloaded_model, observer)
145
+
146
+ # Now we generate a race
147
+ adapter.create(:y, :z)
148
+ expect { adapter2.create(:y, :a) }.
149
+ to raise_error(Statesman::TransitionConflictError)
150
+
151
+ # The preloaded adapter should discard the preloaded info
152
+ expect(adapter2.last).to have_attributes(to_state: "z")
153
+ expect(adapter2.history).to contain_exactly(
154
+ have_attributes(to_state: "y"),
155
+ have_attributes(to_state: "z"),
156
+ )
157
+ end
133
158
  end
134
159
 
135
160
  context "when other exceptions occur" do
@@ -128,6 +128,7 @@ shared_examples_for "an adapter" do |adapter_class, transition_class, options =
128
128
 
129
129
  it { is_expected.to be_a(transition_class) }
130
130
  specify { expect(adapter.last.to_state.to_sym).to eq(:z) }
131
+
131
132
  specify do
132
133
  expect(adapter.last(force_reload: true).to_state.to_sym).to eq(:z)
133
134
  end
@@ -7,6 +7,7 @@ describe Statesman do
7
7
  subject(:error) { Statesman::InvalidStateError.new }
8
8
 
9
9
  its(:message) { is_expected.to eq("Statesman::InvalidStateError") }
10
+
10
11
  its "string matches its message" do
11
12
  expect(error.to_s).to eq(error.message)
12
13
  end
@@ -16,6 +17,7 @@ describe Statesman do
16
17
  subject(:error) { Statesman::InvalidTransitionError.new }
17
18
 
18
19
  its(:message) { is_expected.to eq("Statesman::InvalidTransitionError") }
20
+
19
21
  its "string matches its message" do
20
22
  expect(error.to_s).to eq(error.message)
21
23
  end
@@ -25,6 +27,7 @@ describe Statesman do
25
27
  subject(:error) { Statesman::InvalidTransitionError.new }
26
28
 
27
29
  its(:message) { is_expected.to eq("Statesman::InvalidTransitionError") }
30
+
28
31
  its "string matches its message" do
29
32
  expect(error.to_s).to eq(error.message)
30
33
  end
@@ -34,6 +37,7 @@ describe Statesman do
34
37
  subject(:error) { Statesman::TransitionConflictError.new }
35
38
 
36
39
  its(:message) { is_expected.to eq("Statesman::TransitionConflictError") }
40
+
37
41
  its "string matches its message" do
38
42
  expect(error.to_s).to eq(error.message)
39
43
  end
@@ -43,6 +47,7 @@ describe Statesman do
43
47
  subject(:error) { Statesman::MissingTransitionAssociation.new }
44
48
 
45
49
  its(:message) { is_expected.to eq("Statesman::MissingTransitionAssociation") }
50
+
46
51
  its "string matches its message" do
47
52
  expect(error.to_s).to eq(error.message)
48
53
  end
@@ -52,6 +57,7 @@ describe Statesman do
52
57
  subject(:error) { Statesman::TransitionFailedError.new("from", "to") }
53
58
 
54
59
  its(:message) { is_expected.to eq("Cannot transition from 'from' to 'to'") }
60
+
55
61
  its "string matches its message" do
56
62
  expect(error.to_s).to eq(error.message)
57
63
  end
@@ -63,6 +69,7 @@ describe Statesman do
63
69
  its(:message) do
64
70
  is_expected.to eq("Guard on transition from: 'from' to 'to' returned false")
65
71
  end
72
+
66
73
  its "string matches its message" do
67
74
  expect(error.to_s).to eq(error.message)
68
75
  end
@@ -72,6 +79,7 @@ describe Statesman do
72
79
  subject(:error) { Statesman::UnserializedMetadataError.new("foo") }
73
80
 
74
81
  its(:message) { is_expected.to match(/foo#metadata is not serialized/) }
82
+
75
83
  its "string matches its message" do
76
84
  expect(error.to_s).to eq(error.message)
77
85
  end
@@ -81,6 +89,7 @@ describe Statesman do
81
89
  subject(:error) { Statesman::IncompatibleSerializationError.new("foo") }
82
90
 
83
91
  its(:message) { is_expected.to match(/foo#metadata column type cannot be json/) }
92
+
84
93
  its "string matches its message" do
85
94
  expect(error.to_s).to eq(error.message)
86
95
  end
@@ -234,11 +234,9 @@ describe Statesman::Machine do
234
234
 
235
235
  it "does not add a callback" do
236
236
  expect do
237
- begin
238
- set_callback
239
- rescue error_type
240
- nil
241
- end
237
+ set_callback
238
+ rescue error_type
239
+ nil
242
240
  end.to_not change(machine.callbacks[callback_store], :count)
243
241
  end
244
242
  end
@@ -537,6 +535,34 @@ describe Statesman::Machine do
537
535
  end
538
536
  end
539
537
 
538
+ describe "#last_transition_to" do
539
+ subject { instance.last_transition_to(:y) }
540
+
541
+ before do
542
+ machine.class_eval do
543
+ state :x, initial: true
544
+ state :y
545
+ state :z
546
+ transition from: :x, to: :y
547
+ transition from: :y, to: :z
548
+ transition from: :z, to: :y
549
+ end
550
+
551
+ instance.transition_to!(:y)
552
+ instance.transition_to!(:z)
553
+ end
554
+
555
+ let(:instance) { machine.new(my_model) }
556
+
557
+ it { is_expected.to have_attributes(to_state: "y") }
558
+
559
+ context "when there are 2 transitions to the state" do
560
+ before { instance.transition_to!(:y) }
561
+
562
+ it { is_expected.to eq(instance.last_transition) }
563
+ end
564
+ end
565
+
540
566
  describe "#can_transition_to?" do
541
567
  subject(:can_transition_to?) { instance.can_transition_to?(new_state, metadata) }
542
568
 
@@ -64,7 +64,7 @@ class CreateMyActiveRecordModelMigration < MIGRATION_CLASS
64
64
  end
65
65
 
66
66
  # TODO: make this a module we can extend from the app? Or a generator?
67
- # rubocop:disable MethodLength, Metrics/AbcSize
67
+ # rubocop:disable Metrics/MethodLength
68
68
  class CreateMyActiveRecordModelTransitionMigration < MIGRATION_CLASS
69
69
  def change
70
70
  create_table :my_active_record_model_transitions do |t|
@@ -110,7 +110,7 @@ class CreateMyActiveRecordModelTransitionMigration < MIGRATION_CLASS
110
110
  end
111
111
  end
112
112
  end
113
- # rubocop:enable MethodLength, Metrics/AbcSize
113
+ # rubocop:enable Metrics/MethodLength
114
114
 
115
115
  class OtherActiveRecordModel < ActiveRecord::Base
116
116
  has_many :other_active_record_model_transitions, autosave: false
@@ -144,7 +144,7 @@ class CreateOtherActiveRecordModelMigration < MIGRATION_CLASS
144
144
  end
145
145
  end
146
146
 
147
- # rubocop:disable MethodLength
147
+ # rubocop:disable Metrics/MethodLength
148
148
  class CreateOtherActiveRecordModelTransitionMigration < MIGRATION_CLASS
149
149
  def change
150
150
  create_table :other_active_record_model_transitions do |t|
@@ -188,7 +188,7 @@ class CreateOtherActiveRecordModelTransitionMigration < MIGRATION_CLASS
188
188
  end
189
189
  end
190
190
  end
191
- # rubocop:enable MethodLength
191
+ # rubocop:enable Metrics/MethodLength
192
192
 
193
193
  class DropMostRecentColumn < MIGRATION_CLASS
194
194
  def change
@@ -242,7 +242,7 @@ class CreateNamespacedARModelMigration < MIGRATION_CLASS
242
242
  end
243
243
  end
244
244
 
245
- # rubocop:disable MethodLength
245
+ # rubocop:disable Metrics/MethodLength
246
246
  class CreateNamespacedARModelTransitionMigration < MIGRATION_CLASS
247
247
  def change
248
248
  create_table :my_namespace_my_active_record_model_transitions do |t|
@@ -282,5 +282,5 @@ class CreateNamespacedARModelTransitionMigration < MIGRATION_CLASS
282
282
  name: "index_namespace_model_transitions_parent_latest"
283
283
  end
284
284
  end
285
- # rubocop:enable MethodLength
285
+ # rubocop:enable Metrics/MethodLength
286
286
  end
data/statesman.gemspec CHANGED
@@ -21,11 +21,11 @@ Gem::Specification.new do |spec|
21
21
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
22
22
  spec.require_paths = ["lib"]
23
23
 
24
- spec.required_ruby_version = ">= 2.2"
24
+ spec.required_ruby_version = ">= 2.5"
25
25
 
26
26
  spec.add_development_dependency "ammeter", "~> 1.1"
27
- spec.add_development_dependency "bundler", "~> 2.1.4"
28
- spec.add_development_dependency "gc_ruboconfig", "~> 2.3.9"
27
+ spec.add_development_dependency "bundler", "~> 2"
28
+ spec.add_development_dependency "gc_ruboconfig", "~> 2.26.0"
29
29
  spec.add_development_dependency "mysql2", ">= 0.4", "< 0.6"
30
30
  spec.add_development_dependency "pg", ">= 0.18", "<= 1.3"
31
31
  spec.add_development_dependency "pry"
@@ -33,8 +33,8 @@ Gem::Specification.new do |spec|
33
33
  spec.add_development_dependency "rake", "~> 13.0.0"
34
34
  spec.add_development_dependency "rspec", "~> 3.1"
35
35
  spec.add_development_dependency "rspec-its", "~> 1.1"
36
- spec.add_development_dependency "rspec-rails", "~> 3.1"
37
36
  spec.add_development_dependency "rspec_junit_formatter", "~> 0.4.0"
37
+ spec.add_development_dependency "rspec-rails", "~> 3.1"
38
38
  spec.add_development_dependency "sqlite3", "~> 1.4.2"
39
39
  spec.add_development_dependency "timecop", "~> 0.9.1"
40
40
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: statesman
3
3
  version: !ruby/object:Gem::Version
4
- version: 8.0.0
4
+ version: 9.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - GoCardless
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-01-06 00:00:00.000000000 Z
11
+ date: 2021-08-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: ammeter
@@ -30,28 +30,28 @@ dependencies:
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: 2.1.4
33
+ version: '2'
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: 2.1.4
40
+ version: '2'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: gc_ruboconfig
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: 2.3.9
47
+ version: 2.26.0
48
48
  type: :development
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
52
  - - "~>"
53
53
  - !ruby/object:Gem::Version
54
- version: 2.3.9
54
+ version: 2.26.0
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: mysql2
57
57
  requirement: !ruby/object:Gem::Requirement
@@ -163,33 +163,33 @@ dependencies:
163
163
  - !ruby/object:Gem::Version
164
164
  version: '1.1'
165
165
  - !ruby/object:Gem::Dependency
166
- name: rspec-rails
166
+ name: rspec_junit_formatter
167
167
  requirement: !ruby/object:Gem::Requirement
168
168
  requirements:
169
169
  - - "~>"
170
170
  - !ruby/object:Gem::Version
171
- version: '3.1'
171
+ version: 0.4.0
172
172
  type: :development
173
173
  prerelease: false
174
174
  version_requirements: !ruby/object:Gem::Requirement
175
175
  requirements:
176
176
  - - "~>"
177
177
  - !ruby/object:Gem::Version
178
- version: '3.1'
178
+ version: 0.4.0
179
179
  - !ruby/object:Gem::Dependency
180
- name: rspec_junit_formatter
180
+ name: rspec-rails
181
181
  requirement: !ruby/object:Gem::Requirement
182
182
  requirements:
183
183
  - - "~>"
184
184
  - !ruby/object:Gem::Version
185
- version: 0.4.0
185
+ version: '3.1'
186
186
  type: :development
187
187
  prerelease: false
188
188
  version_requirements: !ruby/object:Gem::Requirement
189
189
  requirements:
190
190
  - - "~>"
191
191
  - !ruby/object:Gem::Version
192
- version: 0.4.0
192
+ version: '3.1'
193
193
  - !ruby/object:Gem::Dependency
194
194
  name: sqlite3
195
195
  requirement: !ruby/object:Gem::Requirement
@@ -226,9 +226,11 @@ extensions: []
226
226
  extra_rdoc_files: []
227
227
  files:
228
228
  - ".circleci/config.yml"
229
+ - ".github/dependabot.yml"
229
230
  - ".gitignore"
230
231
  - ".rubocop.yml"
231
232
  - ".rubocop_todo.yml"
233
+ - ".ruby-version"
232
234
  - CHANGELOG.md
233
235
  - CONTRIBUTING.md
234
236
  - Gemfile
@@ -296,14 +298,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
296
298
  requirements:
297
299
  - - ">="
298
300
  - !ruby/object:Gem::Version
299
- version: '2.2'
301
+ version: '2.5'
300
302
  required_rubygems_version: !ruby/object:Gem::Requirement
301
303
  requirements:
302
304
  - - ">="
303
305
  - !ruby/object:Gem::Version
304
306
  version: '0'
305
307
  requirements: []
306
- rubygems_version: 3.1.2
308
+ rubygems_version: 3.2.22
307
309
  signing_key:
308
310
  specification_version: 4
309
311
  summary: A statesman-like state machine library