# frozen_string_literal: true require_relative 'interpolation' module Puppet::Pops module Lookup # The EnvironmentContext is adapted to the current environment # class EnvironmentContext < Adaptable::Adapter class FileData attr_reader :data def initialize(path, inode, mtime, size, data) @path = path @inode = inode @mtime = mtime @size = size @data = data end def valid?(stat) stat.ino == @inode && stat.mtime == @mtime && stat.size == @size end end attr_reader :environment_name def self.create_adapter(environment) new(environment) end def initialize(environment) @environment_name = environment.name @file_data_cache = {} end # Loads the contents of the file given by _path_. The content is then yielded to the provided block in # case a block is given, and the returned value from that block is cached and returned by this method. # If no block is given, the content is stored instead. # # The cache is retained as long as the inode, mtime, and size of the file remains unchanged. # # @param path [String] path to the file to be read # @yieldparam content [String] the content that was read from the file # @yieldreturn [Object] some result based on the content # @return [Object] the content, or if a block was given, the return value of the block # def cached_file_data(path) file_data = @file_data_cache[path] stat = Puppet::FileSystem.stat(path) unless file_data && file_data.valid?(stat) Puppet.debug { "File at '#{path}' was changed, reloading" } if file_data content = Puppet::FileSystem.read(path, :encoding => 'utf-8') file_data = FileData.new(path, stat.ino, stat.mtime, stat.size, block_given? ? yield(content) : content) @file_data_cache[path] = file_data end file_data.data end end # A FunctionContext is created for each unique hierarchy entry and adapted to the Compiler (and hence shares # the compiler's life-cycle). # @api private class FunctionContext include Interpolation attr_reader :module_name, :function attr_accessor :data_hash def initialize(environment_context, module_name, function) @data_hash = nil @cache = {} @environment_context = environment_context @module_name = module_name @function = function end def cache(key, value) @cache[key] = value end def cache_all(hash) @cache.merge!(hash) nil end def cache_has_key(key) @cache.include?(key) end def cached_value(key) @cache[key] end def cached_entries(&block) if block_given? @cache.each_pair do |pair| if block.arity == 2 yield(*pair) else yield(pair) end end nil else Types::Iterable.on(@cache) end end def cached_file_data(path, &block) @environment_context.cached_file_data(path, &block) end def environment_name @environment_context.environment_name end end # The Context is created once for each call to a function. It provides a combination of the {Invocation} object needed # to provide explanation support and the {FunctionContext} object needed to provide the private cache. # The {Context} is part of the public API. It will be passed to a _data_hash_, _data_dig_, or _lookup_key_ function and its # attributes and methods can be used in a Puppet function as well as in a Ruby function. # The {Context} is maps to the Pcore type 'Puppet::LookupContext' # # @api public class Context include Types::PuppetObject extend Forwardable def self._pcore_type @type end def self.register_ptype(loader, ir) tf = Types::TypeFactory key_type = tf.optional(tf.scalar) @type = Pcore::create_object_type(loader, ir, self, 'Puppet::LookupContext', 'Any', { 'environment_name' => { Types::KEY_TYPE => Types::PStringType::NON_EMPTY, Types::KEY_KIND => Types::PObjectType::ATTRIBUTE_KIND_DERIVED }, 'module_name' => { Types::KEY_TYPE => tf.variant(Types::PStringType::NON_EMPTY, Types::PUndefType::DEFAULT) } }, { 'not_found' => tf.callable([0, 0], tf.undef), 'explain' => tf.callable([0, 0, tf.callable(0,0)], tf.undef), 'interpolate' => tf.callable(1, 1), 'cache' => tf.callable([key_type, tf.any], tf.any), 'cache_all' => tf.callable([tf.hash_kv(key_type, tf.any)], tf.undef), 'cache_has_key' => tf.callable([key_type], tf.boolean), 'cached_value' => tf.callable([key_type], tf.any), 'cached_entries' => tf.variant( tf.callable([0, 0, tf.callable(1,1)], tf.undef), tf.callable([0, 0, tf.callable(2,2)], tf.undef), tf.callable([0, 0], tf.iterable(tf.tuple([key_type, tf.any])))), 'cached_file_data' => tf.callable(tf.string, tf.optional(tf.callable([1, 1]))) } ).resolve(loader) end # Mainly for test purposes. Makes it possible to create a {Context} in Puppet code provided that a current {Invocation} exists. def self.from_asserted_args(module_name) new(FunctionContext.new(EnvironmentContext.adapt(Puppet.lookup(:environments).get(Puppet[:environment])), module_name, nil), Invocation.current) end # Public methods delegated to the {FunctionContext} def_delegators :@function_context, :cache, :cache_all, :cache_has_key, :cached_value, :cached_entries, :environment_name, :module_name, :cached_file_data def initialize(function_context, lookup_invocation) @lookup_invocation = lookup_invocation @function_context = function_context end # Will call the given block to obtain a textual explanation if explanation support is active. # def explain(&block) @lookup_invocation.report_text(&block) nil end # Resolve interpolation expressions in the given value # @param [Object] value # @return [Object] the value with all interpolation expressions resolved def interpolate(value) @function_context.interpolate(value, @lookup_invocation, true) end def not_found throw :no_such_key end # @api private def invocation @lookup_invocation end end end end