How to determine ActiveModel::Errors validation type - ruby-on-rails-3

With the migration from Rails 2 to Rails 3 validation errors were moved from ActiveRecord::Error to ActiveModel::Errors.
In rails 2 the validation error had a type and a message (among other things) and you could check the type of the validation error by doing something like the following:
rescue ActiveRecord::RecordInvalid => e
e.record.errors.each do |attr, error|
if error.type == :foo
do_something
end
end
end
But with Rails 3 it seems everything but the invalid attribute and message has been lost. As a result the only way to determine the type is to compare the error message:
rescue ActiveRecord::RecordInvalid => e
e.record.errors.each do |attr, error|
if error == "foobar"
do_something
end
end
end
Which is not at all ideal (eg. what if you have several validations which use the same message?).
Question:
Is there a better way in rails 3.0 to determine the type of validation error?

Check for added? on ActiveModel::Errors:
https://github.com/rails/rails/blob/master/activemodel/lib/active_model/errors.rb#L331
That allows you to do this:
record.errors.added?(:field, :error)

I needed it not only for test purposes, but also for API. I've ended up with monkey patch:
module CoreExt
module ActiveModel
module Errors
# When validation on model fails, ActiveModel sets only human readable
# messages. This does not allow programmatically identify which
# validation rule exactly was violated.
#
# This module patches {ActiveModel::Errors} to have +details+ property,
# that keeps name of violated validators.
#
# #example
# customer.valid? # => false
# customer.errors.messages # => { email: ["must be present"] }
# customer.errors.details # => { email: { blank: ["must be present"] } }
module Details
extend ActiveSupport::Concern
included do
if instance_methods.include?(:details)
fail("Can't monkey patch. ActiveModel::Errors already has method #details")
end
def details
#__details ||= Hash.new do |attr_hash, attr_key|
attr_hash[attr_key] = Hash.new { |h, k| h[k] = [] }
end
end
def add_with_details(attribute, message = nil, options = {})
error_type = message.is_a?(Symbol) ? message : :invalid
normalized_message = normalize_message(attribute, message, options)
details[attribute][error_type] << normalized_message
add_without_details(attribute, message, options)
end
alias_method_chain :add, :details
def clear_with_details
details.clear
clear_without_details
end
alias_method_chain :clear, :details
end
end
end
end
end
# Apply monkey patches
::ActiveModel::Errors.send(:include, ::CoreExt::ActiveModel::Errors::Details)

Related

Rspec to mock my call to external api from my function and return custom response back to the function

So i have written a module which has a function. And now i am trying to test the function.
My question here is that how do i use mocking or stub etc. to test that for a random userid we grab the request to github from the function and return a custom json back to the function.?
Module being tested:
require 'json'
require 'httparty'
module util
GITHUB_URL = 'http://github.com/'
def get_name(id)
begin
response = HTTParty.get("#{GITHUB_URL}#{id}.json")
data = JSON.parse(response.body)
return data.first['actor_attributes']['name']
rescue Exception => e
return nil
end
end
end
My Rspec file:
# coding: UTF-8
require 'spec_helper'
class DummyClass
end
describe 'GET_NAME' do
before(:each) do
#dummy_class = DummyClass.new
#dummy_class.extend(util)
end
context 'for invalid Github ID' do
it 'return nil' do
expect(#dummy_class.getName('invalid')).to be_nil
end
end
end
Thank you all for your help.
Checkout https://github.com/bblimke/webmock or http://fakeweb.rubyforge.org/
Then you do something like:
stub_request(
:post, "#{GITHUB_URL}1.json"
).to_return(
body: { actor_attributes: {name: "Bill"} }.to_json
)
I'm a bigger fan of Webmock than Fakeweb (it's got a few more features), but they'll both do for you, and it's pretty easy to switch between them.

Sidekiq Client Middleware missing the "after" code?

It appears that the code AFTER the yield in my sidekiq client middleware is not executing. No exception was raised (I'm trapping that), so I can't imagine how the code is being skipped (no return inside the lambda, either). Can anyone explain why? Here's the middleware code:
class SidekiqClientWorkless
def initialize(options = nil)
end
def call(worker, msg, queue)
Log.create!(task: "testing", message: "This message gets logged")
begin
yield
rescue Exception => e
require 'ruby-debug'
debugger
end
Log.create!(task: "testing", message: "This message does not get logged")
end
end
Sidekiq.configure_client do |config|
config.redis = { :url => 'redis://user:pwd#barb.redistogo.com:9725/',
:namespace => 'mynamespace' }
config.client_middleware do |chain|
chain.add SidekiqClientWorkless, :foo => 1, :bar => 2
end
end
Have you tried using ensure as per the example from the docs?
I'm honestly not sure why it would be required, but it seems to be...

writing a caching version of Mechanize

I'd like a caching version of Mechanize. The idea is that #get(uri...) checks to see if that uri has been previously fetched, and if so, fetch the response from the cache rather than hitting the web. If not in the cache, it hits the web and saves the response in the cache.
My naive approach doesn't work. (I probably don't need to mention that CachedWebPage is a subclass of ActiveRecord::Base):
class CachingMechanize < Mechanize
def get(uri, parameters = [], referer = nil, headers = {})
page = if (record = CachedWebPage.find_by_uri(uri.to_s))
record.contents
else
super.tap {|contents| CachedWebPage.create!(:uri => uri, :contents => contents)}
end
yield page if block_given?
page
end
end
This fails because the object returned by Mechanize#get() is a complex, circular structure that neither YAML nor JSON want to serialize for storage into the database.
I realize that what I want is to capture the low-level contents before Mechanize parses it.
Is there clean way to do this? I think I can use Mechanize's post_connect hook to access the raw page coming in, but I don't see how to subsequently pass the cached raw page to Mechanize for parsing.
Is there some package I should be using that does web page caching already?
It turns out the solution was simple, albeit not entirely clean. It's a simple matter to cache the results of Mechanize#get() like this:
class CachingMechanize < Mechanize
def get(uri, parameters = [], referer = nil, headers = {})
WebCache.with_web_cache(uri.to_s) { super }
end
end
... where with_web_cache() uses YAML to serialize and cache the object returned by super.
My problem was that by default, Mechanize#get() returns a Mechanize::Page object containing some lambda object, which cannot be dumped and loaded by YAML. The fix was to eliminate those lambdas, which turned out to be rather simple. Full code follows.
class CachingMechanize < Mechanize
def initialize(*args)
super
sanitize_scheme_handlers
end
def get(uri, parameters = [], referer = nil, headers = {})
WebCache.with_web_cache(uri.to_s) { super }
end
# private
def sanitize_scheme_handlers
scheme_handlers['http'] = SchemeHandler.new
scheme_handlers['https'] = scheme_handlers['http']
scheme_handlers['relative'] = scheme_handlers['http']
scheme_handlers['file'] = scheme_handlers['http']
end
class SchemeHandler
def call(link, page) ; link ; end
end
end
the moral: don't try to YAML.dump and YAML.load objects containing lambda or proc
This goes beyond just this example: if you see a YAML error that reads:
TypeError: allocator undefined for Proc
Check to see if there's a lambda or proc in the object you're trying to serialize and deserialize. If you are able (as I was in this case) to replace the lambda with a method call to an object, you should be able to work around the problem.
Hope this helps someone else.
Update
In response to #Martin's request for the definition of WebCache, here 'tis:
# Simple model for caching pages fetched from the web. Assumes
# a schema like this:
#
# create_table "web_caches", :force => true do |t|
# t.text "key"
# t.text "value"
# t.datetime "expires_at"
# t.datetime "created_at", :null => false
# t.datetime "updated_at", :null => false
# end
# add_index "web_caches", ["key"], :name => "index_web_caches_on_key", :unique => true
#
class WebCache < ActiveRecord::Base
serialize :value
# WebCache.with_web_cache(key) {
# ...body...
# }
#
# Searches the web_caches table for an entry with a matching key. If
# found, and if the entry has not expired, the value for that entry is
# returned. If not found, or if the entry has expired, yield to the
# body and cache the yielded value before returning it.
#
# Options:
# :expires_at sets the expiration date for this entry upon creation.
# Defaults to one year from now.
# :expired_prior_to overrides the value of 'now' when checking for
# expired entries. Mostly useful for unit testing.
#
def self.with_web_cache(key, opts = {})
serialized_key = YAML.dump(key)
expires_at = opts[:expires_at] || 1.year.from_now
expired_prior_to = opts[:expired_prior_to] || Time.zone.now
if (r = self.where(:key => serialized_key).where("expires_at > ?", expired_prior_to)).exists?
# cache hit
r.first.value
else
# cache miss
yield.tap {|value| self.create!(:key => serialized_key, :value => value, :expires_at => expires_at)}
end
end
# Prune expired entries. Typically called by a cron job.
def self.delete_expired_entries(expired_prior_to = Time.zone.now)
self.where("expires_at < ?", expired_prior_to).destroy_all
end
end

