lib/configurable.rb in configurable-0.5.0 vs lib/configurable.rb in configurable-0.6.0

- old
+ new

@@ -1,63 +1,63 @@ +require 'configurable/version' require 'configurable/module_methods' # Configurable enables the specification of configurations within a class # definition. # # class ConfigClass # include Configurable -# # config :one, 'one' # config :two, 'two' # config :three, 'three' -# # end # # c = ConfigClass.new -# c.config.class # => Configurable::DelegateHash -# c.config # => {:one => 'one', :two => 'two', :three => 'three'} +# c.config.class # => Configurable::ConfigHash +# c.config # => {:one => 'one', :two => 'two', :three => 'three'} # # Instances have a <tt>config</tt> object that acts like a forwarding hash; # configuration keys delegate to accessors while undeclared key-value pairs # are stored internally: # # c.config[:one] = 'ONE' -# c.one # => 'ONE' +# c.one # => 'ONE' # # c.one = 1 -# c.config # => {:one => 1, :two => 'two', :three => 'three'} +# c.config # => {:one => 1, :two => 'two', :three => 'three'} # # c.config[:undeclared] = 'value' -# c.config.store # => {:undeclared => 'value'} +# c.config.store # => {:undeclared => 'value'} # # The writer for a configuration can be defined by providing a block to config. # The Validation module provides a number of common validation/transform # blocks accessible through the class method 'c': # -# class SubClass < ConfigClass +# class ValidationClass +# include Configurable # config(:one, 'one') {|v| v.upcase } # config :two, 2, &c.integer # end # -# s = SubClass.new -# s.config # => {:one => 'ONE', :two => 2, :three => 'three'} +# c = ValidationClass.new +# c.config # => {:one => 'ONE', :two => 2} # -# s.one = 'aNothER' -# s.one # => 'ANOTHER' +# c.one = 'aNothER' +# c.one # => 'ANOTHER' # -# s.two = -2 -# s.two # => -2 -# s.two = "3" -# s.two # => 3 -# s.two = nil # !> ValidationError -# s.two = 'str' # !> ValidationError +# c.two = -2 +# c.two # => -2 +# c.two = "3" +# c.two # => 3 +# c.two = nil # !> ValidationError +# c.two = 'str' # !> ValidationError # # Note that config blocks are defined in class-context and will have access # to variables outside the block (as you would expect). For instance, these # are analagous declarations: # -# class ClassConfig +# class ExampleClass # config :key, 'value' do |input| # input.upcase # end # end # @@ -71,18 +71,56 @@ # # To have the block literally define the writer, use the config_attr method. # Blocks provided to config_attr will have instance context and must set # the instance variable themselves. # -# class ConfigClass +# class LiteralClass # config_attr :key, 'value' do |input| # @key = input.upcase # end # end # -# Configurations are inherited and may be overridden in subclasses. +# Configurations are inherited and may be overridden in subclasses. They may +# also be included from a module: # +# module A +# include Configurable +# config :a, 'a' +# config :b, 'b' +# end +# +# class B +# include A +# end +# +# class C < B +# config :b, 'B' +# config :c, 'C' +# end +# +# B.new.config.to_hash # => {:a => 'a', :b => 'b'} +# C.new.config.to_hash # => {:a => 'a', :b => 'B', :c => 'C'} +# +# Lastly, configurable classes may be nested through the nest method. Nesting +# creates a configurable class with the configs defined in the nest block; +# nested configs may be accessed by chaining method calls, or through nested +# calls to config. +# +# class NestingClass +# include Configurable +# config :one, 'one' +# nest :two do +# config :three, 'three' +# end +# end +# +# c = NestingClass.new +# c.config.to_hash # => {:one => 'one', :two => {:three => 'three'}} +# +# c.two.three = 'THREE' +# c.config[:two][:three] # => 'THREE' +# # === Attributes # # Alternative reader and writer methods may be specified as config attributes. # When alternate methods are specified, Configurable assumes the methods are # declared elsewhere and will not define accessors. @@ -125,48 +163,63 @@ # Attributes provide metadata for how to use configurations in various contexts. # In general, attributes can be used to set any metadata an application # needs. A few attributes are used internally by Configurable. # # Attribute:: Use:: -# set_default:: When set to false, the delegate will not map a default value -# during bind. Specify when you manually initialize a config -# variable. +# init:: When set to false, the config will not initialize itself. +# Specify when you manually initialize a config. # type:: Specifies the type of option ConfigParser generates for this -# Delegate (ex: :switch, :flag, :list, :hidden) +# Config (ex: :switch, :flag, :list, :hidden) # desc:: The description string used in the ConfigParser help # long:: The long option (default: key) # short:: The short option. # +# Validation blocks have default attributes already assigned to them (ex type). +# In cases where a user-defined block gets used multiple times, it may be useful +# to register default attributes for that block. To do so, use this pattern: +# +# class AttributesClass +# include Configurable +# block = c.register(:type => :upcase) {|v| v.upcase } +# +# config :a, 'A', &block +# config :b, 'B', &block +# end +# +# AttributesClass.configurations[:a][:type] # => :upcase +# AttributesClass.configurations[:b][:type] # => :upcase +# module Configurable autoload(:Utils, 'configurable/utils') - # A DelegateHash bound to self + # A ConfigHash bound to self. Accessing configurations through config + # is much slower (although sometimes more convenient) than through the + # config accessors. attr_reader :config # Initializes config, if necessary, and then calls super. If initialize # is overridden without calling super, be sure to call initialize_config # manually within the new initialize method. def initialize(*args) initialize_config unless instance_variable_defined?(:@config) super end - # Reconfigures self with the given overrides. Only the - # specified configs are modified. + # Reconfigures self with the given overrides. Only the specified configs + # are modified. # # Returns self. def reconfigure(overrides={}) config.merge!(overrides) self end - # Reinitializes configurations in the copy such that - # the new object has it's own set of configurations, - # separate from the original object. + # Reinitializes configurations in the copy such that the new object has + # it's own set of configurations, separate from the original object. def initialize_copy(orig) super - initialize_config(orig.config.dup) + @config = ConfigHash.new(self, orig.config.store.dup, false) end protected # Opens the file specified by io and yield it to the block. If io is an @@ -205,12 +258,51 @@ when nil then nil else yield(io) end end - # Initializes config. Default config values - # are overridden as specified by overrides. + # Initializes config. Default config values are overridden as specified. def initialize_config(overrides={}) - @config = overrides.kind_of?(DelegateHash) ? overrides : DelegateHash.new(self.class.configurations, overrides) - @config.bind(self) + @config = ConfigHash.new(self, overrides, false) + + # cache as configs (equivalent to self.class.configurations) + # as an optimization + configs = @config.configs + + # hash overrides by delegate so they may be set + # in the correct order below + initial_values = {} + overrides.each_key do |key| + if config = configs[key] + + # check that the config may be initialized + unless config.init? + key = configs.keys.find {|k| configs[k] == config } + raise "initialization values are not allowed for: #{key.inspect}" + end + + # check that multiple overrides are not specified for a + # single config, as may happen with indifferent access + # (ex 'key' and :key) + if initial_values.has_key?(config) + key = configs.keys.find {|k| configs[k] == config } + raise "multiple values map to config: #{key.inspect}" + end + + # since overrides are used as the ConfigHash store, + # the overriding values must be removed, not read + initial_values[config] = overrides.delete(key) + end + end + + # now initialize configs in order + configs.each_pair do |key, config| + next unless config.init? + + if initial_values.has_key?(config) + config.set(self, initial_values[config]) + else + config.init(self) + end + end end end \ No newline at end of file