meta_states 0.1.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 (83) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +46 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +255 -0
  5. data/lib/generators/meta_states/install/install_generator.rb +43 -0
  6. data/lib/generators/meta_states/install/templates/create_indexes_on_meta_states_states.rb.erb +11 -0
  7. data/lib/generators/meta_states/install/templates/create_meta_states_states.rb.erb +18 -0
  8. data/lib/generators/meta_states/install/templates/initializer.rb.erb +42 -0
  9. data/lib/meta_states/base.rb +70 -0
  10. data/lib/meta_states/callback.rb +58 -0
  11. data/lib/meta_states/configuration/model_configuration.rb +64 -0
  12. data/lib/meta_states/configuration/state_type_configuration.rb +17 -0
  13. data/lib/meta_states/configuration.rb +142 -0
  14. data/lib/meta_states/railtie.rb +8 -0
  15. data/lib/meta_states/state.rb +5 -0
  16. data/lib/meta_states/stateable.rb +26 -0
  17. data/lib/meta_states/version.rb +5 -0
  18. data/lib/meta_states.rb +25 -0
  19. data/spec/dummy/Gemfile +40 -0
  20. data/spec/dummy/Gemfile.lock +308 -0
  21. data/spec/dummy/README.md +24 -0
  22. data/spec/dummy/Rakefile +8 -0
  23. data/spec/dummy/app/controllers/application_controller.rb +4 -0
  24. data/spec/dummy/app/jobs/application_job.rb +9 -0
  25. data/spec/dummy/app/models/application_record.rb +5 -0
  26. data/spec/dummy/app/models/company.rb +3 -0
  27. data/spec/dummy/app/models/user.rb +3 -0
  28. data/spec/dummy/bin/brakeman +9 -0
  29. data/spec/dummy/bin/dev +4 -0
  30. data/spec/dummy/bin/docker-entrypoint +14 -0
  31. data/spec/dummy/bin/rails +6 -0
  32. data/spec/dummy/bin/rake +6 -0
  33. data/spec/dummy/bin/rubocop +10 -0
  34. data/spec/dummy/bin/setup +36 -0
  35. data/spec/dummy/bin/thrust +7 -0
  36. data/spec/dummy/config/application.rb +27 -0
  37. data/spec/dummy/config/boot.rb +5 -0
  38. data/spec/dummy/config/credentials.yml.enc +1 -0
  39. data/spec/dummy/config/database.yml +37 -0
  40. data/spec/dummy/config/environment.rb +7 -0
  41. data/spec/dummy/config/environments/development.rb +54 -0
  42. data/spec/dummy/config/environments/production.rb +69 -0
  43. data/spec/dummy/config/environments/test.rb +44 -0
  44. data/spec/dummy/config/initializers/cors.rb +18 -0
  45. data/spec/dummy/config/initializers/filter_parameter_logging.rb +10 -0
  46. data/spec/dummy/config/initializers/inflections.rb +18 -0
  47. data/spec/dummy/config/initializers/meta_states.rb +45 -0
  48. data/spec/dummy/config/locales/en.yml +31 -0
  49. data/spec/dummy/config/master.key +1 -0
  50. data/spec/dummy/config/puma.rb +43 -0
  51. data/spec/dummy/config/routes.rb +12 -0
  52. data/spec/dummy/config.ru +8 -0
  53. data/spec/dummy/db/migrate/20241221171423_create_test_models.rb +15 -0
  54. data/spec/dummy/db/migrate/20241223212128_create_has_states_states.rb +18 -0
  55. data/spec/dummy/db/migrate/20250114175939_create_indexes_on_has_states_states.rb +10 -0
  56. data/spec/dummy/db/schema.rb +44 -0
  57. data/spec/dummy/db/seeds.rb +11 -0
  58. data/spec/dummy/log/development.log +416 -0
  59. data/spec/dummy/log/test.log +55143 -0
  60. data/spec/dummy/public/robots.txt +1 -0
  61. data/spec/dummy/storage/development.sqlite3 +0 -0
  62. data/spec/dummy/storage/test.sqlite3 +0 -0
  63. data/spec/dummy/tmp/local_secret.txt +1 -0
  64. data/spec/factories/meta_states.rb +19 -0
  65. data/spec/generators/meta_states/install_generator_spec.rb +27 -0
  66. data/spec/generators/templates/config/initializers/meta_states.rb +42 -0
  67. data/spec/generators/templates/db/migrate/20250605170637_create_indexes_on_meta_states_states.rb +11 -0
  68. data/spec/generators/templates/db/migrate/20250605170637_create_meta_states_states.rb +18 -0
  69. data/spec/meta_states/callback_spec.rb +92 -0
  70. data/spec/meta_states/configuration_spec.rb +218 -0
  71. data/spec/meta_states/state_limit_spec.rb +107 -0
  72. data/spec/meta_states/state_metadata_schema_spec.rb +75 -0
  73. data/spec/meta_states/state_spec.rb +349 -0
  74. data/spec/meta_states/stateable_spec.rb +183 -0
  75. data/spec/meta_states_spec.rb +52 -0
  76. data/spec/rails_helper.rb +19 -0
  77. data/spec/spec_helper.rb +16 -0
  78. data/spec/support/database_cleaner.rb +17 -0
  79. data/spec/support/factory_bot.rb +8 -0
  80. data/spec/support/shoulda_matchers.rb +10 -0
  81. data/spec/tmp/config/initializers/has_states.rb +12 -0
  82. data/spec/tmp/db/migrate/20241223004024_create_has_states_states.rb +20 -0
  83. metadata +141 -0
