Rails: method_missing is not called on model when using associations - ruby-on-rails

My current code:
class Product < ActiveRecord::Base
belongs_to :category
end
class Category < ActiveRecord::Base
def method_missing name
true
end
end
Category.new.ex_undefined_method #=> true
Product.last.category.ex_undefined_method #=> NoMethodError: undefined method `ex_undefined_method' for #<ActiveRecord::Associations::BelongsToAssociation:0xc4cd52c>
This happens because of this code in rails which only passes methods that exist to the model.
private
def method_missing(method, *args)
if load_target
if #target.respond_to?(method)
if block_given?
#target.send(method, *args) { |*block_args| yield(*block_args) }
else
#target.send(method, *args)
end
else
super
end
end
end
This is what I want:
Product.last.category.ex_undefined_method #=> true
How can I accomplish this?

Note that the AssociationProxy object only sends on methods that the target claims to respond_to?. Therefore, the fix here is to update respond_to? as well:
class Category < ActiveRecord::Base
def method_missing(name, *args, &block)
if name =~ /^handleable/
"Handled"
else
super
end
end
def respond_to?(name)
if name =~ /^handleable/
true
else
super
end
end
end
In fact, you should always update respond_to? if you redefine method_missing - you've changed the interface of your class, so you need to make sure that everyone knows about it. See here.

Chowlett's response is indeed the way to go imho.
But, if you are using Rails 3*, make sure to include the second argument that has been introduced in the responds_to? definition:
def respond_to?(name,include_private = false)
if name =~ /^handleable/
true
else
super
end
end

Replace
if #target.respond_to?(method)
if block_given?
#target.send(method, *args) { |*block_args| yield(*block_args) }
else
#target.send(method, *args)
end
else
super
end
by
if block_given?
#target.send(method, *args) { |*block_args| yield(*block_args) }
else
#target.send(method, *args)
end
As monkey patch to the AssociationProxy

Related

Rspec - Argument error after overwriting method_missing and respond_to_missing

I have a controller that i want to write rspec for
results_controller.rb
class Api::V1::ResultsController < Api::V1::ApplicationController
before_action :devices
include DataHelper
def show
results = get_dr_results
render json: { data: results }
end
private
def get_dr_results
program_ids = method_defined_in_crucible_helper
end
end
module DataHelper
include Cruciblehelper
def method_missing(method_name, *args, &block)
if condition
do_something
else
super.method_missing(method_name, *args, &block)
end
end
def respond_to_missing?
true
end
end
module CrucibleHelper
def method_defined_in_crucible_helper
end
end
Now in my rspec, I try to mock the method method_defined_in_crucible_helper.
describe Api::V1::DrResultsController, type: :controller do
describe 'GET #show' do
before do
allow_any_instance_of(CrucibleHelper).to receive(:method_defined_in_crucible_helper) { [utility_program.id, utility_program2.id] }
end
context 'returns data' do
context 'returns expected events' do
it 'should return success response with expected events' do
get :show
expect(JSON.parse(response.body)).to eq(expected_response)
end
end
I am getting
Failure/Error:
def respond_to_missing?
true
end
ArgumentError:
wrong number of arguments (given 2, expected 0)
# ./app/helpers/data_helper.rb:72:in `respond_to_missing?'
If I comment out respond_to_missing? method, then my specs are executing OK. Can someone help me in fixing this error?
Ruby Delegator#respond_to_missing? is method take responsible for returning whether a missing method be able to handled by the object or not, it takes 2 parameters: the missing method name and the option include_private.
The best practice is: always define respond_to_missing? when overriding method_missing.
However i do not prefer the way you applied, the reason behind that is The Rule of Least Surprise, take a look:
class DataHelper
def method_missing(method_name, *args, &block)
if method_name.to_s.start_with?('delegate')
puts "a delegate method"
else
super
end
end
def respond_to_missing?(method_name, include_private = false)
true
end
end
d = DataHelper.new
d.respond_to?(:answer) # true
d.answer # `method_missing': undefined method `answer' ... SURPRISE
as you can see, d response that he can responsible for the answer method but when call that method, a method_missing error be raised.
So, you need to make both method_missing and respond_to_missing? match together:
class DataHelper
def method_missing(method_name, *args, &block)
if can_handle?(method_name)
puts "a delegate method"
else
super
end
end
def respond_to_missing?(method_name, include_private = false)
return true if can_handle?(method_name)
super
end
private
def can_handle?(method_name)
method_name.to_s.start_with?('delegate')
end
end
d = D.new
d.respond_to?(:delegate_answer) # true
d.delegate_answer # delegate method
d.respond_to?(:answer) # false
d.answer # error

Code duplication in a module methods of class and instance levels

I have Memoize module, that provides methods for caching of class and instance methods.
module Memoize
def instance_memoize(*methods)
memoizer = Module.new do
methods.each do |method|
define_method method do
#_memoized_results ||= {}
if #_memoized_results.include? method
#_memoized_results[method]
else
#_memoized_results[method] = super()
end
end
end
end
prepend memoizer
end
def class_memoize(*methods)
methods.each do |method|
define_singleton_method method do
#_memoized_results ||= {}
if #_memoized_results.include? method
#_memoized_results[method]
else
#_memoized_results[method] = super()
end
end
end
end
end
This is an example of how I use it:
class Foo
extend Memoize
instance_memoize :instance_method1, :instance_method2
class_memoize :class_method1, :class_method2
...
end
Please advice how to avoid code duplication in this module.
One might define a lambda:
λ = lambda do
#_memoized_results ||= {}
if #_memoized_results.include? method
#_memoized_results[method]
else
#_memoized_results[method] = super()
end
end
And. then:
define_method method, &λ
Please be aware of an ampersand in front of λ, it is used to notice define_method about it’s receiving a block, rather than a regular argument.
I did not get what failed with this approach on your side, but here is a bullet-proof version:
method_declaration = %Q{
def %{method}
#_memoized_results ||= {}
if #_memoized_results.include? :%{method}
#_memoized_results[:%{method}]
else
#_memoized_results[:%{method}] = super()
end
end
}
methods.each do |method|
class_eval method_declaration % {method: method}
end

How to add an instance method from inside of a class method which accepts a block to be executed in the context of the instance

I've entirely reworded this question as I feel this more accurately reflects what I wanted to ask the first time in a less roundabout way.
After instantiating a FormObject, calls to dynamically defined methods do not evaluate their block parameter in the context I'm trying for. For example:
#registration = RegistrationForm.new
#registration.user
# undefined local variable or method `user_params' for RegistrationForm:Class
RegistrationForm calls a class method exposing(:user) { User.new(user_params) } which I would like to have define a new method that looks like this:
def user
#user ||= User.new(user_params)
end
My implementation doesn't use #ivar ||= to cache the value (since falsey values will cause the method to be re-evaluated). I borrowed the idea from rspec's memoized_helpers and I 'think' I understand how it works. What I don't understand is what I should replace class_eval with in lib/form_object/memoized_helpers.rb.
Thank you
lib/form_object/base.rb
class FormObject::Base
include ActiveModel::Model
include FormObject::MemoizedHelpers
attr_reader :params, :errors
def initialize(params = {})
#params = ActionController::Parameters.new(params)
#errors = ActiveModel::Errors.new(self)
end
def save
valid? && persist
end
end
lib/form_object/memoized_helpers.rb
module FormObject
module MemoizedHelpers
private
def __memoized
#__memoized ||= {}
end
def self.included(mod)
mod.extend(ClassMethods)
end
module ClassMethods
def exposing(name, &block)
raise "#exposing called without a block" unless block_given?
class_eval do
define_method(name) { __memoized.fetch(name) { |k| __memoized[k] = block.call } }
end
end
end
end
end
app/forms/registration_form.rb
class RegistrationForm < FormObject::Base
exposing(:user) { User.new(user_params) { |u| u.is_admin = true } }
exposing(:tenant) { user.build_tenant(tenant_params) }
validate do
tenant.errors.each do |key, value|
errors.add("#{tenant.class.name.underscore}_#{key}", value)
end unless tenant.valid?
end
validate do
user.errors.each do |key, value|
errors.add("#{user.class.name.underscore}_#{key}", value)
end unless user.valid?
end
private
def persist
user.save
end
def user_params
params.fetch(:user, {}).permit(:first_name, :last_name, :email, :password, :password_confirmation)
end
def tenant_params
params.fetch(:tenant, {}).permit(:name)
end
end
So, I might have simplified this example too much, but I think this is what you want:
module Exposing
def exposing(name, &block)
instance_eval do
define_method(name, block)
end
end
end
class Form
extend Exposing
exposing(:user) { user_params }
def user_params
{:hi => 'ho'}
end
end
Form.new.user
You can fiddle around here: http://repl.it/OCa

