state_machines-activerecord 0.8.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.
Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE.txt +1 -1
  3. data/README.md +178 -8
  4. data/lib/state_machines/integrations/active_record/locale.rb +12 -9
  5. data/lib/state_machines/integrations/active_record/type/integer.rb +122 -0
  6. data/lib/state_machines/integrations/active_record/version.rb +3 -1
  7. data/lib/state_machines/integrations/active_record.rb +548 -181
  8. data/lib/state_machines-activerecord.rb +2 -0
  9. metadata +37 -154
  10. data/.gitignore +0 -22
  11. data/.travis.yml +0 -19
  12. data/Appraisals +0 -34
  13. data/Gemfile +0 -6
  14. data/Rakefile +0 -9
  15. data/gemfiles/active_record_5.1.gemfile +0 -14
  16. data/gemfiles/active_record_5.2.gemfile +0 -14
  17. data/gemfiles/active_record_6.0.gemfile +0 -14
  18. data/gemfiles/active_record_6.1.gemfile +0 -14
  19. data/gemfiles/active_record_edge.gemfile +0 -14
  20. data/log/.gitkeep +0 -0
  21. data/state_machines-activerecord.gemspec +0 -28
  22. data/test/files/en.yml +0 -5
  23. data/test/files/models/post.rb +0 -11
  24. data/test/integration_test.rb +0 -25
  25. data/test/machine_by_default_test.rb +0 -16
  26. data/test/machine_errors_test.rb +0 -19
  27. data/test/machine_multiple_test.rb +0 -17
  28. data/test/machine_nested_action_test.rb +0 -38
  29. data/test/machine_unmigrated_test.rb +0 -14
  30. data/test/machine_with_aliased_attribute_test.rb +0 -23
  31. data/test/machine_with_callbacks_test.rb +0 -172
  32. data/test/machine_with_column_state_attribute_test.rb +0 -44
  33. data/test/machine_with_complex_pluralization_scopes_test.rb +0 -16
  34. data/test/machine_with_conflicting_predicate_test.rb +0 -18
  35. data/test/machine_with_conflicting_state_name_test.rb +0 -29
  36. data/test/machine_with_custom_attribute_test.rb +0 -21
  37. data/test/machine_with_default_scope_test.rb +0 -18
  38. data/test/machine_with_different_column_default_test.rb +0 -27
  39. data/test/machine_with_different_integer_column_default_test.rb +0 -29
  40. data/test/machine_with_dirty_attribute_and_custom_attributes_during_loopback_test.rb +0 -24
  41. data/test/machine_with_dirty_attribute_and_state_events_test.rb +0 -20
  42. data/test/machine_with_dirty_attributes_and_custom_attribute_test.rb +0 -32
  43. data/test/machine_with_dirty_attributes_during_loopback_test.rb +0 -22
  44. data/test/machine_with_dirty_attributes_test.rb +0 -35
  45. data/test/machine_with_dynamic_initial_state_test.rb +0 -99
  46. data/test/machine_with_event_attributes_on_autosave_test.rb +0 -48
  47. data/test/machine_with_event_attributes_on_custom_action_test.rb +0 -41
  48. data/test/machine_with_event_attributes_on_save_bang_test.rb +0 -82
  49. data/test/machine_with_event_attributes_on_save_test.rb +0 -244
  50. data/test/machine_with_event_attributes_on_validation_test.rb +0 -143
  51. data/test/machine_with_events_test.rb +0 -13
  52. data/test/machine_with_failed_action_test.rb +0 -40
  53. data/test/machine_with_failed_after_callbacks_test.rb +0 -35
  54. data/test/machine_with_failed_before_callbacks_test.rb +0 -36
  55. data/test/machine_with_initialized_state_test.rb +0 -41
  56. data/test/machine_with_internationalization_test.rb +0 -180
  57. data/test/machine_with_loopback_test.rb +0 -22
  58. data/test/machine_with_non_column_state_attribute_defined_test.rb +0 -29
  59. data/test/machine_with_same_column_default_test.rb +0 -26
  60. data/test/machine_with_same_integer_column_default_test.rb +0 -30
  61. data/test/machine_with_scopes_and_joins_test.rb +0 -37
  62. data/test/machine_with_scopes_and_owner_subclass_test.rb +0 -27
  63. data/test/machine_with_scopes_test.rb +0 -70
  64. data/test/machine_with_state_driven_validations_test.rb +0 -30
  65. data/test/machine_with_states_test.rb +0 -13
  66. data/test/machine_with_static_initial_state_test.rb +0 -166
  67. data/test/machine_with_transactions_test.rb +0 -26
  68. data/test/machine_with_validations_and_custom_attribute_test.rb +0 -21
  69. data/test/machine_with_validations_test.rb +0 -47
  70. data/test/machine_without_database_test.rb +0 -20
  71. data/test/machine_without_transactions_test.rb +0 -26
  72. data/test/model_test.rb +0 -12
  73. data/test/test_helper.rb +0 -52
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d4d6c3db18b13bbece06c33f047109e49d17eb1b97e8d26d8e001ed02a554b7a
4
- data.tar.gz: ebd085709c837eb3c76994e23780b884136a338f92b63eb77c8c64620bd1980e
3
+ metadata.gz: f022e63c2bfd6af186733c95cbfc19cb7f25dfff128c791e5199b1a3f4e6d760
4
+ data.tar.gz: '094b3a2befdd974fa1828e9237f75f0a5a38319b7e38b56b8a3dda376a1e797d'
5
5
  SHA512:
