# This class is the backing implementation of the Puppet function 'lookup'. # See puppet/parser/functions/lookup.rb for documentation. # class Puppet::Pops::Binder::Lookup def self.parse_lookup_args(args) options = {} pblock = if args[-1].respond_to?(:puppet_lambda) args.pop end case args.size when 1 # name, or all options if args[ 0 ].is_a?(Hash) options = to_symbolic_hash(args[ 0 ]) else options[ :name ] = args[ 0 ] end when 2 # name and type, or name and options if args[ 1 ].is_a?(Hash) options = to_symbolic_hash(args[ 1 ]) options[:name] = args[ 0 ] # silently overwrite option with given name else options[:name] = args[ 0 ] options[:type] = args[ 1 ] end when 3 # name, type, default (no options) options[ :name ] = args[ 0 ] options[ :type ] = args[ 1 ] options[ :default ] = args[ 2 ] else raise Puppet::ParseError, "The lookup function accepts 1-3 arguments, got #{args.size}" end options[:pblock] = pblock options end def self.to_symbolic_hash(input) names = [:name, :type, :default, :accept_undef, :extra, :override] options = {} names.each {|n| options[n] = undef_as_nil(input[n.to_s] || input[n]) } options end private_class_method :to_symbolic_hash def self.type_mismatch(type_calculator, expected, got) "has wrong type, expected #{type_calculator.string(expected)}, got #{type_calculator.string(got)}" end private_class_method :type_mismatch def self.fail(msg) raise Puppet::ParseError, "Function lookup() " + msg end private_class_method :fail def self.fail_lookup(names) name_part = if names.size == 1 "the name '#{names[0]}'" else "any of the names ['" + names.join(', ') + "']" end fail("did not find a value for #{name_part}") end private_class_method :fail_lookup def self.validate_options(options, type_calculator) type_parser = Puppet::Pops::Types::TypeParser.new name_type = type_parser.parse('Variant[Array[String], String]') if is_nil_or_undef?(options[:name]) || options[:name].is_a?(Array) && options[:name].empty? fail ("requires a name, or array of names. Got nothing to lookup.") end t = type_calculator.infer(options[:name]) if ! type_calculator.assignable?(name_type, t) fail("given 'name' argument, #{type_mismatch(type_calculator, name_type, t)}") end # unless a type is already given (future case), parse the type (or default 'Data'), fails if invalid type is given unless options[:type].is_a?(Puppet::Pops::Types::PAnyType) options[:type] = type_parser.parse(options[:type] || 'Data') end # default value must comply with the given type if options[:default] t = type_calculator.infer(options[:default]) if ! type_calculator.assignable?(options[:type], t) fail("'default' value #{type_mismatch(type_calculator, options[:type], t)}") end end if options[:extra] && !options[:extra].is_a?(Hash) # do not perform inference here, it is enough to know that it is not a hash fail("'extra' value must be a Hash, got #{options[:extra].class}") end options[:extra] = {} unless options[:extra] if options[:override] && !options[:override].is_a?(Hash) # do not perform inference here, it is enough to know that it is not a hash fail("'override' value must be a Hash, got #{options[:extra].class}") end options[:override] = {} unless options[:override] end private_class_method :validate_options def self.nil_as_undef(x) x.nil? ? :undef : x end private_class_method :nil_as_undef def self.undef_as_nil(x) is_nil_or_undef?(x) ? nil : x end private_class_method :undef_as_nil def self.is_nil_or_undef?(x) x.nil? || x == :undef end private_class_method :is_nil_or_undef? # This is used as a marker - a value that cannot (at least not easily) by mistake be found in # hiera data. # class PrivateNotFoundMarker; end def self.search_for(scope, type, name, options) # search in order, override, injector, hiera, then extra if !(result = options[:override][name]).nil? result elsif !(result = scope.compiler.injector.lookup(scope, type, name)).nil? result else result = call_hiera_function(scope, name, PrivateNotFoundMarker) if !result.nil? && result != PrivateNotFoundMarker result else options[:extra][name] end end end private_class_method :search_for def self.call_hiera_function(scope, name, dflt) loader = scope.compiler.loaders.private_environment_loader func = loader.load(:function, :hiera) unless loader.nil? raise Error, 'Function not found: hiera' if func.nil? func.call(scope, name, dflt) end private_class_method :call_hiera_function # This is the method called from the puppet/parser/functions/lookup.rb # @param args [Array] array following the puppet function call conventions def self.lookup(scope, args) type_calculator = Puppet::Pops::Types::TypeCalculator.new options = parse_lookup_args(args) validate_options(options, type_calculator) names = [options[:name]].flatten type = options[:type] result_with_name = names.reduce([]) do |memo, name| break memo if !memo[1].nil? [name, search_for(scope, type, name, options)] end result = if result_with_name[1].nil? # not found, use default (which may be nil), the default is already type checked options[:default] else # injector.lookup is type-safe already do no need to type check the result result_with_name[1] end # If a block is given it is called with :undef passed as 'nil' since the lookup function # is available from 3x with --binder turned on, and the evaluation is always 4x. # TODO PUPPET4: Simply pass the value # result = if pblock = options[:pblock] result2 = case pblock.parameter_count when 1 pblock.call(undef_as_nil(result)) when 2 pblock.call(result_with_name[ 0 ], undef_as_nil(result)) else pblock.call(result_with_name[ 0 ], undef_as_nil(result), undef_as_nil(options[ :default ])) end # if the given result was returned, there is no need to type-check it again if !result2.equal?(result) t = type_calculator.infer(undef_as_nil(result2)) if !type_calculator.assignable?(type, t) fail "the value produced by the given code block #{type_mismatch(type_calculator, type, t)}" end end result2 else result end # Finally, the result if nil must be acceptable or an error is raised if is_nil_or_undef?(result) && !options[:accept_undef] fail_lookup(names) else # Since the function may be used without future parser being in effect, nil is not handled in a good # way, and should instead be turned into :undef. # TODO PUPPET4: Simply return the result # Puppet.future_parser? ? result : nil_as_undef(result) end end end