has_state_machine 0.6.1 → 1.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: 7d4ecde120f4c419dfe63154f2e61cbb8459a77aedf0b45cc169a3eeac3bcb29
4
- data.tar.gz: df52aa5a5a23abd0d230281e3d124eacbd56e9bbb17cc094a202a02a208fb732
3
+ metadata.gz: 7aad95d7b489183fa7a7c49af178871cb5cc169dbc3a9ae6bd4a93f7ce48ebea
4
+ data.tar.gz: 180dbc281d868326a908955013b17fcc935e54de43dbb367212f017e642b994d
5
5
  SHA512:
6
- metadata.gz: 6c2e58ee081bdbd77e5b9b04adfc8eff0ff94f2bd50c63ed06317be69ae1ff18f28eb32bb149629f06ab41f285c7a4fa4e18788d7a8140b1d91372d3cc3ce8c1
7
- data.tar.gz: f4aefba6c6484abf2dfb0f79a390ca80c8e686637accd18869f3c0991b74496f38bbbe65b47958213ebf12cc48079d3b01ba373a0f9bff16fbe03c11f64a357e
6
+ metadata.gz: 1eca09471018292e2cc4f41a6a53ec2cf12bc087de32af80595901099a4b589131b529bbf53891de89eacc86c5a9a3c16ca7102f31faec63443003c697c45a7e
7
+ data.tar.gz: 1b5a1e0a93e9f0648deac92b4fcba653ffeec037e9b3959092e71b5f9b391c93118e352ced5e7c1f44f385ceed1130ab62f1fde11dc4282ea2fb17097e66d47a
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # HasStateMachine
2
2
 