6
- metadata.gz: febb1df5b304114be86875716fb620b7c1b7fe1d0d44bea3c52c92451345e2cf2fb57e11cbd87d9b277079a52ebb9f82734d4174ea6722e8c6d933d274f88f6d
7
- data.tar.gz: 3ee22472203164647a9d65ef4a5df8edfeda3874144b57281b9dce79ae210006f171bc02fdcdd24d44afd08ed4b9dff409b61d443a61052a1baeee27d8f3ed16
6
+ metadata.gz: cfb9f8bca11c37eeaff38ff0a237c2cc241986bba88ee4a8859199ea05ee7d06b4449da73574e0125f678de3082f3ba4e472f491f0efe9100fc2b585cac5e818
7
+ data.tar.gz: 8fb449a5ead40d4cf43e92208d307bb34f557e9aae068cda123c968ded28aecfa7a97c416426b04f99afd2550a50b54f852bc6e04c5ab6dc4fef62199d5e615d
data/LICENSE.txt CHANGED
@@ -1,5 +1,5 @@
1
1
  Copyright (c) 2006-2012 Aaron Pfeifer
2
- Copyright (c) 2014-2021 Abdelkader Boudih
2
+ Copyright (c) 2014-2025 Abdelkader Boudih
3
3
 
4
4
  MIT License
5
5
 
