Skip to content

Commit

Permalink
Merge pull request #6729 from hlindberg/PUP-7675_add-conversion-to-lo…
Browse files Browse the repository at this point in the history
…okup-options

(PUP-7675) Add support for convert_to lookup option
  • Loading branch information
thallgren committed Mar 13, 2018
2 parents 1129834 + f70b792 commit 4dd20f6
Show file tree
Hide file tree
Showing 5 changed files with 114 additions and 8 deletions.
3 changes: 2 additions & 1 deletion lib/puppet/functions/lookup.rb
Expand Up @@ -207,7 +207,8 @@ def lookup_5(scope, name, options_hash, &block)
end

def do_lookup(scope, name, value_type, default_value, has_default, override, default_values_hash, merge, &block)
Puppet::Pops::Lookup.lookup(name, value_type, default_value, has_default, merge, Puppet::Pops::Lookup::Invocation.new(scope, override, default_values_hash), &block)
Puppet::Pops::Lookup.lookup(name, value_type, default_value, has_default, merge,
Puppet::Pops::Lookup::Invocation.new(scope, override, default_values_hash), &block)
end

def hash_args(options_hash)
Expand Down
61 changes: 55 additions & 6 deletions lib/puppet/pops/lookup/lookup_adapter.rb
Expand Up @@ -16,6 +16,8 @@ class LookupAdapter < DataAdapter

HASH = 'hash'.freeze
MERGE = 'merge'.freeze
CONVERT_TO = 'convert_to'.freeze
NEW = 'new'.freeze

def self.create_adapter(compiler)
new(compiler)
Expand Down Expand Up @@ -52,18 +54,65 @@ def lookup(key, lookup_invocation, merge)
catch(:no_such_key) { do_lookup(LookupKey::LOOKUP_OPTIONS, lookup_invocation, HASH) }
nil
else
lookup_options = lookup_lookup_options(key, lookup_invocation) || {}

if merge.nil?
# Used cached lookup_options
merge = lookup_merge_options(key, lookup_invocation)
# merge = lookup_merge_options(key, lookup_invocation)
merge = lookup_options[MERGE]
lookup_invocation.report_merge_source(LOOKUP_OPTIONS) unless merge.nil?
end
lookup_invocation.with(:data, key.to_s) do
catch(:no_such_key) { return do_lookup(key, lookup_invocation, merge) }
throw :no_such_key if lookup_invocation.global_only?
key.dig(lookup_invocation, lookup_default_in_module(key, lookup_invocation))
end
convert_result(key.to_s, lookup_options, lookup_invocation, lambda do
lookup_invocation.with(:data, key.to_s) do
catch(:no_such_key) { return do_lookup(key, lookup_invocation, merge) }
throw :no_such_key if lookup_invocation.global_only?
key.dig(lookup_invocation, lookup_default_in_module(key, lookup_invocation))
end
end)
end
end
end

# Performs a possible conversion of the result of calling `the_lookup` lambda
# The conversion takes place if there is a 'convert_to' key in the lookup_options
# If there is no conversion, the result of calling `the_lookup` is returned
# otherwise the successfully converted value.
# Errors are raised if the convert_to is faulty (bad type string, or if a call to
# new(T, <args>) fails.
#
# @param key [String] The key to lookup
# @param lookup_options [Hash] a hash of options
# @param lookup_invocation [Invocation] the lookup invocation
# @param the_lookup [Lambda] zero arg lambda that performs the lookup of a value
# @return [Object] the looked up value, or converted value if there was conversion
# @throw :no_such_key when the object is not found (if thrown by `the_lookup`)
#
def convert_result(key, lookup_options, lookup_invocation, the_lookup)
result = the_lookup.call
convert_to = lookup_options[CONVERT_TO]
return result if convert_to.nil?

