diff --git a/lib/puppet/parser/functions/each.rb b/lib/puppet/parser/functions/each.rb index 39d26ba38..6c8b6356f 100644 --- a/lib/puppet/parser/functions/each.rb +++ b/lib/puppet/parser/functions/each.rb @@ -1,109 +1,110 @@ 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) - serving_size = pblock.parameter_count + # 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 end diff --git a/lib/puppet/parser/functions/filter.rb b/lib/puppet/parser/functions/filter.rb index 5760a8a75..17c3f132c 100644 --- a/lib/puppet/parser/functions/filter.rb +++ b/lib/puppet/parser/functions/filter.rb @@ -1,100 +1,102 @@ 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 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. *Examples* # 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 - requires `parser = future` ENDHEREDOC 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 receiver = args[0] pblock = args[1] raise ArgumentError, ("filter(): wrong argument type (#{pblock.class}; must be a parameterized block.") unless pblock.respond_to?(:puppet_lambda) - serving_size = pblock.parameter_count + + # 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 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 end diff --git a/lib/puppet/parser/functions/map.rb b/lib/puppet/parser/functions/map.rb index 8bc9fd383..fb10c7177 100644 --- a/lib/puppet/parser/functions/map.rb +++ b/lib/puppet/parser/functions/map.rb @@ -1,96 +1,98 @@ 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] } # 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. *Examples* # 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 - requires `parser = future` ENDHEREDOC 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 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) - serving_size = pblock.parameter_count + + # 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, "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 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 end diff --git a/lib/puppet/parser/functions/reduce.rb b/lib/puppet/parser/functions/reduce.rb index 078ebc2e9..3686bdb03 100644 --- a/lib/puppet/parser/functions/reduce.rb +++ b/lib/puppet/parser/functions/reduce.rb @@ -1,100 +1,102 @@ 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 - serving_size = pblock.parameter_count + # 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 end diff --git a/lib/puppet/parser/functions/slice.rb b/lib/puppet/parser/functions/slice.rb index bcc830f74..b50726d3f 100644 --- a/lib/puppet/parser/functions/slice.rb +++ b/lib/puppet/parser/functions/slice.rb @@ -1,116 +1,120 @@ 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. 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. $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. 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 - requires `parser = future`. ENDHEREDOC require 'puppet/parser/ast/lambda' require 'puppet/parser/scope' def each_Common(o, slice_size, filler, scope, pblock) - serving_size = pblock ? pblock.parameter_count : 1 + 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 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 end diff --git a/lib/puppet/pops/evaluator/closure.rb b/lib/puppet/pops/evaluator/closure.rb index 47c0a54e0..27bd9b570 100644 --- a/lib/puppet/pops/evaluator/closure.rb +++ b/lib/puppet/pops/evaluator/closure.rb @@ -1,112 +1,112 @@ # A Closure represents logic bound to a particular scope. # As long as the runtime (basically the scope implementation) has the behaviour of Puppet 3x it is not # safe to use this closure when the scope given to it when initialized goes "out of scope". # # Note that the implementation is backwards compatible in that the call method accepts a scope, but this # scope is not used. # # Note that this class is a CallableSignature, and the methods defined there should be used # as the API for obtaining information in a callable implementation agnostic way. # class Puppet::Pops::Evaluator::Closure < Puppet::Pops::Evaluator::CallableSignature attr_reader :evaluator attr_reader :model attr_reader :enclosing_scope def initialize(evaluator, model, scope) @evaluator = evaluator @model = model @enclosing_scope = scope end # marker method checked with respond_to :puppet_lambda # @api private # @deprecated Use the type system to query if an object is of Callable type, then use its signatures method for info def puppet_lambda() true end # compatible with 3x AST::Lambda # @api public def call(scope, *args) @evaluator.call(self, args, @enclosing_scope) end # Call closure with argument assignment by name def call_by_name(scope, args_hash, spill_over = false) @evaluator.call_by_name(self, args_hash, @enclosing_scope, spill_over) end # incompatible with 3x except that it is an array of the same size def parameters() @model.parameters || [] end # Returns the number of parameters (required and optional) # @return [Integer] the total number of accepted parameters def parameter_count # yes, this is duplication of code, but it saves a method call (@model.parameters || []).size end # Returns the number of optional parameters. # @return [Integer] the number of optional accepted parameters def optional_parameter_count @model.parameters.count { |p| !p.value.nil? } end # @api public def parameter_names @model.parameters.collect {|p| p.name } end # @api public def type @callable || create_callable_type end # @api public def last_captures_rest? - # TODO: No support for this yet - false + last = (@model.parameters || [])[-1] + last && last.captures_rest end # @api public def block_name # TODO: Lambda's does not support blocks yet. This is a placeholder 'unsupported_block' end private def create_callable_type() t = Puppet::Pops::Types::PCallableType.new() tuple_t = Puppet::Pops::Types::PTupleType.new() # since closure lambdas are currently untyped, each parameter becomes Optional[Object] parameter_names.each do |name| # TODO: Change when Closure supports typed parameters tuple_t.addTypes(Puppet::Pops::Types::TypeFactory.optional_object()) end # TODO: A Lambda can not currently declare varargs to = parameter_count from = to - optional_parameter_count if from != to size_t = Puppet::Pops::Types::PIntegerType.new() size_t.from = from size_t.to = to tuple_t.size_type = size_t end t.param_types = tuple_t # TODO: A Lambda can not currently declare that it accepts a lambda, except as an explicit parameter # being a Callable t end # Produces information about parameters compatible with a 4x Function (which can have multiple signatures) def signatures [ self ] end end diff --git a/lib/puppet/pops/evaluator/evaluator_impl.rb b/lib/puppet/pops/evaluator/evaluator_impl.rb index b16dc31c6..36a3ff24b 100644 --- a/lib/puppet/pops/evaluator/evaluator_impl.rb +++ b/lib/puppet/pops/evaluator/evaluator_impl.rb @@ -1,1138 +1,1207 @@ require 'rgen/ecore/ecore' require 'puppet/pops/evaluator/compare_operator' require 'puppet/pops/evaluator/relationship_operator' require 'puppet/pops/evaluator/access_operator' require 'puppet/pops/evaluator/closure' require 'puppet/pops/evaluator/external_syntax_support' # This implementation of {Puppet::Pops::Evaluator} performs evaluation using the puppet 3.x runtime system # in a manner largely compatible with Puppet 3.x, but adds new features and introduces constraints. # # The evaluation uses _polymorphic dispatch_ which works by dispatching to the first found method named after # the class or one of its super-classes. The EvaluatorImpl itself mainly deals with evaluation (it currently # also handles assignment), and it uses a delegation pattern to more specialized handlers of some operators # that in turn use polymorphic dispatch; this to not clutter EvaluatorImpl with too much responsibility). # # Since a pattern is used, only the main entry points are fully documented. The parameters _o_ and _scope_ are # the same in all the polymorphic methods, (the type of the parameter _o_ is reflected in the method's name; # either the actual class, or one of its super classes). The _scope_ parameter is always the scope in which # the evaluation takes place. If nothing else is mentioned, the return is always the result of evaluation. # # See {Puppet::Pops::Visitable} and {Puppet::Pops::Visitor} for more information about # polymorphic calling. # class Puppet::Pops::Evaluator::EvaluatorImpl include Puppet::Pops::Utils # Provides access to the Puppet 3.x runtime (scope, etc.) # This separation has been made to make it easier to later migrate the evaluator to an improved runtime. # include Puppet::Pops::Evaluator::Runtime3Support include Puppet::Pops::Evaluator::ExternalSyntaxSupport # This constant is not defined as Float::INFINITY in Ruby 1.8.7 (but is available in later version # Refactor when support is dropped for Ruby 1.8.7. # INFINITY = 1.0 / 0.0 # Reference to Issues name space makes it easier to refer to issues # (Issues are shared with the validator). # Issues = Puppet::Pops::Issues def initialize @@eval_visitor ||= Puppet::Pops::Visitor.new(self, "eval", 1, 1) @@lvalue_visitor ||= Puppet::Pops::Visitor.new(self, "lvalue", 1, 1) @@assign_visitor ||= Puppet::Pops::Visitor.new(self, "assign", 3, 3) @@string_visitor ||= Puppet::Pops::Visitor.new(self, "string", 1, 1) @@type_calculator ||= Puppet::Pops::Types::TypeCalculator.new() @@type_parser ||= Puppet::Pops::Types::TypeParser.new() @@compare_operator ||= Puppet::Pops::Evaluator::CompareOperator.new() @@relationship_operator ||= Puppet::Pops::Evaluator::RelationshipOperator.new() # Initialize the runtime module Puppet::Pops::Evaluator::Runtime3Support.instance_method(:initialize).bind(self).call() end # @api private def type_calculator @@type_calculator end # Polymorphic evaluate - calls eval_TYPE # # ## Polymorphic evaluate # Polymorphic evaluate calls a method on the format eval_TYPE where classname is the last # part of the class of the given _target_. A search is performed starting with the actual class, continuing # with each of the _target_ class's super classes until a matching method is found. # # # Description # Evaluates the given _target_ object in the given scope, optionally passing a block which will be # called with the result of the evaluation. # # @overload evaluate(target, scope, {|result| block}) # @param target [Object] evaluation target - see methods on the pattern assign_TYPE for actual supported types. # @param scope [Object] the runtime specific scope class where evaluation should take place # @return [Object] the result of the evaluation # # @api # def evaluate(target, scope) begin @@eval_visitor.visit_this_1(self, target, scope) rescue Puppet::Pops::SemanticError => e # a raised issue may not know the semantic target fail(e.issue, e.semantic || target, e.options, e) rescue StandardError => e if e.is_a? Puppet::ParseError # ParseError's are supposed to be fully configured with location information raise e end fail(Issues::RUNTIME_ERROR, target, {:detail => e.message}, e) end end # Polymorphic assign - calls assign_TYPE # # ## Polymorphic assign # Polymorphic assign calls a method on the format assign_TYPE where TYPE is the last # part of the class of the given _target_. A search is performed starting with the actual class, continuing # with each of the _target_ class's super classes until a matching method is found. # # # Description # Assigns the given _value_ to the given _target_. The additional argument _o_ is the instruction that # produced the target/value tuple and it is used to set the origin of the result. # @param target [Object] assignment target - see methods on the pattern assign_TYPE for actual supported types. # @param value [Object] the value to assign to `target` # @param o [Puppet::Pops::Model::PopsObject] originating instruction # @param scope [Object] the runtime specific scope where evaluation should take place # # @api # def assign(target, value, o, scope) @@assign_visitor.visit_this_3(self, target, value, o, scope) end def lvalue(o, scope) @@lvalue_visitor.visit_this_1(self, o, scope) end def string(o, scope) @@string_visitor.visit_this_1(self, o, scope) end # Call a closure matching arguments by name - Can only be called with a Closure (for now), may be refactored later # to also handle other types of calls (function calls are also handled by CallNamedFunction and CallMethod, they # could create similar objects to Closure, wait until other types of defines are instantiated - they may behave # as special cases of calls - i.e. 'new'). # # Call by name supports a "spill_over" mode where extra arguments in the given args_hash are introduced # as variables in the resulting scope. # # @raise ArgumentError, if there are to many or too few arguments # @raise ArgumentError, if given closure is not a Puppet::Pops::Evaluator::Closure # def call_by_name(closure, args_hash, scope, spill_over = false) raise ArgumentError, "Can only call a Lambda" unless closure.is_a?(Puppet::Pops::Evaluator::Closure) pblock = closure.model parameters = pblock.parameters || [] if !spill_over && args_hash.size > parameters.size raise ArgumentError, "Too many arguments: #{args_hash.size} for #{parameters.size}" end # associate values with parameters scope_hash = {} parameters.each do |p| scope_hash[p.name] = args_hash[p.name] || evaluate(p.value, scope) end missing = scope_hash.reduce([]) {|memo, entry| memo << entry[0] if entry[1].nil?; memo } unless missing.empty? optional = parameters.count { |p| !p.value.nil? } raise ArgumentError, "Too few arguments; no value given for required parameters #{missing.join(" ,")}" end if spill_over # all args from given hash should be used, nil entries replaced by default values should win scope_hash = args_hash.merge(scope_hash) end # Store the evaluated name => value associations in a new inner/local/ephemeral scope # (This is made complicated due to the fact that the implementation of scope is overloaded with # functionality and an inner ephemeral scope must be used (as opposed to just pushing a local scope # on a scope "stack"). # Ensure variable exists with nil value if error occurs. # Some ruby implementations does not like creating variable on return result = nil begin scope_memo = get_scope_nesting_level(scope) # change to create local scope_from - cannot give it file and line - that is the place of the call, not # "here" create_local_scope_from(scope_hash, scope) result = evaluate(pblock.body, scope) ensure set_scope_nesting_level(scope, scope_memo) end result end # Call a closure - Can only be called with a Closure (for now), may be refactored later # to also handle other types of calls (function calls are also handled by CallNamedFunction and CallMethod, they # could create similar objects to Closure, wait until other types of defines are instantiated - they may behave # as special cases of calls - i.e. 'new') # # @raise ArgumentError, if there are to many or too few arguments # @raise ArgumentError, if given closure is not a Puppet::Pops::Evaluator::Closure # def call(closure, args, scope) raise ArgumentError, "Can only call a Lambda" unless closure.is_a?(Puppet::Pops::Evaluator::Closure) pblock = closure.model parameters = pblock.parameters || [] + parameters_size = parameters.size + last_captures_rest = parameters_size > 0 && parameters[-1].captures_rest + args_size = args.size - raise ArgumentError, "Too many arguments: #{args.size} for #{parameters.size}" unless args.size <= parameters.size + unless args_size <= parameters_size || last_captures_rest + raise ArgumentError, "Too many arguments: #{args_size} for #{parameters_size}" + end + +#<<<<<<< HEAD +# # calculate missing arguments +# args_diff = parameters.size - args.size +# missing = parameters.slice(args.size, args_diff).select {|p| p.value.nil? } +# unless missing.empty? +# optional = parameters.count { |p| !p.value.nil? } +# raise ArgumentError, "Too few arguments; #{args.size} for #{optional > 0 ? ' min ' : ''}#{parameters.size - optional}" +# end +# # associate values with parameters (pad missing with :missing) +# merged = parameters.zip(args.fill(:missing, args.size, args_diff)) +# +#======= + # associate values with parameters (NOTE: excess args for captures rest are not included in merged) + merged = parameters.zip(args) # calculate missing arguments - args_diff = parameters.size - args.size - missing = parameters.slice(args.size, args_diff).select {|p| p.value.nil? } - unless missing.empty? - optional = parameters.count { |p| !p.value.nil? } - raise ArgumentError, "Too few arguments; #{args.size} for #{optional > 0 ? ' min ' : ''}#{parameters.size - optional}" + if args_size < parameters_size + missing = parameters.slice(args_size, parameters_size - args_size).select {|p| p.value.nil? } + unless missing.empty? + optional = parameters.count { |p| !p.value.nil? || p.captures_rest } + raise ArgumentError, "Too few arguments; #{args_size} for #{optional > 0 ? ' min ' : ''}#{parameters_size - optional}" + end end - # associate values with parameters (pad missing with :missing) - merged = parameters.zip(args.fill(:missing, args.size, args_diff)) - +#>>>>>>> 0582fa7... (PUP-514) Support last_captures_rest for lambdas evaluated = merged.collect do |m| # m can be one of # m = [Parameter{name => "name", value => nil], "given"] # | [Parameter{name => "name", value => Expression}, "given"] # | [Parameter{name => "name", value => Expression}, :missing] # # "given" may be nil or :undef which means that this is the value to use, # not a default expression. # - given_argument = m[1] - argument_name = m[0].name - default_expression = m[0].value - - # Use default value if a value was not given (NOTE: An :undef overrides - just a nil overrides default in ruby). - value = - if given_argument == :missing - # nothing was given, use default (it is guaranteed to exist) - evaluate(default_expression, scope) +#<<<<<<< HEAD +# given_argument = m[1] +# argument_name = m[0].name +# default_expression = m[0].value +# +# # Use default value if a value was not given (NOTE: An :undef overrides - just a nil overrides default in ruby). +# value = +# if given_argument == :missing +# # nothing was given, use default (it is guaranteed to exist) +# evaluate(default_expression, scope) +# else +# # use the given value +# given_argument +#======= + # "given" is always an optional entry. If a parameter was provided then + # the entry will be in the array, otherwise the m array will be a + # single element. + given_argument = m[ 1 ] + argument_name = m[ 0 ].name + param_captures = m[ 0 ].captures_rest + default_expression = m[ 0 ].value + + if m.size == 1 + # not given + if default_expression + # not given, has default + value = evaluate(default_expression, scope) + if param_captures && !value.is_a?(Array) + # correct non array default value + value = [ value ] + end + else + # not given, does not have default + if param_captures + # default for captures rest is an empty array + value = [ ] + else + # should have been caught earlier + raise ArgumentError, "InternalError: Should not happen! non optional parameter not caught earlier in evaluator call" + end + end else - # use the given value - given_argument + # given + if param_captures + # get excess arguments + value = args[(parameters_size-1)..-1] + # If the input was a single nil, or undef, and there is a default, use the default + if value.size == 1 && (given_argument.nil? || given_argument == :undef) && default_expression + value = evaluate(default_expression, scope) + # and ensure it is an array + value = [value] unless value.is_a?(Array) + end + else + # Do not use given if there is a default and given is nil / undefined + # else, let the value through + if (given_argument.nil? || given_argument == :undef) && default_expression + value = evaluate(default_expression, scope) + else + value = given_argument + end + end +#>>>>>>> 0582fa7... (PUP-514) Support last_captures_rest for lambdas end - [argument_name, value] + [ argument_name, value ] end # Store the evaluated name => value associations in a new inner/local/ephemeral scope # (This is made complicated due to the fact that the implementation of scope is overloaded with # functionality and an inner ephemeral scope must be used (as opposed to just pushing a local scope # on a scope "stack"). # Ensure variable exists with nil value if error occurs. # Some ruby implementations does not like creating variable on return result = nil begin scope_memo = get_scope_nesting_level(scope) # change to create local scope_from - cannot give it file and line - that is the place of the call, not # "here" create_local_scope_from(Hash[evaluated], scope) result = evaluate(pblock.body, scope) ensure set_scope_nesting_level(scope, scope_memo) end result end protected def lvalue_VariableExpression(o, scope) # evaluate the name evaluate(o.expr, scope) end # Catches all illegal lvalues # def lvalue_Object(o, scope) fail(Issues::ILLEGAL_ASSIGNMENT, o) end # Assign value to named variable. # The '$' sign is never part of the name. # @example In Puppet DSL # $name = value # @param name [String] name of variable without $ # @param value [Object] value to assign to the variable # @param o [Puppet::Pops::Model::PopsObject] originating instruction # @param scope [Object] the runtime specific scope where evaluation should take place # @return [value] # def assign_String(name, value, o, scope) if name =~ /::/ fail(Issues::CROSS_SCOPE_ASSIGNMENT, o.left_expr, {:name => name}) end set_variable(name, value, o, scope) value end def assign_Numeric(n, value, o, scope) fail(Issues::ILLEGAL_NUMERIC_ASSIGNMENT, o.left_expr, {:varname => n.to_s}) end # Catches all illegal assignment (e.g. 1 = 2, {'a'=>1} = 2, etc) # def assign_Object(name, value, o, scope) fail(Issues::ILLEGAL_ASSIGNMENT, o) end def eval_Factory(o, scope) evaluate(o.current, scope) end # Evaluates any object not evaluated to something else to itself. def eval_Object o, scope o end # Allows nil to be used as a Nop. # Evaluates to nil # TODO: What is the difference between literal undef, nil, and nop? # def eval_NilClass(o, scope) nil end # Evaluates Nop to nil. # TODO: or is this the same as :undef # TODO: is this even needed as a separate instruction when there is a literal undef? def eval_Nop(o, scope) nil end # Captures all LiteralValues not handled elsewhere. # def eval_LiteralValue(o, scope) o.value end # Reserved Words fail to evaluate # def eval_ReservedWord(o, scope) fail(Puppet::Pops::Issues::RESERVED_WORD, o, {:word => o.word}) end def eval_LiteralDefault(o, scope) :default end def eval_LiteralUndef(o, scope) :undef # TODO: or just use nil for this? end # A QualifiedReference (i.e. a capitalized qualified name such as Foo, or Foo::Bar) evaluates to a PType # def eval_QualifiedReference(o, scope) @@type_parser.interpret(o) end def eval_NotExpression(o, scope) ! is_true?(evaluate(o.expr, scope)) end def eval_UnaryMinusExpression(o, scope) - coerce_numeric(evaluate(o.expr, scope), o, scope) end def eval_UnfoldExpression(o, scope) candidate = evaluate(o.expr, scope) case candidate when Array candidate when Hash candidate.to_a else # turns anything else into an array (so result can be unfolded) [candidate] end end # Abstract evaluation, returns array [left, right] with the evaluated result of left_expr and # right_expr # @return > array with result of evaluating left and right expressions # def eval_BinaryExpression o, scope [ evaluate(o.left_expr, scope), evaluate(o.right_expr, scope) ] end # Evaluates assignment with operators =, +=, -= and # # @example Puppet DSL # $a = 1 # $a += 1 # $a -= 1 # def eval_AssignmentExpression(o, scope) name = lvalue(o.left_expr, scope) value = evaluate(o.right_expr, scope) case o.operator when :'=' # regular assignment assign(name, value, o, scope) when :'+=' # if value does not exist and strict is on, looking it up fails, else it is nil or :undef existing_value = get_variable_value(name, o, scope) begin if existing_value.nil? || existing_value == :undef assign(name, value, o, scope) else # Delegate to calculate function to deal with check of LHS, and perform ´+´ as arithmetic or concatenation the # same way as ArithmeticExpression performs `+`. assign(name, calculate(existing_value, value, :'+', o.left_expr, o.right_expr, scope), o, scope) end rescue ArgumentError => e fail(Issues::APPEND_FAILED, o, {:message => e.message}) end when :'-=' # If an attempt is made to delete values from something that does not exists, the value is :undef (it is guaranteed to not # include any values the user wants deleted anyway :-) # # if value does not exist and strict is on, looking it up fails, else it is nil or :undef existing_value = get_variable_value(name, o, scope) begin if existing_value.nil? || existing_value == :undef assign(name, :undef, o, scope) else # Delegate to delete function to deal with check of LHS, and perform deletion assign(name, delete(get_variable_value(name, o, scope), value), o, scope) end rescue ArgumentError => e fail(Issues::APPEND_FAILED, o, {:message => e.message}, e) end else fail(Issues::UNSUPPORTED_OPERATOR, o, {:operator => o.operator}) end value end ARITHMETIC_OPERATORS = [:'+', :'-', :'*', :'/', :'%', :'<<', :'>>'] COLLECTION_OPERATORS = [:'+', :'-', :'<<'] # Handles binary expression where lhs and rhs are array/hash or numeric and operator is +, - , *, % / << >> # def eval_ArithmeticExpression(o, scope) left, right = eval_BinaryExpression(o, scope) begin result = calculate(left, right, o.operator, o.left_expr, o.right_expr, scope) rescue ArgumentError => e fail(Issues::RUNTIME_ERROR, o, {:detail => e.message}, e) end result end # Handles binary expression where lhs and rhs are array/hash or numeric and operator is +, - , *, % / << >> # def calculate(left, right, operator, left_o, right_o, scope) unless ARITHMETIC_OPERATORS.include?(operator) fail(Issues::UNSUPPORTED_OPERATOR, left_o.eContainer, {:operator => o.operator}) end if (left.is_a?(Array) || left.is_a?(Hash)) && COLLECTION_OPERATORS.include?(operator) # Handle operation on collections case operator when :'+' concatenate(left, right) when :'-' delete(left, right) when :'<<' unless left.is_a?(Array) fail(Issues::OPERATOR_NOT_APPLICABLE, left_o, {:operator => operator, :left_value => left}) end left + [right] end else # Handle operation on numeric left = coerce_numeric(left, left_o, scope) right = coerce_numeric(right, right_o, scope) begin if operator == :'%' && (left.is_a?(Float) || right.is_a?(Float)) # Deny users the fun of seeing severe rounding errors and confusing results fail(Issues::OPERATOR_NOT_APPLICABLE, left_o, {:operator => operator, :left_value => left}) end result = left.send(operator, right) rescue NoMethodError => e fail(Issues::OPERATOR_NOT_APPLICABLE, left_o, {:operator => operator, :left_value => left}) rescue ZeroDivisionError => e fail(Issues::DIV_BY_ZERO, right_o) end if result == INFINITY || result == -INFINITY fail(Issues::RESULT_IS_INFINITY, left_o, {:operator => operator}) end result end end def eval_EppExpression(o, scope) scope["@epp"] = [] evaluate(o.body, scope) result = scope["@epp"].join('') result end def eval_RenderStringExpression(o, scope) scope["@epp"] << o.value.dup nil end def eval_RenderExpression(o, scope) scope["@epp"] << string(evaluate(o.expr, scope), scope) nil end # Evaluates Puppet DSL ->, ~>, <-, and <~ def eval_RelationshipExpression(o, scope) # First level evaluation, reduction to basic data types or puppet types, the relationship operator then translates this # to the final set of references (turning strings into references, which can not naturally be done by the main evaluator since # all strings should not be turned into references. # real = eval_BinaryExpression(o, scope) @@relationship_operator.evaluate(real, o, scope) end # Evaluates x[key, key, ...] # def eval_AccessExpression(o, scope) left = evaluate(o.left_expr, scope) keys = o.keys.nil? ? [] : o.keys.collect {|key| evaluate(key, scope) } Puppet::Pops::Evaluator::AccessOperator.new(o).access(left, scope, *keys) end # Evaluates <, <=, >, >=, and == # def eval_ComparisonExpression o, scope left, right = eval_BinaryExpression o, scope begin # Left is a type if left.is_a?(Puppet::Pops::Types::PAbstractType) case o.operator when :'==' @@type_calculator.equals(left,right) when :'!=' !@@type_calculator.equals(left,right) when :'<' # left can be assigned to right, but they are not equal @@type_calculator.assignable?(right, left) && ! @@type_calculator.equals(left,right) when :'<=' # left can be assigned to right @@type_calculator.assignable?(right, left) when :'>' # right can be assigned to left, but they are not equal @@type_calculator.assignable?(left,right) && ! @@type_calculator.equals(left,right) when :'>=' # right can be assigned to left @@type_calculator.assignable?(left, right) else fail(Issues::UNSUPPORTED_OPERATOR, o, {:operator => o.operator}) end else case o.operator when :'==' @@compare_operator.equals(left,right) when :'!=' ! @@compare_operator.equals(left,right) when :'<' @@compare_operator.compare(left,right) < 0 when :'<=' @@compare_operator.compare(left,right) <= 0 when :'>' @@compare_operator.compare(left,right) > 0 when :'>=' @@compare_operator.compare(left,right) >= 0 else fail(Issues::UNSUPPORTED_OPERATOR, o, {:operator => o.operator}) end end rescue ArgumentError => e fail(Issues::COMPARISON_NOT_POSSIBLE, o, { :operator => o.operator, :left_value => left, :right_value => right, :detail => e.message}, e) end end # Evaluates matching expressions with type, string or regexp rhs expression. # If RHS is a type, the =~ matches compatible (instance? of) type. # # @example # x =~ /abc.*/ # @example # x =~ "abc.*/" # @example # y = "abc" # x =~ "${y}.*" # @example # [1,2,3] =~ Array[Integer[1,10]] # # Note that a string is not instance? of Regexp, only Regular expressions are. # The Pattern type should instead be used as it is specified as subtype of String. # # @return [Boolean] if a match was made or not. Also sets $0..$n to matchdata in current scope. # def eval_MatchExpression o, scope left, pattern = eval_BinaryExpression o, scope # matches RHS types as instance of for all types except a parameterized Regexp[R] if pattern.is_a?(Puppet::Pops::Types::PAbstractType) # evaluate as instance? of type check matched = @@type_calculator.instance?(pattern, left) # convert match result to Boolean true, or false return o.operator == :'=~' ? !!matched : !matched end begin pattern = Regexp.new(pattern) unless pattern.is_a?(Regexp) rescue StandardError => e fail(Issues::MATCH_NOT_REGEXP, o.right_expr, {:detail => e.message}, e) end unless left.is_a?(String) fail(Issues::MATCH_NOT_STRING, o.left_expr, {:left_value => left}) end matched = pattern.match(left) # nil, or MatchData set_match_data(matched, o, scope) # creates ephemeral # convert match result to Boolean true, or false o.operator == :'=~' ? !!matched : !matched end # Evaluates Puppet DSL `in` expression # def eval_InExpression o, scope left, right = eval_BinaryExpression o, scope @@compare_operator.include?(right, left) end # @example # $a and $b # b is only evaluated if a is true # def eval_AndExpression o, scope is_true?(evaluate(o.left_expr, scope)) ? is_true?(evaluate(o.right_expr, scope)) : false end # @example # a or b # b is only evaluated if a is false # def eval_OrExpression o, scope is_true?(evaluate(o.left_expr, scope)) ? true : is_true?(evaluate(o.right_expr, scope)) end # Evaluates each entry of the literal list and creates a new Array # Supports unfolding of entries # @return [Array] with the evaluated content # def eval_LiteralList o, scope unfold([], o.values, scope) end # Evaluates each entry of the literal hash and creates a new Hash. # @return [Hash] with the evaluated content # def eval_LiteralHash o, scope h = Hash.new o.entries.each {|entry| h[ evaluate(entry.key, scope)]= evaluate(entry.value, scope)} h end # Evaluates all statements and produces the last evaluated value # def eval_BlockExpression o, scope r = nil o.statements.each {|s| r = evaluate(s, scope)} r end # Performs optimized search over case option values, lazily evaluating each # until there is a match. If no match is found, the case expression's default expression # is evaluated (it may be nil or Nop if there is no default, thus producing nil). # If an option matches, the result of evaluating that option is returned. # @return [Object, nil] what a matched option returns, or nil if nothing matched. # def eval_CaseExpression(o, scope) # memo scope level before evaluating test - don't want a match in the case test to leak $n match vars # to expressions after the case expression. # with_guarded_scope(scope) do test = evaluate(o.test, scope) result = nil the_default = nil if o.options.find do |co| # the first case option that matches if co.values.find do |c| case c when Puppet::Pops::Model::LiteralDefault the_default = co.then_expr is_match?(test, evaluate(c, scope), c, scope) when Puppet::Pops::Model::UnfoldExpression # not ideal for error reporting, since it is not known which unfolded result # that caused an error - the entire unfold expression is blamed (i.e. the var c, passed to is_match?) evaluate(c, scope).any? {|v| is_match?(test, v, c, scope) } else is_match?(test, evaluate(c, scope), c, scope) end end result = evaluate(co.then_expr, scope) true # the option was picked end end result # an option was picked, and produced a result else evaluate(the_default, scope) # evaluate the default (should be a nop/nil) if there is no default). end end end # Evaluates a CollectExpression by transforming it into a 3x AST::Collection and then evaluating that. # This is done because of the complex API between compiler, indirector, backends, and difference between # collecting virtual resources and exported resources. # def eval_CollectExpression o, scope # The Collect Expression and its contained query expressions are implemented in such a way in # 3x that it is almost impossible to do anything about them (the AST objects are lazily evaluated, # and the built structure consists of both higher order functions and arrays with query expressions # that are either used as a predicate filter, or given to an indirection terminus (such as the Puppet DB # resource terminus). Unfortunately, the 3x implementation has many inconsistencies that the implementation # below carries forward. # collect_3x = Puppet::Pops::Model::AstTransformer.new().transform(o) collected = collect_3x.evaluate(scope) # the 3x returns an instance of Parser::Collector (but it is only registered with the compiler at this # point and does not contain any valuable information (like the result) # Dilemma: If this object is returned, it is a first class value in the Puppet Language and we # need to be able to perform operations on it. We can forbid it from leaking by making CollectExpression # a non R-value. This makes it possible for the evaluator logic to make use of the Collector. collected end def eval_ParenthesizedExpression(o, scope) evaluate(o.expr, scope) end # This evaluates classes, nodes and resource type definitions to nil, since 3x: # instantiates them, and evaluates their parameters and body. This is achieved by # providing bridge AST classes in Puppet::Parser::AST::PopsBridge that bridges a # Pops Program and a Pops Expression. # # Since all Definitions are handled "out of band", they are treated as a no-op when # evaluated. # def eval_Definition(o, scope) nil end def eval_Program(o, scope) evaluate(o.body, scope) end # Produces Array[PObjectType], an array of resource references # def eval_ResourceExpression(o, scope) exported = o.exported virtual = o.virtual type_name = evaluate(o.type_name, scope) o.bodies.map do |body| titles = [evaluate(body.title, scope)].flatten evaluated_parameters = body.operations.map {|op| evaluate(op, scope) } create_resources(o, scope, virtual, exported, type_name, titles, evaluated_parameters) end.flatten.compact end def eval_ResourceOverrideExpression(o, scope) evaluated_resources = evaluate(o.resources, scope) evaluated_parameters = o.operations.map { |op| evaluate(op, scope) } create_resource_overrides(o, scope, [evaluated_resources].flatten, evaluated_parameters) evaluated_resources end # Produces 3x array of parameters def eval_AttributeOperation(o, scope) create_resource_parameter(o, scope, o.attribute_name, evaluate(o.value_expr, scope), o.operator) end # Sets default parameter values for a type, produces the type # def eval_ResourceDefaultsExpression(o, scope) type_name = o.type_ref.value # a QualifiedName's string value evaluated_parameters = o.operations.map {|op| evaluate(op, scope) } create_resource_defaults(o, scope, type_name, evaluated_parameters) # Produce the type evaluate(o.type_ref, scope) end # Evaluates function call by name. # def eval_CallNamedFunctionExpression(o, scope) # The functor expression is not evaluated, it is not possible to select the function to call # via an expression like $a() case o.functor_expr when Puppet::Pops::Model::QualifiedName # ok when Puppet::Pops::Model::RenderStringExpression # helpful to point out this easy to make Epp error fail(Issues::ILLEGAL_EPP_PARAMETERS, o) else fail(Issues::ILLEGAL_EXPRESSION, o.functor_expr, {:feature=>'function name', :container => o}) end name = o.functor_expr.value evaluated_arguments = unfold([], o.arguments, scope) # wrap lambda in a callable block if it is present evaluated_arguments << Puppet::Pops::Evaluator::Closure.new(self, o.lambda, scope) if o.lambda call_function(name, evaluated_arguments, o, scope) end # Evaluation of CallMethodExpression handles a NamedAccessExpression functor (receiver.function_name) # def eval_CallMethodExpression(o, scope) unless o.functor_expr.is_a? Puppet::Pops::Model::NamedAccessExpression fail(Issues::ILLEGAL_EXPRESSION, o.functor_expr, {:feature=>'function accessor', :container => o}) end receiver = evaluate(o.functor_expr.left_expr, scope) name = o.functor_expr.right_expr unless name.is_a? Puppet::Pops::Model::QualifiedName fail(Issues::ILLEGAL_EXPRESSION, o.functor_expr, {:feature=>'function name', :container => o}) end name = name.value # the string function name evaluated_arguments = unfold([receiver], o.arguments || [], scope) # wrap lambda in a callable block if it is present evaluated_arguments << Puppet::Pops::Evaluator::Closure.new(self, o.lambda, scope) if o.lambda call_function(name, evaluated_arguments, o, scope) end # @example # $x ? { 10 => true, 20 => false, default => 0 } # def eval_SelectorExpression o, scope # memo scope level before evaluating test - don't want a match in the case test to leak $n match vars # to expressions after the selector expression. # with_guarded_scope(scope) do test = evaluate(o.left_expr, scope) the_default = nil selected = o.selectors.find do |s| me = s.matching_expr case me when Puppet::Pops::Model::LiteralDefault the_default = s.value_expr false when Puppet::Pops::Model::UnfoldExpression # not ideal for error reporting, since it is not known which unfolded result # that caused an error - the entire unfold expression is blamed (i.e. the var c, passed to is_match?) evaluate(me, scope).any? {|v| is_match?(test, v, me, scope) } else is_match?(test, evaluate(me, scope), me, scope) end end if selected evaluate(selected.value_expr, scope) elsif the_default evaluate(the_default, scope) else fail(Issues::UNMATCHED_SELECTOR, o.left_expr, :param_value => test) end end end # SubLocatable is simply an expression that holds location information def eval_SubLocatedExpression o, scope evaluate(o.expr, scope) end # Evaluates Puppet DSL Heredoc def eval_HeredocExpression o, scope result = evaluate(o.text_expr, scope) assert_external_syntax(scope, result, o.syntax, o.text_expr) result end # Evaluates Puppet DSL `if` def eval_IfExpression o, scope with_guarded_scope(scope) do if is_true?(evaluate(o.test, scope)) evaluate(o.then_expr, scope) else evaluate(o.else_expr, scope) end end end # Evaluates Puppet DSL `unless` def eval_UnlessExpression o, scope with_guarded_scope(scope) do unless is_true?(evaluate(o.test, scope)) evaluate(o.then_expr, scope) else evaluate(o.else_expr, scope) end end end # Evaluates a variable (getting its value) # The evaluator is lenient; any expression producing a String is used as a name # of a variable. # def eval_VariableExpression o, scope # Evaluator is not too fussy about what constitutes a name as long as the result # is a String and a valid variable name # name = evaluate(o.expr, scope) # Should be caught by validation, but make this explicit here as well, or mysterious evaluation issues # may occur. case name when String when Numeric else fail(Issues::ILLEGAL_VARIABLE_EXPRESSION, o.expr) end # TODO: Check for valid variable name (Task for validator) # TODO: semantics of undefined variable in scope, this just returns what scope does == value or nil get_variable_value(name, o, scope) end # Evaluates double quoted strings that may contain interpolation # def eval_ConcatenatedString o, scope o.segments.collect {|expr| string(evaluate(expr, scope), scope)}.join end # If the wrapped expression is a QualifiedName, it is taken as the name of a variable in scope. # Note that this is different from the 3.x implementation, where an initial qualified name # is accepted. (e.g. `"---${var + 1}---"` is legal. This implementation requires such concrete # syntax to be expressed in a model as `(TextExpression (+ (Variable var) 1)` - i.e. moving the decision to # the parser. # # Semantics; the result of an expression is turned into a string, nil is silently transformed to empty # string. # @return [String] the interpolated result # def eval_TextExpression o, scope if o.expr.is_a?(Puppet::Pops::Model::QualifiedName) # TODO: formalize, when scope returns nil, vs error string(get_variable_value(o.expr.value, o, scope), scope) else string(evaluate(o.expr, scope), scope) end end def string_Object(o, scope) o.to_s end def string_Symbol(o, scope) case o when :undef '' else o.to_s end end def string_Array(o, scope) ['[', o.map {|e| string(e, scope)}.join(', '), ']'].join() end def string_Hash(o, scope) ['{', o.map {|k,v| string(k, scope) + " => " + string(v, scope)}.join(', '), '}'].join() end def string_Regexp(o, scope) ['/', o.source, '/'].join() end def string_PAbstractType(o, scope) @@type_calculator.string(o) end # Produces concatenation / merge of x and y. # # When x is an Array, y of type produces: # # * Array => concatenation `[1,2], [3,4] => [1,2,3,4]` # * Hash => concatenation of hash as array `[key, value, key, value, ...]` # * any other => concatenation of single value # # When x is a Hash, y of type produces: # # * Array => merge of array interpreted as `[key, value, key, value,...]` # * Hash => a merge, where entries in `y` overrides # * any other => error # # When x is something else, wrap it in an array first. # # When x is nil, an empty array is used instead. # # @note to concatenate an Array, nest the array - i.e. `[1,2], [[2,3]]` # # @overload concatenate(obj_x, obj_y) # @param obj_x [Object] object to wrap in an array and concatenate to; see other overloaded methods for return type # @param ary_y [Object] array to concatenate at end of `ary_x` # @return [Object] wraps obj_x in array before using other overloaded option based on type of obj_y # @overload concatenate(ary_x, ary_y) # @param ary_x [Array] array to concatenate to # @param ary_y [Array] array to concatenate at end of `ary_x` # @return [Array] new array with `ary_x` + `ary_y` # @overload concatenate(ary_x, hsh_y) # @param ary_x [Array] array to concatenate to # @param hsh_y [Hash] converted to array form, and concatenated to array # @return [Array] new array with `ary_x` + `hsh_y` converted to array # @overload concatenate (ary_x, obj_y) # @param ary_x [Array] array to concatenate to # @param obj_y [Object] non array or hash object to add to array # @return [Array] new array with `ary_x` + `obj_y` added as last entry # @overload concatenate(hsh_x, ary_y) # @param hsh_x [Hash] the hash to merge with # @param ary_y [Array] array interpreted as even numbered sequence of key, value merged with `hsh_x` # @return [Hash] new hash with `hsh_x` merged with `ary_y` interpreted as hash in array form # @overload concatenate(hsh_x, hsh_y) # @param hsh_x [Hash] the hash to merge to # @param hsh_y [Hash] hash merged with `hsh_x` # @return [Hash] new hash with `hsh_x` merged with `hsh_y` # @raise [ArgumentError] when `xxx_x` is neither an Array nor a Hash # @raise [ArgumentError] when `xxx_x` is a Hash, and `xxx_y` is neither Array nor Hash. # def concatenate(x, y) x = [x] unless x.is_a?(Array) || x.is_a?(Hash) case x when Array y = case y when Array then y when Hash then y.to_a else [y] end x + y # new array with concatenation when Hash y = case y when Hash then y when Array # Hash[[a, 1, b, 2]] => {} # Hash[a,1,b,2] => {a => 1, b => 2} # Hash[[a,1], [b,2]] => {[a,1] => [b,2]} # Hash[[[a,1], [b,2]]] => {a => 1, b => 2} # Use type calcultor to determine if array is Array[Array[?]], and if so use second form # of call t = @@type_calculator.infer(y) if t.element_type.is_a? Puppet::Pops::Types::PArrayType Hash[y] else Hash[*y] end else raise ArgumentError.new("Can only append Array or Hash to a Hash") end x.merge y # new hash with overwrite else raise ArgumentError.new("Can only append to an Array or a Hash.") end end # Produces the result x \ y (set difference) # When `x` is an Array, `y` is transformed to an array and then all matching elements removed from x. # When `x` is a Hash, all contained keys are removed from x as listed in `y` if it is an Array, or all its keys if it is a Hash. # The difference is returned. The given `x` and `y` are not modified by this operation. # @raise [ArgumentError] when `x` is neither an Array nor a Hash # def delete(x, y) result = x.dup case x when Array y = case y when Array then y when Hash then y.to_a else [y] end y.each {|e| result.delete(e) } when Hash y = case y when Array then y when Hash then y.keys else [y] end y.each {|e| result.delete(e) } else raise ArgumentError.new("Can only delete from an Array or Hash.") end result end # Implementation of case option matching. # # This is the type of matching performed in a case option, using == for every type # of value except regular expression where a match is performed. # def is_match? left, right, o, scope if right.is_a?(Regexp) return false unless left.is_a? String matched = right.match(left) set_match_data(matched, o, scope) # creates or clears ephemeral !!matched # convert to boolean elsif right.is_a?(Puppet::Pops::Types::PAbstractType) # right is a type and left is not - check if left is an instance of the given type # (The reverse is not terribly meaningful - computing which of the case options that first produces # an instance of a given type). # @@type_calculator.instance?(right, left) else # Handle equality the same way as the language '==' operator (case insensitive etc.) @@compare_operator.equals(left,right) end end def with_guarded_scope(scope) scope_memo = get_scope_nesting_level(scope) begin yield ensure set_scope_nesting_level(scope, scope_memo) end end # Maps the expression in the given array to their product except for UnfoldExpressions which are first unfolded. # The result is added to the given result Array. # @param result [Array] Where to add the result (may contain information to add to) # @param array [Array[Puppet::Pops::Model::Expression] the expressions to map # @param scope [Puppet::Parser::Scope] the scope to evaluate in # @return [Array] the given result array with content added from the operation # def unfold(result, array, scope) array.each do |x| if x.is_a?(Puppet::Pops::Model::UnfoldExpression) result.concat(evaluate(x, scope)) else result << evaluate(x, scope) end end result end private :unfold end diff --git a/lib/puppet/pops/evaluator/runtime3_support.rb b/lib/puppet/pops/evaluator/runtime3_support.rb index 5c20bfa98..244d2f480 100644 --- a/lib/puppet/pops/evaluator/runtime3_support.rb +++ b/lib/puppet/pops/evaluator/runtime3_support.rb @@ -1,537 +1,545 @@ # 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 end 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 = args.map {|a| convert(a, scope, '') } + mapped_args = FUNCTIONS_4x[name] ? 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) case catalog_type when Puppet::Pops::Types::PHostClassType class_name = catalog_type.class_name ['class', class_name.nil? ? nil : class_name.sub(/^::/, '')] when Puppet::Pops::Types::PResourceType type_name = catalog_type.type_name title = catalog_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 is neither a PHostClassType, nor a PResourceClass." 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/spec/unit/parser/methods/each_spec.rb b/spec/unit/parser/methods/each_spec.rb index 5e9ce4e0c..053d978e9 100644 --- a/spec/unit/parser/methods/each_spec.rb +++ b/spec/unit/parser/methods/each_spec.rb @@ -1,91 +1,106 @@ require 'puppet' require 'spec_helper' require 'puppet_spec/compiler' require 'rubygems' 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 end diff --git a/spec/unit/parser/methods/filter_spec.rb b/spec/unit/parser/methods/filter_spec.rb index c9fed31d2..13d24a18f 100644 --- a/spec/unit/parser/methods/filter_spec.rb +++ b/spec/unit/parser/methods/filter_spec.rb @@ -1,135 +1,147 @@ require 'puppet' require 'spec_helper' require 'puppet_spec/compiler' require 'unit/parser/methods/shared' describe 'the filter method' do include PuppetSpec::Compiler 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 catalog.resource(:file, "/file_strawberry")['ensure'].should == 'present' catalog.resource(:file, "/file_blueberry")['ensure'].should == '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 catalog.resource(:file, "/file_3")['ensure'].should == 'present' catalog.resource(:file, "/file_6")['ensure'].should == 'present' catalog.resource(:file, "/file_9")['ensure'].should == '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 catalog.resource(:file, "/file_10")['ensure'].should == 'present' catalog.resource(:file, "/file_13")['ensure'].should == 'present' catalog.resource(:file, "/file_16")['ensure'].should == '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 catalog.resource(:file, "/file_strawberry")['ensure'].should == 'present' catalog.resource(:file, "/file_blueberry")['ensure'].should == '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 catalog.resource(:file, "/file_strawberry")['ensure'].should == 'present' catalog.resource(:file, "/file_orange")['ensure'].should == '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 + + catalog.resource(:file, "/file_strawberry")['ensure'].should == 'present' + catalog.resource(:file, "/file_orange")['ensure'].should == '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 catalog.resource(:file, "/file_strawberry")['ensure'].should == 'present' catalog.resource(:file, "/file_blueberry")['ensure'].should == '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 catalog.resource(:file, "/file_red")['ensure'].should == 'present' catalog.resource(:file, "/file_blue")['ensure'].should == 'present' catalog.resource(:file, "/file_")['ensure'].should == '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 catalog.resource(:file, "/file_strawb")['ensure'].should == 'present' catalog.resource(:file, "/file_blueb")['ensure'].should == '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/parser/methods/map_spec.rb index 08f4cd124..102f9195d 100644 --- a/spec/unit/parser/methods/map_spec.rb +++ b/spec/unit/parser/methods/map_spec.rb @@ -1,196 +1,208 @@ require 'puppet' require 'spec_helper' require 'puppet_spec/compiler' require 'unit/parser/methods/shared' describe 'the map method' do include PuppetSpec::Compiler 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 catalog.resource(:file, "/file_2")['ensure'].should == 'present' catalog.resource(:file, "/file_4")['ensure'].should == 'present' catalog.resource(:file, "/file_6")['ensure'].should == '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 catalog.resource(:file, "/file_2")['ensure'].should == 'present' catalog.resource(:file, "/file_4")['ensure'].should == 'present' catalog.resource(:file, "/file_6")['ensure'].should == '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 catalog.resource(:file, "/file_0")['ensure'].should == 'present' catalog.resource(:file, "/file_3")['ensure'].should == 'present' catalog.resource(:file, "/file_6")['ensure'].should == '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 catalog.resource(:file, "/file_x")['ensure'].should == 'present' catalog.resource(:file, "/file_y")['ensure'].should == '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 catalog.resource(:file, "/file_1")['ensure'].should == 'present' catalog.resource(:file, "/file_20")['ensure'].should == 'present' catalog.resource(:file, "/file_3")['ensure'].should == '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 catalog.resource(:file, "/file_a")['ensure'].should == 'present' catalog.resource(:file, "/file_b")['ensure'].should == 'present' catalog.resource(:file, "/file_c")['ensure'].should == '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 catalog.resource(:file, "/file_a")['ensure'].should == 'present' catalog.resource(:file, "/file_b")['ensure'].should == 'present' catalog.resource(:file, "/file_c")['ensure'].should == '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 + + 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 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 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 a hash selecting value - using two bloc parameters' do + 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 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 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 catalog.resource(:file, "/file_0.false")['ensure'].should == 'present' catalog.resource(:file, "/file_1.false")['ensure'].should == '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 catalog.resource(:file, "/file_0.")['ensure'].should == '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/parser/methods/reduce_spec.rb index 4f0c14e5e..9e116ffde 100644 --- a/spec/unit/parser/methods/reduce_spec.rb +++ b/spec/unit/parser/methods/reduce_spec.rb @@ -1,78 +1,88 @@ require 'puppet' require 'spec_helper' require 'puppet_spec/compiler' describe 'the reduce method' do include PuppetSpec::Compiler before :all do # enable switching back @saved_parser = Puppet[:parser] # These tests only work with future parser 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 as" do it 'reduce on an array' do catalog = compile_to_catalog(<<-MANIFEST) $a = [1,2,3] $b = $a.reduce |$memo, $x| { $memo + $x } file { "/file_$b": ensure => present } MANIFEST catalog.resource(:file, "/file_6")['ensure'].should == 'present' end + it 'reduce on an array with patures rest in lambda' do + catalog = compile_to_catalog(<<-MANIFEST) + $a = [1,2,3] + $b = $a.reduce |*$mx| { $mx[0] + $mx[1] } + file { "/file_$b": ensure => present } + MANIFEST + + catalog.resource(:file, "/file_6")['ensure'].should == 'present' + end + it 'reduce on enumerable type' do catalog = compile_to_catalog(<<-MANIFEST) $a = Integer[1,3] $b = $a.reduce |$memo, $x| { $memo + $x } file { "/file_$b": ensure => present } MANIFEST catalog.resource(:file, "/file_6")['ensure'].should == 'present' end it 'reduce on an array with start value' do catalog = compile_to_catalog(<<-MANIFEST) $a = [1,2,3] $b = $a.reduce(4) |$memo, $x| { $memo + $x } file { "/file_$b": ensure => present } MANIFEST catalog.resource(:file, "/file_10")['ensure'].should == 'present' end it 'reduce on a hash' do catalog = compile_to_catalog(<<-MANIFEST) $a = {a=>1, b=>2, c=>3} $start = [ignored, 4] $b = $a.reduce |$memo, $x| {['sum', $memo[1] + $x[1]] } file { "/file_${$b[0]}_${$b[1]}": ensure => present } MANIFEST catalog.resource(:file, "/file_sum_6")['ensure'].should == 'present' end it 'reduce on a hash with start value' do catalog = compile_to_catalog(<<-MANIFEST) $a = {a=>1, b=>2, c=>3} $start = ['ignored', 4] $b = $a.reduce($start) |$memo, $x| { ['sum', $memo[1] + $x[1]] } file { "/file_${$b[0]}_${$b[1]}": ensure => present } MANIFEST catalog.resource(:file, "/file_sum_10")['ensure'].should == 'present' end end end diff --git a/spec/unit/parser/methods/slice_spec.rb b/spec/unit/parser/methods/slice_spec.rb index 1de1dd0f1..d619fd6f8 100644 --- a/spec/unit/parser/methods/slice_spec.rb +++ b/spec/unit/parser/methods/slice_spec.rb @@ -1,135 +1,148 @@ require 'puppet' require 'spec_helper' require 'puppet_spec/compiler' require 'rubygems' describe 'methods' do include PuppetSpec::Compiler 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 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 '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 + + 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 '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 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 '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 catalog.resource(:file, "/file_1.2")['ensure'].should == 'present' catalog.resource(:file, "/file_3.")['ensure'].should == '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 catalog.resource(:file, "/file_1.2")['ensure'].should == 'present' catalog.resource(:file, "/file_3.")['ensure'].should == '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 catalog.resource(:file, "/file_1.2")['ensure'].should == 'present' catalog.resource(:file, "/file_3.4")['ensure'].should == 'present' end it 'slice with integer' do catalog = compile_to_catalog(<<-MANIFEST) 4.slice(2) |$a,$b| { file { "/file_${a}.${b}": ensure => present } } MANIFEST catalog.resource(:file, "/file_0.1")['ensure'].should == 'present' catalog.resource(:file, "/file_2.3")['ensure'].should == 'present' end it 'slice with string' do catalog = compile_to_catalog(<<-MANIFEST) 'abcd'.slice(2) |$a,$b| { file { "/file_${a}.${b}": ensure => present } } MANIFEST catalog.resource(:file, "/file_a.b")['ensure'].should == 'present' catalog.resource(:file, "/file_c.d")['ensure'].should == '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 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 end end