Configurability

module
Extended With
Loggability

A unified, unintrusive, assume-nothing configuration system for Ruby

Constants

REVISION

Version-control revision constant

VERSION

Library version constant

Attributes

after_configure_hooks[R]
configurable_objects[RW]

Public Class Methods

anchor
after_configure( &block )

Register a callback to be run after the config is loaded.

# File lib/configurability.rb, line 61
def self::after_configure( &block )
        raise LocalJumpError, "no block given" unless block
        self.after_configure_hooks << block

        # Call the block immediately if the hooks have already been called or are in
        # the process of being called.
        block.call if self.after_configure_hooks_run?
end
anchor
after_configure_hooks_run=( new_value )

Set the flag that indicates that the after-configure hooks have run at least once.

# File lib/configurability.rb, line 55
def self::after_configure_hooks_run=( new_value )
        @after_configure_hooks_run = new_value ? true : false
end
anchor
after_configure_hooks_run?()

Returns true if the after-configuration hooks have run at least once.

# File lib/configurability.rb, line 48
def self::after_configure_hooks_run?
        return @after_configure_hooks_run ? true : false
end
anchor
call_after_configure_hooks()

Call the post-configuration callbacks.

# File lib/configurability.rb, line 73
def self::call_after_configure_hooks
        self.log.debug "  calling %d post-config hooks" % [ self.after_configure_hooks.length ]
        @after_configure_hooks_run = true

        self.after_configure_hooks.to_a.each do |hook|
                # self.log.debug "    %s line %s..." % hook.source_location
                hook.call
        end
end
anchor
configure_objects( config )

Configure objects that have had Configurability added to them with the sections of the specified config that correspond to their config_key. If the config doesn't respond_to the object's config_key, the object's configure method is called with nil instead.

# File lib/configurability.rb, line 135
def self::configure_objects( config )
        self.log.debug "Splitting up config %p between %d objects with configurability." %
                [ config, self.configurable_objects.length ]

        self.reset
        self.loaded_config = config

        self.configurable_objects.each do |obj|
                self.install_config( config, obj )
        end

        self.call_after_configure_hooks
end
anchor
default_config()

Gather the default configuration in a Configurability::Config object and return it.

# File lib/configurability.rb, line 244
def self::default_config
        return self.gather_defaults( Configurability::Config.new )
end
anchor
expand_config_hash( key, hash )

Nest the specified hash inside subhashes for each subsection of the given key and return the result.

# File lib/configurability.rb, line 204
def self::expand_config_hash( key, hash )
        return key.to_s.split( '__' ).reverse.inject( hash ) do |inner_hash, subkey|
                { subkey.to_sym => inner_hash }
        end
end
anchor
extend_object( object )

Add configurability to the given object.

# File lib/configurability.rb, line 94
def self::extend_object( object )
        self.log.debug "Adding Configurability to %p" % [ object ]
        super
        self.configurable_objects << object

        # If the config has already been propagated, add deferred configuration to the extending
        # object in case it overrides #configure later.
        if (( config = self.loaded_config ))
                self.install_config( config, object )
                object.extend( Configurability::DeferredConfig )
        end
end
anchor
find_config_section( config, key )

Find the section of the specified config object that corresponds to the given key.

# File lib/configurability.rb, line 172
def self::find_config_section( config, key )
        return key.to_s.split( '__' ).inject( config ) do |section, subkey|
                next nil if section.nil?
                self.get_config_subsection( section, subkey.to_sym )
        end
end
anchor
gather_defaults( collection={} )

Gather defaults from objects with Configurability in the given collection object. Objects that wish to add a section to the defaults should implement a defaults method in the same scope as configure that returns the Hash of default, or set one of the constants in the default implementation of defaults. The hash for each object will be merged into the collection via merge!.

