Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

ActiveRecord enum: use validation if exists instead of raising ArgumentError #13971

Closed
tjschuck opened this Issue Feb 7, 2014 · 15 comments

Comments

Projects
None yet
@tjschuck
Copy link
Contributor

tjschuck commented Feb 7, 2014

Regarding ActiveRecord::Enum, assume you had something like

class Project < ActiveRecord::Base
  enum color: [:blue, :green, :red]
  validates :color, inclusion: { in: [:blue, :green, :red] }
end

Currently, if a "bad" value (say, "orange") is passed in, an ArgumentError is raised.

It would be nice if the validation was preferred (with the ArgumentError as a fallback if there was no validation on that attribute) so that you'd be able to return a nicer error on the object later via the standard means:

object.errors.messages
# => {:color=>["is not included in the list"]} kind of thing

@rafaelfranca rafaelfranca added this to the 4.1.0 milestone Feb 8, 2014

@rafaelfranca

This comment has been minimized.

Copy link
Member

rafaelfranca commented Feb 8, 2014

Not sure if we have a easy fix for this.

To make validations work we will have to store the invalid value somewhere as string/symbol.

Worth to check.

@rafaelfranca rafaelfranca removed this from the 4.1.0 milestone Feb 8, 2014

@chancancode

This comment has been minimized.

Copy link
Member

chancancode commented Feb 8, 2014

Hi @tjschuck, thanks for installing the beta and testing things early on – and most importantly providing feedback 💙

With what is being implemented right now, I feel that enum is not really a good fit for what you are trying to do. The current implementation is really just narrowly focused on ("optimized for" if you prefer) the use case suggested in the documentation – used by your application internally (to track internal "states"). For this purpose, I think raising an ArgumentError is the right/more helpful behaviour.

I wouldn't go as far as saying that you shouldn't use enums for your use case (more generally, to expose user-facing attributes). However, if you do use it for that purpose, you'd have to fill in a lot of gaps manually at the moment – such as form helpers, translating the internal symbol/string to user facing labels, i18n, validations, etc. That might or might not change in the future.

Personally, I'm not against expanding the focus to cover the second use case as well – provided that can be implemented without bloating this too much and without having to add more code to other parts of Active Record to make things work. (I'm not sure if that's feasible – this might be something that's better left with other gems.) However, even if we were to do that, it would most likely not make it to Rails 4.1.

I propose that we should close this for now (we don't use the github issue tracker to track feature requests), and you can start a discussion on the rails-core mailing list if you want. But I'll let @dhh make the call.

cc @senny

@senny

This comment has been minimized.

Copy link
Member

senny commented Feb 8, 2014

Agree with @chancancode. 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.

I'm closing this for now but please do as @chancancode proposed and start a discussion on the mailing list.

@tjschuck thank you for using the beta 💛 !

@heaven

This comment has been minimized.

Copy link

heaven commented Jan 21, 2016

Hi, we do implement a REST API and have something like this in our controller:

def handle_action
  yield
rescue Unauthorized
  render :nothing => true, :status => 401
rescue CanCan::AccessDenied
  render :nothing => true, :status => 403
rescue ActiveRecord::RecordNotFound
  render :nothing => true, :status => 404
# ...
rescue ActiveRecord::RecordInvalid => e
  render :json => { :error => e.message }, :status => 422
rescue => e
  notify_airbrake(e)
  render :nothing => true, :status => 500
ensure
  ...
end

Our model has an enum attribute called kind, and if we receive an invalid value the ArgumentError being raised, the user receives 500 error code. And I don't see a good way to handle this at our end. We have to manually validate submitted values in controller, create additional services for that, etc. Doesn't seem right to me.

It would be helpful if it at least raise a more specific error, like ActiveRecord::EnumValueError or something like that.

@kaspth

This comment has been minimized.

Copy link
Member

kaspth commented Jan 21, 2016

@heaven:

We have to manually validate submitted values in controller

You should absolutely do that 👍

Also we don't take feature requests on here, try the Rails Core mailing list to see if you can garner support on your proposal. Thanks ❤️

hughdavenport added a commit to hughdavenport/powershop_devtrain_monopoly that referenced this issue Apr 28, 2016

Mainly green specs for players controller
4 yellows due to rails bug rails/rails#13971,
skip tag in code.

hughdavenport added a commit to hughdavenport/powershop_devtrain_monopoly that referenced this issue May 5, 2016

@manbeardave

This comment has been minimized.

Copy link

manbeardave commented Jul 26, 2016

Hey all -

We just ran into this issue too on an API-only project. Looked at validating things manually and decided that the performance hit from just using the two strings (in our case 'mi' and 'km', describing length unit for a given radius) was acceptable in order to utilize ActiveRecord validation on our model for this (and all the nice automated error messaging that comes with it!)

@drewish

This comment has been minimized.

Copy link

drewish commented Feb 27, 2017

Ugg... just ran into this. Sad to see that two years later there's still no built in validation for enums.

@dhh

This comment has been minimized.

Copy link
Member

dhh commented Feb 28, 2017

@maschiner

This comment has been minimized.

Copy link

maschiner commented Mar 16, 2017

If you are looking for a quick fix you can add this to your application_record.rb. It will suppress the exception when you have a validation for the enum defined.

class ApplicationRecord < ActiveRecord::Base
  def initialize(*)
    super
  rescue ArgumentError
    raise if valid?
  end
end
class Project < ApplicationRecord
  enum color: [:blue, :green, :red]
  validates :color, inclusion: { in: colors }
end
@drewish

This comment has been minimized.

Copy link

drewish commented Mar 16, 2017

@maschiner That wouldn't do anything about assignment though would it?

irb(main):007:0> p = Project.new(color: :purple)
=> #<Project id: nil, color: nil, created_at: nil, updated_at: nil>
irb(main):008:0> p.color = 'greenish'
ArgumentError: 'greenish' is not a valid color

Updated: It also seems to stop assigning values once it hits an invalid enum:

irb(main):011:0> p = Project.new id: 2, created_at: Time.now
=> #<Project id: 2, color: nil, created_at: "2017-03-16 19:52:18", updated_at: nil>
irb(main):012:0> p = Project.new color:'adf', id: 2, created_at: Time.now
=> #<Project id: nil, color: nil, created_at: nil, updated_at: nil>
@maschiner

This comment has been minimized.

Copy link

maschiner commented Mar 16, 2017

@drewish Yes, I guess that fix was too quick ;)
I will try to clean this up and open a PR.

@drewish

This comment has been minimized.

Copy link

drewish commented Mar 17, 2017

I think to @rafaelfranca's original point, since the current behavior only allows assignment of valid values, we'd need to store the invalid string/symbol for validation to work. I don't know enough about the rails internals to understand the best way forward with that. If folks have some hints I'm happy to dig at it further.

@NealJMD NealJMD referenced this issue Mar 24, 2017

Merged

Publish actions #901

@CristiRazvi

This comment has been minimized.

Copy link

CristiRazvi commented Nov 13, 2017

i have made a gem for validating enums inclusion
https://github.com/CristiRazvi/enum_attributes_validation

@swaathi

This comment has been minimized.

Copy link
Contributor

swaathi commented Feb 13, 2018

@chancancode Do you think this is still a non issue? Or can I work on providing support for enums through out the entire process? I can work on form builders, scaffolding and validation. Please let me know your thoughts.

@henriquemenezes

This comment has been minimized.

Copy link

henriquemenezes commented Apr 24, 2018

In Rails 5.2, you can override assert_valid_value in initializers/active_record_enum.rb as follow:

module ActiveRecord
  module Enum
    class EnumType < Type::Value
      def assert_valid_value(value)
        unless value.blank? || mapping.has_key?(value) || mapping.has_value?(value)
          nil
        end
      end
    end
  end
end

Instead raise ArgumentError. You will set nil to a invalid value.

So, you can validate the enum as follow:

class User < ApplicationRecord
  enum gender: [:male, :female]
  validates :gender, inclusion: { in: genders.keys, message: :invalid }
end

Or if you want to use the value before casting you can use the attribute self.gender_before_type_cast to validate raw input.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.