diff options
author | elijah <elijah@riseup.net> | 2016-07-01 14:49:27 -0700 |
---|---|---|
committer | elijah <elijah@riseup.net> | 2016-07-01 14:49:27 -0700 |
commit | 174dbf380fe1b0962a9e173caa6dd0a0f76a14e1 (patch) | |
tree | d2f1414652d2c0fbd1003347c2abf2be1ffff473 /lib/leap_cli/config/object.rb | |
parent | 2d277c7d2aa3ddfbe3c2c65f68d7290930934b70 (diff) | |
parent | f9284ff22da6eec685782dbc9aa4a4ded0efec29 (diff) |
Merge branch 'feature/newcli' into develop
Diffstat (limited to 'lib/leap_cli/config/object.rb')
-rw-r--r-- | lib/leap_cli/config/object.rb | 428 |
1 files changed, 0 insertions, 428 deletions
diff --git a/lib/leap_cli/config/object.rb b/lib/leap_cli/config/object.rb deleted file mode 100644 index b117c2f..0000000 --- a/lib/leap_cli/config/object.rb +++ /dev/null @@ -1,428 +0,0 @@ -# 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 - - ## - ## 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 =~ /^=/ - if value =~ /^=> (.*)$/ - value = evaluate_later(key, $1) - elsif value =~ /^= (.*)$/ - value = context.evaluate_ruby(key, $1) - end - self[key] = value - 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
\ No newline at end of file |