# 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/` directory, where `` 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] 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] 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 # 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 # 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