lib/configurable/class_methods.rb in configurable-0.5.0 vs lib/configurable/class_methods.rb in configurable-0.6.0
- old
+ new
@@ -1,41 +1,40 @@
-require 'lazydoc'
-require 'configurable/delegate_hash'
+require 'configurable/config_hash'
require 'configurable/indifferent_access'
require 'configurable/validation'
autoload(:ConfigParser, 'config_parser')
module Configurable
- # ClassMethods extends classes that include Configurable and
- # provides methods for declaring configurations.
+ # ClassMethods extends classes that include Configurable and provides methods
+ # for declaring configurations.
module ClassMethods
- include Lazydoc::Attributes
-
- # A hash of (key, Delegate) pairs defining the class configurations.
- attr_reader :configurations
-
- def inherited(child) # :nodoc:
- unless child.instance_variable_defined?(:@source_file)
- caller[0] =~ Lazydoc::CALLER_REGEXP
- child.instance_variable_set(:@source_file, File.expand_path($1))
+ CONFIGURATIONS_CLASS = Hash
+
+ # A hash of (key, Config) pairs tracking configs defined on self. See
+ # configurations for all configs declared across all ancestors.
+ attr_reader :config_registry
+
+ def self.initialize(base) # :nodoc:
+ unless base.instance_variable_defined?(:@config_registry)
+ base.instance_variable_set(:@config_registry, CONFIGURATIONS_CLASS.new)
end
-
- # deep duplicate configurations
- unless child.instance_variable_defined?(:@configurations)
- duplicate = child.instance_variable_set(:@configurations, configurations.dup)
- duplicate.each_pair {|key, config| duplicate[key] = config.dup }
- duplicate.extend(IndifferentAccess) if configurations.kind_of?(IndifferentAccess)
+
+ unless base.instance_variable_defined?(:@use_indifferent_access)
+ base.instance_variable_set(:@use_indifferent_access, true)
end
- super
+
+ unless base.instance_variable_defined?(:@configurations)
+ base.instance_variable_set(:@configurations, nil)
+ end
end
# Parses configurations from argv in a non-destructive manner by generating
# a ConfigParser using the configurations for self. Returns an array like
# [args, config] where the args are the arguments that remain after parsing,
- # and config is a hash of the parsed configs. The parser will is yielded to
+ # and config is a hash of the parsed configs. The parser is yielded to
# the block, if given, to register additonal options.
#
# See ConfigParser#parse for more information.
def parse(argv=ARGV, options={}) # :yields: parser
parse!(argv.dup, options)
@@ -48,20 +47,59 @@
args = parser.parse!(argv, options)
[args, parser.config]
end
- protected
+ # A hash of (key, Config) pairs representing all configurations defined
+ # on this class or inherited from ancestors. The configurations hash is
+ # generated on each call to ensure it accurately reflects any configs
+ # added on ancestors. This slows down initialization and config access
+ # through instance.config.
+ #
+ # Call cache_configurations after all configs have been declared in order
+ # to prevent regeneration of configurations and to significantly improve
+ # performance.
+ def configurations
+ return @configurations if @configurations
+
+ configurations = CONFIGURATIONS_CLASS.new
+ configurations.extend(IndifferentAccess) if @use_indifferent_access
+
+ ancestors.reverse.each do |ancestor|
+ next unless ancestor.kind_of?(ClassMethods)
+ ancestor.config_registry.each_pair do |key, value|
+ if value.nil?
+ configurations.delete(key)
+ else
+ configurations[key] = value
+ end
+ end
+ end
+
+ configurations
+ end
+ # Caches the configurations hash so as to improve peformance. Call
+ # with on set to false to turn off caching.
+ def cache_configurations(on=true)
+ @configurations = nil
+ @configurations = self.configurations if on
+ end
+
+ protected
+
# Sets configurations to symbolize keys for AGET ([]) and ASET([]=)
# operations, or not. By default, configurations will use
# indifferent access.
def use_indifferent_access(input=true)
- if input
+ @use_indifferent_access = input
+ return unless @configurations
+
+ if @use_indifferent_access
@configurations.extend(IndifferentAccess)
else
- @configurations = configurations.dup
+ @configurations = @configurations.dup
end
end
# Declares a class configuration and generates the associated accessors.
# If a block is given, the <tt>key=</tt> method will set <tt>@key</tt>
@@ -127,21 +165,24 @@
# === Attributes
#
# Several attributes may be specified to modify how a config is constructed.
# Attribute keys should be specified as symbols.
#
- # Attribute:: Description
+ # Attribute:: Description
+ # init:: When set to false the config will not initialize
+ # during initialize_config. (default: true)
# reader:: The method used to read the configuration.
# (default: key)
# writer:: The method used to write the configuration
# (default: "#{key}=")
#
- # Neither attribute may be set to nil, but they may be set to non-default
- # values. In that case, config_attr will register the method names as
- # provided, but it will not define the methods themselves. Specifying true
- # uses and defines the default methods. Specifying false uses the default
- # method name, but does not define the method itself.
+ # Neither reader nor writer may be set to nil, but they may be set to
+ # non-default values. In that case, config_attr will register the method
+ # names as provided, but it will not define the methods themselves.
+ # Specifying true defines the default methods. Specifying false makes
+ # the config expect the default method name, but does not define the method
+ # itself.
#
# Any additional attributes are registered with the configuration.
def config_attr(key, value=nil, attributes={}, &block)
attributes = merge_attributes(block, attributes)
@@ -159,11 +200,14 @@
writer = define_attribute_method(:writer, attributes, "#{key}=") do |attribute|
block_given? ? define_method(attribute, &block) : attr_writer(key)
public(attribute)
end
- configurations[key] = Delegate.new(reader, writer, value, attributes)
+ # define the configuration
+ init = attributes.has_key?(:init) ? attributes.delete(:init) : true
+ dup = attributes.has_key?(:dup) ? attributes.delete(:dup) : nil
+ config_registry[key] = Config.new(reader, writer, value, attributes, init, dup)
end
# Adds nested configurations to self. Nest creates a new configurable
# class using the block, and provides accessors to an instance of the
# new class. Everything is set up so you can access configs through
@@ -235,55 +279,32 @@
# # }
# # }}
#
# === Attributes
#
- # Nest provides a number of attributes that can modify how a nest is
- # constructed. Attribute keys should be specified as symbols.
+ # Nest uses the same attributes as config_attr, with a couple additions:
#
# Attribute:: Description
# const_name:: Determines the constant name of the configurable
# class within the nesting class. May be nil.
# (default: key.to_s.capitalize)
- # instance_reader:: The method accessing the nested instance. (default: key)
- # instance_writer:: The method to set the nested instance. (default: "#{key}=")
- # reader:: The method used to read the instance config.
- # (default: "#{key}_config_reader")
- # writer:: The method used to reconfigure the instance.
- # (default: "#{key}_config_writer")
+ # type:: By default set to :nest.
#
- # Except for const_name, these attributes are used to define methods
- # required for nesting to work properly. None of the method attributes may
- # be set to nil, but they may be set to non-default values. In that case,
- # nest will register the method names as provided, but it will not define
- # the methods themselves. The user must define methods with the following
- # functionality:
- #
- # Attribute:: Function
- # instance_reader:: Returns the instance of the configurable class
- # (initializing if necessary, by default nest initializes
- # using configurable_class.new)
- # instance_writer:: Inputs and sets the instance of the configurable class
- # reader:: Returns instance.config
- # writer:: Reconfigures instance using the input overrides, or
- # sets instance if provided.
- #
- # Methods can be public or otherwise. Specifying true uses and defines the
- # default methods. Specifying false uses the default method name, but does
- # not define the method itself.
- #
- # Any additional attributes are registered with the configuration.
def nest(key, configurable_class=nil, attributes={}, &block)
attributes = merge_attributes(block, attributes)
attributes = {
- :instance_reader => true,
- :instance_writer => true,
+ :reader => true,
+ :writer => true,
+ :type => :nest
}.merge(attributes)
# define the nested configurable
if configurable_class
- raise "a block is not allowed when a configurable class is specified" if block_given?
+ if block_given?
+ configurable_class = Class.new(configurable_class)
+ configurable_class.class_eval(&block)
+ end
else
configurable_class = Class.new { include Configurable }
configurable_class.class_eval(&block) if block_given?
end
@@ -299,67 +320,107 @@
# class defines the configurable_class
unless const_defined?(const_name) && const_get(const_name) == configurable_class
const_set(const_name, configurable_class)
end
end
+ const_name = nil
- # define instance reader
- instance_reader = define_attribute_method(:instance_reader, attributes, key) do |attribute|
- instance_variable = "@#{key}".to_sym
-
- # gets or initializes the instance
- define_method(attribute) do
- if instance_variable_defined?(instance_variable)
- instance_variable_get(instance_variable)
- else
- instance_variable_set(instance_variable, configurable_class.new)
- end
- end
-
- public(key)
+ # define the reader.
+ reader = define_attribute_method(:reader, attributes, key) do |attribute|
+ attr_reader attribute
+ public(attribute)
end
- # define instance writer
- instance_writer = define_attribute_method(:instance_writer, attributes, "#{key}=") do |attribute|
- attr_writer(key)
+ # define the writer. the default the writer validates the
+ # instance is the correct class then sets the instance variable
+ instance_variable = "@#{key}".to_sym
+ writer = define_attribute_method(:writer, attributes, "#{key}=") do |attribute|
+ define_method(attribute) do |value|
+ Validation.validate(value, [configurable_class])
+ instance_variable_set(instance_variable, value)
+ end
public(attribute)
end
- # define the reader
- reader = define_attribute_method(:reader, attributes, "#{key}_config_reader") do |attribute|
- define_method(attribute) do
- send(instance_reader).config
- end
- private(attribute)
+ # define the configuration
+ init = attributes.has_key?(:init) ? attributes.delete(:init) : true
+ config_registry[key] = NestConfig.new(configurable_class, reader, writer, attributes, init)
+ check_infinite_nest(configurable_class)
+ end
+
+ # Removes a configuration much like remove_method removes a method. The
+ # reader and writer for the config are likewise removed. Nested configs
+ # can be removed using this method.
+ #
+ # Setting :reader or :writer to false in the options prevents those methods
+ # from being removed.
+ #
+ def remove_config(key, options={})
+ unless config_registry.has_key?(key)
+ raise NameError.new("#{key.inspect} is not a config on #{self}")
end
- # define the writer
- writer = define_attribute_method(:writer, attributes, "#{key}_config_writer") do |attribute|
- define_method(attribute) do |value|
- if value.kind_of?(configurable_class)
- send(instance_writer, value)
- else
- send(instance_reader).reconfigure(value)
- end
- end
- private(attribute)
+ options = {
+ :reader => true,
+ :writer => true
+ }.merge(options)
+
+ config = config_registry.delete(key)
+ cache_configurations(@configurations != nil)
+
+ undef_method(config.reader) if options[:reader]
+ undef_method(config.writer) if options[:writer]
+ end
+
+ # Undefines a configuration much like undef_method undefines a method. The
+ # reader and writer for the config are likewise undefined. Nested configs
+ # can be undefined using this method.
+ #
+ # Setting :reader or :writer to false in the options prevents those methods
+ # from being undefined.
+ #
+ # ==== Implementation Note
+ #
+ # Configurations are undefined by setting the key to nil in the registry.
+ # Deleting the config is not sufficient because the registry needs to
+ # convey to self and subclasses to not inherit the config from ancestors.
+ #
+ # This is unlike remove_config where the config is simply deleted from
+ # the config_registry.
+ #
+ def undef_config(key, options={})
+ # temporarily cache as an optimization
+ configs = configurations
+ unless configs.has_key?(key)
+ raise NameError.new("#{key.inspect} is not a config on #{self}")
end
- # define the configuration
- nested_config = DelegateHash.new(configurable_class.configurations)
- configurations[key] = Delegate.new(reader, writer, nested_config, attributes)
+ options = {
+ :reader => true,
+ :writer => true
+ }.merge(options)
- check_infinite_nest(configurable_class.configurations)
- end
+ config = configs[key]
+ config_registry[key] = nil
+ cache_configurations(@configurations != nil)
+
+ undef_method(config.reader) if options[:reader]
+ undef_method(config.writer) if options[:writer]
+ end
# Alias for Validation
def c
Validation
end
private
+ def inherited(base) # :nodoc:
+ ClassMethods.initialize(base)
+ super
+ end
+
# a helper to define methods that may be overridden in attributes.
# yields the default to the block if the default is supposed to
# be defined. returns the symbolized method name.
def define_attribute_method(name, attributes, default) # :nodoc:
attribute = attributes.delete(name)
@@ -382,16 +443,10 @@
# and lets the user define the method.
attribute.to_sym
end
- # a helper to initialize configurations for the first time,
- # mainly implemented as a hook for OrderedHashPatch
- def initialize_configurations # :nodoc:
- @configurations ||= {}
- end
-
# a helper method to merge the default attributes for the block with
# the input attributes. also registers a Trailer description.
def merge_attributes(block, attributes) # :nodoc:
defaults = DEFAULT_ATTRIBUTES[nil].dup
defaults.merge!(DEFAULT_ATTRIBUTES[block]) if block
@@ -402,97 +457,21 @@
defaults
end
# helper to recursively check for an infinite nest
- def check_infinite_nest(delegates) # :nodoc:
- raise "infinite nest detected" if delegates == self.configurations
+ def check_infinite_nest(klass) # :nodoc:
+ raise "infinite nest detected" if klass == self
- delegates.each_pair do |key, delegate|
- if delegate.is_nest?
- check_infinite_nest(delegate.default(false).delegates)
+ klass.configurations.each_value do |delegate|
+ if delegate.kind_of?(NestConfig)
+ check_infinite_nest(delegate.nest_class)
end
end
end
end
end
-module Configurable
-
- # Beginning with ruby 1.9, Hash tracks the order of insertion and methods
- # like each_pair return pairs in order. Configurable leverages this feature
- # to keep configurations in order for the command line documentation produced
- # by ConfigParser.
- #
- # Pre-1.9 ruby implementations require a patched Hash that tracks insertion
- # order. This very thin subclass of hash does that for ASET insertions and
- # each_pair. OrderedHashPatches are used as the configurations object in
- # Configurable classes for pre-1.9 ruby implementations and for nothing else.
- class OrderedHashPatch < Hash
- def initialize
- super
- @insertion_order = []
- end
-
- # ASET insertion, tracking insertion order.
- def []=(key, value)
- @insertion_order << key unless @insertion_order.include?(key)
- super
- end
-
- # Keys, sorted into insertion order
- def keys
- super.sort_by do |key|
- @insertion_order.index(key) || length
- end
- end
-
- # Yields each key-value pair to the block in insertion order.
- def each_pair
- keys.each do |key|
- yield(key, fetch(key))
- end
- end
-
- # Ensures the insertion order of duplicates is separate from parents.
- def initialize_copy(orig)
- super
- @insertion_order = orig.instance_variable_get(:@insertion_order).dup
- end
-
- # Overridden to load an array of [key, value] pairs in order (see to_yaml).
- # The default behavior for loading from a hash of key-value pairs is
- # preserved, but the insertion order will not be preserved.
- def yaml_initialize( tag, val )
- @insertion_order ||= []
-
- if Array === val
- val.each do |k, v|
- self[k] = v
- end
- else
- super
- end
- end
-
- # Overridden to preserve insertion order by serializing self as an array
- # of [key, value] pairs.
- def to_yaml( opts = {} )
- YAML::quick_emit( object_id, opts ) do |out|
- out.seq( taguri, to_yaml_style ) do |seq|
- each_pair do |key, value|
- seq.add( [key, value] )
- end
- end
- end
- end
- end
-
- module ClassMethods
- undef_method :initialize_configurations
-
- # applies OrderedHashPatch
- def initialize_configurations # :nodoc:
- @configurations ||= OrderedHashPatch.new
- end
- end
-end if RUBY_VERSION < '1.9'
+# Apply the ordered hash patch if necessary
+if RUBY_VERSION < '1.9'
+ require 'configurable/ordered_hash_patch'
+end
\ No newline at end of file