Friday, October 10, 2025
HomeRuby On RailsExchange aasm with Rails Enum right this moment

Exchange aasm with Rails Enum right this moment


There’s an important probability that your Rails app incorporates one of many gems offering so known as state machine implementation. There’s occasion a higher probability that it will likely be aasm previously generally known as acts_as_state_machine. Btw. Who remembers acts_as_hasselhoff? — okay, boomer. The aasm does quite a bit when included into your ActiveRecord mannequin — the query is do you actually need all these issues?

My drawback with aasm

I used to be struck by reckless use of this gem so many instances that very first thing I do after becoming a member of a brand new challenge is working cat Gemfile | grep aasm and right here comes the meme which I made ~1.5 years in the past:

My foremost concern with use of this gem is that you just in all probability don’t want all of the options it provides. Extra options means extra temptation to make use of them. Higher use throughout the codebase means extra coupling to exterior library which may be incompatible with upcoming Rails variations, blocking you from improve or making it fairly pricey. It’s a must to learn one more changelog to verify if there aren’t any breaking modifications or some refined conduct change working your critical enterprise software into issues.

Subsequent factor is that it promotes patterns like callbacks which I personally see as an enormous drawback inside complicated rails functions. I desire specific code, callbacks aren’t such. Ackchyually callbacks are part of the framework, I do know, however let’s depart this dialogue for an additional place in time.

It’s additionally wrestle for all of the IDEs, Language Server Suppliers to seek out definitions of strategies outlined by aasm’s DSL and you might be compelled to make use of grep, learn the code rigorously and resolve whether or not these energetic! technique comes from or the place my pending scope is outlined.

I’ve lately realized that it additionally autogenerates constants for every state so I don’t should. I’ve lately noticed Transaction::STATUS_FAILED someplace, it took fairly a while to determine the origin of this fixed which wasn’t explicitly outlined throughout the Transaction class.

These free lunches may be tasty, however ultimately you can be compelled to pay for them.

Final, however not least, having attribute like state or standing usually recommend poor design in your codebase, however that’s a very completely different story.

Place to begin

9 years previous legacy Rails app backing very profitable enterprise. Let’s take a look at a number of the particulars:

-> ruby -v
ruby 3.3.0 (2023-12-25 revision 5124f9ac75)

-> bin/rails r "p Rails.model"
"7.1.3.2"

-> bundle record | wc -l
319

-> rg embrace AASM -l | wc -l
33

Few makes use of of mannequin.aasm.states.map(&:identify)

Two makes use of of SwitchRequest.aasm.states_for_select to offer choices for some <choose> tags.

Rails, the white knight

As a Rails developer, you’re in all probability aware of enum which was launched in Rails 6.0 and allowed to declare an attribute the place the values map to integers within the database. It advanced a bit with subsequent framework variations, however Rails 7.1 lastly introduced all of the options required to switch all the aasm makes use of within the codebase I’m presently engaged on.

Let’s make use of this straightforward class for example for our additional work:

class Transaction < ApplicationRecord
  embrace AASM

  PROCESSING = :processing
  FAILED = :failed
  SUCCESSFUL = :profitable
  CANCELED = :canceled

  aasm column: :standing do
    state PROCESSING, preliminary: true
    state FAILED
    state SUCCESSFUL
    state CANCELED
  finish
finish

What’s required to get actual the identical conduct

  • Scopes like Transaction.profitable to question all of the transactions having standing profitable — we will have these at no cost from ActiveRecord::Enum.
  • Occasion strategies to:
    • verify whether or not our object’s standing is profitable?enum acquired you lined
    • change the standing to profitable and run save! as we acquired used to it — identical right here, enum will do it’s job right here in the event you name transaction.profitable!
  • Set desired preliminary state for brand spanking new objects — this may be executed by offering default key phrase argument to enum technique, like default: PROCESSING.
  • Values have to be saved as strings, not as integers within the db — it’s potential since Rails 7.0
  • Hopefully there have been no transitions or guards in state machines in our software — but another excuse to not make use of aasm — however I can picture implementing it with ActiveRecord::Soiled fairly straightforward. And even higher, implement this conduct as a better stage abstracion and preserve you mannequin dummy on this matter.
  • No callbacks both, yay!
  • Constants containing all of the potential states had been already in place, so these outlined by aasm, like STATUS_PROCESSING, STATUS_SUCCESSFUL and so forth had been one thing no one requested for.

Validation of the offered worth

There’s slight distinction in default enum conduct. If the offered a worth doesn’t match specified values, you can be struck with ArgumentError when making an attempt to assign one.

As talked about earlier, in Rails 7.1 there’s a chance to offer validate: true key phrase argument. Enum will behave precisely the identical as aasm on this method which checks the validity of offered worth earlier than save leading to ActiveRecord::RecordInvalid as a substitute.

Present me the code

class Transaction < ApplicationRecord
    PROCESSING = :processing
    FAILED = :failed
    SUCCESSFUL = :profitable
    CANCELED = :canceled

    enum :standing,
         { processing: PROCESSING,
           failed: FAILED,
           profitable: SUCCESSFUL,
           canceled: CANCELED
         }.transform_values(&:to_s),
         default: PROCESSING.to_s,
         validate: true
finish

That’s it. Give up clear and comprehensible, isn’t it?

The values quirk

There’s one quirk, you’ve in all probability already observed: transform_values(&:to_s). I used to be fairly confused what’s occurring once I’ve offered symbols as values initially. Let’s see how the code regarded like earlier than:

