RubyGreenBlue

acts_as_convertible_to_csv

Posted by keith over 2 years ago
Vote for this article on digg.com: Digg It!

update: convertible to csv now has a project page

A little ruby magic in the form of meta-programming allows you to define your models as being able to output data in csv format. The definition is a DSL style acts_as_convertible_to_csv with :header and :fields as valid specifications.

Here's how you would use it.

In your model declaration:

class Customer < ActiveRecord::Base
  acts_as_convertible_to_csv :header => true, 
                             :fields => %w(id firstname lastname email_address)
end

The first row will contain fields names if you specify :header => true. You can specify which fields and the order in which they appear by using :fields. If you omit :fields, the field list will default to those returned by the human_readable method.

Getting the csv data from a collection of records:

Customer.find(:all).to_csv

You can pass a block:

Customer.find(:all).to_csv do |line|
  # write the line to a file or something
end

You can also convert individual records to csv:

Customer.find(:first).to_csv

The code

class ActiveRecord::Base

  def self.acts_as_convertible_to_csv(*args)

    @@use_header_fields = false

    args.each do |param|
      if param.is_a? Hash
        if param.include? :header and param[:header]
          @@use_header_fields = true
        end

        if param.include? :fields and param[:fields] and param[:fields].is_a? Array
          class_eval <<-CEEND
            def self.csv_header
              %w(#{param[:fields].join(' ')})
            end
          CEEND
        else
          class_eval <<-CEEND
            def self.csv_header
              field_names = Array.new
              self.content_columns.each do |column|
                field_names << column.name
              end
              field_names
            end
          CEEND
        end
      end
    end

    class_eval <<-CEEND
      def self.use_header_fields
        @@use_header_fields
      end

      def to_csv
        line = ""
        self.class.csv_header.each do |field_name|
          line = line + "," + self.send(field_name).to_s
        end
        # remove first ', '
        line.gsub! /^,/, ''
      end
    CEEND
  end

end

# Allow a collection class to repond to to_csv
class Array

  def to_csv(&block)
    data = Array.new
    # check to see if the objects in the array are capable of to_csv    
    if self[0] and self[0].class.method_defined?(:to_csv)
      if self[0].class.use_header_fields
        data << self[0].class.csv_header.join(',')
        yield self[0].class.csv_header.join(',') if block_given?
      end
      each do |object|
        data << object.to_csv
        yield object.to_csv if block_given?
      end
    end
    data
  end

end