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. module ClassMethods 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 unless base.instance_variable_defined?(:@use_indifferent_access) base.instance_variable_set(:@use_indifferent_access, true) end 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 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) end # Same as parse, but removes parsed args from argv. def parse!(argv=ARGV, options={}) parser = ConfigParser.new parser.add(configurations) args = parser.parse!(argv, options) [args, parser.config] end # 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) @use_indifferent_access = input return unless @configurations if @use_indifferent_access @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 # # === Attributes # # Several attributes may be specified to modify how a config is constructed. # Attribute keys should be specified as symbols. # # 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 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) # define the reader reader = define_attribute_method(:reader, attributes, key) do |attribute| attr_reader(attribute) public(attribute) end # define the writer if block_given? && attributes[:writer] != true raise ArgumentError, "a block may not be specified without writer == true" end writer = define_attribute_method(:writer, attributes, "#{key}=") do |attribute| block_given? ? define_method(attribute, &block) : attr_writer(key) public(attribute) end # 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 # the instance or through config. # # class A # include Configurable # # config :key, 'one' # nest :nest do # config :key, 'two' # end # end # # a = A.new # a.key # => 'one' # a.config[:key] # => 'one' # # a.nest.key # => 'two' # a.config[:nest][:key] # => 'two' # # a.nest.key = 'TWO' # a.config[:nest][:key] # => 'TWO' # # a.config[:nest][:key] = 2 # a.nest.key # => 2 # # a.config.to_hash # => {:key => 'one', :nest => {:key => 2}} # a.nest.config.to_hash # => {:key => 2} # a.nest.class # => A::Nest # # An existing configurable class may be provided instead of using the block # to define a new configurable class. Recursive nesting is supported. # # class B # include Configurable # # config :key, 1, &c.integer # nest :nest do # config :key, 2, &c.integer # nest :nest do # config :key, 3, &c.integer # end # end # end # # class C # include Configurable # nest :a, A # nest :b, B # end # # c = C.new # c.b.key = 7 # c.b.nest.key = "8" # c.config[:b][:nest][:nest][:key] = "9" # # c.config.to_hash # # => { # # :a => { # # :key => 'one', # # :nest => {:key => 'two'} # # }, # # :b => { # # :key => 7, # # :nest => { # # :key => 8, # # :nest => {:key => 9} # # } # # }} # # === Attributes # # 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) # type:: By default set to :nest. # def nest(key, configurable_class=nil, attributes={}, &block) attributes = merge_attributes(block, attributes) attributes = { :reader => true, :writer => true, :type => :nest }.merge(attributes) # define the nested configurable if configurable_class 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 # set the new constant const_name = if attributes.has_key?(:const_name) attributes.delete(:const_name) else key.to_s.capitalize end if const_name # this prevents a warning in cases where the nesting # 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 the reader. reader = define_attribute_method(:reader, attributes, key) do |attribute| attr_reader attribute public(attribute) end # 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 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 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 options = { :reader => true, :writer => true }.merge(options) 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) case attribute when true # true means use the default and define the method attribute = default yield(attribute) when false # false means use the default, but let the user define the method attribute = default when nil # nil is not allowed raise "#{name.inspect} attribute cannot be nil" end # ... all other values specify what the method should be, # and lets the user define the method. attribute.to_sym 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(klass) # :nodoc: raise "infinite nest detected" if klass == self klass.configurations.each_value do |delegate| if delegate.kind_of?(NestConfig) check_infinite_nest(delegate.nest_class) end end end end end # Apply the ordered hash patch if necessary if RUBY_VERSION < '1.9' require 'configurable/ordered_hash_patch' end