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.
Files changed (27) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +25 -1
  3. data/README.md +1 -1
  4. data/lib/generators/has_states/install/install_generator.rb +27 -9
  5. data/lib/generators/has_states/install/templates/create_has_states_states.rb.erb +3 -2
  6. data/lib/generators/has_states/install/templates/create_indexes_on_has_states_states.rb.erb +11 -0
  7. data/lib/has_states/base.rb +41 -0
  8. data/lib/has_states/configuration/model_configuration.rb +16 -0
  9. data/lib/has_states/state.rb +1 -37
  10. data/lib/has_states/stateable.rb +11 -7
  11. data/lib/has_states/version.rb +1 -1
  12. data/lib/has_states.rb +2 -1
  13. data/spec/dummy/Gemfile.lock +2 -2
  14. data/spec/dummy/config/initializers/has_states.rb +45 -0
  15. data/spec/{generators/tmp/db/migrate/20241223020432_create_has_states_states.rb → dummy/db/migrate/20241223212128_create_has_states_states.rb} +2 -1
  16. data/spec/dummy/db/migrate/20250114175939_create_indexes_on_has_states_states.rb +10 -0
  17. data/spec/dummy/db/schema.rb +26 -22
  18. data/spec/dummy/log/development.log +203 -0
  19. data/spec/dummy/log/test.log +7151 -0
  20. data/spec/dummy/storage/development.sqlite3 +0 -0
  21. data/spec/dummy/storage/test.sqlite3 +0 -0
  22. data/spec/{dummy/db/migrate/20241221183116_create_has_states_tables.rb → generators/tmp/db/migrate/20250114180401_create_has_states_states.rb} +4 -5
  23. data/spec/generators/tmp/db/migrate/20250114180401_create_indexes_on_has_states_states.rb +11 -0
  24. data/spec/has_states/state_spec.rb +96 -12
  25. data/spec/has_states/stateable_spec.rb +184 -0
  26. data/spec/rails_helper.rb +1 -0
  27. metadata +10 -4
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 58bd1cdf2dd440bf776a3fd5607b2b965d2dc4a47b03174e2ef542777b62f95d
4
- data.tar.gz: 94f8c8473326519ba676df54e010037f699b0985976a68d09832f1a42ebb567a
3
+ metadata.gz: 8eb127e7632f183232f299936b44df5fc17f3208decb1da2863c802e8f4f2b69
4
+ data.tar.gz: bb6062a0184b16afa0a933f8e9c35fd9ca90ad1e8129b6de95ce6d1c1433ffb2
5
5
  SHA512:
6
- metadata.gz: 436f47aaac1ba1216157ed927ea9ba758570b8c36b75f77e6ad3e21d905070f34f9beca5e4065b247306b10898fbf356a99a4935d89917c1ed8df296543f27ba
7
- data.tar.gz: 629afd3e4a9280c03571bbcfcb12b9da2d9d7da6c53bbbd2f98d3bc902164e10dcb3fba6a079b29c2d7f7a133895ad8d8f3d56d5645ca9d64198b02e5551690c
6
+ metadata.gz: b254a3c531422e64ec011fae308ab9999b812f1b85fea61bceb159656091ac9d9fab2723b5b0e8e0802944a1c92d29d3c94822e89f5cbc772cbf06218487f903
7
+ data.tar.gz: d27742123bce580f7813de17adcee9c5485d5bfc60e2a628fa8f068ce3a6874af8a0e372127b89b86454263738a8563c22e2fb8455fa2c3dda75ef6a6b5d33df
data/CHANGELOG.md CHANGED
@@ -1,6 +1,30 @@
1
1
  ## Released
2
2
 
3
- ## [0.1.0] - 2024-12-21
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
@@ -17,7 +17,7 @@ HasStates is a flexible state management gem for Ruby on Rails that allows you t
17
17
  Add this line to your application's Gemfile:
18
18
 
19
19
  ```ruby
20
- gem 'has_states'
20
+ gem 'stateful_models'
21
21
  ```
22
22
 
23
23
  Then execute:
@@ -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
- 'create_has_states_states.rb.erb',
14
- "db/migrate/#{Time.now.utc.strftime('%Y%m%d%H%M%S')}_create_has_states_states.rb"
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
- class CreateHasStatesStates < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
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
@@ -1,41 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HasStates
4
- class State < 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
4
+ class State < Base; end
41
5
  end
@@ -5,18 +5,22 @@ module HasStates
5
5
  extend ActiveSupport::Concern
6
6
 
7
7
  included do
8
- has_many :states, class_name: 'HasStates::State',
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
- state_type: type,
17
- status: status,
18
- metadata: metadata
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module HasStates
4
- VERSION = '0.0.1'
4
+ VERSION = '0.0.3'
5
5
  end
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'
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: ../..
3
3
  specs:
4
- has_state (0.0.1)
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
@@ -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: 20_241_221_183_116) do
16
- create_table 'companies', force: :cascade do |t|
17
- t.string 'name'
18
- t.datetime 'created_at', null: false
19
- t.datetime 'updated_at', null: false
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 'has_states_states', force: :cascade do |t|
23
- t.string 'state_type'
24
- t.string 'status', null: false
25
- t.json 'metadata', default: {}, null: false
26
- t.string 'stateable_type', null: false
27
- t.integer 'stateable_id', null: false
28
- t.datetime 'completed_at'
29
- t.datetime 'created_at', null: false
30
- t.datetime 'updated_at', null: false
31
- t.index %w[stateable_type stateable_id], name: 'index_has_states_states_on_stateable'
32
- t.index %w[stateable_type stateable_id], name: 'index_has_states_states_on_stateable_type_and_stateable_id'
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 'users', force: :cascade do |t|
36
- t.string 'name'
37
- t.datetime 'created_at', null: false
38
- t.datetime 'updated_at', null: false
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