require 'lazydoc' require 'configurable/delegate_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. module ClassMethods include Lazydoc::Attributes # A hash of (key, Delegate) pairs defining the class configurations. attr_reader :configurations def self.extended(base) # :nodoc: unless base.instance_variable_defined?(:@source_file) caller[2] =~ Lazydoc::CALLER_REGEXP base.instance_variable_set(:@source_file, File.expand_path($1)) end base.send(:initialize_configurations).extend(IndifferentAccess) end 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)) 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) end super end # 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| parser.add(configurations) yield(parser) if block_given? end.parse(argv, config) end # Same as parse, but removes parsed args from argv. def parse!(argv=ARGV, config={}) argv.replace(parse(argv, config)) 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 @configurations.extend(IndifferentAccess) else @configurations = configurations.dup end end # Declares a class configuration and generates the associated accessors. # If a block is given, the key= method will set @key # to the return of the block, which executes in class-context. # # class SampleClass # include Configurable # # config :str, 'value' # config(:upcase, 'value') {|input| input.upcase } # end # # # An equivalent class to illustrate class-context # class EquivalentClass # attr_accessor :str # attr_reader :upcase # # UPCASE_BLOCK = lambda {|input| input.upcase } # # def upcase=(input) # @upcase = UPCASE_BLOCK.call(input) # end # end # def config(key, value=nil, attributes={}, &block) attributes = merge_attributes(block, attributes) if block_given? instance_variable = "@#{key}".to_sym config_attr(key, value, attributes) do |input| instance_variable_set(instance_variable, yield(input)) end else config_attr(key, value, attributes) end end # Declares a class configuration and generates the associated accessors. # If a block is given, the key= method will perform the block # with instance-context. # # class SampleClass # include Configurable # # def initialize # initialize_config # end # # config_attr :str, 'value' # config_attr(:upcase, 'value') {|input| @upcase = input.upcase } # end # # # An equivalent class to illustrate instance-context # class EquivalentClass # attr_accessor :str # attr_reader :upcase # # def upcase=(input) # @upcase = input.upcase # end # end # def config_attr(key, value=nil, attributes={}, &block) attributes = merge_attributes(block, attributes) # define the default public reader method reader = attributes.delete(:reader) case reader when true reader = key attr_reader(key) public(key) when false reader = key end # define the default public writer method writer = attributes.delete(:writer) if block_given? && writer != true raise ArgumentError, "a block may not be specified without writer == true" end case writer when true writer = "#{key}=" block_given? ? define_method(writer, &block) : attr_writer(key) public writer when false writer = "#{key}=" end 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 # the instance config method. # # class A # include Configurable # config :key, 'value' # # def initialize(overrides={}) # initialize_config(overrides) # end # end # # class B # include Configurable # nest :a, A # # def initialize(overrides={}) # initialize_config(overrides) # end # end # # b = B.new # b.config[:a] # => {:key => 'value'} # # 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) } # # def initialize(overrides={}) # initialize_config(overrides) # end # end # # c = C.new # c.a.key # => "value" # # c.a.key = "one" # c.config[:a].to_hash # => {:key => 'one'} # # c.config[:a][:key] = 'two' # c.a.key # => "two" # # c.config[:a] = {:key => 'three'} # c.a.key # => "three" # # The initialize block executes in class context, much like config. # # # 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 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_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 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 # the writer initializes the instance if necessary, # 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, send(initializer, value)) end end private(reader, writer) else reader = writer = nil end value = DelegateHash.new(configurable_class.configurations) configurations[key] = Delegate.new(reader, writer, value, attributes) check_infinite_nest(configurable_class.configurations) end # Alias for Validation def c Validation end private # 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 # 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 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 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'