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