require 'yaml' require 'facets/core/kernel/constant' require 'facets/more/synchash' require 'facets/more/dictionary' require 'facets/core/string/capitalized' require 'facets/core/class/cattr' module Glue # A Configuration holds a group of Settings organized by # Owners. An owner is a class that represents the system to be # configured. An alias for this class is Settings. # # You can pass strings, constants or symbols as keys for the # classes to be configured. Passing symbols you can configure # classes even before they are defined. #-- # TODO: implement with annotations. #++ class Configuration # A hash of setting owners. Use double @'s to allow for # the Settings alias. #-- # TODO: find a better name. #++ @@owners = SyncHash.new # A datastructure to store Settings metadata. # # Please note the difference between :default and :value, # :default does NOT override :value. class Setting attr_accessor :owner, :name, :type, :value, :options def initialize(owner, name, options) if options.key? :value @value = options[:value] elsif options.key? :default @value = options[:default] else raise ArgumentError.new('A value is required') end @owner, @name = owner, name @options = options @type = options[:type] = options[:type] || @value.class end # Update the setting from an options hash. The update does # NOT take default values into account! def update(hash) if hash.key? :value @value = hash[:value] @type = @value.class end if hash.key? :type @type = hash[:type] end @options.update(hash) end # Text representation of this setting. def to_s @value.to_s end end # A collection of Settings. This helper enables intuitive # settings initialization like this: # # Configuration.Compiler.template_root = 'public' # instead of # Configuration.setting :compiler, :template_root, :value => 'public' class SettingCollection < Hash attr_accessor :owner # Handles setting readers and writers. def method_missing(sym, *args) if sym.to_s =~ /=$/ # Remove trailing sym = sym.to_s.gsub(/=/, '').to_sym Configuration.setting @owner, sym, :value => args.first else self[sym] end end end class << self # The configuration mode, typical modes include :debug, # :stage, :live. # # [:debug] # useful when debugging, extra debug information # is emmited, actions, templates and shaders are # reloaded, etc. The execution speed of the application # is impaired. # # [:stage] # test the application with live parameters # (typically on a staging server). # # [:live] # use the parameters for the live (production) # server. Optimized for speed. # # Tries to set the default value from the NITRO_MODE # environment variable. attr_accessor :mode def mode mode = nil if mode = ENV['CONFIGURATION_MODE'] $stderr.puts "CONFIGURATION_MODE is deprecated, use NITRO_MODE" end @mode || mode || ENV.fetch('NITRO_MODE', :debug).to_sym end # The log output destination. # # ['STDOUT' 'STDERR'] # Sets the log to stdout or stderr. # # [str] # Any other string is regarded as path attr_accessor :log def log return @log if @log log = ENV.fetch('NITRO_LOG', Logger.new('log/app.log')) if log.kind_of?(String) if log =~ /(STD(?:OUT|ERR))/i log = Object.const_get($1.upcase) elsif File.directory?(File.dirname(log)) log = Logger.new(log) else log = 'log/app.log' end end @log = log end alias kernel_load load # Attempt to load external configuration in Ruby or # YAML format. The files: # # * conf/mode.rb # * conf/mode.yaml # # are considered. def load(m = mode) ruby_conf = "conf/#{m}.rb" kernel_load(ruby_conf) if File.exist?(ruby_conf) # Try to configure from a yaml file. yml_conf = "conf/#{m}.yml" Configuration.load_yaml(yml_conf) if File.exist?(yml_conf) end # Inject the configuration parameters provided as a hash # (dictionary, ordered) to classes to be configured. # # === Warning: Pass an ordered hash (dictionary) def setup(options) options.each do |owner, ss| next unless ss ss.each do |name, s| add_setting(owner, name.to_sym, :value => s) end end end # Parse configuration parameters in yaml format. def parse_yaml(options) temp = YAML::load(options) options = Dictionary.new temp.each do |k, v| options[k.gsub(/\./, '::').to_sym] = v end setup(options) end # Load an external ruby configuration file. Constant missing # errors are not reported for extra flexibility. def load_ruby(filename) end # Load and parse an external yaml configuration file. def load_yaml(filename) parse_yaml(File.read(filename)) end # Manually add a configuration setting. The class key can # be the actual class name constant or a symbol. If the # setting is already defined it updates it. # # === Examples # # Configuration.add_setting Compiler, :verification, :value => 12, :doc => '...' # Configuration.setting :IdPart, :verify_registration_email, :value => false # s = Configuration.Compiler.verification.value def add_setting(owner, name, options) owner = owner.to_s.to_sym @@owners[owner] ||= {} if s = @@owners[owner][name] # The setting already exists, update it. s.update(options) else # The setting does not exist, create it. @@owners[owner][name] = Setting.new(owner, name, options) end end alias_method :setting, :add_setting # Return the settings for the given owner. The owner is # typically the Class that represents the system to be # configured. If no class is provided, it returns all the # registered settings. def settings(owner = nil) if owner owner = owner.to_s.to_sym @@owners[owner] else @@owners.values.inject([]) { |memo, obj| memo.concat(obj.values) } end end alias_method :all, :settings alias_method :[], :settings #-- # FIXME: this does not work as expected. #++ def method_missing(sym) if sym.to_s.capitalized? bdl = SettingCollection.new bdl.owner = sym if hash = self[sym] bdl.update(hash) end return bdl end end end end # Alias for the Configuration class (shorter). Settings = Configuration end class Module # Defines a configuration setting for the enclosing class. # # === Example # # class Compiler # setting :template_root, :default => 'src/template', :doc => 'The template root dir' # end def setting(sym, options = {}) Glue::Configuration.add_setting(self, sym, options) module_eval %{ def self.#{sym} Glue::Configuration[#{self}][:#{sym}].value end def self.#{sym}=(obj) Glue::Configuration.setting #{self}, :#{sym}, :value => obj end } end end