Rails API - Casting string parameters to enum values - ruby-on-rails

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 ..)

Related

ActiveRecord::Enum initialized with hash or array?

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.

Rails - ArgumentError in using ActiveRecord::Enum

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

Ruby on Rails 4.2 enum attributes

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.

Rails field enum as_json : get integer instead of string

In my projet, I have a User model with a gender enum :
class User < ActiveRecord::Base
enum gender: [:female, :male]
end
When I'm calling as_json on one of my object, I get the string "female" or "male". Is there any way to render the integer value instead of the string ?
I would try looking over the docs
http://edgeapi.rubyonrails.org/classes/ActiveRecord/Enum.html
for this, perhaps you could use a hash instead
class User < ActiveRecord::Base
enum gender: { female: 0, male: 1 }
end
hope its of any help
User.genders is an array of your enum and integer value. So, use User.genders[#user.gender].to_json.
If you're not happy with the default, to_json, investigate jBuilder to create the exact JSON you

Using enum in a model, when saving it doesn't like the integer value

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.

Resources