state_machines-activemodel 0.9.0 → 0.31.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 (36) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +7 -26
  3. data/lib/state_machines/integrations/active_model/locale.rb +7 -3
  4. data/lib/state_machines/integrations/active_model/version.rb +3 -1
  5. data/lib/state_machines/integrations/active_model.rb +71 -102
  6. data/lib/state_machines-activemodel.rb +2 -0
  7. data/test/event_human_name_test.rb +176 -0
  8. data/test/human_name_preservation_test.rb +75 -0
  9. data/test/integration_test.rb +3 -1
  10. data/test/machine_by_default_test.rb +3 -1
  11. data/test/machine_errors_test.rb +3 -1
  12. data/test/machine_initialization_compatibility_test.rb +60 -0
  13. data/test/machine_multiple_test.rb +3 -1
  14. data/test/machine_with_callbacks_test.rb +3 -1
  15. data/test/machine_with_dirty_attribute_and_custom_attributes_during_loopback_test.rb +3 -1
  16. data/test/machine_with_dirty_attribute_and_state_events_test.rb +3 -1
  17. data/test/machine_with_dirty_attributes_and_custom_attribute_test.rb +3 -1
  18. data/test/machine_with_dirty_attributes_during_loopback_test.rb +3 -1
  19. data/test/machine_with_dirty_attributes_test.rb +3 -1
  20. data/test/machine_with_dynamic_initial_state_test.rb +3 -1
  21. data/test/machine_with_events_test.rb +3 -1
  22. data/test/machine_with_failed_after_callbacks_test.rb +3 -1
  23. data/test/machine_with_failed_before_callbacks_test.rb +3 -1
  24. data/test/machine_with_initialized_aliased_attribute_test.rb +3 -1
  25. data/test/machine_with_initialized_state_test.rb +3 -1
  26. data/test/machine_with_internationalization_test.rb +12 -10
  27. data/test/machine_with_model_state_attribute_test.rb +3 -1
  28. data/test/machine_with_non_model_state_attribute_undefined_test.rb +3 -1
  29. data/test/machine_with_state_driven_validations_test.rb +4 -2
  30. data/test/machine_with_states_test.rb +3 -1
  31. data/test/machine_with_static_initial_state_test.rb +3 -1
  32. data/test/machine_with_validations_and_custom_attribute_test.rb +3 -1
  33. data/test/machine_with_validations_test.rb +3 -1
  34. data/test/state_human_name_test.rb +152 -0
  35. data/test/test_helper.rb +55 -0
  36. metadata +18 -11
@@ -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
@@ -1,4 +1,6 @@
1
- require_relative 'test_helper'
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
2
4
 
3
5
  class IntegrationTest < BaseTestCase
4
6
  def test_should_be_registered
@@ -1,4 +1,6 @@
1
- require_relative 'test_helper'
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
2
4
 
3
5
  class MachineByDefaultTest < BaseTestCase
4
6
  def setup
@@ -1,4 +1,6 @@
1
- require_relative 'test_helper'
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
2
4
 
3
5
  class MachineErrorsTest < BaseTestCase
4
6
  def setup
@@ -0,0 +1,60 @@
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
+ attr_accessor :state
10
+ end
11
+
12
+ @machine = StateMachines::Machine.new(@model, initial: :parked)
13
+ @machine.state :parked, :idling
14
+ @machine.event :ignite
15
+ end
16
+
17
+ def test_should_accept_positional_hash_argument
18
+ record = @model.new({ state: 'idling' })
19
+ assert_equal 'idling', record.state
20
+ end
21
+
22
+ def test_should_accept_keyword_arguments
23
+ record = @model.new(state: 'idling')
24
+ assert_equal 'idling', record.state
25
+ end
26
+
27
+ def test_should_accept_empty_initialization
28
+ record = @model.new
29
+ assert_equal 'parked', record.state
30
+ end
31
+
32
+ def test_should_handle_attribute_aliases
33
+ @model.class_eval do
34
+ def self.attribute_aliases
35
+ { 'status' => 'state' }
36
+ end
37
+ end
38
+
39
+ record = @model.new(status: 'idling')
40
+ assert_equal 'idling', record.state
41
+ end
42
+
43
+ def test_should_prefer_positional_hash_over_keywords_when_both_present
44
+ # If someone accidentally provides both, positional takes precedence
45
+ record = @model.new({ state: 'idling' }, state: 'parked')
46
+ assert_equal 'idling', record.state
47
+ end
48
+
49
+ def test_should_handle_empty_positional_hash
50
+ # Empty hash should still be treated as positional argument
51
+ record = @model.new({})
52
+ assert_equal 'parked', record.state # Gets default initial state
53
+ end
54
+
55
+ def test_should_use_keywords_when_empty_hash_and_keywords_present
56
+ # With the fix, keywords are ignored even with empty positional hash
57
+ record = @model.new({}, state: 'idling')
58
+ assert_equal 'parked', record.state # Empty hash takes precedence
59
+ end
60
+ end
@@ -1,4 +1,6 @@
1
- require_relative 'test_helper'
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
2
4
 
