diff --git a/lib/puppet/functions.rb b/lib/puppet/functions.rb index 965d7b8f1..bbb7a1ec4 100644 --- a/lib/puppet/functions.rb +++ b/lib/puppet/functions.rb @@ -1,555 +1,569 @@ # @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/` 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 corrosponding method is called. # # 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 # # 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 `*`). # # @example 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 # # 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) 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 # 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 # 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" end any_signature(*min_max_param(the_class.instance_method(func_name))) end # @api private 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 # 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] factory = Puppet::Pops::Types::TypeFactory [factory.callable(factory.any, from, to), 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 @type_parser ||= Puppet::Pops::Types::TypeParser.new @all_callables ||= Puppet::Pops::Types::TypeFactory.all_callables DispatcherBuilder.new(dispatcher, @type_parser, @all_callables) 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, &block) end 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 # @api private def initialize(dispatcher, type_parser, all_callables) @type_parser = type_parser @all_callables = all_callables @dispatcher = dispatcher end # Defines a positional parameter with type and name # # @param type [String] The type specification for the parameter. # @param name [String] The name of the parameter. This is primarily used # for error message output and does not have to match the name of the # parameter on the implementation method. # @return [Void] # # @api public def param(type, name) if type.is_a?(String) @types << type @names << name # mark what should be picked for this position when dispatching @weaving << @names.size()-1 else raise ArgumentError, "Type signature argument must be a String reference to a Puppet Data Type. Got #{type.class}" end 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 required_block_param(*type_and_name) case type_and_name.size when 0 # the type must be an independent instance since it will be contained in another type type = @all_callables.copy name = 'block' when 1 # the type must be an independent instance since it will be contained in another type type = @all_callables.copy name = type_and_name[0] when 2 type_string, name = type_and_name type = @type_parser.parse(type_string) else raise ArgumentError, "block_param accepts max 2 arguments (type, name), got #{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}" end unless name.is_a?(String) || name.is_a?(Symbol) raise ArgumentError, "Expected block_param name to be a String or Symbol, got #{name.class}" end if @block_type.nil? @block_type = type @block_name = name else raise ArgumentError, "Attempt to redefine block" end end # 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 # Specifies the min and max occurance of arguments (of the specified types) # if something other than the exact count from the number of specified # types). The max value may be specified as :default if an infinite number of # arguments are supported. When max is > than the number of specified # types, the last specified type repeats. # # @api public def arg_count(min_occurs, max_occurs) @min = min_occurs @max = max_occurs unless min_occurs.is_a?(Integer) && min_occurs >= 0 raise ArgumentError, "min arg_count of function parameter must be an Integer >=0, got #{min_occurs.class} '#{min_occurs}'" end unless max_occurs == :default || (max_occurs.is_a?(Integer) && max_occurs >= 0) raise ArgumentError, "max arg_count of function parameter must be an Integer >= 0, or :default, got #{max_occurs.class} '#{max_occurs}'" end unless max_occurs == :default || (max_occurs.is_a?(Integer) && max_occurs >= min_occurs) raise ArgumentError, "max arg_count must be :default (infinite) or >= min arg_count, got min: '#{min_occurs}, max: '#{max_occurs}'" end end # Specifies that the last argument captures the rest. # # @api public def last_captures_rest @last_captures = true end private # @api private def dispatch(meth_name, &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 = nil @max = nil @last_captures = false @block_type = nil @block_name = nil self.instance_eval &block callable_t = create_callable(@types, @block_type, @min, @max) @dispatcher.add_dispatch(callable_t, meth_name, @names, @block_name, @injections, @weaving, @last_captures) 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, from, to) mapped_types = types.map do |t| @type_parser.parse(t) end if !(from.nil? && to.nil?) mapped_types << from mapped_types << to end if block_type mapped_types << block_type end Puppet::Pops::Types::TypeFactory.callable(*mapped_types) end end private # @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. # # Injection Support # === # The Function API supports injection of data and services. It is possible to # make injection that takes effect when the function is loaded (for services # and runtime configuration that does not change depending on how/from where # in what context the function is called. It is also possible to inject and # weave argument values into a call. # # Injection of attributes # --- # Injection of attributes is performed by one of the methods `attr_injected`, # and `attr_injected_producer`. The injected attributes are available via # accessor method calls. # # @example using injected attributes # Puppet::Functions.create_function('test') do # attr_injected String, :larger, 'message_larger' # attr_injected String, :smaller, 'message_smaller' # def test(a, b) # a > b ? larger() : smaller() # end # end # # @api private class InternalFunction < Function # @api private def self.builder @type_parser ||= Puppet::Pops::Types::TypeParser.new @all_callables ||= Puppet::Pops::Types::TypeFactory.all_callables InternalDispatchBuilder.new(dispatcher, @type_parser, @all_callables) end # Defines class level injected attribute with reader method # # @api private def self.attr_injected(type, attribute_name, injection_name = nil) define_method(attribute_name) do ivar = :"@#{attribute_name.to_s}" unless instance_variable_defined?(ivar) injector = Puppet.lookup(:injector) instance_variable_set(ivar, injector.lookup(closure_scope, type, injection_name)) end instance_variable_get(ivar) end end # Defines class level injected producer attribute with reader method # # @api private def self.attr_injected_producer(type, attribute_name, injection_name = nil) define_method(attribute_name) do ivar = :"@#{attribute_name.to_s}" unless instance_variable_defined?(ivar) injector = Puppet.lookup(:injector) instance_variable_set(ivar, injector.lookup_producer(closure_scope, type, injection_name)) end instance_variable_get(ivar) end 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) + internal_call_function(scope, function_name, args) + 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. # # Injection and Weaving of parameters # --- # It is possible to inject and weave 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' # injected_param 'String', 'larger', 'message_larger' # injected_param 'String', 'smaller', 'message_smaller' # end # def test(a, b, larger, smaller) # a > b ? larger : smaller # end # end # # The function in the example above is called like this: # # test(10, 20) # # Using injected value as default # --- # Default value assignment is handled by using the regular Ruby mechanism (a # value is assigned to the variable). The dispatch simply indicates that the # value is optional. If the default value should be injected, it can be # handled different ways depending on what is desired: # # * by calling the accessor method for an injected Function class attribute. # This is suitable if the value is constant across all instantiations of the # function, and across all calls. # * by injecting a parameter into the call # to the left of the parameter, and then assigning that as the default value. # * One of the above forms, but using an injected producer instead of a # directly injected value. # # @example method with injected default values # Puppet::Functions.create_function('test') do # dispatch :test do # injected_param String, 'b_default', 'b_default_value_key' # param 'Scalar', 'a' # param 'Scalar', 'b' # end # def test(b_default, a, b = b_default) # # ... # end # end # # @api private class InternalDispatchBuilder < DispatcherBuilder def scope_param() @injections << [:scope, 'scope', '', :dispatcher_internal] # mark what should be picked for this position when dispatching @weaving << [@injections.size()-1] end # TODO: is param name really needed? Perhaps for error messages? (it is unused now) # # @api private def injected_param(type, name, injection_name = '') @injections << [type, name, injection_name] # mark what should be picked for this position when dispatching @weaving << [@injections.size() -1] end # TODO: is param name really needed? Perhaps for error messages? (it is unused now) # # @api private def injected_producer_param(type, name, injection_name = '') @injections << [type, name, injection_name, :producer] # mark what should be picked for this position when dispatching @weaving << [@injections.size()-1] end end end diff --git a/lib/puppet/pops/functions/function.rb b/lib/puppet/pops/functions/function.rb index 6211bb801..c9ca5fb87 100644 --- a/lib/puppet/pops/functions/function.rb +++ b/lib/puppet/pops/functions/function.rb @@ -1,77 +1,108 @@ # @note WARNING: This new function API is still under development and may change at # any time # # A function in the puppet evaluator. # # Functions are normally defined by another system, which produces subclasses # of this class as well as constructing delegations to call the appropriate methods. # # This class should rarely be used directly. Instead functions should be # constructed using {Puppet::Functions.create_function}. # # @api public class Puppet::Pops::Functions::Function # The scope where the function was defined attr_reader :closure_scope # The loader that loaded this function. # Should be used if function wants to load other things. # attr_reader :loader def initialize(closure_scope, loader) @closure_scope = closure_scope @loader = loader end # Invokes the function via the dispatching logic that performs type check and weaving. # A specialized function may override this method to do its own dispatching and checking of # the raw arguments. A specialized implementation can rearrange arguments, add or remove # arguments and then delegate to the dispatching logic by calling: # # @example Delegating to the dispatcher # def call(scope, *args) # manipulated_args = args + ['easter_egg'] # self.class.dispatcher.dispatch(self, scope, manipulated_args) # end # # System functions that must have access to the calling scope can use this technique. Functions # in general should not need the calling scope. (The closure scope; what is visible where the function # is defined) is available via the method `closure_scope`). # # @api public def call(scope, *args) self.class.dispatcher.dispatch(self, scope, args) end # Allows the implementation of a function to call other functions by name. The callable functions # are those visible to the same loader that loaded this function (the calling function). # + # @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(function_name, *args) - if the_loader = loader - func = the_loader.load(:function, function_name) - if func - return func.call(closure_scope, *args) - end - end - # Raise a generic error to allow upper layers to fill in the details about where in a puppet manifest this - # error originates. (Such information is not available here). - # - raise ArgumentError, "Function #{self.class.name}(): cannot call function '#{function_name}' - not found" + internal_call_function(closure_scope, function_name, args) end # The dispatcher for the function # # @api private def self.dispatcher @dispatcher ||= Puppet::Pops::Functions::Dispatcher.new end # Produces information about parameters in a way that is compatible with Closure # # @api private def self.signatures @dispatcher.signatures end + + private + + # 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 [Array] array of arguments + # @return [Object] The result returned by the called function + # + # @api public + def internal_call_function(scope, function_name, args) + + the_loader = loader + raise ArgumentError, "Function #{self.class.name}(): cannot call function '#{function_name}' - no loader specified" unless the_loader + + func = the_loader.load(:function, function_name) + return func.call(scope, *args) if func + + # Check if a 3x function is present. Raise a generic error if it's not to allow upper layers to fill in the details + # about where in a puppet manifest this error originates. (Such information is not available here). + loader_scope = closure_scope + func_3x = Puppet::Parser::Functions.function(function_name, loader_scope.environment) if loader_scope.is_a?(Puppet::Parser::Scope) + raise ArgumentError, "Function #{self.class.name}(): cannot call function '#{function_name}' - not found" unless func_3x + + # Call via 3x API + # Arguments must be mapped since functions are unaware of the new and magical creatures in 4x. + # NOTE: Passing an empty string last converts nil/:undef to empty string + result = scope.send(func_3x, Puppet::Pops::Evaluator::Runtime3Converter.convert_args(args, loader_scope, '')) + + # Prevent non r-value functions from leaking their result (they are not written to care about this) + Puppet::Parser::Functions.rvalue?(function_name) ? result : nil + end + end diff --git a/spec/fixtures/unit/pops/loaders/loaders/mix_4x_and_3x_functions/usee/lib/puppet/parser/functions/callee.rb b/spec/fixtures/unit/pops/loaders/loaders/mix_4x_and_3x_functions/usee/lib/puppet/parser/functions/callee.rb new file mode 100644 index 000000000..a95604576 --- /dev/null +++ b/spec/fixtures/unit/pops/loaders/loaders/mix_4x_and_3x_functions/usee/lib/puppet/parser/functions/callee.rb @@ -0,0 +1,8 @@ +module Puppet::Parser::Functions + newfunction(:callee, :type => :rvalue, :doc => <<-EOS + A function using the 3x API + EOS + ) do |arguments| + "usee::callee() got '#{arguments[0]}'" + end +end diff --git a/spec/fixtures/unit/pops/loaders/loaders/mix_4x_and_3x_functions/usee/lib/puppet/parser/functions/callee_ws.rb b/spec/fixtures/unit/pops/loaders/loaders/mix_4x_and_3x_functions/usee/lib/puppet/parser/functions/callee_ws.rb new file mode 100644 index 000000000..c7f087f4b --- /dev/null +++ b/spec/fixtures/unit/pops/loaders/loaders/mix_4x_and_3x_functions/usee/lib/puppet/parser/functions/callee_ws.rb @@ -0,0 +1,8 @@ +module Puppet::Parser::Functions + newfunction(:callee_ws, :type => :rvalue, :doc => <<-EOS + A function using the 3x API + EOS + ) do |arguments| + "usee::callee_ws() got '#{self['passed_in_scope']}'" + end +end \ No newline at end of file diff --git a/spec/fixtures/unit/pops/loaders/loaders/mix_4x_and_3x_functions/usee/metadata.json b/spec/fixtures/unit/pops/loaders/loaders/mix_4x_and_3x_functions/usee/metadata.json new file mode 100644 index 000000000..35daeaf58 --- /dev/null +++ b/spec/fixtures/unit/pops/loaders/loaders/mix_4x_and_3x_functions/usee/metadata.json @@ -0,0 +1,9 @@ +{ + "name": "test-usee", + "author": "test", + "description": "", + "license": "", + "source": "", + "version": "1.0.0", + "dependencies": [] +} diff --git a/spec/fixtures/unit/pops/loaders/loaders/mix_4x_and_3x_functions/user/lib/puppet/functions/user/caller.rb b/spec/fixtures/unit/pops/loaders/loaders/mix_4x_and_3x_functions/user/lib/puppet/functions/user/caller.rb new file mode 100644 index 000000000..d0bdc1692 --- /dev/null +++ b/spec/fixtures/unit/pops/loaders/loaders/mix_4x_and_3x_functions/user/lib/puppet/functions/user/caller.rb @@ -0,0 +1,5 @@ +Puppet::Functions.create_function(:'user::caller') do + def caller() + call_function('callee', 'first') + ' - ' + call_function('callee', 'second') + end +end diff --git a/spec/fixtures/unit/pops/loaders/loaders/mix_4x_and_3x_functions/user/lib/puppet/functions/user/caller_ws.rb b/spec/fixtures/unit/pops/loaders/loaders/mix_4x_and_3x_functions/user/lib/puppet/functions/user/caller_ws.rb new file mode 100644 index 000000000..999a84cbd --- /dev/null +++ b/spec/fixtures/unit/pops/loaders/loaders/mix_4x_and_3x_functions/user/lib/puppet/functions/user/caller_ws.rb @@ -0,0 +1,12 @@ +Puppet::Functions.create_function(:'user::caller_ws', Puppet::Functions::InternalFunction) do + dispatch :caller_ws do + scope_param + param 'String', 'value' + end + + def caller_ws(scope, value) + scope = scope.compiler.newscope(scope) + scope['passed_in_scope'] = value + call_function_with_scope(scope, 'callee_ws') + end +end diff --git a/spec/fixtures/unit/pops/loaders/loaders/mix_4x_and_3x_functions/user/metadata.json b/spec/fixtures/unit/pops/loaders/loaders/mix_4x_and_3x_functions/user/metadata.json new file mode 100644 index 000000000..833e45932 --- /dev/null +++ b/spec/fixtures/unit/pops/loaders/loaders/mix_4x_and_3x_functions/user/metadata.json @@ -0,0 +1,9 @@ +{ + "name": "test-user", + "author": "test", + "description": "", + "license": "", + "source": "", + "version": "1.0.0", + "dependencies": [{ "name": "test/usee" }] +} diff --git a/spec/unit/pops/loaders/loaders_spec.rb b/spec/unit/pops/loaders/loaders_spec.rb index 831236698..50a88ca4f 100644 --- a/spec/unit/pops/loaders/loaders_spec.rb +++ b/spec/unit/pops/loaders/loaders_spec.rb @@ -1,125 +1,146 @@ require 'spec_helper' require 'puppet_spec/files' require 'puppet/pops' require 'puppet/loaders' describe 'loader helper classes' do it 'NamedEntry holds values and is frozen' do ne = Puppet::Pops::Loader::Loader::NamedEntry.new('name', 'value', 'origin') expect(ne.frozen?).to be_true expect(ne.typed_name).to eql('name') expect(ne.origin).to eq('origin') expect(ne.value).to eq('value') end it 'TypedName holds values and is frozen' do tn = Puppet::Pops::Loader::Loader::TypedName.new(:function, '::foo::bar') expect(tn.frozen?).to be_true expect(tn.type).to eq(:function) expect(tn.name_parts).to eq(['foo', 'bar']) expect(tn.name).to eq('foo::bar') expect(tn.qualified).to be_true end end describe 'loaders' do include PuppetSpec::Files let(:module_without_metadata) { File.join(config_dir('wo_metadata_module'), 'modules') } + let(:mix_4x_and_3x_functions) { config_dir('mix_4x_and_3x_functions') } let(:module_with_metadata) { File.join(config_dir('single_module'), 'modules') } let(:dependent_modules_with_metadata) { config_dir('dependent_modules_with_metadata') } let(:empty_test_env) { environment_for() } # Loaders caches the puppet_system_loader, must reset between tests before(:each) { Puppet::Pops::Loaders.clear() } it 'creates a puppet_system loader' do loaders = Puppet::Pops::Loaders.new(empty_test_env) expect(loaders.puppet_system_loader()).to be_a(Puppet::Pops::Loader::ModuleLoaders::FileBased) end it 'creates an environment loader' do loaders = Puppet::Pops::Loaders.new(empty_test_env) expect(loaders.public_environment_loader()).to be_a(Puppet::Pops::Loader::SimpleEnvironmentLoader) expect(loaders.public_environment_loader().to_s).to eql("(SimpleEnvironmentLoader 'environment:*test*')") expect(loaders.private_environment_loader()).to be_a(Puppet::Pops::Loader::DependencyLoader) expect(loaders.private_environment_loader().to_s).to eql("(DependencyLoader 'environment' [])") end it 'can load a function using a qualified or unqualified name from a module with metadata' do loaders = Puppet::Pops::Loaders.new(environment_for(module_with_metadata)) modulea_loader = loaders.public_loader_for_module('modulea') unqualified_function = modulea_loader.load_typed(typed_name(:function, 'rb_func_a')).value qualified_function = modulea_loader.load_typed(typed_name(:function, 'modulea::rb_func_a')).value expect(unqualified_function).to be_a(Puppet::Functions::Function) expect(qualified_function).to be_a(Puppet::Functions::Function) expect(unqualified_function.class.name).to eq('rb_func_a') expect(qualified_function.class.name).to eq('modulea::rb_func_a') end it 'can load a function with a qualified name from module without metadata' do loaders = Puppet::Pops::Loaders.new(environment_for(module_without_metadata)) moduleb_loader = loaders.public_loader_for_module('moduleb') function = moduleb_loader.load_typed(typed_name(:function, 'moduleb::rb_func_b')).value expect(function).to be_a(Puppet::Functions::Function) expect(function.class.name).to eq('moduleb::rb_func_b') end it 'cannot load an unqualified function from a module without metadata' do loaders = Puppet::Pops::Loaders.new(environment_for(module_without_metadata)) moduleb_loader = loaders.public_loader_for_module('moduleb') expect(moduleb_loader.load_typed(typed_name(:function, 'rb_func_b'))).to be_nil end it 'makes all other modules visible to a module without metadata' do env = environment_for(module_with_metadata, module_without_metadata) loaders = Puppet::Pops::Loaders.new(env) moduleb_loader = loaders.private_loader_for_module('moduleb') function = moduleb_loader.load_typed(typed_name(:function, 'moduleb::rb_func_b')).value expect(function.call({})).to eql("I am modulea::rb_func_a() + I am moduleb::rb_func_b()") end it 'makes dependent modules visible to a module with metadata' do env = environment_for(dependent_modules_with_metadata) loaders = Puppet::Pops::Loaders.new(env) moduleb_loader = loaders.private_loader_for_module('user') function = moduleb_loader.load_typed(typed_name(:function, 'user::caller')).value expect(function.call({})).to eql("usee::callee() was told 'passed value' + I am user::caller()") end + context 'with scope' do + let(:env) { environment_for(mix_4x_and_3x_functions) } + let(:scope) { Puppet::Parser::Compiler.new(Puppet::Node.new("test", :environment => env)).newscope(nil) } + let(:loader) { Puppet::Pops::Loaders.new(env).private_loader_for_module('user') } + + it 'can call 3x function in dependent module from a 4x function' do + Puppet.override({ :current_environment => scope.environment, :global_scope => scope }) do + function = loader.load_typed(typed_name(:function, 'user::caller')).value + expect(function.call(scope)).to eql("usee::callee() got 'first' - usee::callee() got 'second'") + end + end + + it 'can call 3x function and propagate caller scope from a 4x function' do + Puppet.override({ :current_environment => scope.environment, :global_scope => scope }) do + function = loader.load_typed(typed_name(:function, 'user::caller_ws')).value + expect(function.call(scope, 'passed in scope')).to eql("usee::callee_ws() got 'passed in scope'") + end + end + end + it 'can load a function more than once from modules' do env = environment_for(dependent_modules_with_metadata) loaders = Puppet::Pops::Loaders.new(env) moduleb_loader = loaders.private_loader_for_module('user') function = moduleb_loader.load_typed(typed_name(:function, 'user::caller')).value expect(function.call({})).to eql("usee::callee() was told 'passed value' + I am user::caller()") function = moduleb_loader.load_typed(typed_name(:function, 'user::caller')).value expect(function.call({})).to eql("usee::callee() was told 'passed value' + I am user::caller()") end def environment_for(*module_paths) - Puppet::Node::Environment.create(:'*test*', module_paths, '') + Puppet::Node::Environment.create(:'*test*', module_paths) end def typed_name(type, name) Puppet::Pops::Loader::Loader::TypedName.new(type, name) end def config_dir(config_name) my_fixture(config_name) end end