How to spy on a Hash in Ruby

February 24, 2010

Let's say you're dealing with a large Rails codebase and you've got a Hash stored in a global variable or a constant and you want to know who is changing that Hash. Here's a contrived example:

IMPORTANT_STUFF = {
  :password => "too many secrets"
}

def change_password(h)
  h[:password] = "FAIL"
end

def print_password
  puts IMPORTANT_STUFF[:password]
end

print_password
change_password(IMPORTANT_STUFF)
print_password

Here it's pretty obvious where the Hash gets changed, but as I said, imagine you are trying to figure this out in a much larger codebase. Something is changing the value of IMPORTANT_STUFF and you don't know what. So how do you figure out what is? Easy, you do what Lester Freeman would do!

Lester Freeman from The Wire

We set up a sting! We put a wire tap on IMPORTANT_STUFF and monitor all communication with IMPORTANT_STUFF. So how do we do that? Let's create a class that proxies all communication with a Hash:

class HashSpy

  def initialize(hash={})
    @hash = hash
  end

  def method_missing(method_name, *args, &block)
    puts "***** hash access"
    puts "  before: #{@hash.inspect}"
    r = @hash.send(method_name, *args, &block)
    puts "  after: #{@hash.inspect}"
    puts "  backtrace:\n    #{caller.join("\n    ")}"
    r
  end

end

This uses a couple of interesting Ruby techniques. First, we just pass the actual Hash to the constructor. Then, we use method missing so that any method that is called on the HashSpy will be then called on the Hash and the return value of that method call with be called instead. Note that in Ruby 1.8, this isn't a transparent proxy because if you called class on the HashSpy, you would get HashSpy, not Hash. In Ruby 1.9, you can have your object inherit from BasicObject, which won't have those methods, making it easier to be a transparent proxy. In Ruby 1.8, you can use Jim Weirich's Blank Slate pattern

In HashSpy's method missing, we use caller to get a backtrace of the current call stack, which will tell us who the perpetrator is.

So, if we just change IMPORTANT_STUFF to be created like this:

IMPORTANT_STUFF = HashSpy.new(
  :password => "too many secrets"
)

Now when we run the program, we'll get output something like this:

***** hash access
  before: {:password=>"too many secrets"}
  after: {:password=>"too many secrets"}
  backtrace:
    hash_spy.rb:27:in `print_password'
    hash_spy.rb:30
too many secrets
***** hash access
  before: {:password=>"too many secrets"}
  after: {:password=>"FAIL"}
  backtrace:
    hash_spy.rb:23:in `change_password'
    hash_spy.rb:31
