Rails delegate and alias - infinite loop? - ruby-on-rails

I don't understand the following infinite loop involving delegate and alias
class Company
field :name
end
class Employee < Professional
include CompanyMember
end
class Professional
include UserProfile
end
module CompanyMember
belongs_to :company
delegate :name, to: :company, prefix: true
alias :organization_name :company_name
end
module UserProfile
def to_s
out = "#{name} "
out += "(#{organization_name})" if respond_to?(:organization_name)
end
def inspect
to_s + super
end
end
I have an Employee with a missing company, and I have the following infinite loop
app/models/concerns/user_profile.rb:94:in `inspect'
app/models/concerns/company_member.rb:8:in `rescue in company_name'
app/models/concerns/company_member.rb:8:in `company_name'
app/models/concerns/user_profile.rb:89:in `to_s'
app/models/concerns/user_profile.rb:94:in `inspect'
app/models/concerns/company_member.rb:8:in `rescue in company_name'
app/models/concerns/company_member.rb:8:in `company_name'
app/models/concerns/user_profile.rb:89:in `to_s'

The problem is in your override of inspect. When you attempt to call a delegated name on a missing company, NoMethodError is raised. Delegated method then tries to rescue it and show you helpful error message.
exception = %(raise DelegationError, "#{self}##{method_prefix}#{method} delegated to #{to}.#{method}, but #{to} is nil: \#{self.inspect}")
You see, it calls inspect to get printable version of your object. Unfortunately, it calls .to_s, which is where infinite recursion begins.

Related

Undefined class method using Rails Concerns

I did everything pretty much as described here: question
But I keep getting error:
NoMethodError: undefined method `parent_model' for Stream (call 'Stream.connection' to establish a connection):Class
In model/concerns faculty_block.rb
module FacultyBlock
extend ActiveSupport::Concern
included do
def find_faculty
resource = self
until resource.respond_to?(:faculty)
resource = resource.parent
end
resource.faculty
end
def parent
self.send(self.class.parent)
end
end
module ClassMethods
def parent_model(model)
##parent = model
end
end
end
[Program, Stream, Course, Department, Teacher].each do |model|
model.send(:include, FacultyBlock)
model.send(:extend, FacultyBlock::ClassMethods) # I added this just to try
end
In initializers:
require "faculty_block"
method call:
class Stream < ActiveRecord::Base
parent_model :program
end
It seems that the Stream is loaded before loading concern, make sure that you have applied the concerns inside the class definition. When rails loader matches class name for Stream constant, it autoloads it before the finishing evaliation of the faculty_block, so replace constants in it with symbols:
[:Program, :Stream, :Course, :Department, :Teacher].each do |sym|
model = sym.to_s.constantize
model.send(:include, FacultyBlock)
model.send(:extend, FacultyBlock::ClassMethods) # I added this just to try
end

Stubing a model constant for assosiation (undefined method `relation_delegate_class')

