pax_global_header 0000666 0000000 0000000 00000000064 15160070525 0014513 g ustar 00root root 0000000 0000000 52 comment=e47fda1158be7cfdfb7b9d941614ede148558f6c
state-machines-state_machines-activemodel-eff468b/ 0000775 0000000 0000000 00000000000 15160070525 0022405 5 ustar 00root root 0000000 0000000 state-machines-state_machines-activemodel-eff468b/.github/ 0000775 0000000 0000000 00000000000 15160070525 0023745 5 ustar 00root root 0000000 0000000 state-machines-state_machines-activemodel-eff468b/.github/workflows/ 0000775 0000000 0000000 00000000000 15160070525 0026002 5 ustar 00root root 0000000 0000000 state-machines-state_machines-activemodel-eff468b/.github/workflows/engines.yml 0000664 0000000 0000000 00000001122 15160070525 0030151 0 ustar 00root root 0000000 0000000 name: Exotic Ruby
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
gemfiles:
- gemfiles/active_model_7.2.gemfile
- gemfiles/active_model_8.0.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: '3.4'
bundler-cache: true
- name: Run tests
run: bundle exec rake
state-machines-state_machines-activemodel-eff468b/.github/workflows/release.yml 0000664 0000000 0000000 00000001770 15160070525 0030152 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
id: release
- name: Checkout
if: ${{ steps.release.outputs.release_created }}
uses: actions/checkout@v4
- name: Update COSS version
if: ${{ steps.release.outputs.release_created }}
run: |
VERSION=$(grep "VERSION = " lib/state_machines/integrations/active_model/version.rb | sed "s/.*'\(.*\)'.*/\1/")
sed -i "s/^version = .*/version = \"$VERSION\"/" coss.toml
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git add coss.toml
if ! git diff --cached --quiet; then
git commit -m "chore: update COSS version to $VERSION"
git push
fi
state-machines-state_machines-activemodel-eff468b/.github/workflows/ruby.yml 0000664 0000000 0000000 00000001566 15160070525 0027516 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_model_7.2.gemfile
- gemfiles/active_model_8.0.gemfile
- gemfiles/active_model_8.1.gemfile
include:
- ruby-version: '4.0'
gemfiles: gemfiles/active_model_edge.gemfile
experimental: true
continue-on-error: ${{ matrix.experimental == true }}
env:
BUNDLE_GEMFILE: ${{ github.workspace }}/${{ matrix.gemfiles }}
steps:
- uses: actions/checkout@v6
- 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-activemodel-eff468b/.gitignore 0000664 0000000 0000000 00000000255 15160070525 0024377 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
mkmf.log
.idea/
*.lock
state-machines-state_machines-activemodel-eff468b/.release-please-manifest.json 0000664 0000000 0000000 00000000026 15160070525 0030047 0 ustar 00root root 0000000 0000000 {
".": "0.102.0"
}
state-machines-state_machines-activemodel-eff468b/.rubocop.yml 0000664 0000000 0000000 00000001411 15160070525 0024654 0 ustar 00root root 0000000 0000000 AllCops:
NewCops: enable
TargetRubyVersion: 3.0
SuggestExtensions: false
# Allow nested method definitions in tests - they're used for test setup
Lint/NestedMethodDefinition:
Exclude:
- 'test/**/*_test.rb'
# Test setup methods can be longer
Metrics/MethodLength:
Exclude:
- 'test/**/*_test.rb'
- 'test/test_helper.rb'
Max: 10
# Use bracket style for percent literals
Style/PercentLiteralDelimiters:
PreferredDelimiters:
'%w': '[]'
'%W': '[]'
'%i': '[]'
'%I': '[]'
# The save method in tests returns a boolean, it's not a predicate method
Naming/PredicateMethod:
Exclude:
- 'test/**/*_test.rb'
# In tests, we sometimes need empty initialize methods for stubbing
Style/RedundantInitialize:
Exclude:
- 'test/**/*_test.rb' state-machines-state_machines-activemodel-eff468b/Appraisals 0000664 0000000 0000000 00000000405 15160070525 0024426 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
# ActiveModel integrations
appraise 'active_model_7.2' do
gem 'activemodel', '~> 7.2.0'
end
appraise 'active_model_8.0' do
gem 'activemodel', '~> 8.0.0'
end
appraise 'active_model_8.1' do
gem 'activemodel', '~> 8.1.0'
end
state-machines-state_machines-activemodel-eff468b/CHANGELOG.md 0000664 0000000 0000000 00000014073 15160070525 0024223 0 ustar 00root root 0000000 0000000 # Changelog
## [0.102.0](https://github.com/state-machines/state_machines-activemodel/compare/state_machines-activemodel/v0.101.0...state_machines-activemodel/v0.102.0) (2026-03-22)
### Bug Fixes
* bump state_machines min to 0.101.0 and update checkout to v6 ([d8b72c9](https://github.com/state-machines/state_machines-activemodel/commit/d8b72c9f123bf8b62086e4490c3af3ac475c310b))
* remove bump-patch-for-minor-pre-major ([7eff50d](https://github.com/state-machines/state_machines-activemodel/commit/7eff50d10fc50906a4fdc8645922a9decc96fff4))
## [0.101.0](https://github.com/state-machines/state_machines-activemodel/compare/state_machines-activemodel-v0.100.0...state_machines-activemodel/v0.101.0) (2025-11-03)
### Features
* modernize ActiveModel integration with backward compatibility ([a0a6c5f](https://github.com/state-machines/state_machines-activemodel/commit/a0a6c5f033687913b669fbc78e0f42c782502c83))
* modernize ActiveModel integration with backward compatibility ([8b6a779](https://github.com/state-machines/state_machines-activemodel/commit/8b6a779e5683dcdef18f2e082f261c581e2a9d32))
* modernize ActiveModel integration with Ruby 3+ patterns ([6a15719](https://github.com/state-machines/state_machines-activemodel/commit/6a15719013d2a734ff33e60482e624c37022c8ba))
* modernize ActiveModel integration with Ruby 3+ patterns ([f3ee659](https://github.com/state-machines/state_machines-activemodel/commit/f3ee6592f5fa49878885eebc927844ca1ab91038))
* remove EOL version of ruby and rails ([865e3e2](https://github.com/state-machines/state_machines-activemodel/commit/865e3e2e42ef16dbe3abe99aa46e9eb4b5870543))
* reset version to 0.100.0 for proper semantic versioning ([55277f8](https://github.com/state-machines/state_machines-activemodel/commit/55277f8a1ef544bc07b41b4455d6546f9a2bece3)), closes [#49](https://github.com/state-machines/state_machines-activemodel/issues/49)
* upgrade to Rails 8.1.0 stable and state_machines 0.100.4 ([#51](https://github.com/state-machines/state_machines-activemodel/issues/51)) ([eba0dd1](https://github.com/state-machines/state_machines-activemodel/commit/eba0dd10354015d10e59b6f55b2c543524dc50af))
### Bug Fixes
* prepare to release ([8d06fb0](https://github.com/state-machines/state_machines-activemodel/commit/8d06fb049c01aca1b9d45e8c01abaaceaa3f77f9))
* prepare to release 0.10.0 ([43dd535](https://github.com/state-machines/state_machines-activemodel/commit/43dd5352c16911f683e6b411e4ffd64fafeff3c4))
* preserve custom human_name for both states and events ([3e19695](https://github.com/state-machines/state_machines-activemodel/commit/3e196956f050a85e1074bc4c108eb77b27a16b07))
* preserve custom human_name for both states and events ([d7a41ba](https://github.com/state-machines/state_machines-activemodel/commit/d7a41bad420f0e999089fa87b0c00bc34f7b93f6)), closes [#37](https://github.com/state-machines/state_machines-activemodel/issues/37) [#38](https://github.com/state-machines/state_machines-activemodel/issues/38)
* update tests to avoid warning syntax ([#47](https://github.com/state-machines/state_machines-activemodel/issues/47)) ([51fe6fc](https://github.com/state-machines/state_machines-activemodel/commit/51fe6fc2dc6977b40b733aa940faba4db3dc48d6))
## [0.31.2](https://github.com/state-machines/state_machines-activemodel/compare/state_machines-activemodel/v0.31.1...state_machines-activemodel/v0.31.2) (2025-11-03)
### Features
* upgrade to Rails 8.1.0 stable and state_machines 0.100.4 ([#51](https://github.com/state-machines/state_machines-activemodel/issues/51)) ([eba0dd1](https://github.com/state-machines/state_machines-activemodel/commit/eba0dd10354015d10e59b6f55b2c543524dc50af))
## [0.31.1](https://github.com/state-machines/state_machines-activemodel/compare/state_machines-activemodel/v0.31.0...state_machines-activemodel/v0.31.1) (2025-07-25)
### Bug Fixes
* update tests to avoid warning syntax ([#47](https://github.com/state-machines/state_machines-activemodel/issues/47)) ([51fe6fc](https://github.com/state-machines/state_machines-activemodel/commit/51fe6fc2dc6977b40b733aa940faba4db3dc48d6))
## [0.31.0](https://github.com/state-machines/state_machines-activemodel/compare/state_machines-activemodel/v0.10.0...state_machines-activemodel/v0.31.0) (2025-06-29)
### Features
* modernize ActiveModel integration with backward compatibility ([a0a6c5f](https://github.com/state-machines/state_machines-activemodel/commit/a0a6c5f033687913b669fbc78e0f42c782502c83))
* modernize ActiveModel integration with backward compatibility ([8b6a779](https://github.com/state-machines/state_machines-activemodel/commit/8b6a779e5683dcdef18f2e082f261c581e2a9d32))
* modernize ActiveModel integration with Ruby 3+ patterns ([6a15719](https://github.com/state-machines/state_machines-activemodel/commit/6a15719013d2a734ff33e60482e624c37022c8ba))
* modernize ActiveModel integration with Ruby 3+ patterns ([f3ee659](https://github.com/state-machines/state_machines-activemodel/commit/f3ee6592f5fa49878885eebc927844ca1ab91038))
### Bug Fixes
* preserve custom human_name for both states and events ([3e19695](https://github.com/state-machines/state_machines-activemodel/commit/3e196956f050a85e1074bc4c108eb77b27a16b07))
* preserve custom human_name for both states and events ([d7a41ba](https://github.com/state-machines/state_machines-activemodel/commit/d7a41bad420f0e999089fa87b0c00bc34f7b93f6)), closes [#37](https://github.com/state-machines/state_machines-activemodel/issues/37) [#38](https://github.com/state-machines/state_machines-activemodel/issues/38)
## [0.10.0](https://github.com/state-machines/state_machines-activemodel/compare/state_machines-activemodel-v0.9.0...state_machines-activemodel/v0.10.0) (2025-06-12)
### Features
* remove EOL version of ruby and rails ([865e3e2](https://github.com/state-machines/state_machines-activemodel/commit/865e3e2e42ef16dbe3abe99aa46e9eb4b5870543))
### Bug Fixes
* prepare to release ([8d06fb0](https://github.com/state-machines/state_machines-activemodel/commit/8d06fb049c01aca1b9d45e8c01abaaceaa3f77f9))
* prepare to release 0.10.0 ([43dd535](https://github.com/state-machines/state_machines-activemodel/commit/43dd5352c16911f683e6b411e4ffd64fafeff3c4))
state-machines-state_machines-activemodel-eff468b/Gemfile 0000664 0000000 0000000 00000000303 15160070525 0023674 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
source 'https://rubygems.org'
# Specify your gem's dependencies in state_machine2_activemodel.gemspec
gemspec
platforms :mri do
gem 'debug'
end
gem 'rubocop'
state-machines-state_machines-activemodel-eff468b/LICENSE.txt 0000664 0000000 0000000 00000002135 15160070525 0024231 0 ustar 00root root 0000000 0000000 Copyright (c) 2006-2012 Aaron Pfeifer
Copyright (c) 2014-2023 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-activemodel-eff468b/README.md 0000664 0000000 0000000 00000003073 15160070525 0023667 0 ustar 00root root 0000000 0000000 
# StateMachines ActiveModel Integration
The ActiveModel integration is useful for both standalone usage and for providing
the base implementation for ORMs which implement the ActiveModel API. This
integration adds support for validation errors and dirty attribute tracking.
## Installation
Add this line to your application's Gemfile:
gem 'state_machines-activemodel'
And then execute:
$ bundle
Or install it yourself as:
$ gem install state_machines-activemodel
## Dependencies
Active Model 7.1+
## Usage
```ruby
class Vehicle
include ActiveModel::Dirty
include ActiveModel::Validations
attr_accessor :state
define_attribute_methods [:state]
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
```
## Contributing
1. Fork it ( https://github.com/state-machines/state_machines-activemodel/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-activemodel-eff468b/Rakefile 0000664 0000000 0000000 00000000345 15160070525 0024054 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require 'bundler/gem_tasks'
require 'rake/testtask'
Rake::TestTask.new do |t|
t.libs << 'test'
t.test_files = FileList['test/*_test.rb']
end
desc 'Default: run all tests.'
task default: :test
state-machines-state_machines-activemodel-eff468b/bin/ 0000775 0000000 0000000 00000000000 15160070525 0023155 5 ustar 00root root 0000000 0000000 state-machines-state_machines-activemodel-eff468b/bin/console 0000775 0000000 0000000 00000000607 15160070525 0024550 0 ustar 00root root 0000000 0000000 #!/usr/bin/env ruby
# frozen_string_literal: true
require "bundler/setup"
require "state_machines-activemodel"
# You can add fixtures and/or initialization code here to make experimenting
# with your gem easier. You can also use a different console, if you like.
# (If you use this, don't forget to add pry to your Gemfile!)
# require "pry"
# Pry.start
require "irb"
IRB.start(__FILE__)
state-machines-state_machines-activemodel-eff468b/bin/setup 0000775 0000000 0000000 00000000203 15160070525 0024236 0 ustar 00root root 0000000 0000000 #!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
set -vx
bundle install
# Do any other automated setup that you need to do here
state-machines-state_machines-activemodel-eff468b/coss.toml 0000664 0000000 0000000 00000012610 15160070525 0024251 0 ustar 00root root 0000000 0000000 # COSS Metadata Template v0.0.2
# This is the official COSS (Contriboss Open Source Standard) specification template
# Copy this file to your project root as coss.toml and customize for your project
##########################################################
# 1. Basic Project Information
name = "state_machines-activemodel"
version = "0.102.0"
description = "Adds support for creating state machines for attributes on ActiveModel models"
licenses = ["MIT"]
ai_contributions = true
coss_compliant = true
homepage = "https://github.com/state-machines/state_machines-activemodel"
keywords = [
"ruby",
"state-machine",
"activemodel",
"rails",
"validations",
"callbacks",
]
##########################################################
# 2. Repository and Issue Tracking
repository = "https://github.com/state-machines/state_machines-activemodel"
issue_tracker = "https://github.com/state-machines/state_machines-activemodel/issues"
documentation = "https://github.com/state-machines/state_machines-activemodel/blob/master/README.md"
security_policy = ""
##########################################################
# 3. Languages, Frameworks, and Platforms
languages = ["ruby"]
[frameworks]
rails = "7.1+"
activemodel = "7.1+"
supported_platforms = ["linux", "darwin", "windows"]
##########################################################
# 4. Dependency Lock Files
[dependency_locks]
ruby = "Gemfile.lock"
appraisal = "gemfiles/*.gemfile.lock"
[packaging]
ruby = "gem build state_machines-activemodel.gemspec"
##########################################################
# 5. Maintainers and Governance
maintainers = ["terminale@gmail.com"]
governance = { type = "informal" }
##########################################################
# 6. Linting, Formatting, and Static Analysis
lint = "bundle exec rubocop"
format = "bundle exec rubocop -a"
static_analysis = ["bundle exec rubocop"]
##########################################################
# 7. CI and Build Commands
build = "bundle install"
test = "rake test"
test_all = "appraisal rake test"
coverage = ""
##########################################################
# 8. Tests and Quality Metrics
[test_frameworks]
ruby = "minitest"
appraisal = true # Tests against multiple Rails versions
test_report_format = "minitest"
coverage_threshold = 0
##########################################################
# 9. Commit Guidelines and Formats
commit_message_format = ""
##########################################################
# 10. Release and Changelog
changelog = "CHANGELOG.md"
release_tag_pattern = "v{version}"
##########################################################
# 11. Badges and Integrations (Optional)
[badges]
ci = "https://github.com/state-machines/state_machines-activemodel/actions/workflows/ruby.yml/badge.svg"
coverage = ""
license_badge = ""
##########################################################
# 12. Optional Miscellaneous Fields
chat = ""
support = { type = "github", contact = "https://github.com/state-machines/state_machines-activemodel/issues" }
apidocs = ""
##########################################################
# 13. Environment and Runtime Info
[environments]
ruby = "3.1+"
rails = "7.1+"
##########################################################
# 15. Project Classification
project_type = "library"
maturity = "stable"
audience = ["developers", "ruby-developers", "rails-developers"]
##########################################################
# 16. Localization / Internationalization
[i18n]
default_locale = "en"
supported_locales = ["en"]
translation_files = "lib/state_machines/integrations/active_model/locale.rb"
##########################################################
# 17. Contribution Automation
[contribution_tooling]
dependabot = false
precommit_hooks = false
ai_review = "disabled"
codeowners = ""
##########################################################
# 18. Security Scanning and SBOM
[security]
sbom = ""
vulnerability_scanner = ""
license_compliance_tool = ""
##########################################################
# 19. Documentation Quality Flags
[docs]
coverage = 0
style = ""
ai_summary_enabled = false
##########################################################
# 20. Submodules and Component References
[dependencies]
# Core dependency
state_machines = ">= 0.31.0"
activemodel = ">= 7.1"
[related_projects]
# Other gems in the state_machines ecosystem
state_machines = "https://github.com/state-machines/state_machines"
state_machines-activerecord = "https://github.com/state-machines/state_machines-activerecord"
state_machines-audit_trail = "https://github.com/state-machines/state_machines-audit_trail"
state_machines-graphviz = "https://github.com/state-machines/state_machines-graphviz"
state_machines-yard = "https://github.com/state-machines/state_machines-yard"
##########################################################
# 21. Integration-Specific Information
[activemodel_integration]
features = [
"state validations",
"transition callbacks",
"dirty attribute tracking",
"i18n support",
"mass assignment protection",
"validation errors on invalid transitions",
]
tested_versions = ["7.1", "7.2", "8.0", "edge"]
##########################################################
# 22. Testing Commands
[testing]
specific_file = "ruby -Itest test/path/to/test.rb"
specific_test = "ruby -Itest test/path/to/test.rb:line_number"
appraisal_setup = "appraisal install"
rails_version_test = "appraisal rails-7-1 rake test"
state-machines-state_machines-activemodel-eff468b/gemfiles/ 0000775 0000000 0000000 00000000000 15160070525 0024200 5 ustar 00root root 0000000 0000000 state-machines-state_machines-activemodel-eff468b/gemfiles/active_model_7.2.gemfile 0000664 0000000 0000000 00000000255 15160070525 0030555 0 ustar 00root root 0000000 0000000 # This file was generated by Appraisal
source "https://rubygems.org"
gem "rubocop"
gem "activemodel", "~> 7.2.0"
platforms :mri do
gem "debug"
end
gemspec path: "../"
state-machines-state_machines-activemodel-eff468b/gemfiles/active_model_8.0.gemfile 0000664 0000000 0000000 00000000255 15160070525 0030554 0 ustar 00root root 0000000 0000000 # This file was generated by Appraisal
source "https://rubygems.org"
gem "rubocop"
gem "activemodel", "~> 8.0.0"
platforms :mri do
gem "debug"
end
gemspec path: "../"
state-machines-state_machines-activemodel-eff468b/gemfiles/active_model_8.1.gemfile 0000664 0000000 0000000 00000000255 15160070525 0030555 0 ustar 00root root 0000000 0000000 # This file was generated by Appraisal
source "https://rubygems.org"
gem "rubocop"
gem "activemodel", "~> 8.1.0"
platforms :mri do
gem "debug"
end
gemspec path: "../"
state-machines-state_machines-activemodel-eff468b/gemfiles/active_model_edge.gemfile 0000664 0000000 0000000 00000000310 15160070525 0031143 0 ustar 00root root 0000000 0000000 # This file was generated by Appraisal
source "https://rubygems.org"
gem "rubocop"
gem "activemodel", github: "rails/rails", branch: "main"
platforms :mri do
gem "debug"
end
gemspec path: "../"
state-machines-state_machines-activemodel-eff468b/lib/ 0000775 0000000 0000000 00000000000 15160070525 0023153 5 ustar 00root root 0000000 0000000 state-machines-state_machines-activemodel-eff468b/lib/state_machines-activemodel.rb 0000664 0000000 0000000 00000000364 15160070525 0030764 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require 'active_support'
require 'state_machines/integrations/active_model'
ActiveSupport.on_load(:i18n) do
I18n.load_path << File.expand_path('state_machines/integrations/active_model/locale.rb', __dir__)
end
state-machines-state_machines-activemodel-eff468b/lib/state_machines/ 0000775 0000000 0000000 00000000000 15160070525 0026142 5 ustar 00root root 0000000 0000000 state-machines-state_machines-activemodel-eff468b/lib/state_machines/integrations/ 0000775 0000000 0000000 00000000000 15160070525 0030650 5 ustar 00root root 0000000 0000000 state-machines-state_machines-activemodel-eff468b/lib/state_machines/integrations/active_model.rb 0000664 0000000 0000000 00000043303 15160070525 0033633 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require 'active_model'
require 'active_support/core_ext/hash/keys'
require 'active_support/core_ext/module/attribute_accessors'
require 'state_machines'
require 'state_machines/integrations/base'
require 'state_machines/integrations/active_model/version'
module StateMachines
module Integrations # :nodoc:
# Adds support for integrating state machines with ActiveModel classes.
#
# == Examples
#
# If using ActiveModel directly within your class, then any one of the
# following features need to be included in order for the integration to be
# detected:
# * ActiveModel::Validations
#
# Below is an example of a simple state machine defined within an
# ActiveModel class:
#
# class Vehicle
# include ActiveModel::Validations
#
# attr_accessor :state
# define_attribute_methods [:state]
#
# 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, no action will be invoked when a state is transitioned. This
# means that if you want to save changes when transitioning, you must
# define the action yourself like so:
#
# class Vehicle
# include ActiveModel::Validations
# attr_accessor :state
#
# state_machine action: :save do
# ...
# end
#
# def save
# # Save changes
# end
# end
#
# == Validations
#
# As mentioned in StateMachine::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 ActiveModel's validation
# framework, custom validators will not work as expected when defined to run
# in multiple states. For example:
#
# class Vehicle
# include ActiveModel::Validations
#
# 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
# include ActiveModel::Validations
#
# state_machine do
# ...
# state :first_gear, :second_gear do
# validate { |vehicle| vehicle.speed_is_legal }
# end
# end
# end
#
# == Validation errors
#
# In order to hook in validation support for your model, the
# ActiveModel::Validations feature must be included. If this is included
# and an event fails to successfully fire because there are no matching
# transitions for the object, a validation error is added to the object's
# state attribute to help in determining why it failed.
#
# For example,
#
# vehicle = Vehicle.new
# vehicle.ignite # => false
# vehicle.errors.full_messages # => ["State cannot transition via \"ignite\""]
#
# 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! # => StateMachine::InvalidTransition: Cannot transition state via :ignite from :parked (Reason(s): Name cannot be blank)
#
# === 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, the attribute
# should be protected using Strong Parameters in your controllers:
#
# class Vehicle
# attr_accessor :state
#
# state_machine do
# ...
# end
# end
#
# # In your controller
# def vehicle_params
# params.require(:vehicle).permit(:attribute1, :attribute2) # Exclude :state_event
# end
#
# If you want to only have *some* events be able to fire via mass-assignment,
# you can build two state machines (one private and one public) like so:
#
# class Vehicle
# attr_accessor :state
#
# 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
# end
#
# # In your controller
# def vehicle_params
# # Only permit events from the public state machine
# params.require(:vehicle).permit(:attribute1, :attribute2, :public_state_event)
# # The private state_event is not permitted
# end
#
# == Callbacks
#
# All before/after transition callbacks defined for ActiveModel models
# behave in the same way that other ActiveSupport callbacks behave. The
# object involved in the transition is passed in as an argument.
#
# For example,
#
# class Vehicle
# include ActiveModel::Validations
# attr_accessor :state
#
# 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.
#
# == Internationalization
#
# Any error message that is generated from performing invalid transitions
# can be localized. The following default translations are used:
#
# en:
# activemodel:
# 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:
# activemodel:
# 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":
# * activemodel.state_machines.#{model_name}.#{machine_name}.states.#{state_name}
# * activemodel.state_machines.#{model_name}.states.#{state_name}
# * activemodel.state_machines.#{machine_name}.states.#{state_name}
# * activemodel.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":
# * activemodel.state_machines.#{model_name}.#{machine_name}.events.#{event_name}
# * activemodel.state_machines.#{model_name}.events.#{event_name}
# * activemodel.state_machines.#{machine_name}.events.#{event_name}
# * activemodel.state_machines.events.#{event_name}
#
# An example translation configuration might look like so:
#
# es:
# activemodel:
# state_machines:
# states:
# parked: 'estacionado'
# events:
# park: 'estacionarse'
#
# == Dirty Attribute Tracking
#
# When using the ActiveModel::Dirty extension, your model will keep track of
# any changes that are made to attributes. Depending on your ORM, an object
# will only be saved when there are attributes that have changed on the
# object. When integrating with state_machine, typically the +state+ field
# will be marked as dirty after a transition occurs. In some situations,
# however, this isn't the case.
#
# If you define loopback transitions in your state machine, the value for
# the machine's attribute (e.g. state) will not change. Unless you explicitly
# indicate so, this means that your object won't persist anything on a
# loopback. For example:
#
# class Vehicle
# include ActiveModel::Validations
# include ActiveModel::Dirty
# attr_accessor :state
#
# state_machine initial: :parked do
# event :park do
# transition parked: :parked, ...
# end
# end
# end
#
# If, instead, you'd like your object to always persist regardless of
# whether the value actually changed, you can do so by using the
# #{attribute}_will_change! helpers or defining a +before_transition+
# callback that actually changes an attribute on the model. For example:
#
# class Vehicle
# ...
# state_machine initial: :parked do
# before_transition all => same do |vehicle|
# vehicle.state_will_change!
#
# # Alternative solution, updating timestamp
# # vehicle.updated_at = Time.current
# end
# end
# end
#
# == Creating new integrations
#
# If you want to integrate state_machine with an ORM that implements parts
# or all of the ActiveModel API, only the machine defaults need to be
# specified. Otherwise, the implementation is similar to any other
# integration.
#
# For example,
#
# module StateMachine::Integrations::MyORM
# include ActiveModel
#
# mattr_accessor(:defaults) { { action: :persist } }
#
# def self.matches?(klass)
# defined?(::MyORM::Base) && klass <= ::MyORM::Base
# end
#
# protected
#
# def runs_validations_on_action?
# action == :persist
# end
# end
#
# If you wish to implement other features, such as attribute initialization
# with protected attributes, named scopes, or database transactions, you
# must add these independent of the ActiveModel integration. See the
# ActiveRecord implementation for examples of these customizations.
module ActiveModel
include Base
@defaults = {}
# Classes that include ActiveModel::Validations
# will automatically use the ActiveModel integration.
def self.matching_ancestors
[::ActiveModel, ::ActiveModel::Validations]
end
# Adds a validation error to the given object
def invalidate(object, attribute, message, values = [])
return unless supports_validations?
attribute = self.attribute(attribute)
options = values.to_h
default_options = default_error_message_options(object, attribute, message)
object.errors.add(attribute, message, **options, **default_options)
end
# Describes the current validation errors on the given object. If none
# are specific, then the default error is interpeted as a "halt".
def errors_for(object)
object.errors.empty? ? 'Transition halted' : object.errors.full_messages.join(', ')
end
# Resets any errors previously added when invalidating the given object
def reset(object)
object.errors.clear if supports_validations?
end
# Runs state events around the object's validation process
def around_validation(object, &)
object.class.state_machines.transitions(object, action, after: false).perform(&)
end
protected
def define_state_initializer
define_helper :instance, <<-END_EVAL, __FILE__, __LINE__ + 1
def initialize(params = nil, **kwargs)
# Support both positional hash and keyword arguments
attrs = params.nil? ? kwargs : params
#{' '}
attrs.transform_keys! do |key|
self.class.attribute_aliases[key.to_s] || key.to_s
end if self.class.respond_to?(:attribute_aliases)
# Call super with the appropriate arguments based on what we received
self.class.state_machines.initialize_states(self, {}, attrs) do
if params
super(params)
else
super(**kwargs)
end
end
end
END_EVAL
end
# Whether validations are supported in the integration. Only true if
# the ActiveModel feature is enabled on the owner class.
def supports_validations?
defined?(::ActiveModel::Validations) && owner_class <= ::ActiveModel::Validations
end
# Do validations run when the action configured this machine is
# invoked? This is used to determine whether to fire off attribute-based
# event transitions when the action is run.
def runs_validations_on_action?
false
end
# Gets the terminator to use for callbacks
def callback_terminator
@callback_terminator ||= ->(result) { result == false }
end
# Determines the base scope to use when looking up translations
def i18n_scope(klass)
klass.i18n_scope
end
# The default options to use when generating messages for validation
# errors
def default_error_message_options(_object, _attribute, message)
{ message: @messages[message] }
end
# Translates the given key / value combo. Translation keys are looked
# up in the following order:
# * #{i18n_scope}.state_machines.#{model_name}.#{machine_name}.#{plural_key}.#{value}
# * #{i18n_scope}.state_machines.#{model_name}.#{plural_key}.#{value}
# * #{i18n_scope}.state_machines.#{machine_name}.#{plural_key}.#{value}
# * #{i18n_scope}.state_machines.#{plural_key}.#{value}
#
# If no keys are found, then the humanized value will be the fallback.
def translate(klass, key, value)
ancestors = ancestors_for(klass)
group = key.to_s.pluralize
value = value ? value.to_s : 'nil'
# Generate all possible translation keys
translations = ancestors.map { |ancestor| :"#{ancestor.model_name.to_s.underscore}.#{name}.#{group}.#{value}" }
translations.concat(ancestors.map { |ancestor| :"#{ancestor.model_name.to_s.underscore}.#{group}.#{value}" })
translations.push(:"#{name}.#{group}.#{value}", :"#{group}.#{value}", value.humanize.downcase)
I18n.translate(translations.shift, default: translations, scope: [i18n_scope(klass), :state_machines])
end
# Build a list of ancestors for the given class to use when
# determining which localization key to use for a particular string.
def ancestors_for(klass)
klass.lookup_ancestors
end
# Skips defining reader/writer methods since this is done automatically
def define_state_accessor
name = self.name
return unless supports_validations?
owner_class.validates_each(attribute) do |object|
machine = object.class.state_machine(name)
machine.invalidate(object, :state, :invalid) unless machine.states.match(object)
end
end
# Adds hooks into validation for automatically firing events
def define_action_helpers
super
define_validation_hook if runs_validations_on_action?
end
# Hooks into validations by defining around callbacks for the
# :validation event
def define_validation_hook
owner_class.set_callback(:validation, :around, self, prepend: true)
end
# Creates a new callback in the callback chain, always inserting it
# before the default Observer callbacks that were created after
# initialization.
def add_callback(type, options, &)
options[:terminator] = callback_terminator
super
end
# Configures new states with the built-in humanize scheme
def add_states(*)
super.each do |new_state|
# Only set the translation lambda if human_name is the default auto-generated value
# This preserves user-specified human names while still applying translations for defaults
default_human_name = new_state.name ? new_state.name.to_s.tr('_', ' ') : 'nil'
if new_state.human_name == default_human_name
new_state.human_name = ->(state, klass) { translate(klass, :state, state.name) }
end
end
end
# Configures new event with the built-in humanize scheme
def add_events(*)
super.each do |new_event|
# Only set the translation lambda if human_name is the default auto-generated value
# This preserves user-specified human names while still applying translations for defaults
default_human_name = new_event.name ? new_event.name.to_s.tr('_', ' ') : 'nil'
if new_event.human_name == default_human_name
new_event.human_name = ->(event, klass) { translate(klass, :event, event.name) }
end
end
end
end
register(ActiveModel)
end
end
state-machines-state_machines-activemodel-eff468b/lib/state_machines/integrations/active_model/ 0000775 0000000 0000000 00000000000 15160070525 0033303 5 ustar 00root root 0000000 0000000 locale.rb 0000664 0000000 0000000 00000001121 15160070525 0035003 0 ustar 00root root 0000000 0000000 state-machines-state_machines-activemodel-eff468b/lib/state_machines/integrations/active_model # 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: {
activemodel: {
errors: {
messages: {
invalid: lambda { |*| StateMachines::Machine.default_messages[:invalid] },
invalid_event: lambda { |*| StateMachines::Machine.default_messages[:invalid_event] % ['%{state}'] },
invalid_transition: lambda { |*| StateMachines::Machine.default_messages[:invalid_transition] % ['%{event}'] }
}
}
}
} }
version.rb 0000664 0000000 0000000 00000000215 15160070525 0035234 0 ustar 00root root 0000000 0000000 state-machines-state_machines-activemodel-eff468b/lib/state_machines/integrations/active_model # frozen_string_literal: true
module StateMachines
module Integrations
module ActiveModel
VERSION = '0.102.0'
end
end
end
state-machines-state_machines-activemodel-eff468b/release-please-config.json 0000664 0000000 0000000 00000000314 15160070525 0027430 0 ustar 00root root 0000000 0000000 {
"packages": {
".": {
"release-type": "ruby",
"package-name": "state_machines-activemodel",
"version-file": "lib/state_machines/integrations/active_model/version.rb"
}
}
}
state-machines-state_machines-activemodel-eff468b/state_machines-activemodel.gemspec 0000664 0000000 0000000 00000002354 15160070525 0031237 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require_relative 'lib/state_machines/integrations/active_model/version'
Gem::Specification.new do |spec|
spec.name = 'state_machines-activemodel'
spec.version = StateMachines::Integrations::ActiveModel::VERSION
spec.authors = ['Abdelkader Boudih', 'Aaron Pfeifer']
spec.email = %w(terminale@gmail.com aaron@pluginaweek.org)
spec.summary = 'ActiveModel integration for State Machines'
spec.description = 'Adds support for creating state machines for attributes on ActiveModel'
spec.homepage = 'https://github.com/state-machines/state_machines-activemodel'
spec.license = 'MIT'
spec.files = Dir.glob('{lib}/**/*') + %w(LICENSE.txt README.md)
spec.test_files = Dir.glob('test/**/{*_test,test_*}.rb')
spec.require_paths = ['lib']
spec.required_ruby_version = '>= 3.2.0'
spec.add_dependency 'state_machines', '>= 0.101.0'
spec.add_dependency 'activemodel', '>= 7.2'
spec.add_development_dependency 'bundler', '>= 1.6'
spec.add_development_dependency 'rake', '>= 10'
spec.add_development_dependency 'appraisal', '>= 1'
spec.add_development_dependency 'minitest', '= 5.27.0'
spec.add_development_dependency 'minitest-reporters'
end
state-machines-state_machines-activemodel-eff468b/test/ 0000775 0000000 0000000 00000000000 15160070525 0023364 5 ustar 00root root 0000000 0000000 state-machines-state_machines-activemodel-eff468b/test/event_human_name_test.rb 0000664 0000000 0000000 00000013645 15160070525 0030272 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require_relative 'test_helper'
class EventHumanNameTest < BaseTestCase
def setup
@model = new_model do
include ActiveModel::Validations
attr_accessor :status
end
end
def test_should_allow_custom_human_name_on_event
machine = StateMachines::Machine.new(@model, :status, initial: :parked) do
event :start, human_name: 'Start Engine' do
transition parked: :running
end
event :stop do
transition running: :parked
end
event :pause, human_name: 'Temporarily Pause' do
transition running: :paused
end
end
assert_equal 'Start Engine', machine.events[:start].human_name(@model)
assert_equal 'Temporarily Pause', machine.events[:pause].human_name(@model)
end
def test_should_not_override_custom_event_human_name_with_translation
# Set up I18n translations
I18n.backend.store_translations(:en, {
activemodel: {
state_machines: {
events: {
ignite: 'Translation for Ignite',
park: 'Translation for Park',
repair: 'Translation for Repair'
}
}
}
})
machine = StateMachines::Machine.new(@model, :status, initial: :parked) do
event :ignite, human_name: 'Custom Ignition' do
transition parked: :idling
end
event :park do
transition idling: :parked
end
event :repair, human_name: 'Custom Repair Process' do
transition any => :parked
end
end
# Custom human names should be preserved
assert_equal 'Custom Ignition', machine.events[:ignite].human_name(@model)
assert_equal 'Custom Repair Process', machine.events[:repair].human_name(@model)
# Event without custom human_name should use translation
assert_equal 'Translation for Park', machine.events[:park].human_name(@model)
end
def test_should_allow_custom_event_human_name_as_string
machine = StateMachines::Machine.new(@model, :status) do
event :activate, human_name: 'Turn On'
end
assert_equal 'Turn On', machine.events[:activate].human_name(@model)
end
def test_should_allow_custom_event_human_name_as_lambda
machine = StateMachines::Machine.new(@model, :status) do
event :process, human_name: ->(event, klass) { "#{klass.name}: #{event.name.to_s.capitalize} Action" }
end
assert_equal 'Foo: Process Action', machine.events[:process].human_name(@model)
end
def test_should_use_default_translation_when_no_custom_event_human_name
machine = StateMachines::Machine.new(@model, :status) do
event :idle
end
# Should fall back to humanized version when no translation exists
assert_equal 'idle', machine.events[:idle].human_name(@model)
end
def test_should_handle_nil_event_human_name
machine = StateMachines::Machine.new(@model, :status) do
event :wait
end
# Explicitly set to nil
machine.events[:wait].human_name = nil
# When human_name is nil, Event#human_name returns nil
assert_nil machine.events[:wait].human_name(@model)
end
def test_should_preserve_event_human_name_through_multiple_definitions
machine = StateMachines::Machine.new(@model, :status, initial: :draft)
# First define event with custom human name
machine.event :publish, human_name: 'Make Public' do
transition draft: :published
end
# Redefine the same event (this should not override the human_name)
machine.event :publish do
transition pending: :published
end
assert_equal 'Make Public', machine.events[:publish].human_name(@model)
end
def test_should_work_with_state_machine_helper_method
@model.class_eval do
state_machine :status, initial: :pending do
event :approve, human_name: 'Grant Approval' do
transition pending: :approved
end
event :reject do
transition pending: :rejected
end
end
end
machine = @model.state_machine(:status)
assert_equal 'Grant Approval', machine.events[:approve].human_name(@model)
end
def test_should_handle_complex_i18n_lookup_with_custom_event_human_name
# Set up complex I18n structure
I18n.backend.store_translations(:en, {
activemodel: {
state_machines: {
foo: {
status: {
events: {
submit: 'Model Specific Submit'
}
}
},
status: {
events: {
submit: 'Machine Specific Submit'
}
},
events: {
submit: 'Generic Submit'
}
}
}
})
machine = StateMachines::Machine.new(@model, :status) do
event :submit, human_name: 'Send for Review' do
transition draft: :pending
end
end
# Should use the custom human_name, not any of the I18n translations
assert_equal 'Send for Review', machine.events[:submit].human_name(@model)
end
def teardown
# Clear I18n translations after each test
I18n.backend.reload!
end
end
state-machines-state_machines-activemodel-eff468b/test/files/ 0000775 0000000 0000000 00000000000 15160070525 0024466 5 ustar 00root root 0000000 0000000 state-machines-state_machines-activemodel-eff468b/test/files/en.yml 0000664 0000000 0000000 00000000134 15160070525 0025611 0 ustar 00root root 0000000 0000000 en:
activemodel:
errors:
messages:
invalid_transition: "cannot %{event}" state-machines-state_machines-activemodel-eff468b/test/human_name_preservation_test.rb 0000664 0000000 0000000 00000004525 15160070525 0031667 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require_relative 'test_helper'
class HumanNamePreservationTest < BaseTestCase
def setup
@model = new_model do
include ActiveModel::Validations
attr_accessor :status
end
end
def test_should_preserve_custom_state_human_name_when_using_activemodel_integration
# This test specifically verifies that PR #38's fix works:
# Using ||= instead of = in add_states method
@model.class_eval do
state_machine :status, initial: :pending do
# Define a state with a custom human_name
state :pending, human_name: 'My Custom Pending'
state :approved
end
end
machine = @model.state_machine(:status)
# The custom human_name should be preserved, not overwritten by the integration
assert_equal 'My Custom Pending', machine.states[:pending].human_name(@model)
end
def test_should_preserve_custom_event_human_name_when_using_activemodel_integration
# This test verifies our additional fix for events:
# Using ||= instead of = in add_events method
@model.class_eval do
state_machine :status, initial: :pending do
event :approve, human_name: 'Grant Authorization' do
transition pending: :approved
end
event :reject do
transition pending: :rejected
end
end
end
machine = @model.state_machine(:status)
# The custom human_name should be preserved, not overwritten by the integration
assert_equal 'Grant Authorization', machine.events[:approve].human_name(@model)
end
def test_regression_issue_37_hard_coded_human_name_preserved
# This is the exact regression test for issue #37
# "Hard-coded human_name is being overwritten"
@model.class_eval do
state_machine :status do
state :pending, human_name: 'Pending Approval'
state :active, human_name: 'Active State'
event :activate, human_name: 'Activate Now' do
transition pending: :active
end
end
end
machine = @model.state_machine(:status)
# Both states and events should preserve their hard-coded human names
assert_equal 'Pending Approval', machine.states[:pending].human_name(@model)
assert_equal 'Active State', machine.states[:active].human_name(@model)
assert_equal 'Activate Now', machine.events[:activate].human_name(@model)
end
end
state-machines-state_machines-activemodel-eff468b/test/integration_test.rb 0000664 0000000 0000000 00000001641 15160070525 0027275 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require 'test_helper'
class IntegrationTest < BaseTestCase
def test_should_be_registered
assert_includes StateMachines::Integrations.list, StateMachines::Integrations::ActiveModel
end
def test_should_register_one_integration
assert_equal 1, StateMachines::Integrations.list.size
end
def test_should_have_an_integration_name
assert_equal :active_model, StateMachines::Integrations::ActiveModel.integration_name
end
def test_should_match_if_class_includes_validations_feature
assert StateMachines::Integrations::ActiveModel.matches?(new_model { include ActiveModel::Validations })
end
def test_should_not_match_if_class_does_not_include_active_model_features
refute StateMachines::Integrations::ActiveModel.matches?(new_plain_model)
end
def test_should_have_no_defaults
assert_equal({}, StateMachines::Integrations::ActiveModel.defaults)
end
end
state-machines-state_machines-activemodel-eff468b/test/machine_by_default_test.rb 0000664 0000000 0000000 00000001124 15160070525 0030550 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require 'test_helper'
class MachineByDefaultTest < BaseTestCase
def setup
@model = new_model
@machine = StateMachines::Machine.new(@model, integration: :active_model)
end
def test_should_not_have_action
assert_nil @machine.action
end
def test_should_use_transactions
assert_equal true, @machine.use_transactions
end
def test_should_not_have_any_before_callbacks
assert_equal 0, @machine.callbacks[:before].size
end
def test_should_not_have_any_after_callbacks
assert_equal 0, @machine.callbacks[:after].size
end
end
state-machines-state_machines-activemodel-eff468b/test/machine_errors_test.rb 0000664 0000000 0000000 00000001173 15160070525 0027752 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require 'test_helper'
class MachineErrorsTest < BaseTestCase
def setup
@model = new_model { include ActiveModel::Validations }
@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-activemodel-eff468b/test/machine_initialization_compatibility_test.rb 0000664 0000000 0000000 00000003215 15160070525 0034415 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require_relative 'test_helper'
class MachineInitializationCompatibilityTest < BaseTestCase
def setup
@model = new_model do
include ActiveModel::Validations
end
@machine = StateMachines::Machine.new(@model, initial: :parked)
@machine.state :parked, :idling
@machine.event :ignite
end
def test_should_accept_positional_hash_argument
record = @model.new({ state: 'idling' })
assert_equal 'idling', record.state
end
def test_should_accept_keyword_arguments
record = @model.new(state: 'idling')
assert_equal 'idling', record.state
end
def test_should_accept_empty_initialization
record = @model.new
assert_equal 'parked', record.state
end
def test_should_handle_attribute_aliases
@model.class_eval do
alias_attribute :status, :state
end
record = @model.new(status: 'idling')
assert_equal 'idling', record.state
end
def test_should_prefer_positional_hash_over_keywords_when_both_present
# If someone accidentally provides both, positional takes precedence
record = @model.new({ state: 'idling' }, state: 'parked')
assert_equal 'idling', record.state
end
def test_should_handle_empty_positional_hash
# Empty hash should still be treated as positional argument
record = @model.new({})
assert_equal 'parked', record.state # Gets default initial state
end
def test_should_use_keywords_when_empty_hash_and_keywords_present
# With the fix, keywords are ignored even with empty positional hash
record = @model.new({}, state: 'idling')
assert_equal 'parked', record.state # Empty hash takes precedence
end
end
state-machines-state_machines-activemodel-eff468b/test/machine_multiple_test.rb 0000664 0000000 0000000 00000001056 15160070525 0030271 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require 'test_helper'
class MachineMultipleTest < BaseTestCase
def setup
@model = new_model do
attribute :status, :string
end
@state_machine = StateMachines::Machine.new(@model, initial: :parked, integration: :active_model)
@status_machine = StateMachines::Machine.new(@model, :status, initial: :idling, integration: :active_model)
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-activemodel-eff468b/test/machine_with_callbacks_test.rb 0000664 0000000 0000000 00000006355 15160070525 0031417 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require 'test_helper'
class MachineWithCallbacksTest < BaseTestCase
def setup
@model = new_model
@machine = StateMachines::Machine.new(@model, initial: :parked, integration: :active_model)
@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_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: lambda {}
assert_equal [: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
end
machine_with_dirty_attribute_and_custom_attributes_during_loopback_test.rb 0000664 0000000 0000000 00000001435 15160070525 0042535 0 ustar 00root root 0000000 0000000 state-machines-state_machines-activemodel-eff468b/test # frozen_string_literal: true
require 'test_helper'
class MachineWithDirtyAttributeAndCustomAttributesDuringLoopbackTest < BaseTestCase
def setup
@model = new_model do
attribute :status, :string
def save
if valid?
changes_applied
true
else
false
end
end
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
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 00000001234 15160070525 0036234 0 ustar 00root root 0000000 0000000 state-machines-state_machines-activemodel-eff468b/test # frozen_string_literal: true
require 'test_helper'
class MachineWithDirtyAttributeAndStateEventsTest < BaseTestCase
def setup
@model = new_model do
def save
if valid?
changes_applied
true
else
false
end
end
end
@machine = StateMachines::Machine.new(@model, action: :save, 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 00000002070 15160070525 0037307 0 ustar 00root root 0000000 0000000 state-machines-state_machines-activemodel-eff468b/test # frozen_string_literal: true
require 'test_helper'
class MachineWithDirtyAttributesAndCustomAttributeTest < BaseTestCase
def setup
@model = new_model do
attribute :status, :string
def save
if valid?
changes_applied
true
else
false
end
end
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
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
assert_equal %w[parked idling], @record.changes['status']
end
end
machine_with_dirty_attributes_during_loopback_test.rb 0000664 0000000 0000000 00000001337 15160070525 0036237 0 ustar 00root root 0000000 0000000 state-machines-state_machines-activemodel-eff468b/test # frozen_string_literal: true
require 'test_helper'
class MachineWithDirtyAttributesDuringLoopbackTest < BaseTestCase
def setup
@model = new_model do
def save
if valid?
changes_applied
true
else
false
end
end
end
@machine = StateMachines::Machine.new(@model, initial: :parked)
@machine.event :park
@record = @model.create
@transition = StateMachines::Transition.new(@record, @machine, :park, :parked, :parked)
@transition.perform
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-activemodel-eff468b/test/machine_with_dirty_attributes_test.rb 0000664 0000000 0000000 00000001770 15160070525 0033075 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require 'test_helper'
class MachineWithDirtyAttributesTest < BaseTestCase
def setup
@model = new_model do
def save
if valid?
changes_applied
true
else
false
end
end
end
@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
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
assert_equal %w[parked idling], @record.changes['state']
end
end
state-machines-state_machines-activemodel-eff468b/test/machine_with_dynamic_initial_state_test.rb 0000664 0000000 0000000 00000000650 15160070525 0034025 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require 'test_helper'
class MachineWithDynamicInitialStateTest < BaseTestCase
def setup
@model = new_model
@machine = StateMachines::Machine.new(@model, initial: lambda { |_object| :parked }, integration: :active_model)
@machine.state :parked
end
def test_should_set_initial_state_on_created_object
record = @model.new
assert_equal 'parked', record.state
end
end
state-machines-state_machines-activemodel-eff468b/test/machine_with_events_test.rb 0000664 0000000 0000000 00000000506 15160070525 0030774 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require '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-activemodel-eff468b/test/machine_with_failed_after_callbacks_test.rb 0000664 0000000 0000000 00000001744 15160070525 0034101 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require 'test_helper'
class MachineWithFailedAfterCallbacksTest < BaseTestCase
def setup
@callbacks = []
@model = new_model
@machine = StateMachines::Machine.new(@model, integration: :active_model)
@machine.state :parked, :idling
@machine.event :ignite
@machine.after_transition { @callbacks << :after_1; false }
@machine.after_transition { @callbacks << :after_2 }
@machine.around_transition { |block| @callbacks << :around_before; block.call; @callbacks << :around_after }
@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_not_run_further_after_callbacks
assert_equal [:around_before, :around_after, :after_1], @callbacks
end
end
state-machines-state_machines-activemodel-eff468b/test/machine_with_failed_before_callbacks_test.rb 0000664 0000000 0000000 00000002004 15160070525 0034230 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require 'test_helper'
class MachineWithFailedBeforeCallbacksTest < BaseTestCase
def setup
@callbacks = []
@model = new_model
@machine = StateMachines::Machine.new(@model, integration: :active_model)
@machine.state :parked, :idling
@machine.event :ignite
@machine.before_transition { @callbacks << :before_1; false }
@machine.before_transition { @callbacks << :before_2 }
@machine.after_transition { @callbacks << :after }
@machine.around_transition { |block| @callbacks << :around_before; block.call; @callbacks << :around_after }
@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_further_callbacks
assert_equal [:before_1], @callbacks
end
end
machine_with_initialized_aliased_attribute_test.rb 0000664 0000000 0000000 00000002271 15160070525 0035464 0 ustar 00root root 0000000 0000000 state-machines-state_machines-activemodel-eff468b/test # frozen_string_literal: true
require 'test_helper'
class MachineWithInitializedAliasedAttributeTest < BaseTestCase
def test_should_match_original_attribute_value_with_attribute_methods
model = new_model do
include ActiveModel::AttributeMethods
alias_attribute :custom_status, :state
end
machine = StateMachines::Machine.new(model, initial: :parked, integration: :active_model)
machine.other_states(:started)
record = model.new(custom_status: 'started')
refute record.state?(:parked)
assert record.state?(:started)
end
def test_should_not_match_original_attribute_value_without_attribute_methods
model = new_plain_model do
include ActiveModel::Model
attr_accessor :state
def self.alias_attribute(new_name, old_name)
alias_method new_name, old_name
alias_method "#{new_name}=", "#{old_name}="
end
alias_attribute :custom_status, :state
end
machine = StateMachines::Machine.new(model, initial: :parked, integration: :active_model)
machine.other_states(:started)
record = model.new(custom_status: 'started')
assert record.state?(:parked)
refute record.state?(:started)
end
end
state-machines-state_machines-activemodel-eff468b/test/machine_with_initialized_state_test.rb 0000664 0000000 0000000 00000001677 15160070525 0033207 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require 'test_helper'
class MachineWithInitializedStateTest < BaseTestCase
def setup
@model = new_model
@machine = StateMachines::Machine.new(@model, initial: :parked, integration: :active_model)
@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_dynamic
@machine.initial_state = -> { :parked }
record = @model.new(state: 'idling')
assert_equal 'idling', record.state
end
end
state-machines-state_machines-activemodel-eff468b/test/machine_with_internationalization_test.rb 0000664 0000000 0000000 00000013756 15160070525 0033750 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require 'test_helper'
require 'i18n'
class MachineWithInternationalizationTest < BaseTestCase
def setup
I18n.backend = I18n::Backend::Simple.new
@model = new_model { include ActiveModel::Validations }
end
def test_should_use_defaults
I18n.backend.store_translations(:en,
activemodel: { errors: { messages: { invalid_transition: 'cannot %{event}' } } }
)
machine = StateMachines::Machine.new(@model, action: :save)
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,
activemodel: { errors: { messages: { bad_transition: 'cannot %{event}' } } }
)
machine = StateMachines::Machine.new(@model, action: :save, messages: { invalid_transition: :bad_transition })
machine.state :parked, :idling
record = @model.new
record.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, action: :save, messages: { invalid_transition: 'cannot %{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,
activemodel: { state_machines: { :'foo' => { state: { states: { parked: 'shutdown' } } } } }
)
machine = StateMachines::Machine.new(@model)
machine.state :parked
assert_equal 'shutdown', machine.state(:parked).human_name(@model)
end
def test_should_allow_customized_state_key_scoped_to_class
I18n.backend.store_translations(:en,
activemodel: { state_machines: { :'foo' => { states: { parked: 'shutdown' } } } }
)
machine = StateMachines::Machine.new(@model)
machine.state :parked
assert_equal 'shutdown', machine.state(:parked).human_name(@model)
end
def test_should_allow_customized_state_key_scoped_to_machine
I18n.backend.store_translations(:en,
activemodel: { state_machines: { state: { states: { parked: 'shutdown' } } } }
)
machine = StateMachines::Machine.new(@model)
machine.state :parked
assert_equal 'shutdown', machine.state(:parked).human_name(@model)
end
def test_should_allow_customized_state_key_unscoped
I18n.backend.store_translations(:en,
activemodel: { state_machines: { states: { parked: 'shutdown' } } }
)
machine = StateMachines::Machine.new(@model)
machine.state :parked
assert_equal 'shutdown', machine.state(:parked).human_name(@model)
end
def test_should_support_nil_state_key
I18n.backend.store_translations(:en,
activemodel: { state_machines: { states: { nil: 'empty' } } }
)
machine = StateMachines::Machine.new(@model)
assert_equal 'empty', machine.state(nil).human_name(@model)
end
def test_should_allow_customized_event_key_scoped_to_class_and_machine
I18n.backend.store_translations(:en,
activemodel: { state_machines: { :'foo' => { state: { events: { park: 'stop' } } } } }
)
machine = StateMachines::Machine.new(@model)
machine.event :park
assert_equal 'stop', machine.event(:park).human_name(@model)
end
def test_should_allow_customized_event_key_scoped_to_class
I18n.backend.store_translations(:en,
activemodel: { state_machines: { :'foo' => { events: { park: 'stop' } } } }
)
machine = StateMachines::Machine.new(@model)
machine.event :park
assert_equal 'stop', machine.event(:park).human_name(@model)
end
def test_should_allow_customized_event_key_scoped_to_machine
I18n.backend.store_translations(:en,
activemodel: { state_machines: { state: { events: { park: 'stop' } } } }
)
machine = StateMachines::Machine.new(@model)
machine.event :park
assert_equal 'stop', machine.event(:park).human_name(@model)
end
def test_should_allow_customized_event_key_unscoped
I18n.backend.store_translations(:en,
activemodel: { state_machines: { events: { park: 'stop' } } }
)
machine = StateMachines::Machine.new(@model)
machine.event :park
assert_equal 'stop', machine.event(:park).human_name(@model)
end
def test_should_have_locale_once_in_load_path
assert_equal 1, I18n.load_path.select { |path| path =~ %r{active_model/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 ignite'], record.errors.full_messages
ensure
I18n.load_path = @original_load_path
end
end
state-machines-state_machines-activemodel-eff468b/test/machine_with_model_state_attribute_test.rb 0000664 0000000 0000000 00000001607 15160070525 0034056 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require 'test_helper'
class MachineWithModelStateAttributeTest < BaseTestCase
def setup
@model = new_model
@machine = StateMachines::Machine.new(@model, initial: :parked, integration: :active_model)
@machine.other_states(:idling)
@record = @model.new
end
def test_should_have_an_attribute_predicate
assert @record.respond_to?(:state?)
end
def test_should_raise_exception_for_predicate_without_parameters
assert_raises(ArgumentError) { @record.state? }
end
def test_should_return_false_for_predicate_if_does_not_match_current_value
assert !@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_non_model_state_attribute_undefined_test.rb 0000664 0000000 0000000 00000001275 15160070525 0036673 0 ustar 00root root 0000000 0000000 state-machines-state_machines-activemodel-eff468b/test # frozen_string_literal: true
require 'test_helper'
class MachineWithNonModelStateAttributeUndefinedTest < BaseTestCase
def setup
@model = new_plain_model do
def initialize; end
end
@machine = StateMachines::Machine.new(@model, :status, initial: :parked, integration: :active_model)
@machine.other_states(:idling)
@record = @model.new
end
def test_should_not_define_a_reader_attribute_for_the_attribute
assert !@record.respond_to?(:status)
end
def test_should_not_define_a_writer_attribute_for_the_attribute
assert !@record.respond_to?(:status=)
end
def test_should_define_an_attribute_predicate
assert @record.respond_to?(:status?)
end
end
state-machines-state_machines-activemodel-eff468b/test/machine_with_state_driven_validations_test.rb0000664 0000000 0000000 00000001604 15160070525 0034554 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require 'test_helper'
class MachineWithStateDrivenValidationsTest < BaseTestCase
def setup
@model = new_model do
include ActiveModel::Validations
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)
assert !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-activemodel-eff468b/test/machine_with_states_test.rb 0000664 0000000 0000000 00000000514 15160070525 0030772 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require '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-activemodel-eff468b/test/machine_with_static_initial_state_test.rb 0000664 0000000 0000000 00000000567 15160070525 0033677 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require 'test_helper'
class MachineWithStaticInitialStateTest < BaseTestCase
def setup
@model = new_model
@machine = StateMachines::Machine.new(@model, initial: :parked, integration: :active_model)
end
def test_should_set_initial_state_on_created_object
record = @model.new
assert_equal 'parked', record.state
end
end
machine_with_validations_and_custom_attribute_test.rb 0000664 0000000 0000000 00000001107 15160070525 0036223 0 ustar 00root root 0000000 0000000 state-machines-state_machines-activemodel-eff468b/test # frozen_string_literal: true
require 'test_helper'
class MachineWithValidationsAndCustomAttributeTest < BaseTestCase
def setup
@model = new_model { include ActiveModel::Validations }
@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'
assert !@record.valid?
assert_equal ['State is invalid'], @record.errors.full_messages
@record.state = 'parked'
assert @record.valid?
end
end
state-machines-state_machines-activemodel-eff468b/test/machine_with_validations_test.rb 0000664 0000000 0000000 00000002443 15160070525 0032007 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require 'test_helper'
class MachineWithValidationsTest < BaseTestCase
def setup
@model = new_model { include ActiveModel::Validations }
@machine = StateMachines::Machine.new(@model, action: :save)
@machine.state :parked
@record = @model.new
end
def test_should_invalidate_using_errors
I18n.backend = I18n::Backend::Simple.new if Object.const_defined?(:I18n)
@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'
assert !@record.valid?
assert_equal ['State is invalid'], @record.errors.full_messages
end
end
state-machines-state_machines-activemodel-eff468b/test/state_human_name_test.rb 0000664 0000000 0000000 00000013145 15160070525 0030264 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require_relative 'test_helper'
class StateHumanNameTest < BaseTestCase
def setup
@model = new_model do
include ActiveModel::Validations
attr_accessor :status
end
end
def test_should_allow_custom_human_name_on_state
machine = StateMachines::Machine.new(@model, :status, initial: :pending) do
state :pending, human_name: 'Awaiting Approval'
state :approved
state :rejected, human_name: 'Denied'
end
assert_equal 'Awaiting Approval', machine.states[:pending].human_name(@model)
assert_equal 'Denied', machine.states[:rejected].human_name(@model)
end
def test_should_not_override_custom_human_name_with_translation
# Set up I18n translations
I18n.backend.store_translations(:en, {
activemodel: {
state_machines: {
states: {
pending: 'Translation for Pending',
approved: 'Translation for Approved',
rejected: 'Translation for Rejected'
}
}
}
})
machine = StateMachines::Machine.new(@model, :status, initial: :pending) do
state :pending, human_name: 'Custom Pending Name'
state :approved
state :rejected, human_name: 'Custom Rejected Name'
end
# Custom human names should be preserved
assert_equal 'Custom Pending Name', machine.states[:pending].human_name(@model)
assert_equal 'Custom Rejected Name', machine.states[:rejected].human_name(@model)
# State without custom human_name gets default behavior (which might not use translations in this test setup)
# The key test is that custom human names are preserved, not overwritten
refute_equal 'Custom Pending Name', machine.states[:approved].human_name(@model)
end
def test_should_allow_custom_human_name_as_string
machine = StateMachines::Machine.new(@model, :status) do
state :active, human_name: 'Currently Active'
end
assert_equal 'Currently Active', machine.states[:active].human_name(@model)
end
def test_should_allow_custom_human_name_as_lambda
machine = StateMachines::Machine.new(@model, :status) do
state :processing, human_name: ->(state, klass) { "#{klass.name} is #{state.name.to_s.upcase}" }
end
assert_equal 'Foo is PROCESSING', machine.states[:processing].human_name(@model)
end
def test_should_use_default_translation_when_no_custom_human_name
machine = StateMachines::Machine.new(@model, :status) do
state :idle
end
# Should fall back to humanized version when no translation exists
assert_equal 'idle', machine.states[:idle].human_name(@model)
end
def test_should_handle_nil_human_name
machine = StateMachines::Machine.new(@model, :status) do
state :waiting
end
# Explicitly set to nil (should still get default behavior)
machine.states[:waiting].human_name = nil
# When human_name is nil, State#human_name returns nil
assert_nil machine.states[:waiting].human_name(@model)
end
def test_should_preserve_human_name_through_multiple_state_definitions
machine = StateMachines::Machine.new(@model, :status)
# First define state with custom human name
machine.state :draft, human_name: 'Work in Progress'
# Redefine the same state (this should not override the human_name)
machine.state :draft do
# Add some behavior
end
assert_equal 'Work in Progress', machine.states[:draft].human_name(@model)
end
def test_should_work_with_state_machine_helper_method
@model.class_eval do
state_machine :status, initial: :pending do
state :pending, human_name: 'Awaiting Review'
state :reviewed
end
end
machine = @model.state_machine(:status)
assert_equal 'Awaiting Review', machine.states[:pending].human_name(@model)
end
def test_should_handle_complex_i18n_lookup_with_custom_human_name
# Set up complex I18n structure
I18n.backend.store_translations(:en, {
activemodel: {
state_machines: {
foo: {
status: {
states: {
pending: 'Model Specific Pending'
}
}
},
status: {
states: {
pending: 'Machine Specific Pending'
}
},
states: {
pending: 'Generic Pending'
}
}
}
})
machine = StateMachines::Machine.new(@model, :status) do
state :pending, human_name: 'Overridden Pending'
end
# Should use the custom human_name, not any of the I18n translations
assert_equal 'Overridden Pending', machine.states[:pending].human_name(@model)
end
def teardown
# Clear I18n translations after each test
I18n.backend.reload!
end
end
state-machines-state_machines-activemodel-eff468b/test/test_helper.rb 0000664 0000000 0000000 00000002020 15160070525 0026221 0 ustar 00root root 0000000 0000000 # frozen_string_literal: true
require 'debug'
require 'state_machines-activemodel'
require 'minitest/autorun'
require 'minitest/reporters'
require 'active_support/all'
Minitest::Reporters.use! [Minitest::Reporters::ProgressReporter.new]
I18n.enforce_available_locales = true
class BaseTestCase < ActiveSupport::TestCase
protected
# Creates a plain model without ActiveModel features
def new_plain_model(&block)
model = Class.new do
def self.name
'Foo'
end
end
model.class_eval(&block) if block_given?
model
end
# Creates a new ActiveModel model (and the associated table)
def new_model(&block)
model = Class.new do
include ActiveModel::Model
include ActiveModel::Attributes
include ActiveModel::Dirty
attribute :state, :string
def self.name
'Foo'
end
def self.create
new.tap { |instance| instance.save if instance.respond_to?(:save) }
end
end
model.class_eval(&block) if block_given?
model
end
end