Rainman is an experiment in writing drivers and handlers. It is a Ruby implementation of the abstract factory pattern. Abstract factories provide the general API used to interact with any number of interfaces. Interfaces perform actual operations. Rainman provides a simple DSL for implementing this design.
In Rainman, drivers represent abstract factories and handlers represent the interfaces those factories interact with. In simpler terms, drivers define what things you can do; handlers define how to do those things.
Rainman drivers are implemented as Modules. They must be extended with
Rainman::Driver
and use the driver DSL to define their public API. An
example Domain driver might look like this:
require 'rainman'
# The Domain module handles creating and deleting domains, and listing
# nameservers
module Domain
extend Rainman::Driver
# Set global configuration variables you need to access across all handlers.
config[:username] = 'username'
config[:user_agent] = 'SuperAwesome Domain Manager'
# Register Domain::Abc as a handler. An optional block yields a config hash
# which can be used to store variables needed by the handler class, in this
# case a username and password specific for Domain::Abc.
register_handler :abc do
config[:username] = 'username'
config[:password] = 'pass'
end
# Register Domain::Xyz as a handler.
register_handler :xyz do
config[:username] = 'username'
config[:password] = 'pass'
# Specify a required parameter
#
# When defined in a handler, parameters cascade down to all of its
# actions.
param :username, :required => true
end
# Register Domain.create as a public method. An optional block yields a
# config hash that can be used to specify validations to be run before the
# method is invoked.
define_action :create do
# Specify an optional parameter
#
# We required username above for Xyz. This will make that parameter
# optional when Xyz's `create` method is called.
param :username, :required => false
end
# Register Domain.destroy as a public method
define_action :destroy
# Register Domain.namservers.list as a public method
namespace :nameservers do
define_action :list
end
end
Driver handlers are implemented as classes. They must be within the namespace of the driver Module. Using the example above, here are example handlers for Abc and Xyz:
class Domain::Abc
# Public: Creates a new domain.
#
# Returns a Hash.
def create(params = {})
end
# Public: Destroy a domain
#
# Returns true or false.
def destroy(params = {})
end
end
class Domain::Xyz
# Public: Creates a new domain.
#
# Returns a Hash.
def create(params = {})
end
# Public: Destroy a domain
#
# Returns true or false.
def destroy(params = {})
end
end
Handler classes automatically have config
and params
class methods
available. These are hashes that contain configs/params specific to the
handler. The config hash is meant to store things like usernames, passwords,
etc, that would be used by a handler. The params hash is to store parameters
that must be set for a handler to perform a valid request.
The example driver above also defined nameservers
namespace with a list
action (eg: Domain.nameservers.list
). To implement this, a Nameservers class
is created within each handler's namespace:
class Domain::Abc::Nameserver
# Public: Lists nameservers for this domain.
#
# Returns an Array.
def list(params = {})
end
end
class Domain::Xyz::Nameserver
# Public: Lists nameservers for this domain.
#
# Returns an Array.
def list(params = {})
end
end
With a driver and handler defined, the driver can now be used in a few different ways.
A driver's actions are available as singleton methods. By default, actions are sent to the current handler, or a default handler if a handler is not currently in use.
# Create a domain
Domain.create({})
# Destroy a domain
Domain.destroy({})
# List domain nameservers
Domain.nameservers.list({})
It is possible to change the handler used at runtime using the with_handler
method. This method temporarily changes the current handler. This means, if
you have a default handler set, and use with_handler
, that default handler
is preserved.
Domain.with_handler(:abc) do |driver|
# Here, current_handler is now set to :abc
driver.create
end
You can also change the current handler for the duration of your code/session.
Domain.set_current_handler :xyz
Domain.create # create an :xyz domain
Domain.set_current_handler :abc
Domain.create # create an :abc domain
Domain.transfer # transfer an :abc domain
It is highly suggested you stick to using with_handler
unless you have a
reason.
A driver can be included in another class and it's actions are available as instance methods.
class Service
include Domain
end
s = Service.new
s.create
s.destroy
s.nameservers.list
s.with_handler(:abc) do |driver|
driver.create
end
s.set_current_handler :zyz
s.create
- Fork the project.
- Make your feature addition or bug fix.
- Add tests for it. This is important so I don't break it in a future version unintentionally.
- Commit, do not bump version. (If you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull).
- Send me a pull request. Bonus points for topic branches.
Copyright (c) 2011 Site5 LLC. See LICENSE for details.