require 'yaml' require 'madvertise/ext/hash' ## # 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 # 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) result = 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_hash(item) end else value end 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) unless value.is_a?(Hash) value = Section.from_hash(YAML.load(File.read(value))) end self.deep_merge!(value[:default]) if value.has_key?(:default) self.deep_merge!(value[:generic]) if value.has_key?(:generic) if value.has_key?(@mode) self.deep_merge!(value[@mode]) else self.deep_merge!(value) 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) value = NilSection.new if value.nil? 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(mode = :development) @mode = mode 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 ## # The {Helpers} module can be included in all classes that wish to load # configuration file(s). In order to load custom configuration files the # including class needs to set the +@config_file+ instance variable. # module Helpers # Load the configuration. The default configuration is located at # +lib/ganymed/config.yml+ inside the Ganymed source tree. # # @return [Configuration] The configuration object. See madvertise-ext gem # for details. def config @config ||= Configuration.new(Env.mode) do |config| config.mixin(@default_config_file) if @default_config_file config.mixin(@config_file) if @config_file end end 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 self def tap self end # @private def method_missing(*args, &block) self end end