简体   繁体   English

Rails 枚举验证不起作用但引发 ArgumentError

[英]Rails enum validation not working but raise ArgumentError

A thread was created here , but it doesn't solve my problem. 这里创建了一个线程,但它并没有解决我的问题。

My code is:我的代码是:

course.rb课程.rb

class Course < ApplicationRecord
  COURSE_TYPES = %i( trial limited unlimited )
  enum course_type: COURSE_TYPES
  validates_inclusion_of :course_type, in: COURSE_TYPES
end

courses_controller.rb课程控制器.rb

class CoursesController < ApiController
  def create
    course = Course.new(course_params) # <-- Exception here
    if course.save # <-- But I expect the process can go here
      render json: course, status: :ok
    else
      render json: {error: 'Failed to create course'}, status: :unprocessable_entity
    end
  end

  private    
    def course_params
      params.require(:course).permit(:course_type)
    end
end

My test cases:我的测试用例:

courses_controller_spec.rb courses_controller_spec.rb

describe '#create' do
  context 'when invalid course type' do
    let(:params) { { course_type: 'english' } }
    before { post :create, params: { course: params } }

    it 'returns 422' do
      expect(response.status).to eq(422)
    end
  end
end

When running the above test case, I got an ArgumentError exception which was described at Rails issues当运行上面的测试用例时,我得到了一个ArgumentError异常,在Rails issues中有描述

So I expect if I set an invalid course_type to enum, it will fail in validation phase instead of raising an exception .所以我希望如果我将无效的course_type设置为枚举,它将在验证阶段失败而不是引发异常

Additionally, I know what really happens under the hook in rails at here and I don't want to manually rescue this kind of exception in every block of code which assigns an enum type value!此外,我知道这里的 rails 钩子下到底发生了什么,我不想在每个分配枚举类型值的代码块中手动挽救这种异常!

Any suggestion on this?对此有何建议?

I've found a solution.我找到了解决办法。 Tested by myself in Rails 6.我自己在 Rails 6 中测试过。

# app/models/contact.rb
class Contact < ApplicationRecord
  include LiberalEnum

  enum kind: {
    phone: 'phone', skype: 'skype', whatsapp: 'whatsapp'
  }

  liberal_enum :kind

  validates :kind, presence: true, inclusion: { in: kinds.values }
end
# app/models/concerns/liberal_enum.rb
module LiberalEnum
  extend ActiveSupport::Concern

  class_methods do
    def liberal_enum(attribute)
      decorate_attribute_type(attribute, :enum) do |subtype|
        LiberalEnumType.new(attribute, public_send(attribute.to_s.pluralize), subtype)
      end
    end
  end
end
# app/types/liberal_enum_type.rb
class LiberalEnumType < ActiveRecord::Enum::EnumType
  # suppress <ArgumentError>
  # returns a value to be able to use +inclusion+ validation
  def assert_valid_value(value)
    value
  end
end

Usage:用法:

contact = Contact.new(kind: 'foo')
contact.valid? #=> false
contact.errors.full_messages #=> ["Kind is not included in the list"]

UPDATED to support .valid?更新以支持.valid? to have idempotent validations.进行幂等验证。

This solution isn't really elegant, but it works.这个解决方案不是很优雅,但它有效。

We had this problem in an API application.我们在 API 应用程序中遇到了这个问题。 We do not like the idea of rescue ing this error every time it is needed to be used in any controller or action.我们不喜欢在每次需要在任何控制器或操作中使用此错误时rescue此错误的想法。 So we rescue d it in the model-side as follows:所以我们在model-side中将它rescue出来如下:

class Course < ApplicationRecord
  validate :course_type_should_be_valid

  def course_type=(value)
    super value
    @course_type_backup = nil
  rescue ArgumentError => exception
    error_message = 'is not a valid course_type'
    if exception.message.include? error_message
      @course_type_backup = value
      self[:course_type] = nil
    else
      raise
    end
  end

  private

  def course_type_should_be_valid
    if @course_type_backup
      self.course_type ||= @course_type_backup
      error_message = 'is not a valid course_type'
      errors.add(:course_type, error_message)
    end
  end
end

