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 rescueing this error every time it is needed to be used in any controller or action. So we rescued 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.
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. Works well on my Rails 6 application.
Using the above answer of the logic of Dmitry I made this dynamic solution to the ActiveRecord model
Solution 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.
#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
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.
As such, the above solution will raise a NoMethodError similar to the following:
NoMethodError (undefined method `decorate_attribute_type' for <Model>:Class)
To implement that solution in Rails 7 consider using the following modified concern instead:
# 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
The answer posted by fin-cos won't work for me in Rails 7.0.4.2
In my tests I get the following error:
ArgumentError: You tried to define an enum named "name_of_the_enum" on the model "Model", but this will generate a instance method "name_of_the_enum_value?", which is already defined by another enum.
The Rails docs state that attribute overrides existing definitions. But somehow that isn't true.
UPDATE (2023-02-20):
I took Dmitry Shveikus solution, which was working for me and mixed it with the concern approach. So I ended up with:
models/concerns/validates_enum.rb:
module ValidatesEnum
extend ActiveSupport::Concern
class_methods do
def validates_enum(*enums)
enums.each do |enum_attribute|
define_method(:"#{enum_attribute}_types") do
self.class.const_get(:"#{enum_attribute.to_s.upcase}_TYPES").keys.map(&:to_s)
end
define_method(:"#{enum_attribute}=") do |value|
if !send("#{enum_attribute}_types").include?(value)
self.instance_variable_set(:"#not_valid_#{enum_attribute}_type", true)
else
super value
end
end
validate do
if self.instance_variable_get(:"#not_valid_#{enum_attribute}_type")
errors.add(enum_attribute, "Not a valid #{enum_attribute} type, please select from the list: #{send(:"#{enum_attribute}_types").join(', ')}")
end
end
end
end
end
end
And in your model:
class Model < ApplicationRecord
include ValidatesEnum
THE_ENUM_TYPES = {
something: 0,
something_other: 1,
}
enum the_enum: THE_ENUM_TYPES
validates_enum :the_enum
end
The convention is to declare the enum values in a constant with the role ENUM_NAME_TYPES (where ENUM_NAME is the name of your defined enum), which you then pass to define the enum itself. The concern will check for that and validate against it.
If you have multiple enums in your model, repeat the above steps. But you can call: validates_enum with multiple enums like so:
validates_enum :enum1, :enum2
Hope that helps!
Related
My intention is to create custom error classes in various places for my Rails application since most of the error classes have the same methods. I have decided to create a YAML file to contain all the information from various error classes, and use a class factory script to generate all the classes in runtime. Here is what I have:
chat_policy.rb
class ChatPolicy; ... end
class ChatPolicy::Error < StandardError
ERROR_CLASSES = GLOBAL_ERROR_CLASSES['chat_policy']
ERROR_CLASSES.each do |cls|
const_set(cls['class_name'], Class.new(ChatPolicy::Error) {
attr_reader :object
def initialize(object)
#object = object
end
define_method(:message) do
cls['message']
end
define_method(:code) do
cls['code']
end
})
end
the GLOBAL_ERROR_CLASSES is loaded from YAML.load and turned to an object.
error_classes.yml
chat_policy:
- class_name: UserBlacklisted
message: You are not allowed to do this
code: ECP01
- class_name: UserSuspended
message: You are not allowed to do this
code: ECP02
- class_name: UserNotEligibleToRent
message: You are not allowed to do this
code: ECP03
- class_name: MembershipTierNotAllowed
message: You are not allowed to do this
code: ECP04
* __ Question is __ *
Now I have other files like register_policy, checkout_policy, discount_policy ..etc. It would be very duplicated if I have to do the class generation in every policy file. I wonder if I can shorten the code to something like this:
chat_policy_intended.rb
class ChatPolicy::Error < StandardError
ERROR_CLASSES = GLOBAL_ERROR_CLASSES['chat_policy']
error_class_factory(ChatPolicy::Error, ERROR_CLASSES)
end
discount_policy_intended.rb
class DiscountPolicy::Error < StandardError
ERROR_CLASSES = GLOBAL_ERROR_CLASSES['discount_policy']
error_class_factory(DiscountPolicy::Error, ERROR_CLASSES)
end
error_clas_factory.rb
ERROR_CLASSES.each do |cls|
const_set(cls['class_name'], Class.new(/*class_variable*/) {
attr_reader :object
def initialize(object)
#object = object
end
define_method(:message) do
cls['message']
end
define_method(:code) do
cls['code']
end
})
end
What I tried
I tried to create a .rb file basically copying the class factory script. And use eval method to eval it in runtime, but it seems I can pass in variables into the script
eval File.read(File.join(Rails.root, 'lib', 'evals', 'error_class_generator.rb'))
What should I do?
I appreciate the effort of avoiding to repeat yourself at all costs, but I find your code quite complex for the problem you're trying to solve, namely send errors to your app users.
How about sticking to the a simpler < MyAppError inheritance hierarchy to avoid the duplicated code?
class MyAppError < StandardError
attr_reader :object
def message(message)
# does stuff
end
def code(code)
# also does stuff
end
end
class ChatPolicyError < MyAppError
def message(message)
'[CHAT POLICY]' + super
end
end
class UserBlacklisted < ChatPolicyError
def message(message)
# Does stuff too
super
end
end
[...] # You get the idea
I'm trying to handle the situation where the user has entered info incorrectly, so I have a path that follows roughly:
class Thing < AR
before_validation :byebug_hook
def byebug_hook
byebug
end
end
thing = Thing.find x
thing.errors.add(:foo, "bad foo")
# Check byebug here, and errors added
if thing.update_attributes(params)
DelayedJobThatDoesntLikeFoo.perform
else
flash.now.errors = #...
end
byebug for byebug_hook> errors.messages #=> {}
Originally I thought that maybe the model was running its own validations and overwriting the ones I added, but as you can see even when I add the before hook the errors are missing, and I'm not sure what's causing it
ACTUAL SOLUTION
So, #SteveTurczyn was right that the errors needed to happen in a certain place, in this case a service object called in my controller
The change I made was
class Thing < AR
validate :includes_builder_added_errors
def builder_added_errors
#builder_added_errors ||= Hash.new { |hash, key| hash[key] = [] }
end
def includes_builder_added_errors
builder_added_errors.each {|k, v| errors.set(k, v) }
end
end
and in the builder object
thing = Thing.find x
# to my thinking this mirrors the `errors.add` syntax better
thing.builder_added_errors[:foo].push("bad foo") if unshown_code_does_stuff?
if thing.update_attributes(params)
DelayedJobThatDoesntLikeFoo.perform
else
flash.now.errors = #...
end
update_attributes will validate the model... this includes clearing all existing errors and then running any before_validation callbacks. Which is why there are never any errors at the pont of before_validation
If you want to add an error condition to the "normal" validation errors you would be better served to do it as a custom validation method in the model.
class Thing < ActiveRecord::Base
validate :add_foo_error
def add_foo_error
errors.add(:foo, "bad foo")
end
end
If you want some validations to occur only in certain controllers or conditions, you can do that by setting an attr_accessor value on the model, and setting a value before you run validations directly (:valid?) or indirectly (:update, :save).
class Thing < ActiveRecord::Base
attr_accessor :check_foo
validate :add_foo_error
def add_foo_error
errors.add(:foo, "bad foo") if check_foo
end
end
In the controller...
thing = Thing.find x
thing.check_foo = true
if thing.update_attributes(params)
DelayedJobThatDoesntLikeFoo.perform
else
flash.now.errors = #...
end
I am facing a design decision I cannot solve. In the application a user will have the ability to create a campaign from a set of different campaign types available to them.
Originally, I implemented this by creating a Campaign and CampaignType model where a campaign has a campaign_type_id attribute to know which type of campaign it was.
I seeded the database with the possible CampaignType models. This allows me to fetch all CampaignType's and display them as options to users when creating a Campaign.
I was looking to refactor because in this solution I am stuck using switch or if/else blocks to check what type a campaign is before performing logic (no subclasses).
The alternative is to get rid of CampaignType table and use a simple type attribute on the Campaign model. This allows me to create Subclasses of Campaign and get rid of the switch and if/else blocks.
The problem with this approach is I still need to be able to list all available campaign types to my users. This means I need to iterate Campaign.subclasses to get the classes. This works except it also means I need to add a bunch of attributes to each subclass as methods for displaying in UI.
Original
CampaignType.create! :fa_icon => "fa-line-chart", :avatar=> "spend.png", :name => "Spend Based", :short_description => "Spend X Get Y"
In STI
class SpendBasedCampaign < Campaign
def name
"Spend Based"
end
def fa_icon
"fa-line-chart"
end
def avatar
"spend.png"
end
end
Neither way feels right to me. What is the best approach to this problem?
A not very performant solution using phantom methods. This technique only works with Ruby >= 2.0, because since 2.0, unbound methods from modules can be bound to any object, while in earlier versions, any unbound method can only be bound to the objects kind_of? the class defining that method.
# app/models/campaign.rb
class Campaign < ActiveRecord::Base
enum :campaign_type => [:spend_based, ...]
def method_missing(name, *args, &block)
campaign_type_module.instance_method(name).bind(self).call
rescue NameError
super
end
def respond_to_missing?(name, include_private=false)
super || campaign_type_module.instance_methods(include_private).include?(name)
end
private
def campaign_type_module
Campaigns.const_get(campaign_type.camelize)
end
end
# app/models/campaigns/spend_based.rb
module Campaigns
module SpendBased
def name
"Spend Based"
end
def fa_icon
"fa-line-chart"
end
def avatar
"spend.png"
end
end
end
Update
Use class macros to improve performance, and keep your models as clean as possible by hiding nasty things to concerns and builder.
This is your model class:
# app/models/campaign.rb
class Campaign < ActiveRecord::Base
include CampaignAttributes
enum :campaign_type => [:spend_based, ...]
campaign_attr :name, :fa_icon, :avatar, ...
end
And this is your campaign type definition:
# app/models/campaigns/spend_based.rb
Campaigns.build 'SpendBased' do
name 'Spend Based'
fa_icon 'fa-line-chart'
avatar 'spend.png'
end
A concern providing campaign_attr to your model class:
# app/models/concerns/campaign_attributes.rb
module CampaignAttributes
extend ActiveSupport::Concern
module ClassMethods
private
def campaign_attr(*names)
names.each do |name|
class_eval <<-EOS, __FILE__, __LINE__ + 1
def #{name}
Campaigns.const_get(campaign_type.camelize).instance_method(:#{name}).bind(self).call
end
EOS
end
end
end
end
And finally, the module builder:
# app/models/campaigns/builder.rb
module Campaigns
class Builder < BasicObject
def initialize
#mod = ::Module.new
end
def method_missing(name, *args)
value = args.shift
#mod.send(:define_method, name) { value }
end
def build(&block)
instance_eval &block
#mod
end
end
def self.build(module_name, &block)
const_set module_name, Builder.new.build(&block)
end
end
I have defined a callback after_find for checking some settings based on the retrieved instance of the model. If the settings aren't fulfilled I don't want the instance to be return from the find method. Is that possible?
an example
the controller looks like:
class UtilsController < ApplicationController
def show
#util = Util.find(params[:id])
end
end
the model:
class Util < ActiveRecord::Base
after_find :valid_util_setting
def valid_util_setting
# calculate_availability? complex calculation
# that can not be part of the sql statement or a scope
unless self.setting.calculate_availability?(User.current.session)
#if not available => clear the record for view
else
#nothing to do here
end
end
end
Instead of trying to clear the record, you could just raise an exception?
E.g.
unless self.setting.calculate_availability?(User.current.session)
raise ActiveRecord::RecordNotFound
else
...
I'm afraid you can't clear found record in this callback
Maybe you should find in scope with all your options from the beginning?
I.e. #util = Util.scoped.find(params[:id])
I found a solution
def valid_util_setting
Object.const_get(self.class.name).new().attributes.symbolize_keys!.each do |k,v|
begin
self.assign_attributes({k => v})#, :without_protection => true)
rescue ActiveModel::MassAssignmentSecurity::Error => e; end
end
end
With this I'm able to create an almost empty object
I'm basically attempting to implement the solution from Railscast #32, modernized for Rails 3.0.7
http://railscasts.com/episodes/32-time-in-text-field
class Task < ActiveRecord::Base
attr_accessible :description, :complete, :deadline
validate :deadline_string_no_errors
def deadline_string
self.deadline.to_s
end
def deadline_string=(deadline_str)
unless deadline_str.blank?
begin
self.deadline = Chronic.parse(deadline_str)
rescue
self.deadline = Time.parse(deadline_str)
rescue
#deadline_invalid = true
end
end
end
def deadline_string_no_errors
errors.add(:deadline_string, "Is Invalid") if #deadline_invalid
end
I want to set a validation error when a string, such as 'foobar' is put into #deadline_string=, either by console or (intended) form entry.
What I'm particularly concerned about is a more strict formatting of time, Chronic.parse('2011-05-25 08:19:00 UTC') Which throws a TypeError. In that case I want to fallback to Time.parse, which can understand this formatting.
This alternative form also does not work with deadline_string="foobar" as the initial call.
# snip
def deadline_string=(deadline_str)
unless deadline_str.blank?
self.deadline = ( parse_time(deadline_str, Chronic) || parse_time(deadline_str, Time) )
end
end
private
def parse_time(string, parser)
begin
parser.parse(string)
rescue
#deadline_invalid = true
end
end
def deadline_string_no_errors
#snip
end
No matter what I try it doesn't ever seem to get to the second rescue. There is also some strangeness around the deadline attribute, which is specified in the schema as a datetime.
The solution in the end was something like this, but it could probably stand to some refactoring. I'm anxious about setting self.deadline in the method call, but not sure why I should be.
Class Task < ActiveRecord::Base
attr_accessible: description, :complete, :deadline
validate :deadline_string_no_errors
def deadline_string
self.deadline.to_s
end
def deadline_string=(deadline_str)
unless deadline_str.blank?
self.deadline = ( parse_time(deadline_str, Chronic) || parse_time(deadline_str, Time) )
#deadline_invalid = true if self.deadline.nil?
end
end
private
def parse_time(string, parser)
begin
parser.parse(string)
rescue
end
end
def deadline_string_no_errors
errors.add(:deadline_string, "Is Invalid") if #deadline_invalid
end
Refactoring is welcome, and will be marked as an answer.
A few of things:
$ Chronic.parse 'foo'
yields nil and not an exception.
$ Time.parse 'foo'
yields a Time object, not an exception. (Edit: In Ruby 1.8.7)
When you use a rescue, you need to specify the type of exception that you're rescuing
rescue SyntaxError, NameError => boom