Ruby function signatures
Functions can specify how many arguments they expect, and a data type for each argument. The rule set for a function’s arguments is called a signature.
Because Puppet functions support more advanced argument checking than Ruby does, the Ruby functions API uses a lightweight domain-specific language (DSL) to specify signatures.
Ruby functions can have multiple signatures. Using multiple signatures is an easy way to have a function behave differently when passed by different types or quantities of arguments. Instead of writing complex logic to decide what to do, you can write separate implementations and let Puppet select the correct signature.
If a function has multiple signatures, Puppet uses its data type system to check each signature in order, comparing the allowed arguments to the arguments that were actually passed. As soon as Puppet finds a signature that can accept the provided arguments, it calls the associated implementation method, passing the arguments to that method. When the method finishes running and returns a value, Puppet uses that as the function’s return value. If none of the function’s signatures match the provided arguments, Puppet fails compilation and logs an error message describing the mismatch between the provided and expected arguments.
Conversion of Puppet and Ruby data types
When function arguments are passed to a Ruby method, they’re converted to Ruby objects. Similarly, when the Puppet manifest regains control, it converts the method’s return value into a Puppet data type.
Puppet | Ruby |
---|---|
Boolean |
Boolean
|
Undef |
NilClass (value nil ) |
String |
String
|
Number | subtype of Numeric
|
Array |
Array
|
Hash |
Hash
|
Default |
Symbol (value :default ) |
Regexp |
Regexp
|
Resource reference |
Puppet::Pops::Types::PResourceType, or
Puppet::Pops::Types::PHostClassType
|
Lambda (code block) |
Puppet::Pops::Evaluator::Closure
|
Data
type (Type ) |
A type class under Puppet::Pops::Types . For
example, Puppet::Pops::Types::PIntegerType
|
Writing signatures with dispatch
To write a signature, use the dispatch
method.
The dispatch
method takes:
-
The name of an implementation method, provided as a Ruby symbol. The corresponding method must be defined somewhere in the
create_function
block, usually after all the signatures. -
A block of code which only contains calls to the parameter and return methods.
# A signature that takes a single string argument
dispatch :camelcase do
param 'String', :input_string
return_type 'String' # optional
end
Using parameter methods
In the code block of a dispatch
statement, you can specify
arguments with special parameter methods. All of these methods take two arguments:
-
The allowed data type for the argument, as a string. Types are specified using Puppet’s data type syntax.
-
A user-facing name for the argument, as a symbol. This name is only used in documentation and error messages; it doesn’t have to match the argument names in the implementation method.
The order in which you call these methods is important: the function’s first argument goes first, followed by the second, and so on. The following parameter methods are available:
Model name | Description |
---|---|
param or required_param
|
A mandatory argument. You can use any number of these. Position: All mandatory arguments must come first. |
optional_param
|
An argument that can be omitted. You can use any number of these. When there are multiple optional arguments, users can only pass latter ones if they also provide values for the prior ones. This also applies to repeated arguments. Position: Must come after any required arguments. |
repeated_param or optional_repeated_param
|
A repeatable argument, which can receive zero or more values. A signature can only use one repeatable argument. Position: Must come after any non-repeating arguments. |
required_repeated_param
|
A repeatable argument, which must receive one or more values. A signature can only use one repeatable argument. Position: Must come after any non-repeating arguments. |
block_param or required_block_param
|
A mandatory lambda (block of Puppet code). A signature can only use one block. Position: Must come after all other arguments. |
optional_block_param
|
An optional lambda. A signature can only use one block. Position: Must come after all other arguments. |
When specifying a repeatable argument, note that:
-
In your implementation method, the repeatable argument appears as an array, which contains all the provided values that weren’t assigned to earlier, non-repeatable arguments.
-
The specified data type is matched against each value for the repeatable argument, not the repeatable argument as a whole. For example, if you want to accept any number of numbers, specify
repeated_param 'Numeric', :values_to_average, not repeated_param 'Array[Numeric]', :values_to_average
.
For lambdas, note that:
-
The data type for a block argument is
Callable
, or aVariant
that only containsCallables
. -
The
Callable
type can optionally specify the type and quantity of parameters that the lambda accepts. For example,Callable[String, String]
matches any lambda that can be called with a pair of strings.
Matching arguments with implementation methods
The implementation method that corresponds to a signature must be able to accept any combination of arguments that the signature might allow.
dispatch :epp do
required_param 'String', :template_file
optional_param 'Hash', :parameters_hash
end
def epp(template_file, parameters_hash = {})
# Note that parameters_hash defaults to an empty hash.
end
If the signature has a repeatable argument, the method must use a splat
parameter (*args
) as its final argument. For
example:dispatch :average do
required_repeated_param 'Numeric', :values_to_average
end
def average(*values)
# Inside the method, the `values` variable is an array of numbers.
end
Using the return_type
method
After specifying a signature’s arguments, you can use the return_type
method to specify the data type of its return value. This method
takes one argument: a Puppet data type, specified as a
string.
dispatch :camelcase do
param 'String', :input_string
return_type 'String'
end
The return type serves two purposes: documentation, and insurance. -
Puppet Strings can include information about the return value of a function.
-
If something goes wrong and your function returns the wrong type (like
nil
when a string is expected), it fails early with an informative error instead of allowing compilation to continue with an incorrect value.
Specifying aliases using local_types
If you're using complicated abstract data types to validate arguments, and you're using these data types in multiple signatures, they can become difficult to work with and maintain. In these cases, you can specify short aliases for your complex data types and use the aliases in your signatures.
To specify aliases, use the local_types
method:
-
You must call
local_types
only one time, before any signatures. -
The
local_types
method takes a lambda, which only contains calls to thetype
method. -
The
type
method takes a single string argument, in the form'<NAME> = <TYPE>'
.-
Capitalize the name, camel case word (
PartColor
), similar to a Ruby class name or the existing Puppet data types. -
The type is a valid Puppet data type.
-
local_types do
type 'PartColor = Enum[blue, red, green, mauve, teal, white, pine]'
type 'Part = Enum[cubicle_wall, chair, wall, desk, carpet]'
type 'PartToColorMap = Hash[Part, PartColor]'
end
dispatch :define_colors do
param 'PartToColorMap', :part_color_map
end
def define_colors(part_color_map)
# etc
end
Using automatic signatures
If your function only needs one signature, and you’re willing to skip the API’s data type checking, you can use an automatic signature. Be aware that there are some drawbacks to using automatic signatures.
Although functions with automatic signatures are simpler to write, they give worse error
messages when called incorrectly. You'll get a useful error if you call the function with
the wrong number of arguments, but if you give the wrong type of argument, you’ll get
something unhelpful. For example, if you pass the function above a number instead of a
string, it reports Error: Evaluation Error: Error while evaluating a
Function Call, undefined method 'split' for 5:Fixnum at /Users/nick/Desktop/test2.pp:7:8
on node magpie.lan.
If it's possible that your function will be used by anyone other than yourself, support
your users by writing a signature with dispatch
.
To use an automatic signature:
-
Do not write a
dispatch
block. -
Define one implementation method whose name matches the final namespace segment of the function’s name.
Puppet::Functions.create_function(:'stdlib::camelcase') do
def camelcase(str)
str.split('_').map{|e| e.capitalize}.join
end
end
In this case, because the last segment of stdlib::camelcase
is camelcase
, we must define a method named camelcase
.
Related topics: Ruby symbols, Abstract data types.