# File lib/configurability.rb, line 217
def self::gather_defaults( collection={} )
        mergefunc = Configurability::Config.method( :merge_complex_hashes )

        self.configurable_objects.each do |obj|
                next unless obj.respond_to?( :defaults )
                if defaults_hash = obj.defaults
                        nested_hash = self.expand_config_hash( obj.config_key, defaults_hash )
                        Configurability.log.debug "Defaults for %p (%p): %p" %
                                [ obj, obj.config_key, nested_hash ]

                        collection.merge!( nested_hash, &mergefunc )
                else
                        Configurability.log.warn "No defaults for %p; skipping" % [ obj ]
                end
        end

        return collection
end
anchor
get_config_subsection( config, key )

Return the subsection of the specified config that corresponds to key, trying both struct-like and hash-like interfaces.

# File lib/configurability.rb, line 182
def self::get_config_subsection( config, key )
        if config.respond_to?( key )
                self.log.debug "  config has a #%s method; using that" % [ key ]
                return config.send( key )
        elsif config.respond_to?( :[] ) && config.respond_to?( :key? )
                self.log.debug "  config has a hash-ish interface..."
                if config.key?( key.to_sym ) || config.key?( key.to_s )
                        self.log.debug "    and has a %s member; using that" % [ key ]
                        return config[ key.to_sym ] || config[ key.to_s ]
                else
                        self.log.debug "    but no `%s` member." % [ key ]
                        return nil
                end
        else
                self.log.debug "  no %p section in %p; configuring with nil" % [ key, config ]
                return nil
        end
end
anchor
included( mod )

Mixin hook: extend including classes instead

# File lib/configurability.rb, line 109
def self::included( mod )
        mod.extend( self )
end
anchor
install_config( config, object )

Install the appropriate section of the config into the given object.

# File lib/configurability.rb, line 158
def self::install_config( config, object )
        self.log.debug "Configuring %p with the %s section of the config." %
                [ object, object.config_key ]

        section = self.find_config_section( config, object.config_key )
        configure_method = object.method( :configure )

        self.log.debug "  calling %p" % [ configure_method ]
        configure_method.call( section )
end
anchor
make_key_from_object( object )

Try to generate a config key from the given object. If it responds_to name, the result will be stringified and stripped of non-word characters. If the object itself doesn't have a name, the name of its class will be used instead.

# File lib/configurability.rb, line 117
def self::make_key_from_object( object )
        if object.respond_to?( :name )
                name = object.name
                name = 'anonymous' if name.nil? || name.empty?
                return name.sub( /.*::/, '' ).gsub( /\W+/, '_' ).downcase.to_sym
        elsif object.class.name && !object.class.name.empty?
                return object.class.name.sub( /.*::/, '' ).gsub( /\W+/, '_' ).downcase.to_sym
        else
                return :anonymous
        end
end
anchor
normalize_config_key( key )

Return the specified key normalized into a valid Symbol config key.

# File lib/configurability.rb, line 238
def self::normalize_config_key( key )
        return key.to_s.gsub( /\./, '__' ).to_sym
end
anchor
reset()

