You have a few different needs when it comes to configuring your gem or app:
- Sane defaults in test. These should be set in the spec_helper.
- Sane defaults in prod, aka zero-configuration defaults.
- Prod efaults specified in a file in a sensible location, probably the user's home directory.
- Some way of setting the above defaults in a clean way.
- Command-line overrides.
- Easy access throughout your application.
Create a Configuration
class in your gem that accepts an empty constructor.
I recommend just subclassing OpenStruct
, as it has a number of nice semantics
that we want. It should be in a file lib/mygem/configuration.rb
. That file
should define the class, and make an instance of it accessible (6) via
MyGem.config
. This class is responsible for prod defaults (2).
Your files can now require this file directly to get access to the config while retaining isolation.
# in configuration.rb
require "ostruct"
module MyGem
class Configuration < OpenStruct
# Optional, but it gives us a convenient way to handle
# string vs symbol keys.
def merge(other)
Configuration.new(self.to_h.merge(other.to_h))
end
def some_val
@some_val ||= "some_val_default"
end
end
def self.config=(value)
@config = value
end
def self.config
@config ||= Configuration.new
end
# Avoid having to type MyGem.config.x by delegating here.
def self.method_missing(method, *args)
if config.respond_to?(method)
config.send(method, *args)
else
super
end
end
end
Additionally, if you choose to define a DSL block for configuring your gem, the entry point of the DSL should be defined in this file. I.e.:
def self.configure
instance = self.new
yield instance
instance
end
Now, in your spec or test helper, you can create a configuration object with the test defaults (1), and simply assign it
require "mygem/configuration"
MyGem.config = MyGem::Configuration.new(...) # or call configure
Now, the CLI is obviously responsible for settings passed on the command line (4).
Simply require mygem/configuration
in the cli.rb, and modify it as needed with
the command line options before assigning the instance to MyGem.config
.
The CLI is also the entry point for discovering settings from some location (3). If your Configuration class can be instantiated from a hash, you can just have the CLI read in a yaml file--the location of which belongs to the CLI.
To wit:
require "mygem"
require "mygem/configuration"
module MyGem
class CLI
def actual_cli_method(x,y)
MyGem.config = configuration(opts)
MyGem::StuffDoer.new(x).go(y)
end
private
def config_path
Pathname.new(ENV['HOME']) + ".mygem.yml"
end
def config_from_file
Configuration.new(config_path.exist? ? YAML.load_file(config_path) : {})
end
def configuration(opts)
config_from_file.merge(Configuration.new(opts))
end
end
end