Guide to Rails Metal 39

Posted by mikong on February 03, 2009

  Writing a Rails Metal app can make you realize just how spoiled we’ve become with all the convenience that comes with Rails. Without the controller and view helpers, it can become a painful experience. Here’s a guide to help make it a better experience.

For this guide, we’re writing a Widget Refresher Metal app. Supposedly, the widget page of our Rails application gets called too much, and so we want to take advantage of Metal. Under our project’s app/metal folder, we create refresher.rb:

  class Refresher < Rails::Rack::Metal
    def self.call(env)
      refresher = RefresherApp.new
      refresher.call(env)
    end
  end

  class RefresherApp
    def call(env)
      # refresh widget path: /widgets/:id/refresh
      if env["PATH_INFO"] =~ /^\/widgets\/(\d+)\/refresh/
        widget_id = $1
        prepare(env, widget_id)
        refresh
      else
        [404, { "Content-Type" => "text/html" }, "Not Found"]
      end
    end

    # to setup the environment
    def prepare(env, widget_id)
      ...
    end

    # the heart of our Metal app
    def refresh
      ...
    end
  end

I like to create a separate class RefresherApp instead of just writing all of it inside the Refresher class (the one that extends from Rails::Rack::Metal). When your Metal app becomes more than just a trivial hello world app, you’ll be needing a bunch of methods calling each other. Since the call method in the Metal app is a class method, putting all the code in one class will require all these methods to be class methods as well. And I think that looks ugly. Feel free to stick it all in one class if you want. If you do, you can change the context to self so you don’t have to keep on defining each method as self.method:

  class Refresher < Rails::Rack::Metal

    # the methods in here are class methods
    class << self
      def call(env)
        ...
      end

      def method
        ...
      end
    end

  end

For the rest of the guide, we’re using my approach. Also, note that when developing a Metal app, you need to keep on restarting your server for your code changes to take effect.

Request and Session

To access the request and the parameters in it, you can use this code:

request = Rack::Request.new(env)
params = request.params
params['mykey'] # String keys, so not params[:mykey]

As you can see, the keys will be of class String, not Symbol. Now for the session, you can get it from the environment:

session = env['rack.session']

We can move all these code into our prepare method. In addition, we can set the params[:id] (using a Symbol if you want), so that in our main refresh method, it would be like in a Rails controller. With the session, we can get the current user. We can also define other methods to make things more like writing code for a Rails controller. This is how it looks like:

  attr_reader :request, :session, :current_user

  def params
    @request.params
  end

  def logged_in?
    !!current_user
  end

  def prepare(env, widget_id)
    @request = Rack::Request.new(env)
    params[:id] = widget_id
    @session = env["rack.session"]
    @current_user = session[:user_id] ? User.find(session[:user_id]) : false
  end

With these out of the way, we go into writing the code for the main method called refresh.

refresh and ActiveRecord

ActiveRecord works out of the box, no setup needed. Cool! Let’s say we just need to return the status of widget to the client side:

  def refresh
    @widget = Widget.find(params[:id])

    return [200, { "Content-Type" => "text/html" }, @widget.status]
  end

We can also send javascript code, or other content types back to our client. Just make sure to set your content type properly. Let’s also add some simple checking if the user is logged in:

  def refresh
    @widget = Widget.find(params[:id])

    if logged_in?
      return [200, { "Content-Type" => "text/javascript" }, "Element.update('status', '#{@widget.status}');"]
    else
      return [200, { "Content-Type" => "text/javascript" }, "Element.update('message', 'Must be logged in for widget status to refresh');"]
    end
  end

When returning more complex javascript however, it’s probably better to escape the newlines and the quotes or we’ll get parsing errors on the browser side. Rails provides a helper method called escape_javascript, but a Metal app doesn’t have access to helpers by default. So…

View Helpers

To use helpers in your Metal app, just include the modules you need:

  include ActionView::Helpers::JavascriptHelper # so escape_javascript works
  include WidgetsHelper # for example

I prefer to avoid including too much of these helpers though.

Request Forgery Protection