If a configuration has been loaded (via {#configure_objects}), clear it.

# File lib/configurability.rb, line 151
def self::reset
        self.loaded_config = nil
        self.after_configure_hooks_run = false
end
anchor
version_string( include_buildnum=false )

Get the library version. If include_buildnum is true, the version string will include the VCS rev ID.

# File lib/configurability.rb, line 86
def self::version_string( include_buildnum=false )
        vstring = "%s %s" % [ self.name, VERSION ]
        vstring << " (build %s)" % [ REVISION[/: ([[:xdigit:]]+)/, 1] || '0' ] if include_buildnum
        return vstring
end

Public Instance Methods

anchor
unknown()

the loaded configuration (after ::configure_objects has been called at least once)

# File lib/configurability.rb, line 36
singleton_class.attr_accessor :loaded_config

Configuration API

↑ top

Public Instance Methods

anchor
config_key( sym=nil )

Get (and optionally set) the config_key (a Symbol).

# File lib/configurability.rb, line 258
def config_key( sym=nil )
        self.config_key = sym unless sym.nil?
        @config_key ||= Configurability.make_key_from_object( self )
        @config_key
end
anchor
config_key=( sym )

Set the config key of the object.

# File lib/configurability.rb, line 266
def config_key=( sym )
        Configurability.configurable_objects |= [ self ]
        @config_key = Configurability.normalize_config_key( sym )
end
anchor
configure( config=nil )

Default configuration method. This will merge the provided config with the defaults if there are any and the config responds to #to_h. If the config responds to #each_pair, any writable attributes of the calling object with the same name as a key of the config will be called with the corresponding value. E.g.,

class MyClass
    extend Configurability
    CONFIG_DEFAULTS = { environment: 'develop', apikey: 'testing-key' }
    config_key :my_class
    class << self
        attr_accessor :environment, :apikey
    end
end

config = { my_class: {apikey: 'demo-key'} }
Configurability.configure_objects( config )

MyClass.apikey
# => 'demo-key'
MyClass.environment
# => 'develop'
# File lib/configurability.rb, line 294
def configure( config=nil )
        config = self.defaults( {} ).merge( config.to_h || {} ) if
                config.nil? || config.respond_to?( :to_h )

        @config = config

        if @config.respond_to?( :each_pair )
                @config.each_pair do |key, value|
                        Configurability.log.debug "Looking for %p config attribute" % [ key ]
                        next unless self.respond_to?( "#{key}=" )
                        Configurability.log.debug "  setting %p to %p via attr_writer" %
                                [ key, value ]
                        self.public_send( "#{key}=", value )
                end
        else
                Configurability.log.
                        debug "config object (%p) isn't iterable; skipping config attributes" % [ @config ]
        end

        return @config
end

Configuration Defaults API

↑ top

Public Instance Methods

anchor
default_config()

Return a Configurability::Config object that contains the configuration defaults for the receiver.

# File lib/configurability.rb, line 375
def default_config
        default_values = self.defaults or return Configurability::Config::Struct.new( {} )
        return Configurability::Config::Struct.new( default_values )
end
anchor
defaults( fallback=nil )

The default implementation of the method called by ::gather_defaults when gathering configuration defaults. This method expects either a DEFAULT_CONFIG or a CONFIG_DEFAULTS constant to contain the configuration defaults, and will just return the fallback value if neither exists.

# File lib/configurability.rb, line 355
def defaults( fallback=nil )

        return fallback unless respond_to?( :const_defined? )

        Configurability.log.debug "Looking for defaults in %p's constants." % [ self ]
        if self.const_defined?( :DEFAULT_CONFIG, false )
                Configurability.log.debug "  found DEFAULT_CONFIG"
                return self.const_get( :DEFAULT_CONFIG, false ).dup
        elsif self.const_defined?( :CONFIG_DEFAULTS, false )
                Configurability.log.debug "  found CONFIG_DEFAULTS"
                return self.const_get( :CONFIG_DEFAULTS, false ).dup
        else
                Configurability.log.debug "  no default constants."
                return fallback
        end
end

Configuration settings block

↑ top

Public Instance Methods

anchor
configurability( config_key=nil, &block )

Declare configuration settings and defaults. In the provided block, you can create a configuration setting using the following syntax:

configurability( :my_config_key ) do
    # Declare a setting with a `nil` default
    setting :a_config_key
    # Declare one with a default value
    setting :another_config_key, default: 18
end
# File lib/configurability.rb, line 331
def configurability( config_key=nil, &block )
        self.config_key = config_key if config_key

        if block
                Configurability.log.debug "Applying config declaration block using a SettingInstaller"
                installer = Configurability::SettingInstaller.new( self )
                installer.instance_eval( &block )
        end

        if (( config = Configurability.loaded_config ))
                Configurability.install_config( config, self )
        end

end