1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 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 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858
|
# frozen_string_literal: true
# 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
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<Function>] the newly created Function class
#
# @api public
def self.create_function(func_name, function_base = Function, &block)
# Ruby < 2.1.0 does not have method on Binding, can only do eval
# and it will fail unless protected with an if defined? if the local
# variable does not exist in the block's binder.
#
loader = block.binding.eval('loader_injected_arg if defined?(loader_injected_arg)')
create_loaded_function(func_name, loader, function_base, &block)
rescue StandardError => e
raise ArgumentError, _("Function Load Error for function '%{function_name}': %{message}") % { function_name: func_name, message: e.message }
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
def self.create_loaded_function(func_name, loader, 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}") % { function_base: 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.
#
# (Do not give the class the block here, as instance variables should be set first)
the_class = Class.new(function_base)
unless loader.nil?
the_class.instance_variable_set(:'@loader', loader.private_loader)
end
# 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
def loader
@loader
end
end
# The given block can now be evaluated and have access to name and loader
#
the_class.class_eval(&block)
# 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] == Float::INFINITY)
the_class.dispatcher.add(Puppet::Pops::Functions::Dispatch.new(type, simple_name, names, 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") % { func_name: func_name }
end
any_signature(*min_max_param(the_class.instance_method(func_name)))
end
# @api private
def self.min_max_param(method)
result = { :req => 0, :opt => 0, :rest => 0 }
# count per parameter kind, and get array of names
names = method.parameters.map { |p| result[p[0]] += 1; p[1].to_s }
from = result[:req]
to = result[:rest] > 0 ? :default : from + result[:opt]
[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]
param_types = Puppet::Pops::Types::PTupleType.new([Puppet::Pops::Types::PAnyType::DEFAULT], Puppet::Pops::Types::PIntegerType.new(from, to))
[Puppet::Pops::Types::PCallableType.new(param_types), 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
DispatcherBuilder.new(dispatcher, Puppet::Pops::Types::PCallableType::DEFAULT, loader)
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, false, &block)
end
end
# Like `dispatch` but used for a specific type of argument mismatch. Will not be include in the list of valid
# parameter overloads for the function.
#
# @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.argument_mismatch(meth_name, &block)
builder().instance_eval do
dispatch(meth_name, true, &block)
end
end
# Allows types local to the function to be defined to ease the use of complex types
# in a 4.x function. Within the given block, calls to `type` can be made with a string
# 'AliasType = ExistingType` can be made to define aliases. The defined aliases are
# available for further aliases, and in all dispatchers.
#
# @since 4.5.0
# @api public
#
def self.local_types(&block)
if loader.nil?
raise ArgumentError, _("No loader present. Call create_loaded_function(:myname, loader,...), instead of 'create_function' if running tests")
end
aliases = LocalTypeAliasesBuilder.new(loader, name)
aliases.instance_eval(&block)
# Add the loaded types to the builder
aliases.local_types.each do |type_alias_expr|
# Bind the type alias to the local_loader using the alias
t = Puppet::Pops::Loader::TypeDefinitionInstantiator.create_from_model(type_alias_expr, aliases.loader)
# Also define a method for convenient access to the defined type alias.
# Since initial capital letter in Ruby means a Constant these names use a prefix of
# `type`. As an example, the type 'MyType' is accessed by calling `type_MyType`.
define_method("type_#{t.name}") { t }
end
# Store the loader in the class
@loader = aliases.loader
end
# Creates a new function instance in the given closure scope (visibility to variables), and a loader
# (visibility to other definitions). The created function will either use the `given_loader` or
# (if it has local type aliases) a loader that was constructed from the loader used when loading
# the function's class.
#
# TODO: It would be of value to get rid of the second parameter here, but that would break API.
#
def self.new(closure_scope, given_loader)
super(closure_scope, @loader || given_loader)
end
end
# Base class for all functions implemented in the puppet language
class PuppetFunction < Function
def self.init_dispatch(a_closure)
# A closure is compatible with a dispatcher - they are both callable signatures
dispatcher.add(a_closure)
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
attr_reader :loader
# @api private
def initialize(dispatcher, all_callables, loader)
@all_callables = all_callables
@dispatcher = dispatcher
@loader = loader
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, 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, 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
type = @all_callables
name = :block
when 1
type = @all_callables
name = type_and_name[0]
when 2
type, name = type_and_name
type = Puppet::Pops::Types::TypeParser.singleton.parse(type, loader) unless type.is_a?(Puppet::Pops::Types::PAnyType)
else
raise ArgumentError, _("block_param accepts max 2 arguments (type, name), got %{size}.") % { size: 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}") % { type_class: type.class }
end
unless name.is_a?(Symbol)
raise ArgumentError, _("Expected block_param name to be a Symbol, got %{name_class}") % { name_class: 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
# Defines the return type. Defaults to 'Any'
# @param [String] type a reference to a Puppet Data Type
#
# @api public
def return_type(type)
unless type.is_a?(String) || type.is_a?(Puppet::Pops::Types::PAnyType)
raise ArgumentError, _("Argument to 'return_type' must be a String reference to a Puppet Data Type. Got %{type_class}") % { type_class: type.class }
end
@return_type = type
end
private
# @api private
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 name.is_a?(String)
raise ArgumentError, _("Parameter name argument must be a Symbol. Got %{name_class}") % { name_class: name.class }
end
if type.is_a?(String) || type.is_a?(Puppet::Pops::Types::PAnyType)
@types << type
@names << name
# mark what should be picked for this position when dispatching
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}") % { type_class: type.class }
end
end
# @api private
def dispatch(meth_name, argument_mismatch_handler, &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
@return_type = nil
@argument_mismatch_hander = argument_mismatch_handler
instance_eval(&block)
callable_t = create_callable(@types, @block_type, @return_type, @min, @max)
@dispatcher.add(Puppet::Pops::Functions::Dispatch.new(callable_t, meth_name, @names, @max == :default, @block_name, @injections, @weaving, @argument_mismatch_hander))
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, return_type, from, to)
mapped_types = types.map do |t|
t.is_a?(Puppet::Pops::Types::PAnyType) ? t : internal_type_parse(t, loader)
end
param_types = Puppet::Pops::Types::PTupleType.new(mapped_types, from > 0 && from == to ? nil : Puppet::Pops::Types::PIntegerType.new(from, to))
return_type = internal_type_parse(return_type, loader) unless return_type.nil? || return_type.is_a?(Puppet::Pops::Types::PAnyType)
Puppet::Pops::Types::PCallableType.new(param_types, block_type, return_type)
end
def internal_type_parse(type_string, loader)
Puppet::Pops::Types::TypeParser.singleton.parse(type_string, loader)
rescue StandardError => e
raise ArgumentError, _("Parsing of type string '\"%{type_string}\"' failed with message: <%{message}>.\n") % {
type_string: type_string,
message: e.message
}
end
private :internal_type_parse
end
# The LocalTypeAliasBuilder is used by the 'local_types' method to collect the individual
# type aliases given by the function's author.
#
class LocalTypeAliasesBuilder
attr_reader :local_types, :parser, :loader
def initialize(loader, name)
@loader = Puppet::Pops::Loader::PredefinedLoader.new(loader, :"local_function_#{name}", loader.environment)
@local_types = []
# get the shared parser used by puppet's compiler
@parser = Puppet::Pops::Parser::EvaluatingParser.singleton()
end
# Defines a local type alias, the given string should be a Puppet Language type alias expression
# in string form without the leading 'type' keyword.
# Calls to local_type must be made before the first parameter definition or an error will
# be raised.
#
# @param assignment_string [String] a string on the form 'AliasType = ExistingType'
# @api public
#
def type(assignment_string)
# Get location to use in case of error - this produces ruby filename and where call to 'type' occurred
# but strips off the rest of the internal "where" as it is not meaningful to user.
#
rb_location = caller(1, 1).first
begin
result = parser.parse_string("type #{assignment_string}", nil)
rescue StandardError => e
rb_location = rb_location.gsub(/:in.*$/, '')
# Create a meaningful location for parse errors - show both what went wrong with the parsing
# and in which ruby file it was found.
raise ArgumentError, _("Parsing of 'type \"%{assignment_string}\"' failed with message: <%{message}>.\n" \
"Called from <%{ruby_file_location}>") % {
assignment_string: assignment_string,
message: e.message,
ruby_file_location: rb_location
}
end
unless result.body.is_a?(Puppet::Pops::Model::TypeAlias)
rb_location = rb_location.gsub(/:in.*$/, '')
raise ArgumentError, _("Expected a type alias assignment on the form 'AliasType = T', got '%{assignment_string}'.\n" \
"Called from <%{ruby_file_location}>") % {
assignment_string: assignment_string,
ruby_file_location: rb_location
}
end
@local_types << result.body
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.
#
# 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 such as injection of well known parameters.
#
# @api private
class InternalFunction < Function
# @api private
def self.builder
InternalDispatchBuilder.new(dispatcher, Puppet::Pops::Types::PCallableType::DEFAULT, loader)
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, &block)
internal_call_function(scope, function_name, args, &block)
end
end
class Function3x < InternalFunction
# Table of optimized parameter names - 0 to 5 parameters
PARAM_NAMES = [
[],
['p0'].freeze,
%w[p0 p1].freeze,
%w[p0 p1 p2].freeze,
%w[p0 p1 p2 p3].freeze,
%w[p0 p1 p2 p3 p4].freeze
]
# Creates an anonymous Function3x class that wraps a 3x function
#
# @api private
def self.create_function(func_name, func_info, loader)
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.
#
# (Do not give the class the block here, as instance variables should be set first)
the_class = Class.new(Function3x)
unless loader.nil?
the_class.instance_variable_set(:'@loader', loader.private_loader)
end
the_class.instance_variable_set(:'@func_name', func_name)
the_class.instance_variable_set(:'@method3x', :"function_#{func_name}")
# 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)
#
the_class.instance_eval do
def name
@func_name
end
def loader
@loader
end
def method3x
@method3x
end
end
# Add the method that is called - it simply delegates to
# the 3.x function by calling it via the calling scope using the @method3x symbol
# :"function_#{name}".
#
# When function is not an rvalue function, make sure it produces nil
#
the_class.class_eval do
# Bypasses making the call via the dispatcher to make sure errors
# are reported exactly the same way as in 3x. The dispatcher is still needed as it is
# used to support other features than calling.
#
def call(scope, *args, &block)
result = catch(:return) do
mapped_args = Puppet::Pops::Evaluator::Runtime3FunctionArgumentConverter.map_args(args, scope, '')
# this is the scope.function_xxx(...) call
return scope.send(self.class.method3x, mapped_args)
end
result.value
rescue Puppet::Pops::Evaluator::Next => jumper
begin
throw :next, jumper.value
rescue Puppet::Parser::Scope::UNCAUGHT_THROW_EXCEPTION
raise Puppet::ParseError.new("next() from context where this is illegal", jumper.file, jumper.line)
end
rescue Puppet::Pops::Evaluator::Return => jumper
begin
throw :return, jumper
rescue Puppet::Parser::Scope::UNCAUGHT_THROW_EXCEPTION
raise Puppet::ParseError.new("return() from context where this is illegal", jumper.file, jumper.line)
end
end
end
# Create a dispatcher based on func_info
type, names = Puppet::Functions.any_signature(*from_to_names(func_info))
last_captures_rest = (type.size_range[1] == Float::INFINITY)
# The method '3x_function' here is a dummy as the dispatcher is not used for calling, only for information.
the_class.dispatcher.add(Puppet::Pops::Functions::Dispatch.new(type, '3x_function', names, last_captures_rest))
# The function class is returned as the result of the create function method
the_class
end
# Compute min and max number of arguments and a list of constructed
# parameter names p0 - pn (since there are no parameter names in 3x functions).
#
# @api private
def self.from_to_names(func_info)
arity = func_info[:arity]
if arity.nil?
arity = -1
end
if arity < 0
from = -arity - 1 # arity -1 is 0 min param, -2 is min 1 param
to = :default # infinite range
count = -arity # the number of named parameters
else
count = from = to = arity
end
# Names of parameters, up to 5 are optimized and use frozen version
# Note that (0..count-1) produces expected empty array for count == 0, 0-n for count >= 1
names = count <= 5 ? PARAM_NAMES[count] : (0..count - 1).map { |n| "p#{n}" }
[from, to, names]
end
end
# Injection and Weaving of parameters
# ---
# It is possible to inject and weave a set of well known 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'
# scope_param
# end
# def test(a, b, scope)
# a > b ? scope['a'] : scope['b']
# end
# end
#
# The function in the example above is called like this:
#
# test(10, 20)
#
# @api private
class InternalDispatchBuilder < DispatcherBuilder
# Inject parameter for `Puppet::Parser::Scope`
def scope_param
inject(:scope)
end
# Inject parameter for `Puppet::Pal::ScriptCompiler`
def script_compiler_param
inject(:pal_script_compiler)
end
# Inject a parameter getting a cached hash for this function
def cache_param
inject(:cache)
end
# Inject parameter for `Puppet::Pal::CatalogCompiler`
def compiler_param
inject(:pal_catalog_compiler)
end
# Inject parameter for either `Puppet::Pal::CatalogCompiler` or `Puppet::Pal::ScriptCompiler`
def pal_compiler_param
inject(:pal_compiler)
end
private
def inject(injection_name)
@injections << injection_name
# mark what should be picked for this position when dispatching
@weaving << [@injections.size() - 1]
end
end
end
|