steady_state 0.0.1 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b6ac44424729680f31b79e25aa8b110323aee6ec97824f19ac03f54921bc2213
4
- data.tar.gz: 77f39b19b580b0930864cf8a867a9e41ae4fc4900356a65123e83359729110f6
3
+ metadata.gz: c80b6cce9dbf7702cdf8fd0d97c21794bcbc6a67c2ea688fcf5b150ee217df5a
4
+ data.tar.gz: 67bc8bc9e117e55ac21b4dcba713dbf97627354c1997e906fbb317ef06b42ac6
5
5
  SHA512:
6
- metadata.gz: 35ac9ceff1d5997e97b3a40840629b5665d26175e491e8ae28df4dd3fa99a64b4aa419122e60a3fc8372e9a6ccc0afa8c07136ea067eca9662d47d65f4099a58
7
- data.tar.gz: e5394a77afb844a74d72f020f22fb343ec1d2d3bea2cdc98a1a0d2db2c6e3a28d0b85cd930b47a4221946830d40f0c52acc9f217d9262337f7c3b4f692678fc0
6
+ metadata.gz: 374352c07075b3ccb50c1c018e6b8e654ebcc84675d44e4024406dde923ba28699f58daf7b67f8161cff8ee41d9e4f75c77f59680f165b62e527787b7dd00e65
7
+ data.tar.gz: 6d493623807a7332a1e3b96a9849d46b6a33d59e8405dbb935ef66de4d9b054b2c471ca8421feb7f75810a2aa6694b0173a64be694a69a31a6210de2232018b0
data/README.md CHANGED
@@ -23,12 +23,9 @@ gem 'steady_state'
23
23
  To enable stateful behavior on an attribute or column, call `steady_state` with the name of the attribute, and define the states as strings, like so:
24
24
 
25
25
  ```ruby
26
- class Material
27
- include ActiveModel::Model
26
+ class Material < ApplicationRecord
28
27
  include SteadyState
29
28
 
30
- attr_accessor :state
31
-
32
29
  steady_state :state do
33
30
  state 'solid', default: true
34
31
  state 'liquid', from: 'solid'
@@ -65,20 +62,19 @@ A class may have any number of these `steady_state` declarations, one per statef
65
62
  After your object has been instantiated (or loaded from a database via your ORM), the transitional validations and rules begin to take effect. To change the state, simply use the attribute's setter (e.g. `state=`), and then call `valid?` to see if the change will be accepted:
66
63
 
67
64
  ```ruby
68
- material.state.solid? # => true
69
- material.state = 'liquid'
70
- material.state # => 'liquid'
71
- material.valid? # => true
72
- ```
73
-
74
- If the change is not valid, a validation error will be added to the object:
75
-
76
- ```ruby
77
- material.state.liquid? # => true
78
- material.state = 'solid'
79
- material.state # => 'solid'
80
- material.valid? # => false
81
- material.errors[:state] # => ['is invalid']
65
+ material.with_lock do # if this is an ActiveRecord, a lock is necessary to avoid race conditions
66
+ material.state.solid? # => true
67
+ material.state = 'liquid'
68
+ material.state # => 'liquid'
69
+ material.valid? # => true
70
+
71
+ # If the change is not valid, a validation error will be added to the object:
72
+ material.state.liquid? # => true
73
+ material.state = 'solid'
74
+ material.state # => 'solid'
75
+ material.valid? # => false
76
+ material.errors[:state] # => ['is invalid']
77
+ end
82
78
  ```
83
79
 
84
80
  #### A Deliberate Design Choice
@@ -98,55 +94,27 @@ model.errors[:amount] # => ['must be greater than 0']
98
94
 
99
95
  In keeping with the general pattern of `ActiveModel::Validations`, we rely on an object's _current state in memory_ to determine whether or not it is valid. For both the `state` and `amount` fields, the attribute is allowed to hold an invalid value, resulting in a validation error on the object.
100
96
 
101
- #### Custom Validations
102
-
103
- In addition to the built-in transitional validations, you can define your own validations that take effect only when the object enters a specific state:
97
+ ### Saving Changes to State
104
98
 
105
- ```ruby
106
- validates :container, absence: true, if: :solid?
107
- validates :container, presence: true, if: :liquid?
108
- ```
109
-
110
- With such a validation in place, a state change will not be valid unless the related validation rules are resolved at the same time:
111
-
112
- ```ruby
113
- object.update!(state: 'liquid') # !! ActiveRecord::RecordInvalid
114
- object.update!(state: 'liquid', container: Cup.new) # 🎉
115
- ```
116
-
117
- With these tools, you can define rich sets of state-aware rules about your object, and then rely simply on built-in methods like `valid?` and `errors` to determine if an operation violates these rules.
118
-
119
- ### Bringing it All Together
120
-
121
- Commonly, state transition events are expected to have names, like "melt" and "evaporate," and other such _action verbs_. SteadyState has no such expectation, and will not define any named events for you.
99
+ Commonly, state transition events are expected to have names, like "melt" and "evaporate," and other such _action verbs_.
100
+ SteadyState has no such expectation, and will not define any named events for you.
122
101
 
123
102
  If you need them, we encourage you to define these transitions using plain ol' Ruby methods, like so:
124
103
 
125
104
  ```ruby
