HasStates
HasStates is a flexible state management gem for Ruby on Rails that allows you to add multiple state machines to your models. It provides a simple way to track state transitions, add metadata, and execute callbacks.
Features
- Multiple state types per model
- Model-specific state configurations
- JSON metadata storage for each state
- Configurable callbacks with conditions
- Limited execution callbacks
- Automatic scope generation
- Simple state transition tracking
Installation
Add this line to your application's Gemfile:
gem 'stateful_models'
Then execute:
$ bundle install
Generate the required migration and initializer:
$ rails generate has_states:install
Finally, run the migration:
$ rails db:migrate
Configuration
Configure your models and their state types in config/initializers/has_states.rb
:
HasStates.configure do |config|
# Configure states on any model
config.configure_model User do |model|
# Define state type and its allowed statuses
model.state_type :kyc do |type|
type.statuses = [
'pending', # Initial state
'documents_required', # Waiting for documents
'under_review', # Documents being reviewed
'approved', # KYC completed successfully
'rejected' # KYC failed
]
end
# Define multiple state types per model with different statuses
model.state_type :onboarding do |type|
type.statuses = [
'pending', # Just started
'email_verified', # Email verification complete
'completed' # Onboarding finished
]
end
end
# Configure multiple models
config.configure_model Company do |model|
model.state_type :verification do |type|
type.statuses = ['pending', 'verified', 'rejected']
end
end
end
Usage
Basic State Management
user = User.create!(name: 'John')
# Add a new state
state = user.add_state('kyc', status: 'pending', metadata: {
documents: ['passport', 'utility_bill'],
notes: 'Awaiting document submission'
})
# Check current state
current_kyc = user.current_state('kyc')
# Predicate methods are generated for every status.
current_kyc.pending? # => true
current_kyc.approved? # => false
# Update state
current_kyc.update!(status: 'under_review')
# Check state for record
user.kyc_pending? # => true
user.kyc_completed? # => false
# See all states for record
user.states # => [#<HasStates::State...>]
Working with Metadata
Each state can store arbitrary metadata as JSON:
# Store complex metadata
state = user.add_state('kyc', metadata: {
documents: {
passport: {
status: 'verified',
verified_at: Time.current,
verified_by: 'admin@example.com'
},
utility_bill: {
status: 'rejected',
reason: 'Document expired'
}
},
risk_score: 85,
notes: ['Requires additional verification', 'High-risk jurisdiction']
})
# Access metadata
state.metadata['documents']['passport']['status'] # => "verified"
state.metadata['risk_score'] # => 85
Callbacks
Register callbacks that execute when states change:
HasStates.configure do |config|
# Basic callback
config.on(:kyc, to: 'completed') do |state|
UserMailer.kyc_completed(state.stateable).deliver_later
end
# Callback with custom ID for easy removal
config.on(:kyc, id: :notify_admin, to: 'rejected') do |state|
AdminNotifier.kyc_rejected(state)
end
# Callback that runs only once
config.on(:onboarding, to: 'completed', times: 1) do |state|
WelcomeMailer.send_welcome(state.stateable)
end
# Callback with from/to conditions
config.on(:kyc, from: 'pending', to: 'under_review') do |state|
NotificationService.notify_review_started(state)
end
end
# Remove callbacks
HasStates.configuration.off(:notify_admin) # Remove by ID
HasStates.configuration.off(callback) # Remove by callback object
Scopes
HasStates automatically generates scopes for your state types:
HasStates::State.kyc # All KYC states
HasStates::State.onboarding # All onboarding states
Contributing
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/has_states.
License
The gem is available as open source under the terms of the MIT License.