lib/configurations/configuration.rb
module Configurations
# Configuration is a blank object in order to allow configuration
# of various properties including keywords
#
class Configuration < BlankObject
class << self
# Make new a private method, but allow __new__ alias. Instantiating
# configurations is not part of the public API.
#
alias_method :__new__, :new
private :new
end
# Initialize a new configuration
# @param [Hash] options The options to initialize a configuration with
# @option options [Hash] methods a hash of method names pointing to procs
# @option options [Proc] not_configured a proc to evaluate for
# not_configured properties
def initialize(options = {}, &block)
@data = Data.new(__configuration_hash__)
@path = options.fetch(:path) { Path.new }
@data_map = options.fetch(:data) { Maps::Data.new }
@methods = options.fetch(:methods) { ::Hash.new }
@method_blocks = options.fetch(:method_blocks) { Maps::Blocks.new }
@not_configured_blocks = options.fetch(:not_configured_blocks) { Maps::Blocks.new }
@reserved_method_validator = Validators::ReservedMethods.new
@key_ambiguity_validator = Validators::Ambiguity.new
__instance_eval__(&options[:defaults]) if options[:defaults]
__instance_eval__(&block) if block
__install_configuration_methods__
end
# Method missing gives access to Kernel methods
#
def method_missing(method, *args, &block)
if __can_delegate_to_kernel?(method)
::Kernel.__send__(method, *args, &block)
else
super
end
end
# Respond to missing according to the method_missing implementation
#
def respond_to_missing?(method, include_private = false)
__can_delegate_to_kernel?(method) || super
end
# A convenience accessor to get a hash representation of the
# current state of the configuration
# @return [Hash] the configuration in hash form
#
def to_h
@data.reduce({}) do |h, (k, v)|
h[k] = v.is_a?(__class__) ? v.to_h : v
h
end
end
# A convenience accessor to instantiate a configuration from a hash
# @param [Hash] h the hash to read into the configuration
# @return [Configuration] the configuration with values assigned
# @raise [ConfigurationError] if the given hash ambiguous values
# - string and symbol keys with the same string value pointing to
# different values
#
def from_h(h)
@key_ambiguity_validator.validate!(h)
h.each do |property, value|
p = property.to_sym
if value.is_a?(::Hash) && __nested?(p)
@data[p].from_h(value)
elsif __configurable?(p)
__assign!(p, value)
end
end
self
end
# Inspect a configuration. Implements inspect without exposing internally
# used instance variables.
# @param [TrueClass, FalseClass] debug whether to show internals, defaults
# to false
# @return [String] The inspect output for this instance
#
def inspect(debug = false)
unless debug
'#<%s:0x00%x @data=%s>' % [__class__, object_id << 1, @data.inspect]
else
super()
end
end
# @param [Symbol] property The property to test for configurability
# @return [Boolean] whether the given property is configurable
#
def __configurable?(property)
if defined?(@configurable_properties) && @configurable_properties
@configurable_properties.configurable?(@path.add(property))
else
true
end
end
# @param [Symbol] property The property to test for
# @return [Boolean] whether the given property has been configured
#
def __configured?(property)
@data.key?(property)
end
# @return [Boolean] whether this configuration is empty
def __empty?
@data.empty?
end
protected
# Installs the given configuration methods for this configuration
# as singleton methods
#
def __install_configuration_methods__
entries = @method_blocks.entries_at(@path)
entries.each do |meth, entry|
@reserved_method_validator.validate!(meth)
__define_singleton_method__(meth, &entry.block)
end
end
# Instantiates an options hash for a nested property
# @param [Symbol] property the nested property to instantiate the hash for
# @return [Hash] a hash to be used for configuration initialization
#
def __options_hash_for__(property)
nested_path = @path.add(property)
hash = {}
hash[:path] = nested_path
hash[:data] = @data_map
hash[:properties] = defined?(@properties) && @properties
hash[:not_configured_blocks] = @not_configured_blocks
hash[:method_blocks] = @method_blocks
hash[:methods] = @methods[property] if @methods.key?(property)
hash
end
# @return [Hash] A configuration hash instantiating subhashes
# if the key is configurable
#
def __configuration_hash__
::Hash.new do |h, k|
h[k] = __class__.__new__(__options_hash_for__(k)) if __configurable?(k)
end
end
# Assigns a value after running the assertions
# @param [Symbol] property the property to type test
# @param [Any] value the given value
#
def __assign!(property, value)
@data_map.add_entry(@path.add(property), value)
@data[property] = value
end
# @param [Symbol] method the method to test for
# @return [Boolean] whether the given method is a writer
#
def __is_writer?(method)
method.to_s.end_with?('=')
end
def __nested?(property)
@data[property].is_a?(__class__)
end
# @param [Symbol] method the method to test for
# @return [Boolean] whether the configuration can delegate
# the given method to Kernel
#
def __can_delegate_to_kernel?(method)
::Kernel.respond_to?(method, true)
end
# @param [Symbol] method the writer method to turn into a property
# @return [Symbol] the property derived from the writer method
#
def __property_from_writer__(method)
method.to_s[0..-2].to_sym
end
end
end