126
105
  def melt
127
- self.state = 'liquid'
128
- valid? # will return `false` if state transition is invalid
106
+ with_lock { update(state: 'liquid') }
129
107
  end
130
108
 
131
109
  def melt!
132
- self.state = 'liquid'
133
- validate! # will raise an exception if state transition is invalid
110
+ with_lock { update!(state: 'liquid') }
134
111
  end
135
112
  ```
136
113
 
137
- If you are using ActiveRecord, you can rely on built-in persistence methods to construct your state transition operations:
138
-
139
- ```ruby
140
- def melt
141
- update(state: 'liquid')
142
- end
143
-
144
- def melt!
145
- update!(state: 'liquid')
146
- end
147
- ```
114
+ The use of `with_lock` is *strongly encouraged* in order to prevent race conditions that might result in invalid state transitions.
148
115
 
149
- For complex persistence operations, we encourage the use of existing persistence helpers like `transaction` and `with_lock`, to provide atomicity and avoid race conditions:
116
+ This is especially important for operations with side-effects, as a transactional lock will both prevent race conditions and guarantee an atomic rollback
117
+ if anything raises an exception:
150
118
 
151
119
  ```ruby
152
120
  def melt
@@ -178,7 +146,7 @@ class MaterialsController < ApplicationController
178
146
  end
179
147
  ```
180
148
 
181
- With the ability to define your states, apply transitional validations, and persist state changes, you should have everything you need to start using SteadyState inside of your application.
149
+ With the ability to define your states, apply transitional validations, and persist state changes, you should have everything you need to start using SteadyState inside of your application!
182
150
 
183
151
  ## Addional Features & Configuration
184
152
 
@@ -207,6 +175,24 @@ material.status.solid? # => true
207
175
  material.status.liquid? # => false
208
176
  ```
209
177
 
178
+ ### Custom Validations
179
+
180
+ Using the supplied predicate methods, you can define your own validations that take effect only when the object enters a specific state:
181
+
182
+ ```ruby
183
+ validates :container, absence: true, if: :solid?
184
+ validates :container, presence: true, if: :liquid?
185
+ ```
186
+
187
+ With such a validation in place, a state change will not be valid unless the related validation rules are resolved at the same time:
188
+
189
+ ```ruby
190
+ object.update!(state: 'liquid') # !! ActiveRecord::RecordInvalid
191
+ object.update!(state: 'liquid', container: Cup.new) # 🎉
192
+ ```
193
+
194
+ With these tools, you can define rich sets of state-aware rules about your object, and then rely simply on built-in methods like `valid?` and `errors` to determine if an operation violates these rules.
195
+
210
196
  ### Scopes
211
197
 
212
198
  On ActiveRecord objects, scopes are automatically defined for each state:
@@ -245,6 +231,33 @@ As it stands, state history is not preserved, but it is still possible to get a
245
231
  material.state.previous_values # => ['solid']
