A thread was created here, but it doesn't solve my problem.
My code is:
course.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
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
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
So I expect if I set an invalid course_type
to enum, it will fail in validation phase instead of raising an exception.
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!
Any suggestion on this?
I've found a solution. Tested by myself in 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?
to have idempotent validations.
This solution isn't really elegant, but it works.
We had this problem in an API application. We do not like the idea of rescue
ing this error every time it is needed to be used in any controller or action. So we rescue
d it in the model-side as follows:
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.
However, for APIs, this will not work because we do not have any control anymore on what values get sent to the server.
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