# encoding: utf-8 require 'erb' require 'json/pure' # pure ruby implementation is required for our sorted trick to work. if $ruby_version < [1,9] $KCODE = 'UTF8' end require 'ya2yaml' # pure ruby yaml module LeapCli module Config # # This class represents the configuration for a single node, service, or tag. # Also, all the nested hashes are also of this type. # # It is called 'object' because it corresponds to an Object in JSON. # class Object < Hash attr_reader :env attr_reader :node def initialize(environment=nil, node=nil) raise ArgumentError unless environment.nil? || environment.is_a?(Config::Environment) @env = environment # an object that is a node as @node equal to self, otherwise all the # child objects point back to the top level node. @node = node || self end def manager @env.manager end # # TODO: deprecate node.global() # def global @env end def environment=(e) self.store('environment', e) end def environment self['environment'] end def duplicate(env) new_object = self.deep_dup new_object.set_environment(env, new_object) end # # export YAML # # We use pure ruby yaml exporter ya2yaml instead of SYCK or PSYCH because it # allows us greater compatibility regardless of installed ruby version and # greater control over how the yaml is exported (sorted keys, in particular). # def dump_yaml evaluate(@node) sorted_ya2yaml(:syck_compatible => true) end # # export JSON # def dump_json(options={}) evaluate(@node) if options[:format] == :compact return self.to_json else excluded = {} if options[:exclude] options[:exclude].each do |key| excluded[key] = self[key] self.delete(key) end end json_str = JSON.sorted_generate(self) if excluded.any? self.merge!(excluded) end return json_str end end def evaluate(context=@node) evaluate_everything(context) late_evaluate_everything(context) end ## ## FETCHING VALUES ## def [](key) get(key) end # Overrride some default methods in Hash that are likely to # be used as attributes. alias_method :hkey, :key def key; get('key'); end # # make hash addressable like an object (e.g. obj['name'] available as obj.name) # def method_missing(method, *args, &block) get!(method) end def get(key) begin get!(key) rescue NoMethodError nil end end # override behavior of #default() from Hash def default get!('default') end # # Like a normal Hash#[], except: # # (1) lazily eval dynamic values when we encounter them. (i.e. strings that start with "= ") # # (2) support for nested references in a single string (e.g. ['a.b'] is the same as ['a']['b']) # the dot path is always absolute, starting at the top-most object. # def get!(key) key = key.to_s if self.has_key?(key) fetch_value(key) elsif key =~ /\./ # for keys with with '.' in them, we start from the root object (@node). keys = key.split('.') value = self.get!(keys.first) if value.is_a? Config::Object value.get!(keys[1..-1].join('.')) else value end else raise NoMethodError.new(key, "No method '#{key}' for #{self.class}") end end # # works like Hash#store(key, value), but supports our nested dot notation, # just like get() does. # def set(key, value) key = key.to_s # for keys with with '.' in them, we pop off the first part # and recursively call ourselves. if key =~ /\./ keys = key.split('.') parent_value = self.get!(keys.first) if parent_value.is_a?(Config::Object) parent_value.set(keys[1..-1].join('.'), value) else parent_value.store(keys[1..-1].join('.'), value) end else self.store(key, value) end return nil end ## ## COPYING ## # # A deep (recursive) merge with another Config::Object. # # If prefer_self is set to true, the value from self will be picked when there is a conflict # that cannot be merged. # # Merging rules: # # - If a value is a hash, we recursively merge it. # - If the value is simple, like a string, the new one overwrites the value. # - If the value is an array: # - If both old and new values are arrays, the new one replaces the old. # - If one of the values is simple but the other is an array, the simple is added to the array. # def deep_merge!(object, prefer_self=false) object.each do |key,new_value| if self.has_key?('+'+key) mode = :add old_value = self.fetch '+'+key, nil self.delete('+'+key) elsif self.has_key?('-'+key) mode = :subtract old_value = self.fetch '-'+key, nil self.delete('-'+key) elsif self.has_key?('!'+key) mode = :replace old_value = self.fetch '!'+key, nil self.delete('!'+key) else mode = :normal old_value = self.fetch key, nil end # clean up boolean new_value = true if new_value == "true" new_value = false if new_value == "false" old_value = true if old_value == "true" old_value = false if old_value == "false" # force replace? if mode == :replace && prefer_self value = old_value # merge hashes elsif old_value.is_a?(Hash) || new_value.is_a?(Hash) value = Config::Object.new(@env, @node) old_value.is_a?(Hash) ? value.deep_merge!(old_value) : (value[key] = old_value if !old_value.nil?) new_value.is_a?(Hash) ? value.deep_merge!(new_value, prefer_self) : (value[key] = new_value if !new_value.nil?) # merge nil elsif new_value.nil? value = old_value elsif old_value.nil? value = new_value # merge arrays when one value is not an array elsif old_value.is_a?(Array) && !new_value.is_a?(Array) (value = (old_value.dup << new_value).compact.uniq).delete('REQUIRED') elsif new_value.is_a?(Array) && !old_value.is_a?(Array) (value = (new_value.dup << old_value).compact.uniq).delete('REQUIRED') # merge two arrays elsif old_value.is_a?(Array) && new_value.is_a?(Array) if mode == :add value = (old_value + new_value).sort.uniq elsif mode == :subtract value = new_value - old_value elsif prefer_self value = old_value else value = new_value end # catch errors elsif type_mismatch?(old_value, new_value) raise 'Type mismatch. Cannot merge %s (%s) with %s (%s). Key is "%s", name is "%s".' % [ old_value.inspect, old_value.class, new_value.inspect, new_value.class, key, self.class ] # merge simple strings & numbers else if prefer_self value = old_value else value = new_value end end # save value self[key] = value end self end def set_environment(env, node) @env = env @node = node self.each do |key, value| if value.is_a?(Config::Object) value.set_environment(env, node) end end end # # like a reverse deep merge # (self takes precedence) # def inherit_from!(object) self.deep_merge!(object, true) end # # Make a copy of ourselves, except only including the specified keys. # # Also, the result is flattened to a single hash, so a key of 'a.b' becomes 'a_b' # def pick(*keys) keys.map(&:to_s).inject(self.class.new(@manager)) do |hsh, key| value = self.get(key) if !value.nil? hsh[key.gsub('.','_')] = value end hsh end end def eval_file(filename) evaluate_ruby(filename, File.read(filename)) end protected # # walks the object tree, eval'ing all the attributes that are dynamic ruby (e.g. value starts with '= ') # def evaluate_everything(context) keys.each do |key| obj = fetch_value(key, context) if is_required_value_not_set?(obj) Util::log 0, :warning, "required property \"#{key}\" is not set in node \"#{node.name}\"." elsif obj.is_a? Config::Object obj.evaluate_everything(context) end end end # # some keys need to be evaluated 'late', after all the other keys have been evaluated. # def late_evaluate_everything(context) if @late_eval_list @late_eval_list.each do |key, value| self[key] = context.evaluate_ruby(key, value) if is_required_value_not_set?(self[key]) Util::log 0, :warning, "required property \"#{key}\" is not set in node \"#{node.name}\"." end end end values.each do |obj| if obj.is_a? Config::Object obj.late_evaluate_everything(context) end end end # # evaluates the string `value` as ruby in the context of self. # (`key` is just passed for debugging purposes) # def evaluate_ruby(key, value) self.instance_eval(value, key, 1) rescue ConfigError => exc raise exc # pass through rescue SystemStackError => exc Util::log 0, :error, "while evaluating node '#{self.name}'" Util::log 0, "offending key: #{key}", :indent => 1 Util::log 0, "offending string: #{value}", :indent => 1 Util::log 0, "STACK OVERFLOW, BAILING OUT. There must be an eval loop of death (variables with circular dependencies).", :indent => 1 raise SystemExit.new(1) rescue FileMissing => exc Util::bail! do if exc.options[:missing] Util::log :missing, exc.options[:missing].gsub('$node', self.name).gsub('$file', exc.path) else Util::log :error, "while evaluating node '#{self.name}'" Util::log "offending key: #{key}", :indent => 1 Util::log "offending string: #{value}", :indent => 1 Util::log "error message: no file '#{exc}'", :indent => 1 end raise exc if DEBUG end rescue AssertionFailed => exc Util.bail! do Util::log :failed, "assertion while evaluating node '#{self.name}'" Util::log 'assertion: %s' % exc.assertion, :indent => 1 Util::log "offending key: #{key}", :indent => 1 raise exc if DEBUG end rescue SyntaxError, StandardError => exc Util::bail! do Util::log :error, "while evaluating node '#{self.name}'" Util::log "offending key: #{key}", :indent => 1 Util::log "offending string: #{value}", :indent => 1 Util::log "error message: #{exc.inspect}", :indent => 1 raise exc if DEBUG end end private # # fetches the value for the key, evaluating the value as ruby if it begins with '=' # def fetch_value(key, context=@node) value = fetch(key, nil) if value.is_a?(String) && value =~ /^=/ # strings prefix with '=' are evaluated as ruby code. if value =~ /^=> (.*)$/ value = evaluate_later(key, $1) elsif value =~ /^= (.*)$/ value = context.evaluate_ruby(key, $1) end self[key] = value elsif value.is_a?(Proc) # the value might be a proc, set by a 'control' file self[key] = value.call end return value end def evaluate_later(key, value) @late_eval_list ||= [] @late_eval_list << [key, value] '<evaluate later>' end # # when merging, we raise an error if this method returns true for the two values. # def type_mismatch?(old_value, new_value) if old_value.is_a?(Boolean) && new_value.is_a?(Boolean) # note: FalseClass and TrueClass are different classes # so we can't do old_value.class == new_value.class return false elsif old_value.is_a?(String) && old_value =~ /^=/ # pass through macros, since we don't know what the type will eventually be. return false elsif new_value.is_a?(String) && new_value =~ /^=/ return false elsif old_value.class == new_value.class return false else return true end end # # returns true if the value has not been changed and the default is "REQUIRED" # def is_required_value_not_set?(value) if value.is_a? Array value == ["REQUIRED"] else value == "REQUIRED" end end end # class end # module end # module