arches.io Config for Ruby Console Applications

10 Sep 2012

The Problem

This technique came from my work on table_print, an irb tool for showing ruby objects in tables. I wanted to let people configure its output on a process-wide basis; eg, a default date format so sll their dates are shown consistently in the way they prefer. Additionally, I wanted a simple, easy-to-remember interface and a clean global namespace.

The Solution

Here's a basic syntax example for table_printing:

# prints all the authors, along with their book titles and photo captions
> tp Author.all, :include => "books.title", "books.photos.caption"
NAME              | BOOKS.TITLE       | BOOKS.PHOTOS.CAPTION
-------------------------------------------------------------------------
Michael Connelly  | The Fifth Witness | Susan was running, fast, away...
                  |                   | Along came a spider.
=> 0.58217

The tp method is the only interface to the application; all input is passed to that method from the command line. A great config interface would be something like:

> tp.set :default_date_format, "%m-%d-%Y"
=> Config Saved.

# NOTE: this is equivalent to:
> tp().set(:default_date_format, "%m-%d-&Y")

# and technically, there's no reason we couldn't print AND set config:
> tp(Author.all, :include => "books.title", "books.photos.caption").set(:default_date_format, "%m-%d-&Y")

# ...except we want to be able to remove the parentheses and simply call tp.set - the cleanest possible interface

By reusing the tp method we maintain a consistent interface and add nothing to the global namespace.

The .set call is operating on the return value of the tp method. Unfortunately, the return value is also used when doing a normal table_print. Looking at the first example, the return value is shown on the last line: => 0.58217. It's the time it takes to print the objects. I chose that value because a) it had to be something, and b) it had to be short. Returning the objects themselves would result in a nicely formatted table followed by the block of mush we were trying to avoid in the first place! So paradoxically, we need to both return something short and useful, that also happens to be a config object.

So we create a Returnable object that both passes config methods through to TablePrint::Config, and also defines a to_s method that returns the decimal.

class Returnable
  def initialize(string_value)
    @string_value = string_value
  end

  def set(*config)
    TablePrint::Config.set(config)
    @string_value = "Config Saved."
    self
  end

  def to_s
    @string_value
  end
end

This dual-use object lets us return one thing from the tp, confident that it will handle either a print or a config.

def tp(data=[], *options)
  time = Time.now

  # maybe print some stuff, maybe not! depends if there's any data
  TablePrint::Printer.new(data, options).print

  # always return something suitable for EITHER display or setting config
  Returnable.new(Time.now - time)
end

Let's follow the code flow for setting config to see how this works in practice.

# call into the tp method with no arguments. The .set will operate on the return value of the tp method!
> tp.set :default_date_format, "%m-%d-%Y"

# tp method does its thing
def tp(data=[], *options)
  time = Time.now

  # data is an empty array, options is nil, so TablePrint performs a no-op
  TablePrint::Printer.new(data, options).print

  # returnable is created and returned to the caller (the console)
  Returnable.new(Time.now - time)
end

# console regains control and continues evaluating the input. It calls .set on the object returned by the tp method:
returnable.set :default_date_format, "%m-%d-%Y"

# returnable.set passes the config through to TablePrint
def set(*config)
  TablePrint::Config.set(config)
  @string_value = "Config Saved."
  self
end

# console regains control, calls .to_s on the object returned by the returnable.set method.
=> Config Saved.