convert_to = convert_to.is_a?(Array) ? convert_to : [convert_to]
if convert_to[0].is_a?(String)
begin
convert_to[0] = Puppet::Pops::Types::TypeParser.singleton.parse(convert_to[0])
rescue StandardError => e
raise Puppet::DataBinding::LookupError,
_("Invalid data type in lookup_options for key '%{key}' could not parse '%{source}', error: '%{msg}") %
{ key: key, source: convert_to[0], msg: e.message}
end
end
begin
result = lookup_invocation.scope.call_function(NEW, [convert_to[0], result, *convert_to[1..-1]])
# TRANSLATORS 'lookup_options', 'convert_to' and args_string variable should not be translated,
args_string = Puppet::Pops::Types::StringConverter.singleton.convert(convert_to)
lookup_invocation.report_text { _("Applying convert_to lookup_option with arguments %{args}") % { args: args_string } }
rescue StandardError => e
raise Puppet::DataBinding::LookupError,
_("The convert_to lookup_option for key '%{key}' raised error: %{msg}") %
{ key: key, msg: e.message}
end
result
end

def lookup_global(key, lookup_invocation, merge_strategy)
Expand Down
1 change: 0 additions & 1 deletion spec/unit/application/lookup_spec.rb
Expand Up @@ -89,7 +89,6 @@ def run_lookup(lookup)
end
end


context 'when given a valid configuration' do
let (:lookup) { Puppet::Application[:lookup] }

Expand Down
52 changes: 52 additions & 0 deletions spec/unit/functions/lookup_spec.rb
Expand Up @@ -2806,9 +2806,29 @@ def ruby_dig(segments, options, context)
mod_a::f:
a:
a: value mod_a::f.a.a (from module)
mod_a::to_array1: 'hello'
mod_a::to_array2: 'hello'
mod_a::to_int: 'bananas'
mod_a::to_bad_type: 'pyjamas'
mod_a::undef_value: null
lookup_options:
mod_a::e:
merge: deep
mod_a::to_array1:
merge: deep
convert_to: "Array"
mod_a::to_array2:
convert_to:
- "Array"
- true
mod_a::to_int:
convert_to: "Integer"
mod_a::to_bad_type:
convert_to: "ComicSans"
mod_a::undef_value:
convert_to:
- "Array"
- true
YAML


Expand Down Expand Up @@ -2896,6 +2916,38 @@ def ruby_dig(segments, options, context)
expect(lookup('mod_a::d')).to eql('a' => 'value mod_a::d.a (from module)')
end

context "and conversion via convert_to" do
it 'converts with a single data type value' do
expect(lookup('mod_a::to_array1')).to eql(['h', 'e', 'l', 'l', 'o'])
end

it 'converts with an array of arguments to the convert_to call' do
expect(lookup('mod_a::to_array2')).to eql(['hello'])
end

it 'converts an undef/nil value that has convert_to option' do
expect(lookup('mod_a::undef_value')).to eql([nil])
end

it 'errors if a convert_to lookup_option cannot be performed because value does not match type' do
expect{lookup('mod_a::to_int')}.to raise_error(/The convert_to lookup_option for key 'mod_a::to_int' raised error.*The string 'bananas' cannot be converted to Integer/)
end

it 'errors if a convert_to lookup_option cannot be performed because type does not exist' do
expect{lookup('mod_a::to_bad_type')}.to raise_error(/The convert_to lookup_option for key 'mod_a::to_bad_type' raised error.*Creation of new instance of type 'TypeReference\['ComicSans'\]' is not supported/)
end

it 'adds explanation that conversion took place with a type' do
explanation = explain('mod_a::to_array1')
expect(explanation).to include('Applying convert_to lookup_option with arguments [Array]')
end

it 'adds explanation that conversion took place with a type and arguments' do
explanation = explain('mod_a::to_array2')
expect(explanation).to include('Applying convert_to lookup_option with arguments [Array, true]')
end
end

it 'the default hierarchy lookup is included in the explain output' do
explanation = explain('mod_a::c')
expect(explanation).to match(/Searching default_hierarchy of module "mod_a".+Original path: "defaults.yaml"/m)
Expand Down
5 changes: 5 additions & 0 deletions spec/unit/pops/lookup/interpolation_spec.rb
Expand Up @@ -25,6 +25,11 @@ def lookup(name, lookup_invocation, merge)
found = sub_lookup(name, lookup_invocation, segments, found) unless segments.empty?
@interpolator.interpolate(found, lookup_invocation, true)
end

# ignore requests for lookup options when testing interpolation
def lookup_lookup_options(_, _)
nil
end
end

let(:interpolator) { Class.new { include Lookup::Interpolation }.new }
Expand Down

0 comments on commit 4dd20f6

Please sign in to comment.