diff --git a/lib/puppet/functions/each.rb b/lib/puppet/functions/each.rb new file mode 100644 index 000000000..908b06a13 --- /dev/null +++ b/lib/puppet/functions/each.rb @@ -0,0 +1,90 @@ +# Applies a parameterized block to each element in a sequence of selected entries from the first +# argument and returns the first argument. +# +# This function takes two mandatory arguments: the first should be an Array or a Hash or something that is +# of enumerable type (integer, Integer range, or String), and the second +# a parameterized block as produced by the puppet syntax: +# +# $a.each |$x| { ... } +# each($a) |$x| { ... } +# +# When the first argument is an Array (or of enumerable type other than Hash), the parameterized block +# should define one or two block parameters. +# For each application of the block, the next element from the array is selected, and it is passed to +# the block if the block has one parameter. If the block has two parameters, the first is the elements +# index, and the second the value. The index starts from 0. +# +# $a.each |$index, $value| { ... } +# each($a) |$index, $value| { ... } +# +# When the first argument is a Hash, the parameterized block should define one or two parameters. +# When one parameter is defined, the iteration is performed with each entry as an array of `[key, value]`, +# and when two parameters are defined the iteration is performed with key and value. +# +# $a.each |$entry| { ..."key ${$entry[0]}, value ${$entry[1]}" } +# $a.each |$key, $value| { ..."key ${key}, value ${value}" } +# +# @example using each +# +# [1,2,3].each |$val| { ... } # 1, 2, 3 +# [5,6,7].each |$index, $val| { ... } # (0, 5), (1, 6), (2, 7) +# {a=>1, b=>2, c=>3}].each |$val| { ... } # ['a', 1], ['b', 2], ['c', 3] +# {a=>1, b=>2, c=>3}.each |$key, $val| { ... } # ('a', 1), ('b', 2), ('c', 3) +# Integer[ 10, 20 ].each |$index, $value| { ... } # (0, 10), (1, 11) ... +# "hello".each |$char| { ... } # 'h', 'e', 'l', 'l', 'o' +# 3.each |$number| { ... } # 0, 1, 2 +# +# @since 3.2 for Array and Hash +# @since 3.5 for other enumerables +# @note requires `parser = future` +# +Puppet::Functions.create_function(:each) do + dispatch :foreach_Hash do + param 'Hash[Object, Object]', :hash + required_block_param + end + + dispatch :foreach_Enumerable do + param 'Object', :enumerable + required_block_param + end + + require 'puppet/util/functions/iterative_support' + include Puppet::Util::Functions::IterativeSupport + + def foreach_Hash(hash, pblock) + enumerator = hash.each_pair + if asserted_serving_size(pblock, 'key') == 1 + hash.size.times do + pblock.call(nil, enumerator.next) + end + else + hash.size.times do + pblock.call(nil, *enumerator.next) + end + end + # produces the receiver + hash + end + + def foreach_Enumerable(enumerable, pblock) + enum = asserted_enumerable(enumerable) + index = 0 + if asserted_serving_size(pblock, 'index') == 1 + begin + loop { pblock.call(nil, enum.next) } + rescue StopIteration + end + else + begin + loop do + pblock.call(nil, index, enum.next) + index += 1 + end + rescue StopIteration + end + end + # produces the receiver + enumerable + end +end diff --git a/lib/puppet/functions/filter.rb b/lib/puppet/functions/filter.rb new file mode 100644 index 000000000..c5e9d8e53 --- /dev/null +++ b/lib/puppet/functions/filter.rb @@ -0,0 +1,92 @@ +# Applies a parameterized block to each element in a sequence of entries from the first +# argument and returns an array or hash (same type as left operand for array/hash, and array for +# other enumerable types) with the entries for which the block evaluates to `true`. +# +# This function takes two mandatory arguments: the first should be an Array, a Hash, or an +# Enumerable object (integer, Integer range, or String), +# and the second a parameterized block as produced by the puppet syntax: +# +# $a.filter |$x| { ... } +# filter($a) |$x| { ... } +# +# When the first argument is something other than a Hash, the block is called with each entry in turn. +# When the first argument is a Hash the entry is an array with `[key, value]`. +# +# @example Using filter with one parameter +# +# # selects all that end with berry +# $a = ["raspberry", "blueberry", "orange"] +# $a.filter |$x| { $x =~ /berry$/ } # rasberry, blueberry +# +# If the block defines two parameters, they will be set to `index, value` (with index starting at 0) for all +# enumerables except Hash, and to `key, value` for a Hash. +# +# @example Using filter with two parameters +# +# # selects all that end with 'berry' at an even numbered index +# $a = ["raspberry", "blueberry", "orange"] +# $a.filter |$index, $x| { $index % 2 == 0 and $x =~ /berry$/ } # raspberry +# +# # selects all that end with 'berry' and value >= 1 +# $a = {"raspberry"=>0, "blueberry"=>1, "orange"=>1} +# $a.filter |$key, $x| { $x =~ /berry$/ and $x >= 1 } # blueberry +# +# @since 3.4 for Array and Hash +# @since 3.5 for other enumerables +# @note requires `parser = future` +# +Puppet::Functions.create_function(:filter) do + dispatch :filter_Hash do + param 'Hash[Object, Object]', :hash + required_block_param + end + + dispatch :filter_Enumerable do + param 'Object', :enumerable + required_block_param + end + + require 'puppet/util/functions/iterative_support' + include Puppet::Util::Functions::IterativeSupport + + def filter_Hash(hash, pblock) + if asserted_serving_size(pblock, 'key') == 1 + result = hash.select {|x, y| pblock.call(self, [x, y]) } + else + result = hash.select {|x, y| pblock.call(self, x, y) } + end + # Ruby 1.8.7 returns Array + result = Hash[result] unless result.is_a? Hash + result + end + + def filter_Enumerable(enumerable, pblock) + result = [] + index = 0 + enum = asserted_enumerable(enumerable) + + if asserted_serving_size(pblock, 'index') == 1 + begin + loop do + it = enum.next + if pblock.call(nil, it) == true + result << it + end + end + rescue StopIteration + end + else + begin + loop do + it = enum.next + if pblock.call(nil, index, it) == true + result << it + end + index += 1 + end + rescue StopIteration + end + end + result + end +end diff --git a/lib/puppet/functions/map.rb b/lib/puppet/functions/map.rb new file mode 100644 index 000000000..1e224e141 --- /dev/null +++ b/lib/puppet/functions/map.rb @@ -0,0 +1,78 @@ +# Applies a parameterized block to each element in a sequence of entries from the first +# argument and returns an array with the result of each invocation of the parameterized block. +# +# This function takes two mandatory arguments: the first should be an Array, Hash, or of Enumerable type +# (integer, Integer range, or String), and the second a parameterized block as produced by the puppet syntax: +# +# $a.map |$x| { ... } +# map($a) |$x| { ... } +# +# When the first argument `$a` is an Array or of enumerable type, the block is called with each entry in turn. +# When the first argument is a hash the entry is an array with `[key, value]`. +# +# @example Using map with two arguments +# +# # Turns hash into array of values +# $a.map |$x|{ $x[1] } +# +# # Turns hash into array of keys +# $a.map |$x| { $x[0] } +# +# When using a block with 2 parameters, the element's index (starting from 0) for an array, and the key for a hash +# is given to the block's first parameter, and the value is given to the block's second parameter.args. +# +# @example Using map with two arguments +# +# # Turns hash into array of values +# $a.map |$key,$val|{ $val } +# +# # Turns hash into array of keys +# $a.map |$key,$val|{ $key } +# +# @since 3.4 for Array and Hash +# @since 3.5 for other enumerables, and support for blocks with 2 parameters +# @note requires `parser = future` +# +Puppet::Functions.create_function(:map) do + dispatch :map_Hash do + param 'Hash[Object, Object]', :hash + required_block_param + end + + dispatch :map_Enumerable do + param 'Object', :enumerable + required_block_param + end + + require 'puppet/util/functions/iterative_support' + include Puppet::Util::Functions::IterativeSupport + + def map_Hash(hash, pblock) + if asserted_serving_size(pblock, 'key') == 1 + hash.map {|x, y| pblock.call(nil, [x, y]) } + else + hash.map {|x, y| pblock.call(nil, x, y) } + end + end + + def map_Enumerable(enumerable, pblock) + result = [] + index = 0 + enum = asserted_enumerable(enumerable) + if asserted_serving_size(pblock, 'index') == 1 + begin + loop { result << pblock.call(nil, enum.next) } + rescue StopIteration + end + else + begin + loop do + result << pblock.call(nil, index, enum.next) + index = index +1 + end + rescue StopIteration + end + end + result + end +end diff --git a/lib/puppet/functions/reduce.rb b/lib/puppet/functions/reduce.rb new file mode 100644 index 000000000..b941a88c8 --- /dev/null +++ b/lib/puppet/functions/reduce.rb @@ -0,0 +1,102 @@ +# Applies a parameterized block to each element in a sequence of entries from the first +# argument (_the enumerable_) and returns the last result of the invocation of the parameterized block. +# +# This function takes two mandatory arguments: the first should be an Array, Hash, or something of +# enumerable type, and the last a parameterized block as produced by the puppet syntax: +# +# $a.reduce |$memo, $x| { ... } +# reduce($a) |$memo, $x| { ... } +# +# When the first argument is an Array or someting of an enumerable type, the block is called with each entry in turn. +# When the first argument is a hash each entry is converted to an array with `[key, value]` before being +# fed to the block. An optional 'start memo' value may be supplied as an argument between the array/hash +# and mandatory block. +# +# $a.reduce(start) |$memo, $x| { ... } +# reduce($a, start) |$memo, $x| { ... } +# +# If no 'start memo' is given, the first invocation of the parameterized block will be given the first and second +# elements of the enumeration, and if the enumerable has fewer than 2 elements, the first +# element is produced as the result of the reduction without invocation of the block. +# +# On each subsequent invocation, the produced value of the invoked parameterized block is given as the memo in the +# next invocation. +# +# @example Using reduce +# +# # Reduce an array +# $a = [1,2,3] +# $a.reduce |$memo, $entry| { $memo + $entry } +# #=> 6 +# +# # Reduce hash values +# $a = {a => 1, b => 2, c => 3} +# $a.reduce |$memo, $entry| { [sum, $memo[1]+$entry[1]] } +# #=> [sum, 6] +# +# # reverse a string +# "abc".reduce |$memo, $char| { "$char$memo" } +# #=>"cbe" +# +# It is possible to provide a starting 'memo' as an argument. +# +# @example Using reduce with given start 'memo' +# +# # Reduce an array +# $a = [1,2,3] +# $a.reduce(4) |$memo, $entry| { $memo + $entry } +# #=> 10 +# +# # Reduce hash values +# $a = {a => 1, b => 2, c => 3} +# $a.reduce([na, 4]) |$memo, $entry| { [sum, $memo[1]+$entry[1]] } +# #=> [sum, 10] +# +# @example Using reduce with an Integer range +# +# Integer[1,4].reduce |$memo, $x| { $memo + $x } +# #=> 10 +# +# @since 3.2 for Array and Hash +# @since 3.5 for additional enumerable types +# @note requires `parser = future`. +# +Puppet::Functions.create_function(:reduce) do + + dispatch :reduce_without_memo do + param 'Object', :enumerable + required_block_param + end + + dispatch :reduce_with_memo do + param 'Object', :enumerable + param 'Object', :memo + required_block_param + end + + require 'puppet/util/functions/iterative_support' + include Puppet::Util::Functions::IterativeSupport + + def reduce_without_memo(enumerable, pblock) + assert_serving_size(pblock) + enum = asserted_enumerable(enumerable) + enum.reduce {|memo, x| pblock.call(nil, memo, x) } + end + + def reduce_with_memo(enumerable, given_memo, pblock) + assert_serving_size(pblock) + enum = asserted_enumerable(enumerable) + enum.reduce(given_memo) {|memo, x| pblock.call(nil, memo, x) } + end + + # Asserts number of lambda parameters with more specific error message than the generic + # mis-matched arguments message that is produced by the dispatcher's type checking. + # + def assert_serving_size(pblock) + serving_size = pblock.parameter_count + unless serving_size == 2 || pblock.last_captures_rest? && serving_size <= 2 + raise ArgumentError, "reduce(): block must define 2 parameters; memo, value. Block has #{serving_size}; "+ + pblock.parameter_names.join(', ') + end + end +end diff --git a/lib/puppet/functions/slice.rb b/lib/puppet/functions/slice.rb new file mode 100644 index 000000000..e103043da --- /dev/null +++ b/lib/puppet/functions/slice.rb @@ -0,0 +1,121 @@ +# Applies a parameterized block to each _slice_ of elements in a sequence of selected entries from the first +# argument and returns the first argument, or if no block is given returns a new array with a concatenation of +# the slices. +# +# This function takes two mandatory arguments: the first, `$a`, should be an Array, Hash, or something of +# enumerable type (integer, Integer range, or String), and the second, `$n`, the number of elements to include +# in each slice. The optional third argument should be a a parameterized block as produced by the puppet syntax: +# +# $a.slice($n) |$x| { ... } +# slice($a) |$x| { ... } +# +# The parameterized block should have either one parameter (receiving an array with the slice), or the same number +# of parameters as specified by the slice size (each parameter receiving its part of the slice). +# In case there are fewer remaining elements than the slice size for the last slice it will contain the remaining +# elements. When the block has multiple parameters, excess parameters are set to :undef for an array or +# enumerable type, and to empty arrays for a Hash. +# +# $a.slice(2) |$first, $second| { ... } +# +# When the first argument is a Hash, each `key,value` entry is counted as one, e.g, a slice size of 2 will produce +# an array of two arrays with key, and value. +# +# @example Using slice with Hash +# +# $a.slice(2) |$entry| { notice "first ${$entry[0]}, second ${$entry[1]}" } +# $a.slice(2) |$first, $second| { notice "first ${first}, second ${second}" } +# +# When called without a block, the function produces a concatenated result of the slices. +# +# @example Using slice without a block +# +# slice([1,2,3,4,5,6], 2) # produces [[1,2], [3,4], [5,6]] +# slice(Integer[1,6], 2) # produces [[1,2], [3,4], [5,6]] +# slice(4,2) # produces [[0,1], [2,3]] +# slice('hello',2) # produces [[h, e], [l, l], [o]] +# +# @since 3.2 for Array and Hash +# @since 3.5 for additional enumerable types +# @note requires `parser = future`. +# +Puppet::Functions.create_function(:slice) do + dispatch :slice_Hash do + param 'Hash[Object, Object]', :hash + param 'Integer[1, default]', :slize_size + optional_block_param + end + + dispatch :slice_Enumerable do + param 'Object', :enumerable + param 'Integer[1, default]', :slize_size + optional_block_param + end + + require 'puppet/util/functions/iterative_support' + include Puppet::Util::Functions::IterativeSupport + + def slice_Hash(hash, slice_size, pblock = nil) + result = slice_Common(hash, slice_size, [], pblock) + pblock ? hash : result + end + + def slice_Enumerable(enumerable, slice_size, pblock = nil) + enum = asserted_enumerable(enumerable) + result = slice_Common(enum, slice_size, :undef, pblock) + pblock ? enumerable : result + end + + def slice_Common(o, slice_size, filler, pblock) + serving_size = asserted_slice_serving_size(pblock, slice_size) + + enumerator = o.each_slice(slice_size) + result = [] + if serving_size == 1 + begin + if pblock + loop do + pblock.call(nil, enumerator.next) + end + else + loop do + result << enumerator.next + end + end + rescue StopIteration + end + else + begin + loop do + a = enumerator.next + if a.size < serving_size + a = a.dup.fill(filler, a.length...serving_size) + end + pblock.call(nil, *a) + end + rescue StopIteration + end + end + if pblock + o + else + result + end + end + + def asserted_slice_serving_size(pblock, slice_size) + if pblock + serving_size = pblock.last_captures_rest? ? slice_size : pblock.parameter_count + else + serving_size = 1 + end + if serving_size == 0 + raise ArgumentError, "slice(): block must define at least one parameter. Block has 0." + end + unless serving_size == 1 || serving_size == slice_size + raise ArgumentError, "slice(): block must define one parameter, or " + + "the same number of parameters as the given size of the slice (#{slice_size}). Block has #{serving_size}; "+ + pblock.parameter_names.join(', ') + end + serving_size + end +end diff --git a/lib/puppet/functions/with.rb b/lib/puppet/functions/with.rb index 1b2b1e0e4..10c0824a8 100644 --- a/lib/puppet/functions/with.rb +++ b/lib/puppet/functions/with.rb @@ -1,15 +1,23 @@ # Call a lambda with the given arguments. Since the parameters of the lambda # are local to the lambda's scope, this can be used to create private sections # of logic in a class so that the variables are not visible outside of the # class. +# +# @example Using with +# +# # notices the array [1, 2, 'foo'] +# with(1, 2, 'foo') |$x, $y, $z| { notice [$x, $y, $z] } +# +# @since 3.7.0 +# Puppet::Functions.create_function(:with) do dispatch :with do param 'Object', 'arg' arg_count(0, :default) required_block_param end def with(*args) args[-1].call({}, *args[0..-2]) end end diff --git a/lib/puppet/parser/functions/assert_type.rb b/lib/puppet/parser/functions/assert_type.rb new file mode 100644 index 000000000..577697420 --- /dev/null +++ b/lib/puppet/parser/functions/assert_type.rb @@ -0,0 +1,31 @@ +Puppet::Parser::Functions::newfunction( + :assert_type, + :type => :rvalue, + :arity => -3, + :doc => "Returns the given value if it is an instance of the given type, and raises an error otherwise. +Optionally, if a block is given (accepting two parameters), it will be called instead of raising +an error. This to enable giving the user richer feedback, or to supply a default value. + +Example: assert that `$b` is a non empty `String` and assign to `$a`: + + $a = assert_type(String[1], $b) + +Example using custom error message: + + $a = assert_type(String[1], $b) |$expected, $actual| { + fail('The name cannot be empty') + } + +Example, using a warning and a default: + + $a = assert_type(String[1], $b) |$expected, $actual| { + warning('Name is empty, using default') + 'anonymous' + } + +See the documentation for 'The Puppet Type System' for more information about types. +- since Puppet 3.7 +- requires future parser/evaluator +") do |args| + function_fail(["assert_type() is only available when parser/evaluator future is in effect"]) +end diff --git a/lib/puppet/parser/functions/collect.rb b/lib/puppet/parser/functions/collect.rb deleted file mode 100644 index fea42a4df..000000000 --- a/lib/puppet/parser/functions/collect.rb +++ /dev/null @@ -1,15 +0,0 @@ -Puppet::Parser::Functions::newfunction( -:collect, -:type => :rvalue, -:arity => 2, -:doc => <<-'ENDHEREDOC') do |args| - The 'collect' function has been renamed to 'map'. Please update your manifests. - - The collect function is reserved for future use. - - Removed as of 3.4 - - requires `parser = future`. - ENDHEREDOC - - raise NotImplementedError, - "The 'collect' function has been renamed to 'map'. Please update your manifests." -end diff --git a/lib/puppet/parser/functions/each.rb b/lib/puppet/parser/functions/each.rb index 6c8b6356f..3f4437eef 100644 --- a/lib/puppet/parser/functions/each.rb +++ b/lib/puppet/parser/functions/each.rb @@ -1,110 +1,48 @@ Puppet::Parser::Functions::newfunction( -:each, -:type => :rvalue, -:arity => 2, -:doc => <<-'ENDHEREDOC') do |args| - Applies a parameterized block to each element in a sequence of selected entries from the first - argument and returns the first argument. - - This function takes two mandatory arguments: the first should be an Array or a Hash or something that is - of enumerable type (integer, Integer range, or String), and the second - a parameterized block as produced by the puppet syntax: - - $a.each |$x| { ... } - each($a) |$x| { ... } - - When the first argument is an Array (or of enumerable type other than Hash), the parameterized block - should define one or two block parameters. - For each application of the block, the next element from the array is selected, and it is passed to - the block if the block has one parameter. If the block has two parameters, the first is the elements - index, and the second the value. The index starts from 0. - - $a.each |$index, $value| { ... } - each($a) |$index, $value| { ... } - - When the first argument is a Hash, the parameterized block should define one or two parameters. - When one parameter is defined, the iteration is performed with each entry as an array of `[key, value]`, - and when two parameters are defined the iteration is performed with key and value. - - $a.each |$entry| { ..."key ${$entry[0]}, value ${$entry[1]}" } - $a.each |$key, $value| { ..."key ${key}, value ${value}" } - - *Examples* - - [1,2,3].each |$val| { ... } # 1, 2, 3 - [5,6,7].each |$index, $val| { ... } # (0, 5), (1, 6), (2, 7) - {a=>1, b=>2, c=>3}].each |$val| { ... } # ['a', 1], ['b', 2], ['c', 3] - {a=>1, b=>2, c=>3}.each |$key, $val| { ... } # ('a', 1), ('b', 2), ('c', 3) - Integer[ 10, 20 ].each |$index, $value| { ... } # (0, 10), (1, 11) ... - "hello".each |$char| { ... } # 'h', 'e', 'l', 'l', 'o' - 3.each |$number| { ... } # 0, 1, 2 - - - Since 3.2 for Array and Hash - - Since 3.5 for other enumerables - - requires `parser = future`. - ENDHEREDOC - require 'puppet/parser/ast/lambda' - - def foreach_Hash(o, scope, pblock, serving_size) - enumerator = o.each_pair - if serving_size == 1 - (o.size).times do - pblock.call(scope, enumerator.next) - end - else - (o.size).times do - pblock.call(scope, *enumerator.next) - end - end - end - - def foreach_Enumerator(enumerator, scope, pblock, serving_size) - index = 0 - if serving_size == 1 - begin - loop { pblock.call(scope, enumerator.next) } - rescue StopIteration - end - else - begin - loop do - pblock.call(scope, index, enumerator.next) - index = index +1 - end - rescue StopIteration - end - end - end - - raise ArgumentError, ("each(): wrong number of arguments (#{args.length}; expected 2, got #{args.length})") if args.length != 2 - receiver = args[0] - pblock = args[1] - raise ArgumentError, ("each(): wrong argument type (#{args[1].class}; must be a parameterized block.") unless pblock.respond_to?(:puppet_lambda) - - # if captures rest, use a serving size of 2 - serving_size = pblock.last_captures_rest? ? 2 : pblock.parameter_count - if serving_size == 0 - raise ArgumentError, "each(): block must define at least one parameter; value. Block has 0." - end - - case receiver - when Hash - if serving_size > 2 - raise ArgumentError, "each(): block must define at most two parameters; key, value. Block has #{serving_size}; "+ - pblock.parameter_names.join(', ') - end - foreach_Hash(receiver, self, pblock, serving_size) - else - if serving_size > 2 - raise ArgumentError, "each(): block must define at most two parameters; index, value. Block has #{serving_size}; "+ - pblock.parameter_names.join(', ') - end - enum = Puppet::Pops::Types::Enumeration.enumerator(receiver) - unless enum - raise ArgumentError, ("each(): wrong argument type (#{receiver.class}; must be something enumerable.") - end - foreach_Enumerator(enum, self, pblock, serving_size) - end - # each always produces the receiver - receiver + :each, + :type => :rvalue, + :arity => -3, + :doc => <<-DOC +Applies a parameterized block to each element in a sequence of selected entries from the first +argument and returns the first argument. + +This function takes two mandatory arguments: the first should be an Array or a Hash or something that is +of enumerable type (integer, Integer range, or String), and the second +a parameterized block as produced by the puppet syntax: + + $a.each |$x| { ... } + each($a) |$x| { ... } + +When the first argument is an Array (or of enumerable type other than Hash), the parameterized block +should define one or two block parameters. +For each application of the block, the next element from the array is selected, and it is passed to +the block if the block has one parameter. If the block has two parameters, the first is the elements +index, and the second the value. The index starts from 0. + + $a.each |$index, $value| { ... } + each($a) |$index, $value| { ... } + +When the first argument is a Hash, the parameterized block should define one or two parameters. +When one parameter is defined, the iteration is performed with each entry as an array of `[key, value]`, +and when two parameters are defined the iteration is performed with key and value. + + $a.each |$entry| { ..."key ${$entry[0]}, value ${$entry[1]}" } + $a.each |$key, $value| { ..."key ${key}, value ${value}" } + +Example using each: + + [1,2,3].each |$val| { ... } # 1, 2, 3 + [5,6,7].each |$index, $val| { ... } # (0, 5), (1, 6), (2, 7) + {a=>1, b=>2, c=>3}].each |$val| { ... } # ['a', 1], ['b', 2], ['c', 3] + {a=>1, b=>2, c=>3}.each |$key, $val| { ... } # ('a', 1), ('b', 2), ('c', 3) + Integer[ 10, 20 ].each |$index, $value| { ... } # (0, 10), (1, 11) ... + "hello".each |$char| { ... } # 'h', 'e', 'l', 'l', 'o' + 3.each |$number| { ... } # 0, 1, 2 + +- since 3.2 for Array and Hash +- since 3.5 for other enumerables +- note requires `parser = future` +DOC +) do |args| + function_fail(["each() is only available when parser/evaluator future is in effect"]) end diff --git a/lib/puppet/parser/functions/epp.rb b/lib/puppet/parser/functions/epp.rb index 616d61422..3e938e129 100644 --- a/lib/puppet/parser/functions/epp.rb +++ b/lib/puppet/parser/functions/epp.rb @@ -1,41 +1,41 @@ Puppet::Parser::Functions::newfunction(:epp, :type => :rvalue, :arity => -2, :doc => "Evaluates an Embedded Puppet Template (EPP) file and returns the rendered text result as a String. EPP support the following tags: * `<%= puppet expression %>` - This tag renders the value of the expression it contains. * `<% puppet expression(s) %>` - This tag will execute the expression(s) it contains, but renders nothing. * `<%# comment %>` - The tag and its content renders nothing. * `<%%` or `%%>` - Renders a literal `<%` or `%>` respectively. * `<%-` - Same as `<%` but suppresses any leading whitespace. * `-%>` - Same as `%>` but suppresses any trailing whitespace on the same line (including line break). -* `<%-( parameters )-%>` - When placed as the first tag declares the template's parameters. +* `<%- |parameters| -%>` - When placed as the first tag declares the template's parameters. File based EPP supports the following visibilities of variables in scope: * Global scope (i.e. top + node scopes) - global scope is always visible * Global + all given arguments - if the EPP template does not declare parameters, and arguments are given * Global + declared parameters - if the EPP declares parameters, given argument names must match EPP supports parameters by placing an optional parameter list as the very first element in the EPP. As an example, -`<%- ($x, $y, $z='unicorn') -%>` when placed first in the EPP text declares that the parameters `x` and `y` must be +`<%- |$x, $y, $z = 'unicorn'| -%>` when placed first in the EPP text declares that the parameters `x` and `y` must be given as template arguments when calling `inline_epp`, and that `z` if not given as a template argument defaults to `'unicorn'`. Template parameters are available as variables, e.g.arguments `$x`, `$y` and `$z` in the example. Note that `<%-` must be used or any leading whitespace will be interpreted as text Arguments are passed to the template by calling `epp` with a Hash as the last argument, where parameters are bound to values, e.g. `epp('...', {'x'=>10, 'y'=>20})`. Excess arguments may be given (i.e. undeclared parameters) only if the EPP templates does not declare any parameters at all. Template parameters shadow variables in outer scopes. File based epp does never have access to variables in the scope where the `epp` function is called from. - See function inline_epp for examples of EPP - Since 3.5 - Requires Future Parser") do |arguments| # Requires future parser unless Puppet[:parser] == "future" raise ArgumentError, "epp(): function is only available when --parser future is in effect" end Puppet::Pops::Evaluator::EppEvaluator.epp(self, arguments[0], self.compiler.environment, arguments[1]) end diff --git a/lib/puppet/parser/functions/filter.rb b/lib/puppet/parser/functions/filter.rb index 17c3f132c..365de9fcf 100644 --- a/lib/puppet/parser/functions/filter.rb +++ b/lib/puppet/parser/functions/filter.rb @@ -1,102 +1,44 @@ -require 'puppet/parser/ast/lambda' - Puppet::Parser::Functions::newfunction( -:filter, -:type => :rvalue, -:arity => 2, -:doc => <<-'ENDHEREDOC') do |args| - Applies a parameterized block to each element in a sequence of entries from the first - argument and returns an array or hash (same type as left operand for array/hash, and array for - other enumerable types) with the entries for which the block evaluates to `true`. - - This function takes two mandatory arguments: the first should be an Array, a Hash, or an - Enumerable object (integer, Integer range, or String), - and the second a parameterized block as produced by the puppet syntax: - - $a.filter |$x| { ... } - filter($a) |$x| { ... } - - When the first argument is something other than a Hash, the block is called with each entry in turn. - When the first argument is a Hash the entry is an array with `[key, value]`. - - *Examples* - - # selects all that end with berry - $a = ["raspberry", "blueberry", "orange"] - $a.filter |$x| { $x =~ /berry$/ } # rasberry, blueberry + :filter, + :arity => -3, + :doc => <<-DOC + Applies a parameterized block to each element in a sequence of entries from the first + argument and returns an array or hash (same type as left operand for array/hash, and array for + other enumerable types) with the entries for which the block evaluates to `true`. - If the block defines two parameters, they will be set to `index, value` (with index starting at 0) for all - enumerables except Hash, and to `key, value` for a Hash. + This function takes two mandatory arguments: the first should be an Array, a Hash, or an + Enumerable object (integer, Integer range, or String), + and the second a parameterized block as produced by the puppet syntax: - *Examples* + $a.filter |$x| { ... } + filter($a) |$x| { ... } - # selects all that end with 'berry' at an even numbered index - $a = ["raspberry", "blueberry", "orange"] - $a.filter |$index, $x| { $index % 2 == 0 and $x =~ /berry$/ } # raspberry + When the first argument is something other than a Hash, the block is called with each entry in turn. + When the first argument is a Hash the entry is an array with `[key, value]`. - # selects all that end with 'berry' and value >= 1 - $a = {"raspberry"=>0, "blueberry"=>1, "orange"=>1} - $a.filter |$key, $x| { $x =~ /berry$/ and $x >= 1 } # blueberry + Example Using filter with one parameter - - Since 3.4 for Array and Hash - - Since 3.5 for other enumerables - - requires `parser = future` - ENDHEREDOC + # selects all that end with berry + $a = ["raspberry", "blueberry", "orange"] + $a.filter |$x| { $x =~ /berry$/ } # rasberry, blueberry - def filter_Enumerator(enumerator, scope, pblock, serving_size) - result = [] - index = 0 - if serving_size == 1 - begin - loop { pblock.call(scope, it = enumerator.next) == true ? result << it : nil } - rescue StopIteration - end - else - begin - loop do - pblock.call(scope, index, it = enumerator.next) == true ? result << it : nil - index = index +1 - end - rescue StopIteration - end - end - result - end + If the block defines two parameters, they will be set to `index, value` (with index starting at 0) for all + enumerables except Hash, and to `key, value` for a Hash. - receiver = args[0] - pblock = args[1] +Example Using filter with two parameters - raise ArgumentError, ("filter(): wrong argument type (#{pblock.class}; must be a parameterized block.") unless pblock.respond_to?(:puppet_lambda) + # selects all that end with 'berry' at an even numbered index + $a = ["raspberry", "blueberry", "orange"] + $a.filter |$index, $x| { $index % 2 == 0 and $x =~ /berry$/ } # raspberry - # if captures rest, use a serving size of 2 - serving_size = pblock.last_captures_rest? ? 2 : pblock.parameter_count - if serving_size == 0 - raise ArgumentError, "filter(): block must define at least one parameter; value. Block has 0." - end + # selects all that end with 'berry' and value >= 1 + $a = {"raspberry"=>0, "blueberry"=>1, "orange"=>1} + $a.filter |$key, $x| { $x =~ /berry$/ and $x >= 1 } # blueberry - case receiver - when Hash - if serving_size > 2 - raise ArgumentError, "filter(): block must define at most two parameters; key, value. Block has #{serving_size}; "+ - pblock.parameter_names.join(', ') - end - if serving_size == 1 - result = receiver.select {|x, y| pblock.call(self, [x, y]) } - else - result = receiver.select {|x, y| pblock.call(self, x, y) } - end - # Ruby 1.8.7 returns Array - result = Hash[result] unless result.is_a? Hash - result - else - if serving_size > 2 - raise ArgumentError, "filter(): block must define at most two parameters; index, value. Block has #{serving_size}; "+ - pblock.parameter_names.join(', ') - end - enum = Puppet::Pops::Types::Enumeration.enumerator(receiver) - unless enum - raise ArgumentError, ("filter(): wrong argument type (#{receiver.class}; must be something enumerable.") - end - filter_Enumerator(enum, self, pblock, serving_size) - end +- since 3.4 for Array and Hash +- since 3.5 for other enumerables +- note requires `parser = future` +DOC +) do |args| + function_fail(["filter() is only available when parser/evaluator future is in effect"]) end diff --git a/lib/puppet/parser/functions/inline_epp.rb b/lib/puppet/parser/functions/inline_epp.rb index cfc1597a1..663e7b0a4 100644 --- a/lib/puppet/parser/functions/inline_epp.rb +++ b/lib/puppet/parser/functions/inline_epp.rb @@ -1,79 +1,79 @@ Puppet::Parser::Functions::newfunction(:inline_epp, :type => :rvalue, :arity => -2, :doc => "Evaluates an Embedded Puppet Template (EPP) string and returns the rendered text result as a String. EPP support the following tags: * `<%= puppet expression %>` - This tag renders the value of the expression it contains. * `<% puppet expression(s) %>` - This tag will execute the expression(s) it contains, but renders nothing. * `<%# comment %>` - The tag and its content renders nothing. * `<%%` or `%%>` - Renders a literal `<%` or `%>` respectively. * `<%-` - Same as `<%` but suppresses any leading whitespace. * `-%>` - Same as `%>` but suppresses any trailing whitespace on the same line (including line break). -* `<%-( parameters )-%>` - When placed as the first tag declares the template's parameters. +* `<%- |parameters| -%>` - When placed as the first tag declares the template's parameters. Inline EPP supports the following visibilities of variables in scope which depends on how EPP parameters are used - see further below: * Global scope (i.e. top + node scopes) - global scope is always visible * Global + Enclosing scope - if the EPP template does not declare parameters, and no arguments are given * Global + all given arguments - if the EPP template does not declare parameters, and arguments are given * Global + declared parameters - if the EPP declares parameters, given argument names must match EPP supports parameters by placing an optional parameter list as the very first element in the EPP. As an example, -`<%-( $x, $y, $z='unicorn' )-%>` when placed first in the EPP text declares that the parameters `x` and `y` must be +`<%- |$x, $y, $z='unicorn'| -%>` when placed first in the EPP text declares that the parameters `x` and `y` must be given as template arguments when calling `inline_epp`, and that `z` if not given as a template argument defaults to `'unicorn'`. Template parameters are available as variables, e.g.arguments `$x`, `$y` and `$z` in the example. Note that `<%-` must be used or any leading whitespace will be interpreted as text Arguments are passed to the template by calling `inline_epp` with a Hash as the last argument, where parameters are bound to values, e.g. `inline_epp('...', {'x'=>10, 'y'=>20})`. Excess arguments may be given (i.e. undeclared parameters) only if the EPP templates does not declare any parameters at all. Template parameters shadow variables in outer scopes. Note: An inline template is best stated using a single-quoted string, or a heredoc since a double-quoted string is subject to expression interpolation before the string is parsed as an EPP template. Here are examples (using heredoc to define the EPP text): # produces 'Hello local variable world!' $x ='local variable' inline_epptemplate(@(END:epp)) - <%-( $x )-%> + <%- |$x| -%> Hello <%= $x %> world! END # produces 'Hello given argument world!' $x ='local variable world' inline_epptemplate(@(END:epp), { x =>'given argument'}) - <%-( $x )-%> + <%- |$x| -%> Hello <%= $x %> world! END # produces 'Hello given argument world!' $x ='local variable world' inline_epptemplate(@(END:epp), { x =>'given argument'}) - <%-( $x )-%> + <%- |$x| -%> Hello <%= $x %>! END # results in error, missing value for y $x ='local variable world' inline_epptemplate(@(END:epp), { x =>'given argument'}) - <%-( $x, $y )-%> + <%- |$x, $y| -%> Hello <%= $x %>! END # Produces 'Hello given argument planet' $x ='local variable world' inline_epptemplate(@(END:epp), { x =>'given argument'}) - <%-( $x, $y=planet)-%> + <%- |$x, $y=planet| -%> Hello <%= $x %> <%= $y %>! END - Since 3.5 - Requires Future Parser") do |arguments| # Requires future parser unless Puppet[:parser] == "future" raise ArgumentError, "inline_epp(): function is only available when --parser future is in effect" end Puppet::Pops::Evaluator::EppEvaluator.inline_epp(self, arguments[0], arguments[1]) end diff --git a/lib/puppet/parser/functions/lookup.rb b/lib/puppet/parser/functions/lookup.rb index 55d56f452..e36c4c378 100644 --- a/lib/puppet/parser/functions/lookup.rb +++ b/lib/puppet/parser/functions/lookup.rb @@ -1,144 +1,144 @@ Puppet::Parser::Functions.newfunction(:lookup, :type => :rvalue, :arity => -2, :doc => <<-'ENDHEREDOC') do |args| Looks up data defined using Puppet Bindings and Hiera. The function is callable with one to three arguments and optionally with a code block to further process the result. The lookup function can be called in one of these ways: lookup(name) lookup(name, type) lookup(name, type, default) lookup(options_hash) lookup(name, options_hash) The function may optionally be called with a code block / lambda with the following signatures: lookup(...) |$result| { ... } lookup(...) |$name, $result| { ... } lookup(...) |$name, $result, $default| { ... } The longer signatures are useful when the block needs to raise an error (it can report the name), or if it needs to know if the given default value was selected. The code block receives the following three arguments: * The `$name` is the last name that was looked up (*the* name if only one name was looked up) * The `$result` is the looked up value (or the default value if not found). * The `$default` is the given default value (`undef` if not given). The block, if present, is called with the result from the lookup. The value produced by the block is also what is produced by the `lookup` function. When a block is used, it is the users responsibility to call `error` if the result does not meet additional criteria, or if an undef value is not acceptable. If a value is not found, and a default has been specified, the default value is given to the block. The content of the options hash is: * `name` - The name or array of names to lookup (first found is returned) * `type` - The type to assert (a Type or a type specification in string form) * `default` - The default value if there was no value found (must comply with the data type) * `accept_undef` - (default `false`) An `undef` result is accepted if this options is set to `true`. * `override` - a hash with map from names to values that are used instead of the underlying bindings. If the name is found here it wins. Defaults to an empty hash. * `extra` - a hash with map from names to values that are used as a last resort to obtain a value. Defaults to an empty hash. When the call is on the form `lookup(name, options_hash)`, or `lookup(name, type, options_hash)`, the given name argument wins over the `options_hash['name']`. The search order is `override` (if given), then `binder`, then `hiera` and finally `extra` (if given). The first to produce a value other than undef for a given name wins. The type specification is one of: * A type in the Puppet Type System, e.g.: * `Integer`, an integral value with optional range e.g.: * `Integer[0, default]` - 0 or positive * `Integer[default, -1]` - negative, * `Integer[1,100]` - value between 1 and 100 inclusive * `String`- any string * `Float` - floating point number (same signature as for Integer for `Integer` ranges) * `Boolean` - true of false (strict) * `Array` - an array (of Data by default), or parameterized as `Array[]`, where `` is the expected type of elements * `Hash`, - a hash (of default `Literal` keys and `Data` values), or parameterized as `Hash[]`, `Hash[, ]`, where ``, and `` are the types of the keys and values respectively (key is `Literal` by default). * `Data` - abstract type representing any `Literal`, `Array[Data]`, or `Hash[Literal, Data]` * `Pattern[, , ..., ]` - an enumeration of valid patterns (one or more) where a pattern is a regular expression string or regular expression, e.g. `Pattern['.com$', '.net$']`, `Pattern[/[a-z]+[0-9]+/]` * `Enum[, , ..., ]`, - an enumeration of exact string values (one or more) e.g. `Enum[blue, red, green]`. * `Variant[, ,...]` - matches one of the listed types (at least one must be given) e.g. `Variant[Integer[8000,8999], Integer[20000, 99999]]` to accept a value in either range * `Regexp`- a regular expression (i.e. the result is a regular expression, not a string matching a regular expression). * A string containing a type description - one of the types as shown above but in string form. If the function is called without specifying a default value, and nothing is bound to the given name an error is raised unless the option `accept_undef` is true. If a block is given it must produce an acceptable value (or call `error`). If the block does not produce an acceptable value an error is raised. Examples: When called with one argument; **the name**, it returns the bound value with the given name after having asserted it has the default datatype `Data`: lookup('the_name') When called with two arguments; **the name**, and **the expected type**, it returns the bound value with the given name after having asserted it has the given data type ('String' in the example): lookup('the_name', 'String') # 3.x lookup('the_name', String) # parser future When called with three arguments, **the name**, the **expected type**, and a **default**, it returns the bound value with the given name, or the default after having asserted the value has the given data type (`String` in the example above): lookup('the_name', 'String', 'Fred') # 3x lookup('the_name', String, 'Fred') # parser future Using a lambda to process the looked up result - asserting that it starts with an upper case letter: # only with parser future lookup('the_size', Integer[1,100]) |$result| { if $large_value_allowed and $result > 10 { error 'Values larger than 10 are not allowed'} $result } Including the name in the error # only with parser future lookup('the_size', Integer[1,100]) |$name, $result| { if $large_value_allowed and $result > 10 { error 'The bound value for '${name}' can not be larger than 10 in this configuration'} $result } When using a block, the value it produces is also asserted against the given type, and it may not be `undef` unless the option `'accept_undef'` is `true`. All options work as the corresponding (direct) argument. The `first_found` option and `accept_undef` are however only available as options. Using first_found semantics option to return the first name that has a bound value: lookup(['apache::port', 'nginx::port'], 'Integer', 80) If you want to make lookup return undef when no value was found instead of raising an error: $are_you_there = lookup('peekaboo', { accept_undef => true} ) $are_you_there = lookup('peekaboo', { accept_undef => true}) |$result| { $result } ENDHEREDOC unless Puppet[:binder] || Puppet[:parser] == 'future' - raise Puppet::ParseError, "The lookup function is only available with settings --binder true, or --parser future" + raise Puppet::ParseError, "The lookup function is only available with settings --binder true, or --parser future" end Puppet::Pops::Binder::Lookup.lookup(self, args) end diff --git a/lib/puppet/parser/functions/map.rb b/lib/puppet/parser/functions/map.rb index fb10c7177..c2ca3aae6 100644 --- a/lib/puppet/parser/functions/map.rb +++ b/lib/puppet/parser/functions/map.rb @@ -1,98 +1,43 @@ -require 'puppet/parser/ast/lambda' - Puppet::Parser::Functions::newfunction( -:map, -:type => :rvalue, -:arity => 2, -:doc => <<-'ENDHEREDOC') do |args| - Applies a parameterized block to each element in a sequence of entries from the first - argument and returns an array with the result of each invocation of the parameterized block. - - This function takes two mandatory arguments: the first should be an Array, Hash, or of Enumerable type - (integer, Integer range, or String), and the second a parameterized block as produced by the puppet syntax: - - $a.map |$x| { ... } - map($a) |$x| { ... } - - When the first argument `$a` is an Array or of enumerable type, the block is called with each entry in turn. - When the first argument is a hash the entry is an array with `[key, value]`. - - *Examples* - - # Turns hash into array of values - $a.map |$x|{ $x[1] } + :map, + :type => :rvalue, + :arity => -3, + :doc => <<-DOC +Applies a parameterized block to each element in a sequence of entries from the first +argument and returns an array with the result of each invocation of the parameterized block. - # Turns hash into array of keys - $a.map |$x| { $x[0] } +This function takes two mandatory arguments: the first should be an Array, Hash, or of Enumerable type +(integer, Integer range, or String), and the second a parameterized block as produced by the puppet syntax: - When using a block with 2 parameters, the element's index (starting from 0) for an array, and the key for a hash - is given to the block's first parameter, and the value is given to the block's second parameter.args. + $a.map |$x| { ... } + map($a) |$x| { ... } - *Examples* +When the first argument `$a` is an Array or of enumerable type, the block is called with each entry in turn. +When the first argument is a hash the entry is an array with `[key, value]`. - # Turns hash into array of values - $a.map |$key,$val|{ $val } +Example Using map with two arguments - # Turns hash into array of keys - $a.map |$key,$val|{ $key } + # Turns hash into array of values + $a.map |$x|{ $x[1] } - - Since 3.4 for Array and Hash - - Since 3.5 for other enumerables, and support for blocks with 2 parameters - - requires `parser = future` - ENDHEREDOC + # Turns hash into array of keys + $a.map |$x| { $x[0] } - def map_Enumerator(enumerator, scope, pblock, serving_size) - result = [] - index = 0 - if serving_size == 1 - begin - loop { result << pblock.call(scope, enumerator.next) } - rescue StopIteration - end - else - begin - loop do - result << pblock.call(scope, index, enumerator.next) - index = index +1 - end - rescue StopIteration - end - end - result - end +When using a block with 2 parameters, the element's index (starting from 0) for an array, and the key for a hash +is given to the block's first parameter, and the value is given to the block's second parameter.args. - receiver = args[0] - pblock = args[1] - raise ArgumentError, ("map(): wrong argument type (#{pblock.class}; must be a parameterized block.") unless pblock.respond_to?(:puppet_lambda) +Example Using map with two arguments - # if captures rest, use a serving size of 2 - serving_size = pblock.last_captures_rest? ? 2 : pblock.parameter_count + # Turns hash into array of values + $a.map |$key,$val|{ $val } - if serving_size == 0 - raise ArgumentError, "map(): block must define at least one parameter; value. Block has 0." - end - case receiver - when Hash - if serving_size > 2 - raise ArgumentError, "map(): block must define at most two parameters; key, value.args Block has #{serving_size}; "+ - pblock.parameter_names.join(', ') - end - if serving_size == 1 - result = receiver.map {|x, y| pblock.call(self, [x, y]) } - else - result = receiver.map {|x, y| pblock.call(self, x, y) } - end - else - if serving_size > 2 - raise ArgumentError, "map(): block must define at most two parameters; index, value. Block has #{serving_size}; "+ - pblock.parameter_names.join(', ') - end + # Turns hash into array of keys + $a.map |$key,$val|{ $key } - enum = Puppet::Pops::Types::Enumeration.enumerator(receiver) - unless enum - raise ArgumentError, ("map(): wrong argument type (#{receiver.class}; must be something enumerable.") - end - result = map_Enumerator(enum, self, pblock, serving_size) - end - result +- since 3.4 for Array and Hash +- since 3.5 for other enumerables, and support for blocks with 2 parameters +- note requires `parser = future` +DOC +) do |args| + function_fail(["map() is only available when parser/evaluator future is in effect"]) end diff --git a/lib/puppet/parser/functions/match.rb b/lib/puppet/parser/functions/match.rb new file mode 100644 index 000000000..33bdf3e88 --- /dev/null +++ b/lib/puppet/parser/functions/match.rb @@ -0,0 +1,28 @@ +Puppet::Parser::Functions::newfunction( + :match, + :arity => 2, + :doc => <<-DOC +Returns the match result of matching a String or Array[String] with one of: + +* Regexp +* String - transformed to a Regexp +* Pattern type +* Regexp type + +Returns An Array with the entire match at index 0, and each subsequent submatch at index 1-n. +If there was no match `undef` is returned. If the value to match is an Array, a array +with mapped match results is returned. + +Example matching: + + "abc123".match(/([a-z]+)[1-9]+/) # => ["abc"] + "abc123".match(/([a-z]+)([1-9]+)/) # => ["abc", "123"] + +See the documentation for "The Puppet Type System" for more information about types. + +- since 3.7.0 +- note requires future parser +DOC +) do |args| + function_fail(["match() is only available when parser/evaluator future is in effect"]) +end diff --git a/lib/puppet/parser/functions/reduce.rb b/lib/puppet/parser/functions/reduce.rb index 3686bdb03..4fe90a239 100644 --- a/lib/puppet/parser/functions/reduce.rb +++ b/lib/puppet/parser/functions/reduce.rb @@ -1,102 +1,71 @@ Puppet::Parser::Functions::newfunction( -:reduce, -:type => :rvalue, -:arity => -2, -:doc => <<-'ENDHEREDOC') do |args| - Applies a parameterized block to each element in a sequence of entries from the first - argument (_the enumerable_) and returns the last result of the invocation of the parameterized block. - - This function takes two mandatory arguments: the first should be an Array, Hash, or something of - enumerable type, and the last a parameterized block as produced by the puppet syntax: - - $a.reduce |$memo, $x| { ... } - reduce($a) |$memo, $x| { ... } - - When the first argument is an Array or someting of an enumerable type, the block is called with each entry in turn. - When the first argument is a hash each entry is converted to an array with `[key, value]` before being - fed to the block. An optional 'start memo' value may be supplied as an argument between the array/hash - and mandatory block. - - $a.reduce(start) |$memo, $x| { ... } - reduce($a, start) |$memo, $x| { ... } - - If no 'start memo' is given, the first invocation of the parameterized block will be given the first and second - elements of the enumeration, and if the enumerable has fewer than 2 elements, the first - element is produced as the result of the reduction without invocation of the block. - - On each subsequent invocation, the produced value of the invoked parameterized block is given as the memo in the - next invocation. - - *Examples* - - # Reduce an array - $a = [1,2,3] - $a.reduce |$memo, $entry| { $memo + $entry } - #=> 6 - - # Reduce hash values - $a = {a => 1, b => 2, c => 3} - $a.reduce |$memo, $entry| { [sum, $memo[1]+$entry[1]] } - #=> [sum, 6] - - # reverse a string - "abc".reduce |$memo, $char| { "$char$memo" } - #=>"cbe" - - It is possible to provide a starting 'memo' as an argument. - - *Examples* - - # Reduce an array - $a = [1,2,3] - $a.reduce(4) |$memo, $entry| { $memo + $entry } - #=> 10 - - # Reduce hash values - $a = {a => 1, b => 2, c => 3} - $a.reduce([na, 4]) |$memo, $entry| { [sum, $memo[1]+$entry[1]] } - #=> [sum, 10] - - *Examples* - - Integer[1,4].reduce |$memo, $x| { $memo + $x } - #=> 10 - - - Since 3.2 for Array and Hash - - Since 3.5 for additional enumerable types - - requires `parser = future`. - ENDHEREDOC - - require 'puppet/parser/ast/lambda' - - case args.length - when 2 - pblock = args[1] - when 3 - pblock = args[2] - else - raise ArgumentError, ("reduce(): wrong number of arguments (#{args.length}; expected 2 or 3, got #{args.length})") - end - unless pblock.respond_to?(:puppet_lambda) - raise ArgumentError, ("reduce(): wrong argument type (#{pblock.class}; must be a parameterized block.") - end - receiver = args[0] - enum = Puppet::Pops::Types::Enumeration.enumerator(receiver) - unless enum - raise ArgumentError, ("reduce(): wrong argument type (#{receiver.class}; must be something enumerable.") - end - - # if captures rest, use a serving size of 2 - serving_size = pblock.last_captures_rest? ? 2 : pblock.parameter_count - - if serving_size != 2 - raise ArgumentError, "reduce(): block must define 2 parameters; memo, value. Block has #{serving_size}; "+ - pblock.parameter_names.join(', ') - end - - if args.length == 3 - enum.reduce(args[1]) {|memo, x| pblock.call(self, memo, x) } - else - enum.reduce {|memo, x| pblock.call(self, memo, x) } - end + :reduce, + :type => :rvalue, + :arity => -3, + :doc => <<-DOC +Applies a parameterized block to each element in a sequence of entries from the first +argument (_the enumerable_) and returns the last result of the invocation of the parameterized block. + +This function takes two mandatory arguments: the first should be an Array, Hash, or something of +enumerable type, and the last a parameterized block as produced by the puppet syntax: + + $a.reduce |$memo, $x| { ... } + reduce($a) |$memo, $x| { ... } + +When the first argument is an Array or someting of an enumerable type, the block is called with each entry in turn. +When the first argument is a hash each entry is converted to an array with `[key, value]` before being +fed to the block. An optional 'start memo' value may be supplied as an argument between the array/hash +and mandatory block. + + $a.reduce(start) |$memo, $x| { ... } + reduce($a, start) |$memo, $x| { ... } + +If no 'start memo' is given, the first invocation of the parameterized block will be given the first and second +elements of the enumeration, and if the enumerable has fewer than 2 elements, the first +element is produced as the result of the reduction without invocation of the block. + +On each subsequent invocation, the produced value of the invoked parameterized block is given as the memo in the +next invocation. + +Example Using reduce + + # Reduce an array + $a = [1,2,3] + $a.reduce |$memo, $entry| { $memo + $entry } + #=> 6 + + # Reduce hash values + $a = {a => 1, b => 2, c => 3} + $a.reduce |$memo, $entry| { [sum, $memo[1]+$entry[1]] } + #=> [sum, 6] + + # reverse a string + "abc".reduce |$memo, $char| { "$char$memo" } + #=>"cbe" + +It is possible to provide a starting 'memo' as an argument. + +Example Using reduce with given start 'memo' + + # Reduce an array + $a = [1,2,3] + $a.reduce(4) |$memo, $entry| { $memo + $entry } + #=> 10 + + # Reduce hash values + $a = {a => 1, b => 2, c => 3} + $a.reduce([na, 4]) |$memo, $entry| { [sum, $memo[1]+$entry[1]] } + #=> [sum, 10] + +Example Using reduce with an Integer range + + Integer[1,4].reduce |$memo, $x| { $memo + $x } + #=> 10 + +- since 3.2 for Array and Hash +- since 3.5 for additional enumerable types +- note requires `parser = future`. +DOC +) do |args| + function_fail(["reduce() is only available when parser/evaluator future is in effect"]) end diff --git a/lib/puppet/parser/functions/select.rb b/lib/puppet/parser/functions/select.rb deleted file mode 100644 index 93924f9d0..000000000 --- a/lib/puppet/parser/functions/select.rb +++ /dev/null @@ -1,15 +0,0 @@ -Puppet::Parser::Functions::newfunction( -:select, -:type => :rvalue, -:arity => 2, -:doc => <<-'ENDHEREDOC') do |args| - The 'select' function has been renamed to 'filter'. Please update your manifests. - - The select function is reserved for future use. - - Removed as of 3.4 - - requires `parser = future`. - ENDHEREDOC - - raise NotImplementedError, - "The 'select' function has been renamed to 'filter'. Please update your manifests." -end diff --git a/lib/puppet/parser/functions/slice.rb b/lib/puppet/parser/functions/slice.rb index b50726d3f..b6e8c5ff7 100644 --- a/lib/puppet/parser/functions/slice.rb +++ b/lib/puppet/parser/functions/slice.rb @@ -1,120 +1,48 @@ Puppet::Parser::Functions::newfunction( -:slice, -:type => :rvalue, -:arity => -2, -:doc => <<-'ENDHEREDOC') do |args| - Applies a parameterized block to each _slice_ of elements in a sequence of selected entries from the first - argument and returns the first argument, or if no block is given returns a new array with a concatenation of - the slices. + :slice, + :type => :rvalue, + :arity => -3, + :doc => <<-DOC +Applies a parameterized block to each _slice_ of elements in a sequence of selected entries from the first +argument and returns the first argument, or if no block is given returns a new array with a concatenation of +the slices. - This function takes two mandatory arguments: the first, `$a`, should be an Array, Hash, or something of - enumerable type (integer, Integer range, or String), and the second, `$n`, the number of elements to include - in each slice. The optional third argument should be a a parameterized block as produced by the puppet syntax: +This function takes two mandatory arguments: the first, `$a`, should be an Array, Hash, or something of +enumerable type (integer, Integer range, or String), and the second, `$n`, the number of elements to include +in each slice. The optional third argument should be a a parameterized block as produced by the puppet syntax: - $a.slice($n) |$x| { ... } - slice($a) |$x| { ... } + $a.slice($n) |$x| { ... } + slice($a) |$x| { ... } - The parameterized block should have either one parameter (receiving an array with the slice), or the same number - of parameters as specified by the slice size (each parameter receiving its part of the slice). - In case there are fewer remaining elements than the slice size for the last slice it will contain the remaining - elements. When the block has multiple parameters, excess parameters are set to :undef for an array or - enumerable type, and to empty arrays for a Hash. +The parameterized block should have either one parameter (receiving an array with the slice), or the same number +of parameters as specified by the slice size (each parameter receiving its part of the slice). +In case there are fewer remaining elements than the slice size for the last slice it will contain the remaining +elements. When the block has multiple parameters, excess parameters are set to :undef for an array or +enumerable type, and to empty arrays for a Hash. - $a.slice(2) |$first, $second| { ... } + $a.slice(2) |$first, $second| { ... } - When the first argument is a Hash, each `key,value` entry is counted as one, e.g, a slice size of 2 will produce - an array of two arrays with key, and value. +When the first argument is a Hash, each `key,value` entry is counted as one, e.g, a slice size of 2 will produce +an array of two arrays with key, and value. - $a.slice(2) |$entry| { notice "first ${$entry[0]}, second ${$entry[1]}" } - $a.slice(2) |$first, $second| { notice "first ${first}, second ${second}" } +Example Using slice with Hash - When called without a block, the function produces a concatenated result of the slices. + $a.slice(2) |$entry| { notice "first ${$entry[0]}, second ${$entry[1]}" } + $a.slice(2) |$first, $second| { notice "first ${first}, second ${second}" } - slice([1,2,3,4,5,6], 2) # produces [[1,2], [3,4], [5,6]] - slice(Integer[1,6], 2) # produces [[1,2], [3,4], [5,6]] - slice(4,2) # produces [[0,1], [2,3]] - slice('hello',2) # produces [[h, e], [l, l], [o]] +When called without a block, the function produces a concatenated result of the slices. - - Since 3.2 for Array and Hash - - Since 3.5 for additional enumerable types - - requires `parser = future`. - ENDHEREDOC - require 'puppet/parser/ast/lambda' - require 'puppet/parser/scope' +Example Using slice without a block - def each_Common(o, slice_size, filler, scope, pblock) - if pblock - serving_size = pblock.last_captures_rest? ? slice_size : pblock.parameter_count - else - serving_size = 1 - end - if serving_size == 0 - raise ArgumentError, "slice(): block must define at least one parameter. Block has 0." - end - unless serving_size == 1 || serving_size == slice_size - raise ArgumentError, "slice(): block must define one parameter, or " + - "the same number of parameters as the given size of the slice (#{slice_size}). Block has #{serving_size}; "+ - pblock.parameter_names.join(', ') - end - enumerator = o.each_slice(slice_size) - result = [] - if serving_size == 1 - begin - if pblock - loop do - pblock.call(scope, enumerator.next) - end - else - loop do - result << enumerator.next - end - end - rescue StopIteration - end - else - begin - loop do - a = enumerator.next - if a.size < serving_size - a = a.dup.fill(filler, a.length...serving_size) - end - pblock.call(scope, *a) - end - rescue StopIteration - end - end - if pblock - o - else - result - end - end + slice([1,2,3,4,5,6], 2) # produces [[1,2], [3,4], [5,6]] + slice(Integer[1,6], 2) # produces [[1,2], [3,4], [5,6]] + slice(4,2) # produces [[0,1], [2,3]] + slice('hello',2) # produces [[h, e], [l, l], [o]] - raise ArgumentError, ("slice(): wrong number of arguments (#{args.length}; must be 2 or 3)") unless args.length == 2 || args.length == 3 - if args.length >= 2 - begin - slice_size = Puppet::Parser::Scope.number?(args[1]) - rescue - raise ArgumentError, ("slice(): wrong argument type (#{args[1]}; must be number.") - end - end - raise ArgumentError, ("slice(): wrong argument type (#{args[1]}; must be number.") unless slice_size - raise ArgumentError, ("slice(): wrong argument value: #{slice_size}; is not a positive integer number > 0") unless slice_size.is_a?(Fixnum) && slice_size > 0 - receiver = args[0] - - # the block is optional, ok if nil, function then produces an array - pblock = args[2] - raise ArgumentError, ("slice(): wrong argument type (#{args[2].class}; must be a parameterized block.") unless pblock.respond_to?(:puppet_lambda) || args.length == 2 - - case receiver - when Hash - each_Common(receiver, slice_size, [], self, pblock) - else - enum = Puppet::Pops::Types::Enumeration.enumerator(receiver) - if enum.nil? - raise ArgumentError, ("slice(): given type '#{tc.string(receiver)}' is not enumerable") - end - result = each_Common(enum, slice_size, :undef, self, pblock) - pblock ? receiver : result - end +- since 3.2 for Array and Hash +- since 3.5 for additional enumerable types +- note requires `parser = future`. +DOC +) do |args| + function_fail(["slice() is only available when parser/evaluator future is in effect"]) end diff --git a/lib/puppet/parser/functions/with.rb b/lib/puppet/parser/functions/with.rb new file mode 100644 index 000000000..07beff6fd --- /dev/null +++ b/lib/puppet/parser/functions/with.rb @@ -0,0 +1,21 @@ +Puppet::Parser::Functions::newfunction( + :with, + :type => :rvalue, + :arity => -1, + :doc => <<-DOC +Call a lambda code block with the given arguments. Since the parameters of the lambda +are local to the lambda's scope, this can be used to create private sections +of logic in a class so that the variables are not visible outside of the +class. + +Example: + + # notices the array [1, 2, 'foo'] + with(1, 2, 'foo') |$x, $y, $z| { notice [$x, $y, $z] } + +- since 3.7.0 +- note requires future parser +DOC +) do |args| + function_fail(["with() is only available when parser/evaluator future is in effect"]) +end diff --git a/lib/puppet/pops/evaluator/runtime3_support.rb b/lib/puppet/pops/evaluator/runtime3_support.rb index 740fe95af..a2bcc22ca 100644 --- a/lib/puppet/pops/evaluator/runtime3_support.rb +++ b/lib/puppet/pops/evaluator/runtime3_support.rb @@ -1,546 +1,543 @@ # A module with bindings between the new evaluator and the 3x runtime. # The intention is to separate all calls into scope, compiler, resource, etc. in this module # to make it easier to later refactor the evaluator for better implementations of the 3x classes. # # @api private module Puppet::Pops::Evaluator::Runtime3Support # Fails the evaluation of _semantic_ with a given issue. # # @param issue [Puppet::Pops::Issue] the issue to report # @param semantic [Puppet::Pops::ModelPopsObject] the object for which evaluation failed in some way. Used to determine origin. # @param options [Hash] hash of optional named data elements for the given issue # @return [!] this method does not return # @raise [Puppet::ParseError] an evaluation error initialized from the arguments (TODO: Change to EvaluationError?) # def fail(issue, semantic, options={}, except=nil) optionally_fail(issue, semantic, options, except) # an error should have been raised since fail always fails raise ArgumentError, "Internal Error: Configuration of runtime error handling wrong: should have raised exception" end # Optionally (based on severity) Fails the evaluation of _semantic_ with a given issue # If the given issue is configured to be of severity < :error it is only reported, and the function returns. # # @param issue [Puppet::Pops::Issue] the issue to report # @param semantic [Puppet::Pops::ModelPopsObject] the object for which evaluation failed in some way. Used to determine origin. # @param options [Hash] hash of optional named data elements for the given issue # @return [!] this method does not return # @raise [Puppet::ParseError] an evaluation error initialized from the arguments (TODO: Change to EvaluationError?) # def optionally_fail(issue, semantic, options={}, except=nil) if except.nil? # Want a stacktrace, and it must be passed as an exception begin raise EvaluationError.new() rescue EvaluationError => e except = e end end diagnostic_producer.accept(issue, semantic, options, except) end # Binds the given variable name to the given value in the given scope. # The reference object `o` is intended to be used for origin information - the 3x scope implementation # only makes use of location when there is an error. This is now handled by other mechanisms; first a check # is made if a variable exists and an error is raised if attempting to change an immutable value. Errors # in name, numeric variable assignment etc. have also been validated prior to this call. In the event the # scope.setvar still raises an error, the general exception handling for evaluation of the assignment # expression knows about its location. Because of this, there is no need to extract the location for each # setting (extraction is somewhat expensive since 3x requires line instead of offset). # def set_variable(name, value, o, scope) # Scope also checks this but requires that location information are passed as options. # Those are expensive to calculate and a test is instead made here to enable failing with better information. # The error is not specific enough to allow catching it - need to check the actual message text. # TODO: Improve the messy implementation in Scope. # if scope.bound?(name) if Puppet::Parser::Scope::RESERVED_VARIABLE_NAMES.include?(name) fail(Puppet::Pops::Issues::ILLEGAL_RESERVED_ASSIGNMENT, o, {:name => name} ) else fail(Puppet::Pops::Issues::ILLEGAL_REASSIGNMENT, o, {:name => name} ) end end scope.setvar(name, value) end # Returns the value of the variable (nil is returned if variable has no value, or if variable does not exist) # def get_variable_value(name, o, scope) # Puppet 3x stores all variables as strings (then converts them back to numeric with a regexp... to see if it is a match variable) # Not ideal, scope should support numeric lookup directly instead. # TODO: consider fixing scope catch(:undefined_variable) { return scope.lookupvar(name.to_s) } # It is always ok to reference numeric variables even if they are not assigned. They are always undef # if not set by a match expression. # unless name =~ Puppet::Pops::Patterns::NUMERIC_VAR_NAME fail(Puppet::Pops::Issues::UNKNOWN_VARIABLE, o, {:name => name}) end end # Returns true if the variable of the given name is set in the given most nested scope. True is returned even if # variable is bound to nil. # def variable_bound?(name, scope) scope.bound?(name.to_s) end # Returns true if the variable is bound to a value or nil, in the scope or it's parent scopes. # def variable_exists?(name, scope) scope.exist?(name.to_s) end def set_match_data(match_data, o, scope) # See set_variable for rationale for not passing file and line to ephemeral_from. # NOTE: The 3x scope adds one ephemeral(match) to its internal stack per match that succeeds ! It never # clears anything. Thus a context that performs many matches will get very deep (there simply is no way to # clear the match variables without rolling back the ephemeral stack.) # This implementation does not attempt to fix this, it behaves the same bad way. unless match_data.nil? scope.ephemeral_from(match_data) end end # Creates a local scope with vairalbes set from a hash of variable name to value # def create_local_scope_from(hash, scope) # two dummy values are needed since the scope tries to give an error message (can not happen in this # case - it is just wrong, the error should be reported by the caller who knows in more detail where it # is in the source. # raise ArgumentError, "Internal error - attempt to create a local scope without a hash" unless hash.is_a?(Hash) scope.ephemeral_from(hash) end # Creates a nested match scope def create_match_scope_from(scope) # Create a transparent match scope (for future matches) scope.new_match_scope(nil) end def get_scope_nesting_level(scope) scope.ephemeral_level end def set_scope_nesting_level(scope, level) # Yup, 3x uses this method to reset the level, it also supports passing :all to destroy all # ephemeral/local scopes - which is a sure way to create havoc. # scope.unset_ephemeral_var(level) end # Adds a relationship between the given `source` and `target` of the given `relationship_type` # @param source [Puppet:Pops::Types::PCatalogEntryType] the source end of the relationship (from) # @param target [Puppet:Pops::Types::PCatalogEntryType] the target end of the relationship (to) # @param relationship_type [:relationship, :subscription] the type of the relationship # def add_relationship(source, target, relationship_type, scope) # The 3x way is to record a Puppet::Parser::Relationship that is evaluated at the end of the compilation. # This means it is not possible to detect any duplicates at this point (and signal where an attempt is made to # add a duplicate. There is also no location information to signal the original place in the logic. The user will have # to go fish. # The 3.x implementation is based on Strings :-o, so the source and target must be transformed. The resolution is # done by Catalog#resource(type, title). To do that, it creates a Puppet::Resource since it is responsible for # translating the name/type/title and create index-keys used by the catalog. The Puppet::Resource has bizarre parsing of # the type and title (scan for [] that is interpreted as type/title (but it gets it wrong). # Moreover if the type is "" or "component", the type is Class, and if the type is :main, it is :main, all other cases # undergo capitalization of name-segments (foo::bar becomes Foo::Bar). (This was earlier done in the reverse by the parser). # Further, the title undergoes the same munging !!! # # That bug infested nest of messy logic needs serious Exorcism! # # Unfortunately it is not easy to simply call more intelligent methods at a lower level as the compiler evaluates the recorded # Relationship object at a much later point, and it is responsible for invoking all the messy logic. # # TODO: Revisit the below logic when there is a sane implementation of the catalog, compiler and resource. For now # concentrate on transforming the type references to what is expected by the wacky logic. # # HOWEVER, the Compiler only records the Relationships, and the only method it calls is @relationships.each{|x| x.evaluate(catalog) } # Which means a smarter Relationship class could do this right. Instead of obtaining the resource from the catalog using # the borked resource(type, title) which creates a resource for the purpose of looking it up, it needs to instead # scan the catalog's resources # # GAAAH, it is even worse! # It starts in the parser, which parses "File['foo']" into an AST::ResourceReference with type = File, and title = foo # This AST is evaluated by looking up the type/title in the scope - causing it to be loaded if it exists, and if not, the given # type name/title is used. It does not search for resource instances, only classes and types. It returns symbolic information # [type, [title, title]]. From this, instances of Puppet::Resource are created and returned. These only have type/title information # filled out. One or an array of resources are returned. # This set of evaluated (empty reference) Resource instances are then passed to the relationship operator. It creates a # Puppet::Parser::Relationship giving it a source and a target that are (empty reference) Resource instances. These are then remembered # until the relationship is evaluated by the compiler (at the end). When evaluation takes place, the (empty reference) Resource instances # are converted to String (!?! WTF) on the simple format "#{type}[#{title}]", and the catalog is told to find a resource, by giving # it this string. If it cannot find the resource it fails, else the before/notify parameter is appended with the target. # The search for the resource begin with (you guessed it) again creating an (empty reference) resource from type and title (WTF?!?!). # The catalog now uses the reference resource to compute a key [r.type, r.title.to_s] and also gets a uniqueness key from the # resource (This is only a reference type created from title and type). If it cannot find it with the first key, it uses the # uniqueness key to lookup. # # This is probably done to allow a resource type to munge/translate the title in some way (but it is quite unclear from the long # and convoluted path of evaluation. # In order to do this in a way that is similar to 3.x two resources are created to be used as keys. # # And if that is not enough, a source/target may be a Collector (a baked query that will be evaluated by the # compiler - it is simply passed through here for processing by the compiler at the right time). # if source.is_a?(Puppet::Parser::Collector) # use verbatim - behavior defined by 3x source_resource = source else # transform into the wonderful String representation in 3x type, title = catalog_type_to_split_type_title(source) source_resource = Puppet::Resource.new(type, title) end if target.is_a?(Puppet::Parser::Collector) # use verbatim - behavior defined by 3x target_resource = target else # transform into the wonderful String representation in 3x type, title = catalog_type_to_split_type_title(target) target_resource = Puppet::Resource.new(type, title) end # Add the relationship to the compiler for later evaluation. scope.compiler.add_relationship(Puppet::Parser::Relationship.new(source_resource, target_resource, relationship_type)) end # Coerce value `v` to numeric or fails. # The given value `v` is coerced to Numeric, and if that fails the operation # calls {#fail}. # @param v [Object] the value to convert # @param o [Object] originating instruction # @param scope [Object] the (runtime specific) scope where evaluation of o takes place # @return [Numeric] value `v` converted to Numeric. # def coerce_numeric(v, o, scope) unless n = Puppet::Pops::Utils.to_n(v) fail(Puppet::Pops::Issues::NOT_NUMERIC, o, {:value => v}) end n end - # Horrible cheat while waiting for iterative functions to be 4x - FUNCTIONS_4x = { 'map' => true, 'each'=>true, 'filter' => true, 'reduce' => true, 'slice' => true } def call_function(name, args, o, scope) - # Call via 4x API if it is available, and the function exists - # - if loaders = Puppet.lookup(:loaders) {nil} - # find the loader that loaded the code, or use the private_environment_loader (sees env + all modules) - adapter = Puppet::Pops::Utils.find_adapter(o, Puppet::Pops::Adapters::LoaderAdapter) - loader = adapter.nil? ? loaders.private_environment_loader : adapter.loader - if loader && func = loader.load(:function, name) - return func.call(scope, *args) - end + # Call via 4x API if the function exists there + loaders = scope.compiler.loaders + # find the loader that loaded the code, or use the private_environment_loader (sees env + all modules) + adapter = Puppet::Pops::Utils.find_adapter(o, Puppet::Pops::Adapters::LoaderAdapter) + loader = adapter.nil? ? loaders.private_environment_loader : adapter.loader + if loader && func = loader.load(:function, name) + return func.call(scope, *args) end + # Call via 3x API if function exists there fail(Puppet::Pops::Issues::UNKNOWN_FUNCTION, o, {:name => name}) unless Puppet::Parser::Functions.function(name) # TODO: if Puppet[:biff] == true, then 3x functions should be called via loaders above # Arguments must be mapped since functions are unaware of the new and magical creatures in 4x. # Do not map the iterative functions, they are capable of dealing with 4x API, and they do # call out to lambdas, and thus, given arguments needs to be preserved (instead of transforming to # '' when undefined). TODO: The iterative functions should be refactored to use the new function API # directly, when this has been done, this special filtering out can be removed # NOTE: Passing an empty string last converts :undef to empty string - mapped_args = FUNCTIONS_4x[name] ? args : args.map {|a| convert(a, scope, '') } + mapped_args = args.map {|a| convert(a, scope, '') } result = scope.send("function_#{name}", mapped_args) # Prevent non r-value functions from leaking their result (they are not written to care about this) Puppet::Parser::Functions.rvalue?(name) ? result : nil end # The o is used for source reference def create_resource_parameter(o, scope, name, value, operator) file, line = extract_file_line(o) Puppet::Parser::Resource::Param.new( :name => name, :value => convert(value, scope, :undef), # converted to 3x since 4x supports additional objects / types :source => scope.source, :line => line, :file => file, :add => operator == :'+>' ) end def create_resources(o, scope, virtual, exported, type_name, resource_titles, evaluated_parameters) # TODO: Unknown resource causes creation of Resource to fail with ArgumentError, should give # a proper Issue. Now the result is "Error while evaluating a Resource Statement" with the message # from the raised exception. (It may be good enough). # resolve in scope. fully_qualified_type, resource_titles = scope.resolve_type_and_titles(type_name, resource_titles) # Not 100% accurate as this is the resource expression location and each title is processed separately # The titles are however the result of evaluation and they have no location at this point (an array # of positions for the source expressions are required for this to work). # TODO: Revisit and possible improve the accuracy. # file, line = extract_file_line(o) # Build a resource for each title resource_titles.map do |resource_title| resource = Puppet::Parser::Resource.new( fully_qualified_type, resource_title, :parameters => evaluated_parameters, :file => file, :line => line, :exported => exported, :virtual => virtual, # WTF is this? Which source is this? The file? The name of the context ? :source => scope.source, :scope => scope, :strict => true ) if resource.resource_type.is_a? Puppet::Resource::Type resource.resource_type.instantiate_resource(scope, resource) end scope.compiler.add_resource(scope, resource) scope.compiler.evaluate_classes([resource_title], scope, false, true) if fully_qualified_type == 'class' # Turn the resource into a PType (a reference to a resource type) # weed out nil's resource_to_ptype(resource) end end # Defines default parameters for a type with the given name. # def create_resource_defaults(o, scope, type_name, evaluated_parameters) # Note that name must be capitalized in this 3x call # The 3x impl creates a Resource instance with a bogus title and then asks the created resource # for the type of the name. # Note, locations are available per parameter. # scope.define_settings(capitalize_qualified_name(type_name), evaluated_parameters) end # Capitalizes each segment of a qualified name # def capitalize_qualified_name(name) name.split(/::/).map(&:capitalize).join('::') end # Creates resource overrides for all resource type objects in evaluated_resources. The same set of # evaluated parameters are applied to all. # def create_resource_overrides(o, scope, evaluated_resources, evaluated_parameters) # Not 100% accurate as this is the resource expression location and each title is processed separately # The titles are however the result of evaluation and they have no location at this point (an array # of positions for the source expressions are required for this to work. # TODO: Revisit and possible improve the accuracy. # file, line = extract_file_line(o) evaluated_resources.each do |r| resource = Puppet::Parser::Resource.new( r.type_name, r.title, :parameters => evaluated_parameters, :file => file, :line => line, # WTF is this? Which source is this? The file? The name of the context ? :source => scope.source, :scope => scope ) scope.compiler.add_override(resource) end end # Finds a resource given a type and a title. # def find_resource(scope, type_name, title) scope.compiler.findresource(type_name, title) end # Returns the value of a resource's parameter by first looking up the parameter in the resource # and then in the defaults for the resource. Since the resource exists (it must in order to look up its # parameters, any overrides have already been applied). Defaults are not applied to a resource until it # has been finished (which typically has not taken place when this is evaluated; hence the dual lookup). # def get_resource_parameter_value(scope, resource, parameter_name) # This gets the parameter value, or nil (for both valid parameters and parameters that do not exist). val = resource[parameter_name] if val.nil? && defaults = scope.lookupdefaults(resource.type) # NOTE: 3x resource keeps defaults as hash using symbol for name as key to Parameter which (again) holds # name and value. # NOTE: meta parameters that are unset ends up here, and there are no defaults for those encoded # in the defaults, they may receive hardcoded defaults later (e.g. 'tag'). param = defaults[parameter_name.to_sym] # Some parameters (meta parameters like 'tag') does not return a param from which the value can be obtained # at all times. Instead, they return a nil param until a value has been set. val = param.nil? ? nil : param.value end val end # Returns true, if the given name is the name of a resource parameter. # def is_parameter_of_resource?(scope, resource, name) resource.valid_parameter?(name) end def resource_to_ptype(resource) nil if resource.nil? type_calculator.infer(resource) end # This is the same type of "truth" as used in the current Puppet DSL. # def is_true? o # Is the value true? This allows us to control the definition of truth # in one place. case o when :undef false else !!o end end # Utility method for TrueClass || FalseClass # @param x [Object] the object to test if it is instance of TrueClass or FalseClass def is_boolean? x x.is_a?(TrueClass) || x.is_a?(FalseClass) end def initialize @@convert_visitor ||= Puppet::Pops::Visitor.new(self, "convert", 2, 2) end # Converts 4x supported values to 3x values. This is required because # resources and other objects do not know about the new type system, and does not support # regular expressions. Unfortunately this has to be done for array and hash as well. # A complication is that catalog types needs to be resolved against the scope. # def convert(o, scope, undef_value) @@convert_visitor.visit_this_2(self, o, scope, undef_value) end def convert_NilClass(o, scope, undef_value) undef_value end def convert_Object(o, scope, undef_value) o end def convert_Array(o, scope, undef_value) o.map {|x| convert(x, scope, undef_value) } end def convert_Hash(o, scope, undef_value) result = {} o.each {|k,v| result[convert(k, scope, undef_value)] = convert(v, scope, undef_value) } result end def convert_Regexp(o, scope, undef_value) # Puppet 3x cannot handle parameter values that are reqular expressions. Turn into regexp string in # source form o.inspect end def convert_Symbol(o, scope, undef_value) case o when :undef undef_value # 3x wants :undef as empty string in function else o # :default, and all others are verbatim since they are new in future evaluator end end def convert_PAbstractType(o, scope, undef_value) o end def convert_PCatalogEntryType(o, scope, undef_value) # Since 4x does not support dynamic scoping, all names are absolute and can be # used as is (with some check/transformation/mangling between absolute/relative form # due to Puppet::Resource's idiosyncratic behavior where some references must be # absolute and others cannot be. # Thus there is no need to call scope.resolve_type_and_titles to do dynamic lookup. Puppet::Resource.new(*catalog_type_to_split_type_title(o)) end private # Produces an array with [type, title] from a PCatalogEntryType # This method is used to produce the arguments for creation of reference resource instances # (used when 3x is operating on a resource). # Ensures that resources are *not* absolute. # def catalog_type_to_split_type_title(catalog_type) split_type = catalog_type.is_a?(Puppet::Pops::Types::PType) ? catalog_type.type : catalog_type case split_type when Puppet::Pops::Types::PHostClassType class_name = split_type.class_name ['class', class_name.nil? ? nil : class_name.sub(/^::/, '')] when Puppet::Pops::Types::PResourceType type_name = split_type.type_name title = split_type.title if type_name =~ /^(::)?[Cc]lass/ ['class', title.nil? ? nil : title.sub(/^::/, '')] else # Ensure that title is '' if nil # Resources with absolute name always results in error because tagging does not support leading :: [type_name.nil? ? nil : type_name.sub(/^::/, ''), title.nil? ? '' : title] end else raise ArgumentError, "Cannot split the type #{catalog_type.class}, it represents neither a PHostClassType, nor a PResourceType." end end def extract_file_line(o) source_pos = Puppet::Pops::Utils.find_closest_positioned(o) return [nil, -1] unless source_pos [source_pos.locator.file, source_pos.line] end def find_closest_positioned(o) return nil if o.nil? || o.is_a?(Puppet::Pops::Model::Program) o.offset.nil? ? find_closest_positioned(o.eContainer) : Puppet::Pops::Adapters::SourcePosAdapter.adapt(o) end # Creates a diagnostic producer def diagnostic_producer Puppet::Pops::Validation::DiagnosticProducer.new( ExceptionRaisingAcceptor.new(), # Raises exception on all issues SeverityProducer.new(), # All issues are errors Puppet::Pops::Model::ModelLabelProvider.new()) end # Configure the severity of failures class SeverityProducer < Puppet::Pops::Validation::SeverityProducer Issues = Puppet::Pops::Issues def initialize super p = self # Issues triggering warning only if --debug is on if Puppet[:debug] p[Issues::EMPTY_RESOURCE_SPECIALIZATION] = :warning else p[Issues::EMPTY_RESOURCE_SPECIALIZATION] = :ignore end end end # An acceptor of diagnostics that immediately raises an exception. class ExceptionRaisingAcceptor < Puppet::Pops::Validation::Acceptor def accept(diagnostic) super Puppet::Pops::IssueReporter.assert_and_report(self, {:message => "Evaluation Error:", :emit_warnings => true }) if errors? raise ArgumentError, "Internal Error: Configuration of runtime error handling wrong: should have raised exception" end end end class EvaluationError < StandardError end end diff --git a/lib/puppet/pops/types/type_calculator.rb b/lib/puppet/pops/types/type_calculator.rb index 46de35b7b..7ae9d1f8c 100644 --- a/lib/puppet/pops/types/type_calculator.rb +++ b/lib/puppet/pops/types/type_calculator.rb @@ -1,1601 +1,1601 @@ # The TypeCalculator can answer questions about puppet types. # # The Puppet type system is primarily based on sub-classing. When asking the type calculator to infer types from Ruby in general, it # may not provide the wanted answer; it does not for instance take module inclusions and extensions into account. In general the type # system should be unsurprising for anyone being exposed to the notion of type. The type `Data` may require a bit more explanation; this # is an abstract type that includes all scalar types, as well as Array with an element type compatible with Data, and Hash with key # compatible with scalar and elements compatible with Data. Expressed differently; Data is what you typically express using JSON (with # the exception that the Puppet type system also includes Pattern (regular expression) as a scalar. # # Inference # --------- # The `infer(o)` method infers a Puppet type for scalar Ruby objects, and for Arrays and Hashes. # The inference result is instance specific for single typed collections # and allows answering questions about its embedded type. It does not however preserve multiple types in # a collection, and can thus not answer questions like `[1,a].infer() =~ Array[Integer, String]` since the inference # computes the common type Scalar when combining Integer and String. # # The `infer_generic(o)` method infers a generic Puppet type for scalar Ruby object, Arrays and Hashes. # This inference result does not contain instance specific information; e.g. Array[Integer] where the integer # range is the generic default. Just `infer` it also combines types into a common type. # # The `infer_set(o)` method works like `infer` but preserves all type information. It does not do any # reduction into common types or ranges. This method of inference is best suited for answering questions # about an object being an instance of a type. It correctly answers: `[1,a].infer_set() =~ Array[Integer, String]` # # The `generalize!(t)` method modifies an instance specific inference result to a generic. The method mutates # the given argument. Basically, this removes string instances from String, and range from Integer and Float. # # Assignability # ------------- # The `assignable?(t1, t2)` method answers if t2 conforms to t1. The type t2 may be an instance, in which case # its type is inferred, or a type. # # Instance? # --------- # The `instance?(t, o)` method answers if the given object (instance) is an instance that is assignable to the given type. # # String # ------ # Creates a string representation of a type. # # Creation of Type instances # -------------------------- # Instance of the classes in the {Puppet::Pops::Types type model} are used to denote a specific type. It is most convenient # to use the {Puppet::Pops::Types::TypeFactory TypeFactory} when creating instances. # # @note # In general, new instances of the wanted type should be created as they are assigned to models using containment, and a # contained object can only be in one container at a time. Also, the type system may include more details in each type # instance, such as if it may be nil, be empty, contain a certain count etc. Or put differently, the puppet types are not # singletons. # # All types support `copy` which should be used when assigning a type where it is unknown if it is bound or not # to a parent type. A check can be made with `t.eContainer().nil?` # # Equality and Hash # ----------------- # Type instances are equal in terms of Ruby eql? and `==` if they describe the same type, but they are not `equal?` if they are not # the same type instance. Two types that describe the same type have identical hash - this makes them usable as hash keys. # # Types and Subclasses # -------------------- # In general, the type calculator should be used to answer questions if a type is a subtype of another (using {#assignable?}, or # {#instance?} if the question is if a given object is an instance of a given type (or is a subtype thereof). # Many of the types also have a Ruby subtype relationship; e.g. PHashType and PArrayType are both subtypes of PCollectionType, and # PIntegerType, PFloatType, PStringType,... are subtypes of PScalarType. Even if it is possible to answer certain questions about # type by looking at the Ruby class of the types this is considered an implementation detail, and such checks should in general # be performed by the type_calculator which implements the type system semantics. # # The PRubyType # ------------- # The PRubyType corresponds to a Ruby Class, except for the puppet types that are specialized (i.e. PRubyType should not be # used for Integer, String, etc. since there are specialized types for those). # When the type calculator deals with PRubyTypes and checks for assignability, it determines the "common ancestor class" of two classes. # This check is made based on the superclasses of the two classes being compared. In order to perform this, the classes must be present # (i.e. they are resolved from the string form in the PRubyType to a loaded, instantiated Ruby Class). In general this is not a problem, # since the question to produce the common super type for two objects means that the classes must be present or there would have been # no instances present in the first place. If however the classes are not present, the type calculator will fall back and state that # the two types at least have Object in common. # # @see Puppet::Pops::Types::TypeFactory TypeFactory for how to create instances of types # @see Puppet::Pops::Types::TypeParser TypeParser how to construct a type instance from a String # @see Puppet::Pops::Types Types for details about the type model # # Using the Type Calculator # ----- # The type calculator can be directly used via its class methods. If doing time critical work and doing many # calls to the type calculator, it is more performant to create an instance and invoke the corresponding # instance methods. Note that inference is an expensive operation, rather than infering the same thing # several times, it is in general better to infer once and then copy the result if mutation to a more generic form is # required. # # @api public # class Puppet::Pops::Types::TypeCalculator Types = Puppet::Pops::Types TheInfinity = 1.0 / 0.0 # because the Infinity symbol is not defined # @api public def self.assignable?(t1, t2) singleton.assignable?(t1,t2) end # Answers, does the given callable accept the arguments given in args (an array or a tuple) # @param callable [Puppet::Pops::Types::PCallableType] - the callable # @param args [Puppet::Pops::Types::PArrayType, Puppet::Pops::Types::PTupleType] args optionally including a lambda callable at the end # @return [Boolan] true if the callable accepts the arguments # # @api public def self.callable?(callable, args) singleton.callable?(callable, args) end # Produces a String representation of the given type. # @param t [Puppet::Pops::Types::PAbstractType] the type to produce a string form # @return [String] the type in string form # # @api public # def self.string(t) singleton.string(t) end # @api public def self.infer(o) singleton.infer(o) end # @api public def self.generalize!(o) singleton.generalize!(o) end # @api public def self.infer_set(o) singleton.infer_set(o) end # @api public def self.debug_string(t) singleton.debug_string(t) end # @api public def self.enumerable(t) singleton.enumerable(t) end # @api private def self.singleton() @tc_instance ||= new end # @api public # def initialize @@assignable_visitor ||= Puppet::Pops::Visitor.new(nil,"assignable",1,1) @@infer_visitor ||= Puppet::Pops::Visitor.new(nil,"infer",0,0) @@infer_set_visitor ||= Puppet::Pops::Visitor.new(nil,"infer_set",0,0) @@instance_of_visitor ||= Puppet::Pops::Visitor.new(nil,"instance_of",1,1) @@string_visitor ||= Puppet::Pops::Visitor.new(nil,"string",0,0) @@inspect_visitor ||= Puppet::Pops::Visitor.new(nil,"debug_string",0,0) @@enumerable_visitor ||= Puppet::Pops::Visitor.new(nil,"enumerable",0,0) @@extract_visitor ||= Puppet::Pops::Visitor.new(nil,"extract",0,0) @@generalize_visitor ||= Puppet::Pops::Visitor.new(nil,"generalize",0,0) @@callable_visitor ||= Puppet::Pops::Visitor.new(nil,"callable",1,1) da = Types::PArrayType.new() da.element_type = Types::PDataType.new() @data_array = da h = Types::PHashType.new() h.element_type = Types::PDataType.new() h.key_type = Types::PScalarType.new() @data_hash = h @data_t = Types::PDataType.new() @scalar_t = Types::PScalarType.new() @numeric_t = Types::PNumericType.new() @t = Types::PObjectType.new() # Data accepts a Tuple that has 0-infinity Data compatible entries (e.g. a Tuple equivalent to Array). data_tuple = Types::PTupleType.new() data_tuple.addTypes(Types::PDataType.new()) data_tuple.size_type = Types::PIntegerType.new() data_tuple.size_type.from = 0 data_tuple.size_type.to = nil # infinity @data_tuple_t = data_tuple # Variant type compatible with Data data_variant = Types::PVariantType.new() data_variant.addTypes(@data_hash.copy) data_variant.addTypes(@data_array.copy) data_variant.addTypes(Types::PScalarType.new) data_variant.addTypes(Types::PNilType.new) data_variant.addTypes(@data_tuple_t.copy) @data_variant_t = data_variant collection_default_size = Types::PIntegerType.new() collection_default_size.from = 0 collection_default_size.to = nil # infinity @collection_default_size_t = collection_default_size non_empty_string = Types::PStringType.new non_empty_string.size_type = Types::PIntegerType.new() non_empty_string.size_type.from = 1 non_empty_string.size_type.to = nil # infinity @non_empty_string_t = non_empty_string @nil_t = Types::PNilType.new end # Convenience method to get a data type for comparisons # @api private the returned value may not be contained in another element # def data @data_t end # Convenience method to get a variant compatible with the Data type. # @api private the returned value may not be contained in another element # def data_variant @data_variant_t end def self.data_variant singleton.data_variant end # Answers the question 'is it possible to inject an instance of the given class' # A class is injectable if it has a special *assisted inject* class method called `inject` taking # an injector and a scope as argument, or if it has a zero args `initialize` method. # # @param klazz [Class, PRubyType] the class/type to check if it is injectable # @return [Class, nil] the injectable Class, or nil if not injectable # @api public # def injectable_class(klazz) # Handle case when we get a PType instead of a class if klazz.is_a?(Types::PRubyType) klazz = Puppet::Pops::Types::ClassLoader.provide(klazz) end # data types can not be injected (check again, it is not safe to assume that given RubyType klazz arg was ok) return false unless type(klazz).is_a?(Types::PRubyType) if (klazz.respond_to?(:inject) && klazz.method(:inject).arity() == -4) || klazz.instance_method(:initialize).arity() == 0 klazz else nil end end # Answers 'can an instance of type t2 be assigned to a variable of type t'. # Does not accept nil/undef unless the type accepts it. # # @api public # def assignable?(t, t2) if t.is_a?(Class) t = type(t) end if t2.is_a?(Class) t2 = type(t2) end @@assignable_visitor.visit_this_1(self, t, t2) end # Returns an enumerable if the t represents something that can be iterated def enumerable(t) @@enumerable_visitor.visit_this_0(self, t) end # Answers, does the given callable accept the arguments given in args (an array or a tuple) # def callable?(callable, args) return false if !callable.is_a?(Types::PCallableType) # Note that polymorphism is for the args type, the callable is always a callable @@callable_visitor.visit_this_1(self, args, callable) end # Answers if the two given types describe the same type def equals(left, right) return false unless left.is_a?(Types::PAbstractType) && right.is_a?(Types::PAbstractType) # Types compare per class only - an extra test must be made if the are mutually assignable # to find all types that represent the same type of instance # left == right || (assignable?(right, left) && assignable?(left, right)) end # Answers 'what is the Puppet Type corresponding to the given Ruby class' # @param c [Class] the class for which a puppet type is wanted # @api public # def type(c) raise ArgumentError, "Argument must be a Class" unless c.is_a? Class # Can't use a visitor here since we don't have an instance of the class case when c <= Integer type = Types::PIntegerType.new() when c == Float type = Types::PFloatType.new() when c == Numeric type = Types::PNumericType.new() when c == String type = Types::PStringType.new() when c == Regexp type = Types::PRegexpType.new() when c == NilClass type = Types::PNilType.new() when c == FalseClass, c == TrueClass type = Types::PBooleanType.new() when c == Class type = Types::PType.new() when c == Array # Assume array of data values type = Types::PArrayType.new() type.element_type = Types::PDataType.new() when c == Hash # Assume hash with scalar keys and data values type = Types::PHashType.new() type.key_type = Types::PScalarType.new() type.element_type = Types::PDataType.new() else type = Types::PRubyType.new() type.ruby_class = c.name end type end # Generalizes value specific types. The given type is mutated and returned. # @api public def generalize!(o) @@generalize_visitor.visit_this_0(self, o) o.eAllContents.each { |x| @@generalize_visitor.visit_this_0(self, x) } o end def generalize_Object(o) # do nothing, there is nothing to change for most types end def generalize_PStringType(o) o.values = [] o.size_type = nil [] end def generalize_PCollectionType(o) # erase the size constraint from Array and Hash (if one exists, it is transformed to -Infinity - + Infinity, which is # not desirable. o.size_type = nil end def generalize_PFloatType(o) o.to = nil o.from = nil end def generalize_PIntegerType(o) o.to = nil o.from = nil end # Answers 'what is the single common Puppet Type describing o', or if o is an Array or Hash, what is the # single common type of the elements (or keys and elements for a Hash). # @api public # def infer(o) @@infer_visitor.visit_this_0(self, o) end def infer_generic(o) result = generalize!(infer(o)) result end # Answers 'what is the set of Puppet Types of o' # @api public # def infer_set(o) @@infer_set_visitor.visit_this_0(self, o) end def instance_of(t, o) @@instance_of_visitor.visit_this_1(self, t, o) end def instance_of_Object(t, o) # Undef is Undef and Object, but nothing else when checking instance? return false if (o.nil? || o == :undef) && t.class != Types::PObjectType assignable?(t, infer(o)) end def instance_of_PArrayType(t, o) return false unless o.is_a?(Array) return false unless o.all? {|element| instance_of(t.element_type, element) } size_t = t.size_type || @collection_default_size_t size_t2 = size_as_type(o) assignable?(size_t, size_t2) end def instance_of_PTupleType(t, o) return false unless o.is_a?(Array) # compute the tuple's min/max size, and check if that size matches size_t = t.size_type || Puppet::Pops::Types::TypeFactory.range(*t.size_range) # compute the array's size as type size_t2 = size_as_type(o) return false unless assignable?(size_t, size_t2) o.each_with_index do |element, index| return false unless instance_of(t.types[index] || t.types[-1], element) end true end def instance_of_PStructType(t, o) return false unless o.is_a?(Hash) h = t.hashed_elements # all keys must be present and have a value (even if nil/undef) (o.keys - h.keys).empty? && h.all? { |k,v| instance_of(v, o[k]) } end def instance_of_PHashType(t, o) return false unless o.is_a?(Hash) key_t = t.key_type element_t = t.element_type return false unless o.keys.all? {|key| instance_of(key_t, key) } && o.values.all? {|value| instance_of(element_t, value) } size_t = t.size_type || @collection_default_size_t size_t2 = size_as_type(o) assignable?(size_t, size_t2) end def instance_of_PDataType(t, o) instance_of(@data_variant_t, o) end def instance_of_PNilType(t, o) return o.nil? || o == :undef end def instance_of_POptionalType(t, o) return true if (o.nil? || o == :undef) instance_of(t.optional_type, o) end def instance_of_PVariantType(t, o) # instance of variant if o is instance? of any of variant's types t.types.any? { |option_t| instance_of(option_t, o) } end # Answers 'is o an instance of type t' # @api public # def self.instance?(t, o) singleton.instance_of(t,o) end # Answers 'is o an instance of type t' # @api public # def instance?(t, o) instance_of(t,o) end # Answers if t is a puppet type # @api public # def is_ptype?(t) return t.is_a?(Types::PAbstractType) end # Answers if t represents the puppet type PNilType # @api public # def is_pnil?(t) return t.nil? || t.is_a?(Types::PNilType) end # Answers, 'What is the common type of t1 and t2?' # # TODO: The current implementation should be optimized for performance # # @api public # def common_type(t1, t2) raise ArgumentError, 'two types expected' unless (is_ptype?(t1) || is_pnil?(t1)) && (is_ptype?(t2) || is_pnil?(t2)) # if either is nil, the common type is the other if is_pnil?(t1) return t2 elsif is_pnil?(t2) return t1 end # Simple case, one is assignable to the other if assignable?(t1, t2) return t1 elsif assignable?(t2, t1) return t2 end # when both are arrays, return an array with common element type if t1.is_a?(Types::PArrayType) && t2.is_a?(Types::PArrayType) type = Types::PArrayType.new() type.element_type = common_type(t1.element_type, t2.element_type) return type end # when both are hashes, return a hash with common key- and element type if t1.is_a?(Types::PHashType) && t2.is_a?(Types::PHashType) type = Types::PHashType.new() type.key_type = common_type(t1.key_type, t2.key_type) type.element_type = common_type(t1.element_type, t2.element_type) return type end # when both are host-classes, reduce to PHostClass[] (since one was not assignable to the other) if t1.is_a?(Types::PHostClassType) && t2.is_a?(Types::PHostClassType) return Types::PHostClassType.new() end # when both are resources, reduce to Resource[T] or Resource[] (since one was not assignable to the other) if t1.is_a?(Types::PResourceType) && t2.is_a?(Types::PResourceType) result = Types::PResourceType.new() # only Resource[] unless the type name is the same if t1.type_name == t2.type_name then result.type_name = t1.type_name end # the cross assignability test above has already determined that they do not have the same type and title return result end # Integers have range, expand the range to the common range if t1.is_a?(Types::PIntegerType) && t2.is_a?(Types::PIntegerType) t1range = from_to_ordered(t1.from, t1.to) t2range = from_to_ordered(t2.from, t2.to) t = Types::PIntegerType.new() from = [t1range[0], t2range[0]].min to = [t1range[1], t2range[1]].max t.from = from unless from == TheInfinity t.to = to unless to == TheInfinity return t end # Floats have range, expand the range to the common range if t1.is_a?(Types::PFloatType) && t2.is_a?(Types::PFloatType) t1range = from_to_ordered(t1.from, t1.to) t2range = from_to_ordered(t2.from, t2.to) t = Types::PFloatType.new() from = [t1range[0], t2range[0]].min to = [t1range[1], t2range[1]].max t.from = from unless from == TheInfinity t.to = to unless to == TheInfinity return t end if t1.is_a?(Types::PStringType) && t2.is_a?(Types::PStringType) t = Types::PStringType.new() t.values = t1.values | t2.values return t end if t1.is_a?(Types::PPatternType) && t2.is_a?(Types::PPatternType) t = Types::PPatternType.new() # must make copies since patterns are contained types, not data-types t.patterns = (t1.patterns | t2.patterns).map {|p| p.copy } return t end if t1.is_a?(Types::PEnumType) && t2.is_a?(Types::PEnumType) # The common type is one that complies with either set t = Types::PEnumType.new t.values = t1.values | t2.values return t end if t1.is_a?(Types::PVariantType) && t2.is_a?(Types::PVariantType) # The common type is one that complies with either set t = Types::PVariantType.new t.types = (t1.types | t2.types).map {|opt_t| opt_t.copy } return t end if t1.is_a?(Types::PRegexpType) && t2.is_a?(Types::PRegexpType) # if they were identical, the general rule would return a parameterized regexp # since they were not, the result is a generic regexp type return Types::PPatternType.new() end if t1.is_a?(Types::PCallableType) && t2.is_a?(Types::PCallableType) # They do not have the same signature, and one is not assignable to the other, # what remains is the most general form of Callable return Types::PCallableType.new() end # Common abstract types, from most specific to most general if common_numeric?(t1, t2) return Types::PNumericType.new() end if common_scalar?(t1, t2) return Types::PScalarType.new() end if common_data?(t1,t2) return Types::PDataType.new() end # Meta types Type[Integer] + Type[String] => Type[Data] if t1.is_a?(Types::PType) && t2.is_a?(Types::PType) type = Types::PType.new() type.type = common_type(t1.type, t2.type) return type end if t1.is_a?(Types::PRubyType) && t2.is_a?(Types::PRubyType) if t1.ruby_class == t2.ruby_class return t1 end # finding the common super class requires that names are resolved to class c1 = Types::ClassLoader.provide_from_type(t1) c2 = Types::ClassLoader.provide_from_type(t2) if c1 && c2 c2_superclasses = superclasses(c2) superclasses(c1).each do|c1_super| c2_superclasses.each do |c2_super| if c1_super == c2_super result = Types::PRubyType.new() result.ruby_class = c1_super.name return result end end end end end # If both are RubyObjects if common_pobject?(t1, t2) return Types::PObjectType.new() end end # Produces the superclasses of the given class, including the class def superclasses(c) result = [c] while s = c.superclass result << s c = s end result end # Produces a string representing the type # @api public # def string(t) @@string_visitor.visit_this_0(self, t) end # Produces a debug string representing the type (possibly with more information that the regular string format) # @api public # def debug_string(t) @@inspect_visitor.visit_this_0(self, t) end # Reduces an enumerable of types to a single common type. # @api public # def reduce_type(enumerable) enumerable.reduce(nil) {|memo, t| common_type(memo, t) } end # Reduce an enumerable of objects to a single common type # @api public # def infer_and_reduce_type(enumerable) reduce_type(enumerable.collect() {|o| infer(o) }) end # The type of all classes is PType # @api private # def infer_Class(o) Types::PType.new() end # @api private def infer_Closure(o) o.type() end # @api private def infer_Function(o) o.class.dispatcher.to_type end # @api private def infer_Object(o) type = Types::PRubyType.new() type.ruby_class = o.class.name type end # The type of all types is PType # @api private # def infer_PAbstractType(o) type = Types::PType.new() type.type = o.copy type end # The type of all types is PType # This is the metatype short circuit. # @api private # def infer_PType(o) type = Types::PType.new() type.type = o.copy type end # @api private def infer_String(o) t = Types::PStringType.new() t.addValues(o) t.size_type = size_as_type(o) t end # @api private def infer_Float(o) t = Types::PFloatType.new() t.from = o t.to = o t end # @api private def infer_Integer(o) t = Types::PIntegerType.new() t.from = o t.to = o t end # @api private def infer_Regexp(o) t = Types::PRegexpType.new() t.pattern = o.source t end # @api private def infer_NilClass(o) Types::PNilType.new() end # Inference of :undef as PNilType, all other are Ruby[Symbol] # @api private def infer_Symbol(o) o == :undef ? infer_NilClass(o) : infer_Object(o) end # @api private def infer_TrueClass(o) Types::PBooleanType.new() end # @api private def infer_FalseClass(o) Types::PBooleanType.new() end # @api private # A Puppet::Parser::Resource, or Puppet::Resource # def infer_Resource(o) t = Types::PResourceType.new() t.type_name = o.type.to_s.downcase # Only Puppet::Resource can have a title that is a symbol :undef, a PResource cannot. # A mapping must be made to empty string. A nil value will result in an error later title = o.title t.title = (title == :undef ? '' : title) type = Types::PType.new() type.type = t type end # @api private def infer_Array(o) type = Types::PArrayType.new() type.element_type = if o.empty? Types::PNilType.new() else infer_and_reduce_type(o) end type.size_type = size_as_type(o) type end # @api private def infer_Hash(o) type = Types::PHashType.new() if o.empty? ktype = Types::PNilType.new() etype = Types::PNilType.new() else ktype = infer_and_reduce_type(o.keys()) etype = infer_and_reduce_type(o.values()) end type.key_type = ktype type.element_type = etype type.size_type = size_as_type(o) type end def size_as_type(collection) size = collection.size t = Types::PIntegerType.new() t.from = size t.to = size t end # Common case for everything that intrinsically only has a single type def infer_set_Object(o) infer(o) end def infer_set_Array(o) if o.empty? type = Types::PArrayType.new() type.element_type = Types::PNilType.new() type.size_type = size_as_type(o) else type = Types::PTupleType.new() type.types = o.map() {|x| infer_set(x) } end type end def infer_set_Hash(o) type = Types::PHashType.new() if o.empty? ktype = Types::PNilType.new() - etype = Types::PNilType.new() + vtype = Types::PNilType.new() else ktype = Types::PVariantType.new() ktype.types = o.keys.map() {|k| infer_set(k) } etype = Types::PVariantType.new() etype.types = o.values.map() {|e| infer_set(e) } end type.key_type = unwrap_single_variant(ktype) - type.element_type = unwrap_single_variant(vtype) + type.element_type = unwrap_single_variant(etype) type.size_type = size_as_type(o) type end def unwrap_single_variant(possible_variant) if possible_variant.is_a?(Types::PVariantType) && possible_variant.types.size == 1 possible_variant.types[0] else possible_variant end end # False in general type calculator # @api private def assignable_Object(t, t2) false end # @api private def assignable_PObjectType(t, t2) t2.is_a?(Types::PObjectType) end # @api private def assignable_PNilType(t, t2) # Only undef/nil is assignable to nil type t2.is_a?(Types::PNilType) end # @api private def assignable_PScalarType(t, t2) t2.is_a?(Types::PScalarType) end # @api private def assignable_PNumericType(t, t2) t2.is_a?(Types::PNumericType) end # @api private def assignable_PIntegerType(t, t2) return false unless t2.is_a?(Types::PIntegerType) trange = from_to_ordered(t.from, t.to) t2range = from_to_ordered(t2.from, t2.to) # If t2 min and max are within the range of t trange[0] <= t2range[0] && trange[1] >= t2range[1] end # Transform int range to a size constraint # if range == nil the constraint is 1,1 # if range.from == nil min size = 1 # if range.to == nil max size == Infinity # def size_range(range) return [1,1] if range.nil? from = range.from to = range.to x = from.nil? ? 1 : from y = to.nil? ? TheInfinity : to if x < y [x, y] else [y, x] end end # @api private def from_to_ordered(from, to) x = (from.nil? || from == :default) ? -TheInfinity : from y = (to.nil? || to == :default) ? TheInfinity : to if x < y [x, y] else [y, x] end end # @api private def assignable_PVariantType(t, t2) # Data is a specific variant t2 = @data_variant_t if t2.is_a?(Types::PDataType) if t2.is_a?(Types::PVariantType) # A variant is assignable if all of its options are assignable to one of this type's options return true if t == t2 t2.types.all? do |other| # if the other is a Variant, all if its options, but be assignable to one of this type's options other = other.is_a?(Types::PDataType) ? @data_variant_t : other if other.is_a?(Types::PVariantType) assignable?(t, other) else t.types.any? {|option_t| assignable?(option_t, other) } end end else # A variant is assignable if t2 is assignable to any of its types t.types.any? { |option_t| assignable?(option_t, t2) } end end # Catch all not callable combinations def callable_Object(o, callable_t) false end def callable_PTupleType(args_tuple, callable_t) if args_tuple.size_type raise ArgumentError, "Callable tuple may not have a size constraint when used as args" end # Assume no block was given - i.e. it is nil, and its type is PNilType block_t = @nil_t if args_tuple.types.last.is_a?(Types::PCallableType) # a split is needed to make it possible to use required, optional, and varargs semantics # of the tuple type. # args_tuple = args_tuple.copy # to drop the callable, it must be removed explicitly since this is an rgen array args_tuple.removeTypes(block_t = args_tuple.types.last()) else # no block was given, if it is required, the below will fail end # unless argument types match parameter types return false unless assignable?(callable_t.param_types, args_tuple) # unless given block (or no block) matches expected block (or no block) assignable?(callable_t.block_type || @nil_t, block_t) end def callable_PArrayType(args_array, callable_t) return false unless assignable?(callable_t.param_types, args_array) # does not support calling with a block, but have to check that callable expects it assignable?(callable_t.block_type || @nil_t, @nil_t) end def max(a,b) a >=b ? a : b end def min(a,b) a <= b ? a : b end def assignable_PTupleType(t, t2) return true if t == t2 || t.types.empty? && (t2.is_a?(Types::PArrayType)) size_t = t.size_type || Puppet::Pops::Types::TypeFactory.range(*t.size_range) if t2.is_a?(Types::PTupleType) size_t2 = t2.size_type || Puppet::Pops::Types::TypeFactory.range(*t2.size_range) # not assignable if the number of types in t2 is outside number of types in t1 if assignable?(size_t, size_t2) t2.types.size.times do |index| return false unless assignable?((t.types[index] || t.types[-1]), t2.types[index]) end return true else return false end elsif t2.is_a?(Types::PArrayType) t2_entry = t2.element_type # Array of anything can not be assigned (unless tuple is tuple of anything) - this case # was handled at the top of this method. # return false if t2_entry.nil? size_t = t.size_type || Puppet::Pops::Types::TypeFactory.range(*t.size_range) size_t2 = t2.size_type || @collection_default_size_t return false unless assignable?(size_t, size_t2) min(t.types.size, size_t2.range()[1]).times do |index| return false unless assignable?((t.types[index] || t.types[-1]), t2_entry) end true else false end end # Produces the tuple entry at the given index given a tuple type, its from/to constraints on the last # type, and an index. # Produces nil if the index is out of bounds # from must be less than to, and from may not be less than 0 # # @api private # def tuple_entry_at(tuple_t, from, to, index) regular = (tuple_t.types.size - 1) if index < regular tuple_t.types[index] elsif index < regular + to # in the varargs part tuple_t.types[-1] else nil end end # @api private # def assignable_PStructType(t, t2) return true if t == t2 || t.elements.empty? && (t2.is_a?(Types::PHashType)) h = t.hashed_elements if t2.is_a?(Types::PStructType) h2 = t2.hashed_elements h.size == h2.size && h.all? {|k, v| assignable?(v, h2[k]) } elsif t2.is_a?(Types::PHashType) size_t2 = t2.size_type || @collection_default_size_t size_t = Types::PIntegerType.new size_t.from = size_t.to = h.size # compatible size # hash key type must be string of min 1 size # hash value t must be assignable to each key element_type = t2.element_type assignable?(size_t, size_t2) && assignable?(@non_empty_string_t, t2.key_type) && h.all? {|k,v| assignable?(v, element_type) } else false end end # @api private def assignable_POptionalType(t, t2) return true if t2.is_a?(Types::PNilType) if t2.is_a?(Types::POptionalType) assignable?(t.optional_type, t2.optional_type) else assignable?(t.optional_type, t2) end end # @api private def assignable_PEnumType(t, t2) return true if t == t2 || (t.values.empty? && (t2.is_a?(Types::PStringType) || t2.is_a?(Types::PEnumType))) if t2.is_a?(Types::PStringType) # if the set of strings are all found in the set of enums t2.values.all? { |s| t.values.any? { |e| e == s }} else false end end # @api private def assignable_PStringType(t, t2) if t.values.empty? # A general string is assignable by any other string or pattern restricted string # if the string has a size constraint it does not match since there is no reasonable way # to compute the min/max length a pattern will match. For enum, it is possible to test that # each enumerator value is within range size_t = t.size_type || @collection_default_size_t case t2 when Types::PStringType # true if size compliant size_t2 = t2.size_type || @collection_default_size_t assignable?(size_t, size_t2) when Types::PPatternType # true if size constraint is at least 0 to +Infinity (which is the same as the default) assignable?(size_t, @collection_default_size_t) when Types::PEnumType if t2.values # true if all enum values are within range min, max = t2.values.map(&:size).minmax trange = from_to_ordered(size_t.from, size_t.to) t2range = [min, max] # If t2 min and max are within the range of t trange[0] <= t2range[0] && trange[1] >= t2range[1] else # no string can match this enum anyway since it does not accept anything false end else # no other type matches string false end elsif t2.is_a?(Types::PStringType) # A specific string acts as a set of strings - must have exactly the same strings # In this case, size does not matter since the definition is very precise anyway Set.new(t.values) == Set.new(t2.values) else # All others are false, since no other type describes the same set of specific strings false end end # @api private def assignable_PPatternType(t, t2) return true if t == t2 return false unless t2.is_a?(Types::PStringType) || t2.is_a?(Types::PEnumType) if t2.values.empty? # Strings / Enums (unknown which ones) cannot all match a pattern, but if there is no pattern it is ok # (There should really always be a pattern, but better safe than sorry). return t.patterns.empty? ? true : false end # all strings in String/Enum type must match one of the patterns in Pattern type regexps = t.patterns.map {|p| p.regexp } t2.values.all? { |v| regexps.any? {|re| re.match(v) } } end # @api private def assignable_PFloatType(t, t2) return false unless t2.is_a?(Types::PFloatType) trange = from_to_ordered(t.from, t.to) t2range = from_to_ordered(t2.from, t2.to) # If t2 min and max are within the range of t trange[0] <= t2range[0] && trange[1] >= t2range[1] end # @api private def assignable_PBooleanType(t, t2) t2.is_a?(Types::PBooleanType) end # @api private def assignable_PRegexpType(t, t2) t2.is_a?(Types::PRegexpType) && (t.pattern.nil? || t.pattern == t2.pattern) end # @api private def assignable_PCallableType(t, t2) return false unless t2.is_a?(Types::PCallableType) # nil param_types means, any other Callable is assignable return true if t.param_types.nil? return false unless assignable?(t.param_types, t2.param_types) # names are ignored, they are just information # Blocks must be compatible this_block_t = t.block_type || @nil_t that_block_t = t2.block_type || @nil_t assignable?(this_block_t, that_block_t) end # @api private def assignable_PCollectionType(t, t2) size_t = t.size_type || @collection_default_size_t case t2 when Types::PCollectionType size_t2 = t2.size_type || @collection_default_size_t assignable?(size_t, size_t2) when Types::PTupleType # compute the tuple's min/max size, and check if that size matches from, to = size_range(t2.size_type) t2s = Types::PIntegerType.new() t2s.from = t2.types.size - 1 + from t2s.to = t2.types.size - 1 + to assignable?(size_t, t2s) when Types::PStructType from = to = t2.elements.size t2s = Types::PIntegerType.new() t2s.from = from t2s.to = to assignable?(size_t, t2s) else false end end # @api private def assignable_PType(t, t2) return false unless t2.is_a?(Types::PType) return true if t.type.nil? # wide enough to handle all types return false if t2.type.nil? # wider than t assignable?(t.type, t2.type) end # Array is assignable if t2 is an Array and t2's element type is assignable, or if t2 is a Tuple # where # @api private def assignable_PArrayType(t, t2) if t2.is_a?(Types::PArrayType) return false unless assignable?(t.element_type, t2.element_type) assignable_PCollectionType(t, t2) elsif t2.is_a?(Types::PTupleType) return false unless t2.types.all? {|t2_element| assignable?(t.element_type, t2_element) } t2_regular = t2.types[0..-2] t2_ranged = t2.types[-1] t2_from, t2_to = size_range(t2.size_type) t2_required = t2_regular.size + t2_from t_entry = t.element_type # Tuple of anything can not be assigned (unless array is tuple of anything) - this case # was handled at the top of this method. # return false if t_entry.nil? # array type may be size constrained size_t = t.size_type || @collection_default_size_t min, max = size_t.range # Tuple with fewer min entries can not be assigned return false if t2_required < min # Tuple with more optionally available entries can not be assigned return false if t2_regular.size + t2_to > max # each tuple type must be assignable to the element type t2_required.times do |index| t2_entry = tuple_entry_at(t2, t2_from, t2_to, index) return false unless assignable?(t_entry, t2_entry) end # ... and so must the last, possibly optional (ranged) type return assignable?(t_entry, t2_ranged) else false end end # Hash is assignable if t2 is a Hash and t2's key and element types are assignable # @api private def assignable_PHashType(t, t2) case t2 when Types::PHashType return false unless assignable?(t.key_type, t2.key_type) && assignable?(t.element_type, t2.element_type) assignable_PCollectionType(t, t2) when Types::PStructType # hash must accept String as key type # hash must accept all value types # hash must accept the size of the struct size_t = t.size_type || @collection_default_size_t min, max = size_t.range struct_size = t2.elements.size element_type = t.element_type ( struct_size >= min && struct_size <= max && assignable?(t.key_type, @non_empty_string_t) && t2.hashed_elements.all? {|k,v| assignable?(element_type, v) }) else false end end # @api private def assignable_PCatalogEntryType(t1, t2) t2.is_a?(Types::PCatalogEntryType) end # @api private def assignable_PHostClassType(t1, t2) return false unless t2.is_a?(Types::PHostClassType) # Class = Class[name}, Class[name] != Class return true if t1.class_name.nil? # Class[name] = Class[name] return t1.class_name == t2.class_name end # @api private def assignable_PResourceType(t1, t2) return false unless t2.is_a?(Types::PResourceType) return true if t1.type_name.nil? return false if t1.type_name != t2.type_name return true if t1.title.nil? return t1.title == t2.title end # Data is assignable by other Data and by Array[Data] and Hash[Scalar, Data] # @api private def assignable_PDataType(t, t2) t2.is_a?(Types::PDataType) || assignable?(@data_variant_t, t2) end # Assignable if t2's ruby class is same or subclass of t1's ruby class # @api private def assignable_PRubyType(t1, t2) return false unless t2.is_a?(Types::PRubyType) return true if t1.ruby_class.nil? # t1 is wider return false if t2.ruby_class.nil? # t1 not nil, so t2 can not be wider c1 = class_from_string(t1.ruby_class) c2 = class_from_string(t2.ruby_class) return false unless c1.is_a?(Class) && c2.is_a?(Class) !!(c2 <= c1) end # @api private def debug_string_Object(t) string(t) end # @api private def string_PType(t) if t.type.nil? "Type" else "Type[#{string(t.type)}]" end end # @api private def string_NilClass(t) ; '?' ; end # @api private def string_String(t) ; t ; end # @api private def string_PObjectType(t) ; "Object" ; end # @api private def string_PNilType(t) ; 'Undef' ; end # @api private def string_PBooleanType(t) ; "Boolean" ; end # @api private def string_PScalarType(t) ; "Scalar" ; end # @api private def string_PDataType(t) ; "Data" ; end # @api private def string_PNumericType(t) ; "Numeric" ; end # @api private def string_PIntegerType(t) range = range_array_part(t) unless range.empty? "Integer[#{range.join(', ')}]" else "Integer" end end # Produces a string from an Integer range type that is used inside other type strings # @api private def range_array_part(t) return [] if t.nil? || (t.from.nil? && t.to.nil?) [t.from.nil? ? 'default' : t.from , t.to.nil? ? 'default' : t.to ] end # @api private def string_PFloatType(t) range = range_array_part(t) unless range.empty? "Float[#{range.join(', ')}]" else "Float" end end # @api private def string_PRegexpType(t) t.pattern.nil? ? "Regexp" : "Regexp[#{t.regexp.inspect}]" end # @api private def string_PStringType(t) # skip values in regular output - see debug_string range = range_array_part(t.size_type) unless range.empty? "String[#{range.join(', ')}]" else "String" end end # @api private def debug_string_PStringType(t) range = range_array_part(t.size_type) range_part = range.empty? ? '' : '[' << range.join(' ,') << '], ' "String[" << range_part << (t.values.map {|s| "'#{s}'" }).join(', ') << ']' end # @api private def string_PEnumType(t) return "Enum" if t.values.empty? "Enum[" << t.values.map {|s| "'#{s}'" }.join(', ') << ']' end # @api private def string_PVariantType(t) return "Variant" if t.types.empty? "Variant[" << t.types.map {|t2| string(t2) }.join(', ') << ']' end # @api private def string_PTupleType(t) range = range_array_part(t.size_type) return "Tuple" if t.types.empty? s = "Tuple[" << t.types.map {|t2| string(t2) }.join(', ') unless range.empty? s << ", " << range.join(', ') end s << "]" s end # @api private def string_PCallableType(t) # generic return "Callable" if t.param_types.nil? if t.param_types.types.empty? range = [0, 0] else range = range_array_part(t.param_types.size_type) end types = t.param_types.types.map {|t2| string(t2) } params_part= types.join(', ') s = "Callable[" << types.join(', ') unless range.empty? (s << ', ') unless types.empty? s << range.join(', ') end # Add block T last (after min, max) if present) # unless t.block_type.nil? (s << ', ') unless types.empty? && range.empty? s << string(t.block_type) end s << "]" s end # @api private def string_PStructType(t) return "Struct" if t.elements.empty? "Struct[{" << t.elements.map {|element| string(element) }.join(', ') << "}]" end def string_PStructElement(t) "'#{t.name}'=>#{string(t.type)}" end # @api private def string_PPatternType(t) return "Pattern" if t.patterns.empty? "Pattern[" << t.patterns.map {|s| "#{s.regexp.inspect}" }.join(', ') << ']' end # @api private def string_PCollectionType(t) range = range_array_part(t.size_type) unless range.empty? "Collection[#{range.join(', ')}]" else "Collection" end end # @api private def string_PRubyType(t) ; "Ruby[#{string(t.ruby_class)}]" ; end # @api private def string_PArrayType(t) parts = [string(t.element_type)] + range_array_part(t.size_type) "Array[#{parts.join(', ')}]" end # @api private def string_PHashType(t) parts = [string(t.key_type), string(t.element_type)] + range_array_part(t.size_type) "Hash[#{parts.join(', ')}]" end # @api private def string_PCatalogEntryType(t) "CatalogEntry" end # @api private def string_PHostClassType(t) if t.class_name "Class[#{t.class_name}]" else "Class" end end # @api private def string_PResourceType(t) if t.type_name if t.title "#{t.type_name.capitalize}['#{t.title}']" else "#{t.type_name.capitalize}" end else "Resource" end end def string_POptionalType(t) if t.optional_type.nil? "Optional" else "Optional[#{string(t.optional_type)}]" end end # Catches all non enumerable types # @api private def enumerable_Object(o) nil end # @api private def enumerable_PIntegerType(t) # Not enumerable if representing an infinite range return nil if t.size == TheInfinity t end def self.copy_as_tuple(t) case t when Types::PTupleType t.copy when Types::PArrayType # transform array to tuple result = Types::PTupleType.new result.addTypes(t.element_type.copy) result.size_type = t.size_type.nil? ? nil : t.size_type.copy result else raise ArgumentError, "Internal Error: Only Array and Tuple can be given to copy_as_tuple" end end private def class_from_string(str) begin str.split('::').inject(Object) do |memo, name_segment| memo.const_get(name_segment) end rescue NameError return nil end end def common_data?(t1, t2) assignable?(@data_t, t1) && assignable?(@data_t, t2) end def common_scalar?(t1, t2) assignable?(@scalar_t, t1) && assignable?(@scalar_t, t2) end def common_numeric?(t1, t2) assignable?(@numeric_t, t1) && assignable?(@numeric_t, t2) end def common_pobject?(t1, t2) assignable?(@t, t1) && assignable?(@t, t2) end end diff --git a/lib/puppet/util/functions/iterative_support.rb b/lib/puppet/util/functions/iterative_support.rb new file mode 100644 index 000000000..cc74154f2 --- /dev/null +++ b/lib/puppet/util/functions/iterative_support.rb @@ -0,0 +1,27 @@ +module Puppet::Util::Functions + module IterativeSupport + def asserted_serving_size(pblock, name_of_first) + size = pblock.parameter_count + if size == 0 + raise ArgumentError, "#{self.class.name}(): block must define at least one parameter; value. Block has 0." + end + if size > 2 + raise ArgumentError, "#{self.class.name}(): block must define at most two parameters; #{name_of_first}, value. Block has #{size}; "+ + pblock.parameter_names.join(', ') + end + if pblock.last_captures_rest? + # it has one or two parameters, and the last captures the rest - deliver args as if it accepts 2 + size = 2 + end + size + end + + def asserted_enumerable(obj) + unless enum = Puppet::Pops::Types::Enumeration.enumerator(obj) + raise ArgumentError, ("#{self.class.name}(): wrong argument type (#{obj.class}; must be something enumerable.") + end + enum + end + + end +end \ No newline at end of file diff --git a/spec/unit/parser/methods/each_spec.rb b/spec/unit/functions/each_spec.rb similarity index 95% rename from spec/unit/parser/methods/each_spec.rb rename to spec/unit/functions/each_spec.rb index 053d978e9..2cf7d07e7 100644 --- a/spec/unit/parser/methods/each_spec.rb +++ b/spec/unit/functions/each_spec.rb @@ -1,106 +1,110 @@ require 'puppet' require 'spec_helper' require 'puppet_spec/compiler' -require 'rubygems' + +require 'unit/functions/shared' describe 'the each method' do include PuppetSpec::Compiler before :each do Puppet[:parser] = 'future' end context "should be callable as" do it 'each on an array selecting each value' do catalog = compile_to_catalog(<<-MANIFEST) $a = [1,2,3] $a.each |$v| { file { "/file_$v": ensure => present } } MANIFEST catalog.resource(:file, "/file_1")['ensure'].should == 'present' catalog.resource(:file, "/file_2")['ensure'].should == 'present' catalog.resource(:file, "/file_3")['ensure'].should == 'present' end it 'each on an array selecting each value - function call style' do catalog = compile_to_catalog(<<-MANIFEST) $a = [1,2,3] each ($a) |$index, $v| { file { "/file_$v": ensure => present } } MANIFEST catalog.resource(:file, "/file_1")['ensure'].should == 'present' catalog.resource(:file, "/file_2")['ensure'].should == 'present' catalog.resource(:file, "/file_3")['ensure'].should == 'present' end it 'each on an array with index' do catalog = compile_to_catalog(<<-MANIFEST) $a = [present, absent, present] $a.each |$k,$v| { file { "/file_${$k+1}": ensure => $v } } MANIFEST catalog.resource(:file, "/file_1")['ensure'].should == 'present' catalog.resource(:file, "/file_2")['ensure'].should == 'absent' catalog.resource(:file, "/file_3")['ensure'].should == 'present' end it 'each on a hash selecting entries' do catalog = compile_to_catalog(<<-MANIFEST) $a = {'a'=>'present','b'=>'absent','c'=>'present'} $a.each |$e| { file { "/file_${e[0]}": ensure => $e[1] } } MANIFEST catalog.resource(:file, "/file_a")['ensure'].should == 'present' catalog.resource(:file, "/file_b")['ensure'].should == 'absent' catalog.resource(:file, "/file_c")['ensure'].should == 'present' end it 'each on a hash selecting key and value' do catalog = compile_to_catalog(<<-MANIFEST) $a = {'a'=>present,'b'=>absent,'c'=>present} $a.each |$k, $v| { file { "/file_$k": ensure => $v } } MANIFEST catalog.resource(:file, "/file_a")['ensure'].should == 'present' catalog.resource(:file, "/file_b")['ensure'].should == 'absent' catalog.resource(:file, "/file_c")['ensure'].should == 'present' end it 'each on a hash selecting key and value (using captures-last parameter)' do catalog = compile_to_catalog(<<-MANIFEST) $a = {'a'=>present,'b'=>absent,'c'=>present} $a.each |*$kv| { file { "/file_${kv[0]}": ensure => $kv[1] } } MANIFEST catalog.resource(:file, "/file_a")['ensure'].should == 'present' catalog.resource(:file, "/file_b")['ensure'].should == 'absent' catalog.resource(:file, "/file_c")['ensure'].should == 'present' end end context "should produce receiver" do it 'each checking produced value using single expression' do catalog = compile_to_catalog(<<-MANIFEST) $a = [1, 3, 2] $b = $a.each |$x| { "unwanted" } file { "/file_${b[1]}": ensure => present } MANIFEST catalog.resource(:file, "/file_3")['ensure'].should == 'present' end end + it_should_behave_like 'all iterative functions argument checks', 'each' + it_should_behave_like 'all iterative functions hash handling', 'each' + end diff --git a/spec/unit/parser/methods/filter_spec.rb b/spec/unit/functions/filter_spec.rb similarity index 99% rename from spec/unit/parser/methods/filter_spec.rb rename to spec/unit/functions/filter_spec.rb index 5639faff7..909837602 100644 --- a/spec/unit/parser/methods/filter_spec.rb +++ b/spec/unit/functions/filter_spec.rb @@ -1,149 +1,149 @@ require 'puppet' require 'spec_helper' require 'puppet_spec/compiler' require 'matchers/resource' -require 'unit/parser/methods/shared' +require 'unit/functions/shared' describe 'the filter method' do include PuppetSpec::Compiler include Matchers::Resource before :each do Puppet[:parser] = 'future' end it 'should filter on an array (all berries)' do catalog = compile_to_catalog(<<-MANIFEST) $a = ['strawberry','blueberry','orange'] $a.filter |$x|{ $x =~ /berry$/}.each |$v|{ file { "/file_$v": ensure => present } } MANIFEST expect(catalog).to have_resource("File[/file_strawberry]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_blueberry]").with_parameter(:ensure, 'present') end it 'should filter on enumerable type (Integer)' do catalog = compile_to_catalog(<<-MANIFEST) $a = Integer[1,10] $a.filter |$x|{ $x % 3 == 0}.each |$v|{ file { "/file_$v": ensure => present } } MANIFEST expect(catalog).to have_resource("File[/file_3]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_6]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_9]").with_parameter(:ensure, 'present') end it 'should filter on enumerable type (Integer) using two args index/value' do catalog = compile_to_catalog(<<-MANIFEST) $a = Integer[10,18] $a.filter |$i, $x|{ $i % 3 == 0}.each |$v|{ file { "/file_$v": ensure => present } } MANIFEST expect(catalog).to have_resource("File[/file_10]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_13]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_16]").with_parameter(:ensure, 'present') end it 'should produce an array when acting on an array' do catalog = compile_to_catalog(<<-MANIFEST) $a = ['strawberry','blueberry','orange'] $b = $a.filter |$x|{ $x =~ /berry$/} file { "/file_${b[0]}": ensure => present } file { "/file_${b[1]}": ensure => present } MANIFEST expect(catalog).to have_resource("File[/file_strawberry]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_blueberry]").with_parameter(:ensure, 'present') end it 'can filter array using index and value' do catalog = compile_to_catalog(<<-MANIFEST) $a = ['strawberry','blueberry','orange'] $b = $a.filter |$index, $x|{ $index == 0 or $index ==2} file { "/file_${b[0]}": ensure => present } file { "/file_${b[1]}": ensure => present } MANIFEST expect(catalog).to have_resource("File[/file_strawberry]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_orange]").with_parameter(:ensure, 'present') end it 'can filter array using index and value (using captures-rest)' do catalog = compile_to_catalog(<<-MANIFEST) $a = ['strawberry','blueberry','orange'] $b = $a.filter |*$ix|{ $ix[0] == 0 or $ix[0] ==2} file { "/file_${b[0]}": ensure => present } file { "/file_${b[1]}": ensure => present } MANIFEST expect(catalog).to have_resource("File[/file_strawberry]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_orange]").with_parameter(:ensure, 'present') end it 'filters on a hash (all berries) by key' do catalog = compile_to_catalog(<<-MANIFEST) $a = {'strawberry'=>'red','blueberry'=>'blue','orange'=>'orange'} $a.filter |$x|{ $x[0] =~ /berry$/}.each |$v|{ file { "/file_${v[0]}": ensure => present } } MANIFEST expect(catalog).to have_resource("File[/file_strawberry]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_blueberry]").with_parameter(:ensure, 'present') end it 'should produce a hash when acting on a hash' do catalog = compile_to_catalog(<<-MANIFEST) $a = {'strawberry'=>'red','blueberry'=>'blue','orange'=>'orange'} $b = $a.filter |$x|{ $x[0] =~ /berry$/} file { "/file_${b['strawberry']}": ensure => present } file { "/file_${b['blueberry']}": ensure => present } file { "/file_${b['orange']}": ensure => present } MANIFEST expect(catalog).to have_resource("File[/file_red]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_blue]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_]").with_parameter(:ensure, 'present') end it 'filters on a hash (all berries) by value' do catalog = compile_to_catalog(<<-MANIFEST) $a = {'strawb'=>'red berry','blueb'=>'blue berry','orange'=>'orange fruit'} $a.filter |$x|{ $x[1] =~ /berry$/}.each |$v|{ file { "/file_${v[0]}": ensure => present } } MANIFEST expect(catalog).to have_resource("File[/file_strawb]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_blueb]").with_parameter(:ensure, 'present') end context 'filter checks arguments and' do it 'raises an error when block has more than 2 argument' do expect do compile_to_catalog(<<-MANIFEST) [1].filter |$indexm, $x, $yikes|{ } MANIFEST end.to raise_error(Puppet::Error, /block must define at most two parameters/) end it 'raises an error when block has fewer than 1 argument' do expect do compile_to_catalog(<<-MANIFEST) [1].filter || { } MANIFEST end.to raise_error(Puppet::Error, /block must define at least one parameter/) end end it_should_behave_like 'all iterative functions argument checks', 'filter' it_should_behave_like 'all iterative functions hash handling', 'filter' end diff --git a/spec/unit/parser/methods/map_spec.rb b/spec/unit/functions/map_spec.rb similarity index 99% rename from spec/unit/parser/methods/map_spec.rb rename to spec/unit/functions/map_spec.rb index 34c5ac7b9..6f54648d9 100644 --- a/spec/unit/parser/methods/map_spec.rb +++ b/spec/unit/functions/map_spec.rb @@ -1,209 +1,209 @@ require 'puppet' require 'spec_helper' require 'puppet_spec/compiler' require 'matchers/resource' -require 'unit/parser/methods/shared' +require 'unit/functions/shared' describe 'the map method' do include PuppetSpec::Compiler include Matchers::Resource before :each do Puppet[:parser] = "future" end context "using future parser" do it 'map on an array (multiplying each value by 2)' do catalog = compile_to_catalog(<<-MANIFEST) $a = [1,2,3] $a.map |$x|{ $x*2}.each |$v|{ file { "/file_$v": ensure => present } } MANIFEST expect(catalog).to have_resource("File[/file_2]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_4]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_6]").with_parameter(:ensure, 'present') end it 'map on an enumerable type (multiplying each value by 2)' do catalog = compile_to_catalog(<<-MANIFEST) $a = Integer[1,3] $a.map |$x|{ $x*2}.each |$v|{ file { "/file_$v": ensure => present } } MANIFEST expect(catalog).to have_resource("File[/file_2]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_4]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_6]").with_parameter(:ensure, 'present') end it 'map on an integer (multiply each by 3)' do catalog = compile_to_catalog(<<-MANIFEST) 3.map |$x|{ $x*3}.each |$v|{ file { "/file_$v": ensure => present } } MANIFEST expect(catalog).to have_resource("File[/file_0]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_3]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_6]").with_parameter(:ensure, 'present') end it 'map on a string' do catalog = compile_to_catalog(<<-MANIFEST) $a = {a=>x, b=>y} "ab".map |$x|{$a[$x]}.each |$v|{ file { "/file_$v": ensure => present } } MANIFEST expect(catalog).to have_resource("File[/file_x]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_y]").with_parameter(:ensure, 'present') end it 'map on an array (multiplying value by 10 in even index position)' do catalog = compile_to_catalog(<<-MANIFEST) $a = [1,2,3] $a.map |$i, $x|{ if $i % 2 == 0 {$x} else {$x*10}}.each |$v|{ file { "/file_$v": ensure => present } } MANIFEST expect(catalog).to have_resource("File[/file_1]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_20]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_3]").with_parameter(:ensure, 'present') end it 'map on a hash selecting keys' do catalog = compile_to_catalog(<<-MANIFEST) $a = {'a'=>1,'b'=>2,'c'=>3} $a.map |$x|{ $x[0]}.each |$k|{ file { "/file_$k": ensure => present } } MANIFEST expect(catalog).to have_resource("File[/file_a]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_b]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_c]").with_parameter(:ensure, 'present') end it 'map on a hash selecting keys - using two block parameters' do catalog = compile_to_catalog(<<-MANIFEST) $a = {'a'=>1,'b'=>2,'c'=>3} $a.map |$k,$v|{ file { "/file_$k": ensure => present } } MANIFEST expect(catalog).to have_resource("File[/file_a]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_b]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_c]").with_parameter(:ensure, 'present') end it 'map on a hash using captures-last parameter' do catalog = compile_to_catalog(<<-MANIFEST) $a = {'a'=>present,'b'=>absent,'c'=>present} $a.map |*$kv|{ file { "/file_${kv[0]}": ensure => $kv[1] } } MANIFEST expect(catalog).to have_resource("File[/file_a]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_b]").with_parameter(:ensure, 'absent') expect(catalog).to have_resource("File[/file_c]").with_parameter(:ensure, 'present') end it 'each on a hash selecting value' do catalog = compile_to_catalog(<<-MANIFEST) $a = {'a'=>1,'b'=>2,'c'=>3} $a.map |$x|{ $x[1]}.each |$k|{ file { "/file_$k": ensure => present } } MANIFEST expect(catalog).to have_resource("File[/file_1]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_2]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_3]").with_parameter(:ensure, 'present') end it 'each on a hash selecting value - using two block parameters' do catalog = compile_to_catalog(<<-MANIFEST) $a = {'a'=>1,'b'=>2,'c'=>3} $a.map |$k,$v|{ file { "/file_$v": ensure => present } } MANIFEST expect(catalog).to have_resource("File[/file_1]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_2]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_3]").with_parameter(:ensure, 'present') end context "handles data type corner cases" do it "map gets values that are false" do catalog = compile_to_catalog(<<-MANIFEST) $a = [false,false] $a.map |$x| { $x }.each |$i, $v| { file { "/file_$i.$v": ensure => present } } MANIFEST expect(catalog).to have_resource("File[/file_0.false]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_1.false]").with_parameter(:ensure, 'present') end it "map gets values that are nil" do Puppet::Parser::Functions.newfunction(:nil_array, :type => :rvalue) do |args| [nil] end catalog = compile_to_catalog(<<-MANIFEST) $a = nil_array() $a.map |$x| { $x }.each |$i, $v| { file { "/file_$i.$v": ensure => present } } MANIFEST expect(catalog).to have_resource("File[/file_0.]").with_parameter(:ensure, 'present') end it "map gets values that are undef" do pending "Test is flawed, but has good intentions - should be rewritten when map has moved to new func API" # The test is broken because: # - a bug caused the given value to always be overridden by a given lambda default # - non existing variable results in nil / undef, which is transformed to empty string in the 3x func API # - when lambda is called, it gets an empty string, and it is then expected to use the default value # # This is not the semantics we want (only missing argument should trigger the default value). # Finally, it is not possible to test missing arguments with the map function since the call adapts itself # to the number of lambda parameters. (There is testing of this elsewhere). # # TODO: Rewrite map function, then test that undef / nil values are passed correctly to the lambda # catalog = compile_to_catalog(<<-MANIFEST) $a = [$does_not_exist] $a.map |$x = "something"| { $x }.each |$i, $v| { file { "/file_$i.$v": ensure => present } } MANIFEST catalog.resource(:file, "/file_0.something")['ensure'].should == 'present' end end context 'map checks arguments and' do it 'raises an error when block has more than 2 argument' do expect do compile_to_catalog(<<-MANIFEST) [1].map |$index, $x, $yikes|{ } MANIFEST end.to raise_error(Puppet::Error, /block must define at most two parameters/) end it 'raises an error when block has fewer than 1 argument' do expect do compile_to_catalog(<<-MANIFEST) [1].map || { } MANIFEST end.to raise_error(Puppet::Error, /block must define at least one parameter/) end end it_should_behave_like 'all iterative functions argument checks', 'map' it_should_behave_like 'all iterative functions hash handling', 'map' end end diff --git a/spec/unit/parser/methods/reduce_spec.rb b/spec/unit/functions/reduce_spec.rb similarity index 100% rename from spec/unit/parser/methods/reduce_spec.rb rename to spec/unit/functions/reduce_spec.rb diff --git a/spec/unit/parser/methods/shared.rb b/spec/unit/functions/shared.rb similarity index 72% rename from spec/unit/parser/methods/shared.rb rename to spec/unit/functions/shared.rb index 42cfd2359..57d2b72a7 100644 --- a/spec/unit/parser/methods/shared.rb +++ b/spec/unit/functions/shared.rb @@ -1,45 +1,45 @@ shared_examples_for 'all iterative functions hash handling' do |func| it 'passes a hash entry as an array of the key and value' do catalog = compile_to_catalog(<<-MANIFEST) {a=>1}.#{func} |$v| { notify { "${v[0]} ${v[1]}": } } MANIFEST catalog.resource(:notify, "a 1").should_not be_nil end end shared_examples_for 'all iterative functions argument checks' do |func| it 'raises an error when used against an unsupported type' do expect do compile_to_catalog(<<-MANIFEST) 3.14.#{func} |$v| { } MANIFEST end.to raise_error(Puppet::Error, /must be something enumerable/) end it 'raises an error when called with any parameters besides a block' do expect do compile_to_catalog(<<-MANIFEST) [1].#{func}(1) |$v| { } MANIFEST - end.to raise_error(Puppet::Error, /Wrong number of arguments/) + end.to raise_error(Puppet::Error, /mis-matched arguments.*expected.*arg count \{2\}.*actual.*arg count \{3\}/m) end it 'raises an error when called without a block' do expect do compile_to_catalog(<<-MANIFEST) [1].#{func}() MANIFEST - end.to raise_error(Puppet::Error, /Wrong number of arguments/) + end.to raise_error(Puppet::Error, /mis-matched arguments.*expected.*arg count \{2\}.*actual.*arg count \{1\}/m) end - it 'raises an error when called without a block' do + it 'raises an error when called with something that is not a block' do expect do compile_to_catalog(<<-MANIFEST) [1].#{func}(1) MANIFEST - end.to raise_error(Puppet::Error, /must be a parameterized block/) + end.to raise_error(Puppet::Error, /mis-matched arguments.*expected.*Callable.*actual(?!Callable\)).*/m) end end diff --git a/spec/unit/parser/methods/slice_spec.rb b/spec/unit/functions/slice_spec.rb similarity index 99% rename from spec/unit/parser/methods/slice_spec.rb rename to spec/unit/functions/slice_spec.rb index 262537045..945cae5c7 100644 --- a/spec/unit/parser/methods/slice_spec.rb +++ b/spec/unit/functions/slice_spec.rb @@ -1,149 +1,148 @@ require 'puppet' require 'spec_helper' require 'puppet_spec/compiler' -require 'rubygems' require 'matchers/resource' describe 'methods' do include PuppetSpec::Compiler include Matchers::Resource before :all do # enable switching back @saved_parser = Puppet[:parser] # These tests only work with future parser Puppet[:parser] = 'future' end after :all do # switch back to original Puppet[:parser] = @saved_parser end before :each do node = Puppet::Node.new("floppy", :environment => 'production') @compiler = Puppet::Parser::Compiler.new(node) @scope = Puppet::Parser::Scope.new(@compiler) @topscope = @scope.compiler.topscope @scope.parent = @topscope Puppet[:parser] = 'future' end context "should be callable on array as" do it 'slice with explicit parameters' do catalog = compile_to_catalog(<<-MANIFEST) $a = [1, present, 2, absent, 3, present] $a.slice(2) |$k,$v| { file { "/file_${$k}": ensure => $v } } MANIFEST expect(catalog).to have_resource("File[/file_1]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_2]").with_parameter(:ensure, 'absent') expect(catalog).to have_resource("File[/file_3]").with_parameter(:ensure, 'present') end it 'slice with captures last' do catalog = compile_to_catalog(<<-MANIFEST) $a = [1, present, 2, absent, 3, present] $a.slice(2) |*$kv| { file { "/file_${$kv[0]}": ensure => $kv[1] } } MANIFEST expect(catalog).to have_resource("File[/file_1]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_2]").with_parameter(:ensure, 'absent') expect(catalog).to have_resource("File[/file_3]").with_parameter(:ensure, 'present') end it 'slice with one parameter' do catalog = compile_to_catalog(<<-MANIFEST) $a = [1, present, 2, absent, 3, present] $a.slice(2) |$k| { file { "/file_${$k[0]}": ensure => $k[1] } } MANIFEST expect(catalog).to have_resource("File[/file_1]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_2]").with_parameter(:ensure, 'absent') expect(catalog).to have_resource("File[/file_3]").with_parameter(:ensure, 'present') end it 'slice with shorter last slice' do catalog = compile_to_catalog(<<-MANIFEST) $a = [1, present, 2, present, 3, absent] $a.slice(4) |$a, $b, $c, $d| { file { "/file_$a.$c": ensure => $b } } MANIFEST expect(catalog).to have_resource("File[/file_1.2]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_3.]").with_parameter(:ensure, 'absent') end end context "should be callable on hash as" do it 'slice with explicit parameters, missing are empty' do catalog = compile_to_catalog(<<-MANIFEST) $a = {1=>present, 2=>present, 3=>absent} $a.slice(2) |$a,$b| { file { "/file_${a[0]}.${b[0]}": ensure => $a[1] } } MANIFEST expect(catalog).to have_resource("File[/file_1.2]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_3.]").with_parameter(:ensure, 'absent') end end context "should be callable on enumerable types as" do it 'slice with integer range' do catalog = compile_to_catalog(<<-MANIFEST) $a = Integer[1,4] $a.slice(2) |$a,$b| { file { "/file_${a}.${b}": ensure => present } } MANIFEST expect(catalog).to have_resource("File[/file_1.2]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_3.4]").with_parameter(:ensure, 'present') end it 'slice with integer' do catalog = compile_to_catalog(<<-MANIFEST) 4.slice(2) |$a,$b| { file { "/file_${a}.${b}": ensure => present } } MANIFEST expect(catalog).to have_resource("File[/file_0.1]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_2.3]").with_parameter(:ensure, 'present') end it 'slice with string' do catalog = compile_to_catalog(<<-MANIFEST) 'abcd'.slice(2) |$a,$b| { file { "/file_${a}.${b}": ensure => present } } MANIFEST expect(catalog).to have_resource("File[/file_a.b]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_c.d]").with_parameter(:ensure, 'present') end end context "when called without a block" do it "should produce an array with the result" do catalog = compile_to_catalog(<<-MANIFEST) $a = [1, present, 2, absent, 3, present] $a.slice(2).each |$k| { file { "/file_${$k[0]}": ensure => $k[1] } } MANIFEST expect(catalog).to have_resource("File[/file_1]").with_parameter(:ensure, 'present') expect(catalog).to have_resource("File[/file_2]").with_parameter(:ensure, 'absent') expect(catalog).to have_resource("File[/file_3]").with_parameter(:ensure, 'present') end end end diff --git a/spec/unit/pops/evaluator/evaluating_parser_spec.rb b/spec/unit/pops/evaluator/evaluating_parser_spec.rb index 8477aa0f2..5ce44e20a 100644 --- a/spec/unit/pops/evaluator/evaluating_parser_spec.rb +++ b/spec/unit/pops/evaluator/evaluating_parser_spec.rb @@ -1,1214 +1,1248 @@ require 'spec_helper' require 'puppet/pops' require 'puppet/pops/evaluator/evaluator_impl' require 'puppet/loaders' require 'puppet_spec/pops' require 'puppet_spec/scope' require 'puppet/parser/e4_parser_adapter' # relative to this spec file (./) does not work as this file is loaded by rspec #require File.join(File.dirname(__FILE__), '/evaluator_rspec_helper') describe 'Puppet::Pops::Evaluator::EvaluatorImpl' do include PuppetSpec::Pops include PuppetSpec::Scope before(:each) do Puppet[:strict_variables] = true - # These must be set since the is 3x logic that triggers on these even if the tests are explicit - # about selection of parser and evaluator + # These must be set since the 3x logic switches some behaviors on these even if the tests explicitly + # use the 4x parser and evaluator. # Puppet[:parser] = 'future' Puppet[:evaluator] = 'future' + # Puppetx cannot be loaded until the correct parser has been set (injector is turned off otherwise) require 'puppetx' + + # Tests needs a known configuration of node/scope/compiler since it parses and evaluates + # snippets as the compiler will evaluate them, butwithout the overhead of compiling a complete + # catalog for each tested expression. + # + @parser = Puppet::Pops::Parser::EvaluatingParser::Transitional.new + @node = Puppet::Node.new('node.example.com') + @node.environment = Puppet::Node::Environment.create(:testing, []) + @compiler = Puppet::Parser::Compiler.new(@node) + @scope = Puppet::Parser::Scope.new(@compiler) + @scope.source = Puppet::Resource::Type.new(:node, 'node.example.com') + @scope.parent = @compiler.topscope end - let(:parser) { Puppet::Pops::Parser::EvaluatingParser::Transitional.new } - let(:node) { 'node.example.com' } - let(:scope) { s = create_test_scope_for_node(node); s } + let(:parser) { @parser } + let(:scope) { @scope } types = Puppet::Pops::Types::TypeFactory context "When evaluator evaluates literals" do { "1" => 1, "010" => 8, "0x10" => 16, "3.14" => 3.14, "0.314e1" => 3.14, "31.4e-1" => 3.14, "'1'" => '1', "'banana'" => 'banana', '"banana"' => 'banana', "banana" => 'banana', "banana::split" => 'banana::split', "false" => false, "true" => true, "Array" => types.array_of_data(), "/.*/" => /.*/ }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end end context "When the evaluator evaluates Lists and Hashes" do { "[]" => [], "[1,2,3]" => [1,2,3], "[1,[2.0, 2.1, [2.2]],[3.0, 3.1]]" => [1,[2.0, 2.1, [2.2]],[3.0, 3.1]], "[2 + 2]" => [4], "[1,2,3] == [1,2,3]" => true, "[1,2,3] != [2,3,4]" => true, "[1,2,3] == [2,2,3]" => false, "[1,2,3] != [1,2,3]" => false, "[1,2,3][2]" => 3, "[1,2,3] + [4,5]" => [1,2,3,4,5], "[1,2,3] + [[4,5]]" => [1,2,3,[4,5]], "[1,2,3] + 4" => [1,2,3,4], "[1,2,3] << [4,5]" => [1,2,3,[4,5]], "[1,2,3] << {'a' => 1, 'b'=>2}" => [1,2,3,{'a' => 1, 'b'=>2}], "[1,2,3] << 4" => [1,2,3,4], "[1,2,3,4] - [2,3]" => [1,4], "[1,2,3,4] - [2,5]" => [1,3,4], "[1,2,3,4] - 2" => [1,3,4], "[1,2,3,[2],4] - 2" => [1,3,[2],4], "[1,2,3,[2,3],4] - [[2,3]]" => [1,2,3,4], "[1,2,3,3,2,4,2,3] - [2,3]" => [1,4], "[1,2,3,['a',1],['b',2]] - {'a' => 1, 'b'=>2}" => [1,2,3], "[1,2,3,{'a'=>1,'b'=>2}] - [{'a' => 1, 'b'=>2}]" => [1,2,3], }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { "[1,2,3] + {'a' => 1, 'b'=>2}" => [1,2,3,['a',1],['b',2]], }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do # This test must be done with match_array since the order of the hash # is undefined and Ruby 1.8.7 and 1.9.3 produce different results. expect(parser.evaluate_string(scope, source, __FILE__)).to match_array(result) end end { "[1,2,3][a]" => :error, }.each do |source, result| it "should parse and raise error for '#{source}'" do expect { parser.evaluate_string(scope, source, __FILE__) }.to raise_error(Puppet::ParseError) end end { "{}" => {}, "{'a'=>1,'b'=>2}" => {'a'=>1,'b'=>2}, "{'a'=>1,'b'=>{'x'=>2.1,'y'=>2.2}}" => {'a'=>1,'b'=>{'x'=>2.1,'y'=>2.2}}, "{'a'=> 2 + 2}" => {'a'=> 4}, "{'a'=> 1, 'b'=>2} == {'a'=> 1, 'b'=>2}" => true, "{'a'=> 1, 'b'=>2} != {'x'=> 1, 'b'=>2}" => true, "{'a'=> 1, 'b'=>2} == {'a'=> 2, 'b'=>3}" => false, "{'a'=> 1, 'b'=>2} != {'a'=> 1, 'b'=>2}" => false, "{a => 1, b => 2}[b]" => 2, "{2+2 => sum, b => 2}[4]" => 'sum', "{'a'=>1, 'b'=>2} + {'c'=>3}" => {'a'=>1,'b'=>2,'c'=>3}, "{'a'=>1, 'b'=>2} + {'b'=>3}" => {'a'=>1,'b'=>3}, "{'a'=>1, 'b'=>2} + ['c', 3, 'b', 3]" => {'a'=>1,'b'=>3, 'c'=>3}, "{'a'=>1, 'b'=>2} + [['c', 3], ['b', 3]]" => {'a'=>1,'b'=>3, 'c'=>3}, "{'a'=>1, 'b'=>2} - {'b' => 3}" => {'a'=>1}, "{'a'=>1, 'b'=>2, 'c'=>3} - ['b', 'c']" => {'a'=>1}, "{'a'=>1, 'b'=>2, 'c'=>3} - 'c'" => {'a'=>1, 'b'=>2}, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { "{'a' => 1, 'b'=>2} << 1" => :error, }.each do |source, result| it "should parse and raise error for '#{source}'" do expect { parser.evaluate_string(scope, source, __FILE__) }.to raise_error(Puppet::ParseError) end end end context "When the evaluator perform comparisons" do { "'a' == 'a'" => true, "'a' == 'b'" => false, "'a' != 'a'" => false, "'a' != 'b'" => true, "'a' < 'b' " => true, "'a' < 'a' " => false, "'b' < 'a' " => false, "'a' <= 'b'" => true, "'a' <= 'a'" => true, "'b' <= 'a'" => false, "'a' > 'b' " => false, "'a' > 'a' " => false, "'b' > 'a' " => true, "'a' >= 'b'" => false, "'a' >= 'a'" => true, "'b' >= 'a'" => true, "'a' == 'A'" => true, "'a' != 'A'" => false, "'a' > 'A'" => false, "'a' >= 'A'" => true, "'A' < 'a'" => false, "'A' <= 'a'" => true, "1 == 1" => true, "1 == 2" => false, "1 != 1" => false, "1 != 2" => true, "1 < 2 " => true, "1 < 1 " => false, "2 < 1 " => false, "1 <= 2" => true, "1 <= 1" => true, "2 <= 1" => false, "1 > 2 " => false, "1 > 1 " => false, "2 > 1 " => true, "1 >= 2" => false, "1 >= 1" => true, "2 >= 1" => true, "1 == 1.0 " => true, "1 < 1.1 " => true, "'1' < 1.1" => true, "1.0 == 1 " => true, "1.0 < 2 " => true, "1.0 < 'a'" => true, "'1.0' < 1.1" => true, "'1.0' < 'a'" => true, "'1.0' < '' " => true, "'1.0' < ' '" => true, "'a' > '1.0'" => true, "/.*/ == /.*/ " => true, "/.*/ != /a.*/" => true, "true == true " => true, "false == false" => true, "true == false" => false, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { "'a' =~ /.*/" => true, "'a' =~ '.*'" => true, "/.*/ != /a.*/" => true, "'a' !~ /b.*/" => true, "'a' !~ 'b.*'" => true, '$x = a; a =~ "$x.*"' => true, "a =~ Pattern['a.*']" => true, "a =~ Regexp['a.*']" => false, # String is not subtype of Regexp. PUP-957 "$x = /a.*/ a =~ $x" => true, "$x = Pattern['a.*'] a =~ $x" => true, "1 =~ Integer" => true, "1 !~ Integer" => false, "[1,2,3] =~ Array[Integer[1,10]]" => true, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { "666 =~ /6/" => :error, "[a] =~ /a/" => :error, "{a=>1} =~ /a/" => :error, "/a/ =~ /a/" => :error, "Array =~ /A/" => :error, }.each do |source, result| it "should parse and raise error for '#{source}'" do expect { parser.evaluate_string(scope, source, __FILE__) }.to raise_error(Puppet::ParseError) end end { "1 in [1,2,3]" => true, "4 in [1,2,3]" => false, "a in {x=>1, a=>2}" => true, "z in {x=>1, a=>2}" => false, "ana in bananas" => true, "xxx in bananas" => false, "/ana/ in bananas" => true, "/xxx/ in bananas" => false, "ANA in bananas" => false, # ANA is a type, not a String "String[1] in bananas" => false, # Philosophically true though :-) "'ANA' in bananas" => true, "ana in 'BANANAS'" => true, "/ana/ in 'BANANAS'" => false, "/ANA/ in 'BANANAS'" => true, "xxx in 'BANANAS'" => false, "[2,3] in [1,[2,3],4]" => true, "[2,4] in [1,[2,3],4]" => false, "[a,b] in ['A',['A','B'],'C']" => true, "[x,y] in ['A',['A','B'],'C']" => false, "a in {a=>1}" => true, "x in {a=>1}" => false, "'A' in {a=>1}" => true, "'X' in {a=>1}" => false, "a in {'A'=>1}" => true, "x in {'A'=>1}" => false, "/xxx/ in {'aaaxxxbbb'=>1}" => true, "/yyy/ in {'aaaxxxbbb'=>1}" => false, "15 in [1, 0xf]" => true, "15 in [1, '0xf']" => true, "'15' in [1, 0xf]" => true, "15 in [1, 115]" => false, "1 in [11, '111']" => false, "'1' in [11, '111']" => false, "Array[Integer] in [2, 3]" => false, "Array[Integer] in [2, [3, 4]]" => true, "Array[Integer] in [2, [a, 4]]" => false, "Integer in { 2 =>'a'}" => true, "Integer[5,10] in [1,5,3]" => true, "Integer[5,10] in [1,2,3]" => false, "Integer in {'a'=>'a'}" => false, "Integer in {'a'=>1}" => false, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { 'Object' => ['Data', 'Scalar', 'Numeric', 'Integer', 'Float', 'Boolean', 'String', 'Pattern', 'Collection', 'Array', 'Hash', 'CatalogEntry', 'Resource', 'Class', 'Undef', 'File', 'NotYetKnownResourceType'], # Note, Data > Collection is false (so not included) 'Data' => ['Scalar', 'Numeric', 'Integer', 'Float', 'Boolean', 'String', 'Pattern', 'Array', 'Hash',], 'Scalar' => ['Numeric', 'Integer', 'Float', 'Boolean', 'String', 'Pattern'], 'Numeric' => ['Integer', 'Float'], 'CatalogEntry' => ['Class', 'Resource', 'File', 'NotYetKnownResourceType'], 'Integer[1,10]' => ['Integer[2,3]'], }.each do |general, specials| specials.each do |special | it "should compute that #{general} > #{special}" do parser.evaluate_string(scope, "#{general} > #{special}", __FILE__).should == true end it "should compute that #{special} < #{general}" do parser.evaluate_string(scope, "#{special} < #{general}", __FILE__).should == true end it "should compute that #{general} != #{special}" do parser.evaluate_string(scope, "#{special} != #{general}", __FILE__).should == true end end end { 'Integer[1,10] > Integer[2,3]' => true, 'Integer[1,10] == Integer[2,3]' => false, 'Integer[1,10] > Integer[0,5]' => false, 'Integer[1,10] > Integer[1,10]' => false, 'Integer[1,10] >= Integer[1,10]' => true, 'Integer[1,10] == Integer[1,10]' => true, }.each do |source, result| it "should parse and evaluate the integer range comparison expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end end context "When the evaluator performs arithmetic" do context "on Integers" do { "2+2" => 4, "2 + 2" => 4, "7 - 3" => 4, "6 * 3" => 18, "6 / 3" => 2, "6 % 3" => 0, "10 % 3" => 1, "-(6/3)" => -2, "-6/3 " => -2, "8 >> 1" => 4, "8 << 1" => 16, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end context "on Floats" do { "2.2 + 2.2" => 4.4, "7.7 - 3.3" => 4.4, "6.1 * 3.1" => 18.91, "6.6 / 3.3" => 2.0, "-(6.0/3.0)" => -2.0, "-6.0/3.0 " => -2.0, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { "3.14 << 2" => :error, "3.14 >> 2" => :error, "6.6 % 3.3" => 0.0, "10.0 % 3.0" => 1.0, }.each do |source, result| it "should parse and raise error for '#{source}'" do expect { parser.evaluate_string(scope, source, __FILE__) }.to raise_error(Puppet::ParseError) end end end context "on strings requiring boxing to Numeric" do { "'2' + '2'" => 4, "'2.2' + '2.2'" => 4.4, "'0xF7' + '010'" => 0xFF, "'0xF7' + '0x8'" => 0xFF, "'0367' + '010'" => 0xFF, "'012.3' + '010'" => 20.3, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { "'0888' + '010'" => :error, "'0xWTF' + '010'" => :error, "'0x12.3' + '010'" => :error, "'0x12.3' + '010'" => :error, }.each do |source, result| it "should parse and raise error for '#{source}'" do expect { parser.evaluate_string(scope, source, __FILE__) }.to raise_error(Puppet::ParseError) end end end end end # arithmetic context "When the evaluator evaluates assignment" do { "$a = 5" => 5, "$a = 5; $a" => 5, "$a = 5; $b = 6; $a" => 5, "$a = $b = 5; $a == $b" => true, "$a = [1,2,3]; [x].map |$x| { $a += x; $a }" => [[1,2,3,'x']], "$a = [a,x,c]; [x].map |$x| { $a -= x; $a }" => [['a','c']], }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { "[a,b,c] = [1,2,3]; $a == 1 and $b == 2 and $c == 3" => :error, "[a,b,c] = {b=>2,c=>3,a=>1}; $a == 1 and $b == 2 and $c == 3" => :error, "$a = [1,2,3]; [x].collect |$x| { [a] += x; $a }" => :error, "$a = [a,x,c]; [x].collect |$x| { [a] -= x; $a }" => :error, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do expect { parser.evaluate_string(scope, source, __FILE__)}.to raise_error(Puppet::ParseError) end end end context "When the evaluator evaluates conditionals" do { "if true {5}" => 5, "if false {5}" => nil, "if false {2} else {5}" => 5, "if false {2} elsif true {5}" => 5, "if false {2} elsif false {5}" => nil, "unless false {5}" => 5, "unless true {5}" => nil, "unless true {2} else {5}" => 5, "$a = if true {5} $a" => 5, "$a = if false {5} $a" => nil, "$a = if false {2} else {5} $a" => 5, "$a = if false {2} elsif true {5} $a" => 5, "$a = if false {2} elsif false {5} $a" => nil, "$a = unless false {5} $a" => 5, "$a = unless true {5} $a" => nil, "$a = unless true {2} else {5} $a" => 5, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { "case 1 { 1 : { yes } }" => 'yes', "case 2 { 1,2,3 : { yes} }" => 'yes', "case 2 { 1,3 : { no } 2: { yes} }" => 'yes', "case 2 { 1,3 : { no } 5: { no } default: { yes }}" => 'yes', "case 2 { 1,3 : { no } 5: { no } }" => nil, "case 'banana' { 1,3 : { no } /.*ana.*/: { yes } }" => 'yes', "case 'banana' { /.*(ana).*/: { $1 } }" => 'ana', "case [1] { Array : { yes } }" => 'yes', "case [1] { Array[String] : { no } Array[Integer]: { yes } }" => 'yes', "case 1 { Integer : { yes } Type[Integer] : { no } }" => 'yes', "case Integer { Integer : { no } Type[Integer] : { yes } }" => 'yes', # supports unfold "case ringo { *[paul, john, ringo, george] : { 'beatle' } }" => 'beatle', }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { "2 ? { 1 => no, 2 => yes}" => 'yes', "3 ? { 1 => no, 2 => no, default => yes }" => 'yes', "3 ? { 1 => no, default => yes, 3 => no }" => 'no', "3 ? { 1 => no, 3 => no, default => yes }" => 'no', "4 ? { 1 => no, default => yes, 3 => no }" => 'yes', "4 ? { 1 => no, 3 => no, default => yes }" => 'yes', "'banana' ? { /.*(ana).*/ => $1 }" => 'ana', "[2] ? { Array[String] => yes, Array => yes}" => 'yes', "ringo ? *[paul, john, ringo, george] => 'beatle'" => 'beatle', }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end it 'fails if a selector does not match' do expect{parser.evaluate_string(scope, "2 ? 3 => 4")}.to raise_error(/No matching entry for selector parameter with value '2'/) end end context "When evaluator evaluated unfold" do { "*[1,2,3]" => [1,2,3], "*1" => [1], "*'a'" => ['a'] }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end it "should parse and evaluate the expression '*{a=>10, b=>20} to [['a',10],['b',20]]" do result = parser.evaluate_string(scope, '*{a=>10, b=>20}', __FILE__) expect(result).to include(['a', 10]) expect(result).to include(['b', 20]) end end context "When evaluator performs [] operations" do { "[1,2,3][0]" => 1, "[1,2,3][2]" => 3, "[1,2,3][3]" => nil, "[1,2,3][-1]" => 3, "[1,2,3][-2]" => 2, "[1,2,3][-4]" => nil, "[1,2,3,4][0,2]" => [1,2], "[1,2,3,4][1,3]" => [2,3,4], "[1,2,3,4][-2,2]" => [3,4], "[1,2,3,4][-3,2]" => [2,3], "[1,2,3,4][3,5]" => [4], "[1,2,3,4][5,2]" => [], "[1,2,3,4][0,-1]" => [1,2,3,4], "[1,2,3,4][0,-2]" => [1,2,3], "[1,2,3,4][0,-4]" => [1], "[1,2,3,4][0,-5]" => [], "[1,2,3,4][-5,2]" => [1], "[1,2,3,4][-5,-3]" => [1,2], "[1,2,3,4][-6,-3]" => [1,2], "[1,2,3,4][2,-3]" => [], "[1,*[2,3],4]" => [1,2,3,4], "[1,*[2,3],4][1]" => 2, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { "{a=>1, b=>2, c=>3}[a]" => 1, "{a=>1, b=>2, c=>3}[c]" => 3, "{a=>1, b=>2, c=>3}[x]" => nil, "{a=>1, b=>2, c=>3}[c,b]" => [3,2], "{a=>1, b=>2, c=>3}[a,b,c]" => [1,2,3], "{a=>{b=>{c=>'it works'}}}[a][b][c]" => 'it works', "$a = {undef => 10} $a[free_lunch]" => nil, "$a = {undef => 10} $a[undef]" => 10, "$a = {undef => 10} $a[$a[free_lunch]]" => 10, "$a = {} $a[free_lunch] == undef" => true, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { "'abc'[0]" => 'a', "'abc'[2]" => 'c', "'abc'[-1]" => 'c', "'abc'[-2]" => 'b', "'abc'[-3]" => 'a', "'abc'[-4]" => '', "'abc'[3]" => '', "abc[0]" => 'a', "abc[2]" => 'c', "abc[-1]" => 'c', "abc[-2]" => 'b', "abc[-3]" => 'a', "abc[-4]" => '', "abc[3]" => '', "'abcd'[0,2]" => 'ab', "'abcd'[1,3]" => 'bcd', "'abcd'[-2,2]" => 'cd', "'abcd'[-3,2]" => 'bc', "'abcd'[3,5]" => 'd', "'abcd'[5,2]" => '', "'abcd'[0,-1]" => 'abcd', "'abcd'[0,-2]" => 'abc', "'abcd'[0,-4]" => 'a', "'abcd'[0,-5]" => '', "'abcd'[-5,2]" => 'a', "'abcd'[-5,-3]" => 'ab', "'abcd'[-6,-3]" => 'ab', "'abcd'[2,-3]" => '', }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end # Type operations (full set tested by tests covering type calculator) { "Array[Integer]" => types.array_of(types.integer), "Array[Integer,1]" => types.constrain_size(types.array_of(types.integer),1, :default), "Array[Integer,1,2]" => types.constrain_size(types.array_of(types.integer),1, 2), "Array[Integer,Integer[1,2]]" => types.constrain_size(types.array_of(types.integer),1, 2), "Array[Integer,Integer[1]]" => types.constrain_size(types.array_of(types.integer),1, :default), "Hash[Integer,Integer]" => types.hash_of(types.integer, types.integer), "Hash[Integer,Integer,1]" => types.constrain_size(types.hash_of(types.integer, types.integer),1, :default), "Hash[Integer,Integer,1,2]" => types.constrain_size(types.hash_of(types.integer, types.integer),1, 2), "Hash[Integer,Integer,Integer[1,2]]" => types.constrain_size(types.hash_of(types.integer, types.integer),1, 2), "Hash[Integer,Integer,Integer[1]]" => types.constrain_size(types.hash_of(types.integer, types.integer),1, :default), "Resource[File]" => types.resource('File'), "Resource['File']" => types.resource(types.resource('File')), "File[foo]" => types.resource('file', 'foo'), "File[foo, bar]" => [types.resource('file', 'foo'), types.resource('file', 'bar')], "Pattern[a, /b/, Pattern[c], Regexp[d]]" => types.pattern('a', 'b', 'c', 'd'), "String[1,2]" => types.constrain_size(types.string,1, 2), "String[Integer[1,2]]" => types.constrain_size(types.string,1, 2), "String[Integer[1]]" => types.constrain_size(types.string,1, :default), }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end # LHS where [] not supported, and missing key(s) { "Array[]" => :error, "'abc'[]" => :error, "Resource[]" => :error, "File[]" => :error, "String[]" => :error, "1[]" => :error, "3.14[]" => :error, "/.*/[]" => :error, "$a=[1] $a[]" => :error, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do expect { parser.evaluate_string(scope, source, __FILE__)}.to raise_error(/Syntax error/) end end # Errors when wrong number/type of keys are used { "Array[0]" => 'Array-Type[] arguments must be types. Got Fixnum', "Hash[0]" => 'Hash-Type[] arguments must be types. Got Fixnum', "Hash[Integer, 0]" => 'Hash-Type[] arguments must be types. Got Fixnum', "Array[Integer,1,2,3]" => 'Array-Type[] accepts 1 to 3 arguments. Got 4', "Array[Integer,String]" => "A Type's size constraint arguments must be a single Integer type, or 1-2 integers (or default). Got a String-Type", "Hash[Integer,String, 1,2,3]" => 'Hash-Type[] accepts 1 to 4 arguments. Got 5', "'abc'[x]" => "The value 'x' cannot be converted to Numeric", "'abc'[1.0]" => "A String[] cannot use Float where Integer is expected", "'abc'[1,2,3]" => "String supports [] with one or two arguments. Got 3", "Resource[0]" => 'First argument to Resource[] must be a resource type or a String. Got Fixnum', "Resource[a, 0]" => 'Error creating type specialization of a Resource-Type, Cannot use Fixnum where String is expected', "File[0]" => 'Error creating type specialization of a File-Type, Cannot use Fixnum where String is expected', "String[a]" => "A Type's size constraint arguments must be a single Integer type, or 1-2 integers (or default). Got a String", "Pattern[0]" => 'Error creating type specialization of a Pattern-Type, Cannot use Fixnum where String or Regexp or Pattern-Type or Regexp-Type is expected', "Regexp[0]" => 'Error creating type specialization of a Regexp-Type, Cannot use Fixnum where String or Regexp is expected', "Regexp[a,b]" => 'A Regexp-Type[] accepts 1 argument. Got 2', "true[0]" => "Operator '[]' is not applicable to a Boolean", "1[0]" => "Operator '[]' is not applicable to an Integer", "3.14[0]" => "Operator '[]' is not applicable to a Float", "/.*/[0]" => "Operator '[]' is not applicable to a Regexp", "[1][a]" => "The value 'a' cannot be converted to Numeric", "[1][0.0]" => "An Array[] cannot use Float where Integer is expected", "[1]['0.0']" => "An Array[] cannot use Float where Integer is expected", "[1,2][1, 0.0]" => "An Array[] cannot use Float where Integer is expected", "[1,2][1.0, -1]" => "An Array[] cannot use Float where Integer is expected", "[1,2][1, -1.0]" => "An Array[] cannot use Float where Integer is expected", }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do expect { parser.evaluate_string(scope, source, __FILE__)}.to raise_error(Regexp.new(Regexp.quote(result))) end end context "on catalog types" do it "[n] gets resource parameter [n]" do source = "notify { 'hello': message=>'yo'} Notify[hello][message]" parser.evaluate_string(scope, source, __FILE__).should == 'yo' end it "[n] gets class parameter [n]" do source = "class wonka($produces='chocolate'){ } include wonka Class[wonka][produces]" # This is more complicated since it needs to run like 3.x and do an import_ast adapted_parser = Puppet::Parser::E4ParserAdapter.new adapted_parser.file = __FILE__ ast = adapted_parser.parse(source) Puppet.override({:global_scope => scope}, "test") do scope.known_resource_types.import_ast(ast, '') ast.code.safeevaluate(scope).should == 'chocolate' end end # Resource default and override expressions and resource parameter access with [] { # Properties "notify { id: message=>explicit} Notify[id][message]" => "explicit", "Notify { message=>by_default} notify {foo:} Notify[foo][message]" => "by_default", "notify {foo:} Notify[foo]{message =>by_override} Notify[foo][message]" => "by_override", # Parameters "notify { id: withpath=>explicit} Notify[id][withpath]" => "explicit", "Notify { withpath=>by_default } notify { foo: } Notify[foo][withpath]" => "by_default", "notify {foo:} Notify[foo]{withpath=>by_override} Notify[foo][withpath]" => "by_override", # Metaparameters "notify { foo: tag => evoe} Notify[foo][tag]" => "evoe", # Does not produce the defaults for tag parameter (title, type or names of scopes) "notify { foo: } Notify[foo][tag]" => nil, # But a default may be specified on the type "Notify { tag=>by_default } notify { foo: } Notify[foo][tag]" => "by_default", "Notify { tag=>by_default } notify { foo: } Notify[foo]{ tag=>by_override } Notify[foo][tag]" => "by_override", }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end # Virtual and realized resource default and overridden resource parameter access with [] { # Properties "@notify { id: message=>explicit } Notify[id][message]" => "explicit", "@notify { id: message=>explicit } realize Notify[id] Notify[id][message]" => "explicit", "Notify { message=>by_default } @notify { id: } Notify[id][message]" => "by_default", "Notify { message=>by_default } @notify { id: tag=>thisone } Notify <| tag == thisone |>; Notify[id][message]" => "by_default", "@notify { id: } Notify[id]{message=>by_override} Notify[id][message]" => "by_override", # Parameters "@notify { id: withpath=>explicit } Notify[id][withpath]" => "explicit", "Notify { withpath=>by_default } @notify { id: } Notify[id][withpath]" => "by_default", "@notify { id: } realize Notify[id] Notify[id]{withpath=>by_override} Notify[id][withpath]" => "by_override", # Metaparameters "@notify { id: tag=>explicit } Notify[id][tag]" => "explicit", }.each do |source, result| it "parses and evaluates virtual and realized resources in the expression '#{source}' to #{result}" do expect(parser.evaluate_string(scope, source, __FILE__)).to eq(result) end end # Exported resource attributes { "@@notify { id: message=>explicit } Notify[id][message]" => "explicit", "@@notify { id: message=>explicit, tag=>thisone } Notify <<| tag == thisone |>> Notify[id][message]" => "explicit", }.each do |source, result| it "parses and evaluates exported resources in the expression '#{source}' to #{result}" do expect(parser.evaluate_string(scope, source, __FILE__)).to eq(result) end end # Resource default and override expressions and resource parameter access error conditions { "notify { xid: message=>explicit} Notify[id][message]" => /Resource not found/, "notify { id: message=>explicit} Notify[id][mustard]" => /does not have a parameter called 'mustard'/, # NOTE: these meta-esque parameters are not recognized as such "notify { id: message=>explicit} Notify[id][title]" => /does not have a parameter called 'title'/, "notify { id: message=>explicit} Notify[id]['type']" => /does not have a parameter called 'type'/, "notify { id: message=>explicit } Notify[id]{message=>override}" => /'message' is already set on Notify\[id\]/ }.each do |source, result| it "should parse '#{source}' and raise error matching #{result}" do expect { parser.evaluate_string(scope, source, __FILE__)}.to raise_error(result) end end context 'with errors' do { "Class['fail-whale']" => /Illegal name/, "Class[0]" => /An Integer cannot be used where a String is expected/, "Class[/.*/]" => /A Regexp cannot be used where a String is expected/, "Class[4.1415]" => /A Float cannot be used where a String is expected/, "Class[Integer]" => /An Integer-Type cannot be used where a String is expected/, "Class[File['tmp']]" => /A File\['tmp'\] Resource-Reference cannot be used where a String is expected/, }.each do | source, error_pattern| it "an error is flagged for '#{source}'" do expect { parser.evaluate_string(scope, source, __FILE__)}.to raise_error(error_pattern) end end end end # end [] operations end context "When the evaluator performs boolean operations" do { "true and true" => true, "false and true" => false, "true and false" => false, "false and false" => false, "true or true" => true, "false or true" => true, "true or false" => true, "false or false" => false, "! true" => false, "!! true" => true, "!! false" => false, "! 'x'" => false, "! ''" => false, "! undef" => true, "! [a]" => false, "! []" => false, "! {a=>1}" => false, "! {}" => false, "true and false and '0xwtf' + 1" => false, "false or true or '0xwtf' + 1" => true, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { "false || false || '0xwtf' + 1" => :error, }.each do |source, result| it "should parse and raise error for '#{source}'" do expect { parser.evaluate_string(scope, source, __FILE__) }.to raise_error(Puppet::ParseError) end end end context "When evaluator performs operations on literal undef" do it "computes non existing hash lookup as undef" do parser.evaluate_string(scope, "{a => 1}[b] == undef", __FILE__).should == true parser.evaluate_string(scope, "undef == {a => 1}[b]", __FILE__).should == true end end context "When evaluator performs calls" do - around(:each) do |example| - Puppet.override(:loaders => Puppet::Pops::Loaders.new(Puppet::Node::Environment.create(:testing, []))) do - example.run - end - end let(:populate) do parser.evaluate_string(scope, "$a = 10 $b = [1,2,3]") end { 'sprintf( "x%iy", $a )' => "x10y", # unfolds 'sprintf( *["x%iy", $a] )' => "x10y", '"x%iy".sprintf( $a )' => "x10y", '$b.reduce |$memo,$x| { $memo + $x }' => 6, 'reduce($b) |$memo,$x| { $memo + $x }' => 6, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do populate parser.evaluate_string(scope, source, __FILE__).should == result end end { '"value is ${a*2} yo"' => :error, }.each do |source, result| it "should parse and raise error for '#{source}'" do expect { parser.evaluate_string(scope, source, __FILE__) }.to raise_error(Puppet::ParseError) end end it "provides location information on error in unparenthesized call logic" do expect{parser.evaluate_string(scope, "include non_existing_class", __FILE__)}.to raise_error(Puppet::ParseError, /line 1\:1/) end it 'defaults can be given in a lambda and used only when arg is missing' do - env_loader = Puppet.lookup(:loaders).public_environment_loader + env_loader = @compiler.loaders.public_environment_loader fc = Puppet::Functions.create_function(:test) do dispatch :test do param 'Integer', 'count' required_block_param end def test(count, block) block.call({}, *[].fill(10, 0, count)) end end the_func = fc.new({}, env_loader) env_loader.add_entry(:function, 'test', the_func, __FILE__) expect(parser.evaluate_string(scope, "test(1) |$x, $y=20| { $x + $y}")).to eql(30) expect(parser.evaluate_string(scope, "test(2) |$x, $y=20| { $x + $y}")).to eql(20) end it 'a given undef does not select the default value' do - env_loader = Puppet.lookup(:loaders).public_environment_loader + env_loader = @compiler.loaders.public_environment_loader fc = Puppet::Functions.create_function(:test) do dispatch :test do param 'Object', 'lambda_arg' required_block_param end def test(lambda_arg, block) block.call({}, lambda_arg) end end the_func = fc.new({}, env_loader) env_loader.add_entry(:function, 'test', the_func, __FILE__) expect(parser.evaluate_string(scope, "test(undef) |$x=20| { $x == undef}")).to eql(true) end end context "When evaluator performs string interpolation" do let(:populate) do parser.evaluate_string(scope, "$a = 10 $b = [1,2,3]") end { '"value is $a yo"' => "value is 10 yo", '"value is \$a yo"' => "value is $a yo", '"value is ${a} yo"' => "value is 10 yo", '"value is \${a} yo"' => "value is ${a} yo", '"value is ${$a} yo"' => "value is 10 yo", '"value is ${$a*2} yo"' => "value is 20 yo", '"value is ${sprintf("x%iy",$a)} yo"' => "value is x10y yo", '"value is ${"x%iy".sprintf($a)} yo"' => "value is x10y yo", '"value is ${[1,2,3]} yo"' => "value is [1, 2, 3] yo", '"value is ${/.*/} yo"' => "value is /.*/ yo", '$x = undef "value is $x yo"' => "value is yo", '$x = default "value is $x yo"' => "value is default yo", '$x = Array[Integer] "value is $x yo"' => "value is Array[Integer] yo", '"value is ${Array[Integer]} yo"' => "value is Array[Integer] yo", }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do populate parser.evaluate_string(scope, source, __FILE__).should == result end end it "should parse and evaluate an interpolation of a hash" do source = '"value is ${{a=>1,b=>2}} yo"' # This test requires testing against two options because a hash to string # produces a result that is unordered hashstr = {'a' => 1, 'b' => 2}.to_s alt_results = ["value is {a => 1, b => 2} yo", "value is {b => 2, a => 1} yo" ] populate parse_result = parser.evaluate_string(scope, source, __FILE__) alt_results.include?(parse_result).should == true end it 'should accept a variable with leading underscore when used directly' do source = '$_x = 10; "$_x"' expect(parser.evaluate_string(scope, source, __FILE__)).to eql('10') end it 'should accept a variable with leading underscore when used as an expression' do source = '$_x = 10; "${_x}"' expect(parser.evaluate_string(scope, source, __FILE__)).to eql('10') end { '"value is ${a*2} yo"' => :error, }.each do |source, result| it "should parse and raise error for '#{source}'" do expect { parser.evaluate_string(scope, source, __FILE__) }.to raise_error(Puppet::ParseError) end end end context "When evaluating variables" do context "that are non existing an error is raised for" do it "unqualified variable" do expect { parser.evaluate_string(scope, "$quantum_gravity", __FILE__) }.to raise_error(/Unknown variable/) end it "qualified variable" do expect { parser.evaluate_string(scope, "$quantum_gravity::graviton", __FILE__) }.to raise_error(/Unknown variable/) end end it "a lex error should be raised for '$foo::::bar'" do expect { parser.evaluate_string(scope, "$foo::::bar") }.to raise_error(Puppet::LexError, /Illegal fully qualified name at line 1:7/) end { '$a = $0' => nil, '$a = $1' => nil, }.each do |source, value| it "it is ok to reference numeric unassigned variables '#{source}'" do parser.evaluate_string(scope, source, __FILE__).should == value end end { '$00 = 0' => /must be a decimal value/, '$0xf = 0' => /must be a decimal value/, '$0777 = 0' => /must be a decimal value/, '$123a = 0' => /must be a decimal value/, }.each do |source, error_pattern| it "should raise an error for '#{source}'" do expect { parser.evaluate_string(scope, source, __FILE__) }.to raise_error(error_pattern) end end context "an initial underscore in the last segment of a var name is allowed" do { '$_a = 1' => 1, '$__a = 1' => 1, }.each do |source, value| it "as in this example '#{source}'" do parser.evaluate_string(scope, source, __FILE__).should == value end end end end context "When evaluating relationships" do it 'should form a relation with File[a] -> File[b]' do source = "File[a] -> File[b]" parser.evaluate_string(scope, source, __FILE__) scope.compiler.should have_relationship(['File', 'a', '->', 'File', 'b']) end it 'should form a relation with resource -> resource' do source = "notify{a:} -> notify{b:}" parser.evaluate_string(scope, source, __FILE__) scope.compiler.should have_relationship(['Notify', 'a', '->', 'Notify', 'b']) end it 'should form a relation with [File[a], File[b]] -> [File[x], File[y]]' do source = "[File[a], File[b]] -> [File[x], File[y]]" parser.evaluate_string(scope, source, __FILE__) scope.compiler.should have_relationship(['File', 'a', '->', 'File', 'x']) scope.compiler.should have_relationship(['File', 'b', '->', 'File', 'x']) scope.compiler.should have_relationship(['File', 'a', '->', 'File', 'y']) scope.compiler.should have_relationship(['File', 'b', '->', 'File', 'y']) end it 'should tolerate (eliminate) duplicates in operands' do source = "[File[a], File[a]] -> File[x]" parser.evaluate_string(scope, source, __FILE__) scope.compiler.should have_relationship(['File', 'a', '->', 'File', 'x']) scope.compiler.relationships.size.should == 1 end it 'should form a relation with <-' do source = "File[a] <- File[b]" parser.evaluate_string(scope, source, __FILE__) scope.compiler.should have_relationship(['File', 'b', '->', 'File', 'a']) end it 'should form a relation with <-' do source = "File[a] <~ File[b]" parser.evaluate_string(scope, source, __FILE__) scope.compiler.should have_relationship(['File', 'b', '~>', 'File', 'a']) end end context "When evaluating heredoc" do it "evaluates plain heredoc" do src = "@(END)\nThis is\nheredoc text\nEND\n" parser.evaluate_string(scope, src).should == "This is\nheredoc text\n" end it "parses heredoc with margin" do src = [ "@(END)", " This is", " heredoc text", " | END", "" ].join("\n") parser.evaluate_string(scope, src).should == "This is\nheredoc text\n" end it "parses heredoc with margin and right newline trim" do src = [ "@(END)", " This is", " heredoc text", " |- END", "" ].join("\n") parser.evaluate_string(scope, src).should == "This is\nheredoc text" end it "parses escape specification" do src = <<-CODE @(END/t) Tex\\tt\\n |- END CODE parser.evaluate_string(scope, src).should == "Tex\tt\\n" end it "parses syntax checked specification" do src = <<-CODE @(END:json) ["foo", "bar"] |- END CODE parser.evaluate_string(scope, src).should == '["foo", "bar"]' end it "parses syntax checked specification with error and reports it" do src = <<-CODE @(END:json) ['foo', "bar"] |- END CODE expect { parser.evaluate_string(scope, src)}.to raise_error(/Cannot parse invalid JSON string/) end it "parses interpolated heredoc expression" do src = <<-CODE $name = 'Fjodor' @("END") Hello $name |- END CODE parser.evaluate_string(scope, src).should == "Hello Fjodor" end end context "Handles Deprecations and Discontinuations" do - around(:each) do |example| - Puppet.override({:loaders => Puppet::Pops::Loaders.new(Puppet::Node::Environment.create(:testing, []))}, 'test') do - example.run - end - end - it 'of import statements' do source = "\nimport foo" # Error references position 5 at the opening '{' # Set file to nil to make it easier to match with line number (no file name in output) expect { parser.evaluate_string(scope, source) }.to raise_error(/'import' has been discontinued.*line 2:1/) end end context "Detailed Error messages are reported" do it 'for illegal type references' do source = '1+1 { "title": }' # Error references position 5 at the opening '{' # Set file to nil to make it easier to match with line number (no file name in output) expect { parser.parse_string(source, nil) }.to raise_error(/Expression is not valid as a resource.*line 1:5/) end it 'for non r-value producing <| |>' do expect { parser.parse_string("$a = File <| |>", nil) }.to raise_error(/A Virtual Query does not produce a value at line 1:6/) end it 'for non r-value producing <<| |>>' do expect { parser.parse_string("$a = File <<| |>>", nil) }.to raise_error(/An Exported Query does not produce a value at line 1:6/) end it 'for non r-value producing define' do Puppet.expects(:err).with("Invalid use of expression. A 'define' expression does not produce a value at line 1:6") Puppet.expects(:err).with("Classes, definitions, and nodes may only appear at toplevel or inside other classes at line 1:6") expect { parser.parse_string("$a = define foo { }", nil) }.to raise_error(/2 errors/) end it 'for non r-value producing class' do Puppet.expects(:err).with("Invalid use of expression. A Host Class Definition does not produce a value at line 1:6") Puppet.expects(:err).with("Classes, definitions, and nodes may only appear at toplevel or inside other classes at line 1:6") expect { parser.parse_string("$a = class foo { }", nil) }.to raise_error(/2 errors/) end it 'for unclosed quote with indication of start position of string' do source = <<-SOURCE.gsub(/^ {6}/,'') $a = "xx yyy SOURCE # first char after opening " reported as being in error. expect { parser.parse_string(source) }.to raise_error(/Unclosed quote after '"' followed by 'xx\\nyy\.\.\.' at line 1:7/) end it 'for multiple errors with a summary exception' do Puppet.expects(:err).with("Invalid use of expression. A Node Definition does not produce a value at line 1:6") Puppet.expects(:err).with("Classes, definitions, and nodes may only appear at toplevel or inside other classes at line 1:6") expect { parser.parse_string("$a = node x { }",nil) }.to raise_error(/2 errors/) end it 'for a bad hostname' do expect { parser.parse_string("node 'macbook+owned+by+name' { }", nil) }.to raise_error(/The hostname 'macbook\+owned\+by\+name' contains illegal characters.*at line 1:6/) end it 'for a hostname with interpolation' do source = <<-SOURCE.gsub(/^ {6}/,'') $name = 'fred' node "macbook-owned-by$name" { } SOURCE expect { parser.parse_string(source, nil) }.to raise_error(/An interpolated expression is not allowed in a hostname of a node at line 2:23/) end end + context 'does not leak variables' do + it 'local variables are gone when lambda ends' do + source = <<-SOURCE + [1,2,3].each |$x| { $y = $x} + $a = $y + SOURCE + expect do + parser.evaluate_string(scope, source) + end.to raise_error(/Unknown variable: 'y'/) + end + + it 'lambda parameters are gone when lambda ends' do + source = <<-SOURCE + [1,2,3].each |$x| { $y = $x} + $a = $x + SOURCE + expect do + parser.evaluate_string(scope, source) + end.to raise_error(/Unknown variable: 'x'/) + end + + it 'does not leak match variables' do + source = <<-SOURCE + if 'xyz' =~ /(x)(y)(z)/ { notice $2 } + case 'abc' { + /(a)(b)(c)/ : { $x = $2 } + } + "-$x-$2-" + SOURCE + expect(parser.evaluate_string(scope, source)).to eq('-b--') + end + end + matcher :have_relationship do |expected| calc = Puppet::Pops::Types::TypeCalculator.new match do |compiler| op_name = {'->' => :relationship, '~>' => :subscription} compiler.relationships.any? do | relation | relation.source.type == expected[0] && relation.source.title == expected[1] && relation.type == op_name[expected[2]] && relation.target.type == expected[3] && relation.target.title == expected[4] end end failure_message_for_should do |actual| "Relationship #{expected[0]}[#{expected[1]}] #{expected[2]} #{expected[3]}[#{expected[4]}] but was unknown to compiler" end end end