If the request is not a GET request, we may need to verify the authenticity token. Here’s one way to do it:

  def refresh
    # before everything else
    return redirect_to_widgets_response unless verified_request?

    # everything else
    ...
  end

  def redirect_to_widgets_response
    return [302, { "Content-Type" => "text/html", "Location" => "/widgets" },
      "<html><body><a class=\"/widgets\">Redirecting...</a></body></html>"]
  end

  # Based on Rails method of the same name but simplified, i.e. no need to check if:
  #   - protection is disabled
  #   - request method is :post
  #   - format is verifiable
  def verified_request?
    form_authenticity_token == params['authenticity_token']
  end

  def form_authenticity_token
    session[:_csrf_token] ||= ActiveSupport::SecureRandom.base64(32)
  end

More Challenges

There are other challenges you may encounter in writing your Rails Metal app. I have tried rendering a partial by directly using ERB but it’s too ugly to show here. And I’ve also struggled with performance. Not all business logic can simply be translated to a Metal app to be fast. It is recommended for very simple things only, or it may not be worth it. Anyway, I hope this guide clears up a few things. Also, if you have better ways of doing any of the above, feel free to post in the comments. Thanks!

  • Posted in Rails, Ruby
  • Meta 39 comments, permalink, rss

Setting up your Ubuntu Server for Merb 34

Posted by mikong on November 27, 2008

I prepared a documentation about this for a company and I thought I might as well post it here (modified a bit). I had to make sure the setup worked from scratch so I tested it using Sun’s VirtualBox.

Table of Contents

  1. Ubuntu Server
  2. Package Manager: apt-get
  3. Ruby
  4. RubyGems
  5. Apache 2
  6. MySQL
  7. Merb + DataMapper (+ SQLite 3?)
  8. Phusion Passenger
  9. Deploy the Merb App

1. Ubuntu Server

This setup was tested on the Ubuntu Server OS Hardy Heron, the latest version with LTS as of writing. Go to the Ubuntu website to download the installer of the Ubuntu version you want, preferably the latest with LTS.

The following instructions may work in other Debian-based OSes because it relies mainly on the apt-get package manager.

2. Package Manager: apt-get

Attribution: The content of this section, Package Manager: apt-get, is copied from Configure the Package Manager section of this Slicehost wiki page.

Ubuntu’s package management is done through apt-get. But it starts out handicapped. You need to edit a configuration file to add some additional sources.

sudo nano /etc/apt/sources.list

Uncomment these lines (remove the “# “; ignore if not commented out).

# deb archive.ubuntu.com/ubuntu/ hardy main restricted universe
# deb-src archive.ubuntu.com/ubuntu/ hardy main restricted universe
...
# deb security.ubuntu.com/ubuntu hardy-security main restricted universe
# deb-src security.ubuntu.com/ubuntu hardy-security main restricted universe

Now update the repository index and upgrade your built-in software:

sudo apt-get update && sudo apt-get upgrade

3. Ruby

To install Ruby, execute the following in the server’s command line:

sudo apt-get install ruby1.8-dev ruby1.8 ri1.8 rdoc1.8 irb1.8 libreadline-ruby1.8 libruby1.8 libopenssl-ruby
sudo ln -s /usr/bin/ruby1.8 /usr/local/bin/ruby
sudo ln -s /usr/bin/ri1.8 /usr/local/bin/ri
sudo ln -s /usr/bin/rdoc1.8 /usr/local/bin/rdoc
sudo ln -s /usr/bin/irb1.8 /usr/local/bin/irb

4. RubyGems

First, install the essential tools for compiling:

sudo apt-get install build-essential

Then, install RubyGems (latest is version 1.3.1 as of writing; update the script if necessary):

mkdir sources; cd sources
wget rubyforge.org/frs/download.php/45905/rubygems-1.3.1.tgz
tar zxvf rubygems-1.3.1.tgz
cd rubygems-1.3.1
sudo ruby setup.rb
cd ~
sudo ln -s /usr/bin/gem1.8 /usr/local/bin/gem
sudo gem update --system

5. Apache 2

To install Apache, simply execute the following in the server’s command line:

sudo apt-get install apache2

6. MySQL

To install MySQL:

sudo apt-get install mysql-server mysql-client

Note: During installation, specify the root password for the MySQL when it’s asked.

7. Merb + DataMapper (+ SQLite 3?)

Unfortunately, installing the latest version of Merb (1.0 as of writing) requires SQLite 3. So for the moment,

sudo apt-get install sqlite3 libsqlite3-dev

Then, to install Merb and make it work with MySQL:

sudo gem install merb
sudo apt-get install libmysqlclient15-dev
sudo gem install do_mysql

8. Phusion Passenger

To install Passenger, do the following (from Phusion Passenger’s install page):

  1. Open a terminal and type:
    sudo gem install passenger
  2. Type:
    sudo passenger-install-apache2-module

    And follow the instructions.

Re-run ‘passenger-install-apache2-module’ if you were asked to install other dependencies. For example, if you followed the instructions in this document, you will probably be asked to install development libraries of apache2, so:

sudo apt-get install apache2-prefork-dev
sudo passenger-install-apache2-module

After that, you will probably be asked to edit your Apache configuration file (see /etc/apache2/httpd.conf) to add the following (note that version numbers may vary):

LoadModule passenger_module /usr/lib/ruby/gems/1.8/gems/passenger-2.0.3/ext/apache2/mod_passenger.so
PassengerRoot /usr/lib/ruby/gems/1.8/gems/passenger-2.0.3
PassengerRuby /usr/bin/ruby1.8

Since a Merb app is a Rack-based Ruby application, check out section 4 (or “Deploying a Rack-based Ruby Application” section) of the Phusion Passenger User’s Guide. From section 4.2 (or “Deploying to a virtual host’s root” section),

Add a virtual host entry to your Apache configuration file. The virtual host’s document root must point to the application’s public folder. For example,

  <VirtualHost *:80>
    ServerName www.yourdomain.com
    DocumentRoot /var/www/apps/my_app/public
  </VirtualHost>

Your Merb app needs to satisfy a certain directory layout for Passenger to work. This is described in section 4 of the User’s guide. In the root directory of your application, you need a public folder (which a standard Merb app should already have), a tmp folder (simply create an empty one), and a config.ru file containing the configuration detailed in section 4.5.4 of the User’s guide.

The Phusion Passenger User’s Guide is quite a comprehensive documentation. If you encounter any problems, be sure to check its other sections like the one on Troubleshooting. For example, if you have static assets (such as stylesheets) in your application’s public folder, you are likely to encounter the problem described in section 6.3.4. The solution is also there.

9. Deploy the Merb App

Basically, to get your Merb App to running you only need to do the following:

  1. Make sure the gem dependencies are satisfied by either installing the gems in your server, or freezing them in your app.
  2. Make sure your app satisfies the requirements of Passenger (i.e. tmp and public folders and config.ru file, see Phusion Passenger section above).
  3. Place a copy of the application in an appropriate directory such as /var/www/apps/my_app. Wherever it is, make sure that it is consistent with the specified directory in the Apache configuration file (see Phusion Passenger section above).
  4. Prepare your database: create it, configure your database.yml, and migrate your tables and data.
  5. Start your Apache server.
    sudo apache2ctl start

    Note that with Passenger, restarting your app is done by creating a restart.txt file in the Merb app’s tmp folder.

That should get you started. If you want more, there’s a great talk about Deploying a Merb App by Lindsay and you can download it from the MerbCamp videos page. It talks about freezing Merb and other gems, web servers, restarting your app, monitoring, configuration management, exception handling, and some other tips.

  • Posted in Merb, Open Source, Ruby
  • Meta 34 comments, permalink, rss

Custom rake tasks in Merb: Data Backup and Import 61

Posted by mikong on November 03, 2008

There are a lot of data import solutions in Rails, most of which depend on ActiveRecord. Since Merb supports ActiveRecord too, you can use those solutions in your Merb app. But I’m using DataMapper in my Merb app, so I had to look for another way.

This article shows how to create a simple rake task in a Merb + DataMapper project. It then talks about the Data Backup and Import rake tasks db:dump_data and db:load_data. I’ve added some notes for those with a Rails background.

A simple rake task

When I generated my Merb app, the lib folder wasn’t generated. It looked something like this:

sample_app
  |--> app
  |--> autotest
  |--> config
  |--> doc
  |--> gems
  |--> merb
  |--> public
  |--> spec
  |--> Rakefile
  `--> tasks

The folder structure seems to suggest that you write your custom rake tasks under the tasks folder of your Merb app, but this is not the case. Read the Rakefile and you can see these 3 important details:

  • Add your custom tasks named file_name.rake in /lib/tasks.
  • The Merb environment is initialized to the MERB_ENV value, or ‘rake’ environment if MERB_ENV is not set.
  • To start the runner environment, in case you need access to your application’s classes, there is a task called :merb_env.

So the location is just like in Rails, i.e. in the lib/tasks folder. Try creating a custom.rake file in the lib/tasks folder and add the following:

  desc "Print all classes that include DataMapper::Resource."
  task :print_dm_resources => :merb_env do
    DataMapper::Resource.descendants.each do |resource|
      puts resource
    end
  end

The rake task above depends on the :merb_env task in order to access the application’s models. This is just like a rake task in Rails that depends on the :environment task. To run the task:

$ MERB_ENV=development rake print_dm_resources

Data Backup and Import

The following rake tasks are based off of the rake file provided in Tobias Lutke’s old blog post about migration between databases. I’ve translated it to work with Merb + DataMapper:

namespace :db do

  def interesting_tables
    DataMapper::Resource.descendants.reject! do |table|
      [Merb::DataMapperSessionStore].include?(table)
    end
  end

  desc "Dump data from the current environment's DB."
  task :dump_data => :merb_env do
    dir = Merb.root_path("config/data/#{Merb.env}")
    FileUtils.mkdir_p(dir)
    FileUtils.chdir(dir)

    interesting_tables.each do |table|
      puts "Dumping #{table}..."

      File.open("#{table}.yml", 'w+') { |f| YAML.dump(table.all.collect(&:attributes), f) }
    end
  end

  desc "Load data (from config/data/<environment>) into the current environment's DB."
  task :load_data => :merb_env do
    dir = Merb.root_path("config/data/#{Merb.env}")
    FileUtils.mkdir_p(dir)
    FileUtils.chdir(dir)

    adapter = DataMapper.repository(:default).adapter

    interesting_tables.each do |table|
      table.transaction do |txn|
        puts "Loading #{table} data..."
        YAML.load_file("#{table}.yml").each do |fixture|
          adapter.execute("INSERT INTO #{table.name.pluralize.snake_case} (#{fixture.keys.join(",")}) VALUES (#{fixture.values.collect {|value| adapter.send(:quote_column_value, value)}.join(",")})")
        end
      end
    end
  end
end

Add the above to your custom rake file. To dump the data from your development environment, run the following:

$ MERB_ENV=development rake db:dump_data

This will create the folder config/data/development if it doesn’t exist yet, and generate a ModelName.yml file for each of your models that included DataMapper::Resource. It is necessary to specify MERB_ENV, otherwise the environment will be initialized to the ‘rake’ environment and the folder config/data/rake will be created instead.

Then as you migrate your database to the latest version of your models, our database is cleared of its data (a side effect when using DataMapper’s automigrate):

$ rake db:automigrate

After migrating, we can just reload the data using our other rake task:

$ MERB_ENV=development rake db:load_data

If you were to use the sample process above, you might want to update the yaml files to handle the changes that happened with the migration. If there’s a new model, you can just create a new yaml file for it.

Here are some of the things you can do with the script:

  • change the path where the yaml files are stored
  • use a different file format for the data (quite a big change though)
  • edit interesting_tables method to exclude more models
  • clear tables before loading the data (this could be dangerous!)
  • create a task that depends on dump_data, automigrate, and load_data

I didn’t clear the tables before loading the data because I prefer that the rake task throw an error when I’m running it on a populated database.

There are probably better ways to approach this problem. In my case though, I just needed a quick solution to reload my data after running automigrate.

  • Posted in Merb, Ruby
  • Meta 61 comments, permalink, rss

Basic User Signup with MerbAuth 65

Posted by mikong on October 27, 2008

It’s amazing how fast Merb has changed since I’ve dabbled with it early last month. I used the merb-auth plugin then and it was pretty ok, being familiar with the restful authentication plugin for Rails. But the merb-auth plugin is now obsolete, after only being introduced last June.

Enter MerbAuth

Daniel Neighman (aka hassox) gave a talk during MerbCamp on MerbAuth and MerbSlices and you can download his slides from his github account. There’s also a recipe in the Merb Wiki cookbook for an Authenticated Hello World using MerbAuth, so be sure to check those out before following the tutorial below.

MerbAuth Setup

As detailed in the Authenticated Hello World tutorial, MerbAuth is already in your Merb stack and you can use it right after:

  1. generating your merb app
  2. setting up your database, and
  3. creating a hello world controller

So you just add the authentication in your router or your controller and you’re good to go.

Basic User Signup

This tutorial will show how to add validations to your User model, prepare your Signup page, and setup your Users controller. There are comments along the way that explain basic Merb stuff to someone with a Rails background. It assumes your Merb app uses Datamapper for its ORM and ERb for its templating engine, the defaults when generating a new Merb app.

Validations

It may be a bit weird that the User model is almost empty, looking like this:

class User
  include DataMapper::Resource

  property :id,     Serial
  property :login, String
end

You don’t see it there, but MerbAuth already has your back for validating the presence of password, and making sure it’s confirmed with the password_confirmation field. But you probably want to add that the login is unique, and perhaps validate the length of your login and password.

class User
  include DataMapper::Resource

  property :id,     Serial
  property :login, String

  validates_length      :login,        :within => 3..40
  validates_is_unique :login
  validates_length      :password, :within => 4..40, :if => :password_required?
end

Those are all pretty basic. But it’s good to note that there’s a :password_required? inside MerbAuth that you can use here, just as you were able to in the old days of merb-auth plugin. Add other fields such as created_at, updated_at or email and add more validations as you see fit.

Signup page

Create a new.html.erb file under app/views/users:

<%= error_messages_for @user %>
<%= form_for @user, :action => url(:users) do %>
  <p>
    <%= text_field :login, :label => "Login" %>
  </p>
  <p>
    <%= password_field :password, :label => "Password" %>
  </p>
  <p>
    <%= password_field :password_confirmation, :label => "Password Confirmation" %>
  </p>
  <p>
    <%= submit "Sign up" %>
  </p>
<% end =%>

It’s very similar to Rails but there are subtle things to note here. First, we use ‘form_for’ because we have a user resource for the form fields, just like in Rails. If it were a custom form that’s not based off of a resource, we’d use ‘form’ (Merb’s counterpart to Rails’ ‘form_tag’). For Merb, it’s important that you don’t miss the = in the ‘<%= form_for … end =%>’ or you would only see a blank Signup page.

Second, we have the field helpers. We had text_control, password_control and submit_button in the old Merb but now, we have text_field, password_field and submit and I think the view looks better. These same helpers are used even if you’re using ‘form’ instead of ‘form_for’, unlike having to use a different set of *_tag helpers in Rails. Note also the :label option of the field helper.

Users controller

Run ‘merb-gen controller users’ or create a users.rb file under app/controllers:

class Users < Application

  def index
    render
  end

  def new
    only_provides :html
    @user = User.new
    display @user
  end

  def create(user)
    session.abandon!
    @user = User.new(user)
    if @user.save
      redirect "/", :message => {:notice => "Signup complete"}
    else
      message[:error] = "Signup failed"
      render :new
    end
  end

end

A Merb controller differs with a Rails one in a lot of ways and I’ll just comment on a few. Merb has render and display methods. Then, there’s the convenience of specifying a parameter to be stored in a variable in your action like in the create action above. In that case, the params[:user] is automatically stored in a local variable named user.

In Merb, your controller actions need to explicitly return something. If you remove ‘render’ in the index action above, it’ll be like returning nil as your response. Return a string, like “hello world” in the Authenticated Hello World example, and it sends that string as response to the client. If you need to return an XML representation of your object, it’s as easy as having the action return @object.to_xml.

Last thing to note, we call session.abandon! in our create action to clear the session, practically logging out the user. This method is provided by MerbAuth.

Routing

You may add the following to your router.rb file:

  resources :users
  match('/signup').to(:controller => 'users', :action => 'new')

That’s it!

I hope this makes a good companion to the Authenticated Hello World recipe, and does it’s job of showing what you can do next with MerbAuth while introducing some of the basic concepts in Merb. There’s more to MerbAuth than what we’ve covered here. You might want to check its RDoc. There’s even the concept of MerbAuth Strategy (see hassox’s MerbAuth slides) I haven’t explored yet.

  • Posted in Merb, Rails, Ruby
  • Meta 65 comments, permalink, rss

Quick notes for setting up a secure remote Git repository 67

Posted by mikong on August 29, 2008

Toolman Tim wrote a good article about setting up a remote Git repository. It didn’t include the part of creating a git user, so I’ve created my own notes below. His article does offer more explanation on the setup so be sure to check it out.

Prepare the bare Git repo:

$ ssh myserver.com
$ mkdir /var/git
$ mkdir /var/git/myapp.git
$ cd /var/git/myapp.git
$ git --bare init

Create your git user:

$ addgroup git
$ adduser -g git git
$ passwd git
$ chown -R git:git /var/git/myapp.git

Copy your local computer’s public key to the git user’s authorized keys:

$ vi ~/../git/.ssh/authorized_keys

Locate your git-shell:

$ which git-shell
/usr/local/bin/git-shell

And change your git user’s shell from /bin/bash to the git-shell path:

$ vi /etc/passwd

On your local computer, go to your project directory and point it to the remote server:

$ cd ~/dev/myapp
$ git remote add origin ssh://git@myserver.com/var/git/myapp.git
$ git push origin master

To set the remote repository as the default branch to push and pull to (so you don’t have to specify “origin master” with every push, pull, etc), open your project’s Git config:

$ vi ~/dev/myapp/.git/config

And add the following:

  [branch "master"]
    remote = origin
    merge = refs/heads/master

And that’s all there is to it!

  • Posted in Open Source
  • Meta 67 comments, permalink, rss

Programming and My Other Passion 1

Posted by mikong on July 24, 2008

I sometimes wanted to write an article about Go (board game) in this development blog, but I could never justify it. But now that a couple of Ruby/Rails blogs posted articles this year relating programming to Go, I finally have an excuse. spacer

The first article is from the O’Reilly Ruby blog entitled “Ruby Conferences vs. Go Tournaments”. The other one is from the Rails Spikes blog, “Why programmers should play Go”, which Fabio Akita even translated to Portuguese (with additional notes) in his Akita on Rails blog. Check them out!

I was pleasantly surprised to find that these developers also have passions for both Go and programming and even went so far as comparing them. Previously, the only way I could think of linking the two was to create a Go program using Ruby. But now that I think about it, it seems like they’re connected on a deeper level.

Go and programming are my passions that take up most of my time. It has been a real struggle to balance the two. Every time I program too much, I would start to miss Go and do my best to find time for it. At times it’s the other way around. And then I just realized, I started to learn both Ruby and Go at about the same time (end of December 2006). I even joined the Philippine Ruby Users Group (PhRUG) and started the Philippine Go Association mailing list at about those times too. A coincidence? I THINK NOT!

If you don’t know Go, I encourage you to read about it. If you’re a fellow PhRUG member who knows Go and wants to play a game with me, or wants to learn it, you can send me an email through the group. Or just join our Go mailing list.

  • Posted in Rails
  • Meta 1 comment, permalink, rss

Metaprogramming Talk 45

Posted by mikong on May 22, 2008

I’d like to thank everyone who attended the Philippine Ruby Users Group (PHRUG) May ‘08 Meetup and listened to my over-2-hour talk on Metaprogramming. Thanks for the patience, for not sleeping (or not making it obvious), and for those wonderful questions. It’s tough discussing the object model of Ruby when terms are repeated like ‘the superclass of the metaclass of the Ninja class is the metaclass of the Object class’. I literally had a headache after the talk.

Talks

As I mentioned, my talk was largely based off of Dave Thomas’ “Metaprogramming” talk in QCon London ‘06 and Patrick Farley’s “Ruby Internals” talk in the MountainWest Ruby Conference (MWRubyConf) ‘08. Click on the links to see/download their presentations.

There are other talks related to metaprogramming in MWRubyConf ‘08 like those by Giles Bowkett, Jeremy McAnally, and Joe O’Brien. Or just go to the Confreaks site to check other cool talks on Ruby.

Coding Session

I had a problem with Ruby2Ruby.translate at one point in the coding session. I’ve just confirmed what was pointed out to me. Indeed, Ruby2Ruby couldn’t translate a class without a method definition. What you all wanted to see was what would this:

  module Taijutsu
    def punch
      puts 'punch'
    end
  end

  class Ninja
    include Taijutsu
  end

look like when Ninja is translated using Ruby2Ruby. Here it is:

  class Ninja < Object
    def punch
      puts 'punch'
    end
  end

Unfortunately, Ruby2Ruby won’t show an include call, as a lot of you had hoped.

Slides

I’ve uploaded the pdf of my slides at the Files section of our PHRUG Google Group. But you might be better off watching the talks I mentioned above.

Minor note: I used back quotes in my slides to refer to singleton classes and metaclasses. I thought this was the standard but looks like it’s not. Patrick Farley used the normal single quotes before the names in his slides. The pickaxe book used single quotes after the class name (for metaclass) and ‘anon’ which means anonymous class (to refer to the singleton classes and include classes of modules).

Thanks

Thanks again! If you enjoyed my talk, you might want to recommend me at Working with Rails. And you might want to check out this article by Ola Bini on Dynamically created methods in Ruby.

  • Posted in Ruby
  • Meta 45 comments, permalink, rss

Ruby 1.9 Hash in Ruby 1.8 46

Posted by mikong on April 03, 2008

I’m learning about Ruby 1.9 features but I realized I won’t be able to use it in any of my projects. So I thought of an exercise where I learn Ruby metaprogramming by trying to implement some of Ruby 1.9’s new functionality. Aside from learning metaprogramming and Ruby 1.9, I’d also end up with a library that I can use in my Ruby 1.8 projects.

But first, let me introduce…

The new Hash

We have an alternative hash syntax in Ruby 1.9:

  # old way that still works in Ruby 1.9
  my_hash = { :a => 'apple', :b => 'banana' }

  # new way
  my_hash = { a: 'apple', b: 'banana' }

A nice addition in Ruby 1.9 is that the order in which you added the items to a hash is remembered and will be used when the hash is iterated.

There’s also a new class method try_convert where if you call

  Hash.try_convert(myobject)

myobject’s to_hash method will be called to return a hash. If there’s no to_hash, nil will be returned.

And then we have these new instance methods (a few were simply borrowed from the Array class): assoc, compare_by_identity, compare_by_identity?, flatten, key and rassoc.

Trying Metaprogramming

Let’s first try to implement the try_convert class method. Luckily, Chris Wanstrath’s try() article gave us something we could use:

class Object
  ##
  #   @person ? @person.name :nil
  # vs
  #   @person.try(:name)
  def try(method)
    send method if respond_to? method
  end
end

By building on his code above, we could do this:

class Hash
  def self.try_convert(obj)
    obj.try(:to_hash)
  end
end

Looking at that, I’m starting to think Ruby 1.9 should have included the try() method instead of providing try_convert(). But that’s beside the point of this exercise.

Let’s try the simple instance method flatten. The documentation said it converts the hash to an array, then invokes Array#flatten! on the result. So it seems to be simply this:

class Hash
  ...
  def flatten
    to_a.flatten!
  end
end

But looking closer there’s actually a depth parameter (default is 1 - or so it seems but it behaves like -1 in my version of Ruby 1.9) demonstrated in the documentation’s example:

h = { feline: [ "felix", "tom"], :equine: "ed" }
h.flatten    # => [:feline, ["felix", "tom"], :equine, "ed"]
h.flatten(1) # => [:feline, ["felix", "tom"], :equine, "ed"]
h.flatten(2) # => [:feline, "felix", "tom", :equine, "ed"]

It turns out that the flatten and flatten! methods in Ruby 1.9 Array has also changed with a new level parameter. The default value of -1 makes it behave like the original (i.e. it recursively flattens the array). A level value of 0 performs no flattening and a level greater than zero flattens only to that depth (like the depth parameter in the Hash#flatten example above).

The flatten method we implemented above works fine. If you want the depth parameter, I’ve added it

gipoco.com is neither affiliated with the authors of this page nor responsible for its contents. This is a safe-cache copy of the original web site.