pax_global_header 0000666 0000000 0000000 00000000064 15213055011 0014503 g ustar 00root root 0000000 0000000 52 comment=e0e4d2911598de3423029b68e7fd02e25a27dbd3
state-machines-state_machines-activerecord-b9cb1e5/ 0000775 0000000 0000000 00000000000 15213055011 0022541 5 ustar 00root root 0000000 0000000 state-machines-state_machines-activerecord-b9cb1e5/.github/ 0000775 0000000 0000000 00000000000 15213055011 0024101 5 ustar 00root root 0000000 0000000 state-machines-state_machines-activerecord-b9cb1e5/.github/workflows/ 0000775 0000000 0000000 00000000000 15213055011 0026136 5 ustar 00root root 0000000 0000000 state-machines-state_machines-activerecord-b9cb1e5/.github/workflows/release.yml 0000664 0000000 0000000 00000000414 15213055011 0030300 0 ustar 00root root 0000000 0000000 name: release-please
on:
push:
branches:
- master
workflow_dispatch:
permissions:
contents: write
pull-requests: write
issues: write
jobs:
release-please:
runs-on: ubuntu-latest
steps:
- uses: googleapis/release-please-action@v4
state-machines-state_machines-activerecord-b9cb1e5/.github/workflows/ruby.yml 0000664 0000000 0000000 00000001266 15213055011 0027647 0 ustar 00root root 0000000 0000000 name: Ruby
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
ruby-version: ['3.4', '4.0']
gemfiles:
- gemfiles/active_record_7.2.gemfile
- gemfiles/active_record_8.0.gemfile
- gemfiles/active_record_8.1.gemfile
env:
BUNDLE_GEMFILE: ${{ github.workspace }}/${{ matrix.gemfiles }}
steps:
- uses: actions/checkout@v4
- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby-version }}
bundler-cache: true
- name: Run tests
run: bundle exec rake
state-machines-state_machines-activerecord-b9cb1e5/.gitignore 0000664 0000000 0000000 00000000271 15213055011 0024531 0 ustar 00root root 0000000 0000000 *.gem
*.rbc
.bundle
.config
.yardoc
Gemfile.lock
InstalledFiles
_yardoc
coverage
doc/
lib/bundler/man
pkg
rdoc
spec/reports
tmp
*.bundle
*.so
*.o
*.a
log/active_record.log
.idea/
*.lock state-machines-state_machines-activerecord-b9cb1e5/.release-please-manifest.json 0000664 0000000 0000000 00000000025 15213055011 0030202 0 ustar 00root root 0000000 0000000 {
".": "0.200.0"
}
state-machines-state_machines-activerecord-b9cb1e5/Appraisals 0000664 0000000 0000000 00000001463 15213055011 0024567 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
appraise 'active_record_7.2' do
gem 'state_machines', '>= 0.100.4'
gem 'state_machines-activemodel', '>= 0.101.0'
gem 'sqlite3', platforms: :mri
gem 'activerecord-jdbcsqlite3-adapter', platform: %i[jruby truffleruby]
gem 'activerecord', '~> 7.2.0'
end
appraise 'active_record_8.0' do
gem 'state_machines', '>= 0.100.4'
gem 'state_machines-activemodel', '>= 0.101.0'
gem 'sqlite3', platforms: :mri
gem 'activerecord-jdbcsqlite3-adapter', platform: %i[jruby truffleruby]
gem 'activerecord', '~> 8.0.0'
end
appraise 'active_record_8.1' do
gem 'state_machines', '>= 0.100.4'
gem 'state_machines-activemodel', '>= 0.101.0'
gem 'sqlite3', platforms: :mri
gem 'activerecord-jdbcsqlite3-adapter', platform: %i[jruby truffleruby]
gem 'activerecord', '~> 8.1.0'
end
state-machines-state_machines-activerecord-b9cb1e5/CHANGELOG.md 0000664 0000000 0000000 00000013776 15213055011 0024370 0 ustar 00root root 0000000 0000000 # Changelog
## [0.200.0](https://github.com/state-machines/state_machines-activerecord/compare/state_machines-activerecord/v0.103.0...state_machines-activerecord/v0.200.0) (2026-06-12)
### Features
* remove redundant run_scope override ([5aab883](https://github.com/state-machines/state_machines-activerecord/commit/5aab8838cd04e5c741244f938be0faedd4eb1d13))
* remove redundant run_scope override ([c8197a6](https://github.com/state-machines/state_machines-activerecord/commit/c8197a6f3cce8aeaacfe34e35ae2fe0757f48c71))
* support after_commit option on after_transition callbacks ([#138](https://github.com/state-machines/state_machines-activerecord/issues/138)) ([b717068](https://github.com/state-machines/state_machines-activerecord/commit/b717068531bc184b801549e2b959a8a1248b0b89)), closes [#112](https://github.com/state-machines/state_machines-activerecord/issues/112)
### Bug Fixes
* add opt-out for integer state attribute conversion ([4f0596b](https://github.com/state-machines/state_machines-activerecord/commit/4f0596bc6a72dcd83ff46948a80812f9e60604ea))
* handle auto-indexed integer column defaults ([#137](https://github.com/state-machines/state_machines-activerecord/issues/137)) ([b839a99](https://github.com/state-machines/state_machines-activerecord/commit/b839a990c3c554d3b634b935412597292823fba2))
* re-register integer attribute type when a machine is cloned for a subclass ([#136](https://github.com/state-machines/state_machines-activerecord/issues/136)) ([e903f7b](https://github.com/state-machines/state_machines-activerecord/commit/e903f7b1bba3cb8ce563576f19171c8a9170992a))
## [0.103.0](https://github.com/state-machines/state_machines-activerecord/compare/state_machines-activerecord/v0.102.0...state_machines-activerecord/v0.103.0) (2026-03-23)
### Features
* support integer columns for state machine attributes ([452edec](https://github.com/state-machines/state_machines-activerecord/commit/452edec8d23e7f9d5546e95df8c2b3ab237c5b70))
## [0.102.0](https://github.com/state-machines/state_machines-activerecord/compare/state_machines-activerecord/v0.100.0...state_machines-activerecord/v0.102.0) (2026-03-22)
### Features
* Add Rails enum integration with automatic conflict resolution ([#122](https://github.com/state-machines/state_machines-activerecord/issues/122)) ([08c7650](https://github.com/state-machines/state_machines-activerecord/commit/08c765029c02cd9c83768172279ee8a78b641f7e))
* transparent integer column support via custom AR attribute type ([6efa970](https://github.com/state-machines/state_machines-activerecord/commit/6efa970badf3ae2685042f1a4d752192aa2f74b6))
### Bug Fixes
* bump state_machines-activemodel to >= 0.102.0 ([2f99c90](https://github.com/state-machines/state_machines-activerecord/commit/2f99c90e9f4f6ef158b0e195b930d9eb3167bb66))
* bump state_machines-activemodel to 0.101.0 and test Rails 8.1.0 ([#128](https://github.com/state-machines/state_machines-activerecord/issues/128)) ([8794d30](https://github.com/state-machines/state_machines-activerecord/commit/8794d30cd0abee26b9b0f6d4eabd5cb95f409c59))
* remove bump-patch-for-minor-pre-major ([215ccb2](https://github.com/state-machines/state_machines-activerecord/commit/215ccb271e63eeaa46de640b3eb001dc69747d8e))
## [0.100.0](https://github.com/state-machines/state_machines-activerecord/compare/state_machines-activerecord/v0.100.0...state_machines-activerecord/v0.100.0) (2025-07-17)
### Features
* Add Rails enum integration with automatic conflict resolution ([#122](https://github.com/state-machines/state_machines-activerecord/issues/122)) ([08c7650](https://github.com/state-machines/state_machines-activerecord/commit/08c765029c02cd9c83768172279ee8a78b641f7e))
## [0.100.0](https://github.com/state-machines/state_machines-activerecord/compare/state_machines-activerecord/v0.40.0...state_machines-activerecord/v0.100.0) (2025-07-17)
### Bug Fixes
* prevent fiber deadlocks with Ruby 3.4+ and Rails 8 ([#123](https://github.com/state-machines/state_machines-activerecord/issues/123)) ([507bdf0](https://github.com/state-machines/state_machines-activerecord/commit/507bdf0064a9e2fdd97f8915d40a7c961b6e884b))
## [0.40.0](https://github.com/state-machines/state_machines-activerecord/compare/state_machines-activerecord/v0.31.0...state_machines-activerecord/v0.40.0) (2025-06-30)
### Features
* Drop Rails 7.1 support and require Rails 7.2+ ([#120](https://github.com/state-machines/state_machines-activerecord/issues/120)) ([e3df786](https://github.com/state-machines/state_machines-activerecord/commit/e3df786d27ff58937c705d9a5ee54ba6c2e26394)), closes [#57](https://github.com/state-machines/state_machines-activerecord/issues/57)
## [0.31.0](https://github.com/state-machines/state_machines-activerecord/compare/state_machines-activerecord/v0.10.0...state_machines-activerecord/v0.31.0) (2025-06-30)
### Features
* Add transparent scope support (cherry-pick PR [#98](https://github.com/state-machines/state_machines-activerecord/issues/98)) ([5eef308](https://github.com/state-machines/state_machines-activerecord/commit/5eef30895e75840d0817166f1c36969f472ecaa0))
### Bug Fixes
* update I18n lambdas and apply RuboCop corrections ([#117](https://github.com/state-machines/state_machines-activerecord/issues/117)) ([4b739d4](https://github.com/state-machines/state_machines-activerecord/commit/4b739d4b69ebbef581ee6086457267fcf86bd922))
## [0.10.0](https://github.com/state-machines/state_machines-activerecord/compare/state_machines-activerecord-v0.9.0...state_machines-activerecord/v0.10.0) (2025-06-12)
### Features
* drop support for legacy version ([58a29a5](https://github.com/state-machines/state_machines-activerecord/commit/58a29a5dba93496e48ffda34563d2ac59f446bc0))
### Bug Fixes
* prepare to release 0.10.0 ([#115](https://github.com/state-machines/state_machines-activerecord/issues/115)) ([f9f89a4](https://github.com/state-machines/state_machines-activerecord/commit/f9f89a49507bc6bc5de125c4ddff54d0cc43f362))
* readme fix and release ([474cdd4](https://github.com/state-machines/state_machines-activerecord/commit/474cdd406a98fd3004f00b4ebd677ea882ea4182))
state-machines-state_machines-activerecord-b9cb1e5/Gemfile 0000664 0000000 0000000 00000000225 15213055011 0024033 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
source 'https://rubygems.org'
gemspec
platforms :mri do
gem 'debug'
end
group :development do
gem 'rubocop'
end
state-machines-state_machines-activerecord-b9cb1e5/LICENSE.txt 0000664 0000000 0000000 00000002135 15213055011 0024365 0 ustar 00root root 0000000 0000000 Copyright (c) 2006-2012 Aaron Pfeifer
Copyright (c) 2014-2025 Abdelkader Boudih
MIT License
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
state-machines-state_machines-activerecord-b9cb1e5/README.md 0000664 0000000 0000000 00000017473 15213055011 0024034 0 ustar 00root root 0000000 0000000 [](https://github.com/state-machines/state_machines-activerecord/actions/workflows/ruby.yml)
# StateMachines Active Record Integration
The Active Record 7.2+ integration adds support for database transactions, automatically
saving the record, named scopes, validation errors.
## Requirements
- Ruby 3.2+
- Rails 7.2+
## Installation
Add this line to your application's Gemfile:
gem 'state_machines-activerecord'
And then execute:
$ bundle
Or install it yourself as:
$ gem install state_machines-activerecord
## Usage
For the complete usage guide, see http://www.rubydoc.info/github/state-machines/state_machines-activerecord/StateMachines/Integrations/ActiveRecord
### Example
```ruby
class Vehicle < ApplicationRecord
state_machine :initial => :parked do
before_transition :parked => any - :parked, :do => :put_on_seatbelt
after_transition any => :parked do |vehicle, transition|
vehicle.seatbelt = 'off'
end
around_transition :benchmark
event :ignite do
transition :parked => :idling
end
state :first_gear, :second_gear do
validates :seatbelt_on, presence: true
end
end
def put_on_seatbelt
...
end
def benchmark
...
yield
...
end
end
```
### Scopes
Usage of the generated scopes (assuming default column `state`):
```ruby
Vehicle.with_state(:parked) # also plural #with_states
Vehicle.without_states(:first_gear, :second_gear) # also singular #without_state
```
#### Transparent Scopes
State scopes will return all records when `nil` is passed, making them perfect for search filters:
```ruby
Vehicle.with_state(nil) # Returns all vehicles
Vehicle.with_state(params[:state]) # Returns all vehicles if params[:state] is nil
Vehicle.where(color: 'red').with_state(nil) # Returns all red vehicles (chainable)
```
## Rails Enum Integration
When your ActiveRecord model uses Rails enums and defines a state machine on the same attribute, this gem automatically detects the conflict and provides seamless integration. This prevents method name collisions between Rails enum methods and state machine methods.
### Auto-Detection and Conflict Resolution
```ruby
class Order < ApplicationRecord
# Rails enum definition
enum :status, { pending: 0, processing: 1, completed: 2, cancelled: 3 }
# State machine on the same attribute
state_machine :status do
state :pending, :processing, :completed, :cancelled
event :process do
transition pending: :processing
end
event :complete do
transition processing: :completed
end
event :cancel do
transition [:pending, :processing] => :cancelled
end
end
end
```
When enum integration is detected, the gem automatically:
- Preserves original Rails enum methods (`pending?`, `processing?`, etc.)
- Generates prefixed state machine methods to avoid conflicts (`status_pending?`, `status_processing?`, etc.)
- Creates prefixed scope methods (`Order.status_pending`, `Order.status_processing`, etc.)
### Available Methods
**Original Rails enum methods (preserved):**
```ruby
order = Order.create(status: :pending)
order.pending? # => true (Rails enum method)
order.processing? # => false (Rails enum method)
order.processing! # Sets status to :processing (Rails enum method)
Order.pending # Rails enum scope
Order.processing # Rails enum scope
```
**Generated state machine methods (prefixed):**
```ruby
# Predicate methods
order.status_pending? # => true (state machine method)
order.status_processing? # => false (state machine method)
order.status_completed? # => false (state machine method)
# Bang methods (for conflict resolution only)
# These are placeholders and raise runtime errors
order.status_processing! # => raises RuntimeError
# Scope methods
Order.status_pending # State machine scope
Order.status_processing # State machine scope
Order.not_status_pending # Negative state machine scope
```
### Introspection API
The integration provides a comprehensive introspection API for advanced use cases:
```ruby
machine = Order.state_machine(:status)
# Check if enum integration is enabled
machine.enum_integrated? # => true
# Get the Rails enum mapping
machine.enum_mapping # => {"pending"=>0, "processing"=>1, "completed"=>2, "cancelled"=>3}
# Get original Rails enum methods that were preserved
machine.original_enum_methods
# => ["pending?", "processing?", "completed?", "cancelled?", "pending!", "processing!", ...]
# Get state machine methods that were generated
machine.state_machine_methods
# => ["status_pending?", "status_processing?", "status_completed?", "status_cancelled?", ...]
```
## Integer-backed state attributes
Integer columns whose states don't declare explicit values are converted
transparently: application code reads state names while the database stores
integers (mapped by definition order).
```ruby
class Order < ApplicationRecord
state_machine :status, initial: :pending do
state :pending
state :approved
end
end
order = Order.create!
order.status = :approved
order.status # => "approved"
# The database stores 1.
```
Machines where every state declares an explicit integer value keep the classic
raw-integer behavior automatically: reads return the integer and `status_name`
returns the state name:
```ruby
class LegacyOrder < ApplicationRecord
self.table_name = "orders"
state_machine :status, initial: :pending do
state :pending, value: 0
state :approved, value: 1
event :approve do
transition pending: :approved
end
end
end
order = LegacyOrder.create!
order.approve!
order.status # => 1
order.status_name # => :approved
# The database stores 1.
```
Applications that want no type conversion at all can disable it during boot,
before defining affected state machines:
```ruby
# config/initializers/state_machines.rb
StateMachines::Integrations::ActiveRecord.auto_convert_integer_state_attributes = false
```
With this setting disabled, integer-backed state attributes are left entirely
to ActiveRecord's normal integer handling.
### Requirements for Enum Integration
- The state machine attribute must match an existing Rails enum attribute
- Auto-detection is enabled by default when this condition is met
### Configuration Options
The enum integration supports several configuration options:
- `prefix` (default: true) - Adds a prefix to generated methods to avoid conflicts
- `suffix` (default: false) - Alternative naming strategy using suffixes instead of prefixes
- `scopes` (default: true) - Controls whether state machine scopes are generated
## State driven validations
As mentioned in `StateMachines::Machine#state`, you can define behaviors,
like validations, that only execute for certain states. One *important*
caveat here is that, due to a constraint in ActiveRecord's validation
framework, custom validators will not work as expected when defined to run
in multiple states. For example:
```ruby
class Vehicle < ApplicationRecord
state_machine do
state :first_gear, :second_gear do
validate :speed_is_legal
end
end
end
```
In this case, the :speed_is_legal validation will only get run
for the :second_gear state. To avoid this, you can define your
custom validation like so:
```ruby
class Vehicle < ApplicationRecord
state_machine do
state :first_gear, :second_gear do
validate {|vehicle| vehicle.speed_is_legal}
end
end
end
```
## Contributing
1. Fork it ( https://github.com/state-machines/state_machines-activerecord/fork )
2. Create your feature branch (`git checkout -b my-new-feature`)
3. Commit your changes (`git commit -am 'Add some feature'`)
4. Push to the branch (`git push origin my-new-feature`)
5. Create a new Pull Request
state-machines-state_machines-activerecord-b9cb1e5/Rakefile 0000664 0000000 0000000 00000000305 15213055011 0024204 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require 'bundler/gem_tasks'
require 'rake/testtask'
Rake::TestTask.new do |t|
t.pattern = 'test/*_test.rb'
end
desc 'Default: run all tests.'
task default: :test
state-machines-state_machines-activerecord-b9cb1e5/gemfiles/ 0000775 0000000 0000000 00000000000 15213055011 0024334 5 ustar 00root root 0000000 0000000 state-machines-state_machines-activerecord-b9cb1e5/gemfiles/active_record_7.2.gemfile 0000664 0000000 0000000 00000000605 15213055011 0031066 0 ustar 00root root 0000000 0000000 # This file was generated by Appraisal
source "https://rubygems.org"
gem "state_machines", ">= 0.100.4"
gem "state_machines-activemodel", ">= 0.101.0"
gem "sqlite3", platforms: :mri
gem "activerecord-jdbcsqlite3-adapter", platform: [:jruby, :truffleruby]
gem "activerecord", "~> 7.2.0"
group :development do
gem "rubocop"
end
platforms :mri do
gem "debug"
end
gemspec path: "../"
state-machines-state_machines-activerecord-b9cb1e5/gemfiles/active_record_8.0.gemfile 0000664 0000000 0000000 00000000605 15213055011 0031065 0 ustar 00root root 0000000 0000000 # This file was generated by Appraisal
source "https://rubygems.org"
gem "state_machines", ">= 0.100.4"
gem "state_machines-activemodel", ">= 0.101.0"
gem "sqlite3", platforms: :mri
gem "activerecord-jdbcsqlite3-adapter", platform: [:jruby, :truffleruby]
gem "activerecord", "~> 8.0.0"
group :development do
gem "rubocop"
end
platforms :mri do
gem "debug"
end
gemspec path: "../"
state-machines-state_machines-activerecord-b9cb1e5/gemfiles/active_record_8.1.gemfile 0000664 0000000 0000000 00000000633 15213055011 0031067 0 ustar 00root root 0000000 0000000 # This file was generated by Appraisal
source "https://rubygems.org"
gem "state_machines", ">= 0.100.4"
gem "state_machines-activemodel", ">= 0.101.0"
gem "sqlite3", platforms: :mri
gem "activerecord-jdbcsqlite3-adapter", platform: [:jruby, :truffleruby]
gem "activerecord", "~> 8.1.0"
gem "rake", "~> 13.0"
group :development do
gem "rubocop"
end
platforms :mri do
gem "debug"
end
gemspec path: "../"
state-machines-state_machines-activerecord-b9cb1e5/lib/ 0000775 0000000 0000000 00000000000 15213055011 0023307 5 ustar 00root root 0000000 0000000 state-machines-state_machines-activerecord-b9cb1e5/lib/state_machines-activerecord.rb 0000664 0000000 0000000 00000000366 15213055011 0031300 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require 'active_support'
require 'state_machines/integrations/active_record'
ActiveSupport.on_load(:i18n) do
I18n.load_path << File.expand_path('state_machines/integrations/active_record/locale.rb', __dir__)
end
state-machines-state_machines-activerecord-b9cb1e5/lib/state_machines/ 0000775 0000000 0000000 00000000000 15213055011 0026276 5 ustar 00root root 0000000 0000000 state-machines-state_machines-activerecord-b9cb1e5/lib/state_machines/integrations/ 0000775 0000000 0000000 00000000000 15213055011 0031004 5 ustar 00root root 0000000 0000000 state-machines-state_machines-activerecord-b9cb1e5/lib/state_machines/integrations/active_record.rb 0000664 0000000 0000000 00000112614 15213055011 0034147 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require 'state_machines-activemodel'
require 'active_record'
require 'state_machines/integrations/active_record/version'
require 'state_machines/integrations/active_record/type/integer'
module StateMachines
module Integrations # :nodoc:
# Adds support for integrating state machines with ActiveRecord models.
#
# == Examples
#
# Below is an example of a simple state machine defined within an
# ActiveRecord model:
#
# class Vehicle < ApplicationRecord
# state_machine :initial => :parked do
# event :ignite do
# transition :parked => :idling
# end
# end
# end
#
# The examples in the sections below will use the above class as a
# reference.
#
# == Actions
#
# By default, the action that will be invoked when a state is transitioned
# is the +save+ action. This will cause the record to save the changes
# made to the state machine's attribute. *Note* that if any other changes
# were made to the record prior to transition, then those changes will
# be saved as well.
#
# For example,
#
# vehicle = Vehicle.create # => #
# vehicle.name = 'Ford Explorer'
# vehicle.ignite # => true
# vehicle.reload # => #
#
# *Note* that if you want a transition to update additional attributes of the record,
# either the changes need to be made in a +before_transition+ callback or you need
# to save the record manually.
#
# == Events
#
# As described in StateMachines::InstanceMethods#state_machine, event
# attributes are created for every machine that allow transitions to be
# performed automatically when the object's action (in this case, :save)
# is called.
#
# In ActiveRecord, these automated events are run in the following order:
# * before validation - Run before callbacks and persist new states, then validate
# * before save - If validation was skipped, run before callbacks and persist new states, then save
# * after save - Run after callbacks
#
# For example,
#
# vehicle = Vehicle.create # => #
# vehicle.state_event # => nil
# vehicle.state_event = 'invalid'
# vehicle.valid? # => false
# vehicle.errors.full_messages # => ["State event is invalid"]
#
# vehicle.state_event = 'ignite'
# vehicle.valid? # => true
# vehicle.save # => true
# vehicle.state # => "idling"
# vehicle.state_event # => nil
#
# Note that this can also be done on a mass-assignment basis:
#
# vehicle = Vehicle.create(:state_event => 'ignite') # => #
# vehicle.state # => "idling"
#
# This technique is always used for transitioning states when the +save+
# action (which is the default) is configured for the machine.
#
# === Security implications
#
# Beware that public event attributes mean that events can be fired
# whenever mass-assignment is being used. If you want to prevent malicious
# users from tampering with events through URLs / forms, you should use
# Rails' strong parameters to control which attributes are permitted:
#
# class VehiclesController < ApplicationController
# def vehicle_params
# params.require(:vehicle).permit(:color, :make, :model)
# # Exclude state_event to prevent tampering
# end
# end
#
# If you want to only have *some* events be able to fire via mass-assignment,
# you can build two state machines (one public and one protected) like so:
#
# class Vehicle < ApplicationRecord
# # Define private machine
# state_machine do
# # Define private events here
# end
#
# # Public machine targets the same state as the private machine
# state_machine :public_state, :attribute => :state do
# # Define public events here
# end
#
# # Control access via strong parameters in your controller
# end
#
# == Transactions
#
# In order to ensure that any changes made during transition callbacks
# are rolled back during a failed attempt, every transition is wrapped
# within a transaction.
#
# For example,
#
# class Message < ApplicationRecord
# end
#
# Vehicle.state_machine do
# before_transition do |vehicle, transition|
# Message.create(:content => transition.inspect)
# false
# end
# end
#
# vehicle = Vehicle.create # => #
# vehicle.ignite # => false
# Message.count # => 0
#
# *Note* that only before callbacks that halt the callback chain and
# failed attempts to save the record will result in the transaction being
# rolled back. If an after callback halts the chain, the previous result
# still applies and the transaction is *not* rolled back.
#
# To turn off transactions:
#
# class Vehicle < ApplicationRecord
# state_machine :initial => :parked, :use_transactions => false do
# ...
# end
# end
#
# == Validations
#
# As mentioned in StateMachines::Machine#state, you can define behaviors,
# like validations, that only execute for certain states. One *important*
# caveat here is that, due to a constraint in ActiveRecord's validation
# framework, custom validators will not work as expected when defined to run
# in multiple states. For example:
#
# class Vehicle < ApplicationRecord
# state_machine do
# ...
# state :first_gear, :second_gear do
# validate :speed_is_legal
# end
# end
# end
#
# In this case, the :speed_is_legal validation will only get run
# for the :second_gear state. To avoid this, you can define your
# custom validation like so:
#
# class Vehicle < ApplicationRecord
# state_machine do
# ...
# state :first_gear, :second_gear do
# validate {|vehicle| vehicle.speed_is_legal}
# end
# end
# end
#
# == Validation errors
#
# If an event fails to successfully fire because there are no matching
# transitions for the current record, a validation error is added to the
# record's state attribute to help in determining why it failed and for
# reporting via the UI.
#
# For example,
#
# vehicle = Vehicle.create(:state => 'idling') # => #
# vehicle.ignite # => false
# vehicle.errors.full_messages # => ["State cannot transition via \"ignite\""]
#
# If an event fails to fire because of a validation error on the record and
# *not* because a matching transition was not available, no error messages
# will be added to the state attribute.
#
# In addition, if you're using the ignite! version of the event,
# then the failure reason (such as the current validation errors) will be
# included in the exception that gets raised when the event fails. For
# example, assuming there's a validation on a field called +name+ on the class:
#
# vehicle = Vehicle.new
# vehicle.ignite! # => StateMachines::InvalidTransition: Cannot transition state via :ignite from :parked
# # (Reason(s): Name cannot be blank)
#
# == Scopes
#
# To assist in filtering models with specific states, a series of scopes
# are defined on the model for finding records with or without a
# particular set of states.
#
# These scopes are essentially the functional equivalent of the
# following definitions:
#
# class Vehicle < ApplicationRecord
# # with_states also aliased to with_state
# scope :with_states, ->(states) { states.present? ? where(state: states) : all }
#
# # without_states also aliased to without_state
# scope :without_states, ->(states) { states.present? ? where.not(state: states) : all }
# end
#
# *Note*, however, that the states are converted to their stored values
# before being passed into the query.
#
# Because of the way scopes work in ActiveRecord, they can be
# chained like so:
#
# Vehicle.with_state(:parked).order(id: :desc)
#
# Note that states can also be referenced by the string version of their
# name:
#
# Vehicle.with_state('parked')
#
# === Transparent Scopes
#
# When `nil` is passed to any of the state scopes, they return `all` records
# without applying any filters. This allows for more flexible scope chaining
# in search interfaces:
#
# Vehicle.with_state(params[:state]) # Returns all vehicles if params[:state] is nil
# Vehicle.where(color: 'red').with_state(nil) # Returns all red vehicles
#
# == Callbacks
#
# All before/after transition callbacks defined for ActiveRecord models
# behave in the same way that other ActiveRecord callbacks behave. The
# object involved in the transition is passed in as an argument.
#
# For example,
#
# class Vehicle < ApplicationRecord
# state_machine :initial => :parked do
# before_transition any => :idling do |vehicle|
# vehicle.put_on_seatbelt
# end
#
# before_transition do |vehicle, transition|
# # log message
# end
#
# event :ignite do
# transition :parked => :idling
# end
# end
#
# def put_on_seatbelt
# ...
# end
# end
#
# Note, also, that the transition can be accessed by simply defining
# additional arguments in the callback block.
#
# === Failure callbacks
#
# +after_failure+ callbacks allow you to execute behaviors when a transition
# is allowed, but fails to save. This could be useful for something like
# auditing transition attempts. Since callbacks run within transactions in
# ActiveRecord, a save failure will cause any records that get created in
# your callback to roll back. You can work around this issue like so:
#
# class TransitionLog < ApplicationRecord
# connects_to database: { writing: :primary, reading: :primary }
# end
#
# class Vehicle < ApplicationRecord
# state_machine do
# after_failure do |vehicle, transition|
# TransitionLog.create(:vehicle => vehicle, :transition => transition)
# end
#
# ...
# end
# end
#
# The +TransitionLog+ model establishes a separate connection to the database
# that allows new records to be saved without being affected by rollbacks
# in the +Vehicle+ model's transaction.
#
# === Callback Order
#
# Callbacks occur in the following order. Callbacks specific to state_machine
# are bolded. The remaining callbacks are part of ActiveRecord.
#
# * (-) save
# * (-) begin transaction (if enabled)
# * (1) *before_transition*
# * (-) valid
# * (2) before_validation
# * (-) validate
# * (3) after_validation
# * (4) before_save
# * (5) before_create
# * (-) create
# * (6) after_create
# * (7) after_save
# * (8) *after_transition*
# * (-) end transaction (if enabled)
# * (9) after_commit
# * (10) *after_transition* callbacks defined with :after_commit => true
#
# An +after_transition+ callback defined with the :after_commit
# option is deferred until the surrounding database transaction has been
# committed (and discarded if it rolls back). See the documentation for
# +after_transition+ in this integration for more details.
#
# == Internationalization
#
# Any error message that is generated from performing invalid
# transitions can be localized. The following default translations are used:
#
# en:
# activerecord:
# errors:
# messages:
# invalid: "is invalid"
# # %{value} = attribute value, %{state} = Human state name
# invalid_event: "cannot transition when %{state}"
# # %{value} = attribute value, %{event} = Human event name, %{state} = Human current state name
# invalid_transition: "cannot transition via %{event}"
#
# You can override these for a specific model like so:
#
# en:
# activerecord:
# errors:
# models:
# user:
# invalid: "is not valid"
#
# In addition to the above, you can also provide translations for the
# various states / events in each state machine. Using the Vehicle example,
# state translations will be looked for using the following keys, where
# +model_name+ = "vehicle", +machine_name+ = "state" and +state_name+ = "parked":
# * activerecord.state_machines.#{model_name}.#{machine_name}.states.#{state_name}
# * activerecord.state_machines.#{model_name}.states.#{state_name}
# * activerecord.state_machines.#{machine_name}.states.#{state_name}
# * activerecord.state_machines.states.#{state_name}
#
# Event translations will be looked for using the following keys, where
# +model_name+ = "vehicle", +machine_name+ = "state" and +event_name+ = "ignite":
# * activerecord.state_machines.#{model_name}.#{machine_name}.events.#{event_name}
# * activerecord.state_machines.#{model_name}.events.#{event_name}
# * activerecord.state_machines.#{machine_name}.events.#{event_name}
# * activerecord.state_machines.events.#{event_name}
#
# An example translation configuration might look like so:
#
# es:
# activerecord:
# state_machines:
# states:
# parked: 'estacionado'
# events:
# park: 'estacionarse'
module ActiveRecord
include Base
include ActiveModel
# The default options to use for state machines using this integration
@defaults = { action: :save, use_transactions: true }
@auto_convert_integer_state_attributes = true
# Machine-specific methods for enum integration
module MachineMethods
# Enum integration metadata storage
attr_accessor :enum_integration
# Hook called after machine initialization
def after_initialize
super
initialize_enum_integration
register_integer_type if register_integer_type?
end
# Hook called when a machine is assigned to a class, including when an
# inherited machine is cloned for an STI subclass. The cloned machine
# carries the parent's @integer_type_registered flag while the subclass
# still inherits the parent's attribute type, which references the
# parent machine's state collection. Re-register so the subclass type
# sees this machine's (cloned) states, picking up subclass-added states
# as they are defined.
#
# @param klass [Class] the new owner class
# @return [Class] the assigned owner class
def owner_class=(klass)
super.tap do
register_integer_type if integer_type_registered?
end
end
# Check if enum integration should be enabled for this machine
def detect_enum_integration
return nil unless owner_class.defined_enums.key?(attribute.to_s)
# For now, auto-detect enum and enable basic integration
# Later we can add explicit configuration options
{
enabled: true,
prefix: true,
suffix: false,
scopes: true,
enum_values: owner_class.defined_enums[attribute.to_s] || {},
original_enum_methods: detect_existing_enum_methods,
state_machine_methods: []
}
end
# Initialize enum integration if enum is detected
def initialize_enum_integration
detected_config = detect_enum_integration
return unless detected_config
# Store enum integration metadata
self.enum_integration = detected_config
end
# Override state method to trigger method generation after states are defined
def state(*, &)
result = super
# Generate methods after each state addition if enum integration is enabled
generate_state_machine_methods if enum_integrated?
# States defined after initialization (e.g. adding an explicit value
# to the initial state) can change how the column default maps to
# states, so re-evaluate the conflicting-default warning
recheck_conflicting_attribute_default if integer_type_registered?
result
end
# Warns at most once when the column default conflicts with the
# machine's initial state. The conflict is re-evaluated as states are
# defined, so remember when the warning has been issued.
def check_conflicting_attribute_default
return if @attribute_default_conflict_warned
initial_state = states.detect(&:initial)
conflict = !owner_class_attribute_default.nil? && (
dynamic_initial_state? || !owner_class_attribute_default_matches?(initial_state)
)
return unless conflict
@attribute_default_conflict_warned = true
super
end
# Returns true when this machine should use the custom integer attribute type
# to convert between Ruby state names and integer database values. This only
# applies to non-enum integer columns when automatic conversion is enabled.
def register_integer_type?
StateMachines::Integrations::ActiveRecord.auto_convert_integer_state_attributes &&
integer_column? &&
!enum_integrated?
end
# Check if this machine has enum integration enabled
def enum_integrated?
enum_integration && enum_integration[:enabled]
end
# Get the enum mapping for this attribute
def enum_mapping
return {} unless enum_integrated?
enum_integration[:enum_values] || {}
end
# Get list of original enum methods that were preserved
def original_enum_methods
return [] unless enum_integrated?
enum_integration[:original_enum_methods] || []
end
# Get list of state machine methods that were generated
def state_machine_methods
return [] unless enum_integrated?
enum_integration[:state_machine_methods] || []
end
def integer_type_registered?
!!@integer_type_registered
end
# Creates a callback that will be invoked *after* a transition is
# performed, so long as the given requirements match the transition.
#
# In addition to the configuration options supported by the core
# +after_transition+ (see StateMachines::Machine#after_transition), the
# ActiveRecord integration supports:
# * :after_commit - Defer execution of the callback until the
# database transaction wrapping the transition has been committed.
# When no transaction is open at that point, the callback runs
# immediately. When the transaction (or an outer transaction wrapping
# it) is rolled back, the callback is discarded.
#
# This is the safe place to enqueue background jobs that reference the
# record (e.g. via GlobalID), since a regular +after_transition+ runs
# inside the transaction, before the record's changes are visible to
# other connections:
#
# class Vehicle < ApplicationRecord
# state_machine do
# after_transition on: :ignite, after_commit: true do |vehicle|
# EngineWarmupJob.perform_later(vehicle)
# end
#
# ...
# end
# end
#
# Note that a deferred callback cannot halt the callback chain or
# affect the result of the transition: by the time it runs, the
# transition has already been committed. For the same reason, an
# exception raised by a deferred callback is not propagated (doing so
# would revert the record's in-memory state even though the database
# was already updated); it is reported to +ActiveSupport.error_reporter+
# (+Rails.error+) instead. Conditions (:if/:unless)
# and state requirements are evaluated when the transition is
# performed, not at commit time.
#
# Like ActiveRecord's own +after_commit+, a surrounding
# transaction(joinable: false) wrapper is transparent: the
# callback fires at the inner commit. This is what makes it fire
# under transactional test fixtures.
def after_transition(*args, **options, &block)
# The flag may hide in a legacy trailing positional options hash
positional_options = args.last.is_a?(Hash) ? args.pop.dup : {}
options = positional_options.merge(options)
# Only a boolean is the flag — a non-boolean value is the implicit
# state-requirement form for a state named :after_commit
flag = options[:after_commit]
return super unless flag == true || flag == false
options.delete(:after_commit)
return super unless flag
# Method handling goes to a real Callback (reusing core's binding,
# arity and :do semantics); branch matching stays on the wrapper so
# conditions are evaluated at transition time
parsed = parse_callback_arguments(args, options)
method_options = parsed.slice(:do, :bind_to_object)
method_options[:terminator] = callback_terminator
branch_options = parsed.except(:do, :bind_to_object, :terminator)
deferred = Callback.new(:after, method_options, &block)
super(**branch_options, bind_to_object: false) do |object, transition|
object.class.current_transaction.after_commit do
# The transition's catch(:halt) is gone at commit time
catch(:halt) { deferred.call(object, {}, transition) }
rescue StandardError => e
# Raising would roll back in-memory state already committed; report instead
ActiveSupport.error_reporter.report(e, handled: false, source: 'state_machines-activerecord')
end
end
end
# Machine internals (state matching, validations) call read() to get the
# current state value and compare it against state.value. The custom
# integer type already returns the canonical value when state values are
# uniform: raw integers when every named state has an explicit integer
# value (passthrough), name strings when none do (state.value is the
# name). Only machines mixing explicit and auto-indexed values need an
# override, because the type returns name strings while the explicit
# states match on integers; map the stored value back to the matched
# state's canonical state.value.
#
# @param object [ActiveRecord::Base] record being read
# @param attr_sym [Symbol] attribute kind (:state, :event, ...)
# @param ivar [Boolean] whether to read from an instance variable
# @return [Object] a value machine internals can match on state.value
def read(object, attr_sym, ivar = false)
return super unless integer_type_registered? && attr_sym == :state
return super unless mixed_integer_state_values?
raw = object.read_attribute_before_type_cast(attribute.to_s)
if raw.is_a?(::String) || raw.is_a?(::Symbol)
matched = states.detect { |s| s.name && s.name.to_s == raw.to_s }
return matched.value if matched
end
name = owner_class.type_for_attribute(attribute.to_s).deserialize(raw)
matched = states.detect { |s| s.name && s.name.to_s == name.to_s }
matched ? matched.value : raw
end
private
# Re-runs the conflicting-default check for states defined after
# initialization. Skipped until an initial state exists (the DSL block
# evaluates before initial_state= during Machine.new).
#
# @return [void]
def recheck_conflicting_attribute_default
return unless states.detect(&:initial)
check_conflicting_attribute_default
end
# Returns true when named states mix explicit integer values with
# auto-indexed (name-valued) states. Uses value(false) so dynamic
# (Proc) state values are never evaluated for this metadata decision.
#
# @return [Boolean]
def mixed_integer_state_values?
named = states.select(&:name)
explicit = named.count { |s| s.value(false).is_a?(::Integer) }
explicit.positive? && explicit < named.size
end
# Returns true when the state machine attribute is backed by an integer column
def integer_column?
return false unless owner_class.respond_to?(:type_for_attribute)
return false unless owner_class.connected? && owner_class.table_exists?
owner_class.type_for_attribute(attribute.to_s).type == :integer
rescue ::ActiveRecord::StatementInvalid, ::ActiveRecord::ConnectionNotEstablished
false
end
# Registers a custom AR attribute type so that integer columns transparently
# convert between state name strings and stored integers.
# Saves the raw column default first so the conflicting-default check
# (which fires later, during initial_state=) still compares raw integers.
def register_integer_type
@raw_integer_column_default = owner_class.column_defaults[attribute.to_s]
@integer_type_registered = true
# When re-registering (e.g. for an STI subclass that inherited the
# parent's custom type), unwrap it to keep the column's original type
# as the passthrough delegate.
current_type = owner_class.type_for_attribute(attribute.to_s)
raw_type = current_type.is_a?(StateMachines::Type::Integer) ? current_type.raw_type : current_type
owner_class.attribute(attribute.to_s, StateMachines::Type::Integer.new(states, raw_type: raw_type))
end
# Detect existing enum methods for this attribute
def detect_existing_enum_methods
return [] unless owner_class.defined_enums.key?(attribute.to_s)
enum_values = owner_class.defined_enums[attribute.to_s]
methods = []
enum_values.each_key do |value|
# Predicate methods like 'active?'
predicate = "#{value}?"
methods << predicate if owner_class.method_defined?(predicate)
# Bang methods like 'active!'
bang_method = "#{value}!"
methods << bang_method if owner_class.method_defined?(bang_method)
# Scope methods (class-level)
methods << value.to_s if owner_class.respond_to?(value)
methods << "not_#{value}" if owner_class.respond_to?("not_#{value}")
end
methods
end
# Generate method name with prefix/suffix based on configuration
def generate_state_method_name(state_name, method_type)
return state_name unless enum_integrated?
config = enum_integration
base_name = case method_type
when :predicate
"#{state_name}?"
when :bang
"#{state_name}!"
else
state_name.to_s
end
# Apply prefix
if config[:prefix]
prefix = config[:prefix] == true ? "#{attribute}_" : "#{config[:prefix]}_"
base_name = "#{prefix}#{base_name}"
end
# Apply suffix
if config[:suffix]
suffix = config[:suffix] == true ? "_#{attribute}" : "_#{config[:suffix]}"
base_name = base_name.gsub(/(\?|!)$/, "#{suffix}\\1")
base_name = "#{base_name}#{suffix}" unless base_name.end_with?('?', '!')
end
base_name
end
# Generate state machine methods with conflict resolution
def generate_state_machine_methods
return unless enum_integrated?
# Initialize tracking if not already done
@processed_states ||= Set.new
enum_integration[:state_machine_methods] ||= []
# Get all states for this machine
states.each do |state|
state_name = state.name.to_s
next if state.nil? # Skip nil state
next if @processed_states.include?(state_name) # Skip already processed states
# Generate predicate method (e.g., status_pending?)
predicate_method = generate_state_method_name(state_name, :predicate)
if predicate_method != "#{state_name}?"
define_state_predicate_method(state_name, predicate_method)
track_generated_method(predicate_method)
end
# Generate bang method (e.g., status_pending!)
bang_method = generate_state_method_name(state_name, :bang)
if bang_method != "#{state_name}!"
define_state_bang_method(state_name, bang_method)
track_generated_method(bang_method)
end
# Generate scope methods (e.g., status_pending) if scopes are enabled
if enum_integration[:scopes]
scope_method = generate_state_method_name(state_name, :scope)
if scope_method != state_name
define_state_scope_method(state_name, scope_method)
track_generated_method(scope_method)
end
end
# Mark this state as processed
@processed_states.add(state_name)
end
end
# Define a prefixed predicate method for a state
def define_state_predicate_method(state_name, method_name)
machine_attribute = attribute
target_state_name = state_name.to_sym
owner_class.define_method(method_name) do
machine = self.class.state_machine(machine_attribute)
machine.states.matches?(self, target_state_name)
end
end
# Define a prefixed bang method for a state
def define_state_bang_method(state_name, method_name)
owner_class.define_method(method_name) do
# Raise an error with actionable guidance
raise "#{method_name} is a conflict-resolution placeholder. " \
"Use the original enum method '#{state_name}!' or state machine events instead."
end
end
# Define a prefixed scope method for a state
def define_state_scope_method(state_name, method_name)
machine_attribute = attribute
scope_lambda = lambda do |value = true|
machine = state_machine(machine_attribute)
state_value = machine.states[state_name.to_sym].value
if value
where(machine_attribute => state_value)
else
where.not(machine_attribute => state_value)
end
end
owner_class.define_singleton_method(method_name, &scope_lambda)
owner_class.define_singleton_method("not_#{method_name}") do
public_send(method_name, false)
end
end
# Track generated state machine methods for introspection
def track_generated_method(method_name)
return unless enum_integrated?
# Use a Set to ensure no duplicates
enum_integration[:state_machine_methods] ||= []
return if enum_integration[:state_machine_methods].include?(method_name)
enum_integration[:state_machine_methods] << method_name
end
end
# Include MachineMethods to make enum integration methods available on machine instances
include MachineMethods
class << self
attr_accessor :auto_convert_integer_state_attributes
# Classes that inherit from ActiveRecord::Base will automatically use
# the ActiveRecord integration.
def matching_ancestors
[::ActiveRecord::Base]
end
end
protected
# Only runs validations on the action if using :save
def runs_validations_on_action?
action == :save
end
# Gets the db default for the machine's attribute.
# For integer columns the raw pre-type-registration default is returned so
# that check_conflicting_attribute_default can compare integers to integers.
def owner_class_attribute_default
return @raw_integer_column_default if defined?(@raw_integer_column_default)
return unless owner_class.connected? && owner_class.table_exists?
owner_class.column_defaults[attribute.to_s]
end
# Checks whether the given state matches the column default. When the
# custom integer type is registered, auto-indexed states match on their
# name string while the column default is a raw integer, so the default
# is also compared through the type's mapping (e.g. 0 matches the first
# auto-indexed state).
#
# @param state [StateMachines::State] the state to compare (the initial state)
# @return [Boolean] whether the column default represents this state
def owner_class_attribute_default_matches?(state)
matches = super
return matches if matches || !integer_type_registered?
default = owner_class_attribute_default
state.matches?(owner_class.type_for_attribute(attribute.to_s).deserialize(default))
end
def define_state_initializer
define_helper :instance, <<-END_EVAL, __FILE__, __LINE__ + 1
def initialize(attributes = nil, *)
super(attributes) do |*args|
attributes = (attributes || {}).transform_keys { |key| self.class.attribute_aliases[key.to_s] || key }
scoped_attributes = attributes.merge(self.class.scope_attributes)
self.class.state_machines.initialize_states(self, {}, scoped_attributes)
yield(*args) if block_given?
end
end
END_EVAL
end
# Uses around callbacks to run state events if using the :save hook
def define_action_hook
if action_hook == :save
define_helper :instance, <<-END_EVAL, __FILE__, __LINE__ + 1
def save(*, **)
self.class.state_machine(#{name.inspect}).send(:around_save, self) { super }
end
def save!(*, **)
result = self.class.state_machine(#{name.inspect}).send(:around_save, self) { super }
result || raise(ActiveRecord::RecordInvalid.new(self))
end
def changed_for_autosave?
super || self.class.state_machines.any? {|name, machine| machine.action == :save && machine.read(self, :event)}
end
END_EVAL
else
super
end
end
# Runs state events around the machine's :save action
def around_save(object, &)
# Pass fiber: false to avoid deadlocks with ActiveRecord's LoadInterlockAwareMonitor
object.class.state_machines.transitions(object, action, fiber: false).perform(&)
end
# Creates a scope for finding records *with* a particular state or
# states for the attribute
def create_with_scope(_name)
attr_name = attribute
lambda do |klass, values|
if values.present?
klass.where(attr_name => values)
else
klass.all
end
end
end
# Creates a scope for finding records *without* a particular state or
# states for the attribute
def create_without_scope(_name)
attr_name = attribute
lambda do |klass, values|
if values.present?
klass.where.not(attr_name => values)
else
klass.all
end
end
end
# Runs a new database transaction, rolling back any changes by raising
# an ActiveRecord::Rollback exception if the yielded block fails
# (i.e. returns false).
def transaction(object)
result = nil
object.class.transaction do
raise ::ActiveRecord::Rollback unless (result = yield)
end
result
end
def locale_path
"#{File.dirname(__FILE__)}/active_record/locale.rb"
end
private
# ActiveModel's use of method_missing / respond_to for attribute methods
# breaks both ancestor lookups and defined?(super). Need to special-case
# the existence of query attribute methods.
def owner_class_ancestor_has_method?(scope, method)
scope == :instance && method == "#{attribute}?" ? owner_class : super
end
end
register(ActiveRecord)
end
end
state-machines-state_machines-activerecord-b9cb1e5/lib/state_machines/integrations/active_record/ 0000775 0000000 0000000 00000000000 15213055011 0033615 5 ustar 00root root 0000000 0000000 locale.rb 0000664 0000000 0000000 00000001117 15213055011 0035322 0 ustar 00root root 0000000 0000000 state-machines-state_machines-activerecord-b9cb1e5/lib/state_machines/integrations/active_record # frozen_string_literal: true
# Use lazy evaluation to avoid circular dependencies with frozen default_messages
# This ensures messages can be updated after gem loading while maintaining thread safety
{ en: {
activerecord: {
errors: {
messages: {
invalid: ->(*) { StateMachines::Machine.default_messages[:invalid] },
invalid_event: ->(*) { format(StateMachines::Machine.default_messages[:invalid_event], '%s') },
invalid_transition: ->(*) { format(StateMachines::Machine.default_messages[:invalid_transition], '%s') }
}
}
}
} }
type/ 0000775 0000000 0000000 00000000000 15213055011 0034517 5 ustar 00root root 0000000 0000000 state-machines-state_machines-activerecord-b9cb1e5/lib/state_machines/integrations/active_record integer.rb 0000664 0000000 0000000 00000011707 15213055011 0036507 0 ustar 00root root 0000000 0000000 state-machines-state_machines-activerecord-b9cb1e5/lib/state_machines/integrations/active_record/type # frozen_string_literal: true
module StateMachines
module Type
# Custom ActiveRecord attribute type for state machine attributes backed by
# integer columns. Handles bidirectional conversion between state name strings
# (used internally by the state machine) and integer values (stored in the DB).
#
# States without explicit integer values are mapped by their index position
# in the states collection (0, 1, 2, …). States with an explicit integer
# value (e.g. state :pending, value: 2) use that value directly.
#
# When *every* named state has an explicit integer value, the column already
# stores the canonical state values and no name<->integer conversion is
# needed. In that case the type delegates to the column's original integer
# type, preserving the classic raw-integer behavior
# (e.g. record.status # => 1, record.status_name # => :approved).
class Integer < ::ActiveRecord::Type::Value
# The column's original attribute type, exposed so re-registration
# (e.g. for STI subclasses) can reuse it instead of wrapping this type.
#
# @return [ActiveModel::Type::Value]
attr_reader :raw_type
# @param states [StateMachines::StateCollection] live collection of the
# machine's states; held by reference because states are defined after
# the type is registered
# @param raw_type [ActiveModel::Type::Value, nil] the column's original
# attribute type, used verbatim in passthrough mode so adapter-specific
# integer behavior (limits, range checks) is preserved
def initialize(states, raw_type: nil)
@states = states
@raw_type = raw_type || ::ActiveModel::Type::Integer.new
super()
end
# Converts an integer from the database to a state name string.
#
# @param value [Integer, String, nil] raw database value
# @return [String, Integer, nil] state name, or the original type's value
# in passthrough mode, or the raw value when no state matches
def deserialize(value)
states = named_states
return @raw_type.deserialize(value) if passthrough?(states)
return nil if value.nil?
int_val = value.to_i
state = states.detect { |s| state_integer(s, states) == int_val }
state ? state.name.to_s : value
end
# Converts an assigned value (symbol, string, or integer) to the in-memory
# state name string.
#
# @param value [Symbol, String, Integer, nil] assigned value
# @return [String, Integer, nil] state name, or the original type's cast
# in passthrough mode
def cast(value)
states = named_states
return @raw_type.cast(value) if passthrough?(states)
return nil if value.nil?
state = states.detect { |s| s.name.to_s == value.to_s }
state ||= states.detect { |s| state_integer(s, states) == value.to_i } if value.respond_to?(:to_i)
state ? state.name.to_s : value.to_s
end
# Converts a state name string to its integer for the database write.
#
# @param value [String, Symbol, Integer, nil] in-memory value
# @return [Integer, nil] integer to store
def serialize(value)
states = named_states
return @raw_type.serialize(value) if passthrough?(states)
return nil if value.nil?
state = states.detect { |s| s.name.to_s == value.to_s }
state ? state_integer(state, states) : value
end
# @return [Symbol] the ActiveModel type identifier
def type
:integer
end
private
# All non-nil states in definition order, not memoized because states are
# added to the collection after the type is instantiated.
#
# @return [Array]
def named_states
@states.reject { |s| s.name.nil? }
end
# Whether the machine was defined for raw integer storage, in which case
# conversion would change documented behavior. True when every named state
# declares an explicit integer value. Evaluated lazily on every call
# because states (and their values) are defined after the type is
# registered. Uses value(false) so dynamic (Proc) state values are never
# evaluated for this metadata decision.
#
# @param states [Array] pre-computed named states
# @return [Boolean]
def passthrough?(states)
states.any? && states.all? { |s| s.value(false).is_a?(::Integer) }
end
# The integer to use for storage: the explicit state value if set
# (e.g. state :pending, value: 2), otherwise the index position among
# named states.
#
# @param state [StateMachines::State]
# @param states [Array] pre-computed named states
# @return [Integer]
def state_integer(state, states)
value = state.value(false)
value.is_a?(::Integer) ? value : states.index(state)
end
end
end
end
version.rb 0000664 0000000 0000000 00000000216 15213055011 0035547 0 ustar 00root root 0000000 0000000 state-machines-state_machines-activerecord-b9cb1e5/lib/state_machines/integrations/active_record # frozen_string_literal: true
module StateMachines
module Integrations
module ActiveRecord
VERSION = '0.200.0'
end
end
end
state-machines-state_machines-activerecord-b9cb1e5/log/ 0000775 0000000 0000000 00000000000 15213055011 0023322 5 ustar 00root root 0000000 0000000 state-machines-state_machines-activerecord-b9cb1e5/log/.gitkeep 0000664 0000000 0000000 00000000000 15213055011 0024741 0 ustar 00root root 0000000 0000000 state-machines-state_machines-activerecord-b9cb1e5/release-please-config.json 0000664 0000000 0000000 00000000315 15213055011 0027565 0 ustar 00root root 0000000 0000000 {
"packages": {
".": {
"release-type": "ruby",
"package-name": "state_machines-activerecord",
"version-file": "lib/state_machines/integrations/active_record/version.rb"
}
}
}
state-machines-state_machines-activerecord-b9cb1e5/state_machines-activerecord.gemspec 0000664 0000000 0000000 00000002350 15213055011 0031545 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require_relative 'lib/state_machines/integrations/active_record/version'
Gem::Specification.new do |spec|
spec.name = 'state_machines-activerecord'
spec.version = StateMachines::Integrations::ActiveRecord::VERSION
spec.authors = ['Abdelkader Boudih', 'Aaron Pfeifer']
spec.email = %w[terminale@gmail.com aaron@pluginaweek.org]
spec.summary = 'State machines Active Record Integration'
spec.description = 'Adds support for creating state machines for attributes on ActiveRecord'
spec.homepage = 'https://github.com/state-machines/state_machines-activerecord/'
spec.license = 'MIT'
spec.files = Dir['{lib}/**/*', 'LICENSE.txt', 'README.md']
spec.required_ruby_version = '>= 3.2'
spec.require_paths = ['lib']
spec.add_dependency 'activerecord', '>= 7.2'
spec.add_dependency 'state_machines-activemodel', '>= 0.200.0'
spec.add_development_dependency 'appraisal', '>= 1'
spec.add_development_dependency 'minitest', '= 5.27.0'
spec.add_development_dependency 'minitest-reporters'
spec.add_development_dependency 'rake', '~> 13.0'
spec.add_development_dependency 'sqlite3', '~> 2.1'
spec.metadata['rubygems_mfa_required'] = 'true'
end
state-machines-state_machines-activerecord-b9cb1e5/test/ 0000775 0000000 0000000 00000000000 15213055011 0023520 5 ustar 00root root 0000000 0000000 state-machines-state_machines-activerecord-b9cb1e5/test/files/ 0000775 0000000 0000000 00000000000 15213055011 0024622 5 ustar 00root root 0000000 0000000 state-machines-state_machines-activerecord-b9cb1e5/test/files/en.yml 0000664 0000000 0000000 00000000137 15213055011 0025750 0 ustar 00root root 0000000 0000000 en:
activerecord:
errors:
messages:
invalid_transition: "cannot transition" state-machines-state_machines-activerecord-b9cb1e5/test/files/models/ 0000775 0000000 0000000 00000000000 15213055011 0026105 5 ustar 00root root 0000000 0000000 state-machines-state_machines-activerecord-b9cb1e5/test/files/models/post.rb 0000664 0000000 0000000 00000000360 15213055011 0027416 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
ActiveRecord::Base.connection.create_table(:posts, force: true) do |t|
t.string :title
t.string :content
t.string :state
end
class Post < ActiveRecord::Base
state_machine initial: :draft do
end
end
state-machines-state_machines-activerecord-b9cb1e5/test/integration_test.rb 0000664 0000000 0000000 00000002754 15213055011 0027437 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require_relative 'test_helper'
class IntegrationTest < BaseTestCase
def teardown
StateMachines::Integrations::ActiveRecord.auto_convert_integer_state_attributes = true
super
end
def test_should_have_an_integration_name
assert_equal :active_record, StateMachines::Integrations::ActiveRecord.integration_name
end
def test_should_be_before_activemodel
integrations = StateMachines::Integrations.list.to_a
assert StateMachines::Integrations::ActiveRecord, integrations.first
assert StateMachines::Integrations::ActiveModel, integrations.last
end
def test_should_match_if_class_inherits_from_active_record
assert StateMachines::Integrations::ActiveRecord.matches?(new_model)
end
def test_should_not_match_if_class_does_not_inherit_from_active_record
refute StateMachines::Integrations::ActiveRecord.matches?(Class.new)
end
def test_should_have_defaults
assert_equal({ action: :save, use_transactions: true }, StateMachines::Integrations::ActiveRecord.defaults)
end
def test_should_auto_convert_integer_state_attributes_by_default
assert_equal true, StateMachines::Integrations::ActiveRecord.auto_convert_integer_state_attributes
end
def test_should_allow_integer_state_attribute_conversion_to_be_disabled
StateMachines::Integrations::ActiveRecord.auto_convert_integer_state_attributes = false
assert_equal false, StateMachines::Integrations::ActiveRecord.auto_convert_integer_state_attributes
end
end
state-machines-state_machines-activerecord-b9cb1e5/test/machine_by_default_test.rb 0000664 0000000 0000000 00000000567 15213055011 0030716 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require_relative 'test_helper'
class MachineByDefaultTest < BaseTestCase
def setup
@model = new_model
@machine = StateMachines::Machine.new(@model)
end
def test_should_use_save_as_action
assert_equal :save, @machine.action
end
def test_should_use_transactions
assert_equal true, @machine.use_transactions
end
end
state-machines-state_machines-activerecord-b9cb1e5/test/machine_errors_test.rb 0000664 0000000 0000000 00000001137 15213055011 0030106 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require_relative 'test_helper'
class MachineErrorsTest < BaseTestCase
def setup
@model = new_model
@machine = StateMachines::Machine.new(@model)
@record = @model.new
end
def test_should_be_able_to_describe_current_errors
@record.errors.add(:id, 'cannot be blank')
@record.errors.add(:state, 'is invalid')
assert_equal ['Id cannot be blank', 'State is invalid'], @machine.errors_for(@record).split(', ').sort
end
def test_should_describe_as_halted_with_no_errors
assert_equal 'Transition halted', @machine.errors_for(@record)
end
end
state-machines-state_machines-activerecord-b9cb1e5/test/machine_multiple_test.rb 0000664 0000000 0000000 00000001026 15213055011 0030422 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require_relative 'test_helper'
class MachineMultipleTest < BaseTestCase
def setup
@model = new_model do
connection.add_column table_name, :status, :string
end
@state_machine = StateMachines::Machine.new(@model, initial: :parked)
@status_machine = StateMachines::Machine.new(@model, :status, initial: :idling)
end
def test_should_should_initialize_each_state
record = @model.new
assert_equal 'parked', record.state
assert_equal 'idling', record.status
end
end
state-machines-state_machines-activerecord-b9cb1e5/test/machine_nested_action_test.rb 0000664 0000000 0000000 00000001634 15213055011 0031413 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require_relative 'test_helper'
class MachineNestedActionTest < BaseTestCase
def setup
@callbacks = []
@model = new_model
@machine = StateMachines::Machine.new(@model)
@machine.event :ignite do
transition parked: :idling
end
@record = @model.new(state: 'parked')
end
def test_should_allow_transition_prior_to_creation_if_skipping_action
record = @record
@model.before_create { record.ignite(false) }
result = @record.save
assert_equal true, result
assert_equal 'idling', @record.state
@record.reload
assert_equal 'idling', @record.state
end
def test_should_allow_transition_after_creation
record = @record
@model.after_create { record.ignite }
result = @record.save
assert_equal true, result
assert_equal 'idling', @record.state
@record.reload
assert_equal 'idling', @record.state
end
end
state-machines-state_machines-activerecord-b9cb1e5/test/machine_unmigrated_test.rb 0000664 0000000 0000000 00000000625 15213055011 0030732 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require_relative 'test_helper'
class MachineUnmigratedTest < BaseTestCase
def setup
@model = new_model(false)
# Drop the table so that it definitely doesn't exist
@model.connection.drop_table(@model.table_name) if @model.table_exists?
end
def test_should_allow_machine_creation
assert_nothing_raised { StateMachines::Machine.new(@model) }
end
end
machine_with_after_commit_transition_callbacks_test.rb 0000664 0000000 0000000 00000022266 15213055011 0036476 0 ustar 00root root 0000000 0000000 state-machines-state_machines-activerecord-b9cb1e5/test # frozen_string_literal: true
require_relative 'test_helper'
class MachineWithAfterCommitTransitionCallbacksTest < BaseTestCase
def setup
@model = new_model do
attr_reader :callback_log
def positional_method(transition)
log_callback(:positional, transition)
end
def do_method(transition)
log_callback(:do, transition)
end
def log_callback(name, transition)
(@callback_log ||= []) << [name, transition.event]
end
end
@machine = StateMachines::Machine.new(@model, initial: :parked)
@machine.other_states :idling
@machine.event :ignite do
transition parked: :idling
end
end
def test_should_run_callback_after_commit
order = []
@machine.after_transition { order << :after_transition }
@machine.after_transition(after_commit: true) { order << :deferred }
@model.after_commit { order << :model_after_commit }
record = @model.create
order.clear
record.ignite
assert_equal %i[after_transition model_after_commit deferred], order
end
def test_should_defer_callback_until_outer_transaction_commits
called = false
@machine.after_transition(after_commit: true) { called = true }
record = @model.create
called = false
@model.transaction do
record.ignite
refute called, 'Callback should not run before the outer transaction commits'
end
assert called
end
def test_should_discard_callback_on_rollback
called = false
@machine.after_transition(after_commit: true) { called = true }
record = @model.create
called = false
@model.transaction do
record.ignite
raise ActiveRecord::Rollback
end
refute called
assert_equal 'parked', record.reload.state
end
def test_should_pass_record_and_transition_to_callback
callback_args = nil
@machine.after_transition(after_commit: true) { |*args| callback_args = args }
record = @model.create
record.ignite
object, transition = callback_args
assert_equal record, object
assert_instance_of StateMachines::Transition, transition
assert_equal :ignite, transition.event
assert_equal 'parked', transition.from
assert_equal 'idling', transition.to
end
def test_should_run_outside_the_transaction
plain_in_transaction = nil
deferred_in_transaction = nil
@machine.after_transition { |object| plain_in_transaction = object.class.connection.transaction_open? }
@machine.after_transition(after_commit: true) { |object| deferred_in_transaction = object.class.connection.transaction_open? }
record = @model.create
record.ignite
assert plain_in_transaction, 'Plain after_transition should run inside the transaction'
refute deferred_in_transaction, 'Deferred callback should run after the transaction has committed'
end
def test_should_support_do_option_with_method_symbol
@machine.after_transition(after_commit: true, do: :do_method)
record = @model.create
record.ignite
assert_equal [[:do, :ignite]], record.callback_log
end
def test_should_support_positional_method_symbol
@machine.after_transition(:positional_method, after_commit: true)
record = @model.create
record.ignite
assert_equal [[:positional, :ignite]], record.callback_log
end
def test_should_prefer_positional_methods_over_do_option
# Core's parse_callback_arguments drops :do when positional methods are
# given; the deferred path must match to avoid double-running side effects
@machine.after_transition(:positional_method, after_commit: true, do: :do_method)
record = @model.create
record.ignite
assert_equal [[:positional, :ignite]], record.callback_log
end
def test_should_bind_block_to_object_when_requested
callback_self = nil
callback_args = nil
@machine.after_transition(after_commit: true, bind_to_object: true) do |*args|
callback_self = self
callback_args = args
end
record = @model.create
record.ignite
assert_equal record, callback_self
assert_equal 1, callback_args.length
assert_instance_of StateMachines::Transition, callback_args.first
end
def test_should_work_with_global_bind_to_object_default
original = StateMachines::Callback.bind_to_object
StateMachines::Callback.bind_to_object = true
callback_self = nil
transition_arg = nil
@machine.after_transition(after_commit: true) do |transition|
callback_self = self
transition_arg = transition
end
record = @model.create
record.ignite
assert_equal record, callback_self
assert_instance_of StateMachines::Transition, transition_arg
ensure
StateMachines::Callback.bind_to_object = original
end
def test_should_detect_flag_in_positional_options_hash
called = false
@machine.after_transition({ after_commit: true }) { called = true }
record = @model.create
called = false
@model.transaction do
record.ignite
refute called
end
assert called
end
def test_should_respect_branch_requirements
fired = []
@machine.state :stalled
@machine.event :crash do
transition idling: :stalled
end
@machine.after_transition(on: :ignite, after_commit: true) { fired << :ignite_callback }
@machine.after_transition(on: :crash, after_commit: true) { fired << :crash_callback }
record = @model.create
record.ignite
assert_equal [:ignite_callback], fired
record.crash
assert_equal %i[ignite_callback crash_callback], fired
end
def test_should_run_immediately_when_no_transaction_is_open
# With no action, the transition opens no transaction at all, so the
# callback must execute synchronously via the null transaction
model = new_model
machine = StateMachines::Machine.new(model, initial: :parked, action: nil)
machine.other_states :idling
machine.event :ignite do
transition parked: :idling
end
called_during_event = false
machine.after_transition(after_commit: true) do |object|
called_during_event = true
refute object.class.connection.transaction_open?
end
record = model.create
record.ignite
assert called_during_event
end
def test_should_not_affect_transition_result
@machine.after_transition(after_commit: true) { throw :halt }
record = @model.create
assert record.ignite
assert_equal 'idling', record.state
end
def test_should_report_exception_and_preserve_committed_state
subscriber = Class.new do
attr_reader :errors
def initialize
@errors = []
end
def report(error, **)
@errors << error
end
end.new
ActiveSupport.error_reporter.subscribe(subscriber)
@machine.after_transition(after_commit: true) { raise 'boom' }
record = @model.create
assert record.ignite, 'Event should succeed despite the deferred callback raising'
assert_equal 'idling', record.state, 'In-memory state must not be rolled back'
assert_equal 'idling', record.reload.state
assert_equal ['boom'], subscriber.errors.map(&:message)
ensure
ActiveSupport.error_reporter.unsubscribe(subscriber)
end
def test_should_not_mutate_do_option_array
do_methods = [:do_method].freeze
@machine.after_transition(after_commit: true, do: do_methods)
record = @model.create
record.ignite
assert_equal [[:do, :ignite]], record.callback_log
assert_equal [:do_method], do_methods
end
def test_should_match_model_after_commit_semantics_under_non_joinable_wrapper
# joinable: false wrappers (e.g. transactional test fixtures) are
# transparent to all of Rails' commit callbacks; ours must behave the same
fired = []
@model.after_commit { fired << :model }
@machine.after_transition(after_commit: true) { fired << :deferred }
record = @model.create
fired.clear
@model.transaction(joinable: false) do
record.ignite
raise ActiveRecord::Rollback
end
assert_equal %i[model deferred], fired
end
def test_should_keep_state_named_after_commit_as_branch_requirement
fired = []
@machine.state :after_commit, :done
@machine.event :finish do
transition after_commit: :done
end
@machine.after_transition(after_commit: :done) { fired << :scoped }
record = @model.create
record.ignite
assert_empty fired, 'Callback scoped to after_commit => done must not fire for parked => idling'
record.update!(state: 'after_commit')
record.finish
assert_equal [:scoped], fired
end
def test_should_treat_false_flag_as_regular_callback
in_transaction = nil
@machine.after_transition(after_commit: false) { |object| in_transaction = object.class.connection.transaction_open? }
record = @model.create
record.ignite
assert in_transaction, 'after_commit: false should behave as a plain after_transition'
end
def test_should_raise_without_methods
assert_raises(ArgumentError) { @machine.after_transition(after_commit: true) }
end
def test_should_not_alter_plain_after_transition
called = false
@machine.after_transition { called = true }
record = @model.create
called = false
@model.transaction do
record.ignite
assert called, 'Plain after_transition should still run inside the transaction'
end
end
end
state-machines-state_machines-activerecord-b9cb1e5/test/machine_with_aliased_attribute_test.rb 0000664 0000000 0000000 00000001065 15213055011 0033312 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require_relative 'test_helper'
class MachineWithAliasedAttributeTest < BaseTestCase
def setup
@model = new_model do
alias_attribute :vehicle_status, :state
end
@machine = StateMachines::Machine.new(@model, :status, attribute: :vehicle_status)
@machine.state :parked
@record = @model.new
end
def test_should_check_custom_attribute_for_predicate
@record.vehicle_status = nil
refute @record.status?(:parked)
@record.vehicle_status = 'parked'
assert @record.status?(:parked)
end
end
machine_with_auto_indexed_integer_column_default_test.rb 0000664 0000000 0000000 00000003024 15213055011 0037011 0 ustar 00root root 0000000 0000000 state-machines-state_machines-activerecord-b9cb1e5/test # frozen_string_literal: true
require_relative 'test_helper'
require 'stringio'
# Auto-indexed integer machines store the first state as 0, so a column
# default of 0 matches the initial state and must not trigger the
# conflicting-default warning. A genuinely different default still warns.
class MachineWithSameAutoIndexedIntegerColumnDefaultTest < BaseTestCase
def setup
@original_stderr = $stderr
$stderr = StringIO.new
@model = new_model do
connection.add_column table_name, :status, :integer, default: 0
end
@machine = StateMachines::Machine.new(@model, :status, initial: :open) do
state :open
state :closed
end
@record = @model.new
end
def teardown
$stderr = @original_stderr
super
end
def test_should_use_machine_default
assert_equal 'open', @record.status
end
def test_should_not_generate_a_warning
assert_no_match(/have defined a different default/, $stderr.string)
end
end
class MachineWithDifferentAutoIndexedIntegerColumnDefaultTest < BaseTestCase
def setup
@original_stderr = $stderr
$stderr = StringIO.new
@model = new_model do
connection.add_column table_name, :status, :integer, default: 1
end
@machine = StateMachines::Machine.new(@model, :status, initial: :open) do
state :open
state :closed
end
@record = @model.new
end
def teardown
$stderr = @original_stderr
super
end
def test_should_generate_a_warning
assert_match(/have defined a different default/, $stderr.string)
end
end
state-machines-state_machines-activerecord-b9cb1e5/test/machine_with_callbacks_test.rb 0000664 0000000 0000000 00000011146 15213055011 0031545 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require_relative 'test_helper'
class MachineWithCallbacksTest < BaseTestCase
def setup
@model = new_model
@machine = StateMachines::Machine.new(@model, initial: :parked)
@machine.other_states :idling
@machine.event :ignite
@record = @model.new(state: 'parked')
@transition = StateMachines::Transition.new(@record, @machine, :ignite, :parked, :idling)
end
def test_should_run_before_callbacks
called = false
@machine.before_transition { called = true }
@transition.perform
assert called
end
def test_should_pass_record_to_before_callbacks_with_one_argument
record = nil
@machine.before_transition { |arg| record = arg }
@transition.perform
assert_equal @record, record
end
def test_should_pass_record_and_transition_to_before_callbacks_with_multiple_arguments
callback_args = nil
@machine.before_transition { |*args| callback_args = args }
@transition.perform
assert_equal [@record, @transition], callback_args
end
def test_should_run_before_callbacks_outside_the_context_of_the_record
context = nil
@machine.before_transition { context = self }
@transition.perform
assert_equal self, context
end
def test_should_run_after_callbacks
called = false
@machine.after_transition { called = true }
@transition.perform
assert called
end
def test_should_pass_record_to_after_callbacks_with_one_argument
record = nil
@machine.after_transition { |arg| record = arg }
@transition.perform
assert_equal @record, record
end
def test_should_pass_record_and_transition_to_after_callbacks_with_multiple_arguments
callback_args = nil
@machine.after_transition { |*args| callback_args = args }
@transition.perform
assert_equal [@record, @transition], callback_args
end
def test_should_run_after_callbacks_outside_the_context_of_the_record
context = nil
@machine.after_transition { context = self }
@transition.perform
assert_equal self, context
end
def test_should_run_after_callbacks_if_model_callback_added_prior_to_state_machine_definition
model = new_model do
after_save { nil }
end
machine = StateMachines::Machine.new(model, initial: :parked)
machine.other_states :idling
machine.event :ignite
after_called = false
machine.after_transition { after_called = true }
record = model.new(state: 'parked')
transition = StateMachines::Transition.new(record, machine, :ignite, :parked, :idling)
transition.perform
assert_equal true, after_called
end
def test_should_run_around_callbacks
before_called = false
after_called = false
ensure_called = 0
@machine.around_transition do |block|
before_called = true
begin
block.call
ensure
ensure_called += 1
end
after_called = true
end
@transition.perform
assert before_called
assert after_called
assert_equal ensure_called, 1
end
def test_should_include_transition_states_in_known_states
@machine.before_transition to: :first_gear, do: -> {}
assert_equal(%i[parked idling first_gear], @machine.states.map { |state| state.name })
end
def test_should_allow_symbolic_callbacks
callback_args = nil
klass = class << @record
self
end
klass.send(:define_method, :after_ignite) do |*args|
callback_args = args
end
@machine.before_transition(:after_ignite)
@transition.perform
assert_equal [@transition], callback_args
end
def test_should_allow_string_callbacks
class << @record
attr_reader :callback_result
end
@machine.before_transition('@callback_result = [1, 2, 3]')
@transition.perform
assert_equal [1, 2, 3], @record.callback_result
end
def test_should_run_in_expected_order
expected = %i[
before_transition before_validation after_validation
before_save before_create after_create after_save
after_transition
]
callbacks = []
@model.before_validation { callbacks << :before_validation }
@model.after_validation { callbacks << :after_validation }
@model.before_save { callbacks << :before_save }
@model.before_create { callbacks << :before_create }
@model.after_create { callbacks << :after_create }
@model.after_save { callbacks << :after_save }
@model.after_commit { callbacks << :after_commit }
expected << :after_commit
@machine.before_transition { callbacks << :before_transition }
@machine.after_transition { callbacks << :after_transition }
@transition.perform
assert_equal expected, callbacks
end
end
state-machines-state_machines-activerecord-b9cb1e5/test/machine_with_column_state_attribute_test.rb 0000664 0000000 0000000 00000002217 15213055011 0034405 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require_relative 'test_helper'
class MachineWithColumnStateAttributeTest < BaseTestCase
def setup
@model = new_model
@machine = StateMachines::Machine.new(@model, initial: :parked)
@machine.other_states(:idling)
@record = @model.new
end
def test_should_not_override_the_column_reader
@record[:state] = 'parked'
assert_equal 'parked', @record.state
end
def test_should_not_override_the_column_writer
@record.state = 'parked'
assert_equal 'parked', @record[:state]
end
def test_should_have_an_attribute_predicate
assert @record.respond_to?(:state?)
end
def test_should_test_for_existence_on_predicate_without_parameters
assert @record.state?
@record.state = nil
refute @record.state?
end
def test_should_return_false_for_predicate_if_does_not_match_current_value
refute @record.state?(:idling)
end
def test_should_return_true_for_predicate_if_matches_current_value
assert @record.state?(:parked)
end
def test_should_raise_exception_for_predicate_if_invalid_state_specified
assert_raises(IndexError) { @record.state?(:invalid) }
end
end
machine_with_complex_pluralization_scopes_test.rb 0000664 0000000 0000000 00000000646 15213055011 0035552 0 ustar 00root root 0000000 0000000 state-machines-state_machines-activerecord-b9cb1e5/test # frozen_string_literal: true
require_relative 'test_helper'
class MachineWithComplexPluralizationScopesTest < BaseTestCase
def setup
@model = new_model
@machine = StateMachines::Machine.new(@model, :status)
end
def test_should_create_singular_with_scope
assert @model.respond_to?(:with_status)
end
def test_should_create_plural_with_scope
assert @model.respond_to?(:with_statuses)
end
end
state-machines-state_machines-activerecord-b9cb1e5/test/machine_with_conflicting_predicate_test.rb 0000664 0000000 0000000 00000000576 15213055011 0034152 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require_relative 'test_helper'
class MachineWithConflictingPredicateTest < BaseTestCase
def setup
@model = new_model do
def state?(*_args)
true
end
end
@machine = StateMachines::Machine.new(@model)
@record = @model.new
end
def test_should_not_define_attribute_predicate
assert @record.state?
end
end
state-machines-state_machines-activerecord-b9cb1e5/test/machine_with_conflicting_state_name_test.rb 0000664 0000000 0000000 00000001543 15213055011 0034325 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require_relative 'test_helper'
require 'stringio'
class MachineWithConflictingStateNameTest < BaseTestCase
def setup
@original_stderr = $stderr
$stderr = StringIO.new
@model = new_model
end
def test_should_output_warning_with_same_machine_name
@machine = StateMachines::Machine.new(@model)
@machine.state :state
assert_match(/^Instance method "state\?" is already defined in .+, use generic helper instead.*\n$/, $stderr.string)
end
def test_should_output_warning_with_same_machine_attribute
@machine = StateMachines::Machine.new(@model, :public_state, attribute: :state)
@machine.state :state
assert_match(/^Instance method "state\?" is already defined in .+, use generic helper instead.*\n$/, $stderr.string)
end
def teardown
$stderr = @original_stderr
super
end
end
state-machines-state_machines-activerecord-b9cb1e5/test/machine_with_custom_attribute_test.rb 0000664 0000000 0000000 00000001047 15213055011 0033222 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require_relative 'test_helper'
require 'stringio'
class MachineWithCustomAttributeTest < BaseTestCase
def setup
@original_stderr = $stderr
$stderr = StringIO.new
@model = new_model
@machine = StateMachines::Machine.new(@model, :public_state, attribute: :state)
@record = @model.new
end
def test_should_not_delegate_attribute_predicate_with_different_attribute
assert_raise(ArgumentError) { @record.public_state? }
end
def teardown
$stderr = @original_stderr
super
end
end
state-machines-state_machines-activerecord-b9cb1e5/test/machine_with_default_scope_test.rb 0000664 0000000 0000000 00000000720 15213055011 0032437 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require_relative 'test_helper'
class MachineWithDefaultScopeTest < BaseTestCase
def setup
@model = new_model
@machine = StateMachines::Machine.new(@model, initial: :parked)
@machine.state :idling
@model.class_eval do
default_scope { with_state(:parked, :idling) }
end
end
def test_should_set_initial_state_on_created_object
object = @model.new
assert_equal 'parked', object.state
end
end
machine_with_different_column_default_test.rb 0000664 0000000 0000000 00000001504 15213055011 0034573 0 ustar 00root root 0000000 0000000 state-machines-state_machines-activerecord-b9cb1e5/test # frozen_string_literal: true
require_relative 'test_helper'
require 'stringio'
class MachineWithDifferentColumnDefaultTest < BaseTestCase
def setup
@original_stderr = $stderr
$stderr = StringIO.new
@model = new_model do
connection.add_column table_name, :status, :string, default: 'idling'
end
@machine = StateMachines::Machine.new(@model, :status, initial: :parked)
@record = @model.new
end
def test_should_use_machine_default
assert_equal 'parked', @record.status
end
def test_should_generate_a_warning
assert_match(
/Both Foo and its :status machine have defined a different default for "status". Use only one or the other for defining defaults to avoid unexpected behaviors\./, $stderr.string
)
end
def teardown
$stderr = @original_stderr
super
end
end
machine_with_different_integer_column_default_test.rb 0000664 0000000 0000000 00000001543 15213055011 0036313 0 ustar 00root root 0000000 0000000 state-machines-state_machines-activerecord-b9cb1e5/test # frozen_string_literal: true
require_relative 'test_helper'
require 'stringio'
class MachineWithDifferentIntegerColumnDefaultTest < BaseTestCase
def setup
@original_stderr = $stderr
$stderr = StringIO.new
@model = new_model do
connection.add_column table_name, :status, :integer, default: 0
end
@machine = StateMachines::Machine.new(@model, :status, initial: :parked)
@machine.state :parked, value: 1
@record = @model.new
end
def test_should_use_machine_default
assert_equal 1, @record.status
end
def test_should_generate_a_warning
assert_match(
/Both Foo and its :status machine have defined a different default for "status". Use only one or the other for defining defaults to avoid unexpected behaviors\./, $stderr.string
)
end
def teardown
$stderr = @original_stderr
super
end
end
machine_with_dirty_attribute_and_custom_attributes_during_loopback_test.rb 0000664 0000000 0000000 00000001307 15213055011 0042667 0 ustar 00root root 0000000 0000000 state-machines-state_machines-activerecord-b9cb1e5/test # frozen_string_literal: true
require_relative 'test_helper'
class MachineWithDirtyAttributeAndCustomAttributesDuringLoopbackTest < BaseTestCase
def setup
@model = new_model do
connection.add_column table_name, :status, :string
end
@machine = StateMachines::Machine.new(@model, :status, initial: :parked)
@machine.event :park
@record = @model.create
@transition = StateMachines::Transition.new(@record, @machine, :park, :parked, :parked)
@transition.perform(false)
end
def test_should_not_include_state_in_changed_attributes
assert_equal [], @record.changed
end
def test_should_not_track_attribute_changes
assert_nil @record.changes['status']
end
end
machine_with_dirty_attribute_and_state_events_test.rb 0000664 0000000 0000000 00000001016 15213055011 0036366 0 ustar 00root root 0000000 0000000 state-machines-state_machines-activerecord-b9cb1e5/test # frozen_string_literal: true
require_relative 'test_helper'
class MachineWithDirtyAttributeAndStateEventsTest < BaseTestCase
def setup
@model = new_model
@machine = StateMachines::Machine.new(@model, initial: :parked)
@machine.event :ignite
@record = @model.create
@record.state_event = 'ignite'
end
def test_should_not_include_state_in_changed_attributes
assert_equal [], @record.changed
end
def test_should_not_track_attribute_change
assert_nil @record.changes['state']
end
end
machine_with_dirty_attributes_and_custom_attribute_test.rb 0000664 0000000 0000000 00000001751 15213055011 0037450 0 ustar 00root root 0000000 0000000 state-machines-state_machines-activerecord-b9cb1e5/test # frozen_string_literal: true
require_relative 'test_helper'
class MachineWithDirtyAttributesAndCustomAttributeTest < BaseTestCase
def setup
@model = new_model do
connection.add_column table_name, :status, :string
end
@machine = StateMachines::Machine.new(@model, :status, initial: :parked)
@machine.event :ignite
@machine.state :idling
@record = @model.create
@transition = StateMachines::Transition.new(@record, @machine, :ignite, :parked, :idling)
@transition.perform(false)
end
def test_should_include_state_in_changed_attributes
assert_equal %w[status], @record.changed
end
def test_should_track_attribute_change
assert_equal %w[parked idling], @record.changes['status']
end
def test_should_not_reset_changes_on_multiple_transitions
transition = StateMachines::Transition.new(@record, @machine, :ignite, :idling, :idling)
transition.perform(false)
assert_equal %w[parked idling], @record.changes['status']
end
end
machine_with_dirty_attributes_during_loopback_test.rb 0000664 0000000 0000000 00000001147 15213055011 0036372 0 ustar 00root root 0000000 0000000 state-machines-state_machines-activerecord-b9cb1e5/test # frozen_string_literal: true
require_relative 'test_helper'
class MachineWithDirtyAttributesDuringLoopbackTest < BaseTestCase
def setup
@model = new_model
@machine = StateMachines::Machine.new(@model, initial: :parked)
@machine.event :park
@record = @model.create
@transition = StateMachines::Transition.new(@record, @machine, :park, :parked, :parked)
@transition.perform(false)
end
def test_should_not_include_state_in_changed_attributes
assert_equal [], @record.changed
end
def test_should_not_track_attribute_changes
assert_nil @record.changes['state']
end
end
state-machines-state_machines-activerecord-b9cb1e5/test/machine_with_dirty_attributes_test.rb 0000664 0000000 0000000 00000002013 15213055011 0033220 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require_relative 'test_helper'
class MachineWithDirtyAttributesTest < BaseTestCase
def setup
@model = new_model
@machine = StateMachines::Machine.new(@model, initial: :parked)
@machine.event :ignite
@machine.state :idling
@record = @model.create
@transition = StateMachines::Transition.new(@record, @machine, :ignite, :parked, :idling)
@transition.perform(false)
end
def test_should_include_state_in_changed_attributes
assert_equal %w[state], @record.changed
end
def test_should_track_attribute_change
assert_equal %w[parked idling], @record.changes['state']
end
def test_should_not_reset_changes_on_multiple_transitions
transition = StateMachines::Transition.new(@record, @machine, :ignite, :idling, :idling)
transition.perform(false)
assert_equal %w[parked idling], @record.changes['state']
end
def test_should_not_have_changes_when_loaded_from_database
record = @model.find(@record.id)
refute record.changed?
end
end
state-machines-state_machines-activerecord-b9cb1e5/test/machine_with_dynamic_initial_state_test.rb 0000664 0000000 0000000 00000004560 15213055011 0034165 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require_relative 'test_helper'
class MachineWithDynamicInitialStateTest < BaseTestCase
def setup
@model = new_model do
attr_accessor :value
end
@machine = StateMachines::Machine.new(@model, initial: ->(_object) { :parked })
@machine.state :parked
end
def test_should_set_initial_state_on_created_object
record = @model.new
assert_equal 'parked', record.state
end
def test_should_still_set_attributes
record = @model.new(value: 1)
assert_equal 1, record.value
end
def test_should_still_allow_initialize_blocks
block_args = nil
record = @model.new do |*args|
block_args = args
end
assert_equal [record], block_args
end
def test_should_set_attributes_prior_to_initialize_block
state = nil
@model.new do |record|
state = record.state
end
assert_equal 'parked', state
end
def test_should_set_attributes_prior_to_after_initialize_hook
state = nil
@model.after_initialize do |record|
state = record.state
end
@model.new
assert_equal 'parked', state
end
def test_should_set_initial_state_after_setting_attributes
@model.class_eval do
attr_accessor :state_during_setter
remove_method :value=
define_method(:value=) do |_value|
self.state_during_setter = state || 'nil'
end
end
record = @model.new(value: 1)
assert_equal 'nil', record.state_during_setter
end
def test_should_not_set_initial_state_after_already_initialized
record = @model.new(value: 1)
assert_equal 'parked', record.state
record.state = 'idling'
record.attributes = {}
assert_equal 'idling', record.state
end
def test_should_persist_initial_state
record = @model.new
record.save
record.reload
assert_equal 'parked', record.state
end
def test_should_persist_initial_state_on_dup
record = @model.create.dup
record.save
record.reload
assert_equal 'parked', record.state
end
def test_should_use_stored_values_when_loading_from_database
@machine.state :idling
record = @model.find(@model.create(state: 'idling').id)
assert_equal 'idling', record.state
end
def test_should_use_stored_values_when_loading_from_database_with_nil_state
@machine.state nil
record = @model.find(@model.create(state: nil).id)
assert_nil record.state
end
end
state-machines-state_machines-activerecord-b9cb1e5/test/machine_with_enum_integration_test.rb 0000664 0000000 0000000 00000016673 15213055011 0033207 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require_relative 'test_helper'
class MachineWithEnumIntegrationTest < BaseTestCase
def setup
@original_stderr = $stderr
$stderr = StringIO.new
@model = new_model do
connection.add_column table_name, :status, :integer, default: 0
enum :status, { pending: 0, processing: 1, completed: 2, failed: 3 }
end
end
def teardown
StateMachines::Integrations::ActiveRecord.auto_convert_integer_state_attributes = true
$stderr = @original_stderr
super
end
test 'should auto detect enum integration' do
@machine = StateMachines::Machine.new(@model, :status)
@machine.state :pending, :processing, :completed, :failed
# Test enum integration detection
assert @machine.respond_to?(:enum_integrated?), 'Machine should respond to enum_integrated?'
assert @machine.enum_integrated?
assert_equal({ 'pending' => 0, 'processing' => 1, 'completed' => 2, 'failed' => 3 }, @machine.enum_mapping)
# Test that states are properly defined using shared assertions
assert_sm_states_list(@machine, %i[pending processing completed failed])
end
test 'should keep enum integration when integer conversion is disabled' do
StateMachines::Integrations::ActiveRecord.auto_convert_integer_state_attributes = false
machine = @model.state_machine(:status) do
state :pending, :processing, :completed, :failed
end
assert machine.enum_integrated?
refute machine.integer_type_registered?
assert_equal({ 'pending' => 0, 'processing' => 1, 'completed' => 2, 'failed' => 3 }, machine.enum_mapping)
end
test 'should not auto detect when no enum exists' do
@model_without_enum = new_model
machine = @model_without_enum.state_machine(:status) do
state :pending, :processing, :completed, :failed
end
assert_not machine.enum_integrated?
# Test that states are still properly defined even without enum integration
assert_sm_states_list(machine, %i[pending processing completed failed])
end
test 'should detect existing enum methods' do
machine = @model.state_machine(:status) do
state :pending, :processing, :completed, :failed
end
original_methods = machine.original_enum_methods
assert_includes original_methods, 'pending?'
assert_includes original_methods, 'processing?'
assert_includes original_methods, 'completed?'
assert_includes original_methods, 'failed?'
end
test 'should generate prefixed method names for enum integration' do
machine = @model.state_machine(:status) do
state :pending, :processing, :completed, :failed
end
assert_equal 'status_pending?', machine.send(:generate_state_method_name, 'pending', :predicate)
assert_equal 'status_processing!', machine.send(:generate_state_method_name, 'processing', :bang)
assert_equal 'status_completed', machine.send(:generate_state_method_name, 'completed', :scope)
end
test 'should store default enum integration metadata' do
machine = @model.state_machine(:status) do
state :pending, :processing, :completed, :failed
end
config = machine.enum_integration
assert_equal true, config[:prefix]
assert_equal false, config[:suffix]
assert_equal true, config[:scopes]
end
test 'should generate prefixed predicate methods' do
@model.state_machine(:status) do
state :pending, :processing, :completed, :failed
end
record = @model.create(status: :pending)
# Test using shared state machine assertions
assert_sm_state(record, :pending, machine_name: :status)
# Original enum methods should still work
assert record.pending?
assert_not record.processing?
# Prefixed state machine methods should be generated
assert record.respond_to?(:status_pending?)
assert record.respond_to?(:status_processing?)
assert record.respond_to?(:status_completed?)
assert record.respond_to?(:status_failed?)
# Prefixed methods should work correctly
assert record.status_pending?
assert_not record.status_processing?
assert_not record.status_completed?
assert_not record.status_failed?
end
test 'should generate prefixed bang methods' do
@model.state_machine(:status) do
state :pending, :processing, :completed, :failed
event :process do
transition pending: :processing
end
event :complete do
transition processing: :completed
end
event :fail do
transition %i[pending processing] => :failed
end
end
record = @model.create(status: :pending)
# Prefixed bang methods should be available
assert record.respond_to?(:status_processing!)
assert record.respond_to?(:status_completed!)
assert record.respond_to?(:status_failed!)
# Bang methods should exist but raise an exception (conflict resolution, not full functionality)
assert_raises(RuntimeError) do
record.status_failed!
end
end
test 'should generate prefixed scope methods' do
@model.state_machine(:status) do
state :pending, :processing, :completed, :failed
end
pending_record = @model.create(status: :pending)
processing_record = @model.create(status: :processing)
completed_record = @model.create(status: :completed)
# Test record states using shared assertions
assert_sm_state(pending_record, :pending, machine_name: :status)
assert_sm_state(processing_record, :processing, machine_name: :status)
assert_sm_state(completed_record, :completed, machine_name: :status)
# Test state persistence using shared assertions
assert_sm_state_persisted(pending_record, 'pending', :status)
assert_sm_state_persisted(processing_record, 'processing', :status)
assert_sm_state_persisted(completed_record, 'completed', :status)
# Prefixed scope methods should be available
assert @model.respond_to?(:status_pending)
assert @model.respond_to?(:status_processing)
assert @model.respond_to?(:status_completed)
assert @model.respond_to?(:status_failed)
# Negative scope methods should be available
assert @model.respond_to?(:not_status_pending)
assert @model.respond_to?(:not_status_processing)
# Scopes should work correctly
assert_equal [pending_record], @model.status_pending.to_a
assert_equal [processing_record], @model.status_processing.to_a
assert_equal [completed_record], @model.status_completed.to_a
assert_equal [], @model.status_failed.to_a
# Negative scopes should work
assert_equal [processing_record, completed_record], @model.not_status_pending.order(:id).to_a
end
test 'should track generated methods for introspection' do
machine = @model.state_machine(:status) do
state :pending, :processing, :completed, :failed
end
generated_methods = machine.state_machine_methods
# Should track all generated prefixed methods
assert_includes generated_methods, 'status_pending?'
assert_includes generated_methods, 'status_processing?'
assert_includes generated_methods, 'status_completed?'
assert_includes generated_methods, 'status_failed?'
assert_includes generated_methods, 'status_pending!'
assert_includes generated_methods, 'status_processing!'
assert_includes generated_methods, 'status_completed!'
assert_includes generated_methods, 'status_failed!'
assert_includes generated_methods, 'status_pending'
assert_includes generated_methods, 'status_processing'
assert_includes generated_methods, 'status_completed'
assert_includes generated_methods, 'status_failed'
end
end
machine_with_event_attributes_on_autosave_test.rb 0000664 0000000 0000000 00000002741 15213055011 0035542 0 ustar 00root root 0000000 0000000 state-machines-state_machines-activerecord-b9cb1e5/test # frozen_string_literal: true
require_relative 'test_helper'
class MachineWithEventAttributesOnAutosaveTest < BaseTestCase
def setup
@vehicle_model = new_model(:vehicle) do
connection.add_column table_name, :owner_id, :integer
end
MachineWithEventAttributesOnAutosaveTest.const_set('Vehicle', @vehicle_model)
@owner_model = new_model(:owner)
MachineWithEventAttributesOnAutosaveTest.const_set('Owner', @owner_model)
machine = StateMachines::Machine.new(@vehicle_model)
machine.event :ignite do
transition parked: :idling
end
@owner = @owner_model.create
@vehicle = @vehicle_model.create(state: 'parked', owner_id: @owner.id)
end
def test_should_persist_has_one_autosave
@owner_model.has_one :vehicle, class_name: 'MachineWithEventAttributesOnAutosaveTest::Vehicle', autosave: true
@owner.vehicle.state_event = 'ignite'
@owner.save
@vehicle.reload
assert_equal 'idling', @vehicle.state
end
def test_should_persist_has_many_autosave
@owner_model.has_many :vehicles, class_name: 'MachineWithEventAttributesOnAutosaveTest::Vehicle',
autosave: true
@owner.vehicles[0].state_event = 'ignite'
@owner.save
@vehicle.reload
assert_equal 'idling', @vehicle.state
end
def teardown
MachineWithEventAttributesOnAutosaveTest.class_eval do
remove_const('Vehicle')
remove_const('Owner')
end
clear_active_support_dependencies
super
end
end
machine_with_event_attributes_on_custom_action_test.rb 0000664 0000000 0000000 00000001662 15213055011 0036563 0 ustar 00root root 0000000 0000000 state-machines-state_machines-activerecord-b9cb1e5/test # frozen_string_literal: true
require_relative 'test_helper'
class MachineWithEventAttributesOnCustomActionTest < BaseTestCase
def setup
@superclass = new_model do
def persist
create_or_update
end
end
@model = Class.new(@superclass)
@machine = StateMachines::Machine.new(@model, action: :persist)
@machine.event :ignite do
transition parked: :idling
end
@record = @model.new
@record.state = 'parked'
@record.state_event = 'ignite'
end
def test_should_not_transition_on_valid?
@record.valid?
assert_equal 'parked', @record.state
end
def test_should_not_transition_on_save
@record.save
assert_equal 'parked', @record.state
end
def test_should_not_transition_on_save!
@record.save!
assert_equal 'parked', @record.state
end
def test_should_transition_on_custom_action
@record.persist
assert_equal 'idling', @record.state
end
end
machine_with_event_attributes_on_save_bang_test.rb 0000664 0000000 0000000 00000004044 15213055011 0035636 0 ustar 00root root 0000000 0000000 state-machines-state_machines-activerecord-b9cb1e5/test # frozen_string_literal: true
require_relative 'test_helper'
class MachineWithEventAttributesOnSaveBangTest < BaseTestCase
def setup
@model = new_model
@machine = StateMachines::Machine.new(@model)
@machine.event :ignite do
transition parked: :idling
end
@record = @model.new
@record.state = 'parked'
@record.state_event = 'ignite'
end
def test_should_fail_if_event_is_invalid
@record.state_event = 'invalid'
assert_raise(ActiveRecord::RecordInvalid) { @record.save! }
end
def test_should_fail_if_event_has_no_transition
@record.state = 'idling'
assert_raise(ActiveRecord::RecordInvalid) { @record.save! }
end
def test_should_be_successful_if_event_has_transition
assert_equal true, @record.save!
end
def test_should_run_before_callbacks
ran_callback = false
@machine.before_transition { ran_callback = true }
@record.save!
assert ran_callback
end
def test_should_run_before_callbacks_once
before_count = 0
@machine.before_transition { before_count += 1 }
@record.save!
assert_equal 1, before_count
end
def test_should_run_around_callbacks_before_yield
ran_callback = false
@machine.around_transition do |block|
ran_callback = true
block.call
end
@record.save!
assert ran_callback
end
def test_should_run_around_callbacks_before_yield_once
around_before_count = 0
@machine.around_transition do |block|
around_before_count += 1
block.call
end
@record.save!
assert_equal 1, around_before_count
end
def test_should_persist_new_state
@record.save!
assert_equal 'idling', @record.state
end
def test_should_run_after_callbacks
ran_callback = false
@machine.after_transition { ran_callback = true }
@record.save!
assert ran_callback
end
def test_should_run_around_callbacks_after_yield
ran_callback = false
@machine.around_transition do |block|
block.call
ran_callback = true
end
@record.save!
assert ran_callback
end
end
machine_with_event_attributes_on_save_test.rb 0000664 0000000 0000000 00000014604 15213055011 0034652 0 ustar 00root root 0000000 0000000 state-machines-state_machines-activerecord-b9cb1e5/test # frozen_string_literal: true
require_relative 'test_helper'
class MachineWithEventAttributesOnSaveTest < BaseTestCase
def setup
@model = new_model
@machine = StateMachines::Machine.new(@model)
@machine.event :ignite do
transition parked: :idling
end
@record = @model.new
@record.state = 'parked'
@record.state_event = 'ignite'
end
def test_should_fail_if_event_is_invalid
@record.state_event = 'invalid'
assert_equal false, @record.save
end
def test_should_fail_if_event_has_no_transition
@record.state = 'idling'
assert_equal false, @record.save
end
def test_should_run_before_callbacks
ran_callback = false
@machine.before_transition { ran_callback = true }
@record.save
assert ran_callback
end
def test_should_run_before_callbacks_once
before_count = 0
@machine.before_transition { before_count += 1 }
@record.save
assert_equal 1, before_count
end
def test_should_run_around_callbacks_before_yield
ran_callback = false
@machine.around_transition do |block|
ran_callback = true
block.call
end
@record.save
assert ran_callback
end
def test_should_run_around_callbacks_before_yield_once
around_before_count = 0
@machine.around_transition do |block|
around_before_count += 1
block.call
end
@record.save
assert_equal 1, around_before_count
end
def test_should_persist_new_state
@record.save
assert_equal 'idling', @record.state
end
def test_should_run_after_callbacks
ran_callback = false
@machine.after_transition { ran_callback = true }
@record.save
assert ran_callback
end
def test_should_not_run_after_callbacks_with_failures_disabled_if_fails
@model.before_create { |_record| abort_from_callback }
ran_callback = false
@machine.after_transition { ran_callback = true }
begin
@record.save
rescue StandardError
end
refute ran_callback
end
def test_should_run_failure_callbacks__if_fails
@model.before_create { |_record| abort_from_callback }
ran_callback = false
@machine.after_failure { ran_callback = true }
begin
@record.save
rescue StandardError
end
assert ran_callback
end
def test_should_not_run_around_callbacks_if_fails
@model.before_create { |_record| abort_from_callback }
ran_callback = false
@machine.around_transition do |block|
block.call
ran_callback = true
end
begin
@record.save
rescue StandardError
end
refute ran_callback
end
def test_should_run_around_callbacks_after_yield
ran_callback = false
@machine.around_transition do |block|
block.call
ran_callback = true
end
@record.save
assert ran_callback
end
def test_should_run_before_transitions_within_transaction
@machine.before_transition do
@model.create
raise ActiveRecord::Rollback
end
begin
@record.save
rescue Exception
end
assert_equal 0, @model.count
end
def test_should_run_after_transitions_within_transaction
@machine.after_transition do
@model.create
raise ActiveRecord::Rollback
end
begin
@record.save
rescue Exception
end
assert_equal 0, @model.count
end
def test_should_run_around_transition_within_transaction
@machine.around_transition do
@model.create
raise ActiveRecord::Rollback
end
begin
@record.save
rescue Exception
end
assert_equal 0, @model.count
end
def test_should_allow_additional_transitions_to_new_state_in_after_transitions
@machine.event :park do
transition idling: :parked
end
@machine.after_transition(on: :ignite) { @record.park }
@record.save
assert_equal 'parked', @record.state
@record.reload
assert_equal 'parked', @record.state
end
def test_should_allow_additional_transitions_to_previous_state_in_after_transitions
@machine.event :shift_up do
transition idling: :first_gear
end
@machine.after_transition(on: :ignite) { @record.shift_up }
@record.save
assert_equal 'first_gear', @record.state
@record.reload
assert_equal 'first_gear', @record.state
end
def test_should_yield_one_model!
assert_equal true, @record.save!
assert_equal 1, @model.count
end
# explicit tests of #save and #save! to ensure expected behavior
def test_should_yield_two_models_with_before
@machine.before_transition { @model.create! }
assert_equal true, @record.save
assert_equal 2, @model.count
end
def test_should_yield_two_models_with_before!
@machine.before_transition { @model.create! }
assert_equal true, @record.save!
assert_equal 2, @model.count
end
def test_should_raise_on_around_transition_rollback!
@machine.before_transition { @model.create! }
@machine.around_transition do
@model.create!
raise ActiveRecord::Rollback
end
raised = false
begin
@record.save!
rescue Exception
raised = true
end
assert_equal true, raised
assert_equal 0, @model.count
end
def test_should_return_nil_on_around_transition_rollback
@machine.before_transition { @model.create! }
@machine.around_transition do
@model.create!
raise ActiveRecord::Rollback
end
assert_nil @record.save
assert_equal 0, @model.count
end
def test_should_return_nil_on_before_transition_rollback
@machine.before_transition { raise ActiveRecord::Rollback }
assert_nil @record.save
assert_equal 0, @model.count
end
#
# @rosskevin - This fails and I'm not sure why, it was existing behavior.
# see: https://github.com/state-machines/state_machines-activerecord/pull/26#issuecomment-112911886
#
# def test_should_yield_three_models_with_before_and_around_save
# @machine.before_transition { @model.create!; puts "before ran, now #{@model.count}" }
# @machine.around_transition { @model.create!; puts "around ran, now #{@model.count}" }
#
# assert_equal true, @record.save
# assert_equal 3, @model.count
# end
#
# def test_should_yield_three_models_with_before_and_around_save!
# @machine.before_transition { @model.create!; puts "before ran, now #{@model.count}" }
# @machine.around_transition { @model.create!; puts "around ran, now #{@model.count}" }
#
# assert_equal true, @record.save!
# assert_equal 3, @model.count
# end
end
machine_with_event_attributes_on_validation_test.rb 0000664 0000000 0000000 00000006554 15213055011 0036053 0 ustar 00root root 0000000 0000000 state-machines-state_machines-activerecord-b9cb1e5/test # frozen_string_literal: true
require_relative 'test_helper'
class MachineWithEventAttributesOnValidationTest < BaseTestCase
def setup
@model = new_model
@machine = StateMachines::Machine.new(@model)
@machine.event :ignite do
transition parked: :idling
end
@record = @model.new
@record.state = 'parked'
@record.state_event = 'ignite'
end
def test_should_fail_if_event_is_invalid
@record.state_event = 'invalid'
refute @record.valid?
assert_equal ['State event is invalid'], @record.errors.full_messages
end
def test_should_fail_if_event_has_no_transition
@record.state = 'idling'
refute @record.valid?
assert_equal ['State event cannot transition when idling'], @record.errors.full_messages
end
def test_should_be_successful_if_event_has_transition
assert @record.valid?
end
def test_should_run_before_callbacks
ran_callback = false
@machine.before_transition { ran_callback = true }
@record.valid?
assert ran_callback
end
def test_should_run_around_callbacks_before_yield
ran_callback = false
@machine.around_transition do |block|
ran_callback = true
block.call
end
begin
@record.valid?
rescue ArgumentError
raise if StateMachines::Transition.pause_supported?
end
assert ran_callback
end
def test_should_persist_new_state
@record.valid?
assert_equal 'idling', @record.state
end
def test_should_not_run_after_callbacks
ran_callback = false
@machine.after_transition { ran_callback = true }
@record.valid?
refute ran_callback
end
def test_should_not_run_after_callbacks_with_failures_disabled_if_validation_fails
@model.class_eval do
attr_accessor :seatbelt
validates :seatbelt, presence: true
end
ran_callback = false
@machine.after_transition { ran_callback = true }
@record.valid?
refute ran_callback
end
def test_should_run_after_callbacks_if_validation_fails
@model.class_eval do
attr_accessor :seatbelt
validates :seatbelt, presence: true
end
ran_callback = false
@machine.after_failure { ran_callback = true }
@record.valid?
assert ran_callback
end
def test_should_not_run_around_callbacks_after_yield
ran_callback = false
@machine.around_transition do |block|
block.call
ran_callback = true
end
begin
@record.valid?
rescue ArgumentError
raise if StateMachines::Transition.pause_supported?
end
refute ran_callback
end
def test_should_not_run_around_callbacks_after_yield_with_failures_disabled_if_validation_fails
@model.class_eval do
attr_accessor :seatbelt
validates :seatbelt, presence: true
end
ran_callback = false
@machine.around_transition do |block|
block.call
ran_callback = true
end
@record.valid?
refute ran_callback
end
def test_should_rollback_before_transitions_with_raise
@machine.before_transition do
@model.create
raise ActiveRecord::Rollback
end
begin
@record.valid?
rescue Exception
end
assert_equal 0, @model.count
end
def test_should_rollback_before_transitions_with_false
@machine.before_transition do
@model.create
false
end
begin
@record.valid?
rescue Exception
end
assert_equal 0, @model.count
end
end
state-machines-state_machines-activerecord-b9cb1e5/test/machine_with_events_test.rb 0000664 0000000 0000000 00000000517 15213055011 0031132 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require_relative 'test_helper'
class MachineWithEventsTest < BaseTestCase
def setup
@model = new_model
@machine = StateMachines::Machine.new(@model)
@machine.event :shift_up
end
def test_should_humanize_name
assert_equal 'shift up', @machine.event(:shift_up).human_name
end
end
state-machines-state_machines-activerecord-b9cb1e5/test/machine_with_explicit_integer_values_test.rb0000664 0000000 0000000 00000005011 15213055011 0034535 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require_relative 'test_helper'
# Regression test for https://github.com/state-machines/state_machines-activerecord/issues/132
#
# Machines whose named states all declare explicit integer values
# (state :pending, value: 0) are defined for raw integer storage: reads return
# the integer, status_name returns the state name. The custom integer type
# must pass values through untouched for these machines instead of converting
# them to state name strings.
class MachineWithExplicitIntegerValuesTest < BaseTestCase
def setup
@model = new_model do
connection.add_column table_name, :status, :integer, default: 0
end
@machine = StateMachines::Machine.new(@model, :status, initial: :pending) do
state :pending, value: 0
state :approved, value: 1
state :declined, value: 2
event :approve do
transition pending: :approved
end
event :decline do
transition pending: :declined
end
end
@record = @model.new
@record.save!
end
def test_should_read_raw_integer_value
assert_equal 0, @record.status
end
def test_status_name_returns_correct_symbol
assert_equal :pending, @record.status_name
end
def test_should_accept_integer_on_write
@record.status = 2
assert_equal 2, @record.status
assert_equal :declined, @record.status_name
end
def test_should_accept_numeric_string_on_write
@record.status = '1'
assert_equal 1, @record.status
assert_equal :approved, @record.status_name
end
def test_should_persist_explicit_integer_on_save
@record.approve!
raw = @model.connection.select_value("SELECT status FROM #{@model.quoted_table_name} WHERE id = #{@record.id}")
assert_equal 1, raw.to_i
end
def test_should_reload_as_raw_integer
@record.decline!
reloaded = @model.find(@record.id)
assert_equal 2, reloaded.status
end
def test_transition_fires_correctly
@record.approve!
assert_equal 1, @record.status
end
def test_predicate_returns_correct_result
assert @record.pending?
refute @record.approved?
end
def test_scope_returns_correct_records
approved = @model.new
approved.save!
approved.approve!
assert_includes @model.with_status(:pending), @record
assert_includes @model.with_status(:approved), approved
refute_includes @model.with_status(:approved), @record
end
def test_scope_uses_raw_integer_relation_condition
assert_equal({ 'status' => 1 }, @model.with_status(:approved).where_values_hash)
end
end
state-machines-state_machines-activerecord-b9cb1e5/test/machine_with_failed_action_test.rb 0000664 0000000 0000000 00000002273 15213055011 0032410 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require_relative 'test_helper'
class MachineWithFailedActionTest < BaseTestCase
def setup
@model = new_model do
validates_inclusion_of :state, in: %w[first_gear]
end
@machine = StateMachines::Machine.new(@model)
@machine.state :parked, :idling
@machine.event :ignite
@callbacks = []
@machine.before_transition { @callbacks << :before }
@machine.after_transition { @callbacks << :after }
@machine.after_failure { @callbacks << :after_failure }
@machine.around_transition do |block|
@callbacks << :around_before
block.call
@callbacks << :around_after
end
@record = @model.new(state: 'parked')
@transition = StateMachines::Transition.new(@record, @machine, :ignite, :parked, :idling)
@result = @transition.perform
end
def test_should_not_be_successful
refute @result
end
def test_should_not_change_current_state
assert_equal 'parked', @record.state
end
def test_should_not_save_record
assert @record.new_record?
end
def test_should_run_before_callbacks_and_after_callbacks_with_failures
assert_equal %i[before around_before after_failure], @callbacks
end
end
state-machines-state_machines-activerecord-b9cb1e5/test/machine_with_failed_after_callbacks_test.rb 0000664 0000000 0000000 00000002073 15213055011 0034231 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require_relative 'test_helper'
class MachineWithFailedAfterCallbacksTest < BaseTestCase
def setup
@callbacks = []
@model = new_model
@machine = StateMachines::Machine.new(@model)
@machine.state :parked, :idling
@machine.event :ignite
@machine.after_transition do
@callbacks << :after_1
false
end
@machine.after_transition { @callbacks << :after_2 }
@machine.around_transition do |block|
@callbacks << :around_before
block.call
@callbacks << :around_after
end
@record = @model.new(state: 'parked')
@transition = StateMachines::Transition.new(@record, @machine, :ignite, :parked, :idling)
@result = @transition.perform
end
def test_should_be_successful
assert @result
end
def test_should_change_current_state
assert_equal 'idling', @record.state
end
def test_should_save_record
refute @record.new_record?
end
def test_should_not_run_further_after_callbacks
assert_equal %i[around_before around_after after_1], @callbacks
end
end
state-machines-state_machines-activerecord-b9cb1e5/test/machine_with_failed_before_callbacks_test.rb0000664 0000000 0000000 00000002141 15213055011 0034366 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require_relative 'test_helper'
class MachineWithFailedBeforeCallbacksTest < BaseTestCase
def setup
@callbacks = []
@model = new_model
@machine = StateMachines::Machine.new(@model)
@machine.state :parked, :idling
@machine.event :ignite
@machine.before_transition do
@callbacks << :before_1
false
end
@machine.before_transition { @callbacks << :before_2 }
@machine.after_transition { @callbacks << :after }
@machine.around_transition do |block|
@callbacks << :around_before
block.call
@callbacks << :around_after
end
@record = @model.new(state: 'parked')
@transition = StateMachines::Transition.new(@record, @machine, :ignite, :parked, :idling)
@result = @transition.perform
end
def test_should_not_be_successful
refute @result
end
def test_should_not_change_current_state
assert_equal 'parked', @record.state
end
def test_should_not_run_action
assert @record.new_record?
end
def test_should_not_run_further_callbacks
assert_equal [:before_1], @callbacks
end
end
machine_with_initialized_aliased_attribute_test.rb 0000664 0000000 0000000 00000000765 15213055011 0035626 0 ustar 00root root 0000000 0000000 state-machines-state_machines-activerecord-b9cb1e5/test require_relative 'test_helper'
class MachineWithInitializedAliasedAttributeTest < BaseTestCase
def setup
@model = new_model do
alias_attribute :custom_status, :state
end
@machine = StateMachines::Machine.new(@model, :initial => :parked, :attribute => :state)
@machine.state :started
@record = @model.new(:custom_status => :started)
end
def test_should_match_original_attribute_value
refute @record.state?(:parked)
assert @record.state?(:started)
end
end
state-machines-state_machines-activerecord-b9cb1e5/test/machine_with_initialized_state_test.rb 0000664 0000000 0000000 00000002130 15213055011 0033324 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require_relative 'test_helper'
class MachineWithInitializedStateTest < BaseTestCase
def setup
@model = new_model
@machine = StateMachines::Machine.new(@model, initial: :parked)
@machine.state :idling
end
def test_should_allow_nil_initial_state_when_static
@machine.state nil
record = @model.new(state: nil)
assert_nil record.state
end
def test_should_allow_nil_initial_state_when_dynamic
@machine.state nil
@machine.initial_state = -> { :parked }
record = @model.new(state: nil)
assert_nil record.state
end
def test_should_allow_different_initial_state_when_static
record = @model.new(state: 'idling')
assert_equal 'idling', record.state
end
def test_should_allow_different_initial_state_when_using_create_with
record = @model.create_with(state: 'idling').new
assert_equal 'idling', record.state
end
def test_should_allow_different_initial_state_when_dynamic
@machine.initial_state = -> { :parked }
record = @model.new(state: 'idling')
assert_equal 'idling', record.state
end
end
machine_with_integer_column_conversion_disabled_test.rb 0000664 0000000 0000000 00000006266 15213055011 0036664 0 ustar 00root root 0000000 0000000 state-machines-state_machines-activerecord-b9cb1e5/test # frozen_string_literal: true
require_relative 'test_helper'
class MachineWithIntegerColumnConversionDisabledTest < BaseTestCase
def setup
StateMachines::Integrations::ActiveRecord.auto_convert_integer_state_attributes = false
@model = new_model do
connection.add_column table_name, :status, :integer, default: 0
end
@machine = StateMachines::Machine.new(@model, :status, initial: :pending) do
state :pending, value: 0
state :approved, value: 1
state :declined, value: 2
event :approve do
transition pending: :approved
end
event :decline do
transition pending: :declined
end
end
@record = @model.new
@record.save!
end
def teardown
StateMachines::Integrations::ActiveRecord.auto_convert_integer_state_attributes = true
super
end
def test_should_not_register_custom_integer_type
refute @machine.integer_type_registered?
assert_equal :integer, @model.type_for_attribute('status').type
end
def test_should_read_raw_integer_value
assert_equal 0, @record.status
end
def test_machine_read_uses_raw_integer_value
assert_equal 0, @machine.read(@record, :state)
end
def test_status_name_returns_symbol
assert_equal :pending, @record.status_name
end
def test_should_not_convert_state_name_assignment
@record.status = :approved
assert_nil @record.status
assert_equal :approved, @record.status_before_type_cast
end
def test_transition_fires_correctly_with_raw_integer_values
@record.approve!
assert_equal 1, @record.status
end
def test_predicate_returns_correct_result
assert @record.pending?
refute @record.approved?
end
def test_scope_returns_correct_records
approved = @model.new
approved.save!
approved.approve!
assert_includes @model.with_status(:pending), @record
assert_includes @model.with_status(:approved), approved
refute_includes @model.with_status(:approved), @record
end
def test_scope_uses_raw_integer_relation_condition
assert_equal({ 'status' => 1 }, @model.with_status(:approved).where_values_hash)
end
def test_plural_scope_uses_raw_integer_relation_condition
assert_equal({ 'status' => [0, 2] }, @model.with_statuses(:pending, :declined).where_values_hash)
end
def test_without_scope_excludes_records_using_raw_integer_values
approved = @model.new
approved.save!
approved.approve!
assert_includes @model.without_status(:approved), @record
refute_includes @model.without_status(:approved), approved
end
def test_scope_on_custom_integer_attribute_uses_raw_integer_relation_condition
model = new_model do
connection.add_column table_name, :state_cd, :integer, default: 1
end
model.state_machine(:state, attribute: :state_cd, initial: :enabled) do
state :disabled, value: 0
state :enabled, value: 1
end
assert_equal({ 'state_cd' => 1 }, model.with_state(:enabled).where_values_hash)
end
def test_should_persist_raw_integer_on_save
@record.approve!
@record.save!
raw = @model.connection.select_value("SELECT status FROM #{@model.quoted_table_name} WHERE id = #{@record.id}")
assert_equal 1, raw.to_i
end
end
machine_with_integer_column_sti_subclass_test.rb 0000664 0000000 0000000 00000003757 15213055011 0035350 0 ustar 00root root 0000000 0000000 state-machines-state_machines-activerecord-b9cb1e5/test # frozen_string_literal: true
require_relative 'test_helper'
# Subclasses that extend an inherited machine get a cloned state collection,
# but they inherit the parent's custom integer attribute type, which references
# the parent machine's states. The integration must re-register the type on the
# subclass so subclass-added states serialize to their own integers instead of
# silently coercing to 0.
class MachineWithIntegerColumnStiSubclassTest < BaseTestCase
def setup
@base = new_model do
connection.add_column table_name, :status, :integer, default: 0
end
@base_machine = StateMachines::Machine.new(@base, :status, initial: :pending) do
state :pending
state :shipped
event :ship do
transition pending: :shipped
end
end
@subclass = Class.new(@base)
@subclass_machine = @subclass.state_machine(:status) do
state :returned
event :return_order do
transition shipped: :returned
end
end
@record = @subclass.new
@record.ship!
end
def test_subclass_machine_is_a_copy
refute_same @base_machine, @subclass_machine
end
def test_subclass_added_state_persists_its_own_integer
@record.return_order!
raw = @subclass.connection.select_value("SELECT status FROM #{@subclass.quoted_table_name} WHERE id = #{@record.id}")
assert_equal 2, raw.to_i
end
def test_subclass_added_state_reads_back
@record.return_order!
reloaded = @subclass.find(@record.id)
assert_equal 'returned', reloaded.status
assert_equal :returned, reloaded.status_name
end
def test_inherited_states_still_convert_on_subclass
assert_equal 'shipped', @record.status
assert @record.shipped?
end
def test_base_class_type_unaffected
base_record = @base.new
base_record.ship!
raw = @base.connection.select_value("SELECT status FROM #{@base.quoted_table_name} WHERE id = #{base_record.id}")
assert_equal 1, raw.to_i
assert_equal 'shipped', @base.find(base_record.id).status
end
end
state-machines-state_machines-activerecord-b9cb1e5/test/machine_with_integer_column_test.rb 0000664 0000000 0000000 00000003502 15213055011 0032635 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require_relative 'test_helper'
class MachineWithIntegerColumnTest < BaseTestCase
def setup
@model = new_model do
connection.add_column table_name, :status, :integer
end
@machine = StateMachines::Machine.new(@model, :status, initial: :pending)
@machine.state :pending
@machine.state :approved
@machine.state :declined
@machine.event :approve do
transition pending: :approved
end
@machine.event :decline do
transition pending: :declined
end
@record = @model.new
@record.save!
end
def test_should_return_state_name_on_read
assert_equal 'pending', @record.status
end
def test_should_accept_symbol_on_write
@record.status = :declined
assert_equal 'declined', @record.status
end
def test_should_accept_string_on_write
@record.status = 'approved'
assert_equal 'approved', @record.status
end
def test_should_persist_state_as_integer
@record.status = :approved
@record.save!
raw = @model.connection.select_value("SELECT status FROM #{@model.quoted_table_name} WHERE id = #{@record.id}")
assert_equal 1, raw.to_i
end
def test_should_reload_as_state_name
@record.status = :declined
@record.save!
reloaded = @model.find(@record.id)
assert_equal 'declined', reloaded.status
end
def test_status_name_returns_symbol
assert_equal :pending, @record.status_name
end
def test_transition_fires_correctly
@record.approve!
assert_equal 'approved', @record.status
end
def test_scope_with_status_returns_correct_records
approved = @model.new
approved.save!
approved.approve!
assert_includes @model.with_status(:pending), @record
refute_includes @model.with_status(:pending), approved
assert_includes @model.with_status(:approved), approved
end
end
state-machines-state_machines-activerecord-b9cb1e5/test/machine_with_internationalization_test.rb 0000664 0000000 0000000 00000014450 15213055011 0034074 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require_relative 'test_helper'
class MachineWithInternationalizationTest < BaseTestCase
def setup
I18n.backend = I18n::Backend::Simple.new
# Initialize the backend
StateMachines::Machine.new(new_model)
@model = new_model
end
def test_should_use_defaults
I18n.backend.store_translations(:en, {
activerecord: { errors: { messages: { invalid_transition: "cannot #{interpolation_key('event')}" } } }
})
machine = StateMachines::Machine.new(@model)
machine.state :parked, :idling
machine.event :ignite
record = @model.new(state: 'idling')
machine.invalidate(record, :state, :invalid_transition, [[:event, 'ignite']])
assert_equal ['State cannot transition via "ignite"'], record.errors.full_messages
end
def test_should_allow_customized_error_key
I18n.backend.store_translations(:en, {
activerecord: { errors: { messages: { bad_transition: "cannot #{interpolation_key('event')}" } } }
})
machine = StateMachines::Machine.new(@model, messages: { invalid_transition: :bad_transition })
machine.state :parked, :idling
record = @model.new(state: 'idling')
machine.invalidate(record, :state, :invalid_transition, [[:event, 'ignite']])
assert_equal ['State cannot ignite'], record.errors.full_messages
end
def test_should_allow_customized_error_string
machine = StateMachines::Machine.new(@model,
messages: { invalid_transition: "cannot #{interpolation_key('event')}" })
machine.state :parked, :idling
record = @model.new(state: 'idling')
machine.invalidate(record, :state, :invalid_transition, [[:event, 'ignite']])
assert_equal ['State cannot ignite'], record.errors.full_messages
end
def test_should_allow_customized_state_key_scoped_to_class_and_machine
I18n.backend.store_translations(:en, {
activerecord: { state_machines: { foo: { state: { states: { parked: 'shutdown' } } } } }
})
machine = StateMachines::Machine.new(@model)
machine.state :parked
assert_equal 'shutdown', machine.state(:parked).human_name
end
def test_should_allow_customized_state_key_scoped_to_class
I18n.backend.store_translations(:en, {
activerecord: { state_machines: { foo: { states: { parked: 'shutdown' } } } }
})
machine = StateMachines::Machine.new(@model)
machine.state :parked
assert_equal 'shutdown', machine.state(:parked).human_name
end
def test_should_allow_customized_state_key_scoped_to_machine
I18n.backend.store_translations(:en, {
activerecord: { state_machines: { state: { states: { parked: 'shutdown' } } } }
})
machine = StateMachines::Machine.new(@model)
machine.state :parked
assert_equal 'shutdown', machine.state(:parked).human_name
end
def test_should_allow_customized_state_key_unscoped
I18n.backend.store_translations(:en, {
activerecord: { state_machines: { states: { parked: 'shutdown' } } }
})
machine = StateMachines::Machine.new(@model)
machine.state :parked
assert_equal 'shutdown', machine.state(:parked).human_name
end
def test_should_support_nil_state_key
I18n.backend.store_translations(:en, {
activerecord: { state_machines: { states: { nil: 'empty' } } }
})
machine = StateMachines::Machine.new(@model)
assert_equal 'empty', machine.state(nil).human_name
end
def test_should_allow_customized_event_key_scoped_to_class_and_machine
I18n.backend.store_translations(:en, {
activerecord: { state_machines: { foo: { state: { events: { park: 'stop' } } } } }
})
machine = StateMachines::Machine.new(@model)
machine.event :park
assert_equal 'stop', machine.event(:park).human_name
end
def test_should_allow_customized_event_key_scoped_to_class
I18n.backend.store_translations(:en, {
activerecord: { state_machines: { foo: { events: { park: 'stop' } } } }
})
machine = StateMachines::Machine.new(@model)
machine.event :park
assert_equal 'stop', machine.event(:park).human_name
end
def test_should_allow_customized_event_key_scoped_to_machine
I18n.backend.store_translations(:en, {
activerecord: { state_machines: { state: { events: { park: 'stop' } } } }
})
machine = StateMachines::Machine.new(@model)
machine.event :park
assert_equal 'stop', machine.event(:park).human_name
end
def test_should_allow_customized_event_key_unscoped
I18n.backend.store_translations(:en, {
activerecord: { state_machines: { events: { park: 'stop' } } }
})
machine = StateMachines::Machine.new(@model)
machine.event :park
assert_equal 'stop', machine.event(:park).human_name
end
def test_should_only_add_locale_once_in_load_path
assert_equal 1, I18n.load_path.select { |path| path =~ %r{active_record/locale\.rb$} }.length
# Create another ActiveRecord model that will trigger the i18n feature
new_model
assert_equal 1, I18n.load_path.select { |path| path =~ %r{active_record/locale\.rb$} }.length
end
def test_should_prefer_other_locales_first
@original_load_path = I18n.load_path
I18n.backend = I18n::Backend::Simple.new
I18n.load_path = [File.dirname(__FILE__) + '/files/en.yml']
machine = StateMachines::Machine.new(@model)
machine.state :parked, :idling
machine.event :ignite
record = @model.new(state: 'idling')
machine.invalidate(record, :state, :invalid_transition, [[:event, 'ignite']])
assert_equal ['State cannot transition'], record.errors.full_messages
ensure
I18n.load_path = @original_load_path
end
private
def interpolation_key(key)
"%{#{key}}"
end
end
state-machines-state_machines-activerecord-b9cb1e5/test/machine_with_loopback_test.rb 0000664 0000000 0000000 00000001154 15213055011 0031416 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require_relative 'test_helper'
class MachineWithLoopbackTest < BaseTestCase
def setup
@model = new_model do
connection.add_column table_name, :updated_at, :datetime
end
@machine = StateMachines::Machine.new(@model, initial: :parked)
@machine.event :park
@record = @model.create(updated_at: Time.now - 1)
@transition = StateMachines::Transition.new(@record, @machine, :park, :parked, :parked)
@timestamp = @record.updated_at
@transition.perform
end
def test_should_not_update_record
assert_equal @timestamp, @record.updated_at
end
end
state-machines-state_machines-activerecord-b9cb1e5/test/machine_with_mixed_integer_values_test.rb 0000664 0000000 0000000 00000003163 15213055011 0034030 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require_relative 'test_helper'
# Machines mixing explicit integer values with auto-indexed states keep the
# transparent name conversion, while machine internals match each state on its
# canonical state.value (integer for explicit states, name string for
# auto-indexed ones).
class MachineWithMixedIntegerValuesTest < BaseTestCase
def setup
@model = new_model do
connection.add_column table_name, :status, :integer, default: 0
end
@machine = StateMachines::Machine.new(@model, :status, initial: :pending) do
state :pending # auto-indexed => 0
state :approved, value: 5 # explicit
event :approve do
transition pending: :approved
end
end
@record = @model.new
@record.save!
end
def test_should_read_state_name
assert_equal 'pending', @record.status
end
def test_status_name_returns_correct_symbol
assert_equal :pending, @record.status_name
end
def test_predicate_returns_correct_result
assert @record.pending?
refute @record.approved?
end
def test_transition_fires_correctly
@record.approve!
assert_equal 'approved', @record.status
assert @record.approved?
end
def test_should_persist_explicit_integer_on_save
@record.approve!
raw = @model.connection.select_value("SELECT status FROM #{@model.quoted_table_name} WHERE id = #{@record.id}")
assert_equal 5, raw.to_i
end
def test_should_reload_as_state_name
@record.approve!
reloaded = @model.find(@record.id)
assert_equal 'approved', reloaded.status
assert_equal :approved, reloaded.status_name
end
end
machine_with_non_column_state_attribute_defined_test.rb 0000664 0000000 0000000 00000001474 15213055011 0036662 0 ustar 00root root 0000000 0000000 state-machines-state_machines-activerecord-b9cb1e5/test # frozen_string_literal: true
require_relative 'test_helper'
class MachineWithNonColumnStateAttributeDefinedTest < BaseTestCase
def setup
@model = new_model do
attr_accessor :status
end
@machine = StateMachines::Machine.new(@model, :status, initial: :parked)
@machine.other_states(:idling)
@record = @model.new
end
def test_should_return_false_for_predicate_if_does_not_match_current_value
refute @record.status?(:idling)
end
def test_should_return_true_for_predicate_if_matches_current_value
assert @record.status?(:parked)
end
def test_should_raise_exception_for_predicate_if_invalid_state_specified
assert_raise(IndexError) { @record.status?(:invalid) }
end
def test_should_set_initial_state_on_created_object
assert_equal 'parked', @record.status
end
end
state-machines-state_machines-activerecord-b9cb1e5/test/machine_with_same_column_default_test.rb 0000664 0000000 0000000 00000001250 15213055011 0033627 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require_relative 'test_helper'
class MachineWithSameColumnDefaultTest < BaseTestCase
def setup
@original_stderr = $stderr
$stderr = StringIO.new
@model = new_model do
connection.add_column table_name, :status, :string, default: 'parked'
end
@machine = StateMachines::Machine.new(@model, :status, initial: :parked)
@record = @model.new
end
def test_should_use_machine_default
assert_equal 'parked', @record.status
end
def test_should_not_generate_a_warning
assert_no_match(/have defined a different default/, $stderr.string)
end
def teardown
$stderr = @original_stderr
super
end
end
machine_with_same_integer_column_default_test.rb 0000664 0000000 0000000 00000001336 15213055011 0035272 0 ustar 00root root 0000000 0000000 state-machines-state_machines-activerecord-b9cb1e5/test # frozen_string_literal: true
require_relative 'test_helper'
require 'stringio'
class MachineWithSameIntegerColumnDefaultTest < BaseTestCase
def setup
@original_stderr = $stderr
$stderr = StringIO.new
@model = new_model do
connection.add_column table_name, :status, :integer, default: 1
end
@machine = StateMachines::Machine.new(@model, :status, initial: :parked) do
state :parked, value: 1
end
@record = @model.new
end
def test_should_use_machine_default
assert_equal 1, @record.status
end
def test_should_not_generate_a_warning
assert_no_match(/have defined a different default/, $stderr.string)
end
def teardown
$stderr = @original_stderr
super
end
end
state-machines-state_machines-activerecord-b9cb1e5/test/machine_with_scopes_and_joins_test.rb 0000664 0000000 0000000 00000002463 15213055011 0033150 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require_relative 'test_helper'
class MachineWithScopesAndJoinsTest < BaseTestCase
def setup
@company = new_model(:company)
MachineWithScopesAndJoinsTest.const_set('Company', @company)
@vehicle = new_model(:vehicle) do
connection.add_column table_name, :company_id, :integer
belongs_to :company, class_name: 'MachineWithScopesAndJoinsTest::Company'
end
MachineWithScopesAndJoinsTest.const_set('Vehicle', @vehicle)
@company_machine = StateMachines::Machine.new(@company, initial: :active)
@vehicle_machine = StateMachines::Machine.new(@vehicle, initial: :parked)
@vehicle_machine.state :idling
@ford = @company.create
@mustang = @vehicle.create(company: @ford)
end
def test_should_find_records_in_with_scope
assert_equal [@mustang],
@vehicle.with_states(:parked).joins(:company).where("#{@company.table_name}.state = \"active\"")
end
def test_should_find_records_in_without_scope
assert_equal [@mustang],
@vehicle.without_states(:idling).joins(:company).where("#{@company.table_name}.state = \"active\"")
end
def teardown
MachineWithScopesAndJoinsTest.class_eval do
remove_const('Vehicle')
remove_const('Company')
end
clear_active_support_dependencies
end
end
machine_with_scopes_and_owner_subclass_test.rb 0000664 0000000 0000000 00000001640 15213055011 0034774 0 ustar 00root root 0000000 0000000 state-machines-state_machines-activerecord-b9cb1e5/test # frozen_string_literal: true
require_relative 'test_helper'
class MachineWithScopesAndOwnerSubclassTest < BaseTestCase
def setup
@model = new_model
@machine = StateMachines::Machine.new(@model, :state)
@subclass = Class.new(@model)
@subclass_machine = @subclass.state_machine(:state) {}
@subclass_machine.state :parked, :idling, :first_gear
end
def test_should_only_include_records_with_subclass_states_in_with_scope
parked = @subclass.create state: 'parked'
idling = @subclass.create state: 'idling'
assert_equal [parked, idling], @subclass.with_states(:parked, :idling).all
end
def test_should_only_include_records_without_subclass_states_in_without_scope
parked = @subclass.create state: 'parked'
idling = @subclass.create state: 'idling'
@subclass.create state: 'first_gear'
assert_equal [parked, idling], @subclass.without_states(:first_gear).all
end
end
state-machines-state_machines-activerecord-b9cb1e5/test/machine_with_scopes_test.rb 0000664 0000000 0000000 00000006544 15213055011 0031130 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require_relative 'test_helper'
class MachineWithScopesTest < BaseTestCase
def setup
@model = new_model do
connection.add_column table_name, :name, :string
end
@machine = StateMachines::Machine.new(@model)
@machine.state :parked, :first_gear
@machine.state :idling, value: -> { 'idling' }
end
def test_should_allow_chaining_scopes_with_queries
named = @model.create state: 'parked', name: 'a_name'
@model.create state: 'parked'
assert_equal [named], @model.where(name: 'a_name').with_state(:parked)
end
def test_should_create_singular_with_scope
assert @model.respond_to?(:with_state)
end
def test_should_only_include_records_with_state_in_singular_with_scope
parked = @model.create state: 'parked'
@model.create state: 'idling'
assert_equal [parked], @model.with_state(:parked).all
end
def test_should_allow_transparent_with_state_in_singular_with_scope
@model.create state: 'parked'
@model.create state: 'idling'
assert_equal @model.all, @model.with_state(nil).all
end
def test_should_create_plural_with_scope
assert @model.respond_to?(:with_states)
end
def test_should_only_include_records_with_states_in_plural_with_scope
parked = @model.create state: 'parked'
idling = @model.create state: 'idling'
assert_equal [parked, idling], @model.with_states(:parked, :idling).all
end
def test_should_allow_transparent_with_states_in_plural_with_scope
@model.create state: 'parked'
@model.create state: 'idling'
assert_equal @model.all, @model.with_states(nil).all
end
def test_should_allow_lookup_by_string_name
parked = @model.create state: 'parked'
idling = @model.create state: 'idling'
assert_equal [parked, idling], @model.with_states('parked', 'idling').all
end
def test_should_create_singular_without_scope
assert @model.respond_to?(:without_state)
end
def test_should_only_include_records_without_state_in_singular_without_scope
parked = @model.create state: 'parked'
@model.create state: 'idling'
assert_equal [parked], @model.without_state(:idling).all
end
def test_allow_transparent_without_state_in_singular_without_scope
@model.create state: 'parked'
@model.create state: 'idling'
assert_equal @model.all, @model.without_state(nil).all
end
def test_should_create_plural_without_scope
assert @model.respond_to?(:without_states)
end
def test_should_only_include_records_without_states_in_plural_without_scope
parked = @model.create state: 'parked'
idling = @model.create state: 'idling'
@model.create state: 'first_gear'
assert_equal [parked, idling], @model.without_states(:first_gear).all
end
def test_allow_transparent_without_states_in_plural_without_scope
@model.create state: 'parked'
@model.create state: 'idling'
@model.create state: 'first_gear'
assert_equal @model.all, @model.without_states(nil).all
end
def test_should_allow_chaining_scopes
@model.create state: 'parked'
idling = @model.create state: 'idling'
assert_equal [idling], @model.without_state(:parked).with_state(:idling).all
end
def test_should_allow_chaining_transparent_scopes
@model.create state: 'parked'
idling = @model.create state: 'idling'
assert_equal [idling], @model.with_state(nil).with_state(:idling).all
end
end
machine_with_state_driven_validations_test.rb 0000664 0000000 0000000 00000001545 15213055011 0034635 0 ustar 00root root 0000000 0000000 state-machines-state_machines-activerecord-b9cb1e5/test # frozen_string_literal: true
require_relative 'test_helper'
class MachineWithStateDrivenValidationsTest < BaseTestCase
def setup
@model = new_model do
attr_accessor :seatbelt
end
@machine = StateMachines::Machine.new(@model)
@machine.state :first_gear, :second_gear do
validates :seatbelt, presence: true
end
@machine.other_states :parked
end
def test_should_be_valid_if_validation_fails_outside_state_scope
record = @model.new(state: 'parked', seatbelt: nil)
assert record.valid?
end
def test_should_be_invalid_if_validation_fails_within_state_scope
record = @model.new(state: 'first_gear', seatbelt: nil)
refute record.valid?
end
def test_should_be_valid_if_validation_succeeds_within_state_scope
record = @model.new(state: 'second_gear', seatbelt: true)
assert record.valid?
end
end
state-machines-state_machines-activerecord-b9cb1e5/test/machine_with_states_test.rb 0000664 0000000 0000000 00000000525 15213055011 0031130 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require_relative 'test_helper'
class MachineWithStatesTest < BaseTestCase
def setup
@model = new_model
@machine = StateMachines::Machine.new(@model)
@machine.state :first_gear
end
def test_should_humanize_name
assert_equal 'first gear', @machine.state(:first_gear).human_name
end
end
state-machines-state_machines-activerecord-b9cb1e5/test/machine_with_static_initial_state_test.rb 0000664 0000000 0000000 00000011172 15213055011 0034025 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require_relative 'test_helper'
class MachineWithStaticInitialStateTest < BaseTestCase
def setup
@model = new_model(:vehicle) do
attr_accessor :value
end
@machine = StateMachines::Machine.new(@model, initial: :parked)
end
def test_should_set_initial_state_on_created_object
record = @model.new
assert_equal 'parked', record.state
end
def test_should_set_initial_state_with_nil_attributes
record = @model.new(nil)
assert_equal 'parked', record.state
end
def test_should_still_set_attributes
record = @model.new(value: 1)
assert_equal 1, record.value
end
def test_should_still_allow_initialize_blocks
block_args = nil
record = @model.new do |*args|
block_args = args
end
assert_equal [record], block_args
end
def test_should_set_attributes_prior_to_initialize_block
state = nil
@model.new do |record|
state = record.state
end
assert_equal 'parked', state
end
def test_should_set_attributes_prior_to_after_initialize_hook
state = nil
@model.after_initialize do |record|
state = record.state
end
@model.new
assert_equal 'parked', state
end
def test_should_set_initial_state_before_setting_attributes
@model.class_eval do
attr_accessor :state_during_setter
remove_method :value=
define_method(:value=) do |_value|
self.state_during_setter = state
end
end
record = @model.new
record.value = 1
assert_equal 'parked', record.state_during_setter
end
def test_should_not_set_initial_state_after_already_initialized
record = @model.new(value: 1)
assert_equal 'parked', record.state
record.state = 'idling'
record.attributes = {}
assert_equal 'idling', record.state
end
def test_should_persist_initial_state
record = @model.new
record.save
record.reload
assert_equal 'parked', record.state
end
def test_should_persist_initial_state_on_dup
record = @model.create.dup
record.save
record.reload
assert_equal 'parked', record.state
end
def test_should_use_stored_values_when_loading_from_database
@machine.state :idling
record = @model.find(@model.create(state: 'idling').id)
assert_equal 'idling', record.state
end
def test_should_use_stored_values_when_loading_from_database_with_nil_state
@machine.state nil
record = @model.find(@model.create(state: nil).id)
assert_nil record.state
end
def test_should_use_stored_values_when_loading_for_many_association
@machine.state :idling
@model.connection.add_column @model.table_name, :owner_id, :integer
@model.reset_column_information
MachineWithStaticInitialStateTest.const_set('Vehicle', @model)
owner_model = new_model(:owner) do
has_many :vehicles, class_name: 'MachineWithStaticInitialStateTest::Vehicle'
end
MachineWithStaticInitialStateTest.const_set('Owner', owner_model)
owner = owner_model.create
@model.create(state: 'idling', owner_id: owner.id)
assert_equal 'idling', owner.vehicles[0].state
end
def test_should_use_stored_values_when_loading_for_one_association
@machine.state :idling
@model.connection.add_column @model.table_name, :owner_id, :integer
@model.reset_column_information
MachineWithStaticInitialStateTest.const_set('Vehicle', @model)
owner_model = new_model(:owner) do
has_one :vehicle, class_name: 'MachineWithStaticInitialStateTest::Vehicle'
end
MachineWithStaticInitialStateTest.const_set('Owner', owner_model)
owner = owner_model.create
@model.create(state: 'idling', owner_id: owner.id)
assert_equal 'idling', owner.vehicle.state
end
def test_should_use_stored_values_when_loading_for_belongs_to_association
@machine.state :idling
MachineWithStaticInitialStateTest.const_set('Vehicle', @model)
driver_model = new_model(:driver) do
connection.add_column table_name, :vehicle_id, :integer
belongs_to :vehicle, class_name: 'MachineWithStaticInitialStateTest::Vehicle'
end
MachineWithStaticInitialStateTest.const_set('Driver', driver_model)
record = @model.create(state: 'idling')
driver = driver_model.create(vehicle_id: record.id)
assert_equal 'idling', driver.vehicle.state
end
def teardown
MachineWithStaticInitialStateTest.class_eval do
remove_const('Vehicle') if defined?(MachineWithStaticInitialStateTest::Vehicle)
remove_const('Owner') if defined?(MachineWithStaticInitialStateTest::Owner)
remove_const('Driver') if defined?(MachineWithStaticInitialStateTest::Driver)
end
clear_active_support_dependencies
super
end
end
state-machines-state_machines-activerecord-b9cb1e5/test/machine_with_transactions_test.rb 0000664 0000000 0000000 00000001111 15213055011 0032325 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require_relative 'test_helper'
class MachineWithTransactionsTest < BaseTestCase
def setup
@model = new_model
@machine = StateMachines::Machine.new(@model, use_transactions: true)
end
def test_should_rollback_transaction_if_false
@machine.within_transaction(@model.new) do
@model.create
false
end
assert_equal 0, @model.count
end
def test_should_not_rollback_transaction_if_true
@machine.within_transaction(@model.new) do
@model.create
true
end
assert_equal 1, @model.count
end
end
machine_with_validations_and_custom_attribute_test.rb 0000664 0000000 0000000 00000001051 15213055011 0036355 0 ustar 00root root 0000000 0000000 state-machines-state_machines-activerecord-b9cb1e5/test # frozen_string_literal: true
require_relative 'test_helper'
class MachineWithValidationsAndCustomAttributeTest < BaseTestCase
def setup
@model = new_model
@machine = StateMachines::Machine.new(@model, :status, attribute: :state)
@machine.state :parked
@record = @model.new
end
def test_should_add_validation_errors_to_custom_attribute
@record.state = 'invalid'
refute @record.valid?
assert_equal ['State is invalid'], @record.errors.full_messages
@record.state = 'parked'
assert @record.valid?
end
end
state-machines-state_machines-activerecord-b9cb1e5/test/machine_with_validations_test.rb 0000664 0000000 0000000 00000002327 15213055011 0032144 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require_relative 'test_helper'
class MachineWithValidationsTest < BaseTestCase
def setup
@model = new_model
@machine = StateMachines::Machine.new(@model)
@machine.state :parked
@record = @model.new
end
def test_should_invalidate_using_errors
I18n.backend = I18n::Backend::Simple.new
@record.state = 'parked'
@machine.invalidate(@record, :state, :invalid_transition, [[:event, 'park']])
assert_equal ['State cannot transition via "park"'], @record.errors.full_messages
end
def test_should_auto_prefix_custom_attributes_on_invalidation
@machine.invalidate(@record, :event, :invalid)
assert_equal ['State event is invalid'], @record.errors.full_messages
end
def test_should_clear_errors_on_reset
@record.state = 'parked'
@record.errors.add(:state, 'is invalid')
@machine.reset(@record)
assert_equal [], @record.errors.full_messages
end
def test_should_be_valid_if_state_is_known
@record.state = 'parked'
assert @record.valid?
end
def test_should_not_be_valid_if_state_is_unknown
@record.state = 'invalid'
refute @record.valid?
assert_equal ['State is invalid'], @record.errors.full_messages
end
end
state-machines-state_machines-activerecord-b9cb1e5/test/machine_without_database_test.rb 0000664 0000000 0000000 00000000746 15213055011 0032126 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require_relative 'test_helper'
class MachineWithoutDatabaseTest < BaseTestCase
def setup
@model = new_model(false) do
# Simulate the database not being available entirely
def self.connection
raise ActiveRecord::ConnectionNotEstablished
end
def self.connected?
false
end
end
end
def test_should_allow_machine_creation
assert_nothing_raised { StateMachines::Machine.new(@model) }
end
end
state-machines-state_machines-activerecord-b9cb1e5/test/machine_without_transactions_test.rb 0000664 0000000 0000000 00000001121 15213055011 0033056 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require_relative 'test_helper'
class MachineWithoutTransactionsTest < BaseTestCase
def setup
@model = new_model
@machine = StateMachines::Machine.new(@model, use_transactions: false)
end
def test_should_not_rollback_transaction_if_false
@machine.within_transaction(@model.new) do
@model.create
false
end
assert_equal 1, @model.count
end
def test_should_not_rollback_transaction_if_true
@machine.within_transaction(@model.new) do
@model.create
true
end
assert_equal 1, @model.count
end
end
state-machines-state_machines-activerecord-b9cb1e5/test/model_test.rb 0000664 0000000 0000000 00000000620 15213055011 0026202 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require_relative 'test_helper'
require_relative 'files/models/post'
class ModelTest < ActiveSupport::TestCase
def test_should_have_draft_state_in_defaut_machine
assert_equal 'draft', Post.new.state
end
def test_should_have_the_correct_integration
assert_equal StateMachines::Integrations::ActiveRecord, StateMachines::Integrations.match(Post)
end
end
state-machines-state_machines-activerecord-b9cb1e5/test/test_helper.rb 0000664 0000000 0000000 00000003447 15213055011 0026373 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require 'debug' if RUBY_ENGINE == 'ruby'
require 'minitest/reporters'
Minitest::Reporters.use!(Minitest::Reporters::SpecReporter.new)
# Ensure our local lib directory is loaded first
$LOAD_PATH.unshift File.expand_path('../lib', __dir__)
require 'state_machines-activerecord'
require 'state_machines/test_helper'
require 'minitest/autorun'
require 'securerandom'
# Establish database connection
ActiveRecord::Base.establish_connection('adapter' => 'sqlite3', 'database' => ':memory:')
ActiveRecord::Base.logger = Logger.new("#{File.dirname(__FILE__)}/../log/active_record.log")
ActiveSupport.test_order = :random
class BaseTestCase < ActiveSupport::TestCase
include StateMachines::TestHelper
protected
# Creates a new ActiveRecord model (and the associated table)
def new_model(create_table = :foo, &)
name = create_table || :foo
table_name = "#{name}_#{SecureRandom.hex(6)}"
model = Class.new(ActiveRecord::Base) do
self.table_name = table_name.to_s
connection.create_table(table_name, force: true) { |t| t.string(:state) } if create_table
define_method(:abort_from_callback) do
throw :abort
end
(
class << self
self
end).class_eval do
define_method(:name) { name.to_s.capitalize }
end
end
model.class_eval(&) if block_given?
model.reset_column_information if create_table
model
end
def clear_active_support_dependencies
return unless defined?(ActiveSupport::Dependencies)
if ActiveSupport::Dependencies.respond_to?(:autoloader=)
ActiveSupport::Dependencies.autoloader ||= stubbed_autoloader
end
ActiveSupport::Dependencies.clear
end
def stubbed_autoloader
Object.new.tap do |obj|
obj.define_singleton_method(:reload) {}
end
end
end