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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8eb127e7632f183232f299936b44df5fc17f3208decb1da2863c802e8f4f2b69
|
4
|
+
data.tar.gz: bb6062a0184b16afa0a933f8e9c35fd9ca90ad1e8129b6de95ce6d1c1433ffb2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: b254a3c531422e64ec011fae308ab9999b812f1b85fea61bceb159656091ac9d9fab2723b5b0e8e0802944a1c92d29d3c94822e89f5cbc772cbf06218487f903
|
7
|
+
data.tar.gz: d27742123bce580f7813de17adcee9c5485d5bfc60e2a628fa8f068ce3a6874af8a0e372127b89b86454263738a8563c22e2fb8455fa2c3dda75ef6a6b5d33df
|
data/CHANGELOG.md
CHANGED
@@ -1,6 +1,30 @@
|
|
1
1
|
## Released
|
2
2
|
|
3
|
-
## [0.
|
3
|
+
## [0.0.2] - 2024-12-23
|
4
|
+
|
5
|
+
### Added
|
6
|
+
- Single Table Inheritance (STI) support for custom state types
|
7
|
+
- Ability to create custom state classes by inheriting from `HasStates::Base`
|
8
|
+
- Default state class `HasStates::State` for basic state management
|
9
|
+
- Example implementation of custom state types in documentation
|
10
|
+
|
11
|
+
### Changed
|
12
|
+
- Refactored base state functionality into `HasStates::Base`
|
13
|
+
- Updated `add_state` method to support custom state classes
|
14
|
+
- Improved test coverage for inheritance and custom state types
|
15
|
+
|
16
|
+
## [0.0.3] - 2024-01-14
|
17
|
+
|
18
|
+
- Adding current_state method
|
19
|
+
- Adding DB indexes for state lookups
|
20
|
+
- Adding query methods for state types on stateable (`stateable.state_type` and `stateable.state_types`)
|
21
|
+
|
22
|
+
## [0.0.2] - 2024-12-23
|
23
|
+
|
24
|
+
- Added test coverage
|
25
|
+
- Refactor of State model into a inheritable Base class
|
26
|
+
|
27
|
+
## [0.0.1] - 2024-12-21
|
4
28
|
|
5
29
|
- Initial release
|
6
30
|
- See READme.md
|
data/README.md
CHANGED
@@ -6,20 +6,38 @@ module HasStates
|
|
6
6
|
class InstallGenerator < Rails::Generators::Base
|
7
7
|
source_root File.expand_path('templates', __dir__)
|
8
8
|
|
9
|
+
TEMPLATES = [
|
10
|
+
{
|
11
|
+
source: 'create_has_states_states.rb.erb',
|
12
|
+
destination: "db/migrate/%s_create_has_states_states.rb"
|
13
|
+
},
|
14
|
+
{
|
15
|
+
source: 'create_indexes_on_has_states_states.rb.erb',
|
16
|
+
destination: "db/migrate/%s_create_indexes_on_has_states_states.rb"
|
17
|
+
},
|
18
|
+
{
|
19
|
+
source: 'initializer.rb.erb',
|
20
|
+
destination: "config/initializers/has_states.rb"
|
21
|
+
}
|
22
|
+
].freeze
|
23
|
+
|
9
24
|
def install
|
10
25
|
puts 'Installing HasStates...'
|
11
26
|
|
12
|
-
template
|
13
|
-
|
14
|
-
|
15
|
-
)
|
16
|
-
|
17
|
-
template(
|
18
|
-
'initializer.rb.erb',
|
19
|
-
'config/initializers/has_states.rb'
|
20
|
-
)
|
27
|
+
TEMPLATES.each do |template|
|
28
|
+
make_template(**template)
|
29
|
+
end
|
21
30
|
|
22
31
|
puts 'HasStates installed successfully!'
|
23
32
|
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def make_template(source:, destination:)
|
37
|
+
timestamp = Time.now.utc.strftime('%Y%m%d%H%M%S')
|
38
|
+
destination = destination % timestamp if destination.include?('%s')
|
39
|
+
|
40
|
+
template(source, destination)
|
41
|
+
end
|
24
42
|
end
|
25
43
|
end
|
@@ -1,6 +1,7 @@
|
|
1
|
-
|
1
|
+
class CreateHasStatesStates < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
|
2
2
|
def change
|
3
3
|
create_table :has_states_states do |t|
|
4
|
+
t.string :type, null: false
|
4
5
|
t.string :state_type
|
5
6
|
t.string :status, null: false
|
6
7
|
|
@@ -8,9 +9,9 @@
|
|
8
9
|
|
9
10
|
t.references :stateable, polymorphic: true, null: false
|
10
11
|
|
11
|
-
t.datetime :completed_at
|
12
12
|
t.timestamps
|
13
13
|
|
14
|
+
t.index %i[type stateable_id]
|
14
15
|
t.index %i[stateable_type stateable_id]
|
15
16
|
end
|
16
17
|
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
class CreateIndexesOnHasStatesStates < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
|
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
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module HasStates
|
4
|
+
class Base < ActiveRecord::Base
|
5
|
+
self.table_name = 'has_states_states'
|
6
|
+
|
7
|
+
belongs_to :stateable, polymorphic: true
|
8
|
+
|
9
|
+
validate :status_is_configured
|
10
|
+
validate :state_type_is_configured
|
11
|
+
|
12
|
+
after_save :trigger_callbacks, if: :saved_change_to_status?
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def status_is_configured
|
17
|
+
return if HasStates.configuration.valid_status?(
|
18
|
+
stateable_type.constantize,
|
19
|
+
state_type,
|
20
|
+
status
|
21
|
+
)
|
22
|
+
|
23
|
+
errors.add(:status, 'is not configured')
|
24
|
+
end
|
25
|
+
|
26
|
+
def state_type_is_configured
|
27
|
+
return if HasStates.configuration.valid_state_type?(
|
28
|
+
stateable_type.constantize,
|
29
|
+
state_type
|
30
|
+
)
|
31
|
+
|
32
|
+
errors.add(:state_type, 'is not configured')
|
33
|
+
end
|
34
|
+
|
35
|
+
def trigger_callbacks
|
36
|
+
HasStates.configuration.matching_callbacks(self).each do |callback|
|
37
|
+
callback.call(self)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -17,8 +17,12 @@ module HasStates
|
|
17
17
|
|
18
18
|
@state_types[name.to_s] = type
|
19
19
|
|
20
|
+
# HasStates::State model method generators
|
20
21
|
generate_state_type_scope(name)
|
21
22
|
generate_status_predicates(type.statuses)
|
23
|
+
|
24
|
+
# Included model method generators
|
25
|
+
generate_state_type_queries(name)
|
22
26
|
generate_state_type_status_predicates(name, type.statuses)
|
23
27
|
end
|
24
28
|
|
@@ -36,6 +40,18 @@ module HasStates
|
|
36
40
|
end
|
37
41
|
end
|
38
42
|
|
43
|
+
def generate_state_type_queries(state_type)
|
44
|
+
# Singular for finding the most recent state of a given type and status
|
45
|
+
@model_class.define_method(:"#{state_type}") do
|
46
|
+
states.where(state_type: state_type).order(created_at: :desc).first
|
47
|
+
end
|
48
|
+
|
49
|
+
# Plural for finding all states of a given type and status
|
50
|
+
@model_class.define_method(:"#{ActiveSupport::Inflector.pluralize(state_type)}") do
|
51
|
+
states.where(state_type: state_type).order(created_at: :desc)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
39
55
|
def generate_state_type_status_predicates(state_type, statuses)
|
40
56
|
statuses.each do |status_name|
|
41
57
|
@model_class.define_method(:"#{state_type}_#{status_name}?") do
|
data/lib/has_states/state.rb
CHANGED
@@ -1,41 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module HasStates
|
4
|
-
class State <
|
5
|
-
self.table_name = 'has_states_states'
|
6
|
-
|
7
|
-
belongs_to :stateable, polymorphic: true
|
8
|
-
|
9
|
-
validate :status_is_configured
|
10
|
-
validate :state_type_is_configured
|
11
|
-
|
12
|
-
after_save :trigger_callbacks, if: :saved_change_to_status?
|
13
|
-
|
14
|
-
private
|
15
|
-
|
16
|
-
def status_is_configured
|
17
|
-
return if HasStates.configuration.valid_status?(
|
18
|
-
stateable_type.constantize,
|
19
|
-
state_type,
|
20
|
-
status
|
21
|
-
)
|
22
|
-
|
23
|
-
errors.add(:status, 'is not configured')
|
24
|
-
end
|
25
|
-
|
26
|
-
def state_type_is_configured
|
27
|
-
return if HasStates.configuration.valid_state_type?(
|
28
|
-
stateable_type.constantize,
|
29
|
-
state_type
|
30
|
-
)
|
31
|
-
|
32
|
-
errors.add(:state_type, 'is not configured')
|
33
|
-
end
|
34
|
-
|
35
|
-
def trigger_callbacks
|
36
|
-
HasStates.configuration.matching_callbacks(self).each do |callback|
|
37
|
-
callback.call(self)
|
38
|
-
end
|
39
|
-
end
|
40
|
-
end
|
4
|
+
class State < Base; end
|
41
5
|
end
|
data/lib/has_states/stateable.rb
CHANGED
@@ -5,18 +5,22 @@ module HasStates
|
|
5
5
|
extend ActiveSupport::Concern
|
6
6
|
|
7
7
|
included do
|
8
|
-
has_many :states, class_name: 'HasStates::
|
8
|
+
has_many :states, class_name: 'HasStates::Base',
|
9
9
|
as: :stateable,
|
10
10
|
dependent: :destroy
|
11
11
|
end
|
12
12
|
|
13
13
|
# Instance methods for managing states
|
14
|
-
def add_state(type, status: 'pending', metadata: {})
|
15
|
-
states.create!(
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
)
|
14
|
+
def add_state(type, status: 'pending', metadata: {}, state_class: HasStates::State)
|
15
|
+
states.create!(type: state_class.name, state_type: type, status: status, metadata: metadata)
|
16
|
+
end
|
17
|
+
|
18
|
+
def current_state(type)
|
19
|
+
states.where(state_type: type).order(created_at: :desc).first
|
20
|
+
end
|
21
|
+
|
22
|
+
def current_states(type)
|
23
|
+
states.where(state_type: type).order(created_at: :desc)
|
20
24
|
end
|
21
25
|
end
|
22
26
|
end
|
data/lib/has_states/version.rb
CHANGED
data/lib/has_states.rb
CHANGED
@@ -11,13 +11,14 @@ module HasStates
|
|
11
11
|
def configure
|
12
12
|
yield(configuration)
|
13
13
|
end
|
14
|
-
|
14
|
+
|
15
15
|
def configuration
|
16
16
|
Configuration.instance
|
17
17
|
end
|
18
18
|
end
|
19
19
|
end
|
20
20
|
|
21
|
+
require 'has_states/base'
|
21
22
|
require 'has_states/state'
|
22
23
|
require 'has_states/callback'
|
23
24
|
require 'has_states/stateable'
|
data/spec/dummy/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: ../..
|
3
3
|
specs:
|
4
|
-
|
4
|
+
stateful_models (0.0.2)
|
5
5
|
|
6
6
|
GEM
|
7
7
|
remote: https://rubygems.org/
|
@@ -272,13 +272,13 @@ PLATFORMS
|
|
272
272
|
DEPENDENCIES
|
273
273
|
brakeman
|
274
274
|
debug
|
275
|
-
has_state!
|
276
275
|
kamal
|
277
276
|
puma (>= 5.0)
|
278
277
|
rails (~> 8.0.1)
|
279
278
|
solid_cache
|
280
279
|
solid_queue
|
281
280
|
sqlite3 (>= 2.1)
|
281
|
+
stateful_models!
|
282
282
|
thruster
|
283
283
|
tzinfo-data
|
284
284
|
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Configure after the application is initialized
|
4
|
+
Rails.application.config.after_initialize do
|
5
|
+
HasStates.configure do |config|
|
6
|
+
# Configure your models and their state types below
|
7
|
+
#
|
8
|
+
# Example configuration:
|
9
|
+
#
|
10
|
+
config.configure_model User do |model|
|
11
|
+
# KYC state type with its allowed statuses
|
12
|
+
model.state_type :kyc do |type|
|
13
|
+
type.statuses = [
|
14
|
+
'pending', # Initial state
|
15
|
+
'documents_required', # Waiting for user documents
|
16
|
+
'under_review', # Documents being reviewed
|
17
|
+
'approved', # KYC process completed successfully
|
18
|
+
'rejected' # KYC process failed
|
19
|
+
]
|
20
|
+
end
|
21
|
+
|
22
|
+
# Onboarding state type with different statuses
|
23
|
+
model.state_type :onboarding do |type|
|
24
|
+
type.statuses = [
|
25
|
+
'pending', # Just started
|
26
|
+
'email_verified', # Email verification complete
|
27
|
+
'profile_complete', # User filled all required fields
|
28
|
+
'completed' # Onboarding finished
|
29
|
+
]
|
30
|
+
end
|
31
|
+
end
|
32
|
+
#
|
33
|
+
# config.configure_model Company do |model|
|
34
|
+
# model.state_type :verification do |type|
|
35
|
+
# type.statuses = [
|
36
|
+
# 'pending',
|
37
|
+
# 'documents_submitted',
|
38
|
+
# 'under_review',
|
39
|
+
# 'verified',
|
40
|
+
# 'rejected'
|
41
|
+
# ]
|
42
|
+
# end
|
43
|
+
# end
|
44
|
+
end
|
45
|
+
end
|
@@ -1,6 +1,7 @@
|
|
1
1
|
class CreateHasStatesStates < ActiveRecord::Migration[8.0]
|
2
2
|
def change
|
3
3
|
create_table :has_states_states do |t|
|
4
|
+
t.string :type, null: false
|
4
5
|
t.string :state_type
|
5
6
|
t.string :status, null: false
|
6
7
|
|
@@ -8,9 +9,9 @@
|
|
8
9
|
|
9
10
|
t.references :stateable, polymorphic: true, null: false
|
10
11
|
|
11
|
-
t.datetime :completed_at
|
12
12
|
t.timestamps
|
13
13
|
|
14
|
+
t.index %i[type stateable_id]
|
14
15
|
t.index %i[stateable_type stateable_id]
|
15
16
|
end
|
16
17
|
end
|
@@ -0,0 +1,10 @@
|
|
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
|
+
end
|
9
|
+
end
|
10
|
+
end
|
data/spec/dummy/db/schema.rb
CHANGED
@@ -1,5 +1,3 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
1
|
# This file is auto-generated from the current state of the database. Instead
|
4
2
|
# of editing this file, please use the migrations feature of Active Record to
|
5
3
|
# incrementally modify your database, and then regenerate this schema definition.
|
@@ -12,29 +10,35 @@
|
|
12
10
|
#
|
13
11
|
# It's strongly recommended that you check this file into your version control system.
|
14
12
|
|
15
|
-
ActiveRecord::Schema[8.0].define(version:
|
16
|
-
create_table
|
17
|
-
t.string
|
18
|
-
t.datetime
|
19
|
-
t.datetime
|
13
|
+
ActiveRecord::Schema[8.0].define(version: 2025_01_14_175939) do
|
14
|
+
create_table "companies", force: :cascade do |t|
|
15
|
+
t.string "name"
|
16
|
+
t.datetime "created_at", null: false
|
17
|
+
t.datetime "updated_at", null: false
|
20
18
|
end
|
21
19
|
|
22
|
-
create_table
|
23
|
-
t.string
|
24
|
-
t.string
|
25
|
-
t.
|
26
|
-
t.
|
27
|
-
t.
|
28
|
-
t.
|
29
|
-
t.datetime
|
30
|
-
t.datetime
|
31
|
-
t.
|
32
|
-
t.index
|
20
|
+
create_table "has_states_states", force: :cascade do |t|
|
21
|
+
t.string "type", null: false
|
22
|
+
t.string "state_type"
|
23
|
+
t.string "status", null: false
|
24
|
+
t.json "metadata", default: {}, null: false
|
25
|
+
t.string "stateable_type", null: false
|
26
|
+
t.integer "stateable_id", null: false
|
27
|
+
t.datetime "completed_at"
|
28
|
+
t.datetime "created_at", null: false
|
29
|
+
t.datetime "updated_at", null: false
|
30
|
+
t.index ["stateable_id", "state_type", "created_at"], name: "idx_on_stateable_id_state_type_created_at_b5d09fb6ee"
|
31
|
+
t.index ["stateable_id", "state_type", "status", "created_at"], name: "idx_on_stateable_id_state_type_status_created_at_19e1cf37c2"
|
32
|
+
t.index ["stateable_id", "state_type", "status"], name: "idx_on_stateable_id_state_type_status_6d3d026e4d"
|
33
|
+
t.index ["stateable_id", "state_type"], name: "index_has_states_states_on_stateable_id_and_state_type"
|
34
|
+
t.index ["stateable_type", "stateable_id"], name: "index_has_states_states_on_stateable"
|
35
|
+
t.index ["stateable_type", "stateable_id"], name: "index_has_states_states_on_stateable_type_and_stateable_id"
|
36
|
+
t.index ["type", "stateable_id"], name: "index_has_states_states_on_type_and_stateable_id"
|
33
37
|
end
|
34
38
|
|
35
|
-
create_table
|
36
|
-
t.string
|
37
|
-
t.datetime
|
38
|
-
t.datetime
|
39
|
+
create_table "users", force: :cascade do |t|
|
40
|
+
t.string "name"
|
41
|
+
t.datetime "created_at", null: false
|
42
|
+
t.datetime "updated_at", null: false
|
39
43
|
end
|
40
44
|
end
|