***** hash access
  before: {:password=>"FAIL"}
  after: {:password=>"FAIL"}
  backtrace:
    hash_spy.rb:27:in `print_password'
    hash_spy.rb:32
FAIL

And by reading through the output, we can see that the second time the hash is accessed is when the value is changed, so the perpetrator is on line 23 of hash_spy.rb in the change_password method. Here's the entire script in one gist for reference.

Posted in Technology | Tags Ruby, Rails | 4 Comments

Customizing Generators in Rails 3

January 13, 2010

As you probably already know, Rails 3 is just around the corner. There are some pretty nice features being added and one of them is the ability to customize the way the generators work. I personally prefer Haml over ERB, RSpec over Test::Unit and Factory Girl over Fixtures. So let's see how we can configure a Rails 3 app to do that.

First, follow Yehuda's instructions on how to create a Rails 3 app. Next, you have to tell Rails that you want to use Haml, RSpec and Factory Girl. First, add this somewhere in the Gemfile:

gem "haml"

only :test do
  gem "rspec"
  gem "rspec-rails"
  gem "factory_girl"
end

Then, re-run the bundler and initialize the Haml plugin:

$ gem bundle
$ bin/haml --rails .

Finally, you have to install the custom generators for the frameworks that you want to use. I found a repo on github that already had RSpec, so I forked it and added Haml and Factory Girl. Clone the repo into the lib/generators directory of your app:

$ git clone git://github.com/pjb3/rails3-generators.git lib/generators

Now, in the config/application.rb file in your app, near the bottom there is a section related to config.generators. Put this in that section:

config.generators do |g|
  g.template_engine :haml
  g.test_framework :rspec, :fixture => true, :views => false
  g.fixture_replacement :factory_girl, :dir => "spec/factories"
end

Here is where we reap the benefits of the modularity in Rails 3. What this says is that we want to use Haml as the template enging, we want to use RSpec as the test framework and we want to generate fixtures with our generated specs, but we don't want to generate view specs and that instead of fixtures, we actually want to use factory girl and we want the factories to be put into spec/factories. Whew! So does this all work?

$ script/generate rspec:install
$ script/generate scaffold person name:string dob:date age:integer male:boolean bio:text

At this point you should see that Rails has generated what we want, which is scaffolding that uses Haml for the views, RSpec for the tests and Factory Girl for the fixtures. Run the migrations, start the server and open the browser http://localhost:3000/people to see your scaffolding in action.

Now if you try to actually run rake spec, you'll get an error, at least I do. I'm not sure that RSpec 1.X is going to ever work with Rails 3. I think the intent is for RSpec 2 to be compatible with Rails 3, so keep an eye out for that.

Posted in Technology | Tags FactoryGirl, RSpec, Rails, HAML | 2 Comments

Ain't Nothing But A G Thang

October 9, 2009

You've probably seen more than a few articles on the web showing how to build a Rack app. If not, here's a good one to start with. You'll quickly see that building a Rack app is really simple, which is why Rack is awesome, because it's simple. But what about writing a Rack-compliant server? Well it turns out that is pretty easy as well.

I just pushed a little Rack-compliant HTTP Server that I wrote using GServer to github. The whole thing is less than 200 lines of code. The core of it is short enough that I can explain how it works here.

First, GServer. GServer, which is short for "Generic Server" makes it pretty simple to create a multithreaded TCP Server. Taking out some error handling code, here's what the GServer looks like for our Rack HTTP Server:

module GThang
  class HttpServer < GServer

    attr_reader :port, :rack_app

    def initialize(options={})
      @port = options[:Port] || 8080
      @rack_app = options[:rack_app]
      super(@port)
    end

    def serve(socket)
      RackHandler.new(socket, rack_app, port).handle_request
    end

  end
end

So all there is to a GServer is basically a serve method. This will be called each time a client connects to the server. The argument to the method is the client socket connection. You read and write data from the socket as you see fit for your application. As you can see here, we just pass the socket, along with the rack app and the port to the RackHandler initializer and then call handle_request on that. We'll look at how you setup the rack app in a minute, but first let's take a look at the meat of what the RackHandler does. The handle_request method looks like this:

def handle_request
  return unless add_rack_variables_to_env
  return unless add_connection_info_to_env
  return unless add_request_line_info_to_env
  return unless add_headers_to_env
  send_response(app.call(env))
end

So what happens is the various add_ methods build up the rack environment. Once the environment is ready, we call the rack app. The rack app responds with the standard 3 element array, which we pass off to the send_response method, which writes the actual http response to the client. Take a look at the full code for this on github for the details.

Now the fun part is that we now have a fully functional HTTP server that is capable of acting as a file server or serving a Rails app. All we have to do is give the HttpServer the correct Rails app. If you look in the examples, you see this for the file server:

GThang::HttpServer.run(
  Rack::CommonLogger.new(
    Rack::Lint.new(
      Rack::Directory.new(root, Rack::File.new(root)))),
  :Port => 8080)

Now I choose to write it this way to make it clear what is actually happening. You will normally see the builder DSL used to configure a rack app, which would look like this:

use Rack::CommonLogger.new
use Rack::Lint.new
use Rack::Directory.new(root)
run Rack::File.new(root)

This is obviously a lot cleaner, but to understand how Rack works, you have to realize that all this is doing is what we see in the first example. A Rack app with Rack middleware is simple a chain of apps that call the next app in the chain, possibly modifying the environment or response before or after the rest of the chain is called.

So there you have it, beauty in simplicity.

Posted in Technology | Tags Rack, Ruby, Rails | 8 Comments

ExtJS On Rails

September 23, 2009

If you are looking to give ExtJS a spin, I've created a Rails Application Template to setup a Rails app with ExtJS installed. To use it, just do:

rails -m http://tinyurl.com/extjs-rb my-extjs-app

To see a sample of this in use, take a look at this Rails app. It is a port of Chapter 1 of ExtJS In Action to Rails. Assuming you have Rails 2.3.2 installed, you should be able to download the Rails app from that git repo, run script/server and then try out the app at http://localhost:3000.

UPDATE: This code actually refers to the code in the sample chapter on the Manning website. Chapter 1 in the latest version of the book has actually been completely re-written, so I've included the sample chapter in the git repo. Also be sure to check out the author's blog which covers the development of the book.

Posted in Technology | Tags Javascript, Rails, ExtJS | 0 Comments

Why Rails 3 Will Require Ruby 1.8.7

September 14, 2009

This past weekend I attended the Windy City Rails conference. It was in a great location in the heart of downtown Chicago and seemed to have a pretty good turn out. There were many great talks but this blog post will be focusing on a specific talk, and more precisely, part of a talk. Yehuda Katz gave a talk on the status of Rails 3. One of the things that he mentioned, which you may have already heard, is that Rails 3 will require Ruby 1.8.7 or higher, dropping support for Ruby 1.8.6. He also mentioned why they are doing this and I found the reason to be interesting. It's not that the Rails core team wants to try to take advantage of any specific new features, it's that Ruby 1.8.6 has a bug which has been fixed in 1.8.7.

To see the bug in action, I recommend that you install Ruby Version Manager (rvm). Once you have installed rvm, install Ruby 1.8.6 and Ruby 1.8.7.

The bug is that in Ruby 1.8.6, the hash method for Hash doesn't generate the same hash code for different hashes with the same values:

$ rvm use 1.8.6
$ irb
ruby-1.8.6-p383 > {:x => 1}.hash
 => 1313270 
ruby-1.8.6-p383 > {:x => 1}.hash
 => 1307060 
ruby-1.8.6-p383 > {:x => 1}.hash
 => 1296440 
ruby-1.8.6-p383 > {:x => 1} == {:x => 1}
 => true
ruby-1.8.6-p383 > h = {{:x => 1} => "foo"}
 => {{:x=>1}=>"foo"} 
ruby-1.8.6-p383 > h[{:x => 1}]
 => nil

So despite the fact that two hashes have the same values and are equal, you can't use a hash as a key in a hash, because that depends on the hash codes of the values being equal, which they aren't. This is fixed in Ruby 1.8.7:

$ rvm use 1.8.7
$ irb
ruby-1.8.7-p174 > {:x => 1}.hash
 => 327875 
ruby-1.8.7-p174 > {:x => 1}.hash
 => 327875 
ruby-1.8.7-p174 > {:x => 1}.hash
 => 327875 
ruby-1.8.7-p174 > {:x => 1} == {:x => 1}
 => true 
ruby-1.8.7-p174 > h = {{:x => 1} => "foo"}
 => {{:x=>1}=>"foo"} 
ruby-1.8.7-p174 > h[{:x => 1}]
 => "foo" 

This is important because you could use a hash cache calls to a method that expects a hash, but only if you can use a hash as the key. This is one of the main reasons Rails 3 is going to require 1.8.7. They could make it worth for both 1.8.6 and 1.8.7 and higher, but why? It simplifies things to just require that you upgrade to Ruby 1.8.7 to use Rails 3. If you are using 1.8.6, this is probably a gotcha that you should be aware of.

Posted in Technology | Tags Ruby, Rails | 0 Comments

  Older Articles >>