0.0
The project is in a healthy, maintained state
HasStates provides state management and event system capabilities for Ruby objects. It allows tracking states, state transitions, and triggering callbacks on state changes.
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
 Dependencies

Runtime

 Project Readme

MetaStates

MetaStates 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 meta_states:install

Finally, run the migration:

$ rails db:migrate

Configuration

Configure your models and their state types in config/initializers/meta_states.rb:

MetaStates.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'
})

# Load state(s) also by name as methods on the record
current_kyc = user.current_state('kyc')
current_kyc = user.kyc # Returns most recent state of type kyc if multiple states of the same type exist
all_kyc = user.kycs

# 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 # => [#<MetaStates::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

Metadata Validations

You can define a JSON Schema for validating the metadata of states. This allows you to ensure that the metadata follows a specific structure and contains required fields, leveraging the power of JSON Schema.

MetaStates.configure do |config|
  config.configure_model User do |model|
    model.state_type :kyc do |type|
      type.statuses = [
        'pending',         
        'approved',    
        'rejected'  
      ]

      type.metadata_schema = {
        type: :object,
        properties: {
          name: { type: :string },
          age: { 
            type: :integer,
            minimum: 18
          },
        },
        required: [:name, :age],
        additionalProperties: false,
      }
    end
  end
end

user = User.create!

# Invalid metadata (too young < 18)
user.add_state('kyc', status: 'pending', metadata: { name: 'John Doe', age: 17 })
# ActiveRecord::RecordInvalid: Validation failed: Metadata is invalid (minimum value of 18)

# Valid metadata
user.add_state('kyc', status: 'pending', metadata: { name: 'John Doe', age: 25 })
# => #<MetaStates::State...>

State Limits

You can optionally limit the number of states a record can have for a specific state type:

MetaStates.configure do |config|
  config.configure_model User do |model|
    model.state_type :kyc do |type|
      type.statuses = %w[pending completed]
      type.limit = 1  # Limit to only one KYC state per user
    end
  end
end

When set, the limit is checked when a new state is added. If the limit is exceeded, an ActiveRecord::RecordInvalid error is raised.

Callbacks

Register callbacks that execute when states change:

MetaStates.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
MetaStates.configuration.off(:notify_admin)  # Remove by ID
MetaStates.configuration.off(callback)       # Remove by callback object

Scopes

MetaStates automatically generates scopes for your state types:

MetaStates::State.kyc              # All KYC states
MetaStates::State.onboarding      # All onboarding states

Class Inheritance

MetaStates lets you inherit from the MetaStates::Base class to create custom state classes. This makes validations and custom methods on specific state types easy.

class MyState < MetaStates::Base
  # Add validations or methods

  def do_something
    # Custom method on state
  end
end

Migrating from 0.0.X -> 0.1.X

The gem has been renamed from stateful_models to meta_states. This change doesn't impact any gem functionality, but requires a migration and updating the namespace for projects that choose to upgrade.

# Rename has states table
# 
# https://api.rubyonrails.org/v8.0.2/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html#method-i-rename_table
class ReanmeHasStatesStatesToMetaStatesStates < ActiveRecord::Migration[8.0]
  def change
    rename_table :has_states_states :meta_states_states
  end
end

After renaming the table, proceed to changing any use of the HasStates namespace to MetaStates and any has_states (i.e. filepaths, filenames) to meta_states.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/meta_states.

License

The gem is available as open source under the terms of the MIT License.