lib/configurable/class_methods.rb in configurable-0.1.0 vs lib/configurable/class_methods.rb in configurable-0.3.0

- old
+ new

@@ -1,74 +1,76 @@ -require 'lazydoc/attributes' +require 'lazydoc' require 'configurable/delegate_hash' -require 'configurable/validation' 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. module ClassMethods include Lazydoc::Attributes - # A hash holding the class configurations. + # A hash of (key, Delegate) pairs defining the class configurations. attr_reader :configurations def self.extended(base) # :nodoc: - caller.each_with_index do |line, index| - case line - when /\/configurable.rb/ then next - when Lazydoc::CALLER_REGEXP - base.instance_variable_set(:@source_file, File.expand_path($1)) - break - end + unless base.instance_variable_defined?(:@source_file) + caller[2] =~ Lazydoc::CALLER_REGEXP + base.instance_variable_set(:@source_file, File.expand_path($1)) end - - configurations = {}.extend IndifferentAccess - base.instance_variable_set(:@configurations, configurations) + + base.send(:initialize_configurations).extend(IndifferentAccess) end def inherited(child) # :nodoc: unless child.instance_variable_defined?(:@source_file) - caller.first =~ Lazydoc::CALLER_REGEXP + caller[0] =~ Lazydoc::CALLER_REGEXP child.instance_variable_set(:@source_file, File.expand_path($1)) end - - configurations = {} - configurations.extend IndifferentAccess if @configurations.kind_of?(IndifferentAccess) - @configurations.each_pair {|key, config| configurations[key] = config.dup } - child.instance_variable_set(:@configurations, configurations) + + # 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) + end super end - - def parser + + # Parses configurations from argv in a non-destructive manner by generating + # a ConfigParser using the configurations for self. Parsed configs are + # added to config (note that you must keep a separate reference to + # config as it is not returned by parse). The parser will is yielded to the + # block, if given, to register additonal options. Returns an array of the + # arguments that remain after parsing. + # + # See ConfigParser#parse for more information. + def parse(argv=ARGV, config={}) ConfigParser.new do |parser| - configurations.to_a.sort_by do |(key, config)| - config.attributes[:order] || 0 - end.each do |(key, config)| - parser.define(key, config.default, config.attributes) - end - end + parser.add(configurations) + yield(parser) if block_given? + end.parse(argv, config) end - - # Loads the contents of path as YAML. Returns an empty hash if the path - # is empty, does not exist, or is not a file. - def load_config(path) - # the last check prevents YAML from auto-loading itself for empty files - return {} if path == nil || !File.file?(path) || File.size(path) == 0 - YAML.load_file(path) || {} + + # Same as parse, but removes parsed args from argv. + def parse!(argv=ARGV, config={}) + argv.replace(parse(argv, config)) end protected - - def use_indifferent_access(value=true) - current = @configurations - @configurations = value ? HashWithIndifferentAccess.new : {} - current.each_pair do |key, value| - @configurations[key] = value + + # 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 + @configurations.extend(IndifferentAccess) + else + @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> @@ -91,23 +93,20 @@ # def upcase=(input) # @upcase = UPCASE_BLOCK.call(input) # end # end # - def config(key, value=nil, options={}, &block) - # register with Lazydoc - options[:desc] ||= Lazydoc.register_caller + def config(key, value=nil, attributes={}, &block) + attributes = merge_attributes(block, attributes) if block_given? - options = default_options(block).merge!(options) - instance_variable = "@#{key}".to_sym - config_attr(key, value, options) do |input| + config_attr(key, value, attributes) do |input| instance_variable_set(instance_variable, yield(input)) end else - config_attr(key, value, options) + config_attr(key, value, attributes) end end # Declares a class configuration and generates the associated accessors. # If a block is given, the <tt>key=</tt> method will perform the block @@ -132,15 +131,15 @@ # def upcase=(input) # @upcase = input.upcase # end # end # - def config_attr(key, value=nil, options={}, &block) - options = default_options(block).merge!(options) - + def config_attr(key, value=nil, attributes={}, &block) + attributes = merge_attributes(block, attributes) + # define the default public reader method - reader = options.delete(:reader) + reader = attributes.delete(:reader) case reader when true reader = key attr_reader(key) @@ -148,11 +147,11 @@ when false reader = key end # define the default public writer method - writer = options.delete(:writer) + writer = attributes.delete(:writer) if block_given? && writer != true raise ArgumentError, "a block may not be specified without writer == true" end @@ -163,14 +162,11 @@ public writer when false writer = "#{key}=" end - # register with Lazydoc - options[:desc] ||= Lazydoc.register_caller - - configurations[key] = Delegate.new(reader, writer, value, options) + configurations[key] = Delegate.new(reader, writer, value, attributes) end # Adds a configuration to self accessing the configurations for the # configurable class. Unlike config_attr and config, nest does not # create accessors; the configurations must be accessed through @@ -195,14 +191,13 @@ # end # # b = B.new # b.config[:a] # => {:key => 'value'} # - # Nest may be provided a block which receives the first value for - # the nested config and is expected to initialize an instance of - # configurable_class. In this case a reader for the instance is - # created and access becomes quite natural. + # Nest may be provided a block which initializes an instance of + # configurable_class. In this case accessors for the instance + # are created and access becomes quite natural. # # class C # include Configurable # nest(:a, A) {|overrides| A.new(overrides) } # @@ -220,37 +215,118 @@ # c.config[:a][:key] = 'two' # c.a.key # => "two" # # c.config[:a] = {:key => 'three'} # c.a.key # => "three" - # - # Nesting with an initialization block creates private methods - # that config[:a] uses to read and write the instance configurations; - # these methods are "#{key}_config" and "#{key}_config=" by default, - # but they may be renamed using the :reader and :writer options. # - # Nest checks for recursive nesting and raises an error if - # a recursive nest is detected. + # The initialize block executes in class context, much like config. # - def nest(key, configurable_class, options={}) + # # An equivalent class to illustrate class-context + # class EquivalentClass + # attr_reader :a, A + # + # INITIALIZE_BLOCK = lambda {|overrides| A.new(overrides) } + # + # def initialize(overrides={}) + # @a = INITIALIZE_BLOCK.call(overrides[:a] || {}) + # end + # end + # + # Nest checks for recursive nesting and raises an error if a recursive nest + # is detected. + # + # ==== Attributes + # + # Nesting with an initialization block creates the public accessor for the + # instance, private methods to read and write the instance configurations, + # and a private method to initialize the instance. The default names + # for these methods are listed with the attributes to override them: + # + # :instance_reader key + # :instance_writer "#{key}=" + # :instance_initializer "#{key}_initialize" + # :reader "#{key}_config_reader" + # :writer "#{key}_config_writer" + # + # These attributes are ignored if no block is given; true/false/nil + # values are meaningless and will be treated as the default. + # + def nest(key, configurable_class, attributes={}, &block) + attributes = merge_attributes(block, attributes) + + if block_given? + instance_variable = "@#{key}".to_sym + nest_attr(key, configurable_class, attributes) do |input| + instance_variable_set(instance_variable, yield(input)) + end + else + nest_attr(key, configurable_class, attributes) + end + end + + # Same as nest, except the initialize block executes in instance-context. + # + # class C + # include Configurable + # nest(:a, A) {|overrides| A.new(overrides) } + # + # def initialize(overrides={}) + # initialize_config(overrides) + # end + # end + # + # # An equivalent class to illustrate instance-context + # class EquivalentClass + # attr_reader :a, A + # + # def a_initialize(overrides) + # A.new(overrides) + # end + # + # def initialize(overrides={}) + # @a = send(:a_initialize, overrides[:a] || {}) + # end + # end + # + def nest_attr(key, configurable_class, attributes={}, &block) unless configurable_class.kind_of?(Configurable::ClassMethods) raise ArgumentError, "not a Configurable class: #{configurable_class}" end - - reader = options.delete(:reader) - writer = options.delete(:writer) - + + attributes = merge_attributes(block, attributes) + + # add some tracking attributes + attributes[:receiver] ||= configurable_class + + # remove method attributes + instance_reader = attributes.delete(:instance_reader) + instance_writer = attributes.delete(:instance_writer) + initializer = attributes.delete(:instance_initializer) + reader = attributes.delete(:reader) + writer = attributes.delete(:writer) + if block_given? # define instance accessor methods - instance_var = "@#{key}".to_sym - reader = "#{key}_config" unless reader - writer = "#{key}_config=" unless writer - + instance_reader = boolean_select(instance_reader, key) + instance_writer = boolean_select(instance_writer, "#{key}=") + instance_var = "@#{instance_reader}".to_sym + + initializer = boolean_select(reader, "#{key}_initialize") + reader = boolean_select(reader, "#{key}_config_reader") + writer = boolean_select(writer, "#{key}_config_writer") + # the public accessor - attr_reader key - public(key) - + attr_reader instance_reader + + define_method(instance_writer) do |value| + instance_variable_set(instance_var, value) + end + public(instance_reader, instance_writer) + + # the initializer + define_method(initializer, &block) + # the reader returns the config for the instance define_method(reader) do instance_variable_get(instance_var).config end @@ -258,23 +334,20 @@ # or reconfigures the instance if it already exists define_method(writer) do |value| if instance_variable_defined?(instance_var) instance_variable_get(instance_var).reconfigure(value) else - instance_variable_set(instance_var, yield(value)) + instance_variable_set(instance_var, send(initializer, value)) end end private(reader, writer) else reader = writer = nil end - - # register with Lazydoc - options[:desc] ||= Lazydoc.register_caller - value = DelegateHash.new(configurable_class.configurations).update - configurations[key] = Delegate.new(reader, writer, value, options) + value = DelegateHash.new(configurable_class.configurations) + configurations[key] = Delegate.new(reader, writer, value, attributes) check_infinite_nest(configurable_class.configurations) end # Alias for Validation @@ -282,27 +355,99 @@ Validation end private - def default_options(block) - Validation::ATTRIBUTES[block].merge( - :reader => true, - :writer => true, - :order => configurations.length) + # a helper to select a value or the default, if the default is true, + # false, or nil. used by nest_attr to handle attributes + def boolean_select(value, default) # :nodoc: + case value + when true, false, nil then default + else value + end end - # helper to recursively check a set of - # configurations for an infinite nest - def check_infinite_nest(configurations) # :nodoc: - raise "infinite nest detected" if configurations == self.configurations - - configurations.each_pair do |key, config| - config_hash = config.default(false) + # a helper to initialize configurations for the first time, + # mainly implemented as a hook for OrderedHashPatch + def initialize_configurations # :nodoc: + @configurations ||= {} + end - if config_hash.kind_of?(DelegateHash) - check_infinite_nest(config_hash.delegates) + # 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 + defaults.merge!(attributes) + + # register with Lazydoc + defaults[:desc] ||= Lazydoc.register_caller(Lazydoc::Trailer, 2) + + defaults + end + + # helper to recursively check for an infinite nest + def check_infinite_nest(delegates) # :nodoc: + raise "infinite nest detected" if delegates == self.configurations + + delegates.each_pair do |key, delegate| + if delegate.is_nest? + check_infinite_nest(delegate.default(false).delegates) end 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 + end + + module ClassMethods + undef_method :initialize_configurations + + # applies OrderedHashPatch + def initialize_configurations # :nodoc: + @configurations ||= OrderedHashPatch.new + end + end +end if RUBY_VERSION < '1.9' \ No newline at end of file