246
232
  ```
247
233
 
234
+ ### ActiveModel Support
235
+
236
+ SteadyState is also available to classes that are not database-backed, as long as they include the `ActiveModel::Model` mixin:
237
+
238
+ ```ruby
239
+ class Material
240
+ include ActiveModel::Model
241
+
242
+ attr_accessor :state
243
+
244
+ steady_state :state do
245
+ state 'solid', default: true
246
+ state 'liquid', from: 'solid'
247
+ end
248
+
249
+ def melt
250
+ self.state = 'liquid'
251
+ valid? # will return `false` if state transition is invalid
252
+ end
253
+
254
+ def melt!
255
+ self.state = 'liquid'
256
+ validate! # will raise an exception if state transition is invalid
257
+ end
258
+ end
259
+ ```
260
+
248
261
  ## How to Contribute
249
262
 
250
263
  We would love for you to contribute! Anything that benefits the majority of `steady_state` users—from a documentation fix to an entirely new feature—is encouraged.
@@ -13,7 +13,7 @@ module SteadyState
13
13
  end
14
14
 
15
15
  class_methods do
16
- def steady_state(attr_name, predicates: true, scopes: SteadyState.active_record?(self), &block) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity, Metrics/LineLength
16
+ def steady_state(attr_name, predicates: true, states_getter: true, scopes: SteadyState.active_record?(self), &block) # rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity, Metrics/CyclomaticComplexity, Metrics/LineLength
17
17
  overrides = Module.new do
18
18
  define_method :"validate_#{attr_name}_transition_to" do |next_value|
19
19
  if public_send(attr_name).may_become?(next_value)
@@ -43,6 +43,15 @@ module SteadyState
43
43
  prepend overrides
44
44
 
45
45
  state_machines[attr_name].instance_eval(&block)
46
+
47
+ if states_getter
48
+ cattr_reader(:"#{attr_name.to_s.pluralize}") do
49
+ state_machines[attr_name].states.map do |state|
50
+ State.new(state_machines[attr_name], state, nil)
51
+ end
52
+ end
53
+ end
54
+
46
55
  delegate(*state_machines[attr_name].predicates, to: attr_name, allow_nil: true) if predicates
47
56
  if scopes
48
57
  state_machines[attr_name].states.each do |state|
@@ -1,3 +1,3 @@
1
1
  module SteadyState
2
- VERSION = '0.0.1'.freeze
2
+ VERSION = '0.1.0'.freeze
3
3
  end
@@ -290,6 +290,39 @@ RSpec.describe SteadyState::Attribute do
290
290
  end
291
291
  end
292
292
 
293
+ context 'with the states_getter option' do
294
+ let(:query_object) { double(where: []) } # rubocop:disable RSpec/VerifiedDoubles
295
+
296
+ before do
297
+ options = opts
298
+ steady_state_class.module_eval do
299
+ attr_accessor :car
300
+
301
+ steady_state :car, options do
302
+ state 'driving', default: true
303
+ state 'stopped', from: 'driving'
304
+ state 'parked', from: 'stopped'
305
+ end
306
+ end
307
+ end
308
+
309
+ context 'default' do
310
+ let(:opts) { {} }
311
+
312
+ it 'defines states getter method' do
313
+ expect(steady_state_class.cars).to eq %w(driving stopped parked)
314
+ end
315
+ end
316
+
317
+ context 'disabled' do
318
+ let(:opts) { { states_getter: false } }
319
+
320
+ it 'does not define states getter method' do
321
+ expect { steady_state_class.cars }.to raise_error(NoMethodError, /undefined method `cars'/)
322
+ end
323
+ end
324
+ end
325
+
293
326
  context 'with the scopes option' do
294
327
  let(:query_object) { double(where: []) } # rubocop:disable RSpec/VerifiedDoubles
295
328
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: steady_state
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nathan Griffith
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-10-23 00:00:00.000000000 Z
11
+ date: 2022-02-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel
@@ -119,8 +119,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
119
119
  - !ruby/object:Gem::Version
120
120
  version: '0'
121
121
  requirements: []
122
- rubyforge_project:
123
- rubygems_version: 2.7.7
122
+ rubygems_version: 3.3.7
124
123
  signing_key:
125
124
  specification_version: 4
126
125
  summary: Minimalist state management via "an enum with guard rails"