require 'yaml' require 'facets/core/kernel/constant' require 'facets/core/string/capitalized' require 'facets/more/synchash' require 'facets/more/dictionary' # Settings holds configuration information 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 Settings # 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: # # Settings.Compiler.template_root = 'public' # instead of # Settings.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 Settings.setting @owner, sym, :value => args.first else self[sym] end end end class << self # Inject the Settings 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 Settings parameters in yaml format. def parse(options) temp = YAML::load(options) options = Dictionary.new temp.each do |k, v| begin options[k.gsub(/\./, '::').to_sym] = v rescue Object options[k] = v end end setup(options) end # Load and parse an external yaml Settings file. def load(filename) parse(File.read(filename)) end # Manually add a Settings setting. The class key can # be the actual class name constant or a symbol. If the # setting is already defined it updates it. # # === Examples # # Settings.add_setting Compiler, :verification, :value => 12, :doc => '...' # Settings.setting :IdPart, :verify_registration_email, :value => false # s = Settings.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 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 = {}) Settings.add_setting(self, sym, options) module_eval %{ def self.#{sym} Settings[#{self}][:#{sym}].value end def self.#{sym}=(obj) Settings.setting #{self}, :#{sym}, :value => obj end } end end