require "mini_hiera/version" require 'deep_merge' module Enumerable def find_value(ifnone=nil, &block) each { |*v| o = block.call(*v) and return(o) } ifnone end end class MiniHiera CircularReferenceError = Class.new(RuntimeError) KeyNotFoundError = Class.new(RuntimeError) attr_reader :config, :options, :context_functions, :value_functions def initialize(config, options={}) @config = config @options = options @context_functions = {} @value_functions = {} end def context_function(name, &impl) @context_functions[name] = impl end def value_function(name, &impl) @value_functions[name] = impl end class Context def initialize(hiera, data, options) @hiera, @data, @options = hiera, data, options end def postprocess(value) if value.is_a?(String) value.gsub(/\%\{([^}]+)\}/) do |match| replace = $1 _, block = @hiera.value_functions.find { |name, _| replace =~ /^#{name}\((.*)\)$/ } if block argument = $1 argument = if argument =~ /^["'](.*)['"]$/ $1 else fetch(argument) end block.call(argument) else fetch(replace) end end elsif value.is_a?(Hash) Hash[value.map { |k,v| [k, postprocess(v)] }] elsif value.is_a?(Array) value.map { |v| postprocess(v) } else value end end def error_message_suffix if !!(v = Thread.current[:interpolation_stack]) && !v.empty? "whilst interpolating keys #{v.reverse.join(", ")}" end end def detect_circular_interpolation(key) Thread.current[:interpolation_stack] ||= [] raise CircularReferenceError, "Circular reference when resolving key '#{key}'" if Thread.current[:interpolation_stack].include?(key) Thread.current[:interpolation_stack] << key yield ensure Thread.current[:interpolation_stack].pop end def hiera_data @hiera_data ||= @hiera.resolve(@data).inject({}) { |a,v| a.deep_merge(v, @options.fetch(:deep_merge, {})) } end DefaultObject = Object.new def fetch(key, default=DefaultObject, &default_block) key = key.to_s raise ArgumentError, "default value and block specified" if default != DefaultObject && default_block default_block ||= lambda { default == DefaultObject ? raise(KeyNotFoundError, [" ** Unknown key '#{key}'", error_message_suffix].join(" ")) : default } default_block_wrapper = lambda { |*_| default_block.arity == 0 ? default_block.call : default_block.call(key) } # is the first key a function that yields a tree? split_key = key.split(".") _, block = @hiera.context_functions.find { |name, block| split_key.first =~ /^#{name}\(([^\)]*)\)$/ } argument = $1 if !!block split_key.shift argument = if argument =~ /^["'](.*)['"]$/ $1 else fetch(argument) end block.call(argument).fetch(split_key.join(".")) else data = hiera_data split_key.each do |k| if data.is_a?(Array) raise TypeError, "no implicit conversion of String into Integer" unless k.to_i.to_s == k data = data[k.to_i] elsif data.is_a?(Hash) data = data.fetch(k, &default_block_wrapper) else data = default_block_wrapper.call break end end detect_circular_interpolation(key) do postprocess(data) end end end def [](key) fetch(key) end def has_key?(key) fetch(key) true rescue MiniHiera::KeyNotFoundError false end end def replace(string, dictionary) string.gsub(/\%\{([^}]+)\}/) do |match| dictionary.fetch($1, match) end end CACHE = {} def self.cache(filename) if CACHE.keys.include?(filename) CACHE[filename] else CACHE[filename] = yield end end TYPES = { "yaml" => Proc.new { |filename| cache(filename) { YAML.load(File.read(filename), filename) } }, "json" => Proc.new { |filename| cache(filename) { JSON.load(File.read(filename), filename) } } } def load_path(path) TYPES.map { |s,proc| ["#{path}.#{s}", proc] }.find_value { |filename,proc| proc.call(filename, binding) if File.exist?(filename) } end def resolve(data) [data] + @options[:hierarchy].map { |path| load_path(File.join(@config, replace(path, data))) }.compact end def context(data={}, options={}) Context.new(self, data, @options.deep_merge(options)) end end