diff --git a/lib/puppet/pops/binder/lookup.rb b/lib/puppet/pops/binder/lookup.rb index 969808444..570c8da10 100644 --- a/lib/puppet/pops/binder/lookup.rb +++ b/lib/puppet/pops/binder/lookup.rb @@ -1,216 +1,216 @@ # 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, options[:name], 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 diff --git a/spec/unit/parser/functions/lookup_spec.rb b/spec/unit/parser/functions/lookup_spec.rb index e3763d083..0092898ee 100644 --- a/spec/unit/parser/functions/lookup_spec.rb +++ b/spec/unit/parser/functions/lookup_spec.rb @@ -1,147 +1,151 @@ require 'spec_helper' require 'puppet/pops' require 'stringio' require 'puppet_spec/scope' describe "lookup function" do include PuppetSpec::Scope before(:each) do Puppet[:binder] = true Puppet[:parser] = 'future' end it "must be called with at least a name to lookup" do scope = scope_with_injections_from(bound(bindings)) expect do scope.function_lookup([]) end.to raise_error(ArgumentError, /Wrong number of arguments/) + + expect do + scope.function_lookup([1,2]) + end.to raise_error(Puppet::ParseError, /given 'name' argument, has wrong type/) end it "looks up a value that exists" do scope = scope_with_injections_from(bound(bind_single("a_value", "something"))) expect(scope.function_lookup(['a_value'])).to eq('something') end it "searches for first found if given several names" do scope = scope_with_injections_from(bound(bind_single("a_value", "something"))) expect(scope.function_lookup([['b_value', 'a_value', 'c_value']])).to eq('something') end it "override wins over bound" do scope = scope_with_injections_from(bound(bind_single("a_value", "something"))) options = {:override => { 'a_value' => 'something_overridden' }} expect(scope.function_lookup(['a_value', options])).to eq('something_overridden') end it "extra option is used if nothing is found" do scope = scope_with_injections_from(bound(bind_single("another_value", "something"))) options = {:extra => { 'a_value' => 'something_extra' }} expect(scope.function_lookup(['a_value', options])).to eq('something_extra') end it "hiera is called to lookup if value is not bound" do Hiera.any_instance.stubs(:lookup).returns('from_hiera') scope = scope_with_injections_from(bound(bind_single("another_value", "something"))) expect(scope.function_lookup(['a_value'])).to eq('from_hiera') end it "returns nil when the requested value is not bound and undef is accepted" do scope = scope_with_injections_from(bound(bind_single("a_value", "something"))) expect(scope.function_lookup(['not_bound_value',{'accept_undef' => true}])).to eq(nil) end it "fails if the requested value is not bound and undef is not allowed" do scope = scope_with_injections_from(bound(bind_single("a_value", "something"))) expect do scope.function_lookup(['not_bound_value']) end.to raise_error(/did not find a value for the name 'not_bound_value'/) end it "returns the given default value when the requested value is not bound" do scope = scope_with_injections_from(bound(bind_single("a_value", "something"))) expect(scope.function_lookup(['not_bound_value','String', 'cigar'])).to eq('cigar') end it "accepts values given in a hash of options" do scope = scope_with_injections_from(bound(bind_single("a_value", "something"))) expect(scope.function_lookup(['not_bound_value',{'type' => 'String', 'default' => 'cigar'}])).to eq('cigar') end it "raises an error when the bound type is not assignable to the requested type" do scope = scope_with_injections_from(bound(bind_single("a_value", "something"))) expect do scope.function_lookup(['a_value', 'Integer']) end.to raise_error(ArgumentError, /incompatible type, expected: Integer, got: String/) end it "returns the value if the bound type is assignable to the requested type" do typed_bindings = bindings typed_bindings.bind().string().name("a_value").to("something") scope = scope_with_injections_from(bound(typed_bindings)) expect(scope.function_lookup(['a_value', 'Data'])).to eq("something") end it "yields to a given lambda and returns the result" do scope = scope_with_injections_from(bound(bind_single("a_value", "something"))) expect(scope.function_lookup(['a_value', ast_lambda(scope, '|$x|{something_else}')])).to eq('something_else') end it "fails if given lambda produces undef" do scope = scope_with_injections_from(bound(bind_single("a_value", "something"))) expect do scope.function_lookup(['a_value', ast_lambda(scope, '|$x|{undef}')]) end.to raise_error(/did not find a value for the name 'a_value'/) end it "yields name and result to a given lambda" do scope = scope_with_injections_from(bound(bind_single("a_value", "something"))) expect(scope.function_lookup(['a_value', ast_lambda(scope, '|$name, $result|{[$name, $result]}')])).to eq(['a_value', 'something']) end it "yields name and result and default to a given lambda" do scope = scope_with_injections_from(bound(bind_single("a_value", "something"))) expect(scope.function_lookup(['a_value', {'default' => 'cigar'}, ast_lambda(scope, '|$name, $result, $d|{[$name, $result, $d]}')])).to eq(['a_value', 'something', 'cigar']) end it "yields to a given lambda and returns the result when giving name and type" do scope = scope_with_injections_from(bound(bind_single("a_value", "something"))) expect(scope.function_lookup(['a_value', 'String', ast_lambda(scope, '|$x|{something_else}')])).to eq('something_else') end it "yields :undef when value is not found and using a lambda" do scope = scope_with_injections_from(bound(bind_single("a_value", "something"))) expect(scope.function_lookup(['not_bound_value', ast_lambda(scope, '|$x|{ if $x == undef {good} else {bad}}')])).to eq('good') end def scope_with_injections_from(binder) injector = Puppet::Pops::Binder::Injector.new(binder) scope = create_test_scope_for_node('testing') scope.compiler.injector = injector scope end def bindings Puppet::Pops::Binder::BindingsFactory.named_bindings("testing") end def bind_single(name, value) local_bindings = Puppet::Pops::Binder::BindingsFactory.named_bindings("testing") local_bindings.bind().name(name).to(value) local_bindings end def bound(local_bindings) layered_bindings = Puppet::Pops::Binder::BindingsFactory.layered_bindings(Puppet::Pops::Binder::BindingsFactory.named_layer('test layer', local_bindings.model)) Puppet::Pops::Binder::Binder.new(layered_bindings) end def ast_lambda(scope, puppet_source) puppet_source = "fake_func() " + puppet_source evaluator = Puppet::Pops::Parser::EvaluatingParser.new() model = evaluator.parse_string(puppet_source, __FILE__).current evaluator.closure(model.body.lambda, scope) end end