I am using the Rails 4.1 enum field
class User
    enum category: [ :client, :seller, :provider ]
end
When the user signs up, he chooses from a select box his category. The default is empty, because I want to force the user to choose one option.
If the user does not select any option, I would like to return to the form with a validation message. Here is the select box code in sign up form
    <%= f.select :category, [], {}, class: "form-control" do  %>
        <option value="99">Choose an option</option>
        <% User.categories.each do |cat,code| %>
            <option value="<%= code %>" <% if params["user"] && code.to_s == params["user"]["category"] %>selected='selected'<%end%> ><%= t(cat) %></option>
        <% end %>
    <% end %>
When the controller creates the user, instead of adding a validation error to the record, it raises an exception. How to avoid this?
ArgumentError - '99' is not a valid category:
  (gem) activerecord-4.1.1/lib/active_record/enum.rb:103:in `block (3 levels) in enum'
  (gem) activerecord-4.1.1/lib/active_record/attribute_assignment.rb:45:in `_assign_attribute'
  (gem) activerecord-4.1.1/lib/active_record/attribute_assignment.rb:32:in `block in assign_attributes
                I really dislike the reasoning as stated in the issue listed above. Since the value is coming over the wire, it should be treated the same as a freetext input where the expectation is to validate in the model and not the controller. This is especially true in APIs where the developers have even less of a say as far as expected input coming from form data (for example).
In case anyone wants a hack, here is what I came up with. Passes basic testing, but would love feedback if anyone finds issues with it:
class User
  enum category: [ :client, :seller, :provider ]
  def category=(val)
    super val
  rescue
    @__bad_cat_val = val
    super nil
  end
end
This will reset category to nil and we can then validate the field:
class User
...
  validates :category, presence: true
end
The problem is that we really want to be validating inclusion, not just presence. To get around that we have to capture the input (e.g. as above in def category=). Then we can output our message using that value:
class User
...
  validates :category, inclusion: { 
    in: categories.keys, 
    message: ->(record,error) {
      val = record.instance_variable_get(:@__bad_cat_val)
      return "Category is required" if val.nil?
      "#{val.inspect} is not a valid category"
    }
  }
end
That will give us messages for presence and inclusion. If you need to fine tune it more you would have to use a custom validator (I believe).
Read: https://github.com/rails/rails/issues/13971
Specifically read Senny's comment:
The current focus of AR enums is to map a set of states (labels) to an integer for performance reasons. Currently assigning a wrong state is considered an application level error and not a user input error. That's why you get an ArgumentError.
That being said you can always set nil or an empty string to the enum attribute without raising an error:
<option value="">Choose an option</option>
and add a simple presence validation like:
validates :category, presence: { message: "is required" }
                        If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With