@@ -0,0 +1,349 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails_helper'
4
+
5
+ RSpec.describe MetaStates::State, type: :model do
6
+ subject { build(:state) }
7
+
8
+ before do
9
+ MetaStates.configuration.clear_callbacks!
10
+ MetaStates.configuration.model_configurations.clear
11
+
12
+ MetaStates.configure do |config|
13
+ config.configure_model User do |model|
14
+ model.state_type :kyc do |type|
15
+ type.statuses = %w[pending completed]
16
+ end
17
+
18
+ model.state_type :onboarding do |type|
19
+ type.statuses = %w[pending completed]
20
+ end
21
+ end
22
+ end
23
+ end
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
+
47
+ describe 'validations' do
48
+ context 'status' do
49
+ it 'validates status presence' do
50
+ expect(subject).not_to allow_value(nil).for(:status)
51
+ end
52
+
53
+ it 'validates status is configured' do
54
+ expect(subject).to allow_value('pending').for(:status)
55
+ end
56
+
57
+ it 'validates status that is not configured' do
58
+ expect(subject).not_to allow_value('invalid').for(:status)
59
+ end
60
+ end
61
+
62
+ context 'state_type' do
63
+ it 'validates state_type presence' do
64
+ expect(subject).not_to allow_value(nil).for(:state_type)
65
+ end
66
+
67
+ it 'validates state_type is configured' do
68
+ expect(subject).to allow_value('kyc').for(:state_type)
69
+ end
70
+
71
+ it 'validates state_type that is not configured' do
72
+ expect(subject).not_to allow_value('invalid').for(:state_type)
73
+ end
74
+ end
75
+ end
76
+
77
+ describe 'scopes' do
78
+ let(:state) { create(:state, state_type: 'kyc') }
79
+
80
+ it 'defines a scope for each state type' do
81
+ expect(described_class.kyc).to eq([state])
82
+ end
83
+ end
84
+
85
+ describe 'instance methods' do
86
+ let(:state) { create(:state, state_type: 'kyc', status: 'pending') }
87
+ let(:model_config) { MetaStates.configuration.model_configurations[User] }
88
+ let(:all_statuses) { model_config.state_types.values.flat_map(&:statuses).uniq }
89
+
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
95
+ end
96
+
97
+ it 'returns true if the status is pending' do
98
+ expect(state.pending?).to be(true)
99
+ end
100
+
101
+ it 'returns false if the status is not completed' do
102
+ expect(state.completed?).to be(false)
103
+ end
104
+ end
105
+ end
106
+
107
+ describe 'metadata' do
108
+ let(:user) { FactoryBot.create(:user) }
109
+
110
+ it 'stores and retrieves simple metadata' do
111
+ state = user.add_state('kyc', metadata: { reason: 'documents_missing' })
112
+ expect(state.metadata['reason']).to eq('documents_missing')
113
+ end
114
+
115
+ it 'stores and retrieves nested metadata' do
116
+ metadata = {
117
+ documents: {
118
+ passport: { status: 'rejected', reason: 'expired' },
119
+ utility_bill: { status: 'pending' }
120
+ }
121
+ }
122
+
123
+ state = user.add_state('kyc', metadata: metadata)
124
+ expect(state.metadata['documents']['passport']['reason']).to eq('expired')
125
+ end
126
+
127
+ it 'handles arrays in metadata' do
128
+ metadata = {
129
+ missing_documents: %w[passport utility_bill],
130
+ review_history: [
131
+ { date: '2024-01-01', status: 'rejected' },
132
+ { date: '2024-01-02', status: 'approved' }
133
+ ]
134
+ }
135
+
136
+ state = user.add_state('kyc', metadata: metadata)
137
+ expect(state.metadata['missing_documents']).to eq(%w[passport utility_bill])
138
+ expect(state.metadata['review_history'].size).to eq(2)
139
+ end
140
+
141
+ it 'handles different data types' do
142
+ metadata = {
143
+ string_value: 'test',
144
+ integer_value: 42,
145
+ float_value: 42.5,
146
+ boolean_value: true,
147
+ null_value: nil,
148
+ date_value: '2024-01-01'
149
+ }
150
+
151
+ state = user.add_state('kyc', metadata: metadata)
152
+ expect(state.metadata['string_value']).to eq('test')
153
+ expect(state.metadata['integer_value']).to eq(42)
154
+ expect(state.metadata['float_value']).to eq(42.5)
155
+ expect(state.metadata['boolean_value']).to be true
156
+ expect(state.metadata['null_value']).to be_nil
157
+ expect(state.metadata['date_value']).to eq('2024-01-01')
158
+ end
159
+
160
+ it 'defaults to empty hash when no metadata provided' do
161
+ state = user.add_state('kyc')
162
+ expect(state.metadata).to eq({})
163
+ end
164
+
165
+ it 'persists metadata across database reads' do
166
+ state = user.add_state('kyc', metadata: { key: 'value' })
167
+ reloaded_state = MetaStates::State.find(state.id)
168
+ expect(reloaded_state.metadata['key']).to eq('value')
169
+ end
170
+ end
171
+
172
+ describe 'callbacks' do
173
+ let(:user) { create(:user) }
174
+ let(:callback_executed) { false }
175
+
176
+ before do
177
+ MetaStates.configure do |config|
178
+ config.configure_model User do |model|
179
+ model.state_type :onboarding do |type|
180
+ type.statuses = %w[pending completed]
181
+ end
182
+ end
183
+
184
+ # Register a callback for when onboarding is completed
185
+ config.on(:onboarding, id: :complete_onboarding, to: 'completed') do |state|
186
+ state.stateable.update!(name: 'Onboarded User')
187
+ end
188
+ end
189
+ end
190
+
191
+ it 'executes callback when state changes to completed' do
192
+ # Create initial pending state
193
+ state = user.add_state('onboarding', status: 'pending')
194
+ expect(user.name).not_to eq('Onboarded User')
195
+
196
+ # Update to completed
197
+ state.update!(status: 'completed')
198
+
199
+ # Verify callback was executed
200
+ expect(user.reload.name).to eq('Onboarded User')
201
+ end
202
+
203
+ it 'does not execute callback for other status changes' do
204
+ state = user.add_state('onboarding', status: 'completed')
205
+ user.update!(name: 'Original Name')
206
+
207
+ # Update to pending
208
+ state.update!(status: 'pending')
209
+
210
+ # Verify callback was not executed
211
+ expect(user.reload.name).to eq('Original Name')
212
+ end
213
+ end
214
+
215
+ describe 'limited execution callbacks' do
216
+ let(:user) { create(:user) }
217
+
218
+ before do
219
+ # Clear ALL configuration
220
+ MetaStates.configuration.clear_callbacks!
221
+ MetaStates.configuration.model_configurations.clear
222
+
223
+ # Set up fresh configuration
224
+ MetaStates.configure do |config|
225
+ config.configure_model User do |model|
226
+ model.state_type :onboarding do |type|
227
+ type.statuses = %w[pending completed]
228
+ end
229
+ end
230
+ end
231
+
232
+ @execution_count = 0
233
+ end
234
+
235
+ it 'executes callback only specified number of times' do
236
+ MetaStates.configure do |config|
237
+ config.on(:onboarding, id: :counter, to: 'completed', times: 2) do |state|
238
+ state.stateable.update!(name: "Execution #{@execution_count += 1}")
239
+ end
240
+ end
241
+
242
+ # First execution
243
+ state1 = user.add_state('onboarding', status: 'pending')
244
+ state1.update!(status: 'completed')
245
+ expect(user.reload.name).to eq('Execution 1')
246
+
247
+ # Second execution
248
+ state2 = user.add_state('onboarding', status: 'pending')
249
+ state2.update!(status: 'completed')
250
+ expect(user.reload.name).to eq('Execution 2')
251
+
252
+ # Third execution - callback should be expired
253
+ user.update!(name: 'Final Name')
254
+ state3 = user.add_state('onboarding', status: 'pending')
255
+ state3.update!(status: 'completed')
256
+ expect(user.reload.name).to eq('Final Name') # Name shouldn't change
257
+ end
258
+
259
+ it 'keeps callback active indefinitely when times is not specified' do
260
+ MetaStates.configure do |config|
261
+ config.on(:onboarding, id: :infinite, to: 'completed') do |state|
262
+ state.stateable.update!(name: "Execution #{@execution_count += 1}")
263
+ end
264
+ end
265
+
266
+ 3.times do |i|
267
+ state = user.add_state('onboarding', status: 'pending')
268
+ state.update!(status: 'completed')
269
+ expect(user.reload.name).to eq("Execution #{i + 1}")
270
+ end
271
+ end
272
+
273
+ it 'removes expired callbacks from configuration' do
274
+ MetaStates.configure do |config|
275
+ config.on(:onboarding, id: :one_time, to: 'completed', times: 1) do |state|
276
+ state.stateable.update!(name: 'Executed')
277
+ end
278
+ end
279
+
280
+ expect do
281
+ state = user.add_state('onboarding', status: 'pending')
282
+ state.update!(status: 'completed')
283
+ end.to change { MetaStates.configuration.callbacks.size }.from(1).to(0)
284
+ end
285
+ end
286
+
287
+ describe 'custom state types' do
288
+ # Create a custom state class for testing
289
+ class KYCState < MetaStates::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
+
298
+ errors.add(:metadata, 'must include document_type')
299
+ end
300
+ end
301
+
302
+ let(:user) { create(:user) }
303
+
304
+ it 'allows creation of custom state types' do
305
+ state = user.add_state(
306
+ 'kyc',
307
+ status: 'pending',
308
+ metadata: { document_type: 'passport' },
309
+ state_class: KYCState
310
+ )
311
+
312
+ expect(state).to be_valid
313
+ expect(state).to be_a(KYCState)
314
+ end
315
+
316
+ it 'enforces custom validations' do
317
+ expect do
318
+ user.add_state(
319
+ 'kyc',
320
+ status: 'pending',
321
+ metadata: { other_field: 'value' },
322
+ state_class: KYCState
323
+ )
324
+ end.to raise_error(
325
+ ActiveRecord::RecordInvalid,
326
+ /Metadata must include document_type/
327
+ )
328
+ end
329
+
330
+ it 'defaults to MetaStates::State when no state_class specified' do
331
+ state = user.add_state('kyc', status: 'pending')
332
+
333
+ expect(state).to be_a(MetaStates::State)
334
+ expect(state).to be_valid
335
+ end
336
+
337
+ it 'maintains STI type across database reads' do
338
+ state = user.add_state(
339
+ 'kyc',
340
+ status: 'pending',
341
+ metadata: { document_type: 'passport' },
342
+ state_class: KYCState
343
+ )
344
+
345
+ reloaded_state = MetaStates::Base.find(state.id)
346
+ expect(reloaded_state).to be_a(KYCState)
347
+ end
348
+ end
349
+ end
@@ -0,0 +1,183 @@
1
+ require 'rails_helper'
2
+
3
+ RSpec.describe MetaStates::Stateable do
4
+ # Create a test class that includes the concern
5
+ let(:test_class) do
6
+ Class.new do
7
+ include MetaStates::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
+ # MetaStates.configuration.clear_callbacks!
16
+ # MetaStates.configuration.model_configurations.clear
17
+
18
+ MetaStates.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 'MetaStates::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 'MetaStates::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 'MetaStates::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 MetaStates
72
+ class CustomState < State; end
73
+ end
74
+ end
75
+
76
+ after(:all) do
77
+ MetaStates.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: MetaStates::CustomState)
82
+
83
+ expect(state).to be_persisted
84
+ expect(state.type).to eq 'MetaStates::CustomState'
85
+ end
86
+ end
87
+
88
+ context 'when validation fails' do
89
+ before do
90
+ allow_any_instance_of(MetaStates::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
+ it 'returns nil when no state exists for the given type' do
114
+ expect(user.current_state('nonexistent')).to be_nil
115
+ end
116
+ end
117
+
118
+ describe '#current_states' do
119
+ before do
120
+ 3.times { user.add_state('test_type', status: 'pending') }
121
+ 3.times { user.add_state('other_test_type', status: 'completed') }
122
+ end
123
+
124
+ it 'returns all states for the given type ordered by creation time' do
125
+ test_types = user.current_states('test_type')
126
+
127
+ expect(test_types.size).to eq 3
128
+ expect(test_types).to eq test_types.sort_by(&:created_at).reverse
129
+ end
130
+
131
+ it 'returns empty relation when no states exist for the given type' do
132
+ nonexistent_states = user.current_states('nonexistent')
133
+ expect(nonexistent_states).to be_empty
134
+ expect(nonexistent_states).to be_a(ActiveRecord::Relation)
135
+ end
136
+ end
137
+
138
+ describe 'query methods' do
139
+ before do
140
+ 2.times { user.add_state('test_type', status: 'pending') }
141
+ 2.times { user.add_state('other_test_type', status: 'pending') }
142
+ end
143
+
144
+ context 'find one methods' do
145
+ it 'defines query one methods for configured states' do
146
+ expect(user).to respond_to('test_type')
147
+ expect(user).to respond_to('other_test_type')
148
+ end
149
+
150
+ it 'returns the most recent state for the given status' do
151
+ expect(user.test_type).to eq user.current_state('test_type')
152
+ expect(user.other_test_type).to eq user.current_state('other_test_type')
153
+ end
154
+ end
155
+
156
+ context 'find many methods' do
157
+ it 'defines query all methods for configured states' do
158
+ expect(user).to respond_to('test_types')
159
+ expect(user).to respond_to('other_test_types')
160
+ end
161
+
162
+ it 'returns all states for the given status ordered by creation time' do
163
+ test_types = user.test_types
164
+ other_test_types = user.other_test_types
165
+
166
+ expect(test_types.size).to eq 2
167
+ expect(other_test_types.size).to eq 2
168
+
169
+ expect(test_types).to eq test_types.sort_by(&:created_at).reverse
170
+ expect(other_test_types).to eq other_test_types.sort_by(&:created_at).reverse
171
+ end
172
+ end
173
+ end
174
+
175
+ # Optional: Test the destruction of associated states
176
+ describe 'destroying the stateable object' do
177
+ let!(:state) { user.add_state('test_type') }
178
+
179
+ it 'destroys associated states when the user is destroyed' do
180
+ expect { user.destroy }.to change { MetaStates::Base.count }.by(-1)
181
+ end
182
+ end
183
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe MetaStates do
4
+ it 'has a version number' do
5
+ expect(MetaStates::VERSION).not_to be_nil
6
+ end
7
+
8
+ it 'has a configuration' do
9
+ expect(described_class.configuration).to be_a(MetaStates::Configuration)
10
+ end
11
+
12
+ describe 'configuration' do
13
+ before do
14
+ described_class.configure do |config|
15
+ # User configuration
16
+ config.configure_model User do |model|
17
+ model.state_type :kyc do |type|
18
+ type.statuses = %w[pending completed]
19
+ end
20
+
21
+ model.state_type :onboarding do |type|
22
+ type.statuses = %w[started finished]
23
+ end
24
+ end
25
+
26
+ # Company configuration
27
+ config.configure_model Company do |model|
28
+ model.state_type :procurement do |type|
29
+ type.statuses = %w[in_progress completed failed]
30
+ end
31
+ end
32
+ end
33
+ end
34
+
35
+ it 'configures the state types for User' do
36
+ expect(described_class.configuration.state_types_for(User).keys).to eq(%w[kyc onboarding])
37
+ end
38
+
39
+ it 'configures the statuses for User' do
40
+ expect(described_class.configuration.statuses_for(User, 'kyc')).to eq(%w[pending completed])
41
+ expect(described_class.configuration.statuses_for(User, 'onboarding')).to eq(%w[started finished])
42
+ end
43
+
44
+ it 'configures the state types for Company' do
45
+ expect(described_class.configuration.state_types_for(Company).keys).to eq(['procurement'])
46
+ end
47
+
48
+ it 'configures the statuses for Company' do
49
+ expect(described_class.configuration.statuses_for(Company, 'procurement')).to eq(%w[in_progress completed failed])
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ # Load dummy Rails app if in Rails context
6
+ ENV['RAILS_ENV'] = 'test'
7
+ require File.expand_path('dummy/config/environment.rb', __dir__)
8
+
9
+ # Load support files
10
+ require_relative 'support/factory_bot'
11
+ require_relative 'support/shoulda_matchers'
12
+ require_relative 'support/database_cleaner'
13
+
14
+ RSpec.configure do |config|
15
+ config.include(Shoulda::Matchers::ActiveModel, type: :model)
16
+ config.include(Shoulda::Matchers::ActiveRecord, type: :model)
17
+ config.include(ActiveSupport::Testing::TimeHelpers)
18
+ # ... rest of your RSpec configuration
19
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+ require 'meta_states'
5
+
6
+ RSpec.configure do |config|
7
+ # Enable flags like --only-failures and --next-failure
8
+ config.example_status_persistence_file_path = '.rspec_status'
9
+
10
+ # Disable RSpec exposing methods globally on `Module` and `main`
11
+ config.disable_monkey_patching!
12
+
13
+ config.expect_with :rspec do |c|
14
+ c.syntax = :expect
15
+ end
16
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'database_cleaner/active_record'
4
+
5
+ RSpec.configure do |config|
6
+ # Database Cleaner setup
7
+ config.before(:suite) do
8
+ DatabaseCleaner.strategy = :transaction
9
+ DatabaseCleaner.clean_with(:truncation)
10
+ end
11
+
12
+ config.around do |example|
13
+ DatabaseCleaner.cleaning do
14
+ example.run
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'factory_bot'
4
+ require_relative '../factories/meta_states'
5
+
6
+ RSpec.configure do |config|
7
+ config.include FactoryBot::Syntax::Methods
8
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'shoulda-matchers'
4
+
5
+ Shoulda::Matchers.configure do |config|
6
+ config.integrate do |with|
7
+ with.test_framework :rspec
8
+ with.library :rails
9
+ end
10
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ MetaStates.configure do |config|
4
+ # Configure your models here
5
+ # Example:
6
+ #
7
+ # config.configure_model User do |model|
8
+ # model.state_type :kyc do |type|
9
+ # type.statuses = ['pending', 'completed', 'rejected']
10
+ # end
11
+ # end
12
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ class CreateMetaStatesStates < ActiveRecord::Migration[8.0]
4
+ def change
5
+ create_table :meta_states_states do |t|
6
+ t.string :state_type
7
+ t.string :status, null: false
8
+
9
+ t.json :metadata, null: false, default: {}
10
+
11
+ t.references :stateable, polymorphic: true, null: false
12
+
13
+ t.datetime :completed_at
14
+ t.timestamps
15
+
16
+ t.index [:status]
17
+ t.index %i[stateable_type stateable_id]
18
+ end
19
+ end
20
+ end