Logo Devoh

Flexible Client Configuration

When authoring web service clients, Ruby Gem developers have a bad habit of forcing users to configure the client at the class level. This limits the flexibility of the client to a single set of configuration parameters at a time. Since most clients require authentication credentials, forcing the user to set them at the class level means only one account may be accessed with the client per application.

I mentioned this issue briefly in Raising the Bar, but I want to expand on it further here. I started work on a new client library this weekend, and decided to try to accommodate both of these scenarios the best I could:

  • class-level configuration to cover the most common use cases
  • instant-level configuration to allow multiple instances to be used when needed

The approach I settled on uses a top-level module to store a global instance for the former scenario and a client class that can be used directly to satisfy the latter.

Here's what a stripped-down client class might look like with a single configuration parameter for an API key.

module Service
  class Client
    attr_accessor :api_key

    def configure
      yield(self)
      self
    end
  end
end

This arrangement easily allows for our second use-case:

client = Service::Client.new

client.configure do |config|
  config.api_key = API_KEY
end

Now, about that class-level configuration. Here's the definition for the top-level module. The extend self line effectively means we want to treat all the methods defined in this module as module methods.

module Service
  extend self

  def respond_to?(method, include_private=false)
    client.respond_to?(method) || super
  end

  def method_missing(method, *args, &block)
    if client.respond_to?(method)
      client.send(method, *args, &block)
    else
      super
    end
  end

  def client
    @client ||= Client.new
  end
  private :client
end

There's a decent bit of code here, but most of it should look familiar to anyone who has used Ruby's method_missing before. The basic idea is to delegate the method calls on the Service module to a global instance of Service::Client stored as an instance variable on the Service module. In effect, this behaves a bit like a class variable, and it gives us the concept of a global instance of the client.

We can configure this global instance like so:

Service.configure do |config|
  config.api_key = API_KEY
end

Any other methods implemented on the client will be available for the user to call directly on Service module.

This approach gives us the best of both worlds by allowing both class-level and instance-level configuration of the client. Please spend the time to think about how to implement an approach like this in your own Gems, and someday it will pay off by bringing happiness to the lives of developers the world over.