I have a Lesson model which is assosiated with Permission model:
app/models/lesson.rb:
class Lesson < ActiveRecord::Base
has_many :permissions, :class_name => 'Permission', as: :permissible, dependent: :destroy
...
def create_permissions
Permission::DEFAULTS[:lesson].each do |action, value|
..
end
end
app/models/permission.rb:
class Permission < ActiveRecord::Base
DEFAULTS = {
lesson: {some_more_action: 15}
}
belongs_to :permissible, polymorphic: true
end
I used RSpec stub_const method to stub a nested defined constant:
spec/models/lesson_spec.rb:
require 'rails_helper'
RSpec.describe Lesson, :type => :model do
describe "#create_permissions" do
let!(:lesson) { FactoryGirl.build_stubbed :lesson }
before(:each) do
stub_const('Permission::DEFAULTS', {lesson: {some_action: 5}})
end
it 'should create permissions' do
lesson.create_permissions
permission = lesson.permissions.first
...
end
end
end
But the spec fails with error:
Failure/Error: permission = lesson.permissions.first
NoMethodError:
undefined method `relation_delegate_class' for Permission:Module
# /home/install/.rvm/gems/ruby-2.1.4/gems/activerecord-4.2.3/lib/active_record/relation/delegation.rb:112:in `relation_class_for'
# /home/install/.rvm/gems/ruby-2.1.4/gems/activerecord-4.2.3/lib/active_record/relation/delegation.rb:106:in `create'
# /home/install/.rvm/gems/ruby-2.1.4/gems/activerecord-4.2.3/lib/active_record/associations/collection_association.rb:41:in `reader'
# /home/install/.rvm/gems/ruby-2.1.4/gems/activerecord-4.2.3/lib/active_record/associations/builder/association.rb:115:in `permissions'
# ./spec/models/lesson_spec.rb:285:in `block (3 levels) in <top (required)>'
It looks as if permissions are no longer rails assosiatiated with lesson model. Any ideas on how to get this round.
Gem versions: rspec-3.3.0, rspec-rails-3.3.3, rails-4.2.3.
Take a closer look to this error message:
NoMethodError:
undefined method `relation_delegate_class' for Permission:Module
Permission is described as a Module here, not as a Class. I think this happens because at the moment of stubbing nested const, Permission class has not been loaded yet, and RSpec has to stub it too via Module.
Try this as a workaround:
before(:each) do
Permission # load real ActiveRecord Permission class
stub_const('Permission::DEFAULTS', {lesson: {some_action: 5}})
end
EDIT: As a side note, I don't think that exposing nested constants to other classes is a good idea, you have little control over the constant (just changing a name or value) and you can't wrap any behavior like you do it with a method. I recommend to change public Permission's API to a using a method, it will also be easier to stub:
class Permission < ActiveRecord::Base
DEFAULTS = {
lesson: {some_more_action: 15}
}
def self.defaults
DEFAULTS
end
end
And in your spec:
before(:each) do
allow(Permission).to receive(:defaults).and_return(lesson: {some_action: 5})
end
The only option that I came upon was just to set constant explicitly in an example:
before(:each) do
Permission::DEFAULTS = {lesson: {some_action: 5}}
end
But I don't feel it's a good idea. It rises a couple of warnings also:
spec/models/lesson_spec.rb:279: warning: already initialized constant Permission::DEFAULTS
app/models/permission.rb:2: warning: previous definition of DEFAULTS was here

Why alias_method fails in Rails model

class Country < ActiveRecord::Base
#alias_method :name, :langEN # here fails
#alias_method :name=, :langEN=
#attr_accessible :name
def name; langEN end # here works
end
In first call alias_method fails with:
NameError: undefined method `langEN' for class `Country'
I mean it fails when I do for example Country.first.
But in console I can call Country.first.langEN successfully, and see that second call also works.
What am I missing?
ActiveRecord uses method_missing (AFAIK via ActiveModel::AttributeMethods#method_missing) to create attribute accessor and mutator methods the first time they're called. That means that there is no langEN method when you call alias_method and alias_method :name, :langEN fails with your "undefined method" error. Doing the aliasing explicitly:
def name
langEN
end
works because the langEN method will be created (by method_missing) the first time you try to call it.
Rails offers alias_attribute:
alias_attribute(new_name, old_name)
Allows you to make aliases for attributes, which includes getter, setter, and query methods.
which you can use instead:
alias_attribute :name, :langEN
The built-in method_missing will know about aliases registered with alias_attribute and will set up the appropriate aliases as needed.

ActiveRecord Relationships : undefined method for nil:NilClass

Consider the following:
class Manager < ActiveRecord::Base
has_many :employees
end
class Employee < ActiveRecord::Base
belongs_to :manager
end
employee = Employee.first
puts employee.manager.name
If for some reason an employee does not have a manager I get this:
undefined method `name' for nil:NilClass
Which makes sense. However, is there a clean/suggested way to handle this, so I don't always have to check and see if an employee actually has a manager before I ask for the manager's name?
Try:
puts employee.manager.name unless employee.manager.nil?
Or:
puts (employee.manager.nil? ? "No manager" : employee.manager.name)
Which is equivalent in this case to:
puts (employee.manager ? employee.manager.name : "No manager")
(Equivalent as long as employee.manager can't return false.)
Don't try to be too clever. If you're going to use this repeatedly, make it a function.
class Employee
def manager_name
manager.try(:name).to_s # force empty string for nil
end
end
You can check using the has_attribute? method:
employee.has_attribute? :manager
So something like:
puts employee.manager.name if employee.has_attribute? :manager
I tend to prefer puts employee.manager.try(:name)
The try method returns nil if it was called on nil, but will execute the method properly if manager wasn't nil.
If you want default text:
puts employee.manager.try(:name) || "No Manager"

Rails console includes differently than model

tl;dr: include MyModule brings MyModule's functions into scope when run in rails console, but not when in a rails model. I don't understand and would like to be able to access these functions in my model.
I have this file lib/zip_validator.rb, which I would like to use to validate user input for my User model.
module ActiveModel::Validations::HelperMethods
def validates_zip(*attr_names)
validates_with ZipValidator, _merge_attributes( attr_names )
end
end
class ZipValidator < ActiveModel::EachValidator
def validate_each( record, attr_name, value )
unless is_legitimate_zipcode( self.zip )
record.errors.add( attr_name, :zip, options.merge( value: value ))
end
end
end
In rails console, I can do
irb(main):005:0> include ActiveModel::Validations::HelperMethods
=> Object
irb(main):006:0> validates_zip
NoMethodError: undefined method `validates_with' for main:Object
from /home/bistenes/Programming/myapp/lib/zip_validator.rb:3:in `validates_zip'
from (irb):6
from /usr/lib64/ruby/gems/1.9.1/gems/railties-3.2.13/lib/rails/commands/console.rb:47:in `start'
from /usr/lib64/ruby/gems/1.9.1/gems/railties-3.2.13/lib/rails/commands/console.rb:8:in `start'
from /usr/lib64/ruby/gems/1.9.1/gems/railties-3.2.13/lib/rails/commands.rb:41:in `<top (required)>'
from script/rails:6:in `require'
from script/rails:6:in `<main>'
Look! Clearly it found validates_zip, because it's complaining about a call within that method. Maybe that complaint will go away when I actually try and call it from the model where it's going to be used, app/models/user.rb:
class User < ActiveRecord::Base
include ActiveModel::Validations::HelperMethods
attr_accessible :first_name, :last_name, :zip, :email, :password,
:password_confirmation, :remember_me, :confirmed_at
validates_presence_of :first_name, :last_name, :zip
validates_zip :zip
When I attempt to start the server (Thin), it errors out with:
/usr/lib64/ruby/gems/1.9.1/gems/activerecord-3.2.13/lib/active_record/dynamic_matchers.rb:55:in `method_missing': undefined method `validates_zip' for #<Class:0x00000003e03f28> (NoMethodError)
from /home/bistenes/Programming/myapp/app/models/user.rb:38:in `<class:User>'
What in the world is going on? Why does the server fail to find a function that the console found?
Consider this:
module M
def m
end
end
class C
include M
end
Given that, you can say C.new.m without complaint but you can't C.m. So what's going on here? When you say include M, the methods in M will be added to C as instance methods but if you wanted to say:
class C
include M
m
end
then m would have to be a class method since self when you call m here is the class itself. You can do this using the included hook in the module:
module M
def self.included(base)
base.class_exec do
def m
end
end
end
end
class C
include M
m # This works now
end
In your case, you'd have something like this:
module ActiveModel::Validations::HelperMethods
def self.included(base)
base.class_exec do
def self.validates_zip(*attr_names)
validates_with ZipValidator, _merge_attributes( attr_names )
end
end
end
end
Now your User will have a validates_zip class method that you can call in the desired context.

Resources