How do I dynamically add setter methods in ruby that Rails will see as attributes for mass assignment?

I have a model with a handful of related date fields.
def started_at_date=(value)
#started_at_date = value
end
def completed_at_date=(value)
#completed_at_date = value
end
...
The getters are handled via method_missing and that works great.
def method_missing(method, *args, &block)
if method =~ /^local_(.+)$/
local_time_for_event($1)
elsif method =~ /^((.+)_at)_date$/
self.send :date, $1
elsif method =~ /^((.+)_at)_time$/
self.send :time, $1
else
super
end
end
def date(type)
return self.instance_variable_get("##{type.to_s}_date") if self.instance_variable_get("##{type.to_s}_date")
if self.send type.to_sym
self.send(type.to_sym).in_time_zone(eventable.time_zone).to_date.to_s
end
end
...
I'd like to add the setters dynamically, but I'm not sure how to do so in a way that avoids ActiveRecord::UnknownAttributeErrors.
I think this would work:
def method_missing(method, *args, &block)
super unless method =~ /_date$/
class_eval { attr_accessor method }
super
end
Could you just use virtual attributes?
Class Whatever < ApplicationModel
attr_accessor :started_at_date
attr_accessor :completed_at_date
#if you want to include these attributes in mass-assignment
attr_accessible :started_at_date
attr_accessible :completed_at_date
end
When you need to access the attributes later instead of calling #started_at_date you would call self.started_at_date, etc.
If I understand you correctly, try:
# in SomeModel
def self.new_setter(setter_name, &block)
define_method("#{setter_name}=", &block)
attr_accessible setter_name
end
Usage:
SomeModel.new_setter(:blah) {|val| self.id = val }
SomeModel.new(blah: 5) # => SomeModel's instance with id=5
# or
#sm = SomeModel.new
#sm.blah = 5

Ruby. Calling Super from the overriden method

I am trying to override a the redirect_to method to add an additional param to the get requests (if thats present)
The redirect_to method is here
module ActionController
...................
module Redirecting
extend ActiveSupport::Concern
include AbstractController::Logger
...........................
def redirect_to(options = {}, response_status = {}) #:doc:
............................
self.status = _extract_redirect_to_status(options, response_status)
self.location = _compute_redirect_to_location(options)
self.response_body = "<html><body>You are being redirected.</body></html>"
end
end
end
Here is how I am trying to override
module ActionController
module Redirecting
def redirect_to(options = {}, response_status = {})
if options
if options.is_a?(Hash)
options["custom_param"] = #custom_param
else
if options.include? "?"
options = options + "&custom_param=true"
else
options = options + "?custom_param=true"
end
end
end
super
end
end
end
I am apparently doing it wrong, and the super method call fails to work the way I wanted it. Hope someone could help.
I believe the problem here is that you are redefining the redirect_to method, not defining in a new place. super cannot call the original because it no longer exists.
The method you are looking for is alias_method_chain
module ActionController
module Redirecting
alias_method_chain :redirect_to, :extra_option
def redirect_to_with_extra_option(options = {}, response_status = {})
if options
...
end
redirect_to_without_extra_option(options, response_status)
end
end
end
Though, I think the more Rails friendly way would be to override redirect_to in your ApplicationController
class ApplicationController
....
protected
def redirect_to(...)
if options
....
end
super
end
end
The benefits to this approach are that you aren't patching rails and the application specific param is now set in your application controller.

Resources