Allow my own class in Rails to be configured with block syntax

I often see code of the form
RSpec.configure do |config|
config.include ApiHelper
# ## Mock Framework
#
# If you prefer to use mocha, flexmock or RR, uncomment the appropriate line:
#
# config.mock_with :mocha
# config.mock_with :flexmock
# config.mock_with :rr
# Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
config.fixture_path = "#{::Rails.root}/spec/fixtures"
# If you're not using ActiveRecord, or you'd prefer not to run each of your
# examples within a transaction, remove the following line or assign false
# instead of true.
config.use_transactional_fixtures = true
# If true, the base class of anonymous controllers will be inferred
# automatically. This will be the default behavior in future versions of
# rspec-rails.
config.infer_base_class_for_anonymous_controllers = false
config.include FactoryGirl::Syntax::Methods
end
Is there a feature in Rails that lets me do something similar? I want to be able to configure my own library with a similar syntax within config/initializers/my_class.rb
MyClass.configure do |config|
# allow configuration here
end
Nothing special is needed in Rails - it is simple Ruby code. Here is how it can be done:
class MyClass
def self.configure(&block)
a_hash = { :car => "Red" }
puts "Hello"
yield a_hash
puts "There"
end
end
MyClass.configure do |config|
puts "In Block"
puts config[:car]
end
Output:
Hello
In Block
Red
There
I am yielding a hash, but you can yield whatever object you want to.
Rails will load all the Ruby files in the config/initializers directory when starting up the server.
If you want to use the same style for your own custom configurable class then you just have to implement a configure class method that accepts a block and passes a configuration object to that block. e.g.
class MyClassConfiguration
# configuration attributes
end
class MyClass
def self.configure
yield configuration if block_given?
end
def self.configuration
#config ||= MyClassConfiguration.new
end
end
Using phoet's gem would be even easier.
Its worth taking a look at how RSpec does it if you are curious:
The RSpec.configure method is in https://github.com/rspec/rspec-core/blob/master/lib/rspec/core.rb
The Configuration class is implemented in https://github.com/rspec/rspec-core/blob/master/lib/rspec/core/configuration.rb
i don't know if rails provides a helper for that, but i wrote my own tiny solution for this problem that i use in several gems: https://github.com/phoet/confiture
it let's you define configurations:
module Your
class Configuration
include Confiture::Configuration
confiture_allowed_keys(:secret, :key)
confiture_defaults(secret: 'SECRET_STUFF', key: 'EVEN_MOAR_SECRET')
end
end
and have an easy api to do the configuration:
Your::Configuration.configure do |config|
config.secret = 'your-secret'
config.key = 'your-key'
end
besides this, there are a lot of other config tools out there like configatron or simpleconfig.
A global configuration set via the block.
module VkRobioAPI
module Configuration
OPTION_NAMES = [
:client_id,
:redirect_uri,
:display,
:response_type
]
attr_accessor *OPTION_NAMES
def configure
yield self if block_given?
self
end
end
end
module VkRobioAPI
extend VkRobioAPI::Configuration
class << self
def result
puts VkRobioAPI.client_id
end
end
end
Example:
VkRobioAPI.configure do |config|
config.client_id = '3427211'
config.redirect_uri = 'bEWLUZrNLxff1oQpEa6M'
config.response_type = 'http://localhost:3000/oauth/callback'
config.display = 'token'
end
Result:
VkRobioAPI.result
#3427211
#=> nil
If you are using rails, you can just include ActiveSupport::Configurable into your class and off you go. Outside of rails, you will have to add the activesuport gem to your Gemfile or gemspec and then call require 'active_support/configurable'.
Example
class MyClass
include ActiveSupport::Configurable
end
Usage
With block
MyClass.configure do |config|
config.key = "something"
config.key2 = "something else"
end
or inline, without block
MyClass.config.key = "something"
MyClass.config.key2 = "something else"
or like a hash
MyClass.config[:key] = "somehting"
MyClass.config[:key2] = "something else"
Note: key can be any character of your choice.

