[英]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.