Skip to content

Instantly share code, notes, and snippets.

@malakai97
Last active October 24, 2017 16:52
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save malakai97/affbdb0902c244c9d8ca3fe72b08d6cc to your computer and use it in GitHub Desktop.
Save malakai97/affbdb0902c244c9d8ca3fe72b08d6cc to your computer and use it in GitHub Desktop.
Simple configuration for ruby gems and apps

Problem

You have a few different needs when it comes to configuring your gem or app:

  1. Sane defaults in test. These should be set in the spec_helper.
  2. Sane defaults in prod, aka zero-configuration defaults.
  3. Prod efaults specified in a file in a sensible location, probably the user's home directory.
  4. Some way of setting the above defaults in a clean way.
  5. Command-line overrides.
  6. Easy access throughout your application.

Solution

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
# lib/mygem/cli.rb
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
if config_path.exist?
Configuration.new(YAML.load_file(config_path))
else
Configuration.new
end
end
def configuration(opts)
config_from_file.merge(Configuration.new(opts))
end
end
end
#lib/mygem/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
# lib/mygem.rb
# requires go in this file
module MyGem
end
# spec/spec_helper.rb
require "mygem/configuration"
MyGem.config = MyGem::Configuration.new(...)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment