Modify a method on the fly in Ruby

Posted on

Problem

I have about a hundred Rake tasks that are creating Rails models for me from a legacy datafile. Each task shares some setup and teardown code; all that sets them apart is the code in the middle. For example:

task countries: setup do
  rows = rows_from_file("system/countries", 2)
  bar = RakeProgressbar.new(rows.length)
  info "» Importing countries"
  rows.each do |row|
    Country.new(
      alpha2: row[0],
      name: row[1]
    ).save!
    bar.inc
  end
  bar.finished
end

task telephone_number_types: setup do
  rows = rows_from_file("system/telephone_number_types", 1)
  bar = RakeProgressbar.new(rows.length)
  info "» Importing telephone number types"
  rows.each do |row|
    TelephoneNumberType.new(
      name: row[0]
    ).save!
    bar.inc
  end
  bar.finished
end

task occupations: setup do
  rows = rows_from_file("system/occupations", 3)
  bar = RakeProgressbar.new(rows.length)
  info "» Importing occupations"
  rows.each do |row|
    Occupation.new(
      id: row[1],
      name: row[2]
    ).save!
    bar.inc
  end
  bar.finished
end

How could I extract the progress bar stuff so I don’t keep repeating it? I had thought of writing some generic method that takes the array of rows and some code block (which would contain the Model.new(attrs).save! code) and wraps that stuff with the progress bar stuff, but I can’t quite make it work.

Solution

I’d say you’re on the right track with you idea to extract a method.

While I can’t fully replicate your code and its context, I think something like this should work:

task countries: setup do |task_name|
  rows = rows_from_file("system/#{task_name.to_s}", 2)
  process_rows(task_name, rows) do |row|
    { alpha2: row[0], name: row[1] }
  end
end

# ...

def import_rows(task_name, rows, &block)
  klass = task_name.to_s.classify
  bar = RakeProgressbar.new(rows.length)
  info "» Importing #{task_name.to_s.humanize}"
  rows.each do |row|
    attrs = block.call(row)
    klass.new(attrs).save!
    bar.inc
  end
  bar.finished
end

But it’s going pretty far in its assumptions – it’s basing a lot off of the task name alone.

A somewhat less fragile approach might be this where the correct class is explicitly passed to the helper method and the file name isn’t guessed from the task name

task countries: setup do
  rows = rows_from_file("system/countries", 2)
  process_rows(Country, rows) do |row|
    { alpha2: row[0], name: row[1] }
  end
end

# ...

def import_rows(klass, rows, &block)
  bar = RakeProgressbar.new(rows.length)
  info "» Importing #{klass.to_s.pluralize.humanize}"
  rows.each do |row|
    attrs = block.call(row)
    klass.new(attrs).save!
    bar.inc
  end
  bar.finished
end

Leave a Reply

Your email address will not be published. Required fields are marked *