require 'yaml' require 'set' require 'madvertise/ext/hash' require 'madvertise/ext/environment' ## # A {Configuration} consists of one or more Sections. A section is a hash-like # object that responds to all keys in the hash as if they were methods: # # > s = Section.from_hash({:v1 => 2, :nested => {:v2 => 1}}) # > s.v1 # => 2 # > s.nested.v2 # => 1 # class Section < Hash class << self # How to handle nil values in the configuration? # # Possible values are: # - :nil, nil (return nil) # - :raise (raise an exception) # - :section (return a NilSection which can be chained) # attr_accessor :nil_action # Create a new section from the given hash-like object. # # @param [Hash] hsh The hash to convert into a section. # @return [Section] The new {Section} object. def from_hash(hsh) new.tap do |result| hsh.each do |key, value| result[key.to_sym] = from_value(value) end end end # Convert the given value into a Section, list of Sections or the pure # value. Used to recursively build the Section hash. # # @private def from_value(value) case value when Hash from_hash(value) when Array value.map do |item| from_value(item) end else value end end end # Build the call chain including NilSections. # # @private def method_missing(name, *args) if name.to_s =~ /(.*)=$/ self[$1.to_sym] = Section.from_value(args.first) else value = self[name] value = value.call if value.is_a?(Proc) if value.nil? case self.class.nil_action when :nil, nil # do nothing when :raise raise "value is nil for key #{name}" when :section value = NilSection.new if value.nil? else raise "unknown nil handling: #{self.class.nil_action}" end end self[name] = value end end end ## # The Configuration class provides a simple interface to configuration stored # inside of YAML files. # class Configuration < Section # Create a new {Configuration} object. # # @param [Symbol] mode The mode to load from the configurtion file # (production, development, etc) # @yield [config] The new configuration object. def initialize @mixins = Set.new @callbacks = [] yield self if block_given? end # Load given mixins from +path+. # # @param [String] path The path to mixin files. # @param [Array] mixins_to_use A list of mixins to load from +path+. # @return [void] def load_mixins(path, mixins_to_use) mixins_to_use.map do |mixin_name| File.join(path, "#{mixin_name}.yml") end.each do |mixin_file| mixin(mixin_file) end end # Mixin a configuration snippet into the current section. # # @param [Hash, String] value A hash to merge into the current # configuration. If a string is given a filename # is assumed and the given file is expected to # contain a YAML hash. # @return [void] def mixin(value) if value.is_a?(String) @mixins << value value = YAML.load(File.read(value)) end value = Section.from_hash(value) self.deep_merge!(value[:default]) if value.has_key?(:default) self.deep_merge!(value[:generic]) if value.has_key?(:generic) if value.has_key?(Env.to_sym) self.deep_merge!(value[Env.to_sym]) else self.deep_merge!(value) end @callbacks.each do |callback| callback.call end end # Reload all mixins. # # @return [void] def reload! self.clear @mixins.each do |file| self.mixin(file) end end # Register a callback for config mixins. # # @return [void] def callback(&block) @callbacks << block end end ## # A NilSection is returned for all missing/empty values in the config file. This # allows for terse code when accessing values that have not been configured by # the user. # # Consider code like this: # # config.server.listen.tap do |listen| # open_socket(listen.host, listen.port) # end # # Given that your server component is optional and does not appear in the # configuration file at all, +config.server.listen+ will return a NilSection # that does not call the block given to tap _at all_. # class NilSection # @return true def nil? true end # @return true def empty? true end # @return false def present? false end # @return nil def tap nil end # @private def method_missing(*args, &block) self end end