stateful_models 0.0.1 → 0.0.3
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 +4 -4
- data/CHANGELOG.md +25 -1
- data/README.md +1 -1
- data/lib/generators/has_states/install/install_generator.rb +27 -9
- data/lib/generators/has_states/install/templates/create_has_states_states.rb.erb +3 -2
- data/lib/generators/has_states/install/templates/create_indexes_on_has_states_states.rb.erb +11 -0
- data/lib/has_states/base.rb +41 -0
- data/lib/has_states/configuration/model_configuration.rb +16 -0
- data/lib/has_states/state.rb +1 -37
- data/lib/has_states/stateable.rb +11 -7
- data/lib/has_states/version.rb +1 -1
- data/lib/has_states.rb +2 -1
- data/spec/dummy/Gemfile.lock +2 -2
- data/spec/dummy/config/initializers/has_states.rb +45 -0
- data/spec/{generators/tmp/db/migrate/20241223020432_create_has_states_states.rb → dummy/db/migrate/20241223212128_create_has_states_states.rb} +2 -1
- data/spec/dummy/db/migrate/20250114175939_create_indexes_on_has_states_states.rb +10 -0
- data/spec/dummy/db/schema.rb +26 -22
- data/spec/dummy/log/development.log +203 -0
- data/spec/dummy/log/test.log +7151 -0
- data/spec/dummy/storage/development.sqlite3 +0 -0
- data/spec/dummy/storage/test.sqlite3 +0 -0
- data/spec/{dummy/db/migrate/20241221183116_create_has_states_tables.rb → generators/tmp/db/migrate/20250114180401_create_has_states_states.rb} +4 -5
- data/spec/generators/tmp/db/migrate/20250114180401_create_indexes_on_has_states_states.rb +11 -0
- data/spec/has_states/state_spec.rb +96 -12
- data/spec/has_states/stateable_spec.rb +184 -0
- data/spec/rails_helper.rb +1 -0
- metadata +10 -4
Binary file
|
Binary file
|
@@ -1,8 +1,7 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
class CreateHasStatesTables < ActiveRecord::Migration[7.1]
|
1
|
+
class CreateHasStatesStates < ActiveRecord::Migration[8.0]
|
4
2
|
def change
|
5
3
|
create_table :has_states_states do |t|
|
4
|
+
t.string :type, null: false
|
6
5
|
t.string :state_type
|
7
6
|
t.string :status, null: false
|
8
7
|
|
@@ -10,10 +9,10 @@ class CreateHasStatesTables < ActiveRecord::Migration[7.1]
|
|
10
9
|
|
11
10
|
t.references :stateable, polymorphic: true, null: false
|
12
11
|
|
13
|
-
t.datetime :completed_at
|
14
12
|
t.timestamps
|
15
13
|
|
14
|
+
t.index %i[type stateable_id]
|
16
15
|
t.index %i[stateable_type stateable_id]
|
17
16
|
end
|
18
17
|
end
|
19
|
-
end
|
18
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
class CreateIndexesOnHasStatesStates < ActiveRecord::Migration[8.0]
|
2
|
+
def change
|
3
|
+
change_table :has_states_states do |t|
|
4
|
+
t.index %i[stateable_id state_type]
|
5
|
+
t.index %i[stateable_id state_type status]
|
6
|
+
t.index %i[stateable_id state_type created_at]
|
7
|
+
t.index %i[stateable_id state_type status created_at]
|
8
|
+
t.index %i[stateable_type stateable_id]
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -22,6 +22,28 @@ RSpec.describe HasStates::State, type: :model do
|
|
22
22
|
end
|
23
23
|
end
|
24
24
|
|
25
|
+
describe 'indexes' do
|
26
|
+
it 'defines an index on stateable_type and stateable_id' do
|
27
|
+
expect(subject).to have_db_index(%i[stateable_type stateable_id])
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'defines an index on stateable_id and state_type' do
|
31
|
+
expect(subject).to have_db_index(%i[stateable_id state_type])
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'defines an index on stateable_id, state_type, and status' do
|
35
|
+
expect(subject).to have_db_index(%i[stateable_id state_type status])
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'defines an index on stateable_id, state_type, and created_at' do
|
39
|
+
expect(subject).to have_db_index(%i[stateable_id state_type created_at])
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'defines an index on stateable_id, state_type, status, and created_at' do
|
43
|
+
expect(subject).to have_db_index(%i[stateable_id state_type status created_at])
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
25
47
|
describe 'validations' do
|
26
48
|
context 'status' do
|
27
49
|
it 'validates status presence' do
|
@@ -63,22 +85,22 @@ RSpec.describe HasStates::State, type: :model do
|
|
63
85
|
describe 'instance methods' do
|
64
86
|
let(:state) { create(:state, state_type: 'kyc', status: 'pending') }
|
65
87
|
let(:model_config) { HasStates.configuration.model_configurations[User] }
|
66
|
-
let(:all_statuses)
|
67
|
-
model_config.state_types.values.flat_map(&:statuses).uniq
|
68
|
-
end
|
88
|
+
let(:all_statuses) { model_config.state_types.values.flat_map(&:statuses).uniq }
|
69
89
|
|
70
|
-
|
71
|
-
|
72
|
-
|
90
|
+
context 'predicate methods' do
|
91
|
+
it 'defines predicate methods for all configured statuses' do
|
92
|
+
all_statuses.each do |status|
|
93
|
+
expect(state).to respond_to("#{status}?")
|
94
|
+
end
|
73
95
|
end
|
74
|
-
end
|
75
96
|
|
76
|
-
|
77
|
-
|
78
|
-
|
97
|
+
it 'returns true if the status is pending' do
|
98
|
+
expect(state.pending?).to be(true)
|
99
|
+
end
|
79
100
|
|
80
|
-
|
81
|
-
|
101
|
+
it 'returns false if the status is not completed' do
|
102
|
+
expect(state.completed?).to be(false)
|
103
|
+
end
|
82
104
|
end
|
83
105
|
end
|
84
106
|
|
@@ -261,4 +283,66 @@ RSpec.describe HasStates::State, type: :model do
|
|
261
283
|
end.to change { HasStates.configuration.callbacks.size }.from(1).to(0)
|
262
284
|
end
|
263
285
|
end
|
286
|
+
|
287
|
+
describe 'custom state types' do
|
288
|
+
# Create a custom state class for testing
|
289
|
+
class KYCState < HasStates::Base
|
290
|
+
validates :metadata, presence: true
|
291
|
+
validate :required_metadata_fields
|
292
|
+
|
293
|
+
private
|
294
|
+
|
295
|
+
def required_metadata_fields
|
296
|
+
return if metadata&.key?('document_type')
|
297
|
+
errors.add(:metadata, 'must include document_type')
|
298
|
+
end
|
299
|
+
end
|
300
|
+
|
301
|
+
let(:user) { create(:user) }
|
302
|
+
|
303
|
+
it 'allows creation of custom state types' do
|
304
|
+
state = user.add_state(
|
305
|
+
'kyc',
|
306
|
+
status: 'pending',
|
307
|
+
metadata: { document_type: 'passport' },
|
308
|
+
state_class: KYCState
|
309
|
+
)
|
310
|
+
|
311
|
+
expect(state).to be_valid
|
312
|
+
expect(state).to be_a(KYCState)
|
313
|
+
end
|
314
|
+
|
315
|
+
it 'enforces custom validations' do
|
316
|
+
expect do
|
317
|
+
user.add_state(
|
318
|
+
'kyc',
|
319
|
+
status: 'pending',
|
320
|
+
metadata: { other_field: 'value' },
|
321
|
+
state_class: KYCState
|
322
|
+
)
|
323
|
+
end.to raise_error(
|
324
|
+
ActiveRecord::RecordInvalid,
|
325
|
+
/Metadata must include document_type/
|
326
|
+
)
|
327
|
+
end
|
328
|
+
|
329
|
+
it 'defaults to HasStates::State when no state_class specified' do
|
330
|
+
state = user.add_state('kyc', status: 'pending')
|
331
|
+
|
332
|
+
expect(state).to be_a(HasStates::State)
|
333
|
+
expect(state).to be_valid
|
334
|
+
end
|
335
|
+
|
336
|
+
it 'maintains STI type across database reads' do
|
337
|
+
state = user.add_state(
|
338
|
+
'kyc',
|
339
|
+
status: 'pending',
|
340
|
+
metadata: { document_type: 'passport' },
|
341
|
+
state_class: KYCState
|
342
|
+
)
|
343
|
+
|
344
|
+
reloaded_state = HasStates::Base.find(state.id)
|
345
|
+
expect(reloaded_state).to be_a(KYCState)
|
346
|
+
end
|
347
|
+
end
|
264
348
|
end
|
@@ -0,0 +1,184 @@
|
|
1
|
+
require 'rails_helper'
|
2
|
+
|
3
|
+
RSpec.describe HasStates::Stateable do
|
4
|
+
# Create a test class that includes the concern
|
5
|
+
let(:test_class) do
|
6
|
+
Class.new do
|
7
|
+
include HasStates::Stateable
|
8
|
+
include ActiveRecord::Model
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
let(:user) { create(:user) } # Assuming you have a User factory
|
13
|
+
|
14
|
+
before do
|
15
|
+
# HasStates.configuration.clear_callbacks!
|
16
|
+
# HasStates.configuration.model_configurations.clear
|
17
|
+
|
18
|
+
HasStates.configure do |config|
|
19
|
+
config.configure_model User do |model|
|
20
|
+
model.state_type :test_type do |type|
|
21
|
+
type.statuses = %w[pending completed]
|
22
|
+
end
|
23
|
+
|
24
|
+
model.state_type :other_test_type do |type|
|
25
|
+
type.statuses = %w[pending completed]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe 'associations' do
|
32
|
+
it 'has many states' do
|
33
|
+
association = User.reflect_on_association(:states)
|
34
|
+
|
35
|
+
expect(association.macro).to eq :has_many
|
36
|
+
expect(association.options[:as]).to eq :stateable
|
37
|
+
expect(association.options[:dependent]).to eq :destroy
|
38
|
+
expect(association.options[:class_name]).to eq 'HasStates::Base'
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
describe '#add_state' do
|
43
|
+
context 'with default parameters' do
|
44
|
+
it 'creates a new state with default values' do
|
45
|
+
state = user.add_state('test_type')
|
46
|
+
|
47
|
+
expect(state).to be_persisted
|
48
|
+
expect(state.metadata).to eq({})
|
49
|
+
expect(state.status).to eq 'pending'
|
50
|
+
expect(state.state_type).to eq 'test_type'
|
51
|
+
expect(state.type).to eq 'HasStates::State'
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
context 'with custom parameters' do
|
56
|
+
let(:metadata) { { key: 'value' } }
|
57
|
+
|
58
|
+
it 'creates a new state with provided values' do
|
59
|
+
state = user.add_state('test_type', status: 'completed', metadata: metadata)
|
60
|
+
|
61
|
+
expect(state).to be_persisted
|
62
|
+
expect(state.status).to eq 'completed'
|
63
|
+
expect(state.state_type).to eq 'test_type'
|
64
|
+
expect(state.type).to eq 'HasStates::State'
|
65
|
+
expect(state.metadata).to eq metadata.as_json
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
context 'with custom state class' do
|
70
|
+
before(:all) do
|
71
|
+
module HasStates
|
72
|
+
class CustomState < State; end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
after(:all) do
|
77
|
+
HasStates.send(:remove_const, :CustomState)
|
78
|
+
end
|
79
|
+
|
80
|
+
it 'creates a new state with the specified class' do
|
81
|
+
state = user.add_state('test_type', state_class: HasStates::CustomState)
|
82
|
+
|
83
|
+
expect(state).to be_persisted
|
84
|
+
expect(state.type).to eq 'HasStates::CustomState'
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
context 'when validation fails' do
|
89
|
+
before do
|
90
|
+
allow_any_instance_of(HasStates::Base).to receive(:save!).and_raise(ActiveRecord::RecordInvalid)
|
91
|
+
end
|
92
|
+
|
93
|
+
it 'raises an ActiveRecord::RecordInvalid error' do
|
94
|
+
expect { user.add_state('test_type') }.to raise_error(ActiveRecord::RecordInvalid)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
describe '#current_state' do
|
100
|
+
let!(:latest_state) { user.add_state('test_type', status: 'completed') }
|
101
|
+
|
102
|
+
before do
|
103
|
+
travel_to 1.day.ago do
|
104
|
+
3.times { user.add_state('test_type', status: 'pending') }
|
105
|
+
2.times { user.add_state('other_test_type', status: 'pending') }
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
it 'returns the most recent state for the given type' do
|
110
|
+
expect(user.current_state('test_type')).to eq latest_state
|
111
|
+
end
|
112
|
+
|
113
|
+
|
114
|
+
it 'returns nil when no state exists for the given type' do
|
115
|
+
expect(user.current_state('nonexistent')).to be_nil
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
describe '#current_states' do
|
120
|
+
before do
|
121
|
+
3.times { user.add_state('test_type', status: 'pending') }
|
122
|
+
3.times { user.add_state('other_test_type', status: 'completed') }
|
123
|
+
end
|
124
|
+
|
125
|
+
it 'returns all states for the given type ordered by creation time' do
|
126
|
+
test_types = user.current_states('test_type')
|
127
|
+
|
128
|
+
expect(test_types.size).to eq 3
|
129
|
+
expect(test_types).to eq test_types.sort_by(&:created_at).reverse
|
130
|
+
end
|
131
|
+
|
132
|
+
it 'returns empty relation when no states exist for the given type' do
|
133
|
+
nonexistent_states = user.current_states('nonexistent')
|
134
|
+
expect(nonexistent_states).to be_empty
|
135
|
+
expect(nonexistent_states).to be_a(ActiveRecord::Relation)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
describe 'query methods' do
|
140
|
+
before do
|
141
|
+
2.times { user.add_state('test_type', status: 'pending') }
|
142
|
+
2.times { user.add_state('other_test_type', status: 'pending') }
|
143
|
+
end
|
144
|
+
|
145
|
+
context 'find one methods' do
|
146
|
+
it 'defines query one methods for configured states' do
|
147
|
+
expect(user).to respond_to("test_type")
|
148
|
+
expect(user).to respond_to("other_test_type")
|
149
|
+
end
|
150
|
+
|
151
|
+
it 'returns the most recent state for the given status' do
|
152
|
+
expect(user.test_type).to eq user.current_state('test_type')
|
153
|
+
expect(user.other_test_type).to eq user.current_state('other_test_type')
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
context 'find many methods' do
|
158
|
+
it 'defines query all methods for configured states' do
|
159
|
+
expect(user).to respond_to("test_types")
|
160
|
+
expect(user).to respond_to("other_test_types")
|
161
|
+
end
|
162
|
+
|
163
|
+
it 'returns all states for the given status ordered by creation time' do
|
164
|
+
test_types = user.test_types
|
165
|
+
other_test_types = user.other_test_types
|
166
|
+
|
167
|
+
expect(test_types.size).to eq 2
|
168
|
+
expect(other_test_types.size).to eq 2
|
169
|
+
|
170
|
+
expect(test_types).to eq test_types.sort_by(&:created_at).reverse
|
171
|
+
expect(other_test_types).to eq other_test_types.sort_by(&:created_at).reverse
|
172
|
+
end
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
# Optional: Test the destruction of associated states
|
177
|
+
describe 'destroying the stateable object' do
|
178
|
+
let!(:state) { user.add_state('test_type') }
|
179
|
+
|
180
|
+
it 'destroys associated states when the user is destroyed' do
|
181
|
+
expect { user.destroy }.to change { HasStates::Base.count }.by(-1)
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
data/spec/rails_helper.rb
CHANGED
@@ -14,5 +14,6 @@ require_relative 'support/database_cleaner'
|
|
14
14
|
RSpec.configure do |config|
|
15
15
|
config.include(Shoulda::Matchers::ActiveModel, type: :model)
|
16
16
|
config.include(Shoulda::Matchers::ActiveRecord, type: :model)
|
17
|
+
config.include(ActiveSupport::Testing::TimeHelpers)
|
17
18
|
# ... rest of your RSpec configuration
|
18
19
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: stateful_models
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sebastian Scholl
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2025-01-14 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: "\n HasStates provides state management and event system capabilities
|
14
14
|
for Ruby objects.\n It allows tracking states, state transitions, and triggering
|
@@ -24,8 +24,10 @@ files:
|
|
24
24
|
- README.md
|
25
25
|
- lib/generators/has_states/install/install_generator.rb
|
26
26
|
- lib/generators/has_states/install/templates/create_has_states_states.rb.erb
|
27
|
+
- lib/generators/has_states/install/templates/create_indexes_on_has_states_states.rb.erb
|
27
28
|
- lib/generators/has_states/install/templates/initializer.rb.erb
|
28
29
|
- lib/has_states.rb
|
30
|
+
- lib/has_states/base.rb
|
29
31
|
- lib/has_states/callback.rb
|
30
32
|
- lib/has_states/configuration.rb
|
31
33
|
- lib/has_states/configuration/model_configuration.rb
|
@@ -62,13 +64,15 @@ files:
|
|
62
64
|
- spec/dummy/config/environments/test.rb
|
63
65
|
- spec/dummy/config/initializers/cors.rb
|
64
66
|
- spec/dummy/config/initializers/filter_parameter_logging.rb
|
67
|
+
- spec/dummy/config/initializers/has_states.rb
|
65
68
|
- spec/dummy/config/initializers/inflections.rb
|
66
69
|
- spec/dummy/config/locales/en.yml
|
67
70
|
- spec/dummy/config/master.key
|
68
71
|
- spec/dummy/config/puma.rb
|
69
72
|
- spec/dummy/config/routes.rb
|
70
73
|
- spec/dummy/db/migrate/20241221171423_create_test_models.rb
|
71
|
-
- spec/dummy/db/migrate/
|
74
|
+
- spec/dummy/db/migrate/20241223212128_create_has_states_states.rb
|
75
|
+
- spec/dummy/db/migrate/20250114175939_create_indexes_on_has_states_states.rb
|
72
76
|
- spec/dummy/db/schema.rb
|
73
77
|
- spec/dummy/db/seeds.rb
|
74
78
|
- spec/dummy/log/development.log
|
@@ -80,10 +84,12 @@ files:
|
|
80
84
|
- spec/factories/has_states.rb
|
81
85
|
- spec/generators/has_states/install_generator_spec.rb
|
82
86
|
- spec/generators/tmp/config/initializers/has_states.rb
|
83
|
-
- spec/generators/tmp/db/migrate/
|
87
|
+
- spec/generators/tmp/db/migrate/20250114180401_create_has_states_states.rb
|
88
|
+
- spec/generators/tmp/db/migrate/20250114180401_create_indexes_on_has_states_states.rb
|
84
89
|
- spec/has_states/callback_spec.rb
|
85
90
|
- spec/has_states/configuration_spec.rb
|
86
91
|
- spec/has_states/state_spec.rb
|
92
|
+
- spec/has_states/stateable_spec.rb
|
87
93
|
- spec/has_states_spec.rb
|
88
94
|
- spec/rails_helper.rb
|
89
95
|
- spec/spec_helper.rb
|