Let's say we have this simple model:
class Project < ApplicationRecord
enum stage: {Idea: "idea", Done: "done", "On hold": "on hold", Cancelled: "cancelled"}
enum status: [:draft, :published, :archived]
end
When we access the enums from the model (Project.stages, Project.statuses) we get as result an processed (by ActiveRecord::Enum) response, both as a hash.
irb(main):001:0> Project.stages
=> {"Idea"=>"idea", "Done"=>"done", "On hold"=>"on hold", "Cancelled"=>"cancelled"}
irb(main):002:0> Project.statuses
=> {"draft"=>0, "published"=>1, "archived"=>2}
I'm struggling to know when an enum was declared as a Hash or as an Array having only the model and the enum name.
Any idea on how to get the initial hash or array from an enum?
You should not be treating them differently due to the principle of least suprise.
enum status: [:draft, :published, :archived] is just an implicit shorthand for defining an enum where the mapping is the same as the indices of the array. That's how its documented and having your code do something else opens up for a real WTF moment.
Defining enums implicitly is also regarded as a bad practice since adding new statuses in the middle of the array will break your application in a very sneaky way. There is even a Rubocop cop that detects the use of arrays by using a regular expression on the source code.
Its actually just a sloppy way of writing enum status: { draft: 0, published: 1, archived: 2}. You shouldn't be encouraging its use.
The arguments you pass to enum are just used to metaprogram the setters, getters and create the metadata which stores the enum mapping. It doesn't store the original arguments as there is no point to that.
If you REALLY wanted to do this you could monkeypatch the enum method:
module WonkyEnum
# this is silly. Don't do this
def enum(definitions)
#_enum_definitions ||= []
#_enum_definitions.push(definitions)
super(definitions)
end
end
But whatever the real problem is here there is most likely a better solution.
Related
In a Ruby on Rails application, say there is a model with an enum field like:
class Car < ActiveRecord::Base
enum color: {
blue: 0,
green: 1,
red: 2
}
end
There is an API endpoint exposed for creating new instances of my model.
The value being sent for params[:color] does not match up with the enum keys.
(e.g. something like 'Deep Ocean Blue' is being sent as the parameter value).
I know that I could change my enum to be something like:
class Car < ActiveRecord::Base
enum color: {
'Deep Ocean Blue' => 0,
# ... etc.
}
end
but then this makes the Rails generated methods for the enum strange and hard to use...
my_car.blue?
my_car.send('Deep Ocean Blue?') # not even sure this works but you get the idea
My question is, what is the best ("Railsiest") way to handle a situation like this?
Should I be creating a mapping of parameters and enum keys, and cast the correct enum value in a before_validation hook, or is there a better way of handling this?
Thank you!
You may try using underscore method if you have constant color options
2.6.5 :007 > "Deep Ocean Blue".parameterize.underscore
=> "deep_ocean_blue"
You may keep the enum as
class Car < ActiveRecord::Base
enum color: {
'deep_ocean_blue' => 0,
'golden_rose' => 1,
# ... etc.
}
end
Any while creating the object convert the parameter color to underscore
Car.create(color: params[:color].parameterize.underscore, # other parameters ..)
So I'm working on trying to learn GraphQL for ruby for a project.
I've almost got some parts of it up and running, but I'm having issues with other parts. There are plenty of tutorials out there that cover ultra-basics, but none of them seem to expand in the right directions.
I have a mutation to update my user. So far so good. I can look up the user by their ID, and update a single specific field. I can extend that to updating two fields.
What I cannot do, and this is looking insane, is generalize those fields -- at all. My user model will wind up with over 20 fields attached to it -- phone numbers, addresses, job title, etc etc.
When I create the mutation, I have to define the arguments that go into the resolve method. So far so good. I then define the fields the mutation can return. Again, so far so good.
Then I get to the actual resolve method.
The initial syntax isn't bad. def resolve(user_id:, name:, email:). Then you discover that despite setting required to false, you have to include all the values. You need to specify default values for the optional variables. So it becomes def resolve(user_id:, name: null, email: null) -- but that actually nulls out those values, you can't do partial updates. Worse yet, imagine having 20 fields you have to set this way. You can play games by trying to convert the arguments into a dictionary and rejecting null values -- but then you can't set properties to nil if they need to be nil again.
The solution: a double splat operator. Your syntax becomes def resolve(user_id:, **args). From what I can tell, it turns all remaining named arguments into a dictionary -- and I think unnamed arguments would become an array. Not sure how it would react with a mix of the two.
Full model becomes:
argument :user_id, ID, required: true#, loads: Types::UserType
argument :name, String, required: false
argument :email, String, required: false
field :user, Types::UserType, null: true
field :errors, Types::UserType, null: true
def resolve(user_id:, **args)
user = User.find(user_id)
if user.update(args)
{
user: user,
errors: []
}
else
{
user: nil,
errors: user.errors.full_messages
}
end
end
end
I have created a model Tester with integer column tester_type and declared enum variable in the model.
class Tester < ApplicationRecord
enum tester_type: { junior: 0, senior: 1, group: 2 }
end
I am getting below error while trying to create / initialize an object for that model:
ArgumentError: You tried to define an enum named "tester_type" on the model "Tester", but this will generate a class method "group", which is already defined by Active Record.
So, I tried changing tester_type to type_of_tester but it throws same error:
ArgumentError: You tried to define an enum named "type_of_tester" on the model "Tester", but this will generate a class method "group", which is already defined by Active Record.
I have searched for the solution and I found this error was a constant ENUM_CONFLICT_MESSAGE in ActiveRecord::Enum class but, cannot able to find the cause of this problem.
Please help me.
Thanks.
In this case, if you want to use enum, you are probably better off renaming your label to something else. This is not unique to enums – a lot of Active Record features generates methods for you and usually there aren't ways to opt-out of those generated methods.
then change group to another_name
OR you should follow this also
enum :kind, [:junior, :senior, :group], prefix: :kind
band.kind_group?
You can use the :_prefix or :_suffix options when you need to define multiple enums with same values or in your case, to avoid conflict with already defined methods. If the passed value is true, the methods are prefixed/suffixed with the name of the enum. It is also possible to supply a custom value:
class Conversation < ActiveRecord::Base
enum status: [:active, :archived], _suffix: true
enum comments_status: [:active, :inactive], _prefix: :comments
end
With the above example, the bang and predicate methods along with the associated scopes are now prefixed and/or suffixed accordingly:
conversation.active_status!
conversation.archived_status? # => false
conversation.comments_inactive!
conversation.comments_active? # => false
For your case, my suggestion would be using something like:
class Tester < ApplicationRecord
enum tester_type: { junior: 0, senior: 1, group: 2 }, _prefix: :type
end
Then you can use these scopes as:
tester.type_group!
tester.type_group? # => true
Tester.type_group # SELECT "testers".* FROM "testers" WHERE "testers"."tester_type" = $1 [["tester_type", 2]]
# or,
Tester.where(tester_type: :group) # SELECT "testers".* FROM "testers" WHERE "testers"."tester_type" = $1 [["tester_type", 2]]
check this out. it is the option group you are having a problem with. You can use prefix option as mentioned in this post
enum options
Specifying a prefix option worked for me.
# models/tester.rb
enum tester_type: { junior: 0, senior: 1, group: 2 }, _prefix: true
And then to use it:
Tester.first.tester_type
=> nil
Tester.first.tester_type_junior!
=> true
Tester.first.tester_type
=> 0
Note that the enum values can be given explicit string values instead of integers, with the same notation provided in the question. Which makes the saved db values more human readable.
enum tester_type: { junior: 'junior', senior: 'senior', group: 'group' }, _prefix: true
Tester.first.tester_type_senior!
=> true
Tester.first.tester_type
I'm trying to use new Enum type, everything works well except one issue. When writing functional tests I usually use structure:
order = Order.new(o_status: :one)
post :create, order: order.attributes
# Error message:
# ArgumentError: '0' is not a valid o_status
It's ok as long as I don't have Enum attribute. The problem with enums is that instead of String value .attributes returns it's Integer value which can't be posted as enum attribute value.
In above example model can look like this:
class Order < ActiveRecord::Base
enum o_status: [:one, :two]
end
I figured out that when I do:
order = Order.new(o_status: :one)
atts = order.attributes
atts[:o_status] = "one" # it must be string "one" not symbol or integer 0
post :create, order: order.attributes
It will work OK.
Is it normal or there is some better solution?
EDIT:
The only workaround which I found looks like this:
order = { o_status: :one.to_s }
post :create, order: order
pros: It is short and neat
cons: I cannot validate order with order.valid? before sending with post
This doesn't solve issue with order.attributes when there is Enum inside.
From the Enum documentation:
You can set the default value from the database declaration, like:
create_table :conversations do |t|
t.column :status, :integer, default: 0
end
Good practice is to let the first declared status be the default.
Best to follow that advice and avoid setting a value for an enum as part of create. Having a default value for a column does work in tests as well.
Using rails 4.1.1
My model has an enum like:
class Article < ActiveRecord::Base
enum article_status: { published: 1, draft :2 }
Now in my new.html.erb I have:
<%= form.select :article_status, options_for_select(Article.article_statuses) %>
When going to save the model I get this error:
'1' is not a valid article_status
I was thinking it would be able to handle this during an update.
What am I doing wrong?
The update_attributes or new call in your controllers will expect the stringified version of the enum symbol, not the integer. So you need something like:
options_for_select(Article.article_statuses.
collect{|item, val| [item.humanize, item]}, selected: #article.status)
There is a full example in this article.