aboutsummaryrefslogtreecommitdiff
path: root/lib/leap_cli/config/object.rb
diff options
context:
space:
mode:
authorelijah <elijah@riseup.net>2012-10-11 00:42:46 -0700
committerelijah <elijah@riseup.net>2012-10-11 00:42:46 -0700
commit113d3a59eaa7547433434d155fc1e60aa7c2094c (patch)
treec468f38066f2f7669f84efb2d6b971fac0cf2346 /lib/leap_cli/config/object.rb
parent64073733a1213a5e4fe2fef7722996f62ba89c5c (diff)
downloadleap_cli-113d3a59eaa7547433434d155fc1e60aa7c2094c.tar.gz
leap_cli-113d3a59eaa7547433434d155fc1e60aa7c2094c.tar.bz2
code cleanup. better support for nested configs and templates.
Diffstat (limited to 'lib/leap_cli/config/object.rb')
-rw-r--r--lib/leap_cli/config/object.rb233
1 files changed, 233 insertions, 0 deletions
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