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.
@@ -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('../tmp', __dir__) }
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) } # Assuming you have a User factory
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("test_type")
148
- expect(user).to respond_to("other_test_type")
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("test_types")
160
- expect(user).to respond_to("other_test_types")
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.3
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-01-14 00:00:00.000000000 Z
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/tmp/config/initializers/has_states.rb
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
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.22
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