Module: Configurability

Extended by:
Configurability, Loggability
Included in:
Configurability
Defined in:
lib/configurability.rb

Overview

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

Defined Under Namespace

Modules: DeferredConfig Classes: Config, SettingInstaller

Constant Summary collapse

VERSION =

Library version constant

'4.0.0'
REVISION =

Version-control revision constant

%q$Revision$

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Instance Attribute Details

#after_configure_hooksObject (readonly)

Returns the value of attribute after_configure_hooks.



42
43
44
# File 'lib/configurability.rb', line 42

def after_configure_hooks
  @after_configure_hooks
end

#configurable_objectsObject

The objects that have had Configurability added to them

the Array of objects that have had Configurability added to them



31
32
33
# File 'lib/configurability.rb', line 31

def configurable_objects
  @configurable_objects
end

#loaded_configObject

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



36
37
38
# File 'lib/configurability.rb', line 36

def loaded_config
  @loaded_config
end

Class Method Details

.after_configure(&block) ⇒ Object

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

Raises:

  • (LocalJumpError)


61
62
63
64
65
66
67
68
# 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

.after_configure_hooks_run=(new_value) ⇒ Object

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



55
56
57
# File 'lib/configurability.rb', line 55

def self::after_configure_hooks_run=( new_value )
	@after_configure_hooks_run = new_value ? true : false
end

.after_configure_hooks_run?Boolean

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

Returns:

  • (Boolean)


48
49
50
# File 'lib/configurability.rb', line 48

def self::after_configure_hooks_run?
	return @after_configure_hooks_run ? true : false
end

.call_after_configure_hooksObject

Call the post-configuration callbacks.



73
74
75
76
77
78
79
80
81
# 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

.configure_objects(config) ⇒ Object

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.



135
136
137
138
139
140
141
142
143
144
145
146
147
# 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

.default_configObject

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



244
245
246
# File 'lib/configurability.rb', line 244

def self::default_config
	return self.gather_defaults( Configurability::Config.new )
end

.expand_config_hash(key, hash) ⇒ Object

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



204
205
206
207
208
# 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

.extend_object(object) ⇒ Object

Add configurability to the given object.



94
95
96
97
98
99
100
101
102
103
104
105
# 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

.find_config_section(config, key) ⇒ Object

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



172
173
174
175
176
177
# 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

.gather_defaults(collection = {}) ⇒ Object

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!.



217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
# 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

.get_config_subsection(config, key) ⇒ Object

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



182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
# 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

.included(mod) ⇒ Object

Mixin hook: extend including classes instead



109
110
111
# File 'lib/configurability.rb', line 109

def self::included( mod )
	mod.extend( self )
end

.install_config(config, object) ⇒ Object

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



158
159
160
161
162
163
164
165
166
167
# 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

.make_key_from_object(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.



117
118
119
120
121
122
123
124
125
126
127
# 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

.normalize_config_key(key) ⇒ Object

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



238
239
240
# File 'lib/configurability.rb', line 238

def self::normalize_config_key( key )
	return key.to_s.gsub( /\./, '__' ).to_sym
end

.resetObject

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



151
152
153
154
# File 'lib/configurability.rb', line 151

def self::reset
	self.loaded_config = nil
	self.after_configure_hooks_run = false
end

.version_string(include_buildnum = false) ⇒ Object

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



86
87
88
89
90
# 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

Instance Method Details

#config_key(sym = nil) ⇒ Object

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



258
259
260
261
262
# 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

#config_key=(sym) ⇒ Object

Set the config key of the object.



266
267
268
269
# File 'lib/configurability.rb', line 266

def config_key=( sym )
	Configurability.configurable_objects |= [ self ]
	@config_key = Configurability.normalize_config_key( sym )
end

#configurability(config_key = nil, &block) ⇒ Object

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


330
331
332
333
334
335
336
337
338
339
340
341
342
343
# File 'lib/configurability.rb', line 330

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

#configure(config = nil) ⇒ Object

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'


294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
# 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" % [ 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

#default_configObject

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



374
375
376
377
# File 'lib/configurability.rb', line 374

def default_config
	default_values = self.defaults or return Configurability::Config::Struct.new( {} )
	return Configurability::Config::Struct.new( default_values )
end

#defaults(fallback = nil) ⇒ Object

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.



354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
# File 'lib/configurability.rb', line 354

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