3
5
  class MachineMultipleTest < BaseTestCase
4
6
  def setup
@@ -1,4 +1,6 @@
1
- require_relative 'test_helper'
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
2
4
 
3
5
  class MachineWithCallbacksTest < BaseTestCase
4
6
  def setup
@@ -1,4 +1,6 @@
1
- require_relative 'test_helper'
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
2
4
 
3
5
  class MachineWithDirtyAttributeAndCustomAttributesDuringLoopbackTest < BaseTestCase
4
6
  def setup
@@ -1,4 +1,6 @@
1
- require_relative 'test_helper'
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
2
4
 
3
5
  class MachineWithDirtyAttributeAndStateEventsTest < BaseTestCase
4
6
  def setup
@@ -1,4 +1,6 @@
1
- require_relative 'test_helper'
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
2
4
 
3
5
  class MachineWithDirtyAttributesAndCustomAttributeTest < BaseTestCase
4
6
  def setup
@@ -1,4 +1,6 @@
1
- require_relative 'test_helper'
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
2
4
 
3
5
  class MachineWithDirtyAttributesDuringLoopbackTest < BaseTestCase
4
6
  def setup
@@ -1,4 +1,6 @@
1
- require_relative 'test_helper'
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
2
4
 
3
5
  class MachineWithDirtyAttributesTest < BaseTestCase
4
6
  def setup
@@ -1,4 +1,6 @@
1
- require_relative 'test_helper'
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
2
4
 
3
5
  class MachineWithDynamicInitialStateTest < BaseTestCase
4
6
  def setup
@@ -1,4 +1,6 @@
1
- require_relative 'test_helper'
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
2
4
 
3
5
  class MachineWithEventsTest < BaseTestCase
4
6
  def setup
@@ -1,4 +1,6 @@
1
- require_relative 'test_helper'
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
2
4
 
3
5
  class MachineWithFailedAfterCallbacksTest < BaseTestCase
4
6
  def setup
@@ -1,4 +1,6 @@
1
- require_relative 'test_helper'
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
2
4
 
3
5
  class MachineWithFailedBeforeCallbacksTest < BaseTestCase
4
6
  def setup
@@ -1,4 +1,6 @@
1
- require_relative 'test_helper'
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
2
4
 
3
5
  class MachineWithInitializedAliasedAttributeTest < BaseTestCase
4
6
  def test_should_match_original_attribute_value_with_attribute_methods
@@ -1,4 +1,6 @@
1
- require_relative 'test_helper'
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
2
4
 
3
5
  class MachineWithInitializedStateTest < BaseTestCase
4
6
  def setup
@@ -1,4 +1,6 @@
1
- require_relative 'test_helper'
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
2
4
  require 'i18n'
3
5
 
4
6
  class MachineWithInternationalizationTest < BaseTestCase
@@ -54,7 +56,7 @@ class MachineWithInternationalizationTest < BaseTestCase
54
56
 
55
57
  machine = StateMachines::Machine.new(@model)
56
58
  machine.state :parked
57
- assert_equal 'shutdown', machine.state(:parked).human_name
59
+ assert_equal 'shutdown', machine.state(:parked).human_name(@model)
58
60
  end
59
61
 
60
62
  def test_should_allow_customized_state_key_scoped_to_class
@@ -65,7 +67,7 @@ class MachineWithInternationalizationTest < BaseTestCase
65
67
  machine = StateMachines::Machine.new(@model)
66
68
  machine.state :parked
67
69
 
68
- assert_equal 'shutdown', machine.state(:parked).human_name
70
+ assert_equal 'shutdown', machine.state(:parked).human_name(@model)
69
71
  end
70
72
 
71
73
  def test_should_allow_customized_state_key_scoped_to_machine
@@ -76,7 +78,7 @@ class MachineWithInternationalizationTest < BaseTestCase
76
78
  machine = StateMachines::Machine.new(@model)
77
79
  machine.state :parked
78
80
 
79
- assert_equal 'shutdown', machine.state(:parked).human_name
81
+ assert_equal 'shutdown', machine.state(:parked).human_name(@model)
80
82
  end
81
83
 
82
84
  def test_should_allow_customized_state_key_unscoped
@@ -87,7 +89,7 @@ class MachineWithInternationalizationTest < BaseTestCase
87
89
  machine = StateMachines::Machine.new(@model)
88
90
  machine.state :parked
89
91
 
90
- assert_equal 'shutdown', machine.state(:parked).human_name
92
+ assert_equal 'shutdown', machine.state(:parked).human_name(@model)
91
93
  end
92
94
 
93
95
  def test_should_support_nil_state_key
@@ -97,7 +99,7 @@ class MachineWithInternationalizationTest < BaseTestCase
97
99
 
98
100
  machine = StateMachines::Machine.new(@model)
99
101
 
100
- assert_equal 'empty', machine.state(nil).human_name
102
+ assert_equal 'empty', machine.state(nil).human_name(@model)
101
103
  end
102
104
 
103
105
  def test_should_allow_customized_event_key_scoped_to_class_and_machine
@@ -108,7 +110,7 @@ class MachineWithInternationalizationTest < BaseTestCase
108
110
  machine = StateMachines::Machine.new(@model)
109
111
  machine.event :park
110
112
 
111
- assert_equal 'stop', machine.event(:park).human_name
113
+ assert_equal 'stop', machine.event(:park).human_name(@model)
112
114
  end
113
115
 
114
116
  def test_should_allow_customized_event_key_scoped_to_class
@@ -119,7 +121,7 @@ class MachineWithInternationalizationTest < BaseTestCase
119
121
  machine = StateMachines::Machine.new(@model)
120
122
  machine.event :park
121
123
 
122
- assert_equal 'stop', machine.event(:park).human_name
124
+ assert_equal 'stop', machine.event(:park).human_name(@model)
123
125
  end
124
126
 
125
127
  def test_should_allow_customized_event_key_scoped_to_machine
@@ -130,7 +132,7 @@ class MachineWithInternationalizationTest < BaseTestCase
130
132
  machine = StateMachines::Machine.new(@model)
131
133
  machine.event :park
132
134
 
133
- assert_equal 'stop', machine.event(:park).human_name
135
+ assert_equal 'stop', machine.event(:park).human_name(@model)
134
136
  end
135
137
 
136
138
  def test_should_allow_customized_event_key_unscoped
@@ -141,7 +143,7 @@ class MachineWithInternationalizationTest < BaseTestCase
141
143
  machine = StateMachines::Machine.new(@model)
142
144
  machine.event :park
143
145
 
144
- assert_equal 'stop', machine.event(:park).human_name
146
+ assert_equal 'stop', machine.event(:park).human_name(@model)
145
147
  end
146
148
 
147
149
  def test_should_have_locale_once_in_load_path
@@ -1,4 +1,6 @@
1
- require_relative 'test_helper'
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
2
4
 
3
5
  class MachineWithModelStateAttributeTest < BaseTestCase
4
6
  def setup
@@ -1,4 +1,6 @@
1
- require_relative 'test_helper'
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
2
4
 
3
5
  class MachineWithNonModelStateAttributeUndefinedTest < BaseTestCase
4
6
  def setup
@@ -1,4 +1,6 @@
1
- require_relative 'test_helper'
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
2
4
 
3
5
  class MachineWithStateDrivenValidationsTest < BaseTestCase
4
6
  def setup
@@ -9,7 +11,7 @@ class MachineWithStateDrivenValidationsTest < BaseTestCase
9
11
 
10
12
  @machine = StateMachines::Machine.new(@model)
11
13
  @machine.state :first_gear, :second_gear do
12
- validates_presence_of :seatbelt
14
+ validates :seatbelt, presence: true
13
15
  end
14
16
  @machine.other_states :parked
15
17
  end
@@ -1,4 +1,6 @@
1
- require_relative 'test_helper'
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
2
4
 
3
5
  class MachineWithStatesTest < BaseTestCase
4
6
  def setup
@@ -1,4 +1,6 @@
1
- require_relative 'test_helper'
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
2
4
 
3
5
  class MachineWithStaticInitialStateTest < BaseTestCase
4
6
  def setup
@@ -1,4 +1,6 @@
1
- require_relative 'test_helper'
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
2
4
 
3
5
  class MachineWithValidationsAndCustomAttributeTest < BaseTestCase
4
6
  def setup
@@ -1,4 +1,6 @@
1
- require_relative 'test_helper'
1
+ # frozen_string_literal: true
2
+
3
+ require 'test_helper'
2
4
 
3
5
  class MachineWithValidationsTest < BaseTestCase
4
6
  def setup
@@ -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
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'debug'
4
+
5
+ require 'state_machines-activemodel'
6
+ require 'minitest/autorun'
7
+ require 'minitest/reporters'
8
+ require 'active_support/all'
9
+ Minitest::Reporters.use! [Minitest::Reporters::ProgressReporter.new]
10
+ I18n.enforce_available_locales = true
11
+
12
+ class BaseTestCase < ActiveSupport::TestCase
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
+
26
+ def self.create
27
+ object = new
28
+ object.save
29
+ object
30
+ end
31
+
32
+ def initialize(attrs = {})
33
+ attrs.each { |attr, value| send("#{attr}=", value) }
34
+ end
35
+
36
+ def attributes
37
+ @attributes ||= {}
38
+ end
39
+
40
+ def save
41
+ true
42
+ end
43
+ end
44
+
45
+ model = Class.new(parent) do
46
+ def self.name
47
+ 'Foo'
48
+ end
49
+
50
+ model_attribute :state
51
+ end
52
+ model.class_eval(&block) if block_given?
53
+ model
54
+ end
55
+ end