Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 14 additions & 9 deletions lib/tapioca/dsl/compilers/aasm.rb
Original file line number Diff line number Diff line change
Expand Up @@ -94,14 +94,19 @@ def decorate
create_rest_param("opts", type: "T.untyped"),
create_block_param("block", type: "T.nilable(T.proc.void)"),
]
aasm_6_or_newer = ::Gem::Requirement.new(">= 6").satisfied_by?(::Gem::Version.new(::AASM::VERSION))

state_machine.events.each do |event|
model.create_method(event.name.to_s, parameters: parameters)
model.create_method("#{event.name}!", parameters: parameters)
model.create_method("#{event.name}_without_validation!", parameters: parameters)
model.create_method("may_#{event.name}?", return_type: "T::Boolean")
# As of aasm 6.0 a namespaced state machine defines only the
# namespaced event methods (e.g. `run_foo!`) and not the plain
# methods (`run!`) as aliases.
unless namespace && aasm_6_or_newer
model.create_method(event.name.to_s, parameters: parameters)
model.create_method("#{event.name}!", parameters: parameters)
model.create_method("#{event.name}_without_validation!", parameters: parameters)
model.create_method("may_#{event.name}?", return_type: "T::Boolean")
end

# For events, if there's a namespace the default methods are created in addition to
# namespaced ones.
next unless namespace

name = "#{event.name}_#{namespace}"
Expand All @@ -110,9 +115,9 @@ def decorate
model.create_method("#{name}!", parameters: parameters)
model.create_method("may_#{name}?", return_type: "T::Boolean")

# There's no namespaced method created for `_without_validation`, so skip
# defining a method for:
# "#{name}_without_validation!"
# aasm < 6 does not define a namespaced `_without_validation!`
# method.
model.create_method("#{name}_without_validation!", parameters: parameters) if aasm_6_or_newer
end
end

Expand Down
5 changes: 5 additions & 0 deletions lib/tapioca/helpers/test/template.rb
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,11 @@ def rails_version(selector)
::Gem::Requirement.new(selector).satisfied_by?(ActiveSupport.gem_version)
end

#: (String selector) -> bool
def aasm_version(selector)
::Gem::Requirement.new(selector).satisfied_by?(::Gem::Version.new(::AASM::VERSION))
end

#: (String src, ?trim_mode: String) -> String
def template(src, trim_mode: ">")
erb = ::ERB.new(src, trim_mode: trim_mode)
Expand Down
36 changes: 34 additions & 2 deletions spec/tapioca/dsl/compilers/aasm_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,17 @@ def before_setup
require "aasm"
end

# Runs the block with `AASM::VERSION` stubbed, so both the aasm < 6 and
# aasm >= 6 code paths can be exercised regardless of the installed version.
#: (String version) { -> void } -> void
def with_aasm_version(version, &block)
original = ::AASM::VERSION
::Tapioca::Runtime.silence_warnings { ::AASM.const_set(:VERSION, version) }
block.call
ensure
::Tapioca::Runtime.silence_warnings { ::AASM.const_set(:VERSION, original) }
end

describe "initialize" do
it "gathers no constants if there are no classes that include AASM" do
assert_empty(gathered_constants)
Expand Down Expand Up @@ -345,7 +356,7 @@ def before_run; end
end
RUBY

expected = <<~RBI
rbi = <<~RBI
# typed: strong

class StateMachine
Expand All @@ -358,6 +369,19 @@ def foo_running?; end
sig { returns(T::Boolean) }
def foo_sleeping?; end

<% if aasm_version(">= 6") %>
sig { returns(T::Boolean) }
def may_run_foo?; end

sig { params(opts: T.untyped, block: T.nilable(T.proc.void)).returns(T.untyped) }
def run_foo(*opts, &block); end

sig { params(opts: T.untyped, block: T.nilable(T.proc.void)).returns(T.untyped) }
def run_foo!(*opts, &block); end

sig { params(opts: T.untyped, block: T.nilable(T.proc.void)).returns(T.untyped) }
def run_foo_without_validation!(*opts, &block); end

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the PR. Since this version is brand new, I'd prefer if we kept older tests, we have a similar pattern in here. It is then executed as ERB in here.

I think you can define a similar helper in this file:

def aasm_version(selector)
  ::Gem::Requirement.new(selector).satisfied_by?(::Gem::Version.new(::AASM::VERSION))
end

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, perfect! Thanks for linking those. Updated!

@allan-thatch allan-thatch Jul 21, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm realizing now that there's no tooling currently (like Rails in the CI matrix) that actually installs and drives the test suite for the older version of aasm. I'll push up a commit to stub this out in tests vs. adding another variant to the test matrix (this seemed heavy, and not quite on the same level as Ruby/Rails). I've included sample implementations for both below, so LMK if you'd prefer the other!

