RSpec.describe Order, type: :model do
describe "enum" do
it "to_sym enables comparison" do
o1 = create(:order)
expect(o1.status.to_sym).to eq :building
end
it "doesn't like direct comparison" do
o1 = create(:order)
expect(o1.status).to eq :building
end
end
end
just double checked with the above. Test 2 fails.
perhaps this is a 5 vs 6 thing
I did try to upgrade to 6, but just got lost in the thicket of failing dependencies and lack of any clear guide (that I could find) showing what I needed to know about webpacker
Whatever what value I assign, enums values are casted to String (or AS::StringInquirer rather).
But your test shows that you expect the attribute to be a symbol. It’s not, it never is, it’s a string, always.
That’s probably where the confusion lies. Maybe AS::StringInquirer.new("foo") == :foo should work though, or maybe it should throw an error to tell you not to compare with symbols. I can see how this is a gotcha.
Yeah I get it. Since you use symbols to define the possible values, you expect the model attribute to give you a Symbol. But actually it returns a String.
It’s not a bug per says, as it does what the documentation says.
But I do understand how it doesn’t fit with your expectations. It’s an interesting feedback, and thanks for going through with me to help me understand what your problem was.
just to confirm - I built this on 6.03 and get the same behaviour
require 'rails_helper'
RSpec.describe User, type: :model do
describe "enums" do
it "gives normal method" do
create_user
expect(@user.normal?).to be true
end
it "compares with a sym" do
create_user
expect(@user.user_type.to_sym).to eq :normal
end
#this test passes - which seems crazy
it "doesn't compare directly" do
create_user
expect(@user.user_type).not_to eq :normal
end
end
def create_user
@user = User.new(user_type: :normal)
@user.save
end
end
as you say - I’m not saying this is a bug. Just something that (to me at least) is a wtf
for comparison - without telling any programmer what the language is below, I expect pretty much all of them expect the ‘mysteriousResult’ to be true
enum UserType {
case normal
case special
}
struct User {
var type:UserType
}
let user = User(type: .normal)
let mysteriousResult = (user.type == .normal)
I agree this is confusing, of course this isn’t the only case where you get confused with symbols / strings but it’s a damn fine example. Here’s another head scratcher I had lately https://github.com/rails/rails/issues/38621. I’m sure I can find more cases of confusion. Symbols is one of those things that plague beginners and seniors a like. I do wish we would think about Symbols a bit more and how we can reduce the pains in Rails.
Hi, Here is my issue with enum.
When calling user.invited! it will instantly perform an update query.
How can we use this user.invited! and after set other attributes and perform only one update query?
Let say I could block user, and I have a function to do it
def block(user)
user.blocked! # This will perform one update query
user.blocked_at = Time.now
user.save # This will perform one update query
end
If I want to perform only one query I have to do
def block(user)
user.access_type = "blocked" # This will just set the access_type value to blocked
user.blocked_at = Time.now
user.save # This will perform one update query
end
I think one of the strong aims behind ActiveRecord enums is to define an interface that decouples your application’s understanding of that field and the raw value in the database. The raw value could be a string, it could be a number, or it could be a database native enum which may not be represented well in Rails.
So let’s assume we have a status field which uses the following values for some arcane technical reason:
[
213123, // normal
238129, // special
989342 // extra_special
]
How would we work with this field? Sprinkling magic numbers all over our app probably isn’t the best way to deal with it, eg.
do_something if record.status == 213123
record.update!(status: 213123)
Record.where(status: 213123)
What we need is a lookup, ie. something that maps 213123 to our concept of normal.
And that’s exactly what AR enums do:
enum status {
normal: 213123,
special: 238129,
extra_special: 989342
}
Now instead of using raw values, we use the lookup map everywhere else in our application
do_something if record.status == Record.statuses[:normal]
record.update(status: Record.statuses[:normal])
record.where(status: Record.statuses[:normal])
These cases are actually common enough that Rails provides helpers so that you don’t need to type Record.statuses[:normal] every single time
do_something if record.normal?
record.normal!
Record.normal
I think there’s some ambiguity here between accessing the underlying value stored in the database and the semantic symbol this value represents.
ActiveRecord’s standard method of accessing the value of an attribute stored in the database is to call model.<attribute>. That accessor does the same thing regardless of whether the column is enumerated.
Now, we could change AR to default to returning the semantic symbol for that value when that attribute is enumerated. Question here is, how would you then easily access the underlying value?
You could call user.attributes["status"] to get the raw text value of course, but that’s not intuitive for the majority of people who don’t have a clear understanding of ActiveRecord’s enum functionality.
So maybe it’s not a good idea to return the semantic symbol by default. But you’d prefer to write user.status == :active instead of user.status == User.statuses[:active]. No problem, perhaps you can extend ActiveRecord a little bit to achieve that.
class User < ApplicationRecord
def status
super.to_sym
end
end