ruby-on-rails Rails枚举验证不起作用,但引发ArgumentError

js5cn81o  于 2022-12-20  发布在  Ruby
关注(0)|答案(5)|浏览(170)

创建了一个线程here,但它没有解决我的问题。
我的代码是:

    • 课程. rb**
class Course < ApplicationRecord
  COURSE_TYPES = %i( trial limited unlimited )
  enum course_type: COURSE_TYPES
  validates_inclusion_of :course_type, in: COURSE_TYPES
end
    • 课程_控制器. 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

我的测试用例:

    • 课程控制器规范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

在运行上述测试用例时,我得到了ArgumentError异常,在Rails issues中对此进行了描述
因此,如果我将一个无效的course_type设置为枚举,它将在验证阶段失败,而不是引发异常
另外,我知道在here的rails中的钩子下真正发生了什么,我不想在分配枚举类型值的每个代码块中手动拯救这种异常!
对此有何建议?

vlju58qv

vlju58qv1#

我找到了一个解决方案,我自己在Rails6中测试过。

# 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

用法:

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

lnvxswe22#

已更新以支持.valid?具有幂等验证。

这个解决方案并不十分优雅,但它确实有效。
我们在一个API应用程序中遇到了这个问题。我们不喜欢每次需要在任何控制器或动作中使用时都rescue这个错误的想法。所以我们在模型端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

可以说,rails团队选择引发ArgumentError而不是validation error是正确的,因为我们可以完全控制用户可以从单选按钮组中选择什么选项,或者可以选择select字段,所以如果程序员碰巧添加了一个新的单选按钮,其值有拼写错误,那么最好引发一个error,因为这是一个应用程序错误。而不是用户错误。
然而,对于API,这将不起作用,因为我们不再能够控制发送到服务器的值。

kfgdxczn

kfgdxczn3#

想介绍另一种解决方案。

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

这将避免控制器中的ArgumentError。在我的Rails 6应用程序中运行良好。

b1payxdu

b1payxdu4#

使用Dmitry的逻辑的上述答案,我对ActiveRecord模型进行了此动态解决方案
解决方案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

更新。
解决方案2:这里有另一种动态复制到其他模型的方法。
x一个一个一个一个x一个一个二个x

wvt8vs2t

wvt8vs2t5#

上面的answer by Aliaksandr不适用于Rails 7.0.4,因为decorate_attribute_type方法是removed in Rails 7,并且与attribute方法统一。
因此,上述解决方案将引发类似于以下内容的NoMethodError

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

要在Rails7中实现该解决方案,请考虑使用以下修改后的关注点:

# 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

相关问题