Arguably, the rails-team's choice of raising ArgumentError instead of validation error is correct in the sense that we have full control over what options a user can select from a radio buttons group, or can select over a select field, so if a programmer happens to add a new radio button that has a typo for its value, then it is good to raise an error as it is an application error, and not a user error.可以说,rails 团队选择引发ArgumentError而不是验证错误是正确的,因为我们可以完全控制用户可以从单选按钮组中选择哪些选项,或者可以在select字段上进行select ,所以如果程序员发生了添加一个新的单选按钮,它的值有错别字,那么最好引发错误,因为它是应用程序错误,而不是用户错误。

However, for APIs, this will not work because we do not have any control anymore on what values get sent to the server.但是,对于 API,这将不起作用,因为我们无法再控制将哪些值发送到服务器。

Using the above answer of the logic of Dmitry I made this dynamic solution to the ActiveRecord model使用上面的 Dmitry 逻辑答案,我对 ActiveRecord 模型做了这个动态解决方案

Solution 1:解决方案 1:

#app/models/account.rb
class Account < ApplicationRecord
  ENUMS = %w(state kind meta_mode meta_margin_mode)

  enum state: {disable: 0, enable: 1}
  enum kind:  {slave: 0, copy: 1}
  enum meta_mode:         {demo: 0, real: 1}
  enum meta_margin_mode:  {netting: 0, hedging: 1}


  validate do
    ENUMS.each do |e|
      if instance_variable_get("@not_valid_#{e}")
        errors.add(e.to_sym, "must be #{self.class.send("#{e}s").keys.join(' or ')}")
      end
    end
  end


  after_initialize do |account|
    Account::ENUMS.each do |e| 
      account.class.define_method("#{e}=") do |value|
        if !account.class.send("#{e}s").keys.include?(value)
          instance_variable_set("@not_valid_#{e}", true)
        else
          super value
        end
      end
    end
  end
end

Updated.更新。

Solution2: Here's another approach to dynamically replicate to other models. Solution2:这是动态复制到其他模型的另一种方法。

#lib/lib_enums.rb
module LibEnums
  extend ActiveSupport::Concern

  included do
        validate do
        self.class::ENUMS.each do |e|
          if instance_variable_get("@not_valid_#{e}")
            errors.add(e.to_sym, "must be #{self.class.send("#{e}s").keys.join(' or ')}")
          end
        end
      end

        self::ENUMS.each do |e| 
          self.define_method("#{e}=") do |value|
            if !self.class.send("#{e}s").keys.include?(value)
              instance_variable_set("@not_valid_#{e}", true)
            else
              super value
            end
          end
        end
    end
end
#app/models/account.rb

require 'lib_enums'
class Account < ApplicationRecord
  ENUMS = %w(state kind meta_mode meta_margin_mode)
  include LibEnums
end

Want to introduce another solution.想介绍另一种解决方案。

class Course < ApplicationRecord
  COURSE_TYPES = %i[ trial limited unlimited ]
  enum course_type: COURSE_TYPES

  validate do
    if @not_valid_course_type
      errors.add(:course_type, "Not valid course type, please select from the list: #{COURSE_TYPES}")
    end
  end

  def course_type=(value)
    if !COURSE_TYPES.include?(value.to_sym)
      @not_valid_course_type = true
    else
      super value
    end
  end
end

This will avoid ArgumentError in controllers.这将避免控制器中的ArgumentError Works well on my Rails 6 application.在我的 Rails 6 应用程序上运行良好。

The above answer by Aliaksandr does not work for Rails 7.0.4 as the decorate_attribute_type method was removed in Rails 7 and unified with the attribute method. Aliaksandr的上述回答不适用于 Rails 7.0.4,因为decorate_attribute_type方法已在 Rails 7 中删除并与attribute方法统一。

As such, the above solution will raise a NoMethodError similar to the following:因此,上述解决方案将引发类似于以下内容的NoMethodError

NoMethodError (undefined method `decorate_attribute_type' for <Model>:Class)

To implement that solution in Rails 7 consider using the following modified concern instead:要在 Rails 7 中实现该解决方案,请考虑改用以下修改后的关注点:

# app/models/concerns/liberal_enum.rb
module LiberalEnum
  extend ActiveSupport::Concern

  class_methods do
    def liberal_enum(attribute)
      attribute(attribute, :enum) do |subtype|
        LiberalEnumType.new(attribute, public_send(attribute.to_s.pluralize), subtype)
      end
    end
  end
end

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM