diff --git a/lib/puppet/functions.rb b/lib/puppet/functions.rb index 3924effc1..fb441e332 100644 --- a/lib/puppet/functions.rb +++ b/lib/puppet/functions.rb @@ -1,626 +1,630 @@ # @note WARNING: This new function API is still under development and may change at any time # # Functions in the puppet language can be written in Ruby and distributed in # puppet modules. The function is written by creating a file in the module's # `lib/puppet/functions/` directory, where `` is # replaced with the module's name. The file should have the name of the function. # For example, to create a function named `min` in a module named `math` create # a file named `lib/puppet/functions/math/min.rb` in the module. # # A function is implemented by calling {Puppet::Functions.create_function}, and # passing it a block that defines the implementation of the function. # # Functions are namespaced inside the module that contains them. The name of # the function is prefixed with the name of the module. For example, # `math::min`. # # @example A simple function # Puppet::Functions.create_function('math::min') do # def min(a, b) # a <= b ? a : b # end # end # # Anatomy of a function # --- # # Functions are composed of four parts: the name, the implementation methods, # the signatures, and the dispatches. # # The name is the string given to the {Puppet::Functions.create_function} # method. It specifies the name to use when calling the function in the puppet # language, or from other functions. # # The implementation methods are ruby methods (there can be one or more) that # provide that actual implementation of the function's behavior. In the # simplest case the name of the function (excluding any namespace) and the name # of the method are the same. When that is done no other parts (signatures and # dispatches) need to be used. # # Signatures are a way of specifying the types of the function's parameters. # The types of any arguments will be checked against the types declared in the # signature and an error will be produced if they don't match. The types are # defined by using the same syntax for types as in the puppet language. # # Dispatches are how signatures and implementation methods are tied together. # When the function is called, puppet searches the signatures for one that # matches the supplied arguments. Each signature is part of a dispatch, which # specifies the method that should be called for that signature. When a # matching signature is found, the corrosponding method is called. # # Documentation for the function should be placed as comments to the # implementation method(s). # # @todo Documentation for individual instances of these new functions is not # yet tied into the puppet doc system. # # @example Dispatching to different methods by type # Puppet::Functions.create_function('math::min') do # dispatch :numeric_min do # param 'Numeric', :a # param 'Numeric', :b # end # # dispatch :string_min do # param 'String', :a # param 'String', :b # end # # def numeric_min(a, b) # a <= b ? a : b # end # # def string_min(a, b) # a.downcase <= b.downcase ? a : b # end # end # # Specifying Signatures # --- # # If nothing is specified, the number of arguments given to the function must # be the same as the number of parameters, and all of the parameters are of # type 'Any'. # # The following methods can be used to define a parameter # # - _param_ - the argument must be given in the call. # - _optional_param_ - the argument may be missing in the call. May not be followed by a required parameter # - _repeated_param_ - the type specifies a repeating type that occurs 0 to "infinite" number of times. It may only appear last or just before a block parameter. # - _block_param_ - a block must be given in the call. May only appear last. # - _optional_block_param_ - a block may be given in the call. May only appear last. # # The method name _required_param_ is an alias for _param_ and _required_block_param_ is an alias for _block_param_ # # A parameter definition takes 2 arguments: # - _type_ A string that must conform to a type in the puppet language # - _name_ A symbol denoting the parameter name # # Both arguments are optional when defining a block parameter. The _type_ defaults to "Callable" # and the _name_ to :block. # # Note that the dispatch definition is used to match arguments given in a call to the function with the defined # parameters. It then dispatches the call to the implementation method simply passing the given arguments on to # that method without any further processing and it is the responsibility of that method's implementor to ensure # that it can handle those arguments. # # @example Variable number of arguments # Puppet::Functions.create_function('foo') do # dispatch :foo do # param 'Numeric', :first # repeated_param 'Numeric', :values # end # # def foo(first, *values) # # do something # end # end # # There is no requirement for direct mapping between parameter definitions and the parameters in the # receiving implementation method so the following example is also legal. Here the dispatch will ensure # that `*values` in the receiver will be an array with at least one entry of type String and that any # remaining entries are of type Numeric: # # @example Inexact mapping or parameters # Puppet::Functions.create_function('foo') do # dispatch :foo do # param 'String', :first # repeated_param 'Numeric', :values # end # # def foo(*values) # # do something # end # end # # Access to Scope # --- # In general, functions should not need access to scope; they should be # written to act on their given input only. If they absolutely must look up # variable values, they should do so via the closure scope (the scope where # they are defined) - this is done by calling `closure_scope()`. # # Calling other Functions # --- # Calling other functions by name is directly supported via # {Puppet::Pops::Functions::Function#call_function}. This allows a function to # call other functions visible from its loader. # # @api public module Puppet::Functions # @param func_name [String, Symbol] a simple or qualified function name # @param block [Proc] the block that defines the methods and dispatch of the # Function to create # @return [Class] the newly created Function class # # @api public def self.create_function(func_name, function_base = Function, &block) if function_base.ancestors.none? { |s| s == Puppet::Pops::Functions::Function } raise ArgumentError, "Functions must be based on Puppet::Pops::Functions::Function. Got #{function_base}" end func_name = func_name.to_s # Creates an anonymous class to represent the function # The idea being that it is garbage collected when there are no more # references to it. # the_class = Class.new(function_base, &block) # Make the anonymous class appear to have the class-name # Even if this class is not bound to such a symbol in a global ruby scope and # must be resolved via the loader. # This also overrides any attempt to define a name method in the given block # (Since it redefines it) # # TODO, enforce name in lower case (to further make it stand out since Ruby # class names are upper case) # the_class.instance_eval do @func_name = func_name def name @func_name end end # Automatically create an object dispatcher based on introspection if the # loaded user code did not define any dispatchers. Fail if function name # does not match a given method name in user code. # if the_class.dispatcher.empty? simple_name = func_name.split(/::/)[-1] type, names = default_dispatcher(the_class, simple_name) last_captures_rest = (type.size_range[1] == Puppet::Pops::Types::INFINITY) the_class.dispatcher.add_dispatch(type, simple_name, names, nil, nil, nil, last_captures_rest) end # The function class is returned as the result of the create function method the_class end # Creates a default dispatcher configured from a method with the same name as the function # # @api private def self.default_dispatcher(the_class, func_name) unless the_class.method_defined?(func_name) raise ArgumentError, "Function Creation Error, cannot create a default dispatcher for function '#{func_name}', no method with this name found" end any_signature(*min_max_param(the_class.instance_method(func_name))) end # @api private def self.min_max_param(method) # Ruby 1.8.7 does not have support for details about parameters if method.respond_to?(:parameters) result = {:req => 0, :opt => 0, :rest => 0 } # TODO: Optimize into one map iteration that produces names map, and sets # count as side effect method.parameters.each { |p| result[p[0]] += 1 } from = result[:req] to = result[:rest] > 0 ? :default : from + result[:opt] names = method.parameters.map {|p| p[1].to_s } else # Cannot correctly compute the signature in Ruby 1.8.7 because arity for # optional values is screwed up (there is no way to get the upper limit), # an optional looks the same as a varargs In this case - the failure will # simply come later when the call fails # arity = method.arity from = arity >= 0 ? arity : -arity -1 to = arity >= 0 ? arity : :default # i.e. infinite (which is wrong when there are optional - flaw in 1.8.7) names = [] # no names available end [from, to, names] end # Construct a signature consisting of Object type, with min, and max, and given names. # (there is only one type entry). # # @api private def self.any_signature(from, to, names) # Construct the type for the signature # Tuple[Object, from, to] factory = Puppet::Pops::Types::TypeFactory [factory.callable(factory.any, from, to), names] end # Function # === # This class is the base class for all Puppet 4x Function API functions. A # specialized class is created for each puppet function. # # @api public class Function < Puppet::Pops::Functions::Function # @api private def self.builder @type_parser ||= Puppet::Pops::Types::TypeParser.new @all_callables ||= Puppet::Pops::Types::TypeFactory.all_callables DispatcherBuilder.new(dispatcher, @type_parser, @all_callables) end # Dispatch any calls that match the signature to the provided method name. # # @param meth_name [Symbol] The name of the implementation method to call # when the signature defined in the block matches the arguments to a call # to the function. # @return [Void] # # @api public def self.dispatch(meth_name, &block) builder().instance_eval do dispatch(meth_name, &block) end end end # Public api methods of the DispatcherBuilder are available within dispatch() # blocks declared in a Puppet::Function.create_function() call. # # @api public class DispatcherBuilder # @api private def initialize(dispatcher, type_parser, all_callables) @type_parser = type_parser @all_callables = all_callables @dispatcher = dispatcher end # Defines a required positional parameter with _type_ and _name_. # # @param type [String] The type specification for the parameter. # @param name [Symbol] The name of the parameter. This is primarily used # for error message output and does not have to match an implementation # method parameter. # @return [Void] # # @api public def param(type, name) internal_param(type, name) raise ArgumentError, 'A required parameter cannot be added after an optional parameter' if @min != @max @min += 1 @max += 1 end alias required_param param # Defines an optional positional parameter with _type_ and _name_. # May not be followed by a required parameter. # # @param type [String] The type specification for the parameter. # @param name [Symbol] The name of the parameter. This is primarily used # for error message output and does not have to match an implementation # method parameter. # @return [Void] # # @api public def optional_param(type, name) internal_param(type, name) @max += 1 end # Defines a repeated positional parameter with _type_ and _name_ that may occur 0 to "infinite" number of times. # It may only appear last or just before a block parameter. # # @param type [String] The type specification for the parameter. # @param name [Symbol] The name of the parameter. This is primarily used # for error message output and does not have to match an implementation # method parameter. # @return [Void] # # @api public def repeated_param(type, name) - internal_param(type, name) + internal_param(type, name, true) @max = :default end alias optional_repeated_param repeated_param # Defines a repeated positional parameter with _type_ and _name_ that may occur 1 to "infinite" number of times. # It may only appear last or just before a block parameter. # # @param type [String] The type specification for the parameter. # @param name [Symbol] The name of the parameter. This is primarily used # for error message output and does not have to match an implementation # method parameter. # @return [Void] # # @api public def required_repeated_param(type, name) - internal_param(type, name) + internal_param(type, name, true) raise ArgumentError, 'A required repeated parameter cannot be added after an optional parameter' if @min != @max @min += 1 @max = :default end # Defines one required block parameter that may appear last. If type and name is missing the # default type is "Callable", and the name is "block". If only one # parameter is given, then that is the name and the type is "Callable". # # @api public def block_param(*type_and_name) case type_and_name.size when 0 # the type must be an independent instance since it will be contained in another type type = @all_callables.copy name = 'block' when 1 # the type must be an independent instance since it will be contained in another type type = @all_callables.copy name = type_and_name[0] when 2 type_string, name = type_and_name type = @type_parser.parse(type_string) else raise ArgumentError, "block_param accepts max 2 arguments (type, name), got #{type_and_name.size}." end unless Puppet::Pops::Types::TypeCalculator.is_kind_of_callable?(type, false) raise ArgumentError, "Expected PCallableType or PVariantType thereof, got #{type.class}" end unless name.is_a?(String) || name.is_a?(Symbol) raise ArgumentError, "Expected block_param name to be a String or Symbol, got #{name.class}" end if @block_type.nil? @block_type = type @block_name = name else raise ArgumentError, 'Attempt to redefine block' end end alias required_block_param block_param # Defines one optional block parameter that may appear last. If type or name is missing the # defaults are "any callable", and the name is "block". The implementor of the dispatch target # must use block = nil when it is optional (or an error is raised when the call is made). # # @api public def optional_block_param(*type_and_name) # same as required, only wrap the result in an optional type required_block_param(*type_and_name) @block_type = Puppet::Pops::Types::TypeFactory.optional(@block_type) end private # @api private - def internal_param(type, name) + def internal_param(type, name, repeat = false) raise ArgumentError, 'Parameters cannot be added after a block parameter' unless @block_type.nil? raise ArgumentError, 'Parameters cannot be added after a repeated parameter' if @max == :default if type.is_a?(String) @types << type @names << name # mark what should be picked for this position when dispatching - @weaving << @names.size()-1 + if repeat + @weaving << -@names.size() + else + @weaving << @names.size()-1 + end else raise ArgumentError, "Parameter 'type' must be a String reference to a Puppet Data Type. Got #{type.class}" end end # @api private def dispatch(meth_name, &block) # an array of either an index into names/types, or an array with # injection information [type, name, injection_name] used when the call # is being made to weave injections into the given arguments. # @types = [] @names = [] @weaving = [] @injections = [] @min = 0 @max = 0 @block_type = nil @block_name = nil self.instance_eval &block callable_t = create_callable(@types, @block_type, @min, @max) @dispatcher.add_dispatch(callable_t, meth_name, @names, @block_name, @injections, @weaving, @max == :default) end # Handles creation of a callable type from strings specifications of puppet # types and allows the min/max occurs of the given types to be given as one # or two integer values at the end. The given block_type should be # Optional[Callable], Callable, or nil. # # @api private def create_callable(types, block_type, from, to) mapped_types = types.map do |t| @type_parser.parse(t) end if from != to # :optional and/or :repeated parameters are present. mapped_types << from mapped_types << to end if block_type mapped_types << block_type end Puppet::Pops::Types::TypeFactory.callable(*mapped_types) end end private # @note WARNING: This style of creating functions is not public. It is a system # under development that will be used for creating "system" functions. # # This is a private, internal, system for creating functions. It supports # everything that the public function definition system supports as well as a # few extra features. # # Injection Support # === # The Function API supports injection of data and services. It is possible to # make injection that takes effect when the function is loaded (for services # and runtime configuration that does not change depending on how/from where # in what context the function is called. It is also possible to inject and # weave argument values into a call. # # Injection of attributes # --- # Injection of attributes is performed by one of the methods `attr_injected`, # and `attr_injected_producer`. The injected attributes are available via # accessor method calls. # # @example using injected attributes # Puppet::Functions.create_function('test') do # attr_injected String, :larger, 'message_larger' # attr_injected String, :smaller, 'message_smaller' # def test(a, b) # a > b ? larger() : smaller() # end # end # # @api private class InternalFunction < Function # @api private def self.builder @type_parser ||= Puppet::Pops::Types::TypeParser.new @all_callables ||= Puppet::Pops::Types::TypeFactory.all_callables InternalDispatchBuilder.new(dispatcher, @type_parser, @all_callables) end # Defines class level injected attribute with reader method # # @api private def self.attr_injected(type, attribute_name, injection_name = nil) define_method(attribute_name) do ivar = :"@#{attribute_name.to_s}" unless instance_variable_defined?(ivar) injector = Puppet.lookup(:injector) instance_variable_set(ivar, injector.lookup(closure_scope, type, injection_name)) end instance_variable_get(ivar) end end # Defines class level injected producer attribute with reader method # # @api private def self.attr_injected_producer(type, attribute_name, injection_name = nil) define_method(attribute_name) do ivar = :"@#{attribute_name.to_s}" unless instance_variable_defined?(ivar) injector = Puppet.lookup(:injector) instance_variable_set(ivar, injector.lookup_producer(closure_scope, type, injection_name)) end instance_variable_get(ivar) end end # Allows the implementation of a function to call other functions by name and pass the caller # scope. The callable functions are those visible to the same loader that loaded this function # (the calling function). # # @param scope [Puppet::Parser::Scope] The caller scope # @param function_name [String] The name of the function # @param *args [Object] splat of arguments # @return [Object] The result returned by the called function # # @api public def call_function_with_scope(scope, function_name, *args) internal_call_function(scope, function_name, args) end end # @note WARNING: This style of creating functions is not public. It is a system # under development that will be used for creating "system" functions. # # Injection and Weaving of parameters # --- # It is possible to inject and weave parameters into a call. These extra # parameters are not part of the parameters passed from the Puppet logic, and # they can not be overridden by parameters given as arguments in the call. # They are invisible to the Puppet Language. # # @example using injected parameters # Puppet::Functions.create_function('test') do # dispatch :test do # param 'Scalar', 'a' # param 'Scalar', 'b' # injected_param 'String', 'larger', 'message_larger' # injected_param 'String', 'smaller', 'message_smaller' # end # def test(a, b, larger, smaller) # a > b ? larger : smaller # end # end # # The function in the example above is called like this: # # test(10, 20) # # Using injected value as default # --- # Default value assignment is handled by using the regular Ruby mechanism (a # value is assigned to the variable). The dispatch simply indicates that the # value is optional. If the default value should be injected, it can be # handled different ways depending on what is desired: # # * by calling the accessor method for an injected Function class attribute. # This is suitable if the value is constant across all instantiations of the # function, and across all calls. # * by injecting a parameter into the call # to the left of the parameter, and then assigning that as the default value. # * One of the above forms, but using an injected producer instead of a # directly injected value. # # @example method with injected default values # Puppet::Functions.create_function('test') do # dispatch :test do # injected_param String, 'b_default', 'b_default_value_key' # param 'Scalar', 'a' # param 'Scalar', 'b' # end # def test(b_default, a, b = b_default) # # ... # end # end # # @api private class InternalDispatchBuilder < DispatcherBuilder def scope_param() @injections << [:scope, 'scope', '', :dispatcher_internal] # mark what should be picked for this position when dispatching @weaving << [@injections.size()-1] end # TODO: is param name really needed? Perhaps for error messages? (it is unused now) # # @api private def injected_param(type, name, injection_name = '') @injections << [type, name, injection_name] # mark what should be picked for this position when dispatching @weaving << [@injections.size() -1] end # TODO: is param name really needed? Perhaps for error messages? (it is unused now) # # @api private def injected_producer_param(type, name, injection_name = '') @injections << [type, name, injection_name, :producer] # mark what should be picked for this position when dispatching @weaving << [@injections.size()-1] end end end diff --git a/lib/puppet/functions/defined.rb b/lib/puppet/functions/defined.rb new file mode 100644 index 000000000..3af50197a --- /dev/null +++ b/lib/puppet/functions/defined.rb @@ -0,0 +1,132 @@ +# Determines whether +# a given class or resource type is defined. This function can also determine whether a +# specific resource has been declared, or whether a variable has been assigned a value +# (including undef...as opposed to never having been assigned anything). Returns true +# or false. Accepts class names, type names, resource references, and variable +# reference strings of the form '$name'. When more than one argument is +# supplied, defined() returns true if any are defined. +# +# The `defined` function checks both native and defined types, including types +# provided as plugins via modules. Types and classes are both checked using their names: +# +# defined("file") +# defined("customtype") +# defined("foo") +# defined("foo::bar") +# defined('$name') +# +# Resource declarations are checked using resource references, e.g. +# `defined( File['/tmp/myfile'] )`, or `defined( Class[myclass] )`. +# Checking whether a given resource +# has been declared is, unfortunately, dependent on the evaluation order of +# the configuration, and the following code will not work: +# +# if defined(File['/tmp/foo']) { +# notify { "This configuration includes the /tmp/foo file.":} +# } +# file { "/tmp/foo": +# ensure => present, +# } +# +# However, this order requirement refers to evaluation order only, and ordering of +# resources in the configuration graph (e.g. with `before` or `require`) does not +# affect the behavior of `defined`. +# +# You may also search using types: +# +# defined(Resource['file','/some/file']) +# defined(File['/some/file']) +# defined(Class['foo']) +# +# The `defined` function does not answer if data types (e.g. `Integer`) are defined. If +# given the string 'integer' the result is false, and if given a non CatalogEntry type, +# an error is raised. +# +# The rules for asking for undef, empty strings, and the main class are different from 3.x +# (non future parser) and 4.x (with future parser or in Puppet 4.0.0 and later): +# +# defined('') # 3.x => true, 4.x => false +# defined(undef) # 3.x => true, 4.x => error +# defined('main') # 3.x => false, 4.x => true +# +# With the future parser, it is also possible to ask specifically if a name is +# a resource type (built in or defined), or a class, by giving its type: +# +# defined(Type[Class['foo']]) +# defined(Type[Resource['foo']]) +# +# Which is different from asking: +# +# defined('foo') +# +# Since the later returns true if 'foo' is either a class, a built-in resource type, or a user defined +# resource type, and a specific request like `Type[Class['foo']]` only returns true if `'foo'` is a class. +# +# @since 2.7.0 +# @since 3.6.0 variable reference and future parser types") +# @since 3.8.1 type specific requests with future parser +# +Puppet::Functions.create_function(:'defined', Puppet::Functions::InternalFunction) do + + ARG_TYPE = 'Variant[String,Type[CatalogEntry], Type[Type[CatalogEntry]]]' + + dispatch :is_defined do + scope_param + required_repeated_param ARG_TYPE, 'additional_args' + end + + def is_defined(scope, *vals) + vals.any? do |val| + case val + when String + if val =~ /^\$(.+)$/ + scope.exist?($1) + else + case val + when '' + next nil + when 'main' + # Find the main class (known as ''), it does not have to be in the catalog + scope.find_hostclass('') + else + # Find a resource type, definition or class definition + scope.find_resource_type(val) || scope.find_definition(val) || scope.find_hostclass(val) + #scope.compiler.findresource(:class, val) + end + end + when Puppet::Resource + # Find instance of given resource type and title that is in the catalog + scope.compiler.findresource(val.type, val.title) + + when Puppet::Pops::Types::PResourceType + raise ArgumentError, 'The given resource type is a reference to all kind of types' if val.type_name.nil? + if val.title.nil? + scope.find_builtin_resource_type(val.type_name) || scope.find_definition(val.type_name) + else + scope.compiler.findresource(val.type_name, val.title) + end + + when Puppet::Pops::Types::PHostClassType + raise ArgumentError, 'The given class type is a reference to all classes' if val.class_name.nil? + scope.compiler.findresource(:class, val.class_name) + + when Puppet::Pops::Types::PType + case val.type + when Puppet::Pops::Types::PResourceType + # It is most reasonable to take Type[File] and Type[File[foo]] to mean the same as if not wrapped in a Type + # Since the difference between File and File[foo] already captures the distinction of type vs instance. + is_defined(scope, val.type) + + when Puppet::Pops::Types::PHostClassType + # Interpreted as asking if a class (and nothing else) is defined without having to be included in the catalog + # (this is the same as asking for just the class' name, but with the added certainty that it cannot be a defined type. + # + raise ArgumentError, 'The given class type is a reference to all classes' if val.type.class_name.nil? + scope.find_hostclass(val.type.class_name) + end + else + raise ArgumentError, "Invalid argument of type '#{val.class}' to 'defined'" + end + end + end +end diff --git a/lib/puppet/parser/functions/defined.rb b/lib/puppet/parser/functions/defined.rb index 4ec2018a4..37a3b6918 100644 --- a/lib/puppet/parser/functions/defined.rb +++ b/lib/puppet/parser/functions/defined.rb @@ -1,73 +1,98 @@ # Test whether a given class or definition is defined Puppet::Parser::Functions::newfunction(:defined, :type => :rvalue, :arity => -2, :doc => "Determine whether a given class or resource type is defined. This function can also determine whether a specific resource has been declared, or whether a variable has been assigned a value (including undef...as opposed to never having been assigned anything). Returns true or false. Accepts class names, type names, resource references, and variable reference strings of the form '$name'. When more than one argument is supplied, defined() returns true if any are defined. The `defined` function checks both native and defined types, including types provided as plugins via modules. Types and classes are both checked using their names: defined(\"file\") defined(\"customtype\") defined(\"foo\") defined(\"foo::bar\") defined(\'$name\') Resource declarations are checked using resource references, e.g. `defined( File['/tmp/myfile'] )`. Checking whether a given resource has been declared is, unfortunately, dependent on the parse order of the configuration, and the following code will not work: if defined(File['/tmp/foo']) { notify { \"This configuration includes the /tmp/foo file.\":} } file { \"/tmp/foo\": ensure => present, } However, this order requirement refers to parse order only, and ordering of resources in the configuration graph (e.g. with `before` or `require`) does not affect the behavior of `defined`. If the future parser is in effect, you may also search using types: defined(Resource[\'file\',\'/some/file\']) defined(File[\'/some/file\']) defined(Class[\'foo\']) + The `defined` function does not answer if 4.x data types (e.g. `Integer`) are defined. If + given the string 'integer' the result is false, and if given a non CatalogEntry type, + an error is raised. + + The rules for asking for undef, empty strings, and the main class are different from 3.x + (non future parser) and 4.x (with future parser or in Puppet 4.0.0 and later): + + defined('') # 3.x => true, 4.x => false + defined(undef) # 3.x => true, 4.x => error + defined('main') # 3.x => false, 4.x => true + + With the future parser, it is also possible to ask specifically if a name is + a resource type (built in or defined), or a class, by giving its type: + + defined(Type[Class['foo']]) + defined(Type[Resource['foo']]) + + Which is different from asking: + + defined('foo') + + Since the later returns true if 'foo' is either a class, a built-in resource type, or a user defined + resource type, and a specific request like `Type[Class['foo']]` only returns true if `'foo'` is a class. + - Since 2.7.0 - - Since 3.6.0 variable reference and future parser types") do |vals| + - Since 3.6.0 variable reference and future parser types + - Since 3.8.1 type specific requests with future parser") do |vals| vals = [vals] unless vals.is_a?(Array) vals.any? do |val| case val when String if m = /^\$(.+)$/.match(val) exist?(m[1]) else find_resource_type(val) or find_definition(val) or find_hostclass(val) end when Puppet::Resource compiler.findresource(val.type, val.title) else if Puppet.future_parser? case val when Puppet::Pops::Types::PResourceType raise ArgumentError, "The given resource type is a reference to all kind of types" if val.type_name.nil? if val.title.nil? find_builtin_resource_type(val.type_name) || find_definition(val.type_name) else compiler.findresource(val.type_name, val.title) end when Puppet::Pops::Types::PHostClassType raise ArgumentError, "The given class type is a reference to all classes" if val.class_name.nil? find_hostclass(val.class_name) end else raise ArgumentError, "Invalid argument of type '#{val.class}' to 'defined'" end end end end diff --git a/lib/puppet/pops/functions/dispatch.rb b/lib/puppet/pops/functions/dispatch.rb index c4b1d8abc..8a5eafa56 100644 --- a/lib/puppet/pops/functions/dispatch.rb +++ b/lib/puppet/pops/functions/dispatch.rb @@ -1,80 +1,85 @@ # Defines a connection between a implementation method and the signature that # the method will handle. # # This interface should not be used directly. Instead dispatches should be # constructed using the DSL defined in {Puppet::Functions}. # # @api private class Puppet::Pops::Functions::Dispatch < Puppet::Pops::Evaluator::CallableSignature # @api public attr_reader :type # TODO: refactor to parameter_names since that makes it API attr_reader :param_names attr_reader :injections # Describes how arguments are woven if there are injections, a regular argument is a given arg index, an array # an injection description. # attr_reader :weaving # @api public attr_reader :block_name # @api private def initialize(type, method_name, param_names, block_name, injections, weaving, last_captures) @type = type @method_name = method_name @param_names = param_names || [] @block_name = block_name @injections = injections || [] @weaving = weaving @last_captures = last_captures end # @api private def parameter_names @param_names end # @api private def last_captures_rest? !! @last_captures end # @api private def invoke(instance, calling_scope, args, &block) instance.send(@method_name, *weave(calling_scope, args), &block) end # @api private def weave(scope, args) # no need to weave if there are no injections if @injections.empty? args else injector = nil # lazy lookup of injector Puppet.lookup(:injector) new_args = [] @weaving.each do |knit| if knit.is_a?(Array) injection_data = @injections[knit[0]] new_args << case injection_data[3] when :dispatcher_internal # currently only supports :scope injection scope when :producer injector ||= Puppet.lookup(:injector) injector.lookup_producer(scope, injection_data[0], injection_data[2]) else injector ||= Puppet.lookup(:injector) injector.lookup(scope, injection_data[0], injection_data[2]) end else # Careful so no new nil arguments are added since they would override default # parameter values in the received - new_args << args[knit] if knit < args.size + if knit < 0 + idx = -knit - 1 + new_args += args[idx..-1] if idx < args.size + else + new_args << args[knit] if knit < args.size + end end end new_args end end end diff --git a/spec/unit/functions/defined_spec.rb b/spec/unit/functions/defined_spec.rb new file mode 100755 index 000000000..590749c4c --- /dev/null +++ b/spec/unit/functions/defined_spec.rb @@ -0,0 +1,291 @@ +#! /usr/bin/env ruby +require 'spec_helper' +require 'puppet/pops' +require 'puppet/loaders' + +describe "the 'defined' function" do + after(:all) { Puppet::Pops::Loaders.clear } + + # This loads the function once and makes it easy to call it + # It does not matter that it is not bound to the env used later since the function + # looks up everything via the scope that is given to it. + # The individual tests needs to have a fresh env/catalog set up + # + let(:loaders) { Puppet::Pops::Loaders.new(Puppet::Node::Environment.create(:testing, [])) } + let(:func) { loaders.puppet_system_loader.load(:function, 'defined') } + + before :each do + # This is only for the 4.x version of the defined function + Puppet[:parser] = 'future' + # A fresh environment is needed for each test since tests creates types and resources + environment = Puppet::Node::Environment.create(:testing, []) + @node = Puppet::Node.new('yaynode', :environment => environment) + @known_resource_types = environment.known_resource_types + @compiler = Puppet::Parser::Compiler.new(@node) + @scope = Puppet::Parser::Scope.new(@compiler) + end + + def newclass(name) + @known_resource_types.add Puppet::Resource::Type.new(:hostclass, name) + end + + def newdefine(name) + @known_resource_types.add Puppet::Resource::Type.new(:definition, name) + end + + def newresource(type, title) + resource = Puppet::Resource.new(type, title) + @compiler.add_resource(@scope, resource) + resource + end + + #--- CLASS + # + context 'can determine if a class' do + context 'is defined' do + + it 'by using the class name in string form' do + newclass 'yayness' + expect(func.call(@scope, 'yayness')).to be_true + end + + it 'by using a Type[Class[name]] type reference' do + name = 'yayness' + newclass name + class_type = Puppet::Pops::Types::TypeFactory.host_class(name) + type_type = Puppet::Pops::Types::TypeFactory.type_type(class_type) + expect(func.call(@scope, type_type)).to be_true + end + end + + context 'is not defined' do + it 'by using the class name in string form' do + expect(func.call(@scope, 'yayness')).to be_false + end + + it 'even if there is a define, by using a Type[Class[name]] type reference' do + name = 'yayness' + newdefine name + class_type = Puppet::Pops::Types::TypeFactory.host_class(name) + type_type = Puppet::Pops::Types::TypeFactory.type_type(class_type) + expect(func.call(@scope, type_type)).to be_false + end + end + + context 'is defined and realized' do + it 'by using a Class[name] reference' do + name = 'cowabunga' + newclass name + newresource(:class, name) + class_type = Puppet::Pops::Types::TypeFactory.host_class(name) + expect(func.call(@scope, class_type)).to be_true + end + end + + context 'is not realized' do + it '(although defined) by using a Class[name] reference' do + name = 'cowabunga' + newclass name + class_type = Puppet::Pops::Types::TypeFactory.host_class(name) + expect(func.call(@scope, class_type)).to be_false + end + + it '(and not defined) by using a Class[name] reference' do + name = 'cowabunga' + class_type = Puppet::Pops::Types::TypeFactory.host_class(name) + expect(func.call(@scope, class_type)).to be_false + end + end + end + + #---RESOURCE TYPE + # + context 'can determine if a resource type' do + context 'is defined' do + + it 'by using the type name (of a built in type) in string form' do + expect(func.call(@scope, 'file')).to be_true + end + + it 'by using the type name (of a resource type) in string form' do + newdefine 'yayness' + expect(func.call(@scope, 'yayness')).to be_true + end + + it 'by using a File type reference (built in type)' do + resource_type = Puppet::Pops::Types::TypeFactory.resource('file') + type_type = Puppet::Pops::Types::TypeFactory.type_type(resource_type) + expect(func.call(@scope, type_type)).to be_true + end + + it 'by using a Type[File] type reference' do + resource_type = Puppet::Pops::Types::TypeFactory.resource('file') + type_type = Puppet::Pops::Types::TypeFactory.type_type(resource_type) + expect(func.call(@scope, type_type)).to be_true + end + + it 'by using a Resource[T] type reference (defined type)' do + name = 'yayness' + newdefine name + resource_type = Puppet::Pops::Types::TypeFactory.resource(name) + expect(func.call(@scope, resource_type)).to be_true + end + + it 'by using a Type[Resource[T]] type reference (defined type)' do + name = 'yayness' + newdefine name + resource_type = Puppet::Pops::Types::TypeFactory.resource(name) + type_type = Puppet::Pops::Types::TypeFactory.type_type(resource_type) + expect(func.call(@scope, type_type)).to be_true + end + end + + context 'is not defined' do + it 'by using the resource name in string form' do + expect(func.call(@scope, 'notatype')).to be_false + end + + it 'even if there is a class with the same name, by using a Type[Resource[T]] type reference' do + name = 'yayness' + newclass name + resource_type = Puppet::Pops::Types::TypeFactory.resource(name) + type_type = Puppet::Pops::Types::TypeFactory.type_type(resource_type) + expect(func.call(@scope, type_type)).to be_false + end + end + + context 'is defined and instance realized' do + it 'by using a Resource[T, title] reference for a built in type' do + type_name = 'file' + title = '/tmp/myfile' + newdefine type_name + newresource(type_name, title) + class_type = Puppet::Pops::Types::TypeFactory.resource(type_name, title) + expect(func.call(@scope, class_type)).to be_true + end + + it 'by using a Resource[T, title] reference for a defined type' do + type_name = 'meme' + title = 'cowabunga' + newdefine type_name + newresource(type_name, title) + class_type = Puppet::Pops::Types::TypeFactory.resource(type_name, title) + expect(func.call(@scope, class_type)).to be_true + end + end + + context 'is not realized' do + it '(although defined) by using a Resource[T, title] reference or Type[Resource[T, title]] reference' do + type_name = 'meme' + title = 'cowabunga' + newdefine type_name + resource_type = Puppet::Pops::Types::TypeFactory.resource(type_name, title) + expect(func.call(@scope, resource_type)).to be_false + + type_type = Puppet::Pops::Types::TypeFactory.type_type(resource_type) + expect(func.call(@scope, type_type)).to be_false + end + + it '(and not defined) by using a Resource[T, title] reference or Type[Resource[T, title]] reference' do + type_name = 'meme' + title = 'cowabunga' + resource_type = Puppet::Pops::Types::TypeFactory.resource(type_name, title) + expect(func.call(@scope, resource_type)).to be_false + + type_type = Puppet::Pops::Types::TypeFactory.type_type(resource_type) + expect(func.call(@scope, type_type)).to be_false + end + end + end + + #---VARIABLES + # + context 'can determine if a variable' do + context 'is defined' do + it 'by giving the variable in string form' do + @scope['x'] = 'something' + expect(func.call(@scope, '$x')).to be_true + end + + it 'by giving a :: prefixed variable in string form' do + @compiler.topscope['x'] = 'something' + expect(func.call(@scope, '$::x')).to be_true + end + + it 'by giving a numeric variable in string form (when there is a match scope)' do + # with no match scope, there are no numeric variables defined + expect(func.call(@scope, '$0')).to be_false + expect(func.call(@scope, '$42')).to be_false + pattern = Regexp.new('.*') + @scope.new_match_scope(pattern.match('anything')) + + # with a match scope, all numeric variables are set (the match defines if they have a value or not, but they are defined) + # even if their value is undef. + expect(func.call(@scope, '$0')).to be_true + expect(func.call(@scope, '$42')).to be_true + end + end + + context 'is undefined' do + it 'by giving a :: prefixed or regular variable in string form' do + expect(func.call(@scope, '$x')).to be_false + expect(func.call(@scope, '$::x')).to be_false + end + end + end + + context 'has any? semantics when given multiple arguments' do + it 'and one of the names is a defined user defined type' do + newdefine 'yayness' + expect(func.call(@scope, 'meh', 'yayness', 'booness')).to be_true + end + + it 'and one of the names is a built type' do + expect(func.call(@scope, 'meh', 'file', 'booness')).to be_true + end + + it 'and one of the names is a defined class' do + newclass 'yayness' + expect(func.call(@scope, 'meh', 'yayness', 'booness')).to be_true + end + + it 'is true when at least one variable exists in scope' do + @scope['x'] = 'something' + expect(func.call(@scope, '$y', '$x', '$z')).to be_true + end + + it 'is false when none of the names are defined' do + expect(func.call(@scope, 'meh', 'yayness', 'booness')).to be_false + end + end + + it 'raises an argument error when asking if Resource type is defined' do + resource_type = Puppet::Pops::Types::TypeFactory.resource + expect { func.call(@scope, resource_type)}.to raise_error(ArgumentError, /reference to all.*type/) + end + + it 'raises an argument error if you ask if Class is defined' do + class_type = Puppet::Pops::Types::TypeFactory.host_class + expect { func.call(@scope, class_type) }.to raise_error(ArgumentError, /reference to all.*class/) + end + + it 'raises error if referencing undef' do + expect{func.call(@scope, nil)}.to raise_error(ArgumentError, /mis-matched arguments/) + end + + it 'raises error if referencing a number' do + expect{func.call(@scope, 42)}.to raise_error(ArgumentError, /mis-matched arguments/) + end + + it 'is false if referencing empty string' do + expect(func.call(@scope, '')).to be_false + end + + it "is true if referencing 'main'" do + # mimic what compiler does with "main" in intial import + newclass '' + newresource :class, '' + expect(func.call(@scope, 'main')).to be_true + end + +end diff --git a/spec/unit/functions4_spec.rb b/spec/unit/functions4_spec.rb index 008aa9fd5..c2e6fbe7f 100644 --- a/spec/unit/functions4_spec.rb +++ b/spec/unit/functions4_spec.rb @@ -1,809 +1,828 @@ require 'spec_helper' require 'puppet/pops' require 'puppet/loaders' require 'puppet_spec/pops' require 'puppet_spec/scope' module FunctionAPISpecModule class TestDuck end class TestFunctionLoader < Puppet::Pops::Loader::StaticLoader def initialize @functions = {} end def add_function(name, function) typed_name = Puppet::Pops::Loader::Loader::TypedName.new(:function, name) entry = Puppet::Pops::Loader::Loader::NamedEntry.new(typed_name, function, __FILE__) @functions[typed_name] = entry end # override StaticLoader def load_constant(typed_name) @functions[typed_name] end end end describe 'the 4x function api' do include FunctionAPISpecModule include PuppetSpec::Pops include PuppetSpec::Scope let(:loader) { FunctionAPISpecModule::TestFunctionLoader.new } it 'allows a simple function to be created without dispatch declaration' do f = Puppet::Functions.create_function('min') do def min(x,y) x <= y ? x : y end end # the produced result is a Class inheriting from Function expect(f.class).to be(Class) expect(f.superclass).to be(Puppet::Functions::Function) # and this class had the given name (not a real Ruby class name) expect(f.name).to eql('min') end it 'refuses to create functions that are not based on the Function class' do expect do Puppet::Functions.create_function('testing', Object) {} end.to raise_error(ArgumentError, 'Functions must be based on Puppet::Pops::Functions::Function. Got Object') end it 'a function without arguments can be defined and called without dispatch declaration' do f = create_noargs_function_class() func = f.new(:closure_scope, :loader) expect(func.call({})).to eql(10) end it 'an error is raised when calling a no arguments function with arguments' do f = create_noargs_function_class() func = f.new(:closure_scope, :loader) expect{func.call({}, 'surprise')}.to raise_error(ArgumentError, "function 'test' called with mis-matched arguments expected: test() - arg count {0} actual: test(String) - arg count {1}") end it 'a simple function can be called' do f = create_min_function_class() # TODO: Bogus parameters, not yet used func = f.new(:closure_scope, :loader) expect(func.is_a?(Puppet::Functions::Function)).to be_true expect(func.call({}, 10,20)).to eql(10) end it 'an error is raised if called with too few arguments' do f = create_min_function_class() # TODO: Bogus parameters, not yet used func = f.new(:closure_scope, :loader) expect(func.is_a?(Puppet::Functions::Function)).to be_true signature = if RUBY_VERSION =~ /^1\.8/ 'Any{2}' else 'Any x, Any y' end expect do func.call({}, 10) end.to raise_error(ArgumentError, "function 'min' called with mis-matched arguments expected: min(#{signature}) - arg count {2} actual: min(Integer) - arg count {1}") end it 'an error is raised if called with too many arguments' do f = create_min_function_class() # TODO: Bogus parameters, not yet used func = f.new(:closure_scope, :loader) expect(func.is_a?(Puppet::Functions::Function)).to be_true signature = if RUBY_VERSION =~ /^1\.8/ 'Any{2}' else 'Any x, Any y' end expect do func.call({}, 10, 10, 10) end.to raise_error(ArgumentError, Regexp.new(Regexp.escape( "function 'min' called with mis-matched arguments expected: min(#{signature}) - arg count {2} actual: min(Integer, Integer, Integer) - arg count {3}"))) end it 'an error is raised if simple function-name and method are not matched' do expect do f = create_badly_named_method_function_class() end.to raise_error(ArgumentError, /Function Creation Error, cannot create a default dispatcher for function 'mix', no method with this name found/) end it 'the implementation separates dispatchers for different functions' do # this tests that meta programming / construction puts class attributes in the correct class f1 = create_min_function_class() f2 = create_max_function_class() d1 = f1.dispatcher d2 = f2.dispatcher expect(d1).to_not eql(d2) expect(d1.dispatchers[0]).to_not eql(d2.dispatchers[0]) end context 'when using regular dispatch' do it 'a function can be created using dispatch and called' do f = create_min_function_class_using_dispatch() func = f.new(:closure_scope, :loader) expect(func.call({}, 3,4)).to eql(3) end it 'an error is raised with reference to given parameter names when called with mis-matched arguments' do f = create_min_function_class_using_dispatch() # TODO: Bogus parameters, not yet used func = f.new(:closure_scope, :loader) expect(func.is_a?(Puppet::Functions::Function)).to be_true expect do func.call({}, 10, 10, 10) end.to raise_error(ArgumentError, Regexp.new(Regexp.escape( "function 'min' called with mis-matched arguments expected: min(Numeric a, Numeric b) - arg count {2} actual: min(Integer, Integer, Integer) - arg count {3}"))) end it 'an error includes optional indicators and count for last element' do f = create_function_with_optionals_and_varargs() # TODO: Bogus parameters, not yet used func = f.new(:closure_scope, :loader) expect(func.is_a?(Puppet::Functions::Function)).to be_true signature = if RUBY_VERSION =~ /^1\.8/ 'Any{2,}' else 'Any x, Any y, Any a?, Any b?, Any c{0,}' end expect do func.call({}, 10) end.to raise_error(ArgumentError, "function 'min' called with mis-matched arguments expected: min(#{signature}) - arg count {2,} actual: min(Integer) - arg count {1}") end it 'an error includes optional indicators and count for last element when defined via dispatch' do f = create_function_with_optionals_and_repeated_via_dispatch() # TODO: Bogus parameters, not yet used func = f.new(:closure_scope, :loader) expect(func.is_a?(Puppet::Functions::Function)).to be_true expect do func.call({}, 10) end.to raise_error(ArgumentError, "function 'min' called with mis-matched arguments expected: min(Numeric x, Numeric y, Numeric a?, Numeric b?, Numeric c{0,}) - arg count {2,} actual: min(Integer) - arg count {1}") end it 'can create optional repeated parameter' do f = create_function_with_repeated func = f.new(:closure_scope, :loader) expect(func.call({})).to eql(0) expect(func.call({}, 1)).to eql(1) expect(func.call({}, 1, 2)).to eql(2) f = create_function_with_optional_repeated func = f.new(:closure_scope, :loader) expect(func.call({})).to eql(0) expect(func.call({}, 1)).to eql(1) expect(func.call({}, 1, 2)).to eql(2) end it 'can create required repeated parameter' do f = create_function_with_required_repeated func = f.new(:closure_scope, :loader) expect(func.call({}, 1)).to eql(1) expect(func.call({}, 1, 2)).to eql(2) expect { func.call({}) }.to raise_error(ArgumentError, "function 'count_args' called with mis-matched arguments expected: count_args(Any c{1,}) - arg count {1,} actual: count_args(Undef{0}) - arg count {0}") end + it 'can create scope_param followed by repeated parameter' do + f = create_function_with_scope_param_required_repeat + func = f.new(:closure_scope, :loader) + expect(func.call({}, 'yay', 1,2,3)).to eql([{}, 'yay',1,2,3]) + end + it 'a function can use inexact argument mapping' do f = create_function_with_inexact_dispatch func = f.new(:closure_scope, :loader) expect(func.call({}, 3,4,5)).to eql([Fixnum, Fixnum, Fixnum]) expect(func.call({}, 'Apple', 'Banana')).to eql([String, String]) end it 'a function can be created using dispatch and called' do f = create_min_function_class_disptaching_to_two_methods() func = f.new(:closure_scope, :loader) expect(func.call({}, 3,4)).to eql(3) expect(func.call({}, 'Apple', 'Banana')).to eql('Apple') end it 'a function can not be created with parameters declared after a repeated parameter' do expect { create_function_with_param_after_repeated }.to raise_error(ArgumentError, 'Parameters cannot be added after a repeated parameter') end it 'a function can not be created with required parameters declared after optional ones' do expect { create_function_with_rq_after_opt }.to raise_error(ArgumentError, 'A required parameter cannot be added after an optional parameter') end it 'a function can not be created with required repeated parameters declared after optional ones' do expect { create_function_with_rq_repeated_after_opt }.to raise_error(ArgumentError, 'A required repeated parameter cannot be added after an optional parameter') end it 'an error is raised with reference to multiple methods when called with mis-matched arguments' do f = create_min_function_class_disptaching_to_two_methods() # TODO: Bogus parameters, not yet used func = f.new(:closure_scope, :loader) expect(func.is_a?(Puppet::Functions::Function)).to be_true expect do func.call({}, 10, 10, 10) end.to raise_error(ArgumentError, "function 'min' called with mis-matched arguments expected one of: min(Numeric a, Numeric b) - arg count {2} min(String s1, String s2) - arg count {2} actual: min(Integer, Integer, Integer) - arg count {3}") end context 'can use injection' do before :all do injector = Puppet::Pops::Binder::Injector.create('test') do bind.name('a_string').to('evoe') bind.name('an_int').to(42) end Puppet.push_context({:injector => injector}, "injector for testing function API") end after :all do Puppet.pop_context() end it 'attributes can be injected' do f1 = create_function_with_class_injection() f = f1.new(:closure_scope, :loader) expect(f.test_attr2()).to eql("evoe") expect(f.serial().produce(nil)).to eql(42) expect(f.test_attr().class.name).to eql("FunctionAPISpecModule::TestDuck") end it 'parameters can be injected and woven with regular dispatch' do f1 = create_function_with_param_injection_regular() f = f1.new(:closure_scope, :loader) expect(f.call(nil, 10, 20)).to eql("evoe! 10, and 20 < 42 = true") expect(f.call(nil, 50, 20)).to eql("evoe! 50, and 20 < 42 = false") end end context 'when requesting a type' do it 'responds with a Callable for a single signature' do tf = Puppet::Pops::Types::TypeFactory fc = create_min_function_class_using_dispatch() t = fc.dispatcher.to_type expect(t.class).to be(Puppet::Pops::Types::PCallableType) expect(t.param_types.class).to be(Puppet::Pops::Types::PTupleType) expect(t.param_types.types).to eql([tf.numeric(), tf.numeric()]) expect(t.block_type).to be_nil end it 'responds with a Variant[Callable...] for multiple signatures' do tf = Puppet::Pops::Types::TypeFactory fc = create_min_function_class_disptaching_to_two_methods() t = fc.dispatcher.to_type expect(t.class).to be(Puppet::Pops::Types::PVariantType) expect(t.types.size).to eql(2) t1 = t.types[0] expect(t1.param_types.class).to be(Puppet::Pops::Types::PTupleType) expect(t1.param_types.types).to eql([tf.numeric(), tf.numeric()]) expect(t1.block_type).to be_nil t2 = t.types[1] expect(t2.param_types.class).to be(Puppet::Pops::Types::PTupleType) expect(t2.param_types.types).to eql([tf.string(), tf.string()]) expect(t2.block_type).to be_nil end end context 'supports lambdas' do it 'such that, a required block can be defined and given as an argument' do the_function = create_function_with_required_block_all_defaults().new(:closure_scope, :loader) result = the_function.call({}, 7) { |a,b| a < b ? a : b } expect(result).to eq(7) end it 'such that, a missing required block when called raises an error' do the_function = create_function_with_required_block_all_defaults().new(:closure_scope, :loader) expect do the_function.call({}, 10) end.to raise_error(ArgumentError, "function 'test' called with mis-matched arguments expected: test(Integer x, Callable block) - arg count {2} actual: test(Integer) - arg count {1}") end it 'such that, an optional block can be defined and given as an argument' do the_function = create_function_with_optional_block_all_defaults().new(:closure_scope, :loader) result = the_function.call({}, 4) { |a,b| a < b ? a : b } expect(result).to eql(4) end it 'such that, an optional block can be omitted when called and gets the value nil' do the_function = create_function_with_optional_block_all_defaults().new(:closure_scope, :loader) expect(the_function.call({}, 2)).to be_nil end it 'such that, a scope can be injected and a block can be used' do the_function = create_function_with_scope_required_block_all_defaults().new(:closure_scope, :loader) expect(the_function.call({}, 1) { |a,b| a < b ? a : b }).to eql(1) end end context 'provides signature information' do it 'about capture rest (varargs)' do fc = create_function_with_optionals_and_varargs signatures = fc.signatures expect(signatures.size).to eql(1) signature = signatures[0] expect(signature.last_captures_rest?).to be_true end it 'about optional and required parameters' do fc = create_function_with_optionals_and_varargs signature = fc.signatures[0] expect(signature.args_range).to eql( [2, Puppet::Pops::Types::INFINITY ] ) expect(signature.infinity?(signature.args_range[1])).to be_true end it 'about block not being allowed' do fc = create_function_with_optionals_and_varargs signature = fc.signatures[0] expect(signature.block_range).to eql( [ 0, 0 ] ) expect(signature.block_type).to be_nil end it 'about required block' do fc = create_function_with_required_block_all_defaults signature = fc.signatures[0] expect(signature.block_range).to eql( [ 1, 1 ] ) expect(signature.block_type).to_not be_nil end it 'about optional block' do fc = create_function_with_optional_block_all_defaults signature = fc.signatures[0] expect(signature.block_range).to eql( [ 0, 1 ] ) expect(signature.block_type).to_not be_nil end it 'about the type' do fc = create_function_with_optional_block_all_defaults signature = fc.signatures[0] expect(signature.type.class).to be(Puppet::Pops::Types::PCallableType) end # conditional on Ruby 1.8.7 which does not do parameter introspection if Method.method_defined?(:parameters) it 'about parameter names obtained from ruby introspection' do fc = create_min_function_class signature = fc.signatures[0] expect(signature.parameter_names).to eql(['x', 'y']) end end it 'about parameter names specified with dispatch' do fc = create_min_function_class_using_dispatch signature = fc.signatures[0] expect(signature.parameter_names).to eql(['a', 'b']) end it 'about block_name when it is *not* given in the definition' do # neither type, nor name fc = create_function_with_required_block_all_defaults signature = fc.signatures[0] expect(signature.block_name).to eql('block') # no name given, only type fc = create_function_with_required_block_given_type signature = fc.signatures[0] expect(signature.block_name).to eql('block') end it 'about block_name when it *is* given in the definition' do # neither type, nor name fc = create_function_with_required_block_default_type signature = fc.signatures[0] expect(signature.block_name).to eql('the_block') # no name given, only type fc = create_function_with_required_block_fully_specified signature = fc.signatures[0] expect(signature.block_name).to eql('the_block') end end context 'supports calling other functions' do before(:all) do Puppet.push_context( {:loaders => Puppet::Pops::Loaders.new(Puppet::Node::Environment.create(:testing, []))}) end after(:all) do Puppet.pop_context() end it 'such that, other functions are callable by name' do fc = Puppet::Functions.create_function(:test) do def test() # Call a function available in the puppet system call_function('assert_type', 'Integer', 10) end end # initiate the function the same way the loader initiates it f = fc.new(:closure_scope, Puppet.lookup(:loaders).puppet_system_loader) expect(f.call({})).to eql(10) end it 'such that, calling a non existing function raises an error' do fc = Puppet::Functions.create_function(:test) do def test() # Call a function not available in the puppet system call_function('no_such_function', 'Integer', 'hello') end end # initiate the function the same way the loader initiates it f = fc.new(:closure_scope, Puppet.lookup(:loaders).puppet_system_loader) expect{f.call({})}.to raise_error(ArgumentError, "Function test(): cannot call function 'no_such_function' - not found") end end context 'supports calling ruby functions with lambda from puppet' do before(:all) do Puppet.push_context( {:loaders => Puppet::Pops::Loaders.new(Puppet::Node::Environment.create(:testing, []))}) end after(:all) do Puppet.pop_context() end before(:each) do Puppet[:strict_variables] = true # These must be set since the is 3x logic that triggers on these even if the tests are explicit # about selection of parser and evaluator # Puppet[:parser] = 'future' # Puppetx cannot be loaded until the correct parser has been set (injector is turned off otherwise) require 'puppetx' end let(:parser) { Puppet::Pops::Parser::EvaluatingParser.new } let(:node) { 'node.example.com' } let(:scope) { s = create_test_scope_for_node(node); s } it 'function with required block can be called' do # construct ruby function to call fc = Puppet::Functions.create_function('testing::test') do dispatch :test do param 'Integer', 'x' # block called 'the_block', and using "all_callables" required_block_param #(all_callables(), 'the_block') end def test(x) # call the block with x yield(x) end end # add the function to the loader (as if it had been loaded from somewhere) the_loader = loader() f = fc.new({}, the_loader) loader.add_function('testing::test', f) # evaluate a puppet call source = "testing::test(10) |$x| { $x+1 }" program = parser.parse_string(source, __FILE__) Puppet::Pops::Adapters::LoaderAdapter.adapt(program.model).loader = the_loader expect(parser.evaluate(scope, program)).to eql(11) end end end def create_noargs_function_class f = Puppet::Functions.create_function('test') do def test() 10 end end end def create_min_function_class f = Puppet::Functions.create_function('min') do def min(x,y) x <= y ? x : y end end end def create_max_function_class f = Puppet::Functions.create_function('max') do def max(x,y) x >= y ? x : y end end end def create_badly_named_method_function_class f = Puppet::Functions.create_function('mix') do def mix_up(x,y) x <= y ? x : y end end end def create_min_function_class_using_dispatch f = Puppet::Functions.create_function('min') do dispatch :min do param 'Numeric', 'a' param 'Numeric', 'b' end def min(x,y) x <= y ? x : y end end end def create_min_function_class_disptaching_to_two_methods f = Puppet::Functions.create_function('min') do dispatch :min do param 'Numeric', 'a' param 'Numeric', 'b' end dispatch :min_s do param 'String', 's1' param 'String', 's2' end def min(x,y) x <= y ? x : y end def min_s(x,y) cmp = (x.downcase <=> y.downcase) cmp <= 0 ? x : y end end end def create_function_with_optionals_and_varargs f = Puppet::Functions.create_function('min') do def min(x,y,a=1, b=1, *c) x <= y ? x : y end end end def create_function_with_optionals_and_repeated_via_dispatch f = Puppet::Functions.create_function('min') do dispatch :min do param 'Numeric', :x param 'Numeric', :y optional_param 'Numeric', :a optional_param 'Numeric', :b repeated_param 'Numeric', :c end def min(x,y,a=1, b=1, *c) x <= y ? x : y end end end def create_function_with_repeated f = Puppet::Functions.create_function('count_args') do dispatch :count_args do repeated_param 'Any', :c end def count_args(*c) c.size end end end def create_function_with_optional_repeated f = Puppet::Functions.create_function('count_args') do dispatch :count_args do optional_repeated_param 'Any', :c end def count_args(*c) c.size end end end def create_function_with_required_repeated f = Puppet::Functions.create_function('count_args') do dispatch :count_args do required_repeated_param 'Any', :c end def count_args(*c) c.size end end end def create_function_with_inexact_dispatch f = Puppet::Functions.create_function('t1') do dispatch :t1 do param 'Numeric', :x param 'Numeric', :y repeated_param 'Numeric', :z end dispatch :t1 do param 'String', :x param 'String', :y repeated_param 'String', :z end def t1(first, *x) [first.class, *x.map {|e|e.class}] end end end def create_function_with_rq_after_opt f = Puppet::Functions.create_function('t1') do dispatch :t1 do optional_param 'Numeric', :x param 'Numeric', :y end def t1(*x) x end end end def create_function_with_rq_repeated_after_opt f = Puppet::Functions.create_function('t1') do dispatch :t1 do optional_param 'Numeric', :x required_repeated_param 'Numeric', :y end def t1(x, *y) x end end end def create_function_with_param_after_repeated f = Puppet::Functions.create_function('t1') do dispatch :t1 do repeated_param 'Numeric', :x param 'Numeric', :y end def t1(*x) x end end end def create_function_with_class_injection f = Puppet::Functions.create_function('test', Puppet::Functions::InternalFunction) do attr_injected Puppet::Pops::Types::TypeFactory.type_of(FunctionAPISpecModule::TestDuck), :test_attr attr_injected Puppet::Pops::Types::TypeFactory.string(), :test_attr2, "a_string" attr_injected_producer Puppet::Pops::Types::TypeFactory.integer(), :serial, "an_int" def test(x,y,a=1, b=1, *c) x <= y ? x : y end end end def create_function_with_param_injection_regular f = Puppet::Functions.create_function('test', Puppet::Functions::InternalFunction) do attr_injected Puppet::Pops::Types::TypeFactory.type_of(FunctionAPISpecModule::TestDuck), :test_attr attr_injected Puppet::Pops::Types::TypeFactory.string(), :test_attr2, "a_string" attr_injected_producer Puppet::Pops::Types::TypeFactory.integer(), :serial, "an_int" dispatch :test do injected_param Puppet::Pops::Types::TypeFactory.string, 'x', 'a_string' injected_producer_param Puppet::Pops::Types::TypeFactory.integer, 'y', 'an_int' param 'Scalar', 'a' param 'Scalar', 'b' end def test(x,y,a,b) y_produced = y.produce(nil) "#{x}! #{a}, and #{b} < #{y_produced} = #{ !!(a < y_produced && b < y_produced)}" end end end def create_function_with_required_block_all_defaults f = Puppet::Functions.create_function('test') do dispatch :test do param 'Integer', 'x' # use defaults, any callable, name is 'block' block_param end def test(x) yield(8,x) end end end def create_function_with_scope_required_block_all_defaults f = Puppet::Functions.create_function('test', Puppet::Functions::InternalFunction) do dispatch :test do scope_param param 'Integer', 'x' # use defaults, any callable, name is 'block' required_block_param end def test(scope, x) yield(3,x) end end end def create_function_with_required_block_default_type f = Puppet::Functions.create_function('test') do dispatch :test do param 'Integer', 'x' # use defaults, any callable, name is 'block' required_block_param 'the_block' end def test(x) yield end end end + def create_function_with_scope_param_required_repeat + f = Puppet::Functions.create_function('test', Puppet::Functions::InternalFunction) do + dispatch :test do + scope_param + param 'Any', 'extra' + repeated_param 'Any', 'the_block' + end + def test(scope, *args) + [scope, *args] + end + end + end + def create_function_with_required_block_given_type f = Puppet::Functions.create_function('test') do dispatch :test do param 'Integer', 'x' required_block_param end def test(x) yield end end end def create_function_with_required_block_fully_specified f = Puppet::Functions.create_function('test') do dispatch :test do param 'Integer', 'x' # use defaults, any callable, name is 'block' required_block_param('Callable', 'the_block') end def test(x) yield end end end def create_function_with_optional_block_all_defaults f = Puppet::Functions.create_function('test') do dispatch :test do param 'Integer', 'x' # use defaults, any callable, name is 'block' optional_block_param end def test(x) yield(5,x) if block_given? end end end end