lib/configurable/class_methods.rb in configurable-0.3.0 vs lib/configurable/class_methods.rb in configurable-0.4.0
- old
+ new
@@ -131,240 +131,258 @@
# 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
+ # reader:: The method used to read the configuration.
+ # (default: key)
+ # writer:: The method used to write the configuration
+ # (default: "#{key}=")
+ #
+ # Neither attribute 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
+ # uses and defines the default methods. Specifying false uses 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 default public reader method
- reader = attributes.delete(:reader)
-
- case reader
- when true
- reader = key
- attr_reader(key)
- public(key)
- when false
- reader = key
+ # define the reader
+ reader = define_attribute_method(:reader, attributes, key) do |attribute|
+ attr_reader(attribute)
+ public(attribute)
end
-
- # define the default public writer method
- writer = attributes.delete(:writer)
-
- if block_given? && writer != true
+
+ # define the writer
+ if block_given? && attributes[: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}="
+
+ writer = define_attribute_method(:writer, attributes, "#{key}=") do |attribute|
+ block_given? ? define_method(attribute, &block) : attr_writer(key)
+ public(attribute)
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.
+
+ # 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, 'value'
#
- # def initialize(overrides={})
- # initialize_config(overrides)
+ # 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
- # nest :a, A
#
- # def initialize(overrides={})
- # initialize_config(overrides)
+ # config :key, 1, &c.integer
+ # nest :nest do
+ # config :key, 2, &c.integer
+ # nest :nest do
+ # config :key, 3, &c.integer
+ # end
# 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
+ # nest :a, A
+ # nest :b, B
# end
#
# c = C.new
- # c.a.key # => "value"
+ # c.b.key = 7
+ # c.b.nest.key = "8"
+ # c.config[:b][:nest][:nest][:key] = "9"
#
- # c.a.key = "one"
- # c.config[:a].to_hash # => {:key => 'one'}
+ # c.config.to_hash
+ # # => {
+ # # :a => {
+ # # :key => 'one',
+ # # :nest => {:key => 'two'}
+ # # },
+ # # :b => {
+ # # :key => 7,
+ # # :nest => {
+ # # :key => 8,
+ # # :nest => {:key => 9}
+ # # }
+ # # }}
#
- # c.config[:a][:key] = 'two'
- # c.a.key # => "two"
+ # === Attributes
#
- # c.config[:a] = {:key => 'three'}
- # c.a.key # => "three"
+ # Nest provides a number of attributes that can modify how a nest is
+ # constructed. Attribute keys should be specified as symbols.
#
- # The initialize block executes in class context, much like config.
+ # Attribute:: Description
+ # const_name:: Determines the constant name of the configurable
+ # class within the nesting class. May be nil.
+ # (default: key.to_s.capitalize)
+ # instance_reader:: The method accessing the nested instance. (default: key)
+ # instance_writer:: The method to set the nested instance. (default: "#{key}=")
+ # instance_initializer:: The method that initializes the instance.
+ # (default: "initialize_#{key}")
+ # reader:: The method used to read the instance configuration.
+ # (default: "#{key}_config_reader")
+ # writer:: The method used to initialize or reconfigure the
+ # instance. (default: "#{key}_config_writer")
#
- # # An equivalent class to illustrate class-context
- # class EquivalentClass
- # attr_reader :a, A
+ # Except for const_name, these attributes are used to define methods
+ # required for nesting to work properly. None of the method attributes may
+ # be set to nil, but they may be set to non-default values. In that case,
+ # nest will register the method names as provided, but it will not define
+ # the methods themselves. The user must define methods with the following
+ # functionality:
#
- # INITIALIZE_BLOCK = lambda {|overrides| A.new(overrides) }
+ # Attribute:: Function
+ # instance_reader:: Returns the instance of the configurable class
+ # instance_writer:: Inputs and sets the instance of the configurable class
+ # instance_initializer:: Receives the initial config and return an instance of
+ # configurable class
+ # reader:: Returns instance.config
+ # writer:: Reconfigures instance using the input overrides,
+ # or uses instance_initializer and instance_writer to
+ # initialize and set the instance.
#
- # def initialize(overrides={})
- # @a = INITIALIZE_BLOCK.call(overrides[:a] || {})
- # end
- # end
+ # Methods can be public or otherwise. Specifying true uses and defines the
+ # default methods. Specifying false uses the default method name, but does
+ # not define the method itself.
#
- # 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)
+ # Any additional attributes are registered with the configuration.
+ def nest(key, configurable_class=nil, attributes={}, &block)
attributes = merge_attributes(block, attributes)
+ attributes = {
+ :instance_reader => true,
+ :instance_writer => true,
+ :initializer => true
+ }.merge(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
+ # define the nested configurable
+ if configurable_class
+ raise "a block is not allowed when a configurable class is specified" if block_given?
else
- nest_attr(key, configurable_class, attributes)
+ configurable_class = Class.new { include Configurable }
+ configurable_class.class_eval(&block) if block_given?
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}"
+
+ # set the new constant
+ const_name = if attributes.has_key?(:const_name)
+ attributes.delete(:const_name)
+ else
+ key.to_s.capitalize
end
+ const_set(const_name, configurable_class) if const_name
- attributes = merge_attributes(block, attributes)
+ # define instance reader
+ instance_reader = define_attribute_method(:instance_reader, attributes, key) do |attribute|
+ attr_reader(key)
+ public(key)
+ end
- # add some tracking attributes
- attributes[:receiver] ||= configurable_class
+ # define instance writer
+ instance_writer = define_attribute_method(:instance_writer, attributes, "#{key}=") do |attribute|
+ attr_writer(key)
+ public(attribute)
+ end
- # 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)
+ # define initializer
+ initializer = define_attribute_method(:initializer, attributes, "initialize_#{key}") do |attribute|
+ define_method(attribute) {|config| configurable_class.new.reconfigure(config) }
+ private(attribute)
+ end
- 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)
+ # define the reader
+ reader = define_attribute_method(:reader, attributes, "#{key}_config_reader") do |attribute|
+ define_method(attribute) { send(instance_reader).config }
+ private(attribute)
+ end
+
+ # define the writer
+ writer = define_attribute_method(:writer, attributes, "#{key}_config_writer") do |attribute|
+ define_method(attribute) do |value|
+ if instance = send(instance_reader)
+ instance.reconfigure(value)
else
- instance_variable_set(instance_var, send(initializer, value))
+ send(instance_writer, send(initializer, value))
end
end
- private(reader, writer)
- else
- reader = writer = nil
+ private(attribute)
end
- value = DelegateHash.new(configurable_class.configurations)
- configurations[key] = Delegate.new(reader, writer, value, attributes)
-
+ # define the configuration
+ nested_config = DelegateHash.new(configurable_class.configurations)
+ configurations[key] = Delegate.new(reader, writer, nested_config, attributes)
+
check_infinite_nest(configurable_class.configurations)
- end
-
+ 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
+ # 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 to initialize configurations for the first time,
# mainly implemented as a hook for OrderedHashPatch
def initialize_configurations # :nodoc:
\ No newline at end of file