From 113d3a59eaa7547433434d155fc1e60aa7c2094c Mon Sep 17 00:00:00 2001 From: elijah Date: Thu, 11 Oct 2012 00:42:46 -0700 Subject: code cleanup. better support for nested configs and templates. --- lib/leap_cli/config/object.rb | 233 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 233 insertions(+) create mode 100644 lib/leap_cli/config/object.rb (limited to 'lib/leap_cli/config/object.rb') diff --git a/lib/leap_cli/config/object.rb b/lib/leap_cli/config/object.rb new file mode 100644 index 0000000..2ef7fe8 --- /dev/null +++ b/lib/leap_cli/config/object.rb @@ -0,0 +1,233 @@ +require 'erb' +require 'json/pure' # pure ruby implementation is required for our sorted trick to work. + +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 :node + attr_reader :manager + attr_reader :node_list + alias :global :manager + + def initialize(manager=nil, node=nil) + # keep a global pointer around to the config manager. used a lot in the eval strings and templates + # (which are evaluated in the context of Config::Object) + @manager = manager + + # 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 + + # this is only used by Config::Objects that correspond to services or tags. + @node_list = Config::ObjectList.new + end + + ## + ## FETCHING VALUES + ## + + # + # like a normal hash [], except: + # * lazily eval dynamic values when we encounter them. + # * support for nested hashes (e.g. ['a.b'] is the same as ['a']['b']) + # + 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 + + def get!(key) + key = key.to_s + if key =~ /\./ + keys = key.split('.') + value = get!(keys.first) + if value.is_a? Config::Object + value.get!(keys[1..-1]) + else + value + end + elsif self.has_key?(key) + evaluate_value(key) + elsif @node != self + @node.get!(key) + else + raise NoMethodError.new(key, "No method '#{key}' for #{self.class}") + end + end + + ## + ## COPYING + ## + + # + # 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(Config::Object.new(@manager,@node)) do |hsh, key| + value = self.get(key) + if value + hsh[key.gsub('.','_')] = value + end + hsh + end + end + + # + # a deep (recursive) merge with another Config::Object. + # + def deep_merge!(object) + object.each do |key,new_value| + old_value = self[key] + if old_value.is_a?(Hash) || new_value.is_a?(Hash) + # merge hashes + value = Config::Object.new(@manager, @node) + old_value.is_a?(Hash) ? value.deep_merge!(old_value) : (value[key] = old_value if old_value.any?) + new_value.is_a?(Hash) ? value.deep_merge!(new_value) : (value[key] = new_value if new_value.any?) + elsif old_value.is_a?(Array) || new_value.is_a?(Array) + # merge arrays + value = [] + old_value.is_a?(Array) ? value += old_value : value << old_value + new_value.is_a?(Array) ? value += new_value : value << new_value + value.compact! + elsif new_value.nil? + value = old_value + elsif old_value.nil? + value = new_value + elsif old_value.is_a?(Boolean) && new_value.is_a?(Boolean) + value = new_value + elsif old_value.class != new_value.class + raise 'Type mismatch. Cannot merge %s with %s. Key value is %s, name is %s.' % [old_value.class, new_value.class, key, name] + else + value = new_value + end + self[key] = value + end + self + end + + private + + # + # fetches the value for the key, evaluating the value as ruby if it begins with '=' + # + def evaluate_value(key) + value = fetch(key, nil) + if value.is_a? Array + value + elsif value.nil? + nil + else + if value =~ /^= (.*)$/ + begin + value = eval($1, self.send(:binding)) + self[key] = value + rescue SystemStackError => exc + puts "STACK OVERFLOW, BAILING OUT" + puts "There must be an eval loop of death (variables with circular dependencies). This is the offending string:" + puts + puts " #{$1}" + puts + raise SystemExit.new() + rescue StandardError => exc + puts "Eval error in '#{@node.name}'" + puts " string: #{$1}" + puts " error: #{exc.name}" + end + end + value + end + end + + ## + ## MACROS + ## these are methods used when eval'ing a value in the .json configuration + ## + + # + # the list of all the nodes + # + def nodes + global.nodes + end + + # + # inserts the contents of a file + # + def file(filename) + filepath = Path.find_file(@node.name, filename) + if filepath + if filepath =~ /\.erb$/ + ERB.new(File.read(filepath), nil, '%<>').result(binding) + else + File.read(filepath) + end + else + log0('no such file, "%s"' % filename) + "" + end + end + + # + # Output json from ruby objects in such a manner that all the hashes and arrays are output in alphanumeric sorted order. + # This is required so that our generated configs don't throw puppet or git for a tizzy fit. + # + # Beware: some hacky stuff ahead. + # + # This relies on the pure ruby implementation of JSON.generate (i.e. require 'json/pure') + # see https://github.com/flori/json/blob/master/lib/json/pure/generator.rb + # + # The Oj way that we are not using: Oj.dump(obj, :mode => :compat, :indent => 2) + # + def generate_json(obj) + + # modify hash and array + Hash.class_eval do + alias_method :each_without_sort, :each + def each(&block) + keys.sort {|a,b| a.to_s <=> b.to_s }.each do |key| + yield key, self[key] + end + end + end + Array.class_eval do + alias_method :each_without_sort, :each + def each(&block) + sort {|a,b| a.to_s <=> b.to_s }.each_without_sort &block + end + end + + # generate json + return_value = JSON.pretty_generate(obj) + + # restore hash and array + Hash.class_eval {alias_method :each, :each_without_sort} + Array.class_eval {alias_method :each, :each_without_sort} + + return return_value + end + + end # class + end # module +end # module \ No newline at end of file -- cgit v1.2.3