statesman 8.0.3 → 10.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3d099ca7f60641163125a158ca189d41b571a46e4da8d5277a9391ff12df69e4
4
- data.tar.gz: 5a33e698ded14cbc080f6d3e385d4d00d8191b5d1887e0b4fb062d592a35214f
3
+ metadata.gz: 172b3bbeecd2cbd2df8fc9a5485c548c0d729cf0994fda1771d25c8e44f41877
4
+ data.tar.gz: 3556cc13e2433e920ce9bd275bce155a095f6a0e64547a5ff003e0f5fa4dfc74
5
5
  SHA512:
6
- metadata.gz: 1b7e63cd44278ca20c9aac95b6ff1372b83131d5df988b72d6fabfbbc98057917f33bc6cdbc447c2d1d04f075ac215e58eb5e0beb09d0fd7d2d16e672cebacd2
7
- data.tar.gz: 63588d5869538bab0f4a7900f2761ed3bcfb124895485d2fc1667ec159763e4fad69327af25d7221f113f6c43396aabb3d8548085d6bb25229487c5d19ba11c0
6
+ metadata.gz: 21f91dd2537c3e5e480dc0f5adb592ebbeaef9974cfbede1d94aa8b6946c26e9394199029e0281372a2ec4452e1173cb5cba86f0bf1507de19677c398df20350
7
+ data.tar.gz: 58ee880508df011be1caea1f85a475760cf79e07c1ad60f241f4ef4146983997560f17fe1d3a93f5856c6e519831d71eae6c4bd8f7e70f6d0d4ce49253f1ac3f
data/.circleci/config.yml CHANGED
@@ -1,157 +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
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
16
14
 
17
- - run: bundle install --path vendor/bundle
18
-
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.7"
50
+ - "3.0"
51
+ - "3.1"
29
52
 
30
- - type: store_test_results
31
- path: /tmp/test-results
53
+ rails_versions: &rails_versions
54
+ - "5.2.7"
55
+ - "6.0.4"
56
+ - "6.1.5"
57
+ - "7.0.2"
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"
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
63
+ psql_versions: &psql_versions
64
+ - "9.6"
89
65
 
90
- build-ruby270-rails-602-mysql:
91
- docker:
92
- - image: circleci/ruby:2.7.0-node
93
- environment:
94
- - RAILS_VERSION=6.0.2
95
- - DATABASE_URL=mysql2://root@127.0.0.1/statesman_test
96
- - DATABASE_DEPENDENCY_PORT=3306
97
- - image: circleci/mysql:5.7.18
98
- environment:
99
- - MYSQL_ALLOW_EMPTY_PASSWORD=true
100
- - MYSQL_USER=root
101
- - MYSQL_PASSWORD=
102
- - MYSQL_DATABASE=statesman_test
103
- steps: *steps
104
- build-ruby270-rails-602-postgres:
105
- docker:
106
- - image: circleci/ruby:2.7.0-node
107
- environment:
108
- - RAILS_VERSION=6.0.2
109
- - DATABASE_URL=postgres://postgres@localhost/statesman_test
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
117
- build-ruby270-rails-main-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
118
76
  docker:
119
- - image: circleci/ruby:2.7.0-node
77
+ - image: cimg/ruby:<< parameters.ruby_version >>
120
78
  environment:
121
- - RAILS_VERSION=main
122
- - DATABASE_URL=mysql2://root@127.0.0.1/statesman_test
123
- - DATABASE_DEPENDENCY_PORT=3306
124
- - 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: cimg/mysql:<< parameters.mysql_version >>
125
83
  environment:
126
- - MYSQL_ALLOW_EMPTY_PASSWORD=true
127
- - MYSQL_USER=root
128
- - MYSQL_PASSWORD=
129
- - MYSQL_DATABASE=statesman_test
84
+ MYSQL_ROOT_PASSWORD: password
85
+ MYSQL_USER: foobar
86
+ MYSQL_PASSWORD: password
87
+ MYSQL_DATABASE: statesman_test
130
88
  steps: *steps
131
- build-ruby270-rails-main-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
132
99
  docker:
133
- - image: circleci/ruby:2.7.0-node
100
+ - image: cimg/ruby:<< parameters.ruby_version >>
134
101
  environment:
