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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4902a42140176efda107ea4e5caef0b4bd3f8e64554b2fde61f3507e51e80d96
4
- data.tar.gz: 9f85490534c6667640cd050e13eeaf7ce202bbd5757e7ade6cd4d6cb4c191b90
3
+ metadata.gz: 0a64dc12150ea615573c5fc5ce222e80fa8f2959392c94f3c6a80fae6efa53cd
4
+ data.tar.gz: 63b7a19ac4794c1b75d5e4d8a6b93964e5b9400d394927458d44893d2c9f4a31
5
5
  SHA512:
6
- metadata.gz: 10ae6f605953606f563a80aa15b9cc7a70eedc73593d152aba3d5ab773b0243002dc13b0f66888e83008d6c0a341bee762a5bdbde61712e2310833b1345bc067
7
- data.tar.gz: 8b7c7b452a1e0b962b9ef2f643dcdd220597474c87269dc4565bb5a8974af000e78bed0f2c41f8da40adf663a51bcdd21d0a6b7bb9bb3bab0f8a597c36d90032
6
+ metadata.gz: b705009767fb54820386c8748f1747a45d2145dc177355569a794669d1514bac07cb9f5666999950d158ed1b9fba3808eebb8b28f63fdeab3da1818314919598
7
+ data.tar.gz: 187ee30f697c019617f45b057e709d404a2cd11083e57c0f7387f24272a27a17ce7afce6da31b8dff675914498589b9a13e4a64c1eab0e21fe6101d1aa277b75
data/CHANGELOG.md CHANGED
@@ -1,24 +1,28 @@
1
- # Changelog
2
- All notable changes to this project will be documented in this file.
1
+ ## 0.1.4
3
2
 
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).
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.2
7
-
8
- ### Added
11
+ ## 0.1.3
9
12
 
10
- - basic tests for a library
13
+ * Added more documentation and improvements to the readme
14
+ * Little code styling
11
15
 
12
- ### Changed
16
+ ## 0.1.2
13
17
 
14
- - #ensure_<attribute>_may_transition_to! method now actually verifies that an attribute can transition to a new state.
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
- - Provide real authorts of this gem.
24
+ * Provide real authors.
21
25
 
22
26
  ## 0.1.0
23
27
 
24
- - Initial release
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" useful for defining an enum using string values along with valid state transitions. Validations will be added for the state transitions and a proper enum is going to be defined. For example:
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
- $ bundle add state_machine_enum
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
- $ gem install state_machine_enum
25
+ $ gem install state_machine_enum
23
26
 
24
27
  ## Usage
25
28
 
26
- StateMachineEnum needs to be extended and then it could be used, as example in AR model.
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
- s.permit_transition(:registered, :active)
34
- s.permit_transition(:active, :banned)
35
- s.permit_transition(:banned, :active)
36
- s.permit_transition(:active, :deleted)
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
- And then it will offer bunch of convenient methods and callbacks that ensure proper state transitions.
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
- user = User.new(state: 'registered')
45
- user.active?
46
- user.registered! # throws InvalidState error, because state can not transition to "registered".
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. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
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`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, 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).
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
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module StateMachineEnum
4
- VERSION = "0.1.2"
4
+ VERSION = "0.1.4"
5
5
  end
@@ -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
- attr_reader :after_commit_hooks
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
- def may_transition?(from, to)
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 validate(model, attribute_name)
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
- unless was == is || @transitions.include?([was, is])
66
- model.errors.add(attribute_name, "Invalid transition from #{was} to #{is}")
67
- end
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.validate(model, attribute_name) }
101
+ validate { |model| collector._validate(model, attribute_name) }
86
102
 
87
- # Define inline hooks
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 check methods
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 InvalidState, "#{attribute_name} already is #{val.inspect}" if next_state.to_s == val
117
- raise InvalidState, "#{attribute_name} may not transition from #{val.inspect} to #{next_state.inspect}" unless collector.may_transition?(val, next_state)
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
- collector.may_transition?(val, next_state)
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.2
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: 2024-06-18 00:00:00.000000000 Z
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.5.9
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