And now, the latest entry in the series about the new Puppet Type System introduces the capability to optionally type the parameters of defines, classes, lambdas and EPP template parameters.
There are also some new abilities and changes to the type system that I will cover in this post.
Optionally Typed Parameters
Writing high quality puppet code involves judicious use of type checking of given arguments. This is especially important when writing modules that are consumed by others. Anyone having written a serious module knows that it is a chore to not only deal with all of the parameters in the first place, but that type checking involves one extra call to a standard lib function per parameter. The result is simply lower signal to noise ratio.
From Puppet 3.7 when using the future parser/evaluator (and from 4.0 when the future parser/evaluator becomes the standard), you can now (optionally) type the parameters of defines, classes, EPP template parameters and lambdas. We have even thrown in support for varargs/captures-rest/splatted parameter in lambdas (excess arguments are delivered in an array).
Type Checking Defines and Classes
To opt in to type checking in defines and classes, simply give the type before the parameter declaration:
String $x # $x must be a String
String[1] $x # $x must be a String with at least one character
Array[String] $x # $x must be an Array, and all entries must be Strings
(See earlier posts in this series for other types, and the type system in general).
If you do not type a parameter, it defaults to the type Any
(renamed in 3.7.0 from Object
). And
this type accepts any argument including undef
.
define sinatra(String $regrets, Integer $amount) {
notice "$regrets, I had $amount, I did it my way. Do bi do bi doo..."
}
sinatra{ frank:
regrets => regrets,
amount => 2 # e.g. 'a few'
}
Which results in:
Notice: regrets, I had 2, I did it my way. Do bi do bi doo...
And if the wrong type is given:
sinatra{ frank:
regrets => regrets,
amount => 'a few'
}
The result is:
Error: Expected parameter 'amount' of 'Sinatra[frank]' to have type Integer, got String ...
And while on this topic, here are a couple of details:
- If you supply a default value, it is also type checked
- The type expressions can use global variables - e.g.
String[$minlength]
Type Checking Lambdas
Lambdas can now also have type checked parameters, and lambdas support the notion
of captures-rest (a.k.a varargs
, or splat
) by preceding the last parameter with a *
.
The type checking of lambdas, and the capabilities of passing arguments to lambdas has
been harmonized with the new function API (which I will presenting in a separate blog post).
Before showing how typed lambda parameters works, I want to tell you about a new function called with
that I will use to illustrate the new type checking capabilities.
The 'with' function
Andrew Parker
(@zaphod42) wrote a nifty little function called with
that is very useful for
illustrating (and testing) type checking.
It is also very useful in classes, where you would like to make some logic
(and in particular some variables) local/private to a block of code, this to avoid leaking non-API variables from your classes.
The with
is very simple - it just passes any given arguments to the given lambda. Hence its name; you can think of it as "with these variables, do this...".
with(1) | Integer $x | { notice $x }
Which, calls the lambda with the argument 1
, assigns it to $x
after having checked
for type compliance, and then notices it.
Now if you try this:
with(true) | Integer $x | { notice $x }
You get the error message:
Error while evaluating a Function Call, lambda called with mis-matched arguments
expected:
lambda(Integer x) - arg count {1}
actual:
lambda(Boolean) - arg count {1} at line 1:1 on node ...
Captures-Rest
As mentioned earlier, you can declare the last parameter with a preceding *
to make it capture
any excess arguments given in the call. The type that is given is the type of the elements of
an Array
that is constructed and passed to the lambda's body.
with(1,2,3) | Integer *$x | { notice $x }
Which results in:
Notice: [1, 2, 3]
There is one special rule for captures rest: If the type is an Array
type, it is used as
the type of the resulting array. Thus, if you want to accept elements of Array
type, you must
describe this as an Array
of Array
s (or use the Tuple
type). By declaring an Array
you can
constrain the number of excess arguments that the captures-rest parameter accepts.
with(1,2,3,4,5) | Array[Integer,0,3] *$x | { notice $x }
Will fail with the message:
Error while evaluating a Function Call, lambda called with mis-matched arguments
expected:
lambda(Integer x{0,3}) - arg count {0,3}
actual:
lambda(Integer, Integer, Integer, Integer, Integer) - arg count {5} at line 1:6 on node ...
A couple of details:
- The captures-rest does not affect how arguments are given (in the example above, the lambda could have been changed to have 3 individual parameters with a default value and still be called the same way, and it would accept the same given arguments).
- Captures rest is not supported for classes, defines, or for EPP parameters
Using the assert_type Function
And finally, if the built in type checking capabilities and the generic error messages that
they produce does not work for you there is an assert_type
function that gives you a lot
more flexibility.
In its basic form, it performs the same type checking as for typed parameters. The assert_type
function returns its second argument (the value) which means it can be used to type check
and assign to a resource attribute at the same time:
# Somewhere, there is this untyped definition (that does not work unless $x is
# an Integer).
define my_type($x) { ... }
# And you want to create an instance of it
#
my_type { 'it':
x => assert_type(Integer, hello)
}
Which results in:
Error: assert_type(): Expected type Integer does not match actual: String ...
The flexibility comes in the form of giving a lambda that is called if the assertion would fail
(the lambda "takes over").
This can be used to customize the error message, to issue a warning, and possibly return a default
sanitized value. Since the lambda takes over, you need to call fail
to halt the execution (if
that is what you want).
The lambda is given two arguments; the expected type, and the actual type (inferred from the
second argument given to assert_type
).
assert_type(Integer, hello) |$expected, $actual| {
fail "The value was a $expected must be an Integer (like 1 or 2 or...)"
}
Which results in:
Error: The value was a String must be an Integer (like 1 or 2 or...)
Type checking EPP
Type checking EPP works the same way as elsewhere, the type is simply stated before the parameter and defaults to Any. EPP parameters does not support captures-rest.
See the "Templating with Embedded Puppet Programming Language" for more information about EPP.
In this post
In this post I have showed how the new optionally typed parameters feature in Puppet 3.7.0's future parser/evaluator works and how type checking can be simplified in your Puppet logic.
The Type System Series of Blog Posts
You can find the rest of the blog posts about the type system here.