state_machines-activemodel 0.10.0 → 0.31.1

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: 8a37766e74933c9b284c1bea7acc586937d437222fb51371a12118cb9c28873c
4
- data.tar.gz: 05d7748697762082b5cd5c6a388cfbcdb62d7f54bdaf656587c7cdc9158b78f6
3
+ metadata.gz: 83ec0c17ef0c2f530f146fd28b90e6474a9ff1fde5e37c07f143652d65c60375
4
+ data.tar.gz: 9d3c8fb5814ed1e8bf03f543aa9c25ef7ff0b8a241cb05dd20d7b64f15a039fb
5
5
  SHA512:
6
- metadata.gz: 49059888466d7e77da042aa4d3b296d029d3bbaf600ef70cf514322a04c14211460527444d08fd591a8b3e38a3039b1119c3c719d06bdc6363ab03dadf83b7c0
7
- data.tar.gz: 787f045227838e8ce24c6b5448d991631ef1a35c90f078582f8f2fff66470760d90d0c05f2bc56c174dcb93f4c4662b137100d06ff27e44aefca98a4d4b7f11e
6
+ metadata.gz: f47b157cadbf59127defff4c9f9dade32957b98d439be01963718abe482b0adf638ee8e273347e28fbef56598abbc9e91bfe4d1384f803569b42472abf190d25
7
+ data.tar.gz: 0cd4c365cf6d108b219bd913598b9bfe1425d7fc9cd62a86961b865cea29483480d0421af07db63daa016050135c44da7578c2cf4131e9ab23ff292b5dd75111
data/README.md CHANGED
@@ -1,5 +1,4 @@
1
1
  ![Build Status](https://github.com/state-machines/state_machines-activemodel/actions/workflows/ruby.yml/badge.svg)
2
- [![Code Climate](https://codeclimate.com/github/state-machines/state_machines-activemodel.svg)](https://codeclimate.com/github/state-machines/state_machines-activemodel)
3
2
 
4
3
  # StateMachines ActiveModel Integration
5
4
 
@@ -1,12 +1,14 @@
1
1
  # frozen_string_literal: true
2
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
3
5
  { en: {
4
6
  activemodel: {
5
7
  errors: {
6
8
  messages: {
7
- invalid: StateMachines::Machine.default_messages[:invalid],
8
- invalid_event: StateMachines::Machine.default_messages[:invalid_event] % ['%{state}'],
9
- invalid_transition: StateMachines::Machine.default_messages[:invalid_transition] % ['%{event}']
9
+ invalid: lambda { |*| StateMachines::Machine.default_messages[:invalid] },
10
+ invalid_event: lambda { |*| StateMachines::Machine.default_messages[:invalid_event] % ['%{state}'] },
11
+ invalid_transition: lambda { |*| StateMachines::Machine.default_messages[:invalid_transition] % ['%{event}'] }
10
12
  }
11
13
  }
12
14
  }
@@ -3,7 +3,7 @@
3
3
  module StateMachines
4
4
  module Integrations
5
5
  module ActiveModel
6
- VERSION = '0.10.0'
6
+ VERSION = '0.31.1'
7
7
  end
8
8
  end
9
9
  end
@@ -8,7 +8,7 @@ require 'state_machines/integrations/base'
8
8
  require 'state_machines/integrations/active_model/version'
9
9
 
10
10
  module StateMachines
11
- module Integrations #:nodoc:
11
+ module Integrations # :nodoc:
12
12
  # Adds support for integrating state machines with ActiveModel classes.
13
13
  #
14
14
  # == Examples
@@ -324,13 +324,13 @@ module StateMachines
324
324
 
325
325
  # Adds a validation error to the given object
326
326
  def invalidate(object, attribute, message, values = [])
327
- if supports_validations?
328
- attribute = self.attribute(attribute)
329
- options = values.to_h { |key, value| [key, value] }
327
+ return unless supports_validations?
330
328
 
331
- default_options = default_error_message_options(object, attribute, message)
332
- object.errors.add(attribute, message, **options, **default_options)
333
- end
329
+ attribute = self.attribute(attribute)
330
+ options = values.to_h
331
+
332
+ default_options = default_error_message_options(object, attribute, message)
333
+ object.errors.add(attribute, message, **options, **default_options)
334
334
  end
335
335
 
336
336
  # Describes the current validation errors on the given object. If none
@@ -345,22 +345,32 @@ module StateMachines
345
345
  end
346
346
 
347
347
  # Runs state events around the object's validation process
348
- def around_validation(object)
349
- object.class.state_machines.transitions(object, action, after: false).perform { yield }
348
+ def around_validation(object, &)
349
+ object.class.state_machines.transitions(object, action, after: false).perform(&)
350
350
  end
351
351
 
352
352
  protected
353
353
 
354
354
  def define_state_initializer
355
- define_helper :instance, <<-end_eval, __FILE__, __LINE__ + 1
356
- def initialize(params = {})
357
- params.transform_keys! do |key|
355
+ define_helper :instance, <<-END_EVAL, __FILE__, __LINE__ + 1
356
+ def initialize(params = nil, **kwargs)
357
+ # Support both positional hash and keyword arguments
358
+ attrs = params.nil? ? kwargs : params
359
+ #{' '}
360
+ attrs.transform_keys! do |key|
358
361
  self.class.attribute_aliases[key.to_s] || key.to_s
359
362
  end if self.class.respond_to?(:attribute_aliases)
360
363
 
361
- self.class.state_machines.initialize_states(self, {}, params) { super }
364
+ # Call super with the appropriate arguments based on what we received
365
+ self.class.state_machines.initialize_states(self, {}, attrs) do
366
+ if params
367
+ super(params)
368
+ else
369
+ super(**kwargs)
370
+ end
371
+ end
362
372
  end
363
- end_eval
373
+ END_EVAL
364
374
  end
365
375
 
366
376
  # Whether validations are supported in the integration. Only true if
@@ -378,7 +388,7 @@ module StateMachines
378
388
 
379
389
  # Gets the terminator to use for callbacks
380
390
  def callback_terminator
381
- @terminator ||= ->(result) { result == false }
391
+ @callback_terminator ||= ->(result) { result == false }
382
392
  end
383
393
 
384
394
  # Determines the base scope to use when looking up translations
@@ -408,7 +418,7 @@ module StateMachines
408
418
  # Generate all possible translation keys
409
419
  translations = ancestors.map { |ancestor| :"#{ancestor.model_name.to_s.underscore}.#{name}.#{group}.#{value}" }
410
420
  translations.concat(ancestors.map { |ancestor| :"#{ancestor.model_name.to_s.underscore}.#{group}.#{value}" })
411
- translations.concat([:"#{name}.#{group}.#{value}", :"#{group}.#{value}", value.humanize.downcase])
421
+ translations.push(:"#{name}.#{group}.#{value}", :"#{group}.#{value}", value.humanize.downcase)
412
422
  I18n.translate(translations.shift, default: translations, scope: [i18n_scope(klass), :state_machines])
413
423
  end
414
424
 
@@ -422,10 +432,12 @@ module StateMachines
422
432
  def define_state_accessor
423
433
  name = self.name
424
434
 
435
+ return unless supports_validations?
436
+
425
437
  owner_class.validates_each(attribute) do |object|
426
438
  machine = object.class.state_machine(name)
427
439
  machine.invalidate(object, :state, :invalid) unless machine.states.match(object)
428
- end if supports_validations?
440
+ end
429
441
  end
430
442
 
431
443
  # Adds hooks into validation for automatically firing events
@@ -443,7 +455,7 @@ module StateMachines
443
455
  # Creates a new callback in the callback chain, always inserting it
444
456
  # before the default Observer callbacks that were created after
445
457
  # initialization.
446
- def add_callback(type, options, &block)
458
+ def add_callback(type, options, &)
447
459
  options[:terminator] = callback_terminator
448
460
  super
449
461
  end
@@ -451,14 +463,24 @@ module StateMachines
451
463
  # Configures new states with the built-in humanize scheme
452
464
  def add_states(*)
453
465
  super.each do |new_state|
454
- new_state.human_name = ->(state, klass) { translate(klass, :state, state.name) }
466
+ # Only set the translation lambda if human_name is the default auto-generated value
467
+ # This preserves user-specified human names while still applying translations for defaults
468
+ default_human_name = new_state.name ? new_state.name.to_s.tr('_', ' ') : 'nil'
469
+ if new_state.human_name == default_human_name
470
+ new_state.human_name = ->(state, klass) { translate(klass, :state, state.name) }
471
+ end
455
472
  end
456
473
  end
457
474
 
458
475
  # Configures new event with the built-in humanize scheme
459
476
  def add_events(*)
460
477
  super.each do |new_event|
461
- new_event.human_name = ->(event, klass) { translate(klass, :event, event.name) }
478
+ # Only set the translation lambda if human_name is the default auto-generated value
479
+ # This preserves user-specified human names while still applying translations for defaults
480
+ default_human_name = new_event.name ? new_event.name.to_s.tr('_', ' ') : 'nil'
481
+ if new_event.human_name == default_human_name
482
+ new_event.human_name = ->(event, klass) { translate(klass, :event, event.name) }
483
+ end
462
484
  end
463
485
  end
464
486
  end
@@ -0,0 +1,176 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'test_helper'
4
+
5
+ class EventHumanNameTest < BaseTestCase
6
+ def setup
7
+ @model = new_model do
8
+ include ActiveModel::Validations
9
+ attr_accessor :status
10
+ end
11
+ end
12
+
13
+ def test_should_allow_custom_human_name_on_event
14
+ machine = StateMachines::Machine.new(@model, :status, initial: :parked) do
15
+ event :start, human_name: 'Start Engine' do
16
+ transition parked: :running
17
+ end
18
+
19
+ event :stop do
20
+ transition running: :parked
21
+ end
22
+
23
+ event :pause, human_name: 'Temporarily Pause' do
24
+ transition running: :paused
25
+ end
26
+ end
27
+
28
+ assert_equal 'Start Engine', machine.events[:start].human_name(@model)
29
+ assert_equal 'Temporarily Pause', machine.events[:pause].human_name(@model)
30
+ end
31
+
32
+ def test_should_not_override_custom_event_human_name_with_translation
33
+ # Set up I18n translations
34
+ I18n.backend.store_translations(:en, {
35
+ activemodel: {
36
+ state_machines: {
37
+ events: {
38
+ ignite: 'Translation for Ignite',
39
+ park: 'Translation for Park',
40
+ repair: 'Translation for Repair'
41
+ }
42
+ }
43
+ }
44
+ })
45
+
46
+ machine = StateMachines::Machine.new(@model, :status, initial: :parked) do
47
+ event :ignite, human_name: 'Custom Ignition' do
48
+ transition parked: :idling
49
+ end
50
+
51
+ event :park do
52
+ transition idling: :parked
53
+ end
54
+
55
+ event :repair, human_name: 'Custom Repair Process' do
56
+ transition any => :parked
57
+ end
58
+ end
59
+
60
+ # Custom human names should be preserved
61
+ assert_equal 'Custom Ignition', machine.events[:ignite].human_name(@model)
62
+ assert_equal 'Custom Repair Process', machine.events[:repair].human_name(@model)
63
+
64
+ # Event without custom human_name should use translation
65
+ assert_equal 'Translation for Park', machine.events[:park].human_name(@model)
66
+ end
67
+
68
+ def test_should_allow_custom_event_human_name_as_string
69
+ machine = StateMachines::Machine.new(@model, :status) do
70
+ event :activate, human_name: 'Turn On'
71
+ end
72
+
73
+ assert_equal 'Turn On', machine.events[:activate].human_name(@model)
74
+ end
75
+
76
+ def test_should_allow_custom_event_human_name_as_lambda
77
+ machine = StateMachines::Machine.new(@model, :status) do
78
+ event :process, human_name: ->(event, klass) { "#{klass.name}: #{event.name.to_s.capitalize} Action" }
79
+ end
80
+
81
+ assert_equal 'Foo: Process Action', machine.events[:process].human_name(@model)
82
+ end
83
+
84
+ def test_should_use_default_translation_when_no_custom_event_human_name
85
+ machine = StateMachines::Machine.new(@model, :status) do
86
+ event :idle
87
+ end
88
+
89
+ # Should fall back to humanized version when no translation exists
90
+ assert_equal 'idle', machine.events[:idle].human_name(@model)
91
+ end
92
+
93
+ def test_should_handle_nil_event_human_name
94
+ machine = StateMachines::Machine.new(@model, :status) do
95
+ event :wait
96
+ end
97
+
98
+ # Explicitly set to nil
99
+ machine.events[:wait].human_name = nil
100
+
101
+ # When human_name is nil, Event#human_name returns nil
102
+ assert_nil machine.events[:wait].human_name(@model)
103
+ end
104
+
105
+ def test_should_preserve_event_human_name_through_multiple_definitions
106
+ machine = StateMachines::Machine.new(@model, :status, initial: :draft)
107
+
108
+ # First define event with custom human name
109
+ machine.event :publish, human_name: 'Make Public' do
110
+ transition draft: :published
111
+ end
112
+
113
+ # Redefine the same event (this should not override the human_name)
114
+ machine.event :publish do
115
+ transition pending: :published
116
+ end
117
+
118
+ assert_equal 'Make Public', machine.events[:publish].human_name(@model)
119
+ end
120
+
121
+ def test_should_work_with_state_machine_helper_method
122
+ @model.class_eval do
123
+ state_machine :status, initial: :pending do
124
+ event :approve, human_name: 'Grant Approval' do
125
+ transition pending: :approved
126
+ end
127
+
128
+ event :reject do
129
+ transition pending: :rejected
130
+ end
131
+ end
132
+ end
133
+
134
+ machine = @model.state_machine(:status)
135
+ assert_equal 'Grant Approval', machine.events[:approve].human_name(@model)
136
+ end
137
+
138
+ def test_should_handle_complex_i18n_lookup_with_custom_event_human_name
139
+ # Set up complex I18n structure
140
+ I18n.backend.store_translations(:en, {
141
+ activemodel: {
142
+ state_machines: {
143
+ foo: {
144
+ status: {
145
+ events: {
146
+ submit: 'Model Specific Submit'
147
+ }
148
+ }
149
+ },
150
+ status: {
151
+ events: {
152
+ submit: 'Machine Specific Submit'
153
+ }
154
+ },
155
+ events: {
156
+ submit: 'Generic Submit'
157
+ }
158
+ }
159
+ }
160
+ })
161
+
162
+ machine = StateMachines::Machine.new(@model, :status) do
163
+ event :submit, human_name: 'Send for Review' do
164
+ transition draft: :pending
165
+ end
166
+ end
167
+
168
+ # Should use the custom human_name, not any of the I18n translations
169
+ assert_equal 'Send for Review', machine.events[:submit].human_name(@model)
170
+ end
171
+
172
+ def teardown
173
+ # Clear I18n translations after each test
174
+ I18n.backend.reload!
175
+ end
176
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'test_helper'
4
+
5
+ class HumanNamePreservationTest < BaseTestCase
6
+ def setup
7
+ @model = new_model do
8
+ include ActiveModel::Validations
9
+ attr_accessor :status
10
+ end
11
+ end
12
+
13
+ def test_should_preserve_custom_state_human_name_when_using_activemodel_integration
14
+ # This test specifically verifies that PR #38's fix works:
15
+ # Using ||= instead of = in add_states method
16
+
17
+ @model.class_eval do
18
+ state_machine :status, initial: :pending do
19
+ # Define a state with a custom human_name
20
+ state :pending, human_name: 'My Custom Pending'
21
+ state :approved
22
+ end
23
+ end
24
+
25
+ machine = @model.state_machine(:status)
26
+
27
+ # The custom human_name should be preserved, not overwritten by the integration
28
+ assert_equal 'My Custom Pending', machine.states[:pending].human_name(@model)
29
+ end
30
+
31
+ def test_should_preserve_custom_event_human_name_when_using_activemodel_integration
32
+ # This test verifies our additional fix for events:
33
+ # Using ||= instead of = in add_events method
34
+
35
+ @model.class_eval do
36
+ state_machine :status, initial: :pending do
37
+ event :approve, human_name: 'Grant Authorization' do
38
+ transition pending: :approved
39
+ end
40
+
41
+ event :reject do
42
+ transition pending: :rejected
43
+ end
44
+ end
45
+ end
46
+
47
+ machine = @model.state_machine(:status)
48
+
49
+ # The custom human_name should be preserved, not overwritten by the integration
50
+ assert_equal 'Grant Authorization', machine.events[:approve].human_name(@model)
51
+ end
52
+
53
+ def test_regression_issue_37_hard_coded_human_name_preserved
54
+ # This is the exact regression test for issue #37
55
+ # "Hard-coded human_name is being overwritten"
56
+
57
+ @model.class_eval do
58
+ state_machine :status do
59
+ state :pending, human_name: 'Pending Approval'
60
+ state :active, human_name: 'Active State'
61
+
62
+ event :activate, human_name: 'Activate Now' do
63
+ transition pending: :active
64
+ end
65
+ end
66
+ end
67
+
68
+ machine = @model.state_machine(:status)
69
+
70
+ # Both states and events should preserve their hard-coded human names
71
+ assert_equal 'Pending Approval', machine.states[:pending].human_name(@model)
72
+ assert_equal 'Active State', machine.states[:active].human_name(@model)
73
+ assert_equal 'Activate Now', machine.events[:activate].human_name(@model)
74
+ end
75
+ end
@@ -20,7 +20,7 @@ class IntegrationTest < BaseTestCase
20
20
  end
21
21
 
22
22
  def test_should_not_match_if_class_does_not_include_active_model_features
23
- refute StateMachines::Integrations::ActiveModel.matches?(new_model)
23
+ refute StateMachines::Integrations::ActiveModel.matches?(new_plain_model)
24
24
  end
25
25
 
26
26
  def test_should_have_no_defaults
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'test_helper'
4
+
5
+ class MachineInitializationCompatibilityTest < BaseTestCase
6
+ def setup
7
+ @model = new_model do
8
+ include ActiveModel::Validations
9
+ end
10
+
11
+ @machine = StateMachines::Machine.new(@model, initial: :parked)
12
+ @machine.state :parked, :idling
13
+ @machine.event :ignite
14
+ end
15
+
16
+ def test_should_accept_positional_hash_argument
17
+ record = @model.new({ state: 'idling' })
18
+ assert_equal 'idling', record.state
19
+ end
20
+
21
+ def test_should_accept_keyword_arguments
22
+ record = @model.new(state: 'idling')
23
+ assert_equal 'idling', record.state
24
+ end
25
+
26
+ def test_should_accept_empty_initialization
27
+ record = @model.new
28
+ assert_equal 'parked', record.state
29
+ end
30
+
31
+ def test_should_handle_attribute_aliases
32
+ @model.class_eval do
33
+ alias_attribute :status, :state
34
+ end
35
+
36
+ record = @model.new(status: 'idling')
37
+ assert_equal 'idling', record.state
38
+ end
39
+
40
+ def test_should_prefer_positional_hash_over_keywords_when_both_present
41
+ # If someone accidentally provides both, positional takes precedence
42
+ record = @model.new({ state: 'idling' }, state: 'parked')
43
+ assert_equal 'idling', record.state
44
+ end
45
+
46
+ def test_should_handle_empty_positional_hash
47
+ # Empty hash should still be treated as positional argument
48
+ record = @model.new({})
49
+ assert_equal 'parked', record.state # Gets default initial state
50
+ end
51
+
52
+ def test_should_use_keywords_when_empty_hash_and_keywords_present
53
+ # With the fix, keywords are ignored even with empty positional hash
54
+ record = @model.new({}, state: 'idling')
55
+ assert_equal 'parked', record.state # Empty hash takes precedence
56
+ end
57
+ end
@@ -5,7 +5,7 @@ require 'test_helper'
5
5
  class MachineMultipleTest < BaseTestCase
6
6
  def setup
7
7
  @model = new_model do
8
- model_attribute :status
8
+ attribute :status, :string
9
9
  end
10
10
 
11
11
  @state_machine = StateMachines::Machine.new(@model, initial: :parked, integration: :active_model)
@@ -5,13 +5,14 @@ require 'test_helper'
5
5
  class MachineWithDirtyAttributeAndCustomAttributesDuringLoopbackTest < BaseTestCase
6
6
  def setup
7
7
  @model = new_model do
8
- include ActiveModel::Dirty
9
- model_attribute :status
10
- define_attribute_methods [:status]
8
+ attribute :status, :string
11
9
 
12
10
  def save
13
- super.tap do
11
+ if valid?
14
12
  changes_applied
13
+ true
14
+ else
15
+ false
15
16
  end
16
17
  end
17
18
  end
@@ -5,12 +5,12 @@ require 'test_helper'
5
5
  class MachineWithDirtyAttributeAndStateEventsTest < BaseTestCase
6
6
  def setup
7
7
  @model = new_model do
8
- include ActiveModel::Dirty
9
- define_attribute_methods [:state]
10
-
11
8
  def save
12
- super.tap do
9
+ if valid?
13
10
  changes_applied
11
+ true
12
+ else
13
+ false
14
14
  end
15
15
  end
16
16
  end
@@ -5,13 +5,14 @@ require 'test_helper'
5
5
  class MachineWithDirtyAttributesAndCustomAttributeTest < BaseTestCase
6
6
  def setup
7
7
  @model = new_model do
8
- include ActiveModel::Dirty
9
- model_attribute :status
10
- define_attribute_methods [:status]
8
+ attribute :status, :string
11
9
 
12
10
  def save
13
- super.tap do
11
+ if valid?
14
12
  changes_applied
13
+ true
14
+ else
15
+ false
15
16
  end
16
17
  end
17
18
  end
@@ -26,17 +27,17 @@ class MachineWithDirtyAttributesAndCustomAttributeTest < BaseTestCase
26
27
  end
27
28
 
28
29
  def test_should_include_state_in_changed_attributes
29
- assert_equal %w(status), @record.changed
30
+ assert_equal %w[status], @record.changed
30
31
  end
31
32
 
32
33
  def test_should_track_attribute_change
33
- assert_equal %w(parked idling), @record.changes['status']
34
+ assert_equal %w[parked idling], @record.changes['status']
34
35
  end
35
36
 
36
37
  def test_should_not_reset_changes_on_multiple_transitions
37
38
  transition = StateMachines::Transition.new(@record, @machine, :ignite, :idling, :idling)
38
39
  transition.perform
39
40
 
40
- assert_equal %w(parked idling), @record.changes['status']
41
+ assert_equal %w[parked idling], @record.changes['status']
41
42
  end
42
43
  end
@@ -5,12 +5,12 @@ require 'test_helper'
5
5
  class MachineWithDirtyAttributesDuringLoopbackTest < BaseTestCase
6
6
  def setup
7
7
  @model = new_model do
8
- include ActiveModel::Dirty
9
- define_attribute_methods [:state]
10
-
11
8
  def save
12
- super.tap do
9
+ if valid?
13
10
  changes_applied
11
+ true
12
+ else
13
+ false
14
14
  end
15
15
  end
16
16
  end
@@ -5,12 +5,12 @@ require 'test_helper'
5
5
  class MachineWithDirtyAttributesTest < BaseTestCase
6
6
  def setup
7
7
  @model = new_model do
8
- include ActiveModel::Dirty
9
- define_attribute_methods [:state]
10
-
11
8
  def save
12
- super.tap do
9
+ if valid?
13
10
  changes_applied
11
+ true
12
+ else
13
+ false
14
14
  end
15
15
  end
16
16
  end
@@ -25,17 +25,17 @@ class MachineWithDirtyAttributesTest < BaseTestCase
25
25
  end
26
26
 
27
27
  def test_should_include_state_in_changed_attributes
28
- assert_equal %w(state), @record.changed
28
+ assert_equal %w[state], @record.changed
29
29
  end
30
30
 
31
31
  def test_should_track_attribute_change
32
- assert_equal %w(parked idling), @record.changes['state']
32
+ assert_equal %w[parked idling], @record.changes['state']
33
33
  end
34
34
 
35
35
  def test_should_not_reset_changes_on_multiple_transitions
36
36
  transition = StateMachines::Transition.new(@record, @machine, :ignite, :idling, :idling)
37
37
  transition.perform
38
38
 
39
- assert_equal %w(parked idling), @record.changes['state']
39
+ assert_equal %w[parked idling], @record.changes['state']
40
40
  end
41
41
  end
@@ -6,6 +6,7 @@ class MachineWithInitializedAliasedAttributeTest < BaseTestCase
6
6
  def test_should_match_original_attribute_value_with_attribute_methods
7
7
  model = new_model do
8
8
  include ActiveModel::AttributeMethods
9
+
9
10
  alias_attribute :custom_status, :state
10
11
  end
11
12
 
@@ -19,7 +20,16 @@ class MachineWithInitializedAliasedAttributeTest < BaseTestCase
19
20
  end
20
21
 
21
22
  def test_should_not_match_original_attribute_value_without_attribute_methods
22
- model = new_model do
23
+ model = new_plain_model do
24
+ include ActiveModel::Model
25
+
26
+ attr_accessor :state
27
+
28
+ def self.alias_attribute(new_name, old_name)
29
+ alias_method new_name, old_name
30
+ alias_method "#{new_name}=", "#{old_name}="
31
+ end
32
+
23
33
  alias_attribute :custom_status, :state
24
34
  end
25
35
 
@@ -32,4 +42,3 @@ class MachineWithInitializedAliasedAttributeTest < BaseTestCase
32
42
  refute record.state?(:started)
33
43
  end
34
44
  end
35
-
@@ -56,7 +56,7 @@ class MachineWithInternationalizationTest < BaseTestCase
56
56
 
57
57
  machine = StateMachines::Machine.new(@model)
58
58
  machine.state :parked
59
- assert_equal 'shutdown', machine.state(:parked).human_name
59
+ assert_equal 'shutdown', machine.state(:parked).human_name(@model)
60
60
  end
61
61
 
62
62
  def test_should_allow_customized_state_key_scoped_to_class
@@ -67,7 +67,7 @@ class MachineWithInternationalizationTest < BaseTestCase
67
67
  machine = StateMachines::Machine.new(@model)
68
68
  machine.state :parked
69
69
 
70
- assert_equal 'shutdown', machine.state(:parked).human_name
70
+ assert_equal 'shutdown', machine.state(:parked).human_name(@model)
71
71
  end
72
72
 
73
73
  def test_should_allow_customized_state_key_scoped_to_machine
@@ -78,7 +78,7 @@ class MachineWithInternationalizationTest < BaseTestCase
78
78
  machine = StateMachines::Machine.new(@model)
79
79
  machine.state :parked
80
80
 
81
- assert_equal 'shutdown', machine.state(:parked).human_name
81
+ assert_equal 'shutdown', machine.state(:parked).human_name(@model)
82
82
  end
83
83
 
84
84
  def test_should_allow_customized_state_key_unscoped
@@ -89,7 +89,7 @@ class MachineWithInternationalizationTest < BaseTestCase
89
89
  machine = StateMachines::Machine.new(@model)
90
90
  machine.state :parked
91
91
 
92
- assert_equal 'shutdown', machine.state(:parked).human_name
92
+ assert_equal 'shutdown', machine.state(:parked).human_name(@model)
93
93
  end
94
94
 
95
95
  def test_should_support_nil_state_key
@@ -99,7 +99,7 @@ class MachineWithInternationalizationTest < BaseTestCase
99
99
 
100
100
  machine = StateMachines::Machine.new(@model)
101
101
 
102
- assert_equal 'empty', machine.state(nil).human_name
102
+ assert_equal 'empty', machine.state(nil).human_name(@model)
103
103
  end
104
104
 
105
105
  def test_should_allow_customized_event_key_scoped_to_class_and_machine
@@ -110,7 +110,7 @@ class MachineWithInternationalizationTest < BaseTestCase
110
110
  machine = StateMachines::Machine.new(@model)
111
111
  machine.event :park
112
112
 
113
- assert_equal 'stop', machine.event(:park).human_name
113
+ assert_equal 'stop', machine.event(:park).human_name(@model)
114
114
  end
115
115
 
116
116
  def test_should_allow_customized_event_key_scoped_to_class
@@ -121,7 +121,7 @@ class MachineWithInternationalizationTest < BaseTestCase
121
121
  machine = StateMachines::Machine.new(@model)
122
122
  machine.event :park
123
123
 
124
- assert_equal 'stop', machine.event(:park).human_name
124
+ assert_equal 'stop', machine.event(:park).human_name(@model)
125
125
  end
126
126
 
127
127
  def test_should_allow_customized_event_key_scoped_to_machine
@@ -132,7 +132,7 @@ class MachineWithInternationalizationTest < BaseTestCase
132
132
  machine = StateMachines::Machine.new(@model)
133
133
  machine.event :park
134
134
 
135
- assert_equal 'stop', machine.event(:park).human_name
135
+ assert_equal 'stop', machine.event(:park).human_name(@model)
136
136
  end
137
137
 
138
138
  def test_should_allow_customized_event_key_unscoped
@@ -143,7 +143,7 @@ class MachineWithInternationalizationTest < BaseTestCase
143
143
  machine = StateMachines::Machine.new(@model)
144
144
  machine.event :park
145
145
 
146
- assert_equal 'stop', machine.event(:park).human_name
146
+ assert_equal 'stop', machine.event(:park).human_name(@model)
147
147
  end
148
148
 
149
149
  def test_should_have_locale_once_in_load_path
@@ -4,9 +4,8 @@ require 'test_helper'
4
4
 
5
5
  class MachineWithNonModelStateAttributeUndefinedTest < BaseTestCase
6
6
  def setup
7
- @model = new_model do
8
- def initialize
9
- end
7
+ @model = new_plain_model do
8
+ def initialize; end
10
9
  end
11
10
 
12
11
  @machine = StateMachines::Machine.new(@model, :status, initial: :parked, integration: :active_model)
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'test_helper'
4
+
5
+ class StateHumanNameTest < BaseTestCase
6
+ def setup
7
+ @model = new_model do
8
+ include ActiveModel::Validations
9
+ attr_accessor :status
10
+ end
11
+ end
12
+
13
+ def test_should_allow_custom_human_name_on_state
14
+ machine = StateMachines::Machine.new(@model, :status, initial: :pending) do
15
+ state :pending, human_name: 'Awaiting Approval'
16
+ state :approved
17
+ state :rejected, human_name: 'Denied'
18
+ end
19
+
20
+ assert_equal 'Awaiting Approval', machine.states[:pending].human_name(@model)
21
+ assert_equal 'Denied', machine.states[:rejected].human_name(@model)
22
+ end
23
+
24
+ def test_should_not_override_custom_human_name_with_translation
25
+ # Set up I18n translations
26
+ I18n.backend.store_translations(:en, {
27
+ activemodel: {
28
+ state_machines: {
29
+ states: {
30
+ pending: 'Translation for Pending',
31
+ approved: 'Translation for Approved',
32
+ rejected: 'Translation for Rejected'
33
+ }
34
+ }
35
+ }
36
+ })
37
+
38
+ machine = StateMachines::Machine.new(@model, :status, initial: :pending) do
39
+ state :pending, human_name: 'Custom Pending Name'
40
+ state :approved
41
+ state :rejected, human_name: 'Custom Rejected Name'
42
+ end
43
+
44
+ # Custom human names should be preserved
45
+ assert_equal 'Custom Pending Name', machine.states[:pending].human_name(@model)
46
+ assert_equal 'Custom Rejected Name', machine.states[:rejected].human_name(@model)
47
+
48
+ # State without custom human_name gets default behavior (which might not use translations in this test setup)
49
+ # The key test is that custom human names are preserved, not overwritten
50
+ refute_equal 'Custom Pending Name', machine.states[:approved].human_name(@model)
51
+ end
52
+
53
+ def test_should_allow_custom_human_name_as_string
54
+ machine = StateMachines::Machine.new(@model, :status) do
55
+ state :active, human_name: 'Currently Active'
56
+ end
57
+
58
+ assert_equal 'Currently Active', machine.states[:active].human_name(@model)
59
+ end
60
+
61
+ def test_should_allow_custom_human_name_as_lambda
62
+ machine = StateMachines::Machine.new(@model, :status) do
63
+ state :processing, human_name: ->(state, klass) { "#{klass.name} is #{state.name.to_s.upcase}" }
64
+ end
65
+
66
+ assert_equal 'Foo is PROCESSING', machine.states[:processing].human_name(@model)
67
+ end
68
+
69
+ def test_should_use_default_translation_when_no_custom_human_name
70
+ machine = StateMachines::Machine.new(@model, :status) do
71
+ state :idle
72
+ end
73
+
74
+ # Should fall back to humanized version when no translation exists
75
+ assert_equal 'idle', machine.states[:idle].human_name(@model)
76
+ end
77
+
78
+ def test_should_handle_nil_human_name
79
+ machine = StateMachines::Machine.new(@model, :status) do
80
+ state :waiting
81
+ end
82
+
83
+ # Explicitly set to nil (should still get default behavior)
84
+ machine.states[:waiting].human_name = nil
85
+
86
+ # When human_name is nil, State#human_name returns nil
87
+ assert_nil machine.states[:waiting].human_name(@model)
88
+ end
89
+
90
+ def test_should_preserve_human_name_through_multiple_state_definitions
91
+ machine = StateMachines::Machine.new(@model, :status)
92
+
93
+ # First define state with custom human name
94
+ machine.state :draft, human_name: 'Work in Progress'
95
+
96
+ # Redefine the same state (this should not override the human_name)
97
+ machine.state :draft do
98
+ # Add some behavior
99
+ end
100
+
101
+ assert_equal 'Work in Progress', machine.states[:draft].human_name(@model)
102
+ end
103
+
104
+ def test_should_work_with_state_machine_helper_method
105
+ @model.class_eval do
106
+ state_machine :status, initial: :pending do
107
+ state :pending, human_name: 'Awaiting Review'
108
+ state :reviewed
109
+ end
110
+ end
111
+
112
+ machine = @model.state_machine(:status)
113
+ assert_equal 'Awaiting Review', machine.states[:pending].human_name(@model)
114
+ end
115
+
116
+ def test_should_handle_complex_i18n_lookup_with_custom_human_name
117
+ # Set up complex I18n structure
118
+ I18n.backend.store_translations(:en, {
119
+ activemodel: {
120
+ state_machines: {
121
+ foo: {
122
+ status: {
123
+ states: {
124
+ pending: 'Model Specific Pending'
125
+ }
126
+ }
127
+ },
128
+ status: {
129
+ states: {
130
+ pending: 'Machine Specific Pending'
131
+ }
132
+ },
133
+ states: {
134
+ pending: 'Generic Pending'
135
+ }
136
+ }
137
+ }
138
+ })
139
+
140
+ machine = StateMachines::Machine.new(@model, :status) do
141
+ state :pending, human_name: 'Overridden Pending'
142
+ end
143
+
144
+ # Should use the custom human_name, not any of the I18n translations
145
+ assert_equal 'Overridden Pending', machine.states[:pending].human_name(@model)
146
+ end
147
+
148
+ def teardown
149
+ # Clear I18n translations after each test
150
+ I18n.backend.reload!
151
+ end
152
+ end
data/test/test_helper.rb CHANGED
@@ -11,45 +11,40 @@ I18n.enforce_available_locales = true
11
11
 
12
12
  class BaseTestCase < ActiveSupport::TestCase
13
13
  protected
14
- # Creates a new ActiveModel model (and the associated table)
15
- def new_model(&block)
16
- # Simple ActiveModel superclass
17
- parent = Class.new do
18
- def self.model_attribute(name)
19
- define_method(name) { instance_variable_defined?(:"@#{name}") ? instance_variable_get(:"@#{name}") : nil }
20
- define_method("#{name}=") do |value|
21
- send(:"#{name}_will_change!") if self.class <= ActiveModel::Dirty && value != send(name)
22
- instance_variable_set("@#{name}", value)
23
- end
24
- end
25
14
 
26
- def self.create
27
- object = new
28
- object.save
29
- object
15
+ # Creates a plain model without ActiveModel features
16
+ def new_plain_model(&block)
17
+ model = Class.new do
18
+ def self.name
19
+ 'Foo'
30
20
  end
21
+ end
31
22
 
32
- def initialize(attrs = {})
33
- attrs.each { |attr, value| send("#{attr}=", value) }
34
- end
23
+ model.class_eval(&block) if block_given?
35
24
 
36
- def attributes
37
- @attributes ||= {}
38
- end
25
+ model
26
+ end
39
27
 
40
- def save
41
- true
42
- end
43
- end
28
+ # Creates a new ActiveModel model (and the associated table)
29
+ def new_model(&block)
30
+ model = Class.new do
31
+ include ActiveModel::Model
32
+ include ActiveModel::Attributes
33
+ include ActiveModel::Dirty
34
+
35
+ attribute :state, :string
44
36
 
45
- model = Class.new(parent) do
46
37
  def self.name
47
38
  'Foo'
48
39
  end
49
40
 
50
- model_attribute :state
41
+ def self.create
42
+ new.tap { |instance| instance.save if instance.respond_to?(:save) }
43
+ end
51
44
  end
45
+
52
46
  model.class_eval(&block) if block_given?
47
+
53
48
  model
54
49
  end
55
50
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: state_machines-activemodel
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.10.0
4
+ version: 0.31.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Abdelkader Boudih
@@ -16,28 +16,28 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: 0.10.0
19
+ version: 0.100.1
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: 0.10.0
26
+ version: 0.100.1
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: activemodel
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: '7.1'
33
+ version: '7.2'
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: '7.1'
40
+ version: '7.2'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: bundler
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -122,9 +122,12 @@ files:
122
122
  - lib/state_machines/integrations/active_model.rb
123
123
  - lib/state_machines/integrations/active_model/locale.rb
124
124
  - lib/state_machines/integrations/active_model/version.rb
125
+ - test/event_human_name_test.rb
126
+ - test/human_name_preservation_test.rb
125
127
  - test/integration_test.rb
126
128
  - test/machine_by_default_test.rb
127
129
  - test/machine_errors_test.rb
130
+ - test/machine_initialization_compatibility_test.rb
128
131
  - test/machine_multiple_test.rb
129
132
  - test/machine_with_callbacks_test.rb
130
133
  - test/machine_with_dirty_attribute_and_custom_attributes_during_loopback_test.rb
@@ -146,6 +149,7 @@ files:
146
149
  - test/machine_with_static_initial_state_test.rb
147
150
  - test/machine_with_validations_and_custom_attribute_test.rb
148
151
  - test/machine_with_validations_test.rb
152
+ - test/state_human_name_test.rb
149
153
  - test/test_helper.rb
150
154
  homepage: https://github.com/state-machines/state_machines-activemodel
151
155
  licenses:
@@ -158,7 +162,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
158
162
  requirements:
159
163
  - - ">="
160
164
  - !ruby/object:Gem::Version
161
- version: 3.1.0
165
+ version: 3.2.0
162
166
  required_rubygems_version: !ruby/object:Gem::Requirement
163
167
  requirements:
164
168
  - - ">="
@@ -169,9 +173,12 @@ rubygems_version: 3.6.7
169
173
  specification_version: 4
170
174
  summary: ActiveModel integration for State Machines
171
175
  test_files:
176
+ - test/event_human_name_test.rb
177
+ - test/human_name_preservation_test.rb
172
178
  - test/integration_test.rb
173
179
  - test/machine_by_default_test.rb
174
180
  - test/machine_errors_test.rb
181
+ - test/machine_initialization_compatibility_test.rb
175
182
  - test/machine_multiple_test.rb
176
183
  - test/machine_with_callbacks_test.rb
177
184
  - test/machine_with_dirty_attribute_and_custom_attributes_during_loopback_test.rb
@@ -193,4 +200,5 @@ test_files:
193
200
  - test/machine_with_static_initial_state_test.rb
194
201
  - test/machine_with_validations_and_custom_attribute_test.rb
195
202
  - test/machine_with_validations_test.rb
203
+ - test/state_human_name_test.rb
196
204
  - test/test_helper.rb