Have you ever seen a configuration in Ruby that yields to a block where properties are set on the yielded object? Rails does this with its environments files:
Your::Application.configure do config.cache_classes = true config.consider_all_requests_local = false config.action_controller.perform_caching = true ... end
Devise is another great example of this pattern:
Devise.setup do |config| config.secret_key = '38c8e4958385982971f' config.mailer_sender = "noreply@email.com" config.mailer = "AuthenticationMailer" ... end
This is an established way to pass in configuration options in the Ruby world. How does this work? What are the alternatives?
Under the Hood
Lets start with our own implementation from scratch. We know that we are calling a method (Application.configure
, or Devise.setup
) and that method yields to a block:
module Example class << self def configure yield Config end end end
You can see that this will yield the class Config
when we call Example.configure
and pass in block. Config
will need to define our properties:
module Example class Config class << self attr_accessor :api_url, :consumer_key, :consumer_secret end end end
We can now call our configure method and pass in a block:
Example.configure do |config| config.api_url = "http://api.com/v1/" end
If we try to set a property that does not exist in our Example::Config
class, then we get a helpful NoMethodError
:
Example.configure do |config| config.not_a_real_config_setting = "http://api.com/v1/" end # => NoMethodError: undefined method `not_a_real_config_setting=' for Example::Config:Class
This helps to define your configuration interface. Options are explicitly declared. They cannot be mistyped, or dynamically added without an explicit runtime error.
In practice, The definitions of Example
, and Example::Class
would live in a gem. The Example.configure
invokation would live within the code that wants to configure the properties of that gem with things such as credentials, URLs, etc. This seperation of concerns makes sense. Gems should not know anything about the business logic of your application. It should be instantiated with configuration options passed in from the project. However the project should only be aware of the public interface of the gem. We are agreeing upon where these pieces of information are moving from one domain to another. The project doesn’t know where this information will be used, and the gem doesn’t know what the information is until we pass it. So far so good!
Using this Configuration
Now that we’ve passed our information inside the configuration block, we can reference these class level (static) properties in our gem:
module Example class Request def self.get(path) request = Net::HTTP.get(URI.parse(ExampleConfig.api_url + path)) request['Authorization'] = auth_header(:get, ExampleConfig.api_url) end private def self.auth_header(request_type, uri) SimpleOAuth::Header.new(request_type, uri.to_s, {}, {consumer_key: Config.consumer_key, consumer_secret: Config.consumer_secret}).to_s end end end
This will do a simple GET request passing in SimpleOAuth headers. Inside the get
method we call Config.api_url
to know where the API lives. This was set by us earlier using the Config
object. SimpleOAuth headers are supplied by again calling the Config
. You would invoke it like so:
Example.configure do |config| config.api_url = "http://api.com/v1/" consumer_key = "1895-1192-1234" consumer_secret = '76asdfh3heasd8f6akj3hea0a9s76df' end Example::Request.get('/products') # => {products: [product1, product2, etc]...}" Example::Request.get('/users') # => "{users: [user1, user2, etc]...}"
Example::Config
becomes the holding location for your configuration information. And by making the properties static, you don’t have to worry about passing around a reference to the instance.
Alternatives
If the yielding to a block is a little too clever for you, you can always instantiate a class and pass in the configuration as part of the constructor:
class Example class << self attr_accessor :api_url, :consumer_key, :consumer_secret def config(api_url:, consumer_key:, consumer_secret:) self.api_url = api_url self.consumer_key = consumer_key self.consumer_secret = consumer_secret end end end
This can be instantiated like so:
Example.config( api_url: "http://api.com/v1/" consumer_key: "1895-1192-1234" consumer_secret: '76asdfh3heasd8f6akj3hea0a9s76df' ) Example.api_url # => "http://api.com/v1/"
This feels less encapsulated to me. Instead of having an interface for our configuration settings, we are just settings properties directly onto a class.
What are your thoughts? What advantages does the block style configure offer over the alternative above?
For more information on the Ruby gem configuration pattern see this excellent blog post: