state_machine_enum 0.1.2 → 0.1.3
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 +7 -0
- data/README.md +98 -11
- data/lib/state_machine_enum/version.rb +1 -1
- data/lib/state_machine_enum.rb +26 -19
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 255a61ddbdf562c086709c6fbe51e37533ac15bf47bd698d0aae2086e2157569
|
4
|
+
data.tar.gz: 759c5a66316166c230d37576d2a087cb93978620a1984f9d10ece17b9ba2f89e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8197dc76f3536c972ab610da6259dc4c0d53ebd049addd747965acb925f43be39b4bb78948edf315b3f257ce107f68db3c989a7f7340f8b65931f901a8da505c
|
7
|
+
data.tar.gz: f219d01939f927981cdd45b3c72fe76ab3a19bbd7c56028a5c6e5ebae64a36adfa36c61e1310afd0ae2ab78c3697af00f8ddee44f9afb02ee8ccc73935a13e9d
|
data/CHANGELOG.md
CHANGED
@@ -3,6 +3,13 @@ All notable changes to this project will be documented in this file.
|
|
3
3
|
|
4
4
|
This format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
5
5
|
|
6
|
+
## 0.1.3
|
7
|
+
|
8
|
+
### Changed
|
9
|
+
|
10
|
+
- Added more documentation and improvements to the readme
|
11
|
+
- Little code styling
|
12
|
+
|
6
13
|
## 0.1.2
|
7
14
|
|
8
15
|
### Added
|
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,7 +16,7 @@ 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
|
|
@@ -23,33 +26,117 @@ If bundler is not being used to manage dependencies, install the gem by executin
|
|
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|
|
35
|
+
state_machine_enum :state, prefix: :state do |s|
|
33
36
|
s.permit_transition(:registered, :active)
|
34
37
|
s.permit_transition(:active, :banned)
|
35
38
|
s.permit_transition(:banned, :active)
|
36
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!
|
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.
|
57
|
+
|
58
|
+
# API
|
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
|
77
|
+
```
|
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
|
39
93
|
```
|
40
94
|
|
41
|
-
|
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.
|
42
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
|
43
107
|
```
|
44
|
-
|
45
|
-
|
46
|
-
|
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
|
47
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,38 +30,42 @@ 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
|
|
@@ -82,12 +83,13 @@ module StateMachineEnum
|
|
82
83
|
|
83
84
|
# Define validations for transitions
|
84
85
|
validates attribute_name, presence: true
|
85
|
-
validate { |model| collector.
|
86
|
+
validate { |model| collector._validate(model, attribute_name) }
|
86
87
|
|
87
|
-
# Define
|
88
|
+
# Define after attribute change (before save) hooks
|
88
89
|
before_save do |model|
|
89
90
|
_value_was, value_has_become = model.changes[attribute_name]
|
90
91
|
next unless value_has_become
|
92
|
+
|
91
93
|
hook_procs = collector.after_attribute_write_hooks[value_has_become].to_a + collector.common_after_write_hooks.to_a
|
92
94
|
hook_procs.each do |hook_proc|
|
93
95
|
hook_proc.call(model)
|
@@ -98,29 +100,34 @@ module StateMachineEnum
|
|
98
100
|
after_commit do |model|
|
99
101
|
_value_was, value_has_become = model.previous_changes[attribute_name]
|
100
102
|
next unless value_has_become
|
103
|
+
|
101
104
|
hook_procs = collector.after_commit_hooks[value_has_become].to_a + collector.common_after_commit_hooks.to_a
|
102
105
|
hook_procs.each do |hook_proc|
|
103
106
|
hook_proc.call(model)
|
104
107
|
end
|
105
108
|
end
|
106
109
|
|
107
|
-
# Define the
|
110
|
+
# Define the ensure methods
|
108
111
|
define_method(:"ensure_#{attribute_name}_one_of!") do |*allowed_states|
|
109
112
|
val = self[attribute_name]
|
110
113
|
return if Set.new(allowed_states.map(&:to_s)).include?(val)
|
114
|
+
|
111
115
|
raise InvalidState, "#{attribute_name} must be one of #{allowed_states.inspect} but was #{val.inspect}"
|
112
116
|
end
|
113
117
|
|
114
118
|
define_method(:"ensure_#{attribute_name}_may_transition_to!") do |next_state|
|
115
119
|
val = self[attribute_name]
|
116
120
|
raise InvalidState, "#{attribute_name} already is #{val.inspect}" if next_state.to_s == val
|
117
|
-
|
121
|
+
return if collector._may_transition?(val, next_state)
|
122
|
+
|
123
|
+
raise InvalidState, "#{attribute_name} may not transition from #{val.inspect} to #{next_state.inspect}"
|
118
124
|
end
|
119
125
|
|
120
126
|
define_method(:"#{attribute_name}_may_transition_to?") do |next_state|
|
121
127
|
val = self[attribute_name]
|
122
128
|
return false if val == next_state.to_s
|
123
|
-
|
129
|
+
|
130
|
+
collector._may_transition?(val, next_state)
|
124
131
|
end
|
125
132
|
end
|
126
133
|
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.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Julik Tarkhanov
|
@@ -10,7 +10,7 @@ authors:
|
|
10
10
|
autorequire:
|
11
11
|
bindir: bin
|
12
12
|
cert_chain: []
|
13
|
-
date: 2024-
|
13
|
+
date: 2024-10-07 00:00:00.000000000 Z
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|
16
16
|
name: activesupport
|
@@ -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.5.
|
97
|
+
rubygems_version: 3.5.18
|
98
98
|
signing_key:
|
99
99
|
specification_version: 4
|
100
100
|
summary: Define possible state transitions for a field
|