stateful_models 0.0.3 → 0.0.5
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 +13 -2
- data/README.md +75 -1
- data/lib/generators/has_states/install/install_generator.rb +6 -6
- data/lib/has_states/base.rb +31 -2
- data/lib/has_states/configuration/state_type_configuration.rb +3 -1
- data/lib/has_states/configuration.rb +75 -16
- data/lib/has_states/version.rb +1 -1
- data/lib/has_states.rb +1 -1
- data/spec/dummy/Gemfile.lock +123 -101
- data/spec/dummy/config/initializers/has_states.rb +3 -3
- data/spec/dummy/db/migrate/20241223212128_create_has_states_states.rb +2 -2
- data/spec/dummy/db/migrate/20250114175939_create_indexes_on_has_states_states.rb +3 -3
- data/spec/dummy/db/schema.rb +26 -26
- data/spec/dummy/log/development.log +7 -0
- data/spec/dummy/log/test.log +28108 -0
- data/spec/generators/has_states/install_generator_spec.rb +1 -1
- data/spec/has_states/configuration_spec.rb +57 -0
- data/spec/has_states/state_limit_spec.rb +107 -0
- data/spec/has_states/state_metadata_schema_spec.rb +75 -0
- data/spec/has_states/state_spec.rb +4 -3
- data/spec/has_states/stateable_spec.rb +9 -10
- metadata +23 -7
- /data/spec/generators/{tmp → templates}/config/initializers/has_states.rb +0 -0
- /data/spec/generators/{tmp/db/migrate/20250114180401_create_has_states_states.rb → templates/db/migrate/20250414184759_create_has_states_states.rb} +0 -0
- /data/spec/generators/{tmp/db/migrate/20250114180401_create_indexes_on_has_states_states.rb → templates/db/migrate/20250414184759_create_indexes_on_has_states_states.rb} +0 -0
@@ -4,7 +4,7 @@ require 'rails_helper'
|
|
4
4
|
require 'generators/has_states/install/install_generator'
|
5
5
|
|
6
6
|
RSpec.describe HasStates::InstallGenerator do
|
7
|
-
let(:destination) { File.expand_path('../
|
7
|
+
let(:destination) { File.expand_path('../templates', __dir__) }
|
8
8
|
|
9
9
|
before do
|
10
10
|
FileUtils.rm_rf(destination)
|
@@ -74,6 +74,63 @@ RSpec.describe HasStates::Configuration do
|
|
74
74
|
end
|
75
75
|
end.to raise_error(ArgumentError, /must be an ActiveRecord model/)
|
76
76
|
end
|
77
|
+
|
78
|
+
it 'allows setting a limit on the number of states' do
|
79
|
+
configuration.configure_model User do |model|
|
80
|
+
model.state_type :kyc do |type|
|
81
|
+
type.statuses = %w[pending completed]
|
82
|
+
type.limit = 2
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
expect(configuration.limit_for(User, 'kyc')).to eq(2)
|
87
|
+
end
|
88
|
+
|
89
|
+
it 'returns nil for limit when no limit is set' do
|
90
|
+
configuration.configure_model User do |model|
|
91
|
+
model.state_type :kyc do |type|
|
92
|
+
type.statuses = %w[pending completed]
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
expect(configuration.limit_for(User, 'kyc')).to be_nil
|
97
|
+
end
|
98
|
+
|
99
|
+
context 'when a metadata schema is set' do
|
100
|
+
let(:schema) do
|
101
|
+
{
|
102
|
+
type: :object,
|
103
|
+
properties: {
|
104
|
+
name: { type: :string },
|
105
|
+
age: { type: :integer, minimum: 18 }
|
106
|
+
},
|
107
|
+
required: %i[name age]
|
108
|
+
}
|
109
|
+
end
|
110
|
+
|
111
|
+
before do
|
112
|
+
configuration.configure_model User do |model|
|
113
|
+
model.state_type :kyc do |type|
|
114
|
+
type.statuses = %w[pending completed]
|
115
|
+
type.metadata_schema = schema
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
it 'allows setting a metadata schema for validation' do
|
121
|
+
expect(configuration.metadata_schema_for(User, 'kyc')).to eq(schema)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
it 'returns nil for metadata_schema when no schema is set' do
|
126
|
+
configuration.configure_model User do |model|
|
127
|
+
model.state_type :kyc do |type|
|
128
|
+
type.statuses = %w[pending completed]
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
expect(configuration.metadata_schema_for(User, 'kyc')).to be_nil
|
133
|
+
end
|
77
134
|
end
|
78
135
|
|
79
136
|
describe 'callbacks' do
|
@@ -0,0 +1,107 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails_helper'
|
4
|
+
|
5
|
+
RSpec.describe 'State Limit Feature', type: :model do
|
6
|
+
before do
|
7
|
+
HasStates.configuration.clear_callbacks!
|
8
|
+
HasStates.configuration.model_configurations.clear
|
9
|
+
|
10
|
+
HasStates.configure do |config|
|
11
|
+
config.configure_model User do |model|
|
12
|
+
model.state_type :double_limit_state do |type|
|
13
|
+
type.statuses = %w[pending completed]
|
14
|
+
type.limit = 2
|
15
|
+
end
|
16
|
+
|
17
|
+
model.state_type :no_limit_state do |type|
|
18
|
+
type.statuses = %w[pending completed]
|
19
|
+
# No limit set for this type
|
20
|
+
end
|
21
|
+
|
22
|
+
model.state_type :single_limit_state do |type|
|
23
|
+
type.statuses = %w[pending completed]
|
24
|
+
type.limit = 1
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
describe 'state limit validation' do
|
31
|
+
let(:user) { create(:user) }
|
32
|
+
|
33
|
+
context 'with no limit set' do
|
34
|
+
it 'allows creating multiple states of the same type' do
|
35
|
+
# Create multiple onboarding states (no limit)
|
36
|
+
expect do
|
37
|
+
3.times { user.add_state('no_limit_state', status: 'pending') }
|
38
|
+
end.not_to raise_error
|
39
|
+
|
40
|
+
expect(user.states.where(state_type: 'no_limit_state').count).to eq(3)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
context 'with a limit set' do
|
45
|
+
it 'allows creating states up to the limit' do
|
46
|
+
# Create double_limit_state states up to the limit (2)
|
47
|
+
expect do
|
48
|
+
2.times { user.add_state('double_limit_state', status: 'pending') }
|
49
|
+
end.not_to raise_error
|
50
|
+
|
51
|
+
expect(user.states.where(state_type: 'double_limit_state').count).to eq(2)
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'prevents creating states beyond the limit' do
|
55
|
+
# Create double_limit_state states up to the limit
|
56
|
+
2.times { user.add_state('double_limit_state', status: 'pending') }
|
57
|
+
|
58
|
+
# Try to create one more (exceeding the limit)
|
59
|
+
expect do
|
60
|
+
user.add_state('double_limit_state', status: 'pending')
|
61
|
+
end.to raise_error(ActiveRecord::RecordInvalid, /maximum number of double_limit_state states/)
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'enforces a limit of 1 correctly' do
|
65
|
+
# Create one state
|
66
|
+
user.add_state('single_limit_state', status: 'pending')
|
67
|
+
|
68
|
+
# Try to create another one (exceeding the limit of 1)
|
69
|
+
expect do
|
70
|
+
user.add_state('single_limit_state', status: 'completed')
|
71
|
+
end.to raise_error(ActiveRecord::RecordInvalid, /maximum number of single_limit_state states/)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
context 'with different state types' do
|
76
|
+
it 'applies limits independently to each state type' do
|
77
|
+
# Create states up to the limit for double_limit_state
|
78
|
+
2.times { user.add_state('double_limit_state', status: 'pending') }
|
79
|
+
|
80
|
+
# Create states for no_limit_state (no limit)
|
81
|
+
3.times { user.add_state('no_limit_state', status: 'pending') }
|
82
|
+
|
83
|
+
# Create one state for single_limit_state (limit 1)
|
84
|
+
user.add_state('single_limit_state', status: 'pending')
|
85
|
+
|
86
|
+
# Verify counts
|
87
|
+
expect(user.states.where(state_type: 'double_limit_state').count).to eq(2)
|
88
|
+
expect(user.states.where(state_type: 'no_limit_state').count).to eq(3)
|
89
|
+
expect(user.states.where(state_type: 'single_limit_state').count).to eq(1)
|
90
|
+
|
91
|
+
# Try to exceed limits
|
92
|
+
expect do
|
93
|
+
user.add_state('double_limit_state', status: 'completed')
|
94
|
+
end.to raise_error(ActiveRecord::RecordInvalid, /maximum number of double_limit_state states/)
|
95
|
+
|
96
|
+
expect do
|
97
|
+
user.add_state('single_limit_state', status: 'completed')
|
98
|
+
end.to raise_error(ActiveRecord::RecordInvalid, /maximum number of single_limit_state states/)
|
99
|
+
|
100
|
+
# But can still add more no_limit_state states
|
101
|
+
expect do
|
102
|
+
user.add_state('no_limit_state', status: 'completed')
|
103
|
+
end.not_to raise_error
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rails_helper'
|
4
|
+
|
5
|
+
RSpec.describe 'Metadata Schema Validation', type: :model do
|
6
|
+
let(:user) { create(:user) }
|
7
|
+
let(:schema) do
|
8
|
+
{
|
9
|
+
type: :object,
|
10
|
+
properties: {
|
11
|
+
name: { type: :string },
|
12
|
+
age: {
|
13
|
+
type: :integer,
|
14
|
+
minimum: 18
|
15
|
+
}
|
16
|
+
},
|
17
|
+
required: %i[name age]
|
18
|
+
}
|
19
|
+
end
|
20
|
+
|
21
|
+
before do
|
22
|
+
HasStates.configuration.clear_callbacks!
|
23
|
+
HasStates.configuration.model_configurations.clear
|
24
|
+
|
25
|
+
HasStates.configure do |config|
|
26
|
+
config.configure_model User do |model|
|
27
|
+
model.state_type :schema_enabled_state do |type|
|
28
|
+
type.statuses = %w[pending completed rejected]
|
29
|
+
type.metadata_schema = schema
|
30
|
+
end
|
31
|
+
|
32
|
+
model.state_type :no_schema_state do |type|
|
33
|
+
type.statuses = %w[active inactive]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
describe 'validation' do
|
40
|
+
context 'with a valid schema' do
|
41
|
+
it 'validates valid metadata' do
|
42
|
+
# Invalid metadata (too young < 18)
|
43
|
+
expect do
|
44
|
+
user.add_state('schema_enabled_state', status: 'pending', metadata: { name: 'John Doe', age: 17 })
|
45
|
+
end.to raise_error(ActiveRecord::RecordInvalid, /did not have a minimum value of 18/)
|
46
|
+
|
47
|
+
# Invalid metadata (missing required fields)
|
48
|
+
expect do
|
49
|
+
user.add_state('schema_enabled_state', status: 'pending', metadata: { age: 25 })
|
50
|
+
end.to raise_error(ActiveRecord::RecordInvalid, /did not contain a required property of 'name'/)
|
51
|
+
|
52
|
+
# Valid metadata
|
53
|
+
state = user.add_state('schema_enabled_state', status: 'pending', metadata: { name: 'John Doe', age: 25 })
|
54
|
+
|
55
|
+
expect(state).to be_valid
|
56
|
+
expect(state.persisted?).to be true
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
context 'with no schema defined' do
|
61
|
+
it 'accepts any metadata for state types without a schema' do
|
62
|
+
state = user.add_state('no_schema_state', status: 'active', metadata: {
|
63
|
+
anything: 'goes',
|
64
|
+
nested: {
|
65
|
+
data: 'is fine too'
|
66
|
+
},
|
67
|
+
numbers: [1, 2, 3]
|
68
|
+
})
|
69
|
+
|
70
|
+
expect(state).to be_valid
|
71
|
+
expect(state.persisted?).to be true
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -289,11 +289,12 @@ RSpec.describe HasStates::State, type: :model do
|
|
289
289
|
class KYCState < HasStates::Base
|
290
290
|
validates :metadata, presence: true
|
291
291
|
validate :required_metadata_fields
|
292
|
-
|
292
|
+
|
293
293
|
private
|
294
|
-
|
294
|
+
|
295
295
|
def required_metadata_fields
|
296
296
|
return if metadata&.key?('document_type')
|
297
|
+
|
297
298
|
errors.add(:metadata, 'must include document_type')
|
298
299
|
end
|
299
300
|
end
|
@@ -328,7 +329,7 @@ RSpec.describe HasStates::State, type: :model do
|
|
328
329
|
|
329
330
|
it 'defaults to HasStates::State when no state_class specified' do
|
330
331
|
state = user.add_state('kyc', status: 'pending')
|
331
|
-
|
332
|
+
|
332
333
|
expect(state).to be_a(HasStates::State)
|
333
334
|
expect(state).to be_valid
|
334
335
|
end
|
@@ -9,7 +9,7 @@ RSpec.describe HasStates::Stateable do
|
|
9
9
|
end
|
10
10
|
end
|
11
11
|
|
12
|
-
let(:user) { create(:user) }
|
12
|
+
let(:user) { create(:user) } # Assuming you have a User factory
|
13
13
|
|
14
14
|
before do
|
15
15
|
# HasStates.configuration.clear_callbacks!
|
@@ -54,7 +54,7 @@ RSpec.describe HasStates::Stateable do
|
|
54
54
|
|
55
55
|
context 'with custom parameters' do
|
56
56
|
let(:metadata) { { key: 'value' } }
|
57
|
-
|
57
|
+
|
58
58
|
it 'creates a new state with provided values' do
|
59
59
|
state = user.add_state('test_type', status: 'completed', metadata: metadata)
|
60
60
|
|
@@ -98,7 +98,7 @@ RSpec.describe HasStates::Stateable do
|
|
98
98
|
|
99
99
|
describe '#current_state' do
|
100
100
|
let!(:latest_state) { user.add_state('test_type', status: 'completed') }
|
101
|
-
|
101
|
+
|
102
102
|
before do
|
103
103
|
travel_to 1.day.ago do
|
104
104
|
3.times { user.add_state('test_type', status: 'pending') }
|
@@ -110,7 +110,6 @@ RSpec.describe HasStates::Stateable do
|
|
110
110
|
expect(user.current_state('test_type')).to eq latest_state
|
111
111
|
end
|
112
112
|
|
113
|
-
|
114
113
|
it 'returns nil when no state exists for the given type' do
|
115
114
|
expect(user.current_state('nonexistent')).to be_nil
|
116
115
|
end
|
@@ -124,7 +123,7 @@ RSpec.describe HasStates::Stateable do
|
|
124
123
|
|
125
124
|
it 'returns all states for the given type ordered by creation time' do
|
126
125
|
test_types = user.current_states('test_type')
|
127
|
-
|
126
|
+
|
128
127
|
expect(test_types.size).to eq 3
|
129
128
|
expect(test_types).to eq test_types.sort_by(&:created_at).reverse
|
130
129
|
end
|
@@ -144,8 +143,8 @@ RSpec.describe HasStates::Stateable do
|
|
144
143
|
|
145
144
|
context 'find one methods' do
|
146
145
|
it 'defines query one methods for configured states' do
|
147
|
-
expect(user).to respond_to(
|
148
|
-
expect(user).to respond_to(
|
146
|
+
expect(user).to respond_to('test_type')
|
147
|
+
expect(user).to respond_to('other_test_type')
|
149
148
|
end
|
150
149
|
|
151
150
|
it 'returns the most recent state for the given status' do
|
@@ -156,8 +155,8 @@ RSpec.describe HasStates::Stateable do
|
|
156
155
|
|
157
156
|
context 'find many methods' do
|
158
157
|
it 'defines query all methods for configured states' do
|
159
|
-
expect(user).to respond_to(
|
160
|
-
expect(user).to respond_to(
|
158
|
+
expect(user).to respond_to('test_types')
|
159
|
+
expect(user).to respond_to('other_test_types')
|
161
160
|
end
|
162
161
|
|
163
162
|
it 'returns all states for the given status ordered by creation time' do
|
@@ -181,4 +180,4 @@ RSpec.describe HasStates::Stateable do
|
|
181
180
|
expect { user.destroy }.to change { HasStates::Base.count }.by(-1)
|
182
181
|
end
|
183
182
|
end
|
184
|
-
end
|
183
|
+
end
|
metadata
CHANGED
@@ -1,15 +1,29 @@
|
|
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.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sebastian Scholl
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-
|
12
|
-
dependencies:
|
11
|
+
date: 2025-04-14 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: json-schema
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
13
27
|
description: "\n HasStates provides state management and event system capabilities
|
14
28
|
for Ruby objects.\n It allows tracking states, state transitions, and triggering
|
15
29
|
callbacks on state changes.\n "
|
@@ -83,11 +97,13 @@ files:
|
|
83
97
|
- spec/dummy/tmp/local_secret.txt
|
84
98
|
- spec/factories/has_states.rb
|
85
99
|
- spec/generators/has_states/install_generator_spec.rb
|
86
|
-
- spec/generators/
|
87
|
-
- spec/generators/
|
88
|
-
- spec/generators/
|
100
|
+
- spec/generators/templates/config/initializers/has_states.rb
|
101
|
+
- spec/generators/templates/db/migrate/20250414184759_create_has_states_states.rb
|
102
|
+
- spec/generators/templates/db/migrate/20250414184759_create_indexes_on_has_states_states.rb
|
89
103
|
- spec/has_states/callback_spec.rb
|
90
104
|
- spec/has_states/configuration_spec.rb
|
105
|
+
- spec/has_states/state_limit_spec.rb
|
106
|
+
- spec/has_states/state_metadata_schema_spec.rb
|
91
107
|
- spec/has_states/state_spec.rb
|
92
108
|
- spec/has_states/stateable_spec.rb
|
93
109
|
- spec/has_states_spec.rb
|
@@ -121,7 +137,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
121
137
|
- !ruby/object:Gem::Version
|
122
138
|
version: '0'
|
123
139
|
requirements: []
|
124
|
-
rubygems_version: 3.5.
|
140
|
+
rubygems_version: 3.5.9
|
125
141
|
signing_key:
|
126
142
|
specification_version: 4
|
127
143
|
summary: Simple and flexible state management for Ruby objects
|
File without changes
|
File without changes
|