Test matrix
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index cc6424c9..7f487af6 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -44,6 +44,7 @@ jobs:
         ruby: ["3.2", "3.3", "3.4", "4.0","head"]
         rails: ["8.0", "current", "main"]
         rubygems: ["4.0.10"]
+        aasm: ["current"]
         exclude:
           - ruby: "3.2"
             rails: "main"
@@ -52,9 +53,13 @@ jobs:
             experimental: true
           - ruby: "head"
             experimental: true
-    name: Ruby ${{ matrix.ruby }} - Rails ${{ matrix.rails }} - RubyGems ${{ matrix.rubygems }}
+          - ruby: "3.4"
+            rails: "current"
+            aasm: "5.5"
+    name: Ruby ${{ matrix.ruby }} - Rails ${{ matrix.rails }} - RubyGems ${{ matrix.rubygems }} - AASM ${{ matrix.aasm }}
     env:
       RAILS_VERSION: ${{ matrix.rails }}
+      AASM_VERSION: ${{ matrix.aasm }}
     steps:
       - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
       - name: Install protobuf
diff --git a/Gemfile b/Gemfile
index 43b343c3..83e695fa 100644
--- a/Gemfile
+++ b/Gemfile
@@ -7,6 +7,8 @@ gemspec
 CURRENT_RAILS_VERSION = "8.1"
 rails_version = ENV.fetch("RAILS_VERSION", CURRENT_RAILS_VERSION)
 
+aasm_version = ENV.fetch("AASM_VERSION", "current")
+
 gem "minitest"
 gem "minitest-hooks"
 gem "minitest-reporters"
@@ -52,7 +54,11 @@ group :development, :test do
   gem "sidekiq"
   gem "nokogiri"
   gem "config"
-  gem "aasm"
+  if aasm_version == "current"
+    gem "aasm"
+  else
+    gem "aasm", "~> #{aasm_version}"
+  end
   gem "bcrypt"
   gem "xpath"
   gem "kredis"
Test stub/helper
diff --git a/spec/tapioca/dsl/compilers/aasm_spec.rb b/spec/tapioca/dsl/compilers/aasm_spec.rb
index 52aebda2..d6df99e2 100644
--- a/spec/tapioca/dsl/compilers/aasm_spec.rb
+++ b/spec/tapioca/dsl/compilers/aasm_spec.rb
@@ -14,6 +14,17 @@ module Tapioca
             require "aasm"
           end
 
+          # Runs the block with `AASM::VERSION` stubbed, so both the aasm < 6 and
+          # aasm >= 6 code paths can be exercised regardless of the installed version.
+          #: (String version) { -> void } -> void
+          def with_aasm_version(version, &block)
+            original = ::AASM::VERSION
+            ::Tapioca::Runtime.silence_warnings { ::AASM.const_set(:VERSION, version) }
+            block.call
+          ensure
+            ::Tapioca::Runtime.silence_warnings { ::AASM.const_set(:VERSION, original) }
+          end
+
           describe "initialize" do
             it "gathers no constants if there are no classes that include AASM" do
               assert_empty(gathered_constants)
@@ -345,7 +356,7 @@ module Tapioca
                 end
               RUBY
 
-              expected = template(<<~RBI)
+              rbi = <<~RBI
                 # typed: strong
 
                 class StateMachine
@@ -476,7 +487,14 @@ module Tapioca
                 end
               RBI
 
-              assert_equal(expected, rbi_for(:StateMachine))
+              # Assert against the currently installed aasm version.
+              assert_equal(template(rbi), rbi_for(:StateMachine))
+
+              # Also exercise the aasm < 6 code path, which no longer matches the
+              # installed version once aasm >= 6 is in use.
+              with_aasm_version("5.5.2") do
+                assert_equal(template(rbi), rbi_for(:StateMachine))
+              end
             end
           end
         end

<% else %>
sig { returns(T::Boolean) }
def may_run?; end

Expand All @@ -378,6 +402,7 @@ def run_foo!(*opts, &block); end

sig { params(opts: T.untyped, block: T.nilable(T.proc.void)).returns(T.untyped) }
def run_without_validation!(*opts, &block); end
<% end %>

class << self
sig { params(args: T.untyped, block: T.nilable(T.proc.bind(PrivateAASMMachine).void)).returns(PrivateAASMMachine) }
Expand Down Expand Up @@ -462,7 +487,14 @@ def success(&block); end
end
RBI

assert_equal(expected, rbi_for(:StateMachine))
# Assert against the currently installed aasm version.
assert_equal(template(rbi), rbi_for(:StateMachine))

# Also exercise the aasm < 6 code path, which no longer matches the
# installed version once aasm >= 6 is in use.
with_aasm_version("5.5.2") do
assert_equal(template(rbi), rbi_for(:StateMachine))
end
end
end
end
Expand Down
Loading