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