Configurability

home

configur.ability.guide/

code

hg.sr.ht/~ged/Configurability

docs

deveiate.org/code/configurability

github

github.com/ged/configurability

Description

Configurability is a unified, non-intrusive, assume-nothing configuration system for Ruby. It lets you keep the configuration for multiple objects in a single config file, load the file when it's convenient for you, and distribute the configuration when you're ready, sending it everywhere it needs to go with a single action.

Installation

gem install configurability

Example

# user.rb
require 'configurability'

class User
    extend Configurability

    configurability( :users ) do
        setting :min_password_length, default: 6
    end

end


# config.yml
users:
  min_password_length: 12

In pry:

[1] pry(main)> require 'user'
=> true
[2] pry(main)> User.min_password_length
=> 6
[3] pry(main)> config = Configurability::Config.load( "config.yml" )
=> #<Configurability::Config:0x7fd99a0f635816 loaded from config.yml ...>
[4] pry(main)> config.install
=> [Loggability, User]
[5] pry(main)> User.min_password_length
=> 12

Usage

To add Configurability to your module or class, just extend it and declare some settings:

require 'configurability'
class Database
    extend Configurability

    configurability( :db ) do
        setting :url, default: 'sqlite:/'
        setting :user
        setting :password
        setting :port do |value|
            Integer( value ) if value
        end
    end
end

This sets up the class to use the db config key, and adds attributes for the three subkey settings under it (with getters and setters) to the class. It also adds a configure class method that will set whichever of the settings are passed to it, defaulting the url to the provided value if it's not given.

The setting can include a block which is given the config value before it's set; from this block you can do validity-checking, cast it to something other than a String, etc. The new value will be set to the return value of the block. Note that this will be called with the default for the setting when it's declared, so the block should be able to handle the default (even if it's nil).

If your config file (e.g., config.yml) looks like this:

--
db:
    url: 'postgres:/acme'
    user: tim
    password: "pXVvVY,YjWNRRi[yPWx4"

You can configure the Database class (and all other objects extended with Configurability) with it like so:

require 'configurability/config'

config = Configurability::Config.load( 'config.yml' )
Configurability.configure_objects( config )

After this happens you can access the configuration values like this:

Database.url
# => "postgres:/acme"
Database.user
# => "tim"
Database.password
# => "pXVvVY,YjWNRRi[yPWx4"

You can add helper methods inside the configurability block to eliminate repeated code in your setting blocks:

configurability( :server ) do
  def self::make_path( value )
    return nil unless value
    pn = Pathname( value )
    raise "Can't read from %s!" % [ pn ] unless pn.readable?
    return pn
  end

    setting :template_path do |value|
            make_path( value )
    end

    setting :plugin_path do |*values|
            make_path( value )
    end
end

If a setting is a boolean, you can also have a predicate method created for it alongside its getter and setter:

class Mailer
  extend Configurability
  configurability( :db ) do
    setting :use_whitelist, default: false, predicate: true
  end
end

Mailer.use_whitelist?
# => false
Mailer.use_whitelist = true
Mailer.use_whitelist?
# => true

More Details

Configurability is implemented using Module#extend, so there's very little magic going on. Every object (including Class and Module objects) that is extended with Configurability is registered in an Array of objects that will be configured when the config is loaded.

You extend a Class, of course, as in the above example:

class MyClass
    extend Configurabtility
end

But you can also add it to individual instances of objects to configure them separately:

user = User.new
user.extend( Configurability )

When you call:

Configurability.configure_objects( config )

the specified config will be spliced up and sent to all the objects that have been registered. Configurability expects the configuration to be broken up into a number of sections, each of which is accessible via either a method with the section name or the index operator (#[]) that takes the section name as a Symbol or a String:

config.section_name
config[:section_name]
config['section_name']

The section name is based on an object's config key, which is the argument you specify when declaring your settings. If you don't provide one, it defaults to the name of the object that is being extended with all non-word characters converted into underscores (_). It will also have any leading Ruby-style namespaces stripped, e.g.,

MyClass            -> :myclass
Acme::User         -> :user
"J. Random Hacker" -> :j_random_hacker

If the object responds to the #name method, then the return value of that method is used to derive the name. If it doesn't have a #name method, the name of its Class will be used instead. If its class is anonymous, then the object's config key will be :anonymous.

When the configuration is loaded, any attribute writers that correspond with keys of the config will be called with the configured values, and an instance variable called @config is set to the appropriate section of the config.

As you add more objects to your configuration, it may be useful to group related sections together. You can specify that your object's configuration is part of a group by specifying a config key with either a dot if it's a String or a double underscore if it's a Symbol. E.g.,

# Read from the `db` subsection of `myapp`
configurability( 'myapp.db' )

# Same thing, but with a symbol:
configurability( :myapp__db )

Customization

The default behavior above is just provided as a reasonable default; you may want to customize one or two things about how configuration is handled in your objects.

Setting a Custom Config Key

If you want to customize the config key without calling configurability you can do that by declaring it with the config_key method:

class OutputFormatter
    extend Configurability
    config_key :format
end

or by overriding the #config_key method youtself and returning the desired value as a Symbol:

class User
    extend Configurability
    def self::config_key
        return :employees
    end
end

Changing How an Object Is Configured

You can also change what happens when an object is configured by implementing a #configure method that takes the config section as an argument:

class WebServer
    extend Configurability

    config_key :webserver

    def self::configure( config )
        @default_bind_addr = config[:host]
        @default_port = config[:port]
    end
end

If you still want the @config variable to be set, just super from your implementation; don't if you don't want it to be set.

Configuration Objects

Configurability also includes Configurability::Config, a fairly simple configuration object class that can be used to load a YAML configuration file, and then present both a Hash-like and a Struct-like interface for reading configuration sections and values; it's meant to be used in tandem with Configurability, but it's also useful on its own.

Here's a quick example to demonstrate some of its features. Suppose you have a config file that looks like this:

---
database:
  development:
    adapter: sqlite3
    database: db/dev.db
    pool: 5
    timeout: 5000
  testing:
    adapter: sqlite3
    database: db/testing.db
    pool: 2
    timeout: 5000
  production:
    adapter: postgres
    database: fixedassets
    pool: 25
    timeout: 50
ldap:
  uri: ldap://ldap.acme.com/dc=acme,dc=com
  bind_dn: cn=web,dc=acme,dc=com
  bind_pass: Mut@ge.Mix@ge
branding:
  header: "#333"
  title: "#dedede"
  anchor: "#9fc8d4"

You can load this config like so:

require 'configurability/config'
config = Configurability::Config.load( 'examples/config.yml' )
# => #<Configurability::Config:0x1018a7c7016 loaded from
    examples/config.yml; 3 sections: database, ldap, branding>

And then access it using struct-like methods:

config.database
# => #<Configurability::Config::Struct:101806fb816
    {:development=>{:adapter=>"sqlite3", :database=>"db/dev.db", :pool=>5,
    :timeout=>5000}, :testing=>{:adapter=>"sqlite3",
    :database=>"db/testing.db", :pool=>2, :timeout=>5000},
    :production=>{:adapter=>"postgres", :database=>"fixedassets",
    :pool=>25, :timeout=>50}}>

config.database.development.adapter
# => "sqlite3"

config.ldap.uri
# => "ldap://ldap.acme.com/dc=acme,dc=com"

config.branding.title
# => "#dedede"

or using a Hash-like interface using either Symbols, Strings, or a mix of both:

config[:branding][:title]
# => "#dedede"

config['branding']['header']
# => "#333"

config['branding'][:anchor]
# => "#9fc8d4"

You can install it (i.e., configure your objects) via the Configurability interface:

config.install

If you change the values in the config object, they won't propagate automatically; you'll need to call #install on it again to send the changes to the objects being configured.

You can check to see if the file the config was loaded from has changed since you loaded it:

config.changed?
# => false

# Simulate changing the file by manually changing its mtime
File.utime( Time.now, Time.now, config.path )
config.changed?
# => true

If it has changed (or even if it hasn't), you can reload it, which automatically re-installs it via the Configurability interface if it has:

config.reload

You can make modifications via the same Struct- or Hash-like interfaces and write the modified config back out to the same file:

config.database.testing.adapter = 'mysql'
config[:database]['testing'].database = 't_fixedassets'

then dump it to a YAML string:

config.dump
# => "--- \ndatabase: \n  development: \n    adapter: sqlite3\n  
  database: db/dev.db\n    pool: 5\n    timeout: 5000\n  testing: \n  
  adapter: mysql\n    database: t_fixedassets\n    pool: 2\n    timeout:
  5000\n  production: \n    adapter: postgres\n    database:
  fixedassets\n    pool: 25\n    timeout: 50\nldap: \n  uri:
  ldap://ldap.acme.com/dc=acme,dc=com\n  bind_dn:
  cn=web,dc=acme,dc=com\n  bind_pass: Mut@ge.Mix@ge\nbranding: \n
  header: \"#333\"\n  title: \"#dedede\"\n  anchor: \"#9fc8d4\"\n"

or write it back to the file it was loaded from:

config.write

Note that this is just using YAML.dump, so any comments, ordering, or other nice formatting you have in your config file will be clobbered if you rewrite it.

Configuration Defaults

It's a good idea to provide a set of reasonable defaults for any configured object. The defaults for all settings are added to extended Classes and Modules as a constant named CONFIG_DEFAULTS. You can, of course set this constant yourself as well.

Configurability provides a defaults method that will return the hash of settings and their default values from the CONFIG_DEFAULTS constant. You can also override the defaults method yourself (and super to the original) if you wish to do something different.

There are also a couple of useful functions built on top of this method:

gather_defaults

You can fetch a Hash of the default config values of all objects that have been extended with Configurability by calling Configurabilty.gather_defaults. You can also pass an object that responds to #merge! to the method to merge the defaults into an existing config.

default_config

This will return a Configurability::Config object made from the results of gather_defaults. This makes it easy to write a config file that contains the default configuration: Configurability.default_config.write( "defaults.yml" )

Development

You can submit bug reports, suggestions, clone it with Mercurial, and read more about future plans at the project page. If you prefer Git, there is also a Github mirror.

After checking out the source, run:

$ rake newb

This task will install any missing dependencies, run the tests/specs, and generate the API documentation.

License

Copyright © 2010-2019 Michael Granger and Mahlon E. Smith All rights reserved.

Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS “AS IS” AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.