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