module Puppet::Functions
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/<modulename>` directory, where `<modulename>` 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 corresponding method is called.
Special dispatches designed to create error messages for an argument mismatch can be added using the keyword `argument_mismatch` instead of `dispatch`. The method appointed by an `argument_mismatch` will be called with arguments just like a normal `dispatch` would, but the method must produce a string. The string is then used as the message in the `ArgumentError` that is raised when the method returns. A block parameter can be given, but it is not propagated in the method call.
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
@example Using an argument mismatch handler
Puppet::Functions.create_function('math::min') do dispatch :numeric_min do param 'Numeric', :a param 'Numeric', :b end argument_mismatch :on_error do param 'Any', :a param 'Any', :b end def numeric_min(a, b) a <= b ? a : b end def on_error(a, b) 'both arguments must be of type Numeric' 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
Public Class Methods
Construct a signature consisting of Object
type, with min, and max, and given names. (there is only one type entry).
@api private
# File lib/puppet/functions.rb 291 def self.any_signature(from, to, names) 292 # Construct the type for the signature 293 # Tuple[Object, from, to] 294 param_types = Puppet::Pops::Types::PTupleType.new([Puppet::Pops::Types::PAnyType::DEFAULT], Puppet::Pops::Types::PIntegerType.new(from, to)) 295 [Puppet::Pops::Types::PCallableType.new(param_types), names] 296 end
@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<Function>] the newly created Function
class
@api public
# File lib/puppet/functions.rb 184 def self.create_function(func_name, function_base = Function, &block) 185 # Ruby < 2.1.0 does not have method on Binding, can only do eval 186 # and it will fail unless protected with an if defined? if the local 187 # variable does not exist in the block's binder. 188 # 189 begin 190 loader = block.binding.eval('loader_injected_arg if defined?(loader_injected_arg)') 191 create_loaded_function(func_name, loader, function_base, &block) 192 rescue StandardError => e 193 raise ArgumentError, _("Function Load Error for function '%{function_name}': %{message}") % {function_name: func_name, message: e.message} 194 end 195 end
Creates a function in, or in a local loader under the given loader. This method should only be used when manually creating functions for the sake of testing. Functions
that are autoloaded should always use the `create_function` method and the autoloader will supply the correct loader.
@param func_name [String, Symbol] a simple or qualified function name @param loader [Puppet::Pops::Loaders::Loader] the loader loading the function @param block [Proc] the block that defines the methods and dispatch of the
Function to create
@return [Class<Function>] the newly created Function
class
@api public
# File lib/puppet/functions.rb 210 def self.create_loaded_function(func_name, loader, function_base = Function, &block) 211 if function_base.ancestors.none? { |s| s == Puppet::Pops::Functions::Function } 212 raise ArgumentError, _("Functions must be based on Puppet::Pops::Functions::Function. Got %{function_base}") % { function_base: function_base } 213 end 214 215 func_name = func_name.to_s 216 # Creates an anonymous class to represent the function 217 # The idea being that it is garbage collected when there are no more 218 # references to it. 219 # 220 # (Do not give the class the block here, as instance variables should be set first) 221 the_class = Class.new(function_base) 222 223 unless loader.nil? 224 the_class.instance_variable_set(:'@loader', loader.private_loader) 225 end 226 227 # Make the anonymous class appear to have the class-name <func_name> 228 # Even if this class is not bound to such a symbol in a global ruby scope and 229 # must be resolved via the loader. 230 # This also overrides any attempt to define a name method in the given block 231 # (Since it redefines it) 232 # 233 # TODO, enforce name in lower case (to further make it stand out since Ruby 234 # class names are upper case) 235 # 236 the_class.instance_eval do 237 @func_name = func_name 238 239 def name 240 @func_name 241 end 242 243 def loader 244 @loader 245 end 246 end 247 248 # The given block can now be evaluated and have access to name and loader 249 # 250 the_class.class_eval(&block) 251 252 # Automatically create an object dispatcher based on introspection if the 253 # loaded user code did not define any dispatchers. Fail if function name 254 # does not match a given method name in user code. 255 # 256 if the_class.dispatcher.empty? 257 simple_name = func_name.split(/::/)[-1] 258 type, names = default_dispatcher(the_class, simple_name) 259 last_captures_rest = (type.size_range[1] == Float::INFINITY) 260 the_class.dispatcher.add(Puppet::Pops::Functions::Dispatch.new(type, simple_name, names, last_captures_rest)) 261 end 262 263 # The function class is returned as the result of the create function method 264 the_class 265 end
Creates a default dispatcher configured from a method with the same name as the function
@api private
# File lib/puppet/functions.rb 270 def self.default_dispatcher(the_class, func_name) 271 unless the_class.method_defined?(func_name) 272 raise ArgumentError, _("Function Creation Error, cannot create a default dispatcher for function '%{func_name}', no method with this name found") % { func_name: func_name } 273 end 274 any_signature(*min_max_param(the_class.instance_method(func_name))) 275 end
@api private
# File lib/puppet/functions.rb 278 def self.min_max_param(method) 279 result = {:req => 0, :opt => 0, :rest => 0 } 280 # count per parameter kind, and get array of names 281 names = method.parameters.map { |p| result[p[0]] += 1 ; p[1].to_s } 282 from = result[:req] 283 to = result[:rest] > 0 ? :default : from + result[:opt] 284 [from, to, names] 285 end
Public Instance Methods
# File lib/puppet/functions.rb 243 def loader 244 @loader 245 end
# File lib/puppet/functions.rb 239 def name 240 @func_name 241 end