state_machines-activerecord 0.100.0 → 0.200.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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f022e63c2bfd6af186733c95cbfc19cb7f25dfff128c791e5199b1a3f4e6d760
|
|
4
|
+
data.tar.gz: '094b3a2befdd974fa1828e9237f75f0a5a38319b7e38b56b8a3dda376a1e797d'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cfb9f8bca11c37eeaff38ff0a237c2cc241986bba88ee4a8859199ea05ee7d06b4449da73574e0125f678de3082f3ba4e472f491f0efe9100fc2b585cac5e818
|
|
7
|
+
data.tar.gz: 8fb449a5ead40d4cf43e92208d307bb34f557e9aae068cda123c968ded28aecfa7a97c416426b04f99afd2550a50b54f852bc6e04c5ab6dc4fef62199d5e615d
|
data/README.md
CHANGED
|
@@ -77,7 +77,164 @@ Vehicle.with_state(params[:state]) # Returns all vehicles if par
|
|
|
77
77
|
Vehicle.where(color: 'red').with_state(nil) # Returns all red vehicles (chainable)
|
|
78
78
|
```
|
|
79
79
|
|
|
80
|
-
|
|
80
|
+
## Rails Enum Integration
|
|
81
|
+
|
|
82
|
+
When your ActiveRecord model uses Rails enums and defines a state machine on the same attribute, this gem automatically detects the conflict and provides seamless integration. This prevents method name collisions between Rails enum methods and state machine methods.
|
|
83
|
+
|
|
84
|
+
### Auto-Detection and Conflict Resolution
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
class Order < ApplicationRecord
|
|
88
|
+
# Rails enum definition
|
|
89
|
+
enum :status, { pending: 0, processing: 1, completed: 2, cancelled: 3 }
|
|
90
|
+
|
|
91
|
+
# State machine on the same attribute
|
|
92
|
+
state_machine :status do
|
|
93
|
+
state :pending, :processing, :completed, :cancelled
|
|
94
|
+
|
|
95
|
+
event :process do
|
|
96
|
+
transition pending: :processing
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
event :complete do
|
|
100
|
+
transition processing: :completed
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
event :cancel do
|
|
104
|
+
transition [:pending, :processing] => :cancelled
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
When enum integration is detected, the gem automatically:
|
|
111
|
+
- Preserves original Rails enum methods (`pending?`, `processing?`, etc.)
|
|
112
|
+
- Generates prefixed state machine methods to avoid conflicts (`status_pending?`, `status_processing?`, etc.)
|
|
113
|
+
- Creates prefixed scope methods (`Order.status_pending`, `Order.status_processing`, etc.)
|
|
114
|
+
|
|
115
|
+
### Available Methods
|
|
116
|
+
|
|
117
|
+
**Original Rails enum methods (preserved):**
|
|
118
|
+
```ruby
|
|
119
|
+
order = Order.create(status: :pending)
|
|
120
|
+
order.pending? # => true (Rails enum method)
|
|
121
|
+
order.processing? # => false (Rails enum method)
|
|
122
|
+
order.processing! # Sets status to :processing (Rails enum method)
|
|
123
|
+
|
|
124
|
+
Order.pending # Rails enum scope
|
|
125
|
+
Order.processing # Rails enum scope
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
**Generated state machine methods (prefixed):**
|
|
129
|
+
```ruby
|
|
130
|
+
# Predicate methods
|
|
131
|
+
order.status_pending? # => true (state machine method)
|
|
132
|
+
order.status_processing? # => false (state machine method)
|
|
133
|
+
order.status_completed? # => false (state machine method)
|
|
134
|
+
|
|
135
|
+
# Bang methods (for conflict resolution only)
|
|
136
|
+
# These are placeholders and raise runtime errors
|
|
137
|
+
order.status_processing! # => raises RuntimeError
|
|
138
|
+
|
|
139
|
+
# Scope methods
|
|
140
|
+
Order.status_pending # State machine scope
|
|
141
|
+
Order.status_processing # State machine scope
|
|
142
|
+
Order.not_status_pending # Negative state machine scope
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Introspection API
|
|
146
|
+
|
|
147
|
+
The integration provides a comprehensive introspection API for advanced use cases:
|
|
148
|
+
|
|
149
|
+
```ruby
|
|
150
|
+
machine = Order.state_machine(:status)
|
|
151
|
+
|
|
152
|
+
# Check if enum integration is enabled
|
|
153
|
+
machine.enum_integrated? # => true
|
|
154
|
+
|
|
155
|
+
# Get the Rails enum mapping
|
|
156
|
+
machine.enum_mapping # => {"pending"=>0, "processing"=>1, "completed"=>2, "cancelled"=>3}
|
|
157
|
+
|
|
158
|
+
# Get original Rails enum methods that were preserved
|
|
159
|
+
machine.original_enum_methods
|
|
160
|
+
# => ["pending?", "processing?", "completed?", "cancelled?", "pending!", "processing!", ...]
|
|
161
|
+
|
|
162
|
+
# Get state machine methods that were generated
|
|
163
|
+
machine.state_machine_methods
|
|
164
|
+
# => ["status_pending?", "status_processing?", "status_completed?", "status_cancelled?", ...]
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Integer-backed state attributes
|
|
168
|
+
|
|
169
|
+
Integer columns whose states don't declare explicit values are converted
|
|
170
|
+
transparently: application code reads state names while the database stores
|
|
171
|
+
integers (mapped by definition order).
|
|
172
|
+
|
|
173
|
+
```ruby
|
|
174
|
+
class Order < ApplicationRecord
|
|
175
|
+
state_machine :status, initial: :pending do
|
|
176
|
+
state :pending
|
|
177
|
+
state :approved
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
order = Order.create!
|
|
182
|
+
order.status = :approved
|
|
183
|
+
order.status # => "approved"
|
|
184
|
+
# The database stores 1.
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Machines where every state declares an explicit integer value keep the classic
|
|
188
|
+
raw-integer behavior automatically: reads return the integer and `status_name`
|
|
189
|
+
returns the state name:
|
|
190
|
+
|
|
191
|
+
```ruby
|
|
192
|
+
class LegacyOrder < ApplicationRecord
|
|
193
|
+
self.table_name = "orders"
|
|
194
|
+
|
|
195
|
+
state_machine :status, initial: :pending do
|
|
196
|
+
state :pending, value: 0
|
|
197
|
+
state :approved, value: 1
|
|
198
|
+
|
|
199
|
+
event :approve do
|
|
200
|
+
transition pending: :approved
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
order = LegacyOrder.create!
|
|
206
|
+
order.approve!
|
|
207
|
+
order.status # => 1
|
|
208
|
+
order.status_name # => :approved
|
|
209
|
+
# The database stores 1.
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Applications that want no type conversion at all can disable it during boot,
|
|
213
|
+
before defining affected state machines:
|
|
214
|
+
|
|
215
|
+
```ruby
|
|
216
|
+
# config/initializers/state_machines.rb
|
|
217
|
+
|
|
218
|
+
StateMachines::Integrations::ActiveRecord.auto_convert_integer_state_attributes = false
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
With this setting disabled, integer-backed state attributes are left entirely
|
|
222
|
+
to ActiveRecord's normal integer handling.
|
|
223
|
+
|
|
224
|
+
### Requirements for Enum Integration
|
|
225
|
+
|
|
226
|
+
- The state machine attribute must match an existing Rails enum attribute
|
|
227
|
+
- Auto-detection is enabled by default when this condition is met
|
|
228
|
+
|
|
229
|
+
### Configuration Options
|
|
230
|
+
|
|
231
|
+
The enum integration supports several configuration options:
|
|
232
|
+
|
|
233
|
+
- `prefix` (default: true) - Adds a prefix to generated methods to avoid conflicts
|
|
234
|
+
- `suffix` (default: false) - Alternative naming strategy using suffixes instead of prefixes
|
|
235
|
+
- `scopes` (default: true) - Controls whether state machine scopes are generated
|
|
236
|
+
|
|
237
|
+
## State driven validations
|
|
81
238
|
|
|
82
239
|
As mentioned in `StateMachines::Machine#state`, you can define behaviors,
|
|
83
240
|
like validations, that only execute for certain states. One *important*
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StateMachines
|
|
4
|
+
module Type
|
|
5
|
+
# Custom ActiveRecord attribute type for state machine attributes backed by
|
|
6
|
+
# integer columns. Handles bidirectional conversion between state name strings
|
|
7
|
+
# (used internally by the state machine) and integer values (stored in the DB).
|
|
8
|
+
#
|
|
9
|
+
# States without explicit integer values are mapped by their index position
|
|
10
|
+
# in the states collection (0, 1, 2, …). States with an explicit integer
|
|
11
|
+
# value (e.g. state :pending, value: 2) use that value directly.
|
|
12
|
+
#
|
|
13
|
+
# When *every* named state has an explicit integer value, the column already
|
|
14
|
+
# stores the canonical state values and no name<->integer conversion is
|
|
15
|
+
# needed. In that case the type delegates to the column's original integer
|
|
16
|
+
# type, preserving the classic raw-integer behavior
|
|
17
|
+
# (e.g. record.status # => 1, record.status_name # => :approved).
|
|
18
|
+
class Integer < ::ActiveRecord::Type::Value
|
|
19
|
+
# The column's original attribute type, exposed so re-registration
|
|
20
|
+
# (e.g. for STI subclasses) can reuse it instead of wrapping this type.
|
|
21
|
+
#
|
|
22
|
+
# @return [ActiveModel::Type::Value]
|
|
23
|
+
attr_reader :raw_type
|
|
24
|
+
|
|
25
|
+
# @param states [StateMachines::StateCollection] live collection of the
|
|
26
|
+
# machine's states; held by reference because states are defined after
|
|
27
|
+
# the type is registered
|
|
28
|
+
# @param raw_type [ActiveModel::Type::Value, nil] the column's original
|
|
29
|
+
# attribute type, used verbatim in passthrough mode so adapter-specific
|
|
30
|
+
# integer behavior (limits, range checks) is preserved
|
|
31
|
+
def initialize(states, raw_type: nil)
|
|
32
|
+
@states = states
|
|
33
|
+
@raw_type = raw_type || ::ActiveModel::Type::Integer.new
|
|
34
|
+
super()
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Converts an integer from the database to a state name string.
|
|
38
|
+
#
|
|
39
|
+
# @param value [Integer, String, nil] raw database value
|
|
40
|
+
# @return [String, Integer, nil] state name, or the original type's value
|
|
41
|
+
# in passthrough mode, or the raw value when no state matches
|
|
42
|
+
def deserialize(value)
|
|
43
|
+
states = named_states
|
|
44
|
+
return @raw_type.deserialize(value) if passthrough?(states)
|
|
45
|
+
return nil if value.nil?
|
|
46
|
+
|
|
47
|
+
int_val = value.to_i
|
|
48
|
+
state = states.detect { |s| state_integer(s, states) == int_val }
|
|
49
|
+
state ? state.name.to_s : value
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Converts an assigned value (symbol, string, or integer) to the in-memory
|
|
53
|
+
# state name string.
|
|
54
|
+
#
|
|
55
|
+
# @param value [Symbol, String, Integer, nil] assigned value
|
|
56
|
+
# @return [String, Integer, nil] state name, or the original type's cast
|
|
57
|
+
# in passthrough mode
|
|
58
|
+
def cast(value)
|
|
59
|
+
states = named_states
|
|
60
|
+
return @raw_type.cast(value) if passthrough?(states)
|
|
61
|
+
return nil if value.nil?
|
|
62
|
+
|
|
63
|
+
state = states.detect { |s| s.name.to_s == value.to_s }
|
|
64
|
+
state ||= states.detect { |s| state_integer(s, states) == value.to_i } if value.respond_to?(:to_i)
|
|
65
|
+
state ? state.name.to_s : value.to_s
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Converts a state name string to its integer for the database write.
|
|
69
|
+
#
|
|
70
|
+
# @param value [String, Symbol, Integer, nil] in-memory value
|
|
71
|
+
# @return [Integer, nil] integer to store
|
|
72
|
+
def serialize(value)
|
|
73
|
+
states = named_states
|
|
74
|
+
return @raw_type.serialize(value) if passthrough?(states)
|
|
75
|
+
return nil if value.nil?
|
|
76
|
+
|
|
77
|
+
state = states.detect { |s| s.name.to_s == value.to_s }
|
|
78
|
+
state ? state_integer(state, states) : value
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# @return [Symbol] the ActiveModel type identifier
|
|
82
|
+
def type
|
|
83
|
+
:integer
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
private
|
|
87
|
+
|
|
88
|
+
# All non-nil states in definition order, not memoized because states are
|
|
89
|
+
# added to the collection after the type is instantiated.
|
|
90
|
+
#
|
|
91
|
+
# @return [Array<StateMachines::State>]
|
|
92
|
+
def named_states
|
|
93
|
+
@states.reject { |s| s.name.nil? }
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Whether the machine was defined for raw integer storage, in which case
|
|
97
|
+
# conversion would change documented behavior. True when every named state
|
|
98
|
+
# declares an explicit integer value. Evaluated lazily on every call
|
|
99
|
+
# because states (and their values) are defined after the type is
|
|
100
|
+
# registered. Uses value(false) so dynamic (Proc) state values are never
|
|
101
|
+
# evaluated for this metadata decision.
|
|
102
|
+
#
|
|
103
|
+
# @param states [Array<StateMachines::State>] pre-computed named states
|
|
104
|
+
# @return [Boolean]
|
|
105
|
+
def passthrough?(states)
|
|
106
|
+
states.any? && states.all? { |s| s.value(false).is_a?(::Integer) }
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# The integer to use for storage: the explicit state value if set
|
|
110
|
+
# (e.g. state :pending, value: 2), otherwise the index position among
|
|
111
|
+
# named states.
|
|
112
|
+
#
|
|
113
|
+
# @param state [StateMachines::State]
|
|
114
|
+
# @param states [Array<StateMachines::State>] pre-computed named states
|
|
115
|
+
# @return [Integer]
|
|
116
|
+
def state_integer(state, states)
|
|
117
|
+
value = state.value(false)
|
|
118
|
+
value.is_a?(::Integer) ? value : states.index(state)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
@@ -1,6 +1,9 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'state_machines-activemodel'
|
|
2
4
|
require 'active_record'
|
|
3
5
|
require 'state_machines/integrations/active_record/version'
|
|
6
|
+
require 'state_machines/integrations/active_record/type/integer'
|
|
4
7
|
|
|
5
8
|
module StateMachines
|
|
6
9
|
module Integrations # :nodoc:
|
|
@@ -194,7 +197,8 @@ module StateMachines
|
|
|
194
197
|
# example, assuming there's a validation on a field called +name+ on the class:
|
|
195
198
|
#
|
|
196
199
|
# vehicle = Vehicle.new
|
|
197
|
-
# vehicle.ignite! # => StateMachines::InvalidTransition: Cannot transition state via :ignite from :parked
|
|
200
|
+
# vehicle.ignite! # => StateMachines::InvalidTransition: Cannot transition state via :ignite from :parked
|
|
201
|
+
# # (Reason(s): Name cannot be blank)
|
|
198
202
|
#
|
|
199
203
|
# == Scopes
|
|
200
204
|
#
|
|
@@ -312,6 +316,12 @@ module StateMachines
|
|
|
312
316
|
# * (8) *after_transition*
|
|
313
317
|
# * (-) end transaction (if enabled)
|
|
314
318
|
# * (9) after_commit
|
|
319
|
+
# * (10) *after_transition* callbacks defined with <tt>:after_commit => true</tt>
|
|
320
|
+
#
|
|
321
|
+
# An +after_transition+ callback defined with the <tt>:after_commit</tt>
|
|
322
|
+
# option is deferred until the surrounding database transaction has been
|
|
323
|
+
# committed (and discarded if it rolls back). See the documentation for
|
|
324
|
+
# +after_transition+ in this integration for more details.
|
|
315
325
|
#
|
|
316
326
|
# == Internationalization
|
|
317
327
|
#
|
|
@@ -368,7 +378,440 @@ module StateMachines
|
|
|
368
378
|
|
|
369
379
|
# The default options to use for state machines using this integration
|
|
370
380
|
@defaults = { action: :save, use_transactions: true }
|
|
381
|
+
@auto_convert_integer_state_attributes = true
|
|
382
|
+
|
|
383
|
+
# Machine-specific methods for enum integration
|
|
384
|
+
module MachineMethods
|
|
385
|
+
# Enum integration metadata storage
|
|
386
|
+
attr_accessor :enum_integration
|
|
387
|
+
|
|
388
|
+
# Hook called after machine initialization
|
|
389
|
+
def after_initialize
|
|
390
|
+
super
|
|
391
|
+
initialize_enum_integration
|
|
392
|
+
|
|
393
|
+
register_integer_type if register_integer_type?
|
|
394
|
+
end
|
|
395
|
+
|
|
396
|
+
# Hook called when a machine is assigned to a class, including when an
|
|
397
|
+
# inherited machine is cloned for an STI subclass. The cloned machine
|
|
398
|
+
# carries the parent's @integer_type_registered flag while the subclass
|
|
399
|
+
# still inherits the parent's attribute type, which references the
|
|
400
|
+
# parent machine's state collection. Re-register so the subclass type
|
|
401
|
+
# sees this machine's (cloned) states, picking up subclass-added states
|
|
402
|
+
# as they are defined.
|
|
403
|
+
#
|
|
404
|
+
# @param klass [Class] the new owner class
|
|
405
|
+
# @return [Class] the assigned owner class
|
|
406
|
+
def owner_class=(klass)
|
|
407
|
+
super.tap do
|
|
408
|
+
register_integer_type if integer_type_registered?
|
|
409
|
+
end
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
# Check if enum integration should be enabled for this machine
|
|
413
|
+
def detect_enum_integration
|
|
414
|
+
return nil unless owner_class.defined_enums.key?(attribute.to_s)
|
|
415
|
+
|
|
416
|
+
# For now, auto-detect enum and enable basic integration
|
|
417
|
+
# Later we can add explicit configuration options
|
|
418
|
+
{
|
|
419
|
+
enabled: true,
|
|
420
|
+
prefix: true,
|
|
421
|
+
suffix: false,
|
|
422
|
+
scopes: true,
|
|
423
|
+
enum_values: owner_class.defined_enums[attribute.to_s] || {},
|
|
424
|
+
original_enum_methods: detect_existing_enum_methods,
|
|
425
|
+
state_machine_methods: []
|
|
426
|
+
}
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
# Initialize enum integration if enum is detected
|
|
430
|
+
def initialize_enum_integration
|
|
431
|
+
detected_config = detect_enum_integration
|
|
432
|
+
return unless detected_config
|
|
433
|
+
|
|
434
|
+
# Store enum integration metadata
|
|
435
|
+
self.enum_integration = detected_config
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
# Override state method to trigger method generation after states are defined
|
|
439
|
+
def state(*, &)
|
|
440
|
+
result = super
|
|
441
|
+
|
|
442
|
+
# Generate methods after each state addition if enum integration is enabled
|
|
443
|
+
generate_state_machine_methods if enum_integrated?
|
|
444
|
+
|
|
445
|
+
# States defined after initialization (e.g. adding an explicit value
|
|
446
|
+
# to the initial state) can change how the column default maps to
|
|
447
|
+
# states, so re-evaluate the conflicting-default warning
|
|
448
|
+
recheck_conflicting_attribute_default if integer_type_registered?
|
|
449
|
+
|
|
450
|
+
result
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
# Warns at most once when the column default conflicts with the
|
|
454
|
+
# machine's initial state. The conflict is re-evaluated as states are
|
|
455
|
+
# defined, so remember when the warning has been issued.
|
|
456
|
+
def check_conflicting_attribute_default
|
|
457
|
+
return if @attribute_default_conflict_warned
|
|
458
|
+
|
|
459
|
+
initial_state = states.detect(&:initial)
|
|
460
|
+
conflict = !owner_class_attribute_default.nil? && (
|
|
461
|
+
dynamic_initial_state? || !owner_class_attribute_default_matches?(initial_state)
|
|
462
|
+
)
|
|
463
|
+
return unless conflict
|
|
464
|
+
|
|
465
|
+
@attribute_default_conflict_warned = true
|
|
466
|
+
super
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
# Returns true when this machine should use the custom integer attribute type
|
|
470
|
+
# to convert between Ruby state names and integer database values. This only
|
|
471
|
+
# applies to non-enum integer columns when automatic conversion is enabled.
|
|
472
|
+
def register_integer_type?
|
|
473
|
+
StateMachines::Integrations::ActiveRecord.auto_convert_integer_state_attributes &&
|
|
474
|
+
integer_column? &&
|
|
475
|
+
!enum_integrated?
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
# Check if this machine has enum integration enabled
|
|
479
|
+
def enum_integrated?
|
|
480
|
+
enum_integration && enum_integration[:enabled]
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
# Get the enum mapping for this attribute
|
|
484
|
+
def enum_mapping
|
|
485
|
+
return {} unless enum_integrated?
|
|
486
|
+
|
|
487
|
+
enum_integration[:enum_values] || {}
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
# Get list of original enum methods that were preserved
|
|
491
|
+
def original_enum_methods
|
|
492
|
+
return [] unless enum_integrated?
|
|
493
|
+
|
|
494
|
+
enum_integration[:original_enum_methods] || []
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
# Get list of state machine methods that were generated
|
|
498
|
+
def state_machine_methods
|
|
499
|
+
return [] unless enum_integrated?
|
|
500
|
+
|
|
501
|
+
enum_integration[:state_machine_methods] || []
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
def integer_type_registered?
|
|
505
|
+
!!@integer_type_registered
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
# Creates a callback that will be invoked *after* a transition is
|
|
509
|
+
# performed, so long as the given requirements match the transition.
|
|
510
|
+
#
|
|
511
|
+
# In addition to the configuration options supported by the core
|
|
512
|
+
# +after_transition+ (see StateMachines::Machine#after_transition), the
|
|
513
|
+
# ActiveRecord integration supports:
|
|
514
|
+
# * <tt>:after_commit</tt> - Defer execution of the callback until the
|
|
515
|
+
# database transaction wrapping the transition has been committed.
|
|
516
|
+
# When no transaction is open at that point, the callback runs
|
|
517
|
+
# immediately. When the transaction (or an outer transaction wrapping
|
|
518
|
+
# it) is rolled back, the callback is discarded.
|
|
519
|
+
#
|
|
520
|
+
# This is the safe place to enqueue background jobs that reference the
|
|
521
|
+
# record (e.g. via GlobalID), since a regular +after_transition+ runs
|
|
522
|
+
# inside the transaction, before the record's changes are visible to
|
|
523
|
+
# other connections:
|
|
524
|
+
#
|
|
525
|
+
# class Vehicle < ApplicationRecord
|
|
526
|
+
# state_machine do
|
|
527
|
+
# after_transition on: :ignite, after_commit: true do |vehicle|
|
|
528
|
+
# EngineWarmupJob.perform_later(vehicle)
|
|
529
|
+
# end
|
|
530
|
+
#
|
|
531
|
+
# ...
|
|
532
|
+
# end
|
|
533
|
+
# end
|
|
534
|
+
#
|
|
535
|
+
# Note that a deferred callback cannot halt the callback chain or
|
|
536
|
+
# affect the result of the transition: by the time it runs, the
|
|
537
|
+
# transition has already been committed. For the same reason, an
|
|
538
|
+
# exception raised by a deferred callback is not propagated (doing so
|
|
539
|
+
# would revert the record's in-memory state even though the database
|
|
540
|
+
# was already updated); it is reported to +ActiveSupport.error_reporter+
|
|
541
|
+
# (+Rails.error+) instead. Conditions (<tt>:if</tt>/<tt>:unless</tt>)
|
|
542
|
+
# and state requirements are evaluated when the transition is
|
|
543
|
+
# performed, not at commit time.
|
|
544
|
+
#
|
|
545
|
+
# Like ActiveRecord's own +after_commit+, a surrounding
|
|
546
|
+
# <tt>transaction(joinable: false)</tt> wrapper is transparent: the
|
|
547
|
+
# callback fires at the inner commit. This is what makes it fire
|
|
548
|
+
# under transactional test fixtures.
|
|
549
|
+
def after_transition(*args, **options, &block)
|
|
550
|
+
# The flag may hide in a legacy trailing positional options hash
|
|
551
|
+
positional_options = args.last.is_a?(Hash) ? args.pop.dup : {}
|
|
552
|
+
options = positional_options.merge(options)
|
|
553
|
+
|
|
554
|
+
# Only a boolean is the flag — a non-boolean value is the implicit
|
|
555
|
+
# state-requirement form for a state named :after_commit
|
|
556
|
+
flag = options[:after_commit]
|
|
557
|
+
return super unless flag == true || flag == false
|
|
558
|
+
|
|
559
|
+
options.delete(:after_commit)
|
|
560
|
+
return super unless flag
|
|
561
|
+
|
|
562
|
+
# Method handling goes to a real Callback (reusing core's binding,
|
|
563
|
+
# arity and :do semantics); branch matching stays on the wrapper so
|
|
564
|
+
# conditions are evaluated at transition time
|
|
565
|
+
parsed = parse_callback_arguments(args, options)
|
|
566
|
+
method_options = parsed.slice(:do, :bind_to_object)
|
|
567
|
+
method_options[:terminator] = callback_terminator
|
|
568
|
+
branch_options = parsed.except(:do, :bind_to_object, :terminator)
|
|
569
|
+
|
|
570
|
+
deferred = Callback.new(:after, method_options, &block)
|
|
571
|
+
|
|
572
|
+
super(**branch_options, bind_to_object: false) do |object, transition|
|
|
573
|
+
object.class.current_transaction.after_commit do
|
|
574
|
+
# The transition's catch(:halt) is gone at commit time
|
|
575
|
+
catch(:halt) { deferred.call(object, {}, transition) }
|
|
576
|
+
rescue StandardError => e
|
|
577
|
+
# Raising would roll back in-memory state already committed; report instead
|
|
578
|
+
ActiveSupport.error_reporter.report(e, handled: false, source: 'state_machines-activerecord')
|
|
579
|
+
end
|
|
580
|
+
end
|
|
581
|
+
end
|
|
582
|
+
|
|
583
|
+
# Machine internals (state matching, validations) call read() to get the
|
|
584
|
+
# current state value and compare it against state.value. The custom
|
|
585
|
+
# integer type already returns the canonical value when state values are
|
|
586
|
+
# uniform: raw integers when every named state has an explicit integer
|
|
587
|
+
# value (passthrough), name strings when none do (state.value is the
|
|
588
|
+
# name). Only machines mixing explicit and auto-indexed values need an
|
|
589
|
+
# override, because the type returns name strings while the explicit
|
|
590
|
+
# states match on integers; map the stored value back to the matched
|
|
591
|
+
# state's canonical state.value.
|
|
592
|
+
#
|
|
593
|
+
# @param object [ActiveRecord::Base] record being read
|
|
594
|
+
# @param attr_sym [Symbol] attribute kind (:state, :event, ...)
|
|
595
|
+
# @param ivar [Boolean] whether to read from an instance variable
|
|
596
|
+
# @return [Object] a value machine internals can match on state.value
|
|
597
|
+
def read(object, attr_sym, ivar = false)
|
|
598
|
+
return super unless integer_type_registered? && attr_sym == :state
|
|
599
|
+
return super unless mixed_integer_state_values?
|
|
600
|
+
|
|
601
|
+
raw = object.read_attribute_before_type_cast(attribute.to_s)
|
|
602
|
+
if raw.is_a?(::String) || raw.is_a?(::Symbol)
|
|
603
|
+
matched = states.detect { |s| s.name && s.name.to_s == raw.to_s }
|
|
604
|
+
return matched.value if matched
|
|
605
|
+
end
|
|
606
|
+
|
|
607
|
+
name = owner_class.type_for_attribute(attribute.to_s).deserialize(raw)
|
|
608
|
+
matched = states.detect { |s| s.name && s.name.to_s == name.to_s }
|
|
609
|
+
matched ? matched.value : raw
|
|
610
|
+
end
|
|
611
|
+
|
|
612
|
+
private
|
|
613
|
+
|
|
614
|
+
# Re-runs the conflicting-default check for states defined after
|
|
615
|
+
# initialization. Skipped until an initial state exists (the DSL block
|
|
616
|
+
# evaluates before initial_state= during Machine.new).
|
|
617
|
+
#
|
|
618
|
+
# @return [void]
|
|
619
|
+
def recheck_conflicting_attribute_default
|
|
620
|
+
return unless states.detect(&:initial)
|
|
621
|
+
|
|
622
|
+
check_conflicting_attribute_default
|
|
623
|
+
end
|
|
624
|
+
|
|
625
|
+
# Returns true when named states mix explicit integer values with
|
|
626
|
+
# auto-indexed (name-valued) states. Uses value(false) so dynamic
|
|
627
|
+
# (Proc) state values are never evaluated for this metadata decision.
|
|
628
|
+
#
|
|
629
|
+
# @return [Boolean]
|
|
630
|
+
def mixed_integer_state_values?
|
|
631
|
+
named = states.select(&:name)
|
|
632
|
+
explicit = named.count { |s| s.value(false).is_a?(::Integer) }
|
|
633
|
+
explicit.positive? && explicit < named.size
|
|
634
|
+
end
|
|
635
|
+
|
|
636
|
+
# Returns true when the state machine attribute is backed by an integer column
|
|
637
|
+
def integer_column?
|
|
638
|
+
return false unless owner_class.respond_to?(:type_for_attribute)
|
|
639
|
+
return false unless owner_class.connected? && owner_class.table_exists?
|
|
640
|
+
|
|
641
|
+
owner_class.type_for_attribute(attribute.to_s).type == :integer
|
|
642
|
+
rescue ::ActiveRecord::StatementInvalid, ::ActiveRecord::ConnectionNotEstablished
|
|
643
|
+
false
|
|
644
|
+
end
|
|
645
|
+
|
|
646
|
+
# Registers a custom AR attribute type so that integer columns transparently
|
|
647
|
+
# convert between state name strings and stored integers.
|
|
648
|
+
# Saves the raw column default first so the conflicting-default check
|
|
649
|
+
# (which fires later, during initial_state=) still compares raw integers.
|
|
650
|
+
def register_integer_type
|
|
651
|
+
@raw_integer_column_default = owner_class.column_defaults[attribute.to_s]
|
|
652
|
+
@integer_type_registered = true
|
|
653
|
+
|
|
654
|
+
# When re-registering (e.g. for an STI subclass that inherited the
|
|
655
|
+
# parent's custom type), unwrap it to keep the column's original type
|
|
656
|
+
# as the passthrough delegate.
|
|
657
|
+
current_type = owner_class.type_for_attribute(attribute.to_s)
|
|
658
|
+
raw_type = current_type.is_a?(StateMachines::Type::Integer) ? current_type.raw_type : current_type
|
|
659
|
+
|
|
660
|
+
owner_class.attribute(attribute.to_s, StateMachines::Type::Integer.new(states, raw_type: raw_type))
|
|
661
|
+
end
|
|
662
|
+
|
|
663
|
+
# Detect existing enum methods for this attribute
|
|
664
|
+
def detect_existing_enum_methods
|
|
665
|
+
return [] unless owner_class.defined_enums.key?(attribute.to_s)
|
|
666
|
+
|
|
667
|
+
enum_values = owner_class.defined_enums[attribute.to_s]
|
|
668
|
+
methods = []
|
|
669
|
+
|
|
670
|
+
enum_values.each_key do |value|
|
|
671
|
+
# Predicate methods like 'active?'
|
|
672
|
+
predicate = "#{value}?"
|
|
673
|
+
methods << predicate if owner_class.method_defined?(predicate)
|
|
674
|
+
|
|
675
|
+
# Bang methods like 'active!'
|
|
676
|
+
bang_method = "#{value}!"
|
|
677
|
+
methods << bang_method if owner_class.method_defined?(bang_method)
|
|
678
|
+
|
|
679
|
+
# Scope methods (class-level)
|
|
680
|
+
methods << value.to_s if owner_class.respond_to?(value)
|
|
681
|
+
methods << "not_#{value}" if owner_class.respond_to?("not_#{value}")
|
|
682
|
+
end
|
|
683
|
+
|
|
684
|
+
methods
|
|
685
|
+
end
|
|
686
|
+
|
|
687
|
+
# Generate method name with prefix/suffix based on configuration
|
|
688
|
+
def generate_state_method_name(state_name, method_type)
|
|
689
|
+
return state_name unless enum_integrated?
|
|
690
|
+
|
|
691
|
+
config = enum_integration
|
|
692
|
+
base_name = case method_type
|
|
693
|
+
when :predicate
|
|
694
|
+
"#{state_name}?"
|
|
695
|
+
when :bang
|
|
696
|
+
"#{state_name}!"
|
|
697
|
+
else
|
|
698
|
+
state_name.to_s
|
|
699
|
+
end
|
|
700
|
+
|
|
701
|
+
# Apply prefix
|
|
702
|
+
if config[:prefix]
|
|
703
|
+
prefix = config[:prefix] == true ? "#{attribute}_" : "#{config[:prefix]}_"
|
|
704
|
+
base_name = "#{prefix}#{base_name}"
|
|
705
|
+
end
|
|
706
|
+
|
|
707
|
+
# Apply suffix
|
|
708
|
+
if config[:suffix]
|
|
709
|
+
suffix = config[:suffix] == true ? "_#{attribute}" : "_#{config[:suffix]}"
|
|
710
|
+
base_name = base_name.gsub(/(\?|!)$/, "#{suffix}\\1")
|
|
711
|
+
base_name = "#{base_name}#{suffix}" unless base_name.end_with?('?', '!')
|
|
712
|
+
end
|
|
713
|
+
|
|
714
|
+
base_name
|
|
715
|
+
end
|
|
716
|
+
|
|
717
|
+
# Generate state machine methods with conflict resolution
|
|
718
|
+
def generate_state_machine_methods
|
|
719
|
+
return unless enum_integrated?
|
|
720
|
+
|
|
721
|
+
# Initialize tracking if not already done
|
|
722
|
+
@processed_states ||= Set.new
|
|
723
|
+
enum_integration[:state_machine_methods] ||= []
|
|
724
|
+
|
|
725
|
+
# Get all states for this machine
|
|
726
|
+
states.each do |state|
|
|
727
|
+
state_name = state.name.to_s
|
|
728
|
+
next if state.nil? # Skip nil state
|
|
729
|
+
next if @processed_states.include?(state_name) # Skip already processed states
|
|
730
|
+
|
|
731
|
+
# Generate predicate method (e.g., status_pending?)
|
|
732
|
+
predicate_method = generate_state_method_name(state_name, :predicate)
|
|
733
|
+
if predicate_method != "#{state_name}?"
|
|
734
|
+
define_state_predicate_method(state_name, predicate_method)
|
|
735
|
+
track_generated_method(predicate_method)
|
|
736
|
+
end
|
|
737
|
+
|
|
738
|
+
# Generate bang method (e.g., status_pending!)
|
|
739
|
+
bang_method = generate_state_method_name(state_name, :bang)
|
|
740
|
+
if bang_method != "#{state_name}!"
|
|
741
|
+
define_state_bang_method(state_name, bang_method)
|
|
742
|
+
track_generated_method(bang_method)
|
|
743
|
+
end
|
|
744
|
+
|
|
745
|
+
# Generate scope methods (e.g., status_pending) if scopes are enabled
|
|
746
|
+
if enum_integration[:scopes]
|
|
747
|
+
scope_method = generate_state_method_name(state_name, :scope)
|
|
748
|
+
if scope_method != state_name
|
|
749
|
+
define_state_scope_method(state_name, scope_method)
|
|
750
|
+
track_generated_method(scope_method)
|
|
751
|
+
end
|
|
752
|
+
end
|
|
753
|
+
|
|
754
|
+
# Mark this state as processed
|
|
755
|
+
@processed_states.add(state_name)
|
|
756
|
+
end
|
|
757
|
+
end
|
|
758
|
+
|
|
759
|
+
# Define a prefixed predicate method for a state
|
|
760
|
+
def define_state_predicate_method(state_name, method_name)
|
|
761
|
+
machine_attribute = attribute
|
|
762
|
+
target_state_name = state_name.to_sym
|
|
763
|
+
owner_class.define_method(method_name) do
|
|
764
|
+
machine = self.class.state_machine(machine_attribute)
|
|
765
|
+
machine.states.matches?(self, target_state_name)
|
|
766
|
+
end
|
|
767
|
+
end
|
|
768
|
+
|
|
769
|
+
# Define a prefixed bang method for a state
|
|
770
|
+
def define_state_bang_method(state_name, method_name)
|
|
771
|
+
owner_class.define_method(method_name) do
|
|
772
|
+
# Raise an error with actionable guidance
|
|
773
|
+
raise "#{method_name} is a conflict-resolution placeholder. " \
|
|
774
|
+
"Use the original enum method '#{state_name}!' or state machine events instead."
|
|
775
|
+
end
|
|
776
|
+
end
|
|
777
|
+
|
|
778
|
+
# Define a prefixed scope method for a state
|
|
779
|
+
def define_state_scope_method(state_name, method_name)
|
|
780
|
+
machine_attribute = attribute
|
|
781
|
+
scope_lambda = lambda do |value = true|
|
|
782
|
+
machine = state_machine(machine_attribute)
|
|
783
|
+
state_value = machine.states[state_name.to_sym].value
|
|
784
|
+
if value
|
|
785
|
+
where(machine_attribute => state_value)
|
|
786
|
+
else
|
|
787
|
+
where.not(machine_attribute => state_value)
|
|
788
|
+
end
|
|
789
|
+
end
|
|
790
|
+
|
|
791
|
+
owner_class.define_singleton_method(method_name, &scope_lambda)
|
|
792
|
+
owner_class.define_singleton_method("not_#{method_name}") do
|
|
793
|
+
public_send(method_name, false)
|
|
794
|
+
end
|
|
795
|
+
end
|
|
796
|
+
|
|
797
|
+
# Track generated state machine methods for introspection
|
|
798
|
+
def track_generated_method(method_name)
|
|
799
|
+
return unless enum_integrated?
|
|
800
|
+
|
|
801
|
+
# Use a Set to ensure no duplicates
|
|
802
|
+
enum_integration[:state_machine_methods] ||= []
|
|
803
|
+
return if enum_integration[:state_machine_methods].include?(method_name)
|
|
804
|
+
|
|
805
|
+
enum_integration[:state_machine_methods] << method_name
|
|
806
|
+
end
|
|
807
|
+
end
|
|
808
|
+
|
|
809
|
+
# Include MachineMethods to make enum integration methods available on machine instances
|
|
810
|
+
include MachineMethods
|
|
811
|
+
|
|
371
812
|
class << self
|
|
813
|
+
attr_accessor :auto_convert_integer_state_attributes
|
|
814
|
+
|
|
372
815
|
# Classes that inherit from ActiveRecord::Base will automatically use
|
|
373
816
|
# the ActiveRecord integration.
|
|
374
817
|
def matching_ancestors
|
|
@@ -383,13 +826,32 @@ module StateMachines
|
|
|
383
826
|
action == :save
|
|
384
827
|
end
|
|
385
828
|
|
|
386
|
-
# Gets the db default for the machine's attribute
|
|
829
|
+
# Gets the db default for the machine's attribute.
|
|
830
|
+
# For integer columns the raw pre-type-registration default is returned so
|
|
831
|
+
# that check_conflicting_attribute_default can compare integers to integers.
|
|
387
832
|
def owner_class_attribute_default
|
|
833
|
+
return @raw_integer_column_default if defined?(@raw_integer_column_default)
|
|
388
834
|
return unless owner_class.connected? && owner_class.table_exists?
|
|
389
835
|
|
|
390
836
|
owner_class.column_defaults[attribute.to_s]
|
|
391
837
|
end
|
|
392
838
|
|
|
839
|
+
# Checks whether the given state matches the column default. When the
|
|
840
|
+
# custom integer type is registered, auto-indexed states match on their
|
|
841
|
+
# name string while the column default is a raw integer, so the default
|
|
842
|
+
# is also compared through the type's mapping (e.g. 0 matches the first
|
|
843
|
+
# auto-indexed state).
|
|
844
|
+
#
|
|
845
|
+
# @param state [StateMachines::State] the state to compare (the initial state)
|
|
846
|
+
# @return [Boolean] whether the column default represents this state
|
|
847
|
+
def owner_class_attribute_default_matches?(state)
|
|
848
|
+
matches = super
|
|
849
|
+
return matches if matches || !integer_type_registered?
|
|
850
|
+
|
|
851
|
+
default = owner_class_attribute_default
|
|
852
|
+
state.matches?(owner_class.type_for_attribute(attribute.to_s).deserialize(default))
|
|
853
|
+
end
|
|
854
|
+
|
|
393
855
|
def define_state_initializer
|
|
394
856
|
define_helper :instance, <<-END_EVAL, __FILE__, __LINE__ + 1
|
|
395
857
|
def initialize(attributes = nil, *)
|
|
@@ -434,7 +896,7 @@ module StateMachines
|
|
|
434
896
|
|
|
435
897
|
# Creates a scope for finding records *with* a particular state or
|
|
436
898
|
# states for the attribute
|
|
437
|
-
def create_with_scope(
|
|
899
|
+
def create_with_scope(_name)
|
|
438
900
|
attr_name = attribute
|
|
439
901
|
lambda do |klass, values|
|
|
440
902
|
if values.present?
|
|
@@ -447,7 +909,7 @@ module StateMachines
|
|
|
447
909
|
|
|
448
910
|
# Creates a scope for finding records *without* a particular state or
|
|
449
911
|
# states for the attribute
|
|
450
|
-
def create_without_scope(
|
|
912
|
+
def create_without_scope(_name)
|
|
451
913
|
attr_name = attribute
|
|
452
914
|
lambda do |klass, values|
|
|
453
915
|
if values.present?
|
|
@@ -458,14 +920,13 @@ module StateMachines
|
|
|
458
920
|
end
|
|
459
921
|
end
|
|
460
922
|
|
|
461
|
-
|
|
462
923
|
# Runs a new database transaction, rolling back any changes by raising
|
|
463
924
|
# an ActiveRecord::Rollback exception if the yielded block fails
|
|
464
925
|
# (i.e. returns false).
|
|
465
926
|
def transaction(object)
|
|
466
927
|
result = nil
|
|
467
928
|
object.class.transaction do
|
|
468
|
-
raise ::ActiveRecord::Rollback unless result = yield
|
|
929
|
+
raise ::ActiveRecord::Rollback unless (result = yield)
|
|
469
930
|
end
|
|
470
931
|
result
|
|
471
932
|
end
|
|
@@ -476,13 +937,6 @@ module StateMachines
|
|
|
476
937
|
|
|
477
938
|
private
|
|
478
939
|
|
|
479
|
-
|
|
480
|
-
# Generates the results for the given scope based on one or more states to filter by
|
|
481
|
-
def run_scope(scope, machine, klass, states)
|
|
482
|
-
values = states.flatten.compact.map { |state| machine.states.fetch(state).value }
|
|
483
|
-
scope.call(klass, values)
|
|
484
|
-
end
|
|
485
|
-
|
|
486
940
|
# ActiveModel's use of method_missing / respond_to for attribute methods
|
|
487
941
|
# breaks both ancestor lookups and defined?(super). Need to special-case
|
|
488
942
|
# the existence of query attribute methods.
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: state_machines-activerecord
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.200.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Abdelkader Boudih
|
|
@@ -30,14 +30,14 @@ dependencies:
|
|
|
30
30
|
requirements:
|
|
31
31
|
- - ">="
|
|
32
32
|
- !ruby/object:Gem::Version
|
|
33
|
-
version: 0.
|
|
33
|
+
version: 0.200.0
|
|
34
34
|
type: :runtime
|
|
35
35
|
prerelease: false
|
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
|
37
37
|
requirements:
|
|
38
38
|
- - ">="
|
|
39
39
|
- !ruby/object:Gem::Version
|
|
40
|
-
version: 0.
|
|
40
|
+
version: 0.200.0
|
|
41
41
|
- !ruby/object:Gem::Dependency
|
|
42
42
|
name: appraisal
|
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -56,16 +56,16 @@ dependencies:
|
|
|
56
56
|
name: minitest
|
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
|
58
58
|
requirements:
|
|
59
|
-
- -
|
|
59
|
+
- - '='
|
|
60
60
|
- !ruby/object:Gem::Version
|
|
61
|
-
version: 5.
|
|
61
|
+
version: 5.27.0
|
|
62
62
|
type: :development
|
|
63
63
|
prerelease: false
|
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
|
65
65
|
requirements:
|
|
66
|
-
- -
|
|
66
|
+
- - '='
|
|
67
67
|
- !ruby/object:Gem::Version
|
|
68
|
-
version: 5.
|
|
68
|
+
version: 5.27.0
|
|
69
69
|
- !ruby/object:Gem::Dependency
|
|
70
70
|
name: minitest-reporters
|
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -100,14 +100,14 @@ dependencies:
|
|
|
100
100
|
requirements:
|
|
101
101
|
- - "~>"
|
|
102
102
|
- !ruby/object:Gem::Version
|
|
103
|
-
version: '1
|
|
103
|
+
version: '2.1'
|
|
104
104
|
type: :development
|
|
105
105
|
prerelease: false
|
|
106
106
|
version_requirements: !ruby/object:Gem::Requirement
|
|
107
107
|
requirements:
|
|
108
108
|
- - "~>"
|
|
109
109
|
- !ruby/object:Gem::Version
|
|
110
|
-
version: '1
|
|
110
|
+
version: '2.1'
|
|
111
111
|
description: Adds support for creating state machines for attributes on ActiveRecord
|
|
112
112
|
email:
|
|
113
113
|
- terminale@gmail.com
|
|
@@ -121,6 +121,7 @@ files:
|
|
|
121
121
|
- lib/state_machines-activerecord.rb
|
|
122
122
|
- lib/state_machines/integrations/active_record.rb
|
|
123
123
|
- lib/state_machines/integrations/active_record/locale.rb
|
|
124
|
+
- lib/state_machines/integrations/active_record/type/integer.rb
|
|
124
125
|
- lib/state_machines/integrations/active_record/version.rb
|
|
125
126
|
homepage: https://github.com/state-machines/state_machines-activerecord/
|
|
126
127
|
licenses:
|
|
@@ -141,7 +142,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
141
142
|
- !ruby/object:Gem::Version
|
|
142
143
|
version: '0'
|
|
143
144
|
requirements: []
|
|
144
|
-
rubygems_version:
|
|
145
|
+
rubygems_version: 4.0.10
|
|
145
146
|
specification_version: 4
|
|
146
147
|
summary: State machines Active Record Integration
|
|
147
148
|
test_files: []
|