lib/mixlib/config.rb in mixlib-config-1.1.2 vs lib/mixlib/config.rb in mixlib-config-2.0.0.rc.1

- old
+ new

@@ -6,167 +6,330 @@ # License:: Apache License, Version 2.0 # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at -# +# # http://www.apache.org/licenses/LICENSE-2.0 -# +# # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # +require 'mixlib/config/version' +require 'mixlib/config/configurable' +require 'mixlib/config/unknown_config_option_error' + module Mixlib module Config - def self.extended(base) class << base; attr_accessor :configuration; end + class << base; attr_accessor :configurables; end + class << base; attr_accessor :config_contexts; end base.configuration = Hash.new - end - - # Loads a given ruby file, and runs instance_eval against it in the context of the current - # object. + base.configurables = Hash.new + base.config_contexts = Array.new + end + + # Loads a given ruby file, and runs instance_eval against it in the context of the current + # object. # # Raises an IOError if the file cannot be found, or is not readable. # # === Parameters - # <string>:: A filename to read from + # filename<String>:: A filename to read from def from_file(filename) self.instance_eval(IO.read(filename), filename, 1) end - - # Pass Mixlib::Config.configure() a block, and it will yield self.configuration. + + # Pass Mixlib::Config.configure() a block, and it will yield itself # # === Parameters - # <block>:: A block that is sent self.configuration as its argument + # block<Block>:: A block that is called with self.configuration as the arugment. def configure(&block) block.call(self.configuration) end - - # Get the value of a configuration option + + # Get the value of a config option # # === Parameters - # config_option<Symbol>:: The configuration option to return + # config_option<Symbol>:: The config option to return # # === Returns - # value:: The value of the configuration option + # value:: The value of the config option # # === Raises - # <ArgumentError>:: If the configuration option does not exist + # <UnknownConfigOptionError>:: If the config option does not exist and strict mode is on. def [](config_option) - self.configuration[config_option.to_sym] + internal_get(config_option.to_sym) end - - # Set the value of a configuration option + + # Set the value of a config option # # === Parameters - # config_option<Symbol>:: The configuration option to set (within the []) - # value:: The value for the configuration option + # config_option<Symbol>:: The config option to set (within the []) + # value:: The value for the config option # # === Returns - # value:: The new value of the configuration option + # value:: The new value of the config option + # + # === Raises + # <UnknownConfigOptionError>:: If the config option does not exist and strict mode is on. def []=(config_option, value) - internal_set(config_option,value) + internal_set(config_option, value) end - - # Check if Mixlib::Config has a configuration option. + + # Check if Mixlib::Config has a config option. # # === Parameters - # key<Symbol>:: The configuration option to check for + # key<Symbol>:: The config option to check for # # === Returns - # <True>:: If the configuration option exists - # <False>:: If the configuration option does not exist + # <True>:: If the config option exists + # <False>:: If the config option does not exist def has_key?(key) self.configuration.has_key?(key.to_sym) end + # Resets a config option to its default. + # + # === Parameters + # symbol<Symbol>:: Name of the config option + def delete(symbol) + self.configuration.delete(symbol) + end + + # Resets all config options to their defaults. + def reset + self.configuration = Hash.new + self.config_contexts.each { |config_context| config_context.reset } + end + # Merge an incoming hash with our config options # # === Parameters # hash<Hash>:: The incoming hash # # === Returns # result of Hash#merge! def merge!(hash) self.configuration.merge!(hash) end - + # Return the set of config hash keys # # === Returns # result of Hash#keys def keys self.configuration.keys end - + # Creates a shallow copy of the internal hash # # === Returns # result of Hash#dup def hash_dup self.configuration.dup end - - # Internal dispatch setter, calling either the real defined method or setting the - # hash value directly + + # metaprogramming to ensure that the slot for method_symbol + # gets set to value after any other logic is run # # === Parameters # method_symbol<Symbol>:: Name of the method (variable setter) + # blk<Block>:: logic block to run in setting slot method_symbol to value # value<Object>:: Value to be set in config hash - # - def internal_set(method_symbol,value) - method_name = method_symbol.id2name - if self.respond_to?("#{method_name}=".to_sym) - self.send("#{method_name}=", value) + # + def config_attr_writer(method_symbol, &block) + configurable(method_symbol).writes_value(&block) + end + + # metaprogramming to set the default value for the given config option + # + # === Parameters + # symbol<Symbol>:: Name of the config option + # default_value<Object>:: Default value (can be unspecified) + # block<Block>:: Logic block that calculates default value + def default(symbol, default_value = nil, &block) + configurable(symbol).defaults_to(default_value, &block) + end + + # metaprogramming to set information about a config option. This may be + # used in one of two ways: + # + # 1. Block-based: + # configurable(:attr) do + # defaults_to 4 + # writes_value { |value| 10 } + # end + # + # 2. Chain-based: + # configurable(:attr).defaults_to(4).writes_value { |value| 10 } + # + # Currently supported configuration: + # + # defaults_to(value): value returned when configurable has no explicit value + # defaults_to BLOCK: block is run when the configurable has no explicit value + # writes_value BLOCK: block that is run to filter a value when it is being set + # + # === Parameters + # symbol<Symbol>:: Name of the config option + # default_value<Object>:: Default value [optional] + # block<Block>:: Logic block that calculates default value [optional] + # + # === Returns + # The value of the config option. + def configurable(symbol, &block) + unless configurables[symbol] + configurables[symbol] = Configurable.new(symbol) + end + if block + block.call(configurables[symbol]) + end + configurables[symbol] + end + + # Allows you to create a new config context where you can define new + # options with default values. + # + # For example: + # + # config_context :server_info do + # configurable(:url).defaults_to("http://localhost") + # end + # + # === Parameters + # symbol<Symbol>: the name of the context + # block<Block>: a block that will be run in the context of this new config + # class. + def config_context(symbol, &block) + context = Class.new + context.extend(::Mixlib::Config) + config_contexts << context + if block + context.instance_eval(&block) + end + configurable(symbol).defaults_to(context).writes_value do |value| + raise "config context #{symbol} cannot be modified" + end + end + + NOT_PASSED = Object.new + + # Gets or sets strict mode. When strict mode is on, only values which + # were specified with configurable(), default() or writes_with() may be + # retrieved or set. Getting or setting anything else will cause + # Mixlib::Config::UnknownConfigOptionError to be thrown. + # + # If this is set to :warn, unknown values may be get or set, but a warning + # will be printed with Chef::Log.warn if this occurs. + # + # === Parameters + # value<String>:: pass this value to set strict mode [optional] + # + # === Returns + # Current value of config_strict_mode + # + # === Raises + # <ArgumentError>:: if value is set to something other than true, false, or :warn + # + def config_strict_mode(value = NOT_PASSED) + if value == NOT_PASSED + @config_strict_mode || false else - self.configuration[method_symbol] = value + self.config_strict_mode = value end end - protected :internal_set - - # metaprogramming to ensure that the slot for method_symbol - # gets set to value after any other logic is run + # Sets strict mode. When strict mode is on, only values which + # were specified with configurable(), default() or writes_with() may be + # retrieved or set. All other values + # + # If this is set to :warn, unknown values may be get or set, but a warning + # will be printed with Chef::Log.warn if this occurs. + # # === Parameters - # method_symbol<Symbol>:: Name of the method (variable setter) - # blk<Block>:: logic block to run in setting slot method_symbol to value - # value<Object>:: Value to be set in config hash - # - def config_attr_writer(method_symbol, &blk) - meta = class << self; self; end - method_name = "#{method_symbol.to_s}=".to_sym - meta.send :define_method, method_name do |value| - self.configuration[method_symbol] = blk.call(value) + # value<String>:: pass this value to set strict mode [optional] + # + # === Raises + # <ArgumentError>:: if value is set to something other than true, false, or :warn + # + def config_strict_mode=(value) + if ![ true, false, :warn ].include?(value) + raise ArgumentError, "config_strict_mode must be true, false or :warn" end + @config_strict_mode = value end - # Allows for simple lookups and setting of configuration options via method calls + # Allows for simple lookups and setting of config options via method calls # on Mixlib::Config. If there any arguments to the method, they are used to set - # the value of the configuration option. Otherwise, it's a simple get operation. + # the value of the config option. Otherwise, it's a simple get operation. # # === Parameters - # method_symbol<Symbol>:: The method called. Must match a configuration option. + # method_symbol<Symbol>:: The method called. Must match a config option. # *args:: Any arguments passed to the method # # === Returns - # value:: The value of the configuration option. + # value:: The value of the config option. # # === Raises - # <ArgumentError>:: If the method_symbol does not match a configuration option. + # <UnknownConfigOptionError>:: If the config option does not exist and strict mode is on. def method_missing(method_symbol, *args) num_args = args.length # Setting if num_args > 0 - method_symbol = $1.to_sym unless (method_symbol.to_s =~ /(.+)=$/).nil? - internal_set method_symbol, (num_args == 1 ? args[0] : args) + method_symbol = $1.to_sym if method_symbol.to_s =~ /(.+)=$/ + internal_set(method_symbol, num_args == 1 ? args[0] : args) end - + # Returning - self.configuration[method_symbol] + internal_get(method_symbol) + end + # Internal dispatch setter, calls the setter (def myvar=) if it is defined, + # otherwise calls configurable(method_symbol).set(value) + # + # === Parameters + # method_symbol<Symbol>:: Name of the method (variable setter) + # value<Object>:: Value to be set in config hash + # + def internal_set(method_symbol,value) + # It would be nice not to have to + method_name = method_symbol.id2name + + if self.respond_to?("#{method_name}=".to_sym) + self.send("#{method_name}=", value) + else + if configurables.has_key?(method_symbol) + configurables[method_symbol].set(self.configuration, value) + else + if config_strict_mode == :warn + Chef::Log.warn("Setting unsupported config value #{method_name}..") + elsif self.config_strict_mode + raise UnknownConfigOptionError, "Cannot set unsupported config value #{method_name}." + end + configuration[method_symbol] = value + end + end + end + + protected :internal_set + + private + + def internal_get(symbol) + if configurables.has_key?(symbol) + configurables[symbol].get(self.configuration) + else + if config_strict_mode == :warn + Chef::Log.warn("Reading unsupported config value #{symbol}.") + elsif config_strict_mode + raise UnknownConfigOptionError, "Reading unsupported config value #{symbol}." + end + configuration[symbol] + end end end end