Mongoid dynamic finder with Mongoid::Errors::DocumentNotFound exception raised

I'm building a REST api for this project that uses Mongoid.
I've setup the following to catch the Mongoid::Errors::DocumentNotFound exception:
rescue_from Mongoid::Errors::DocumentNotFound in my base controller
In my controller I've this query code:
#current_account.users.find(:first, :conditions => {:name => "some_name"})
The above query just returns nil. It doesn't raise the exception.
Tried with another syntax as well:
User.find(:conditions => {:name => "same"}).first
All those methods just runs where internally and afaik where doesn't raise exception, its simply returns []
So what can be the solution to this? I want partially dynamic finder but should raise the exception too?
I've met same problem today, and found another solution.
Set raise_not_found_error to false. so your config/mongoid.yml should be
development:
host: localhost
port: 10045
username: ...
password: ...
database: ...
raise_not_found_error: false
from http://mongoid.org/docs/installation/configuration.html
I believe that Mongoid will only raise a DocumentNotFound exception when using the find method by passing in an object's id (and not with conditions). Otherwise it will return nil. From the Mongoid source:
# lib/mongoid/errors/document_not_found.rb
# Raised when querying the database for a document by a specific id which
# does not exist. If multiple ids were passed then it will display all of
# those.
You will have to check manually to see if you got any results and either raise the DocumentNotFound exception yourself (not great), or raise your own custom exception (better solution).
An example of the former would be something like this:
raise Mongoid::Errors::DocumentNotFound.new(User, params[:name]) unless #current_account.users.first(:conditions => {:name => params[:name]})
Update: I haven't tested any of this, but it should allow you to make calls like (or at least point you in the right direction - i hope!):
#current_account.users.where!(:conditions => {:name => params[:name]})
Which will throw a custom Mongoid::CollectionEmpty error, if the collection returned from the query is empty. Note that it's not the most efficient solution, since in order to find out if the returned collection is empty - it has to actually process the query.
Then all you need to do is rescue from Mongoid::CollectionEmpty instead (or as well).
# lib/mongoid_criterion_with_errors.rb
module Mongoid
module Criterion
module WithErrors
extend ActiveSupport::Concern
module ClassMethods
def where!(*args)
criteria = self.where(args)
raise Mongoid::EmptyCollection(criteria) if criteria.empty?
criteria
end
end
end
end
class EmptyCollection < StandardError
def initialize(criteria)
#class_name = criteria.class
#selector = criteria.selector
end
def to_s
"Empty collection found for #{#class_name}, using selector: #{#selector}"
end
end
end
# config/application.rb
module ApplicationName
class Application < Rails::Application
require 'mongoid_criterion_with_errors'
#...snip...
end
end
# app/models/user.rb
class User
include Mongoid::Document
include Mongoid::Timestamps
include Mongoid::Criterion::WithErrors
#...snip...
end