3
- [![Build Status](https://github.com/bharget/has_state_machine/workflows/Tests/badge.svg)](https://github.com/bharget/has_state_machine/actions)
3
+ [![CI](https://github.com/beehiiv/has_state_machine/actions/workflows/ci.yml/badge.svg)](https://github.com/beehiiv/has_state_machine/actions/workflows/ci.yml)
4
4
  [![Ruby Style Guide](https://img.shields.io/badge/code_style-standard-brightgreen.svg)](https://github.com/testdouble/standard)
5
5
 
6
6
  HasStateMachine uses ruby classes to make creating a finite state machine for your ActiveRecord models a breeze.
@@ -10,7 +10,9 @@ HasStateMachine uses ruby classes to make creating a finite state machine for yo
10
10
  - [HasStateMachine](#hasstatemachine)
11
11
  - [Contents](#contents)
12
12
  - [Installation](#installation)
13
- - [Usage](#usage)
13
+ - [Usage](#basic-usage)
14
+ - [Basic Usage](#basic-usage)
15
+ - [Validations & Error Handling](#validations-and-error-handling)
14
16
  - [Advanced Usage](#advanced-usage)
15
17
  - [Contributing](#contributing)
16
18
  - [License](#license)
@@ -35,12 +37,12 @@ Or install it yourself as:
35
37
  $ gem install has_state_machine
36
38
  ```
37
39
 
38
- ## Usage
40
+ ## Basic Usage
39
41
 
40
42
  You must first use the `has_state_machine` macro to define your state machine at
41
43
  a high level. This includes defining the possible states for your object as well
42
44
  as some optional configuration should you want to change the default behavior of
43
- the state machine.
45
+ the state machine (more on this later).
44
46
 
45
47
  ```ruby
46
48
  # By default, it is assumed that the "state" of the object is
@@ -52,7 +54,7 @@ end
52
54
 
53
55
  Now you must define the classes for the states in your state machine. By default,
54
56
  `HasStateMachine` assumes that these will be under the `Workflow` namespace following
55
- the pattern of `Workflow::#{ObjectClass}::#{State}`. The state objects must inherit
57
+ the pattern of `Workflow::#{ObjectClass}::#{State}`. The state classes must inherit
56
58
  from `HasStateMachine::State`.
57
59
 
58
60
  ```ruby
@@ -117,9 +119,83 @@ post.status.transition_to(:archived)
117
119
  # => true
118
120
  ```
119
121
 
122
+ If you'd like to check that an object can be transitioned into a new state, use the `can_transition?` method. This checks to see if the provided argument is in the `transitions_to` array defined on the object's current state. (This does not run any validations that may be defined on the new state)
123
+
124
+ Example:
125
+ ```ruby
126
+ post = Post.create(status: "draft")
127
+
128
+ post.status.can_transition?(:published) # => true
129
+ post.status.can_transition?(:other_state) # => false
130
+ ```
131
+
132
+ ### Validations and Error Handling
133
+
134
+ You can define custom validations on a given state to determine whether an object in that state or a transition to that state is valid.
135
+
136
+ By default, validations defined on the state will be run as part of the object validations if the object is in that state.
137
+
138
+ ```ruby
139
+ post = Post.create(status: "published", title: "Title")
140
+
141
+ post.valid?
142
+ # => true
143
+
144
+ post.title = nil
145
+ post.valid?
146
+ # => false
147
+ ```
148
+
149
+ If you wish to change this behavior and not have the state validations run on the object, you can specify that with the `state_validations_on_object` option when defining your state machine.
150
+
151
+ ```ruby
152
+ class Post < ApplicationRecord
153
+ has_state_machine states: %i[draft published, archived], state_validations_on_object: false
154
+ end
155
+
156
+ post = Post.create(status: "published", title: "Title")
157
+
158
+ post.valid?
159
+ # => true
160
+
161
+ post.title = nil
162
+ post.valid?
163
+ # => true
164
+ ```
165
+
166
+ By default, when attempting to transition an object to another state, it checks:
167
+ * Validations defined on the object
168
+ * That the new state is one of the allowed transitions from the current state
169
+ * Any validations defined on the new state
170
+
171
+ If any are found to be invalid, the transition will fail. Any errors from validations on the new state will be added to the object.
172
+
173
+ ```ruby
174
+ post = Post.create(status: "draft")
175
+
176
+ post.title = nil
177
+ post.status.transition_to(:published)
178
+ # => false
179
+
180
+ post.errors.full_messages
181
+ # => ["Title can't be blank"]
182
+ ```
183
+
184
+ If you wish to bypass this behavior and skip validations during a transition, you can do that:
185
+
186
+ ```ruby
187
+ post = Post.create(status: "draft")
188
+
189
+ post.title = nil
190
+ post.status.transition_to(:published, skip_validations: true)
191
+ # => true
192
+ ```
193
+
120
194
  ### Advanced Usage
121
195
 
122
- Sometimes there may be a situation where you want to manually roll back a state change in one of the provided callbacks. To do this, add the `transactional: true` option to the `state_options` declaration and use the `rollback_transition` method in your callback. This will allow you to prevent the transition from persisting if something further down the line fails.
196
+ #### Transactional Transitions
197
+
198
+ There may be a situation where you want to manually rollback a state change in one of the provided transition callbacks. To do this, add the `transactional: true` option to the `state_options` declaration. This results in the transition being wrapped in a transaction. You can then use the `rollback_transition` method in your callback when you want to trigger a rollback of the transaction. This will allow you to prevent the transition from persisting if something further down the line fails.
123
199
 
124
200
  ```ruby
125
201
  module Workflow
@@ -139,6 +215,29 @@ module Workflow
139
215
  end
140
216
  ```
141
217
 
218
+ #### Transient Transition Variables
219
+
220
+ Sometimes you may may want to pass additional arguments to a state transition for additional context in your transition callbacks. To do this, add the `transients` option to the `state_options` declaration. This allows you to define any additional attributes you want to be able to pass along during a state transition to that state.
221
+
222
+ ```ruby
223
+ module Workflow
224
+ class Post::Archived < HasStateMachine::State
225
+ state_options transients: %i[user]
226
+
227
+ after_transition do
228
+ puts "== Post archived by #{user.name} =="
229
+ end
230
+ end
231
+ end
232
+
233
+ current_user = User.create(name: "John Doe")
234
+ post = Post.create(status: "published")
235
+
236
+ post.status.transition_to(:archived, user: current_user)
237
+ # == Post archived by John Doe ==
238
+ # => true
239
+ ```
240
+
142
241
  ## Contributing
143
242
 
144
243
  Anyone is encouraged to help improve this project. Here are a few ways you can help:
@@ -18,19 +18,34 @@ module HasStateMachine
18
18
  ##
19
19
  # possible_transitions - Retrieves the next available transitions for a given state.
20
20
  # transactional? - Determines whether or not the transition should happen with a transactional block.
21
- delegate :possible_transitions, :transactional?, :state, to: "self.class"
21
+ # state - The underscored name of the state
22
+ # transients - Specified list of optional transient attributes on this state
23
+ delegate :possible_transitions, :transactional?, :state, :transients, to: "self.class"
22
24
 
23
25
  ##
24
26
  # Initializes the HasStateMachine::State instance.
25
27
  #
26
28
  # @example
27
29
  # state = Workflow::Post::Draft.new(post) #=> "draft"
28
- def initialize(object)
30
+ def initialize(object, transient_values = {})
29
31
  @object = object
30
32
 
33
+ transient_values.to_h.slice(*transients).each do |transient, value|
34
+ instance_variable_set(:"@#{transient}", value)
35
+ end
36
+
31
37
  super(state)
32
38
  end
33
39
 
40
+ ##
41
+ # Determines if the given desired state exists in the predetermined
42
+ # list of allowed transitions.
43
+ # @param desired_state [String, Symbol] the state to check if the object can transition to
44
+ # @return [Boolean] whether or not the object can transition to the desired state
45
+ def can_transition?(desired_state)
46
+ possible_transitions.include? desired_state.to_s
47
+ end
48
+
34
49
  ##
35
50
  # Checks to see if the desired state is valid and then gives
36
51
  # responsibility to the desired state's instance to make the
@@ -43,20 +58,24 @@ module HasStateMachine
43
58
  # @return [Boolean] whether or not the transition took place
44
59
  def transition_to(desired_state, **options)
45
60
  transitioned = false
61
+ options = options.transform_keys(&:to_sym)
62
+ desired_state_instance = state_instance(desired_state, options)
46
63
 
47
64
  with_transition_options(options) do
48
- return false unless valid_transition?(desired_state.to_s)
65
+ return false unless valid_transition?(desired_state_instance)
49
66
 
50
- desired_state = state_instance(desired_state.to_s)
51
-
52
- transitioned = if desired_state.transactional?
53
- desired_state.perform_transactional_transition!
67
+ transitioned = if desired_state_instance.transactional?
68
+ desired_state_instance.perform_transactional_transition!
54
69
  else
55
- desired_state.perform_transition!
70
+ desired_state_instance.perform_transition!
56
71
  end
57
72
  end
58
73
 
59
74
  transitioned
75
+ ensure
76
+ (desired_state_instance&.errors || []).each do |error|
77
+ object.errors.add(error.attribute, error.type)
78
+ end
60
79
  end
61
80
 
62
81
  ##
@@ -88,13 +107,6 @@ module HasStateMachine
88
107
  raise ActiveRecord::Rollback
89
108
  end
90
109
 
91
- ##
92
- # Determines if the given desired state exists in the predetermined
93
- # list of allowed transitions.
94
- def can_transition?(desired_state)
95
- possible_transitions.include? desired_state
96
- end
97
-
98
110
  ##
99
111
  # Helper method for grabbing the previous state of the object after
100
112
  # it has been transitioned to the new state. Useful in
@@ -103,17 +115,17 @@ module HasStateMachine
103
115
  object.previous_changes[object.state_attribute]&.first
104
116
  end
105
117
 
106
- def state_instance(desired_state)
118
+ def state_instance(desired_state, transient_values)
107
119
  klass = "#{object.workflow_namespace}::#{desired_state.to_s.classify}".safe_constantize
108
- klass&.new(object)
120
+ klass&.new(object, transient_values)
109
121
  end
110
122
 
111
- def valid_transition?(desired_state)
123
+ def valid_transition?(desired_state_instance)
112
124
  return true if object.skip_state_validations
113
125
 
114
126
  object.valid? &&
115
- can_transition?(desired_state) &&
116
- state_instance(desired_state)&.valid?
127
+ can_transition?(desired_state_instance) &&
128
+ desired_state_instance&.valid?
117
129
  end
118
130
 
119
131
  def with_transition_options(options, &block)
@@ -135,21 +147,24 @@ module HasStateMachine
135
147
  @transactional || false
136
148
  end
137
149
 
138
- ##
139
- # Setter for the HasStateMachine::State classes to define the possible
140
- # states the current state can transition to.
141
- def transitions_to(states)
142
- state_options(transitions_to: states)
143
- HasStateMachine::Deprecation.deprecation_warning(:transitions_to, "use state_options instead")
150
+ def transients
151
+ @transients || []
144
152
  end
145
153
 
146
154
  ##
147
155
  # Set the options for the HasStateMachine::State classes to define the possible
148
156
  # states the current state can transition to and whether or not transitioning
149
157
  # to the state should be performed within a transaction.
150
- def state_options(transitions_to: [], transactional: false)
158
+ def state_options(transitions_to: [], transactional: false, transients: [])
151
159
  @possible_transitions = transitions_to.map(&:to_s)
152
160
  @transactional = transactional
161
+ @transients = transients.map(&:to_sym)
162
+
163
+ transients.each do |transient_name|
164
+ define_method(transient_name) do
165
+ instance_variable_get(:"@#{transient_name}")
166
+ end
167
+ end
153
168
  end
154
169
  end
155
170
  end
@@ -114,10 +114,10 @@ module HasStateMachine
114
114
  def state_instance_validations
115
115
  return unless state_class.present?
116
116
 
117
- state_instance = public_send(state_attribute.to_s)
118
- return if state_instance.valid?
117
+ current_state_instance = public_send(state_attribute.to_s)
118
+ return if current_state_instance.valid?
119
119
 
120
- state_instance.errors.each do |error|
120
+ current_state_instance.errors.each do |error|
121
121
  errors.add(error.attribute, error.type)
122
122
  end
123
123
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HasStateMachine
4
- VERSION = "0.6.1"
4
+ VERSION = "1.0.0"
5
5
  end
metadata CHANGED
@@ -1,15 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: has_state_machine
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.1
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Benjamin Hargett
8
8
  - Jake Hurd
9
- autorequire:
10
9
  bindir: bin
11
10
  cert_chain: []
12
- date: 2024-10-09 00:00:00.000000000 Z
11
+ date: 1980-01-02 00:00:00.000000000 Z
13
12
  dependencies:
14
13
  - !ruby/object:Gem::Dependency
15
14
  name: rails
@@ -29,16 +28,16 @@ dependencies:
29
28
  name: sqlite3
30
29
  requirement: !ruby/object:Gem::Requirement
31
30
  requirements:
32
- - - "~>"
31
+ - - ">="
33
32
  - !ruby/object:Gem::Version
34
- version: '1.4'
33
+ version: '1.5'
35
34
  type: :development
36
35
  prerelease: false
37
36
  version_requirements: !ruby/object:Gem::Requirement
38
37
  requirements:
39
- - - "~>"
38
+ - - ">="
40
39
  - !ruby/object:Gem::Version
41
- version: '1.4'
40
+ version: '1.5'
42
41
  - !ruby/object:Gem::Dependency
43
42
  name: pry
44
43
  requirement: !ruby/object:Gem::Requirement
@@ -119,7 +118,6 @@ homepage: https://www.github.com/encampment/has_state_machine
119
118
  licenses:
120
119
  - MIT
121
120
  metadata: {}
122
- post_install_message:
123
121
  rdoc_options: []
124
122
  require_paths:
125
123
  - lib
@@ -134,8 +132,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
134
132
  - !ruby/object:Gem::Version
135
133
  version: '0'
136
134
  requirements: []
137
- rubygems_version: 3.1.6
138
- signing_key:
135
+ rubygems_version: 3.6.7
139
136
  specification_version: 4
140
137
  summary: Class based state machine for ActiveRecord models.
141
138
  test_files: []