Module: Puppet::Functions

Defined in:
lib/puppet/functions.rb

Overview

TODO:

Documentation for individual instances of these new functions is not yet tied into the puppet doc system.

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/<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 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`.

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 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).

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’.

To express that the last parameter captures the rest, the method ‘last_captures_rest` can be called. This indicates that the last parameter is a varargs parameter and will be passed to the implementing method as an array of the given type.

When defining a dispatch for a function, the resulting dispatch matches against the specified argument types and min/max occurrence of optional entries. When the dispatch makes the call to the implementation method the arguments are simply passed and it is the responsibility of the method’s implementor to ensure it can handle those arguments (i.e. there is no check that what was declared as optional actually has a default value, and that a “captures rest” is declared using a ‘*`).

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 Pops::Functions::Function#call_function. This allows a function to call other functions visible from its loader.

Examples:

A simple function

Puppet::Functions.create_function('math::min') do
  def min(a, b)
    a <= b ? a : b
  end
end

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

Varargs

Puppet::Functions.create_function('foo') do
  dispatch :foo do
    param 'Numeric', 'first'
    param 'Numeric', 'values'
    last_captures_rest
  end

  def foo(first, *values)
    # do something
  end
end

Defined Under Namespace

Classes: DispatcherBuilder, Function, InternalDispatchBuilder, InternalFunction

Class Method Summary collapse

Class Method Details

.any_signature(from, to, names) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Construct a signature consisting of Object type, with min, and max, and given names. (there is only one type entry).



214
215
216
217
218
219
# File 'lib/puppet/functions.rb', line 214

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

.create_function(func_name, function_base = Function, &block) ⇒ Class<Function>

Returns the newly created Function class.

Parameters:

  • func_name (String, Symbol)

    a simple or qualified function name

  • block (Proc)

    the block that defines the methods and dispatch of the Function to create

Returns:



132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
# File 'lib/puppet/functions.rb', line 132

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 <func_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

.default_dispatcher(the_class, func_name) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Creates a default dispatcher configured from a method with the same name as the function



178
179
180
181
182
183
# File 'lib/puppet/functions.rb', line 178

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

.min_max_param(method) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.



186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
# File 'lib/puppet/functions.rb', line 186

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