I'm trying to get Active Model Errors working in a standard ruby class that I'm using for stripe.
class Payment
attr_reader :user, :token, :errors
attr_accessor :base
extend ActiveModel::Naming
def initialize(args)
#user = args[:user]
#token = args[:stripe_token]
#errors = ActiveModel::Errors.new(self)
end
def checking_account
begin
account = Stripe::Account.retrieve(user.stripe_account_id)
account.external_account = token
account.save
rescue Stripe::StripeError => e
errors.add(:base, e.message)
end
end
# The following methods are needed to be minimally implemented
def read_attribute_for_validation(attr)
send(attr)
end
def Payment.human_attribute_name(attr, options = {})
attr
end
def Payment.lookup_ancestors
[self]
end
end
now I'm making checking_account fail on purpose by not supplying a token, and I'm just returned an array currently saying:
=> ["Invalid external_account object: must be a dictionary or a non-empty string. See API docs at https://stripe.com/docs'"]
now I've followed the steps on http://api.rubyonrails.org/classes/ActiveModel/Errors.html so I'm not sure why this isn't working, does anybody have any idea how to fix this?
when I'm calling:
Payment.new(user: User.find(1)).managed_account it triggers the array above, and if I try to call .errors on that I get
NoMethodError: undefined method `errors' for #<Array:0x007f80989d8328>
which is obviously because it's an array and the format isn't correct.
The code you have should work to fill errors. The issue is that you can't call .errors on checking_account because the return value from checking_account is an Array, not a Payment instance. You should be able to make these calls separately on the console to review:
payment = Payment.new(user: User.find(1)) # Returns Payment instance
payment.checking_account
payment.errors # Calls `errors` on Payment instance
Related
I'm working on a refactor for some ruby on rails v6 application. I have made a custom validator to test if the string being passed could be parsed into a Date object, like so:
class DateFormatValidator < ActiveModel::Validator
def validate(record)
if options[:fields].any? do |field|
if record.send(field).nil?
return true
end
unless valid_date?(record.send(field))
record.errors.add(field, :invalid)
return false
end
end
end
end
def valid_date?(date_string)
Date.parse(date_string)
true
rescue ArgumentError
false
end
end
It is being called, and it adds the error to the record with the correct key for the field i'm passing. It is being called like so from inside my model:
validates_with DateFormatValidator, fields: [:date_of_birth]
But, when I ran a test to failed that parser like this:
def test_it_raises_ArgumentError_due_to_invalid_date
customer = customers(:one)
assert_raises(ArgumentError) do
customer.update({date_of_birth: "18/0-8/1989"})
end
end
Now, the assert_raises is my problem. I would expect to be able to do customer.valid? and since an error has been added to the customer it would return false. But instead, it returns:
ArgumentError: invalid date
Any ideas of what am I doing wrong or how to make the valid? turn false when i add an error into the errors obj holding my record??
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!
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 am receiving NoMethodErrors when my DeltaSynWorker runs. This happens in an application that was built as a revision of a currently working application. I am not the original programmer, and I am coming at it from a Java background (I mention this because it is possible I am missing something very obvious to others). I cannot figure out why NoMethodeError is being thrown when the code is very similar to code that is currently working fine in a different web application.
The Error:
NoMethodError: undefined method `client' for #<ApiRequestBuilder:0x0000000705a8f0>
delta_sync_worker.rb
class DeltaSyncWorker < SyncWorker
include Sidekiq::Worker
sidekiq_options queue: "delta_syncs"
def perform(subscriber_id, client_id)
sleep(10)
current_subscriber = ApiSubscriberDecorator.decorate(Subscriber.find(subscriber_id))
Time.zone = current_subscriber.time_zone
client = ApiClientDecorator.decorate(Client.find(client_id))
arb = ApiRequestBuilder.new(URI.parse(SR_HOST + '/servlet/sync/process'))
if arb.client(:subscriber => current_subscriber, :client => client)
arb.transmit
if arb.success?
current_subscriber.touch(:sync_updated_at)
decorated_client = ClientDecorator.decorate(client.model)
ConfirmationsSyncWorker.perform_in(1.hours, current_subscriber.id)
else
error_params = {:subscriber => current_subscriber.id, :response_body => arb.response.body, :request_body => arb.request.body, :sync_type => "DeltaSyncWorker"}
Airbrake.notify(:error_class => "sync_error", :error_message => "Sync Error: #{arb.response.message}", :params => error_params)
end
end
end
end
api_request_builder.rb
require 'nokogiri'
class ApiRequestBuilder < AbstractController::Base
include AbstractController::Rendering
include AbstractController::Layouts
include AbstractController::Helpers
include AbstractController::Translation
include AbstractController::AssetPaths
self.view_paths = "app/api"
attr_accessor :request_body, :request, :response, :append_request_headers, :request_method, :url
def initialize(url, *args)
#url = url
if args
args.each do |arg|
arg.each_pair{ |k,v| instance_variable_set("##{k.to_s}", v) }
end
end
end
# this will search for an api request template in api/requests, render that template and set any instance variables
def method_missing(meth, *args, &block)
if lookup_context.template_exists?("requests/#{meth.to_s}")
if args
args.each do |arg|
arg.each_pair{|k,v| instance_variable_set("##{k.to_s}", v) }
end
end
#request_body = (render :template => "requests/#{meth.to_s}")
else
super
end
end
def transmit
#request_method ||= "Post"
#request = "Net::HTTP::#{#request_method}".constantize.new(#url.path)
#request['x-ss-user'] = #subscriber.sr_user if #subscriber && #subscriber.sr_user.present?
#request['x-ss-pwd'] = #subscriber.sr_password if #subscriber && #subscriber.sr_password.present?
unless #append_request_headers.nil?
#append_request_headers.each_pair{ |k,v| #request[k] = v }
end
#request.body = #request_body if request_body? && #request.request_body_permitted?
#http = Net::HTTP.new(#url.host, #url.port)
#http.use_ssl = true
#http.verify_mode = OpenSSL::SSL::VERIFY_NONE
#response = #http.request(#request)
end
def success?
if #response.code == 200.to_s
return true
else
return false
end
end
def request_body?
unless #request_body.nil?
return true
else
return false
end
end
end
I have been looking at other NoMethodError questions here, but I cannot find an answer I feel applies to my situation. Any help is greatly appreciated. Thanks!
method_missing will catch sent messages for which there is no method defined, and the call to super at the end will pass it up to Ruby's standard method_missing behavior, which is what you are seeing (NoMethodError). However, this only happens if the if condition is not met, which is what I suspect is happening here, and would explain why it works in some situations but not in others. The call to :client, having found no matching methods along the inheritance chain, will look for a template called "requests/client" - try adding this template and see if that fixes the issue.
I know Ive seen this before and I feel like it wasn't what it appeared to be, but ya basically method missing is just intercepting the method call and when you call arb.client, it is caught by method missing and therefore tries to render api/client.xml.arb or api whatever the file type is. -- Note that there should be a file in the initializers directory named somethig like api_template_handler.rb or arb_temmplate_handler.rb, which is what allows rails to see that template type in the view directory -- make sure that is there first. Also sidenote, _client.xml.api is a partial used by the other api request in that directory (full sync),
To debug Id start by, above the following line
if lookup_context.template_exists?("requests/#{meth.to_s}")
Add a log statement
Rails.logger.debug "Can I see View?" + lookup_context.template_exists?("requests/#{meth.to_s}")
If true, then the problem is the error isnt getting caught properly because of method missing. If false, then the sidekiq worker isnt loading rails properly, or the view path isn't being added onto the rails view paths.
If true, Im guessing it might have something to do with the client model not being loaded, or an attribute on the client model not existing, that the builder is trying to call, and the error is somehow bubbling up to the api request builder class.
Oh also, just general stuff, but make sure redis and sidekiq are running, and restart passenger if its non local environment.
Let me know how it goes.
I have a nested resource called App.Routine that has_many activities. When I send the post here is my payload:
{routine: {name:testName,activities:[{name:testName},{name:testName}]}}
This returns a 500 error:
ActiveRecord::AssociationTypeMismatch in RoutinesController#create
Activity(#32627220) expected, got ActiveSupport::HashWithIndifferentAccess(#33577656)
My Rails API is using ActiveModelSerializers:
class RoutineSerializer < ActiveModel::Serializer
attributes :id, :name
has_many :activities, embed: :ids
end
class RoutinesController < ApplicationController
respond_to :json
def create
routine = Routine.create(params[:routine])
end
I believe my problem lies with how I handle the create action in my routines_controller.rb. Rails isn't liking how I am returning the hash of activities inside the routine JSON, but I can't figure out the correct way to handle this.
My original problem was indeed with my Rails API. I followed #dgeb's example again and realized I didn't know much about strong parameters. Thankfully there's a Railscast for that! Once I implemented that correctly I'm good to go!
Added 'gem strong_parameters' to my Gemfile. Then, my #create function on the parent controller makes a call to the update_parameters function where I first create and save the parent, then iterate through the child and save it.
From Dan Gebhart's ember data example:
def permitted_params
params.require(:contact).permit(:first_name,
:last_name,
:email,
:notes,
phone_numbers: [:id, :number])
end
def update_contact(contact)
contact_params = permitted_params
phone_numbers_param = contact_params.extract!(:phone_numbers)
phone_numbers_param = phone_numbers_param[:phone_numbers]
phone_numbers_param ||= []
# Because updates to the contact and its associations should be atomic,
# wrap them in a transaction.
Contact.transaction do
# Update the contact's own attributes first.
contact.attributes = contact_params
contact.save!
# Update the contact's phone numbers, creating/destroying as appropriate.
specified_phone_numbers = []
phone_numbers_param.each do |phone_number_params|
if phone_number_params[:id]
pn = contact.phone_numbers.find(phone_number_params[:id])
pn.update_attributes(phone_number_params)
else
pn = contact.phone_numbers.create(phone_number_params)
end
specified_phone_numbers << pn
end
contact.phone_numbers.each do |pn|
pn.destroy unless specified_phone_numbers.include?(pn)
end
end
# Important! Reload the contact to ensure that changes to its associations
# (i.e. phone numbers) will be serialized correctly.
contact.reload
return true
rescue
return false
end