state_machine_enum 0.1.2 → 0.1.4
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 +4 -4
- data/CHANGELOG.md +16 -12
- data/README.md +104 -17
- data/lib/state_machine_enum/version.rb +1 -1
- data/lib/state_machine_enum.rb +42 -20
- metadata +11 -11
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0a64dc12150ea615573c5fc5ce222e80fa8f2959392c94f3c6a80fae6efa53cd
|
4
|
+
data.tar.gz: 63b7a19ac4794c1b75d5e4d8a6b93964e5b9400d394927458d44893d2c9f4a31
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b705009767fb54820386c8748f1747a45d2145dc177355569a794669d1514bac07cb9f5666999950d158ed1b9fba3808eebb8b28f63fdeab3da1818314919598
|
7
|
+
data.tar.gz: 187ee30f697c019617f45b057e709d404a2cd11083e57c0f7387f24272a27a17ce7afce6da31b8dff675914498589b9a13e4a64c1eab0e21fe6101d1aa277b75
|
data/CHANGELOG.md
CHANGED
@@ -1,24 +1,28 @@
|
|
1
|
-
|
2
|
-
All notable changes to this project will be documented in this file.
|
1
|
+
## 0.1.4
|
3
2
|
|
4
|
-
|
3
|
+
* Raise `InvalidTransition` which is a subclass of `InvalidState`. That exception contains the following properties which can be used:
|
4
|
+
* `attribute_name` is the name of the enum attribute
|
5
|
+
* `from_state` is the state the transition was attempted from
|
6
|
+
* `to_state` is the state the transition was going to
|
7
|
+
* `already_in_target_state?` is `true` if a repeated transition was attempted
|
8
|
+
* Some more code styling
|
9
|
+
* Relax dependencies so that Bundler does not complain on Rails 8.x apps
|
5
10
|
|
6
|
-
## 0.1.
|
7
|
-
|
8
|
-
### Added
|
11
|
+
## 0.1.3
|
9
12
|
|
10
|
-
|
13
|
+
* Added more documentation and improvements to the readme
|
14
|
+
* Little code styling
|
11
15
|
|
12
|
-
|
16
|
+
## 0.1.2
|
13
17
|
|
14
|
-
|
18
|
+
* Add basic tests
|
19
|
+
* `#ensure_<attribute>_may_transition_to!` now actually verifies that an attribute can transition to the new state.
|
15
20
|
|
16
21
|
## 0.1.1
|
17
22
|
|
18
|
-
### Added
|
19
23
|
|
20
|
-
|
24
|
+
* Provide real authors.
|
21
25
|
|
22
26
|
## 0.1.0
|
23
27
|
|
24
|
-
|
28
|
+
* Initial release
|
data/README.md
CHANGED
@@ -1,7 +1,10 @@
|
|
1
1
|
# StateMachineEnum
|
2
2
|
|
3
|
-
This concern adds a method called "state_machine_enum"
|
3
|
+
This concern adds a method called "state_machine_enum".
|
4
|
+
Useful for defining an enum using string values along with valid state transitions.
|
5
|
+
Validations will be added for the state transitions and a proper enum is going to be defined.
|
4
6
|
|
7
|
+
For example:
|
5
8
|
```ruby
|
6
9
|
state_machine_enum :state do |states|
|
7
10
|
states.permit_transition(:created, :approved_pending_settlement)
|
@@ -13,43 +16,127 @@ end
|
|
13
16
|
|
14
17
|
## Installation
|
15
18
|
|
16
|
-
Install the gem and add to the application's Gemfile by executing:
|
19
|
+
Install the gem and add it to the application's Gemfile by executing:
|
17
20
|
|
18
|
-
|
21
|
+
$ bundle add state_machine_enum
|
19
22
|
|
20
23
|
If bundler is not being used to manage dependencies, install the gem by executing:
|
21
24
|
|
22
|
-
|
25
|
+
$ gem install state_machine_enum
|
23
26
|
|
24
27
|
## Usage
|
25
28
|
|
26
|
-
StateMachineEnum needs to be
|
29
|
+
StateMachineEnum needs to be included and then it could be used, for example, in an ActiveRecord model.
|
27
30
|
|
28
|
-
```
|
31
|
+
```ruby
|
29
32
|
class User < ApplicationRecord
|
30
33
|
include StateMachineEnum
|
31
34
|
|
32
|
-
state_machine_enum :state do |s|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
35
|
+
state_machine_enum :state, prefix: :state do |s|
|
36
|
+
s.permit_transition(:registered, :active)
|
37
|
+
s.permit_transition(:active, :banned)
|
38
|
+
s.permit_transition(:banned, :active)
|
39
|
+
s.permit_transition(:active, :deleted)
|
37
40
|
end
|
38
41
|
end
|
42
|
+
|
43
|
+
user = User.new(state: 'active')
|
44
|
+
# with the prefix: :state
|
45
|
+
user.state_active? # => true
|
46
|
+
# or without the prefix: :state
|
47
|
+
user.active? # => true
|
48
|
+
|
49
|
+
# The transition check happens when updating the state like this
|
50
|
+
user.update!(state: :registered)
|
51
|
+
# or when using the shortcut (add state_ because we have prefix: :state above)
|
52
|
+
user.state_registered!
|
39
53
|
```
|
54
|
+
The last command throws an InvalidState error: Invalid transition from active to registered
|
55
|
+
This is because the state was not permitted to transition back to "registered" from "active".
|
56
|
+
If you do want this, `s.permit_transition(:active, :registered)` should be added.
|
40
57
|
|
41
|
-
|
58
|
+
# API
|
42
59
|
|
60
|
+
## state_machine_enum(state, prefix: nil) &block
|
61
|
+
Creation method that sets up the state_machine_enum in your ruby object.
|
62
|
+
Note the prefix here to prefix the method. This is optional of course.
|
63
|
+
This works the same as when you would add `enum :state, {registered: "registered"}` in rails for example, except when using state_machine_enum
|
64
|
+
you don't need to add an `enum` as well, we do this for you.
|
65
|
+
|
66
|
+
## after_inline_transition_to(to) &block
|
67
|
+
Runs the block inside `after_inline_transition_to` as a before_save action.
|
68
|
+
For example the state updates to :registered, but before the model is saved
|
69
|
+
|
70
|
+
```ruby
|
71
|
+
state_machine_enum :state, prefix: "state" do |s|
|
72
|
+
s.permit_transition(:registered, :active)
|
73
|
+
s.after_inline_transition_to(:active) do |model|
|
74
|
+
model.another_attr = Time.now.utc
|
75
|
+
end
|
76
|
+
end
|
43
77
|
```
|
44
|
-
|
45
|
-
|
46
|
-
|
78
|
+
|
79
|
+
`another_attr` is automatically set to the current utc time.
|
80
|
+
|
81
|
+
## after_committed_transition_to(to) &block
|
82
|
+
Runs the block inside `after_committed_transition_to` as an after_commit action.
|
83
|
+
For example if you want to do something after it has committed to the database when the state is
|
84
|
+
updated to :registered
|
85
|
+
|
86
|
+
```ruby
|
87
|
+
state_machine_enum :state, prefix: "state" do |s|
|
88
|
+
s.permit_transition(:registered, :active)
|
89
|
+
s.after_committed_transition_to(:active) do |model|
|
90
|
+
model.send_notification!
|
91
|
+
end
|
92
|
+
end
|
47
93
|
```
|
94
|
+
|
95
|
+
## after_any_committed_transition_to(to) &block
|
96
|
+
Runs together with all the `after_committed_transition_to` hooks.
|
97
|
+
For example if you want to do something after any state update has commited.
|
98
|
+
|
99
|
+
```ruby
|
100
|
+
state_machine_enum :state, prefix: "state" do |s|
|
101
|
+
s.permit_transition(:registered, :active)
|
102
|
+
s.permit_transition(:active, :suspended)
|
103
|
+
s.after_any_committed_transition_to do |model|
|
104
|
+
log_changes!
|
105
|
+
end
|
106
|
+
end
|
107
|
+
```
|
108
|
+
|
109
|
+
## Ensure methods
|
110
|
+
With a couple of ensure methods we can check beforehand for valid state transitions without actually having to do the state transition.
|
111
|
+
This allows you to bail out of calls where the model is not in a desired state or won't be able to perform a transition, by raising an InvalidState exception
|
112
|
+
|
113
|
+
## ensure_<attribute>_one_of!(state1, state2, etc)
|
114
|
+
E.g. seen from the previous examples, calling `ensure_state_one_of!(:registered, :active, :fake)`
|
115
|
+
will raise an InvalidState error because :fake is not present in state enum.
|
116
|
+
|
117
|
+
## ensure_<attribute>_may_transition_to!(to)
|
118
|
+
Calling `ensure_state_may_transition_to!(:active)` when the state is currently in :suspended
|
119
|
+
will raise an InvalidState error because we did not permite the transition from :active to :suspended.
|
120
|
+
|
121
|
+
## <attribute>_may_transition_to?(to)
|
122
|
+
Predicate to check if a transition is possible with the rules we've set.
|
123
|
+
|
124
|
+
```ruby
|
125
|
+
state_machine_enum :state, prefix: "state" do |s|
|
126
|
+
s.permit_transition(:registered, :active)
|
127
|
+
end
|
128
|
+
|
129
|
+
state_may_transition_to?(:active) # => true
|
130
|
+
```
|
131
|
+
|
48
132
|
## Development
|
49
133
|
|
50
|
-
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests.
|
134
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests.
|
135
|
+
You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
51
136
|
|
52
|
-
To install this gem onto your local machine, run `bundle exec rake install`.
|
137
|
+
To install this gem onto your local machine, run `bundle exec rake install`.
|
138
|
+
To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`,
|
139
|
+
which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
53
140
|
|
54
141
|
## Contributing
|
55
142
|
|
data/lib/state_machine_enum.rb
CHANGED
@@ -13,16 +13,13 @@ require "active_support/concern"
|
|
13
13
|
# states.permit_transition(:created, :rejected)
|
14
14
|
# states.permit_transition(:approved_pending_settlement, :settled)
|
15
15
|
# end
|
16
|
-
|
17
16
|
module StateMachineEnum
|
18
17
|
extend ActiveSupport::Concern
|
19
18
|
|
19
|
+
# This keeps track of the states and rules we allow.
|
20
20
|
class StatesCollector
|
21
|
-
attr_reader :states
|
22
|
-
|
23
|
-
attr_reader :common_after_commit_hooks
|
24
|
-
attr_reader :after_attribute_write_hooks
|
25
|
-
attr_reader :common_after_write_hooks
|
21
|
+
attr_reader :states, :after_commit_hooks, :common_after_commit_hooks,
|
22
|
+
:after_attribute_write_hooks, :common_after_write_hooks
|
26
23
|
|
27
24
|
def initialize
|
28
25
|
@transitions = Set.new
|
@@ -33,44 +30,63 @@ module StateMachineEnum
|
|
33
30
|
@common_after_write_hooks = []
|
34
31
|
end
|
35
32
|
|
33
|
+
# Add a 'rule' that allows a transition in a single direction
|
36
34
|
def permit_transition(from, to)
|
37
35
|
@states << from.to_s << to.to_s
|
38
36
|
@transitions << [from.to_s, to.to_s]
|
39
37
|
end
|
40
38
|
|
41
|
-
|
42
|
-
@transitions.include?([from.to_s, to.to_s])
|
43
|
-
end
|
44
|
-
|
39
|
+
# Runs after the attributes have changed, but before the state is saved
|
45
40
|
def after_inline_transition_to(target_state, &blk)
|
46
41
|
@after_attribute_write_hooks[target_state.to_s] ||= []
|
47
42
|
@after_attribute_write_hooks[target_state.to_s] << blk.to_proc
|
48
43
|
end
|
49
44
|
|
45
|
+
# Will run after the specified transition has comitted
|
50
46
|
def after_committed_transition_to(target_state, &blk)
|
51
47
|
@after_commit_hooks[target_state.to_s] ||= []
|
52
48
|
@after_commit_hooks[target_state.to_s] << blk.to_proc
|
53
49
|
end
|
54
50
|
|
51
|
+
# A generic block that will run together with every committed transition.
|
55
52
|
def after_any_committed_transition(&blk)
|
56
53
|
@common_after_commit_hooks << blk.to_proc
|
57
54
|
end
|
58
55
|
|
59
|
-
def
|
56
|
+
def _validate(model, attribute_name)
|
60
57
|
return unless model.persisted?
|
61
58
|
|
62
59
|
was = model.attribute_was(attribute_name)
|
63
60
|
is = model[attribute_name]
|
64
61
|
|
65
|
-
|
66
|
-
|
67
|
-
|
62
|
+
return if (was == is) || @transitions.include?([was, is])
|
63
|
+
|
64
|
+
model.errors.add(attribute_name, "Invalid transition from #{was} to #{is}")
|
65
|
+
end
|
66
|
+
|
67
|
+
def _may_transition?(from, to)
|
68
|
+
@transitions.include?([from.to_s, to.to_s])
|
68
69
|
end
|
69
70
|
end
|
70
71
|
|
71
72
|
class InvalidState < StandardError
|
72
73
|
end
|
73
74
|
|
75
|
+
class InvalidTransition < InvalidState
|
76
|
+
attr_reader :attribute_name, :from_state, :to_state
|
77
|
+
|
78
|
+
def already_in_target_state?
|
79
|
+
@from_state == @to_state
|
80
|
+
end
|
81
|
+
|
82
|
+
def initialize(attribute_name, attempted_transition_from_state, attempted_transition_to_state, *args_for_super)
|
83
|
+
@attribute_name = attribute_name.to_s
|
84
|
+
@from_state = attempted_transition_from_state.to_s
|
85
|
+
@to_state = attempted_transition_to_state.to_s
|
86
|
+
super(*args_for_super)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
74
90
|
class_methods do
|
75
91
|
def state_machine_enum(attribute_name, **options_for_enum)
|
76
92
|
# Collect the states
|
@@ -82,12 +98,13 @@ module StateMachineEnum
|
|
82
98
|
|
83
99
|
# Define validations for transitions
|
84
100
|
validates attribute_name, presence: true
|
85
|
-
validate { |model| collector.
|
101
|
+
validate { |model| collector._validate(model, attribute_name) }
|
86
102
|
|
87
|
-
# Define
|
103
|
+
# Define after attribute change (before save) hooks
|
88
104
|
before_save do |model|
|
89
105
|
_value_was, value_has_become = model.changes[attribute_name]
|
90
106
|
next unless value_has_become
|
107
|
+
|
91
108
|
hook_procs = collector.after_attribute_write_hooks[value_has_become].to_a + collector.common_after_write_hooks.to_a
|
92
109
|
hook_procs.each do |hook_proc|
|
93
110
|
hook_proc.call(model)
|
@@ -98,29 +115,34 @@ module StateMachineEnum
|
|
98
115
|
after_commit do |model|
|
99
116
|
_value_was, value_has_become = model.previous_changes[attribute_name]
|
100
117
|
next unless value_has_become
|
118
|
+
|
101
119
|
hook_procs = collector.after_commit_hooks[value_has_become].to_a + collector.common_after_commit_hooks.to_a
|
102
120
|
hook_procs.each do |hook_proc|
|
103
121
|
hook_proc.call(model)
|
104
122
|
end
|
105
123
|
end
|
106
124
|
|
107
|
-
# Define the
|
125
|
+
# Define the ensure methods
|
108
126
|
define_method(:"ensure_#{attribute_name}_one_of!") do |*allowed_states|
|
109
127
|
val = self[attribute_name]
|
110
128
|
return if Set.new(allowed_states.map(&:to_s)).include?(val)
|
129
|
+
|
111
130
|
raise InvalidState, "#{attribute_name} must be one of #{allowed_states.inspect} but was #{val.inspect}"
|
112
131
|
end
|
113
132
|
|
114
133
|
define_method(:"ensure_#{attribute_name}_may_transition_to!") do |next_state|
|
115
134
|
val = self[attribute_name]
|
116
|
-
raise
|
117
|
-
|
135
|
+
raise InvalidTransition.new(attribute_name, val, next_state, "#{attribute_name} already is #{val.inspect}") if next_state.to_s == val
|
136
|
+
return if collector._may_transition?(val, next_state)
|
137
|
+
|
138
|
+
raise InvalidTransition.new(attribute_name, val, next_state, "#{attribute_name} may not transition from #{val.inspect} to #{next_state.inspect}")
|
118
139
|
end
|
119
140
|
|
120
141
|
define_method(:"#{attribute_name}_may_transition_to?") do |next_state|
|
121
142
|
val = self[attribute_name]
|
122
143
|
return false if val == next_state.to_s
|
123
|
-
|
144
|
+
|
145
|
+
collector._may_transition?(val, next_state)
|
124
146
|
end
|
125
147
|
end
|
126
148
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: state_machine_enum
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.1.
|
4
|
+
version: 0.1.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Julik Tarkhanov
|
@@ -10,22 +10,22 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date:
|
13
|
+
date: 2025-03-24 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: activesupport
|
17
17
|
requirement: !ruby/object:Gem::Requirement
|
18
18
|
requirements:
|
19
|
-
- - "
|
19
|
+
- - ">="
|
20
20
|
- !ruby/object:Gem::Version
|
21
|
-
version: '7'
|
21
|
+
version: '7.0'
|
22
22
|
type: :runtime
|
23
23
|
prerelease: false
|
24
24
|
version_requirements: !ruby/object:Gem::Requirement
|
25
25
|
requirements:
|
26
|
-
- - "
|
26
|
+
- - ">="
|
27
27
|
- !ruby/object:Gem::Version
|
28
|
-
version: '7'
|
28
|
+
version: '7.0'
|
29
29
|
- !ruby/object:Gem::Dependency
|
30
30
|
name: sqlite3
|
31
31
|
requirement: !ruby/object:Gem::Requirement
|
@@ -44,16 +44,16 @@ dependencies:
|
|
44
44
|
name: activerecord
|
45
45
|
requirement: !ruby/object:Gem::Requirement
|
46
46
|
requirements:
|
47
|
-
- - "
|
47
|
+
- - ">="
|
48
48
|
- !ruby/object:Gem::Version
|
49
|
-
version: '7'
|
49
|
+
version: '7.0'
|
50
50
|
type: :development
|
51
51
|
prerelease: false
|
52
52
|
version_requirements: !ruby/object:Gem::Requirement
|
53
53
|
requirements:
|
54
|
-
- - "
|
54
|
+
- - ">="
|
55
55
|
- !ruby/object:Gem::Version
|
56
|
-
version: '7'
|
56
|
+
version: '7.0'
|
57
57
|
description: Concern that makes it easy to define and enforce possibe state transitions
|
58
58
|
for a field/object
|
59
59
|
email:
|
@@ -94,7 +94,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
94
94
|
- !ruby/object:Gem::Version
|
95
95
|
version: '0'
|
96
96
|
requirements: []
|
97
|
-
rubygems_version: 3.
|
97
|
+
rubygems_version: 3.3.7
|
98
98
|
signing_key:
|
99
99
|
specification_version: 4
|
100
100
|
summary: Define possible state transitions for a field
|