data/README.md CHANGED
@@ -1,11 +1,15 @@
1
- [![Build Status](https://travis-ci.com/state-machines/state_machines-activerecord.svg?branch=master)](https://travis-ci.com/state-machines/state_machines-activerecord)
2
- [![Code Climate](https://codeclimate.com/github/state-machines/state_machines-activerecord.svg)](https://codeclimate.com/github/state-machines/state_machines-activerecord)
1
+ [![Build Status](https://github.com/state-machines/state_machines-activerecord/actions/workflows/ruby.yml/badge.svg)](https://github.com/state-machines/state_machines-activerecord/actions/workflows/ruby.yml)
3
2
 
4
3
  # StateMachines Active Record Integration
5
4
 
6
- The Active Record 5.1+ integration adds support for database transactions, automatically
5
+ The Active Record 7.2+ integration adds support for database transactions, automatically
7
6
  saving the record, named scopes, validation errors.
8
7
 
8
+ ## Requirements
9
+
10
+ - Ruby 3.2+
11
+ - Rails 7.2+
12
+
9
13
  ## Installation
10
14
 
11
15
  Add this line to your application's Gemfile:
@@ -27,7 +31,7 @@ For the complete usage guide, see http://www.rubydoc.info/github/state-machines/
27
31
  ### Example
28
32
 
29
33
  ```ruby
30
- class Vehicle < ActiveRecord::Base
34
+ class Vehicle < ApplicationRecord
31
35
  state_machine :initial => :parked do
32
36
  before_transition :parked => any - :parked, :do => :put_on_seatbelt
33
37
  after_transition any => :parked do |vehicle, transition|
@@ -40,7 +44,7 @@ class Vehicle < ActiveRecord::Base
40
44
  end
41
45
 
42
46
  state :first_gear, :second_gear do
43
- validates_presence_of :seatbelt_on
47
+ validates :seatbelt_on, presence: true
44
48
  end
45
49
  end
46
50
 
@@ -64,7 +68,173 @@ Vehicle.with_state(:parked) # also plural #with_states
64
68
  Vehicle.without_states(:first_gear, :second_gear) # also singular #without_state
65
69
  ```
66
70
 
67
- ### State driven validations
71
+ #### Transparent Scopes
72
+ State scopes will return all records when `nil` is passed, making them perfect for search filters:
73
+
74
+ ```ruby
75
+ Vehicle.with_state(nil) # Returns all vehicles
76
+ Vehicle.with_state(params[:state]) # Returns all vehicles if params[:state] is nil
77
+ Vehicle.where(color: 'red').with_state(nil) # Returns all red vehicles (chainable)
78
+ ```
79
+
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
68
238
 
69
239
  As mentioned in `StateMachines::Machine#state`, you can define behaviors,
70
240
  like validations, that only execute for certain states. One *important*
@@ -73,7 +243,7 @@ framework, custom validators will not work as expected when defined to run
73
243
  in multiple states. For example:
74
244
 
75
245
  ```ruby
76
- class Vehicle < ActiveRecord::Base
246
+ class Vehicle < ApplicationRecord
77
247
  state_machine do
78
248
  state :first_gear, :second_gear do
79
249
  validate :speed_is_legal
@@ -87,7 +257,7 @@ for the <tt>:second_gear</tt> state. To avoid this, you can define your
87
257
  custom validation like so:
88
258
 
89
259
  ```ruby
90
- class Vehicle < ActiveRecord::Base
260
+ class Vehicle < ApplicationRecord
91
261
  state_machine do
92
262
  state :first_gear, :second_gear do
93
263
  validate {|vehicle| vehicle.speed_is_legal}
@@ -1,12 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Use lazy evaluation to avoid circular dependencies with frozen default_messages
4
+ # This ensures messages can be updated after gem loading while maintaining thread safety
1
5
  { en: {
2
- activerecord: {
3
- errors: {
4
- messages: {
5
- invalid: StateMachines::Machine.default_messages[:invalid],
6
- invalid_event: StateMachines::Machine.default_messages[:invalid_event] % ['%{state}'],
7
- invalid_transition: StateMachines::Machine.default_messages[:invalid_transition] % ['%{event}']
8
- }
9
- }
6
+ activerecord: {
7
+ errors: {
8
+ messages: {
9
+ invalid: ->(*) { StateMachines::Machine.default_messages[:invalid] },
10
+ invalid_event: ->(*) { format(StateMachines::Machine.default_messages[:invalid_event], '%<state>s') },
11
+ invalid_transition: ->(*) { format(StateMachines::Machine.default_messages[:invalid_transition], '%<event>s') }
12
+ }
10
13
  }
14
+ }
11
15
  } }
12
-
@@ -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,7 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module StateMachines
2
4
  module Integrations
3
5
  module ActiveRecord
4
- VERSION = '0.8.0'
6
+ VERSION = '0.200.0'
5
7
  end
6
8
  end
7
9
  end