tags:

views:

49

answers:

2

Essentially I'm wondering how to place callbacks on objects in ruby, so that when an object is changed in anyway I can automatically trigger other changes:

(EDIT: I confused myself in my own example! Not a good sign… As @proxy is a URI object it has it's own methods, changing the URI object by using it's own methods doesn't call my own proxy= method and update the @http object)

class MyClass
  attr_reader :proxy
  def proxy=(string_proxy = "")
    begin
      @proxy = URI.parse("http://"+((string_proxy.empty?) ? ENV['HTTP_PROXY'] : string_proxy))
      @http = Net::HTTP::Proxy.new(@proxy.host,@proxy.port)
    rescue
      @http = Net::HTTP
    end
  end
end

m = MyClass.new
m.proxy = "myproxy.com:8080"
p m.proxy
# => <URI: @host="myproxy.com" @port=8080>

m.proxy.host = 'otherproxy.com'
p m.proxy
# => <URI: @host="otherproxy.com" @port=8080>
# But accessing a website with @http.get('http://google.com') will still travel through myproxy.com as the @http object hasn't been changed when m.proxy.host was.
A: 

Your line m.proxy = nil will raise a NoMethodError exception, since nil does no respond to empty?. Thus @http is set to Net::HTTP, as in the rescue clause.

This has nothing to do with callbacks/setters. You should modify your code to do what you want (e.g. calling string_proxy.blank? if using activesupport).

Marc-André Lafortune
Sorry about that, I managed to confuse myself and ask the wrong question (I simplified my code to bring it onto stackoverflow) — thanks for your help though!
JP
A: 

I managed to figure this one out for myself!

# Unobtrusive modifications to the Class class.
class Class
  # Pass a block to attr_reader and the block will be evaluated in the context of the class instance before
  # the instance variable is returned.
  def attr_reader(*params,&block)
    if block_given?
      params.each do |sym|
        # Create the reader method
        define_method(sym) do
          # Force the block to execute before we…
         self.instance_eval(&block)
          # … return the instance variable
          self.instance_variable_get("@#{sym}")
        end
      end
    else # Keep the original function of attr_reader
      params.each do |sym|
        attr sym
      end
    end
  end
end

If you add that code somewhere it'll extend the attr_reader method so that if you now do the following:

attr_reader :special_attr { p "This happens before I give you @special_attr" }

It'll trigger the block before it gives you the @special_attr. It's executed in the instance scope so you can use it, for example, in classes where attributes are downloaded from the internet. If you define a method like get_details which does all retrieval and sets @details_retrieved to true then you can define the attr like this:

attr_reader :name, :address, :blah { get_details if @details_retrieved.nil? }
JP