class Transaction < ApplicationRecord
    PROCESSING = :processing
    FAILED = :failed
    SUCCESSFUL = :profitable
    CANCELED = :canceled

    enum :standing,
         { processing: PROCESSING,
           failed: FAILED,
           profitable: SUCCESSFUL,
           canceled: CANCELED
         },
         default: PROCESSING,
         validate: true
finish

The documentation hasn’t clearly acknowledged that values ought to be strings. The instance used String values, however there was no clear expectation about that. Nevertheless, when saving the thing, standing grew to become nil in some unspecified time in the future, regardless of assigning ”profitable” worth.

I wrote a dummy app and take a look at for this particular case apart from the primary software I used to be engaged on. To be 100% certain that there’s no different issue influences this conduct:

require 'bundler/inline'

gemfile do
  supply 'https://rubygems.org'

  gem 'activerecord'
  gem 'sqlite3'
  gem 'minitest'
finish

require 'active_record'
require 'sqlite3'
require 'minitest/autorun'

start
  db_name = 'enum_test.sqlite3'.freeze

  SQLite3::Database.new(db_name)
  ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: db_name)
  ActiveRecord::Schema.outline do
    create_table :transactions, drive: true do |t|
      t.string :standing
    finish
  finish

  class Transaction < ActiveRecord::Base
    PROCESSING = :processing
    FAILED = :failed
    SUCCESSFUL = :profitable
    CANCELED = :canceled

    enum :standing,
         { processing: PROCESSING,
           failed: FAILED,
           profitable: SUCCESSFUL,
           canceled: CANCELED
         },
         default: PROCESSING,
         validate: true
  finish

  class TestTransaction < Minitest::Check
    def test_enum_behavior
      transaction = Transaction.new(standing: :profitable)
      assert_equal 'profitable', transaction.standing

      transaction.save!
      assert_equal 'profitable', transaction.reload.standing
    finish
  finish
guarantee
  Dir.glob(db_name + '*').every  File.delete(file) 
finish

Consequence:

ruby enum.rb
-- create_table(:transactions, {:drive=>true})
   -> 0.0076s
Run choices: --seed 3038

# Working:

F

Completed in 0.013456s, 74.3163 runs/s, 148.6326 assertions/s.

  1) Failure:
TestTransaction#test_enum [enum.rb:44]:
Anticipated: "profitable"
  Precise: nil

1 runs, 2 assertions, 1 failures, 0 errors, 0 skips

Okay, so the worth is appropriately set, nevertheless it disappears when save! known as. After fast session with debugger I used to be in a position to determine that undesired change from "profitable" to nil occurs inside Enum::EnumType#deserialize technique. Generic ActiveModel::Sort::Worth class which is a dad or mum for the ActiveRecord::Enum::EnumType describes the aim of deserialize technique like that:

Converts a worth from database enter to the suitable ruby sort. The return worth of this technique will probably be returned from ActiveRecord::AttributeMethods::Learn#read_attribute. The default implementation simply calls Worth#forged.

Let’s have a have a look at our particular state of affairs:

# worth = "profitable"
# mapping = ActiveSupport::HashWithIndifferentAccess.new({
#  "processing" => :processing,
#  "failed" => :failed,
#  "profitable" => :profitable,
#  "canceled" => :canceled,
# })
# subtype = ActiveModel::Sort::String occasion

def deserialize(worth)
  mapping.key(subtype.deserialize(worth))
finish

What precisely occurs:

  1. subtype.deserialize(worth) returns ”profitable”
  2. mapping is requested to return a key for ”profitable” worth, however there isn’t any such within the hash, there’s a Image :succesful — yikes
  3. deserialize("profitable") returns nil as a substitute of "profitable"

In all probability a single line within the docs like: Don’t put symbols as values when defining the enum would do the job and save my time and probably many different confused builders. What’s much more puzzling is the truth that you’ll be able to present default worth as a Image, you’ll be able to assign a worth which is a Image and it will likely be saved as a String, which is comprehensible, however the definition has to comprise string values — therefore the .transform_values(&:to_s) trick.

Nevertheless, why use symbols to outline potential state?, it’s possible you’ll ask. As a result of it’s a requirement of aasm. State must be Image or object responding to #identify technique. If you happen to present String, you’ll see good NoMethodError as a result of it doesn’t reply to identify.

what’s even funnier? Database returns String when requested for Transactions#standing. Serializing it to JSON can even flip it into String as JSON doesn’t implement symbols. I may think about extra eventualities when fixed casting from Image to String forwards and backwards occur with none explicit purpose. However that’s precisely how third celebration gem (AASM) drives your architectural selections.

We are able to do even higher

If you happen to aren’t utilizing scopes, you’ll be able to merely disable them with scopes: false.

Similar goes as an example strategies like succesful! or failed?, these may be disabled with instance_methods: false.

Many of the fashions that was rewritten required each, however at any time when it was potential I disabled each of the options.

Occurrences of Mannequin.aasm.states.map(&:identify) are replaceable by Mannequin.statuses.values.

Mannequin.aasm.states_for_select helper may be changed with Mannequin.statuses.values.map [I18n.l(name), value] . Extract this to one in every of your helpers.
Possibly you don’t must name I18n in any respect and easy humanize will probably be sufficient.

Now it’s your flip.



RELATED ARTICLES

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Most Popular

Recent Comments