lib/configurations/configurable.rb in configurations-2.0.0.pre vs lib/configurations/configurable.rb in configurations-2.0.0
- old
+ new
@@ -2,125 +2,236 @@
# Module configurable provides the API of configurations
#
module Configurable
extend self
- # Once included, Configurations installs three methods in the host module: configure, configuration_defaults and configurable
+ # Once included, Configurations installs three methods in the host module:
+ # configure, configuration_defaults and configurable
#
def included(base)
install_configure_in(base)
base.class_eval do
extend ClassMethods
end
end
- # Installs #configure in base, and makes sure that it will instantiate configuration as a subclass of the host module
+ # Installs #configure in base, and makes sure that it will instantiate
+ # configuration as a subclass of the host module
#
def install_configure_in(base)
base.class_eval <<-EOF
- class << self
- # The central configure method
- # @params [Proc] block the block to configure host module with
- # @raise [ArgumentError] error when not given a block
- # @example Configure a configuration
- # MyGem.configure do |c|
- # c.foo = :bar
- # end
- #
- def configure(&block)
- raise ArgumentError, 'can not configure without a block' unless block_given?
- @configuration = #{base.name}::Configuration.new(
- defaults: @configuration_defaults,
- methods: @configuration_methods,
- configurable: @configurable,
- not_configured: @not_configured_callback,
- &block
- )
- end
+ # The central configure method
+ # @params [Proc] block the block to configure host module with
+ # @raise [ArgumentError] error when not given a block
+ # @example Configure a configuration
+ # MyGem.configure do |c|
+ # c.foo = :bar
+ # end
+ #
+ def self.configure(&block)
+ fail ArgumentError, "configure needs a block" unless block_given?
+ @configuration = #{base.name}.const_get(configuration_type).new(
+ configuration_options,
+ &block
+ )
end
EOF
end
# Class methods that will get installed in the host module
#
module ClassMethods
# A reader for Configuration
#
def configuration
- @configuration ||= @configuration_defaults && configure { }
+ @configuration ||= @configuration_defaults && configure {}
end
- # Configuration defaults can be used to set the defaults of any Configuration
+ # Configuration defaults can be used to set the defaults of
+ # any Configuration
# @param [Proc] block setting the default values of the configuration
#
def configuration_defaults(&block)
@configuration_defaults = block
end
- # configurable can be used to set the properties which should be configurable, as well as a type which
- # the given property should be asserted to
- # @param [Class, Symbol, Hash] properties a type as a first argument to type assert (if any) or nested properties to allow for setting
- # @param [Proc] block a block with arity 2 to evaluate when a property is set. It will be given: property name and value
+ # configurable can be used to set the properties which should be
+ # configurable, as well as a type which the given property should
+ # be asserted to
+ # @param [Class, Symbol, Hash] properties a type as a first argument to
+ # type assert (if any) or nested properties to allow for setting
+ # @param [Proc] block a block with arity 2 to evaluate when a property
+ # is set. It will be given: property name and value
# @example Define a configurable property
# configurable :foo
# @example Define a type asserted, nested property for type String
# configurable String, bar: :baz
# @example Define a custom assertion for a property
# configurable biz: %i(bi bu) do |value|
- # raise ArgumentError, 'must be one of a, b, c' unless %w(a b c).include?(value)
+ # unless %w(a b c).include?(value)
+ # fail ArgumentError, 'must be one of a, b, c'
+ # end
# end
#
def configurable(*properties, &block)
type = properties.shift if properties.first.is_a?(Class)
+
@configurable ||= {}
@configurable.merge! to_configurable_hash(properties, type, &block)
end
# returns whether a property is set to be configurable
# @param [Symbol] property the property to ask status for
+ # @return [Boolean] whether the property is configurable
#
def configurable?(property)
- @configurable.is_a?(Hash) && @configurable.has_key?(property)
+ @configurable.is_a?(Hash) && @configurable.key?(property)
end
- # configuration method can be used to retrieve properties from the configuration which use your gem's context
+ # configuration method can be used to retrieve properties
+ # from the configuration
+ # which use your gem's context
# @param [Class, Symbol, Hash] method the method to define
# @param [Proc] block the block to evaluate
- # @example Define a configuration method 'foobararg' returning configuration properties 'foo' and 'bar' plus an argument
+ # @example Define a configuration method 'foobararg'
# configuration_method :foobararg do |arg|
# foo + bar + arg
# end
+ # @example Define a configuration method on a nested property
+ # configuration_method foo: { bar: :arg } do
+ # baz + biz
+ # end
#
def configuration_method(method, &block)
- raise ArgumentError, "can not be both a configurable property and a configuration method" if configurable?(method)
+ fail ArgumentError, "can't be configuration property and a method" if configurable?(method)
+
@configuration_methods ||= {}
- @configuration_methods.merge! method => block
+ method_hash = if method.is_a?(Hash)
+ ingest_configuration_block!(method, &block)
+ else
+ { method => block }
+ end
+
+ @configuration_methods.merge! method_hash
end
- # not_configured defines the behaviour when a property has not been configured
- # This can be useful for presence validations of certain properties
- # or behaviour for undefined properties deviating from the original behaviour
- # @param [Proc] block the block to evaluate
+ # not_configured defines the behaviour when a property has not been
+ # configured. This can be useful for presence validations of certain
+ # properties or behaviour for undefined properties deviating from the
+ # original behaviour.
+ # @param [Array, Symbol, Hash] properties the properties to install
+ # the callback on. If omitted, the callback will be installed on
+ # all properties that have no specific callbacks
+ # @param [Proc] block the block to evaluate when a property
+ # has not been configured
# @yield [Symbol] the property that has not been configured
+ # @example Define a specific not_configured callback
+ # not_configured :property1, property2: :property3 do |property|
+ # raise ArgumentError, "#{property} should be configured"
+ # end
+ # @example Define a catch-all not_configured callback
+ # not_configured do |property|
+ # raise StandardError, "You did not configure #{property}"
+ # end
#
- def not_configured(&block)
- @not_configured_callback = block
+ def not_configured(*properties, &block)
+ @not_configured ||= {}
+
+ if properties.empty?
+ @not_configured.default_proc = ->(h, k) { h[k] = block }
+ else
+ nested_merge_not_configured_hash(*properties, &block)
+ end
end
+ # @return the class name of the configuration class to use
+ #
+ def configuration_type
+ if @configurable.nil? || @configurable.empty?
+ :ArbitraryConfiguration
+ else
+ :StrictConfiguration
+ end
+ end
+
private
# Instantiates a configurable hash from a property and a type
- # @param [Symbol, Hash, Array] properties configurable properties, either single or nested
+ # @param [Symbol, Hash, Array] properties configurable properties,
+ # either single or nested
# @param [Class] type the type to assert, if any
# @return a hash with configurable values pointing to their types
#
def to_configurable_hash(properties, type, &block)
assertion_hash = {}
assertion_hash.merge! block: block if block_given?
assertion_hash.merge! type: type if type
- assertions = ([assertion_hash] * properties.size)
- Hash[properties.zip(assertions)]
+ zip_to_hash(assertion_hash, *properties)
+ end
+
+ # Makes all values of hash point to block
+ # @param [Hash] hash the hash to modify
+ # @param [Proc] block the block to point to
+ # @return a hash with all previous values being keys pointing to block
+ #
+ def ingest_configuration_block!(hash, &block)
+ hash.each do |k, v|
+ value = if v.is_a?(Hash)
+ ingest_configuration_block!(v, &block)
+ else
+ zip_to_hash(block, *Array(v))
+ end
+
+ hash.merge! k => value
+ end
+ end
+
+ # @return a hash of configuration options with no nil values
+ #
+ def configuration_options
+ {
+ defaults: @configuration_defaults,
+ methods: @configuration_methods,
+ configurable: @configurable,
+ not_configured: @not_configured
+ }.delete_if { |_, value| value.nil? }
+ end
+
+ # merges the properties given into a not_configured hash
+ # @param [Symbol, Hash, Array] properties the properties to merge
+ # @param [Proc] block the block to point the properties to when
+ # not configured
+ #
+ def nested_merge_not_configured_hash(*properties, &block)
+ nested = properties.last.is_a?(Hash) ? properties.pop : {}
+ nested = ingest_configuration_block!(nested, &block)
+ props = zip_to_hash(block, *properties)
+
+ @not_configured.merge! nested, &method(:configuration_deep_merge)
+ @not_configured.merge! props, &method(:configuration_deep_merge)
+ end
+
+ # Solves merge conflicts when merging
+ # @param [Symbol] key the key that conflicts
+ # @param [Anything] oldval the value of the left side of the merge
+ # @param [Anything] newval the value of the right side of the merge
+ # @return a mergable value with conflicts solved
+ #
+ def configuration_deep_merge(_key, oldval, newval)
+ if oldval.is_a?(Hash) && newval.is_a?(Hash)
+ oldval.merge(newval, &method(:configuration_deep_merge))
+ else
+ Array(oldval) + Array(newval)
+ end
+ end
+
+ # Zip a value with keys to a hash so all keys point to the value
+ # @param [Anything] value the value to point to
+ # @param [Array] keys the keys to install
+ #
+ def zip_to_hash(value, *keys)
+ Hash[keys.zip([value] * keys.size)]
end
end
end
end