135
- - RAILS_VERSION=main
136
- - DATABASE_URL=postgres://postgres@localhost/statesman_test
137
- - EXCLUDE_MONGOID=true
138
- - DATABASE_DEPENDENCY_PORT=5432
139
- - 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 >>
140
106
  environment:
141
- - POSTGRES_USER=postgres
142
- - POSTGRES_DB=statesman_test
143
- - POSTGRES_PASSWORD=statesman
107
+ POSTGRES_USER: postgres
108
+ POSTGRES_DB: statesman_test
109
+ POSTGRES_PASSWORD: statesman
144
110
  steps: *steps
145
111
 
146
112
  workflows:
147
113
  version: 2
148
114
  tests:
149
115
  jobs:
150
- - build-ruby249-rails-524-mysql
151
- - build-ruby249-rails-524-postgres
152
- - build-ruby265-rails-602-mysql
153
- - build-ruby265-rails-602-postgres
154
- - build-ruby270-rails-602-mysql
155
- - build-ruby270-rails-602-postgres
156
- - build-ruby270-rails-main-mysql
157
- - build-ruby270-rails-main-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/.gitignore CHANGED
@@ -1,18 +1,71 @@
1
1
  *.gem
2
2
  *.rbc
3
- .bundle
4
- .config
5
- .rspec
6
- .yardoc
3
+ /.config
4
+ /coverage/
5
+ /InstalledFiles
6
+ /pkg/
7
+ /spec/reports/
8
+ /spec/examples.txt
9
+ /test/tmp/
10
+ /test/version_tmp/
11
+ /tmp/
12
+
13
+ # Used by dotenv library to load environment variables.
14
+ # .env
15
+
16
+ # Ignore Byebug command history file.
17
+ .byebug_history
18
+
19
+ ## Specific to RubyMotion:
20
+ .dat*
21
+ .repl_history
22
+ build/
23
+ *.bridgesupport
24
+ build-iPhoneOS/
25
+ build-iPhoneSimulator/
26
+
27
+ ## Specific to RubyMotion (use of CocoaPods):
28
+ #
29
+ # We recommend against adding the Pods directory to your .gitignore. However
30
+ # you should judge for yourself, the pros and cons are mentioned at:
31
+ # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
32
+ #
33
+ # vendor/Pods/
34
+
35
+ ## Documentation cache and generated files:
36
+ /.yardoc/
37
+ /_yardoc/
38
+ /doc/
39
+ /rdoc/
40
+
41
+ ## Environment normalization:
42
+ /.bundle/
43
+ /vendor/bundle
44
+ /lib/bundler/man/
45
+
46
+ # for a library or gem, you might want to ignore these files since the code is
47
+ # intended to run in multiple environments; otherwise, check them in:
7
48
  Gemfile.lock
8
- InstalledFiles
9
- _yardoc
10
- coverage
11
- doc/
12
- lib/bundler/man
13
- pkg
14
- rdoc
15
- spec/reports
16
- test/tmp
17
- test/version_tmp
18
- tmp
49
+ # .ruby-version
50
+ # .ruby-gemset
51
+
52
+ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
53
+ .rvmrc
54
+
55
+ # Used by RuboCop. Remote config files pulled in from inherit_from directive.
56
+ # .rubocop-https?--*
57
+
58
+ # Project-specific ignores
59
+ .rspec
60
+
61
+ # VSCode
62
+ .vscode
63
+
64
+ # Local History for Visual Studio Code
65
+ .history/
66
+
67
+ # Built Visual Studio Code Extensions
68
+ *.vsix
69
+
70
+ # JetBrains
71
+ .idea
data/.rubocop.yml CHANGED
@@ -4,4 +4,13 @@ inherit_gem:
4
4
  gc_ruboconfig: rubocop.yml
5
5
 
6
6
  AllCops:
7
- TargetRubyVersion: 2.4
7
+ TargetRubyVersion: 3.0
8
+
9
+ Metrics/AbcSize:
10
+ Max: 60
11
+
12
+ Metrics/CyclomaticComplexity:
13
+ Max: 10
14
+
15
+ Metrics/PerceivedComplexity:
16
+ Max: 11
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,8 +1,29 @@
1
- ## UNRELEASED
1
+ ## v10.0.0 17th May 2022
2
+
3
+ ### Changed
4
+ - Added support for Ruby 3.1 [#462](https://github.com/gocardless/statesman/pull/462)
5
+ - Removed support for Ruby 2.5 and 2.6 [#462](https://github.com/gocardless/statesman/pull/462)
6
+ - Added `remove_state` and `remove_transitions` methods to `Statesman::Machine` [#464](https://github.com/gocardless/statesman/pull/464)
7
+
8
+ ## v9.0.1 4th February 2021
9
+
10
+ ### Changed
11
+ - Deprecate `ActiveRecord::Base.default_timezone` in favour of `ActiveRecord.default_timezone` [#446](https://github.com/gocardless/statesman/pull/446)
12
+
13
+ ## v9.0.0 9th August 2021
14
+
15
+ ### Added
16
+ - Added Ruby 3.0 support
17
+
18
+ ### Breaking changes
19
+
20
+ - Removed Ruby 2.4
21
+
22
+ ## v8.0.3 8th June 2021
2
23
 
3
24
  ### Added
4
25
  - Implement `Machine#last_transition_to`, to find the last transition to a given state
5
- [#xxx](https://github.com/gocardless/statesman/pull/xxx)
26
+ [#438](https://github.com/gocardless/statesman/pull/438)
6
27
 
7
28
  ## v8.0.2 30th March 2021
8
29
 
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
7
  if ENV['RAILS_VERSION'] == 'main'
9
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
@@ -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', '~> 8.0.3'
33
+ gem 'statesman', '~> 10.0.0'
34
34
  ```
35
35
 
36
36
  ## Usage
@@ -116,6 +116,52 @@ Order.in_state(:cancelled) # => [#<Order id: "123">]
116
116
  Order.not_in_state(:checking_out) # => [#<Order id: "123">]
117
117
  ```
118
118
 
119
+ If you'd like, you can also define a template for a generic state machine, then alter classes which extend it as required:
120
+
121
+ ```ruby
122
+ module Template
123
+ def define_states
124
+ state :a, initial: true
125
+ state :b
126
+ state :c
127
+ end
128
+
129
+ def define_transitions
130
+ transition from: :a, to: :b
131
+ transition from: :b, to: :c
132
+ transition from: :c, to: :a
133
+ end
134
+ end
135
+
136
+ class Circular
137
+ include Statesman::Machine
138
+ extend Template
139
+
140
+ define_states
141
+ define_transitions
142
+ end
143
+
144
+ class Linear
145
+ include Statesman::Machine
146
+ extend Template
147
+
148
+ define_states
149
+ define_transitions
150
+
151
+ remove_transitions from: :c, to: :a
152
+ end
153
+
154
+ class Shorter
155
+ include Statesman::Machine
156
+ extend Template
157
+
158
+ define_states
159
+ define_transitions
160
+
161
+ remove_state :c
162
+ end
163
+ ```
164
+
119
165
  ## Persistence
120
166
 
121
167
  By default Statesman stores transition history in memory only. It can be
@@ -65,6 +65,7 @@ module Statesman
65
65
  end
66
66
  end
67
67
 
68
+ # rubocop:disable Naming/MemoizedInstanceVariableName
68
69
  def last(force_reload: false)
69
70
  if force_reload
70
71
  @last_transition = history(force_reload: true).last
@@ -72,6 +73,7 @@ module Statesman
72
73
  @last_transition ||= history.last
73
74
  end
74
75
  end
76
+ # rubocop:enable Naming/MemoizedInstanceVariableName
75
77
 
76
78
  def reset
77
79
  @last_transition = nil
@@ -302,10 +304,20 @@ module Statesman
302
304
  return nil if column.nil?
303
305
 
304
306
  [
305
- column, ::ActiveRecord::Base.default_timezone == :utc ? Time.now.utc : Time.now
307
+ column, default_timezone == :utc ? Time.now.utc : Time.now
306
308
  ]
307
309
  end
308
310
 
311
+ def default_timezone
312
+ # Rails 7 deprecates ActiveRecord::Base.default_timezone
313
+ # in favour of ActiveRecord.default_timezone
314
+ if ::ActiveRecord.respond_to?(:default_timezone)
315
+ return ::ActiveRecord.default_timezone
316
+ end
317
+
318
+ ::ActiveRecord::Base.default_timezone
319
+ end
320
+
309
321
  def mysql_gaplock_protection?
310
322
  Statesman.mysql_gaplock_protection?
311
323
  end
@@ -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
@@ -42,6 +42,17 @@ module Statesman
42
42
  states << name
43
43
  end
44
44
 
45
+ def remove_state(state_name)
46
+ state_name = state_name.to_s
47
+
48
+ remove_transitions(from: state_name)
49
+ remove_transitions(to: state_name)
50
+ remove_callbacks(from: state_name)
51
+ remove_callbacks(to: state_name)
52
+
53
+ @states.delete(state_name.to_s)
54
+ end
55
+
45
56
  def successors
46
57
  @successors ||= {}
47
58
  end
@@ -70,6 +81,20 @@ module Statesman
70
81
  successors[from] += to
71
82
  end
72
83
 
84
+ def remove_transitions(from: nil, to: nil)
85
+ raise ArgumentError, "Both from and to can't be nil!" if from.nil? && to.nil?
86
+ return if successors.nil?
87
+
88
+ if from.present?
89
+ @successors[from.to_s].delete(to.to_s) if to.present?
90
+ @successors.delete(from.to_s) if to.nil? || successors[from.to_s].empty?
91
+ elsif to.present?
92
+ @successors.
93
+ transform_values! { |to_states| to_states - [to.to_s] }.
94
+ filter! { |_from_state, to_states| to_states.any? }
95
+ end
96
+ end
97
+
73
98
  def before_transition(options = {}, &block)
74
99
  add_callback(callback_type: :before, callback_class: Callback,
75
100
  from: options[:from], to: options[:to], &block)
@@ -151,6 +176,33 @@ module Statesman
151
176
  callback_class.new(from: from, to: to, callback: block)
152
177
  end
153
178
 
179
+ def remove_callbacks(from: nil, to: nil)
180
+ raise ArgumentError, "Both from and to can't be nil!" if from.nil? && to.nil?
181
+ return if callbacks.nil?
182
+
183
+ @callbacks.transform_values! do |callbacks|
184
+ filter_callbacks(callbacks, from: from, to: to)
185
+ end
186
+ end
187
+
188
+ def filter_callbacks(callbacks, from: nil, to: nil)
189
+ callbacks.filter_map do |callback|
190
+ next if callback.from == from && to.nil?
191
+
192
+ if callback.to.include?(to) && (from.nil? || callback.from == from)
193
+ next if callback.to == [to]
194
+
195
+ callback = Statesman::Callback.new({
196
+ from: callback.from,
197
+ to: callback.to - [to],
198
+ callback: callback.callback,
199
+ })
200
+ end
201
+
202
+ callback
203
+ end
204
+ end
205
+
154
206
  def validate_callback_type_and_class(callback_type, callback_class)
155
207
  raise ArgumentError, "missing keyword: callback_type" if callback_type.nil?
156
208
  raise ArgumentError, "missing keyword: callback_class" if callback_class.nil?
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Statesman
4
- VERSION = "8.0.3"
4
+ VERSION = "10.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
 
@@ -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
@@ -27,6 +27,112 @@ describe Statesman::Machine do
27
27
  end
28
28
  end
29
29
 
30
+ describe ".remove_state" do
31
+ subject(:remove_state) { -> { machine.remove_state(:x) } }
32
+
33
+ before do
34
+ machine.class_eval do
35
+ state :x
36
+ state :y
37
+ state :z
38
+ end
39
+ end
40
+
41
+ it "removes the state" do
42
+ expect(remove_state).
43
+ to change(machine, :states).
44
+ from(match_array(%w[x y z])).
45
+ to(%w[y z])
46
+ end
47
+
48
+ context "with a transition from the removed state" do
49
+ before { machine.transition from: :x, to: :y }
50
+
51
+ it "removes the transition" do
52
+ expect(remove_state).
53
+ to change(machine, :successors).
54
+ from({ "x" => ["y"] }).
55
+ to({})
56
+ end
57
+
58
+ context "with multiple transitions" do
59
+ before { machine.transition from: :x, to: :z }
60
+
61
+ it "removes all transitions" do
62
+ expect(remove_state).
63
+ to change(machine, :successors).
64
+ from({ "x" => %w[y z] }).
65
+ to({})
66
+ end
67
+ end
68
+ end
69
+
70
+ context "with a transition to the removed state" do
71
+ before { machine.transition from: :y, to: :x }
72
+
73
+ it "removes the transition" do
74
+ expect(remove_state).
75
+ to change(machine, :successors).
76
+ from({ "y" => ["x"] }).
77
+ to({})
78
+ end
79
+
80
+ context "with multiple transitions" do
81
+ before { machine.transition from: :z, to: :x }
82
+
83
+ it "removes all transitions" do
84
+ expect(remove_state).
85
+ to change(machine, :successors).
86
+ from({ "y" => ["x"], "z" => ["x"] }).
87
+ to({})
88
+ end
89
+ end
90
+ end
91
+
92
+ context "with a callback from the removed state" do
93
+ before do
94
+ machine.class_eval do
95
+ transition from: :x, to: :y
96
+ transition from: :x, to: :z
97
+ guard_transition(from: :x) { return false }
98
+ guard_transition(from: :x, to: :z) { return true }
99
+ end
100
+ end
101
+
102
+ let(:guards) do
103
+ [having_attributes(from: "x", to: []), having_attributes(from: "x", to: ["z"])]
104
+ end
105
+
106
+ it "removes the guard" do
107
+ expect(remove_state).
108
+ to change(machine, :callbacks).
109
+ from(a_hash_including(guards: match_array(guards))).
110
+ to(a_hash_including(guards: []))
111
+ end
112
+ end
113
+
114
+ context "with a callback to the removed state" do
115
+ before do
116
+ machine.class_eval do
117
+ transition from: :y, to: :x
118
+ guard_transition(to: :x) { return false }
119
+ guard_transition(from: :y, to: :x) { return true }
120
+ end
121
+ end
122
+
123
+ let(:guards) do
124
+ [having_attributes(from: nil, to: ["x"]), having_attributes(from: "y", to: ["x"])]
125
+ end
126
+
127
+ it "removes the guard" do
128
+ expect(remove_state).
129
+ to change(machine, :callbacks).
130
+ from(a_hash_including(guards: match_array(guards))).
131
+ to(a_hash_including(guards: []))
132
+ end
133
+ end
134
+ end
135
+
30
136
  describe ".retry_conflicts" do
31
137
  subject(:transition_state) do
32
138
  described_class.retry_conflicts(retry_attempts) do
@@ -170,6 +276,42 @@ describe Statesman::Machine do
170
276
  end
171
277
  end
172
278
 
279
+ describe ".remove_transitions" do
280
+ before do
281
+ machine.class_eval do
282
+ state :x
283
+ state :y
284
+ state :z
285
+ transition from: :x, to: :y
286
+ transition from: :x, to: :z
287
+ transition from: :y, to: :z
288
+ end
289
+ end
290
+
291
+ let(:initial_successors) { { "x" => %w[y z], "y" => ["z"] } }
292
+
293
+ it "removes the correct transitions when given a from state" do
294
+ expect { machine.remove_transitions(from: :x) }.
295
+ to change(machine, :successors).
296
+ from(initial_successors).
297
+ to({ "y" => ["z"] })
298
+ end
299
+
300
+ it "removes the correct transitions when given a to state" do
301
+ expect { machine.remove_transitions(to: :z) }.
302
+ to change(machine, :successors).
303
+ from(initial_successors).
304
+ to({ "x" => ["y"] })
305
+ end
306
+
307
+ it "removes the correct transitions when given a from and to state" do
308
+ expect { machine.remove_transitions(from: :x, to: :z) }.
309
+ to change(machine, :successors).
310
+ from(initial_successors).
311
+ to({ "x" => ["y"], "y" => ["z"] })
312
+ end
313
+ end
314
+
173
315
  describe ".validate_callback_condition" do
174
316
  before do
175
317
  machine.class_eval do
@@ -234,11 +376,9 @@ describe Statesman::Machine do
234
376
 
235
377
  it "does not add a callback" do
236
378
  expect do
237
- begin
238
- set_callback
239
- rescue error_type
240
- nil
241
- end
379
+ set_callback
380
+ rescue error_type
381
+ nil
242
382
  end.to_not change(machine.callbacks[callback_store], :count)
243
383
  end
244
384
  end
@@ -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.7"
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_junit_formatter", "~> 0.5.1"
36
37
  spec.add_development_dependency "rspec-rails", "~> 3.1"
37
- spec.add_development_dependency "rspec_junit_formatter", "~> 0.4.0"
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.3
4
+ version: 10.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-06-08 00:00:00.000000000 Z
11
+ date: 2022-05-17 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.5.1
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.5.1
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.7'
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