diff --git a/lib/puppet/functions.rb b/lib/puppet/functions.rb index fbab0e787..c0ef0bc26 100644 --- a/lib/puppet/functions.rb +++ b/lib/puppet/functions.rb @@ -1,556 +1,561 @@ # 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] == Float::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) 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] 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 [Symbol] 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) + raise ArgumentError, 'Parameters cannot be added after a block_param' unless @block_type.nil? unless type.is_a?(String) raise ArgumentError, "Type signature argument must be a String reference to a Puppet Data Type. Got #{type.class}" end unless name.is_a?(Symbol) raise ArgumentError, "Parameter name argument must be a Symbol. Got #{type.class}" end @types << type @names << name # mark what should be picked for this position when dispatching @weaving << @names.size()-1 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?(Symbol) raise ArgumentError, "Expected block_param name to be a Symbol, got #{name.class}" end if @block_type.nil? @block_type = type @block_name = name + + # mark what should be picked for this position when dispatching. This is the size of + # the @names array since the block is required to appear last + @weaving << @names.size() 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 occurrence 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/types/type_calculator.rb b/lib/puppet/pops/types/type_calculator.rb index aa6cbd2ef..33830bfbf 100644 --- a/lib/puppet/pops/types/type_calculator.rb +++ b/lib/puppet/pops/types/type_calculator.rb @@ -1,1717 +1,1739 @@ # The TypeCalculator can answer questions about puppet types. # # The Puppet type system is primarily based on sub-classing. When asking the type calculator to infer types from Ruby in general, it # may not provide the wanted answer; it does not for instance take module inclusions and extensions into account. In general the type # system should be unsurprising for anyone being exposed to the notion of type. The type `Data` may require a bit more explanation; this # is an abstract type that includes all scalar types, as well as Array with an element type compatible with Data, and Hash with key # compatible with scalar and elements compatible with Data. Expressed differently; Data is what you typically express using JSON (with # the exception that the Puppet type system also includes Pattern (regular expression) as a scalar. # # Inference # --------- # The `infer(o)` method infers a Puppet type for scalar Ruby objects, and for Arrays and Hashes. # The inference result is instance specific for single typed collections # and allows answering questions about its embedded type. It does not however preserve multiple types in # a collection, and can thus not answer questions like `[1,a].infer() =~ Array[Integer, String]` since the inference # computes the common type Scalar when combining Integer and String. # # The `infer_generic(o)` method infers a generic Puppet type for scalar Ruby object, Arrays and Hashes. # This inference result does not contain instance specific information; e.g. Array[Integer] where the integer # range is the generic default. Just `infer` it also combines types into a common type. # # The `infer_set(o)` method works like `infer` but preserves all type information. It does not do any # reduction into common types or ranges. This method of inference is best suited for answering questions # about an object being an instance of a type. It correctly answers: `[1,a].infer_set() =~ Array[Integer, String]` # # The `generalize!(t)` method modifies an instance specific inference result to a generic. The method mutates # the given argument. Basically, this removes string instances from String, and range from Integer and Float. # # Assignability # ------------- # The `assignable?(t1, t2)` method answers if t2 conforms to t1. The type t2 may be an instance, in which case # its type is inferred, or a type. # # Instance? # --------- # The `instance?(t, o)` method answers if the given object (instance) is an instance that is assignable to the given type. # # String # ------ # Creates a string representation of a type. # # Creation of Type instances # -------------------------- # Instance of the classes in the {Puppet::Pops::Types type model} are used to denote a specific type. It is most convenient # to use the {Puppet::Pops::Types::TypeFactory TypeFactory} when creating instances. # # @note # In general, new instances of the wanted type should be created as they are assigned to models using containment, and a # contained object can only be in one container at a time. Also, the type system may include more details in each type # instance, such as if it may be nil, be empty, contain a certain count etc. Or put differently, the puppet types are not # singletons. # # All types support `copy` which should be used when assigning a type where it is unknown if it is bound or not # to a parent type. A check can be made with `t.eContainer().nil?` # # Equality and Hash # ----------------- # Type instances are equal in terms of Ruby eql? and `==` if they describe the same type, but they are not `equal?` if they are not # the same type instance. Two types that describe the same type have identical hash - this makes them usable as hash keys. # # Types and Subclasses # -------------------- # In general, the type calculator should be used to answer questions if a type is a subtype of another (using {#assignable?}, or # {#instance?} if the question is if a given object is an instance of a given type (or is a subtype thereof). # Many of the types also have a Ruby subtype relationship; e.g. PHashType and PArrayType are both subtypes of PCollectionType, and # PIntegerType, PFloatType, PStringType,... are subtypes of PScalarType. Even if it is possible to answer certain questions about # type by looking at the Ruby class of the types this is considered an implementation detail, and such checks should in general # be performed by the type_calculator which implements the type system semantics. # # The PRuntimeType # ------------- # The PRuntimeType corresponds to a type in the runtime system (currently only supported runtime is 'ruby'). The # type has a runtime_type_name that corresponds to a Ruby Class name. # A Runtime[ruby] type can be used to describe any ruby class except for the puppet types that are specialized # (i.e. PRuntimeType should not be used for Integer, String, etc. since there are specialized types for those). # When the type calculator deals with PRuntimeTypes and checks for assignability, it determines the # "common ancestor class" of two classes. # This check is made based on the superclasses of the two classes being compared. In order to perform this, the # classes must be present (i.e. they are resolved from the string form in the PRuntimeType to a # loaded, instantiated Ruby Class). In general this is not a problem, since the question to produce the common # super type for two objects means that the classes must be present or there would have been # no instances present in the first place. If however the classes are not present, the type # calculator will fall back and state that the two types at least have Any in common. # # @see Puppet::Pops::Types::TypeFactory TypeFactory for how to create instances of types # @see Puppet::Pops::Types::TypeParser TypeParser how to construct a type instance from a String # @see Puppet::Pops::Types Types for details about the type model # # Using the Type Calculator # ----- # The type calculator can be directly used via its class methods. If doing time critical work and doing many # calls to the type calculator, it is more performant to create an instance and invoke the corresponding # instance methods. Note that inference is an expensive operation, rather than inferring the same thing # several times, it is in general better to infer once and then copy the result if mutation to a more generic form is # required. # # @api public # class Puppet::Pops::Types::TypeCalculator Types = Puppet::Pops::Types # @api public def self.assignable?(t1, t2) singleton.assignable?(t1,t2) end # Answers, does the given callable accept the arguments given in args (an array or a tuple) # @param callable [Puppet::Pops::Types::PCallableType] - the callable # @param args [Puppet::Pops::Types::PArrayType, Puppet::Pops::Types::PTupleType] args optionally including a lambda callable at the end # @return [Boolan] true if the callable accepts the arguments # # @api public def self.callable?(callable, args) singleton.callable?(callable, args) end # Produces a String representation of the given type. # @param t [Puppet::Pops::Types::PAnyType] the type to produce a string form # @return [String] the type in string form # # @api public # def self.string(t) singleton.string(t) end # @api public def self.infer(o) singleton.infer(o) end # @api public def self.generalize!(o) singleton.generalize!(o) end # @api public def self.infer_set(o) singleton.infer_set(o) end # @api public def self.debug_string(t) singleton.debug_string(t) end # @api public def self.enumerable(t) singleton.enumerable(t) end # @api private def self.singleton() @tc_instance ||= new end # @api public # def initialize @@assignable_visitor ||= Puppet::Pops::Visitor.new(nil,"assignable",1,1) @@infer_visitor ||= Puppet::Pops::Visitor.new(nil,"infer",0,0) @@infer_set_visitor ||= Puppet::Pops::Visitor.new(nil,"infer_set",0,0) @@instance_of_visitor ||= Puppet::Pops::Visitor.new(nil,"instance_of",1,1) @@string_visitor ||= Puppet::Pops::Visitor.new(nil,"string",0,0) @@inspect_visitor ||= Puppet::Pops::Visitor.new(nil,"debug_string",0,0) @@enumerable_visitor ||= Puppet::Pops::Visitor.new(nil,"enumerable",0,0) @@extract_visitor ||= Puppet::Pops::Visitor.new(nil,"extract",0,0) @@generalize_visitor ||= Puppet::Pops::Visitor.new(nil,"generalize",0,0) @@callable_visitor ||= Puppet::Pops::Visitor.new(nil,"callable",1,1) da = Types::PArrayType.new() da.element_type = Types::PDataType.new() @data_array = da h = Types::PHashType.new() h.element_type = Types::PDataType.new() h.key_type = Types::PScalarType.new() @data_hash = h @data_t = Types::PDataType.new() @scalar_t = Types::PScalarType.new() @numeric_t = Types::PNumericType.new() @t = Types::PAnyType.new() # Data accepts a Tuple that has 0-infinity Data compatible entries (e.g. a Tuple equivalent to Array). data_tuple = Types::PTupleType.new() data_tuple.addTypes(Types::PDataType.new()) data_tuple.size_type = Types::PIntegerType.new() data_tuple.size_type.from = 0 data_tuple.size_type.to = nil # infinity @data_tuple_t = data_tuple # Variant type compatible with Data data_variant = Types::PVariantType.new() data_variant.addTypes(@data_hash.copy) data_variant.addTypes(@data_array.copy) data_variant.addTypes(Types::PScalarType.new) data_variant.addTypes(Types::PNilType.new) data_variant.addTypes(@data_tuple_t.copy) @data_variant_t = data_variant collection_default_size = Types::PIntegerType.new() collection_default_size.from = 0 collection_default_size.to = nil # infinity @collection_default_size_t = collection_default_size non_empty_string = Types::PStringType.new non_empty_string.size_type = Types::PIntegerType.new() non_empty_string.size_type.from = 1 non_empty_string.size_type.to = nil # infinity @non_empty_string_t = non_empty_string @nil_t = Types::PNilType.new end # Convenience method to get a data type for comparisons # @api private the returned value may not be contained in another element # def data @data_t end # Convenience method to get a variant compatible with the Data type. # @api private the returned value may not be contained in another element # def data_variant @data_variant_t end def self.data_variant singleton.data_variant end # Answers the question 'is it possible to inject an instance of the given class' # A class is injectable if it has a special *assisted inject* class method called `inject` taking # an injector and a scope as argument, or if it has a zero args `initialize` method. # # @param klazz [Class, PRuntimeType] the class/type to check if it is injectable # @return [Class, nil] the injectable Class, or nil if not injectable # @api public # def injectable_class(klazz) # Handle case when we get a PType instead of a class if klazz.is_a?(Types::PRuntimeType) klazz = Puppet::Pops::Types::ClassLoader.provide(klazz) end # data types can not be injected (check again, it is not safe to assume that given RubyRuntime klazz arg was ok) return false unless type(klazz).is_a?(Types::PRuntimeType) if (klazz.respond_to?(:inject) && klazz.method(:inject).arity() == -4) || klazz.instance_method(:initialize).arity() == 0 klazz else nil end end # Answers 'can an instance of type t2 be assigned to a variable of type t'. # Does not accept nil/undef unless the type accepts it. # # @api public # def assignable?(t, t2) if t.is_a?(Class) t = type(t) end if t2.is_a?(Class) t2 = type(t2) end # Unit can be assigned to anything return true if t2.class == Types::PUnitType if t2.class == Types::PVariantType # Assignable if all contained types are assignable t2.types.all? { |vt| @@assignable_visitor.visit_this_1(self, t, vt) } else @@assignable_visitor.visit_this_1(self, t, t2) end end # Returns an enumerable if the t represents something that can be iterated def enumerable(t) @@enumerable_visitor.visit_this_0(self, t) end # Answers, does the given callable accept the arguments given in args (an array or a tuple) # def callable?(callable, args) return false if !self.class.is_kind_of_callable?(callable) # Note that polymorphism is for the args type, the callable is always a callable @@callable_visitor.visit_this_1(self, args, callable) end # Answers if the two given types describe the same type def equals(left, right) return false unless left.is_a?(Types::PAnyType) && right.is_a?(Types::PAnyType) # Types compare per class only - an extra test must be made if the are mutually assignable # to find all types that represent the same type of instance # left == right || (assignable?(right, left) && assignable?(left, right)) end # Answers 'what is the Puppet Type corresponding to the given Ruby class' # @param c [Class] the class for which a puppet type is wanted # @api public # def type(c) raise ArgumentError, "Argument must be a Class" unless c.is_a? Class # Can't use a visitor here since we don't have an instance of the class case when c <= Integer type = Types::PIntegerType.new() when c == Float type = Types::PFloatType.new() when c == Numeric type = Types::PNumericType.new() when c == String type = Types::PStringType.new() when c == Regexp type = Types::PRegexpType.new() when c == NilClass type = Types::PNilType.new() when c == FalseClass, c == TrueClass type = Types::PBooleanType.new() when c == Class type = Types::PType.new() when c == Array # Assume array of data values type = Types::PArrayType.new() type.element_type = Types::PDataType.new() when c == Hash # Assume hash with scalar keys and data values type = Types::PHashType.new() type.key_type = Types::PScalarType.new() type.element_type = Types::PDataType.new() else type = Types::PRuntimeType.new(:runtime => :ruby, :runtime_type_name => c.name) end type end # Generalizes value specific types. The given type is mutated and returned. # @api public def generalize!(o) @@generalize_visitor.visit_this_0(self, o) o.eAllContents.each { |x| @@generalize_visitor.visit_this_0(self, x) } o end def generalize_Object(o) # do nothing, there is nothing to change for most types end def generalize_PStringType(o) o.values = [] o.size_type = nil [] end def generalize_PCollectionType(o) # erase the size constraint from Array and Hash (if one exists, it is transformed to -Infinity - + Infinity, which is # not desirable. o.size_type = nil end def generalize_PFloatType(o) o.to = nil o.from = nil end def generalize_PIntegerType(o) o.to = nil o.from = nil end # Answers 'what is the single common Puppet Type describing o', or if o is an Array or Hash, what is the # single common type of the elements (or keys and elements for a Hash). # @api public # def infer(o) @@infer_visitor.visit_this_0(self, o) end def infer_generic(o) result = generalize!(infer(o)) result end # Answers 'what is the set of Puppet Types of o' # @api public # def infer_set(o) @@infer_set_visitor.visit_this_0(self, o) end def instance_of(t, o) @@instance_of_visitor.visit_this_1(self, t, o) end def instance_of_Object(t, o) # Undef is Undef and Any, but nothing else when checking instance? return false if (o.nil?) && t.class != Types::PAnyType assignable?(t, infer(o)) end # Anything is an instance of Unit # @api private def instance_of_PUnitType(t, o) true end def instance_of_PArrayType(t, o) return false unless o.is_a?(Array) return false unless o.all? {|element| instance_of(t.element_type, element) } size_t = t.size_type || @collection_default_size_t # optimize by calling directly return instance_of_PIntegerType(size_t, o.size) end # @api private def instance_of_PIntegerType(t, o) return false unless o.is_a?(Integer) x = t.from x = -Float::INFINITY if x.nil? || x == :default y = t.to y = Float::INFINITY if y.nil? || y == :default return x < y ? x <= o && y >= o : y <= o && x >= o end + # @api private + def instance_of_PStringType(t, o) + return false unless o.is_a?(String) + # true if size compliant + size_t = t.size_type || @collection_default_size_t + instance_of_PIntegerType(size_t, o.size) + end + def instance_of_PTupleType(t, o) return false unless o.is_a?(Array) # compute the tuple's min/max size, and check if that size matches size_t = t.size_type || Puppet::Pops::Types::TypeFactory.range(*t.size_range) return false unless instance_of_PIntegerType(size_t, o.size) o.each_with_index do |element, index| return false unless instance_of(t.types[index] || t.types[-1], element) end true end def instance_of_PStructType(t, o) return false unless o.is_a?(Hash) h = t.hashed_elements # all keys must be present and have a value (even if nil/undef) (o.keys - h.keys).empty? && h.all? { |k,v| instance_of(v, o[k]) } end def instance_of_PHashType(t, o) return false unless o.is_a?(Hash) key_t = t.key_type element_t = t.element_type return false unless o.keys.all? {|key| instance_of(key_t, key) } && o.values.all? {|value| instance_of(element_t, value) } size_t = t.size_type || @collection_default_size_t # optimize by calling directly return instance_of_PIntegerType(size_t, o.size) end def instance_of_PDataType(t, o) instance_of(@data_variant_t, o) end def instance_of_PNilType(t, o) o.nil? || o == :undef end def instance_of_POptionalType(t, o) instance_of_PNilType(t, o) || instance_of(t.optional_type, o) end def instance_of_PVariantType(t, o) # instance of variant if o is instance? of any of variant's types t.types.any? { |option_t| instance_of(option_t, o) } end # Answers 'is o an instance of type t' # @api public # def self.instance?(t, o) singleton.instance_of(t,o) end # Answers 'is o an instance of type t' # @api public # def instance?(t, o) instance_of(t,o) end # Answers if t is a puppet type # @api public # def is_ptype?(t) return t.is_a?(Types::PAnyType) end # Answers if t represents the puppet type PNilType # @api public # def is_pnil?(t) return t.nil? || t.is_a?(Types::PNilType) end # Answers, 'What is the common type of t1 and t2?' # # TODO: The current implementation should be optimized for performance # # @api public # def common_type(t1, t2) raise ArgumentError, 'two types expected' unless (is_ptype?(t1) || is_pnil?(t1)) && (is_ptype?(t2) || is_pnil?(t2)) # TODO: This is not right since Scalar U Undef is Any # if either is nil, the common type is the other if is_pnil?(t1) return t2 elsif is_pnil?(t2) return t1 end # If either side is Unit, it is the other type if t1.is_a?(Types::PUnitType) return t2 elsif t2.is_a?(Types::PUnitType) return t1 end # Simple case, one is assignable to the other if assignable?(t1, t2) return t1 elsif assignable?(t2, t1) return t2 end # when both are arrays, return an array with common element type if t1.is_a?(Types::PArrayType) && t2.is_a?(Types::PArrayType) type = Types::PArrayType.new() type.element_type = common_type(t1.element_type, t2.element_type) return type end # when both are hashes, return a hash with common key- and element type if t1.is_a?(Types::PHashType) && t2.is_a?(Types::PHashType) type = Types::PHashType.new() type.key_type = common_type(t1.key_type, t2.key_type) type.element_type = common_type(t1.element_type, t2.element_type) return type end # when both are host-classes, reduce to PHostClass[] (since one was not assignable to the other) if t1.is_a?(Types::PHostClassType) && t2.is_a?(Types::PHostClassType) return Types::PHostClassType.new() end # when both are resources, reduce to Resource[T] or Resource[] (since one was not assignable to the other) if t1.is_a?(Types::PResourceType) && t2.is_a?(Types::PResourceType) result = Types::PResourceType.new() # only Resource[] unless the type name is the same if t1.type_name == t2.type_name then result.type_name = t1.type_name end # the cross assignability test above has already determined that they do not have the same type and title return result end # Integers have range, expand the range to the common range if t1.is_a?(Types::PIntegerType) && t2.is_a?(Types::PIntegerType) t1range = from_to_ordered(t1.from, t1.to) t2range = from_to_ordered(t2.from, t2.to) t = Types::PIntegerType.new() from = [t1range[0], t2range[0]].min to = [t1range[1], t2range[1]].max t.from = from unless from == Float::INFINITY t.to = to unless to == Float::INFINITY return t end # Floats have range, expand the range to the common range if t1.is_a?(Types::PFloatType) && t2.is_a?(Types::PFloatType) t1range = from_to_ordered(t1.from, t1.to) t2range = from_to_ordered(t2.from, t2.to) t = Types::PFloatType.new() from = [t1range[0], t2range[0]].min to = [t1range[1], t2range[1]].max t.from = from unless from == Float::INFINITY t.to = to unless to == Float::INFINITY return t end if t1.is_a?(Types::PStringType) && t2.is_a?(Types::PStringType) t = Types::PStringType.new() t.values = t1.values | t2.values return t end if t1.is_a?(Types::PPatternType) && t2.is_a?(Types::PPatternType) t = Types::PPatternType.new() # must make copies since patterns are contained types, not data-types t.patterns = (t1.patterns | t2.patterns).map(&:copy) return t end if t1.is_a?(Types::PEnumType) && t2.is_a?(Types::PEnumType) # The common type is one that complies with either set t = Types::PEnumType.new t.values = t1.values | t2.values return t end if t1.is_a?(Types::PVariantType) && t2.is_a?(Types::PVariantType) # The common type is one that complies with either set t = Types::PVariantType.new t.types = (t1.types | t2.types).map(&:copy) return t end if t1.is_a?(Types::PRegexpType) && t2.is_a?(Types::PRegexpType) # if they were identical, the general rule would return a parameterized regexp # since they were not, the result is a generic regexp type return Types::PPatternType.new() end if t1.is_a?(Types::PCallableType) && t2.is_a?(Types::PCallableType) # They do not have the same signature, and one is not assignable to the other, # what remains is the most general form of Callable return Types::PCallableType.new() end # Common abstract types, from most specific to most general if common_numeric?(t1, t2) return Types::PNumericType.new() end if common_scalar?(t1, t2) return Types::PScalarType.new() end if common_data?(t1,t2) return Types::PDataType.new() end # Meta types Type[Integer] + Type[String] => Type[Data] if t1.is_a?(Types::PType) && t2.is_a?(Types::PType) type = Types::PType.new() type.type = common_type(t1.type, t2.type) return type end # If both are Runtime types if t1.is_a?(Types::PRuntimeType) && t2.is_a?(Types::PRuntimeType) if t1.runtime == t2.runtime && t1.runtime_type_name == t2.runtime_type_name return t1 end # finding the common super class requires that names are resolved to class # NOTE: This only supports runtime type of :ruby c1 = Types::ClassLoader.provide_from_type(t1) c2 = Types::ClassLoader.provide_from_type(t2) if c1 && c2 c2_superclasses = superclasses(c2) superclasses(c1).each do|c1_super| c2_superclasses.each do |c2_super| if c1_super == c2_super return Types::PRuntimeType.new(:runtime => :ruby, :runtime_type_name => c1_super.name) end end end end end # They better both be Any type, or the wrong thing was asked and nil is returned if t1.is_a?(Types::PAnyType) && t2.is_a?(Types::PAnyType) return Types::PAnyType.new() end end # Produces the superclasses of the given class, including the class def superclasses(c) result = [c] while s = c.superclass result << s c = s end result end # Produces a string representing the type # @api public # def string(t) @@string_visitor.visit_this_0(self, t) end # Produces a debug string representing the type (possibly with more information that the regular string format) # @api public # def debug_string(t) @@inspect_visitor.visit_this_0(self, t) end # Reduces an enumerable of types to a single common type. # @api public # def reduce_type(enumerable) enumerable.reduce(nil) {|memo, t| common_type(memo, t) } end # Reduce an enumerable of objects to a single common type # @api public # def infer_and_reduce_type(enumerable) reduce_type(enumerable.collect() {|o| infer(o) }) end # The type of all classes is PType # @api private # def infer_Class(o) Types::PType.new() end # @api private def infer_Closure(o) o.type() end # @api private def infer_Function(o) o.class.dispatcher.to_type end # @api private def infer_Object(o) Types::PRuntimeType.new(:runtime => :ruby, :runtime_type_name => o.class.name) end # The type of all types is PType # @api private # def infer_PAnyType(o) type = Types::PType.new() type.type = o.copy type end # The type of all types is PType # This is the metatype short circuit. # @api private # def infer_PType(o) type = Types::PType.new() type.type = o.copy type end # @api private def infer_String(o) t = Types::PStringType.new() t.addValues(o) t.size_type = size_as_type(o) t end # @api private def infer_Float(o) t = Types::PFloatType.new() t.from = o t.to = o t end # @api private def infer_Integer(o) t = Types::PIntegerType.new() t.from = o t.to = o t end # @api private def infer_Regexp(o) t = Types::PRegexpType.new() t.pattern = o.source t end # @api private def infer_NilClass(o) Types::PNilType.new() end # Inference of :default as PDefaultType, and all other are Ruby[Symbol] # @api private def infer_Symbol(o) case o when :default Types::PDefaultType.new() else infer_Object(o) end end # @api private def infer_TrueClass(o) Types::PBooleanType.new() end # @api private def infer_FalseClass(o) Types::PBooleanType.new() end # @api private # A Puppet::Parser::Resource, or Puppet::Resource # def infer_Resource(o) t = Types::PResourceType.new() t.type_name = o.type.to_s.downcase # Only Puppet::Resource can have a title that is a symbol :undef, a PResource cannot. # A mapping must be made to empty string. A nil value will result in an error later title = o.title t.title = (:undef == title ? '' : title) type = Types::PType.new() type.type = t type end # @api private def infer_Array(o) type = Types::PArrayType.new() type.element_type = if o.empty? Types::PNilType.new() else infer_and_reduce_type(o) end type.size_type = size_as_type(o) type end # @api private def infer_Hash(o) type = Types::PHashType.new() if o.empty? ktype = Types::PNilType.new() etype = Types::PNilType.new() else ktype = infer_and_reduce_type(o.keys()) etype = infer_and_reduce_type(o.values()) end type.key_type = ktype type.element_type = etype type.size_type = size_as_type(o) type end def size_as_type(collection) size = collection.size t = Types::PIntegerType.new() t.from = size t.to = size t end # Common case for everything that intrinsically only has a single type def infer_set_Object(o) infer(o) end def infer_set_Array(o) if o.empty? type = Types::PArrayType.new() type.element_type = Types::PNilType.new() type.size_type = size_as_type(o) else type = Types::PTupleType.new() type.types = o.map() {|x| infer_set(x) } end type end def infer_set_Hash(o) - type = Types::PHashType.new() if o.empty? - ktype = Types::PNilType.new() - vtype = Types::PNilType.new() + type = Types::PHashType.new + type.key_type = Types::PNilType.new + type.element_type = Types::PNilType.new + type.size_type = size_as_type(o) else - ktype = Types::PVariantType.new() - ktype.types = o.keys.map() {|k| infer_set(k) } - etype = Types::PVariantType.new() - etype.types = o.values.map() {|e| infer_set(e) } + if o.keys.find {|k| !instance_of_PStringType(@non_empty_string_t, k) } + type = Types::PHashType.new + ktype = Types::PVariantType.new + ktype.types = o.keys.map {|k| infer_set(k) } + etype = Types::PVariantType.new + etype.types = o.values.map {|e| infer_set(e) } + type.key_type = unwrap_single_variant(ktype) + type.element_type = unwrap_single_variant(etype) + type.size_type = size_as_type(o) + else + elements = [] + o.each_pair do |k,v| + element = Types::PStructElement.new + element.name = k + element.type = infer_set(v) + elements << element + end + type = Types::PStructType.new + type.elements = elements + end end - type.key_type = unwrap_single_variant(ktype) - type.element_type = unwrap_single_variant(etype) - type.size_type = size_as_type(o) type end def unwrap_single_variant(possible_variant) if possible_variant.is_a?(Types::PVariantType) && possible_variant.types.size == 1 possible_variant.types[0] else possible_variant end end # False in general type calculator # @api private def assignable_Object(t, t2) false end # @api private def assignable_PAnyType(t, t2) t2.is_a?(Types::PAnyType) end # @api private def assignable_PNilType(t, t2) # Only undef/nil is assignable to nil type t2.is_a?(Types::PNilType) end # Anything is assignable to a Unit type # @api private def assignable_PUnitType(t, t2) true end # @api private def assignable_PDefaultType(t, t2) # Only default is assignable to default type t2.is_a?(Types::PDefaultType) end # @api private def assignable_PScalarType(t, t2) t2.is_a?(Types::PScalarType) end # @api private def assignable_PNumericType(t, t2) t2.is_a?(Types::PNumericType) end # @api private def assignable_PIntegerType(t, t2) return false unless t2.is_a?(Types::PIntegerType) trange = from_to_ordered(t.from, t.to) t2range = from_to_ordered(t2.from, t2.to) # If t2 min and max are within the range of t trange[0] <= t2range[0] && trange[1] >= t2range[1] end # Transform int range to a size constraint # if range == nil the constraint is 1,1 # if range.from == nil min size = 1 # if range.to == nil max size == Infinity # def size_range(range) return [1,1] if range.nil? from = range.from to = range.to x = from.nil? ? 1 : from y = to.nil? ? Float::INFINITY : to if x < y [x, y] else [y, x] end end # @api private def from_to_ordered(from, to) x = (from.nil? || from == :default) ? -Float::INFINITY : from y = (to.nil? || to == :default) ? Float::INFINITY : to if x < y [x, y] else [y, x] end end # @api private def assignable_PVariantType(t, t2) # Data is a specific variant t2 = @data_variant_t if t2.is_a?(Types::PDataType) if t2.is_a?(Types::PVariantType) # A variant is assignable if all of its options are assignable to one of this type's options return true if t == t2 t2.types.all? do |other| # if the other is a Variant, all of its options, but be assignable to one of this type's options other = other.is_a?(Types::PDataType) ? @data_variant_t : other if other.is_a?(Types::PVariantType) assignable?(t, other) else t.types.any? {|option_t| assignable?(option_t, other) } end end else # A variant is assignable if t2 is assignable to any of its types t.types.any? { |option_t| assignable?(option_t, t2) } end end # Catch all not callable combinations def callable_Object(o, callable_t) false end def callable_PTupleType(args_tuple, callable_t) if args_tuple.size_type raise ArgumentError, "Callable tuple may not have a size constraint when used as args" end # Assume no block was given - i.e. it is nil, and its type is PNilType block_t = @nil_t if self.class.is_kind_of_callable?(args_tuple.types.last) # a split is needed to make it possible to use required, optional, and varargs semantics # of the tuple type. # args_tuple = args_tuple.copy # to drop the callable, it must be removed explicitly since this is an rgen array args_tuple.removeTypes(block_t = args_tuple.types.last()) else # no block was given, if it is required, the below will fail end # unless argument types match parameter types return false unless assignable?(callable_t.param_types, args_tuple) # can the given block be *called* with a signature requirement specified by callable_t? assignable?(callable_t.block_type || @nil_t, block_t) end # @api private def self.is_kind_of_callable?(t, optional = true) case t when Types::PCallableType true when Types::POptionalType optional && is_kind_of_callable?(t.optional_type, optional) when Types::PVariantType t.types.all? {|t2| is_kind_of_callable?(t2, optional) } else false end end def callable_PArrayType(args_array, callable_t) return false unless assignable?(callable_t.param_types, args_array) # does not support calling with a block, but have to check that callable is ok with missing block assignable?(callable_t.block_type || @nil_t, @nil_t) end def callable_PNilType(nil_t, callable_t) # if callable_t is Optional (or indeed PNilType), this means that 'missing callable' is accepted assignable?(callable_t, nil_t) end def callable_PCallableType(given_callable_t, required_callable_t) # If the required callable is euqal or more specific than the given, the given is callable assignable?(required_callable_t, given_callable_t) end def max(a,b) a >=b ? a : b end def min(a,b) a <= b ? a : b end def assignable_PTupleType(t, t2) return true if t == t2 || t.types.empty? && (t2.is_a?(Types::PArrayType)) size_t = t.size_type || Puppet::Pops::Types::TypeFactory.range(*t.size_range) if t2.is_a?(Types::PTupleType) size_t2 = t2.size_type || Puppet::Pops::Types::TypeFactory.range(*t2.size_range) # not assignable if the number of types in t2 is outside number of types in t1 if assignable?(size_t, size_t2) t2.types.size.times do |index| return false unless assignable?((t.types[index] || t.types[-1]), t2.types[index]) end return true else return false end elsif t2.is_a?(Types::PArrayType) t2_entry = t2.element_type # Array of anything can not be assigned (unless tuple is tuple of anything) - this case # was handled at the top of this method. # return false if t2_entry.nil? size_t = t.size_type || Puppet::Pops::Types::TypeFactory.range(*t.size_range) size_t2 = t2.size_type || @collection_default_size_t return false unless assignable?(size_t, size_t2) min(t.types.size, size_t2.range()[1]).times do |index| return false unless assignable?((t.types[index] || t.types[-1]), t2_entry) end true else false end end # Produces the tuple entry at the given index given a tuple type, its from/to constraints on the last # type, and an index. # Produces nil if the index is out of bounds # from must be less than to, and from may not be less than 0 # # @api private # def tuple_entry_at(tuple_t, from, to, index) regular = (tuple_t.types.size - 1) if index < regular tuple_t.types[index] elsif index < regular + to # in the varargs part tuple_t.types[-1] else nil end end # @api private # def assignable_PStructType(t, t2) return true if t == t2 || t.elements.empty? && (t2.is_a?(Types::PHashType)) h = t.hashed_elements if t2.is_a?(Types::PStructType) h2 = t2.hashed_elements h.size == h2.size && h.all? {|k, v| assignable?(v, h2[k]) } elsif t2.is_a?(Types::PHashType) size_t2 = t2.size_type || @collection_default_size_t size_t = Types::PIntegerType.new size_t.from = size_t.to = h.size # compatible size # hash key type must be string of min 1 size # hash value t must be assignable to each key element_type = t2.element_type assignable_PIntegerType(size_t, size_t2) && assignable?(@non_empty_string_t, t2.key_type) && h.all? {|k,v| assignable?(v, element_type) } else false end end # @api private def assignable_POptionalType(t, t2) return true if t2.is_a?(Types::PNilType) if t2.is_a?(Types::POptionalType) assignable?(t.optional_type, t2.optional_type) else assignable?(t.optional_type, t2) end end # @api private def assignable_PEnumType(t, t2) return true if t == t2 if t.values.empty? return true if t2.is_a?(Types::PStringType) || t2.is_a?(Types::PEnumType) || t2.is_a?(Types::PPatternType) end case t2 when Types::PStringType # if the set of strings are all found in the set of enums !t2.values.empty?() && t2.values.all? { |s| t.values.any? { |e| e == s }} when Types::PVariantType t2.types.all? {|variant_t| assignable_PEnumType(t, variant_t) } when Types::PEnumType # empty means any enum return true if t.values.empty? !t2.values.empty? && t2.values.all? { |s| t.values.any? {|e| e == s }} else false end end # @api private def assignable_PStringType(t, t2) if t.values.empty? # A general string is assignable by any other string or pattern restricted string # if the string has a size constraint it does not match since there is no reasonable way # to compute the min/max length a pattern will match. For enum, it is possible to test that # each enumerator value is within range size_t = t.size_type || @collection_default_size_t case t2 when Types::PStringType # true if size compliant size_t2 = t2.size_type || @collection_default_size_t assignable_PIntegerType(size_t, size_t2) when Types::PPatternType # true if size constraint is at least 0 to +Infinity (which is the same as the default) assignable_PIntegerType(size_t, @collection_default_size_t) when Types::PEnumType if t2.values && !t2.values.empty? # true if all enum values are within range min, max = t2.values.map(&:size).minmax trange = from_to_ordered(size_t.from, size_t.to) t2range = [min, max] # If t2 min and max are within the range of t trange[0] <= t2range[0] && trange[1] >= t2range[1] else # enum represents all enums, and thus all strings, a sized constrained string can thus not # be assigned any enum (unless it is max size). assignable_PIntegerType(size_t, @collection_default_size_t) end else # no other type matches string false end elsif t2.is_a?(Types::PStringType) # A specific string acts as a set of strings - must have exactly the same strings # In this case, size does not matter since the definition is very precise anyway Set.new(t.values) == Set.new(t2.values) else # All others are false, since no other type describes the same set of specific strings false end end # @api private def assignable_PPatternType(t, t2) return true if t == t2 case t2 when Types::PStringType, Types::PEnumType values = t2.values when Types::PVariantType return t2.types.all? {|variant_t| assignable_PPatternType(t, variant_t) } when Types::PPatternType return t.patterns.empty? ? true : false else return false end if t2.values.empty? # Strings / Enums (unknown which ones) cannot all match a pattern, but if there is no pattern it is ok # (There should really always be a pattern, but better safe than sorry). return t.patterns.empty? ? true : false end # all strings in String/Enum type must match one of the patterns in Pattern type, # or Pattern represents all Patterns == all Strings regexps = t.patterns.map {|p| p.regexp } regexps.empty? || t2.values.all? { |v| regexps.any? {|re| re.match(v) } } end # @api private def assignable_PFloatType(t, t2) return false unless t2.is_a?(Types::PFloatType) trange = from_to_ordered(t.from, t.to) t2range = from_to_ordered(t2.from, t2.to) # If t2 min and max are within the range of t trange[0] <= t2range[0] && trange[1] >= t2range[1] end # @api private def assignable_PBooleanType(t, t2) t2.is_a?(Types::PBooleanType) end # @api private def assignable_PRegexpType(t, t2) t2.is_a?(Types::PRegexpType) && (t.pattern.nil? || t.pattern == t2.pattern) end # @api private def assignable_PCallableType(t, t2) return false unless t2.is_a?(Types::PCallableType) # nil param_types means, any other Callable is assignable return true if t.param_types.nil? # NOTE: these tests are made in reverse as it is calling the callable that is constrained # (it's lower bound), not its upper bound return false unless assignable?(t2.param_types, t.param_types) # names are ignored, they are just information # Blocks must be compatible this_block_t = t.block_type || @nil_t that_block_t = t2.block_type || @nil_t assignable?(that_block_t, this_block_t) end # @api private def assignable_PCollectionType(t, t2) size_t = t.size_type || @collection_default_size_t case t2 when Types::PCollectionType size_t2 = t2.size_type || @collection_default_size_t assignable_PIntegerType(size_t, size_t2) when Types::PTupleType # compute the tuple's min/max size, and check if that size matches from, to = size_range(t2.size_type) t2s = Types::PIntegerType.new() t2s.from = t2.types.size - 1 + from t2s.to = t2.types.size - 1 + to assignable_PIntegerType(size_t, t2s) when Types::PStructType from = to = t2.elements.size t2s = Types::PIntegerType.new() t2s.from = from t2s.to = to assignable_PIntegerType(size_t, t2s) else false end end # @api private def assignable_PType(t, t2) return false unless t2.is_a?(Types::PType) return true if t.type.nil? # wide enough to handle all types return false if t2.type.nil? # wider than t assignable?(t.type, t2.type) end # Array is assignable if t2 is an Array and t2's element type is assignable, or if t2 is a Tuple # @api private def assignable_PArrayType(t, t2) if t2.is_a?(Types::PArrayType) return false unless assignable?(t.element_type, t2.element_type) assignable_PCollectionType(t, t2) elsif t2.is_a?(Types::PTupleType) return false unless t2.types.all? {|t2_element| assignable?(t.element_type, t2_element) } t2_regular = t2.types[0..-2] t2_ranged = t2.types[-1] t2_from, t2_to = size_range(t2.size_type) t2_required = t2_regular.size + t2_from t_entry = t.element_type # Tuple of anything can not be assigned (unless array is tuple of anything) - this case # was handled at the top of this method. # return false if t_entry.nil? # array type may be size constrained size_t = t.size_type || @collection_default_size_t min, max = size_t.range # Tuple with fewer min entries can not be assigned return false if t2_required < min # Tuple with more optionally available entries can not be assigned return false if t2_regular.size + t2_to > max # each tuple type must be assignable to the element type t2_required.times do |index| t2_entry = tuple_entry_at(t2, t2_from, t2_to, index) return false unless assignable?(t_entry, t2_entry) end # ... and so must the last, possibly optional (ranged) type return assignable?(t_entry, t2_ranged) else false end end # Hash is assignable if t2 is a Hash and t2's key and element types are assignable # @api private def assignable_PHashType(t, t2) case t2 when Types::PHashType return false unless assignable?(t.key_type, t2.key_type) && assignable?(t.element_type, t2.element_type) assignable_PCollectionType(t, t2) when Types::PStructType # hash must accept String as key type # hash must accept all value types # hash must accept the size of the struct size_t = t.size_type || @collection_default_size_t min, max = size_t.range struct_size = t2.elements.size + key_type = t.key_type element_type = t.element_type ( struct_size >= min && struct_size <= max && - assignable?(t.key_type, @non_empty_string_t) && - t2.hashed_elements.all? {|k,v| assignable?(element_type, v) }) + t2.elements.all? {|e| instance_of(key_type, e.name) && assignable?(element_type, e.type) }) else false end end # @api private def assignable_PCatalogEntryType(t1, t2) t2.is_a?(Types::PCatalogEntryType) end # @api private def assignable_PHostClassType(t1, t2) return false unless t2.is_a?(Types::PHostClassType) # Class = Class[name}, Class[name] != Class return true if t1.class_name.nil? # Class[name] = Class[name] return t1.class_name == t2.class_name end # @api private def assignable_PResourceType(t1, t2) return false unless t2.is_a?(Types::PResourceType) return true if t1.type_name.nil? return false if t1.type_name != t2.type_name return true if t1.title.nil? return t1.title == t2.title end # Data is assignable by other Data and by Array[Data] and Hash[Scalar, Data] # @api private def assignable_PDataType(t, t2) t2.is_a?(Types::PDataType) || assignable?(@data_variant_t, t2) end # Assignable if t2's has the same runtime and the runtime name resolves to # a class that is the same or subclass of t1's resolved runtime type name # @api private def assignable_PRuntimeType(t1, t2) return false unless t2.is_a?(Types::PRuntimeType) return false unless t1.runtime == t2.runtime return true if t1.runtime_type_name.nil? # t1 is wider return false if t2.runtime_type_name.nil? # t1 not nil, so t2 can not be wider # NOTE: This only supports Ruby, must change when/if the set of runtimes is expanded c1 = class_from_string(t1.runtime_type_name) c2 = class_from_string(t2.runtime_type_name) return false unless c1.is_a?(Class) && c2.is_a?(Class) !!(c2 <= c1) end # @api private def debug_string_Object(t) string(t) end # @api private def string_PType(t) if t.type.nil? "Type" else "Type[#{string(t.type)}]" end end # @api private def string_NilClass(t) ; '?' ; end # @api private def string_String(t) ; t ; end # @api private def string_Symbol(t) ; t.to_s ; end def string_PAnyType(t) ; "Any" ; end # @api private def string_PNilType(t) ; 'Undef' ; end # @api private def string_PDefaultType(t) ; 'Default' ; end # @api private def string_PBooleanType(t) ; "Boolean" ; end # @api private def string_PScalarType(t) ; "Scalar" ; end # @api private def string_PDataType(t) ; "Data" ; end # @api private def string_PNumericType(t) ; "Numeric" ; end # @api private def string_PIntegerType(t) range = range_array_part(t) unless range.empty? "Integer[#{range.join(', ')}]" else "Integer" end end # Produces a string from an Integer range type that is used inside other type strings # @api private def range_array_part(t) return [] if t.nil? || (t.from.nil? && t.to.nil?) [t.from.nil? ? 'default' : t.from , t.to.nil? ? 'default' : t.to ] end # @api private def string_PFloatType(t) range = range_array_part(t) unless range.empty? "Float[#{range.join(', ')}]" else "Float" end end # @api private def string_PRegexpType(t) t.pattern.nil? ? "Regexp" : "Regexp[#{t.regexp.inspect}]" end # @api private def string_PStringType(t) # skip values in regular output - see debug_string range = range_array_part(t.size_type) unless range.empty? "String[#{range.join(', ')}]" else "String" end end # @api private def debug_string_PStringType(t) range = range_array_part(t.size_type) range_part = range.empty? ? '' : '[' << range.join(' ,') << '], ' "String[" << range_part << (t.values.map {|s| "'#{s}'" }).join(', ') << ']' end # @api private def string_PEnumType(t) return "Enum" if t.values.empty? "Enum[" << t.values.map {|s| "'#{s}'" }.join(', ') << ']' end # @api private def string_PVariantType(t) return "Variant" if t.types.empty? "Variant[" << t.types.map {|t2| string(t2) }.join(', ') << ']' end # @api private def string_PTupleType(t) range = range_array_part(t.size_type) return "Tuple" if t.types.empty? s = "Tuple[" << t.types.map {|t2| string(t2) }.join(', ') unless range.empty? s << ", " << range.join(', ') end s << "]" s end # @api private def string_PCallableType(t) # generic return "Callable" if t.param_types.nil? if t.param_types.types.empty? range = [0, 0] else range = range_array_part(t.param_types.size_type) end # translate to string, and skip Unit types types = t.param_types.types.map {|t2| string(t2) unless t2.class == Types::PUnitType }.compact s = "Callable[" << types.join(', ') unless range.empty? (s << ', ') unless types.empty? s << range.join(', ') end # Add block T last (after min, max) if present) # unless t.block_type.nil? (s << ', ') unless types.empty? && range.empty? s << string(t.block_type) end s << "]" s end # @api private def string_PStructType(t) return "Struct" if t.elements.empty? "Struct[{" << t.elements.map {|element| string(element) }.join(', ') << "}]" end def string_PStructElement(t) "'#{t.name}'=>#{string(t.type)}" end # @api private def string_PPatternType(t) return "Pattern" if t.patterns.empty? "Pattern[" << t.patterns.map {|s| "#{s.regexp.inspect}" }.join(', ') << ']' end # @api private def string_PCollectionType(t) range = range_array_part(t.size_type) unless range.empty? "Collection[#{range.join(', ')}]" else "Collection" end end # @api private def string_PUnitType(t) "Unit" end # @api private def string_PRuntimeType(t) ; "Runtime[#{string(t.runtime)}, #{string(t.runtime_type_name)}]" ; end # @api private def string_PArrayType(t) parts = [string(t.element_type)] + range_array_part(t.size_type) "Array[#{parts.join(', ')}]" end # @api private def string_PHashType(t) parts = [string(t.key_type), string(t.element_type)] + range_array_part(t.size_type) "Hash[#{parts.join(', ')}]" end # @api private def string_PCatalogEntryType(t) "CatalogEntry" end # @api private def string_PHostClassType(t) if t.class_name "Class[#{t.class_name}]" else "Class" end end # @api private def string_PResourceType(t) if t.type_name if t.title "#{capitalize_segments(t.type_name)}['#{t.title}']" else capitalize_segments(t.type_name) end else "Resource" end end def string_POptionalType(t) if t.optional_type.nil? "Optional" else "Optional[#{string(t.optional_type)}]" end end # Catches all non enumerable types # @api private def enumerable_Object(o) nil end # @api private def enumerable_PIntegerType(t) # Not enumerable if representing an infinite range return nil if t.size == Float::INFINITY t end def self.copy_as_tuple(t) case t when Types::PTupleType t.copy when Types::PArrayType # transform array to tuple result = Types::PTupleType.new result.addTypes(t.element_type.copy) result.size_type = t.size_type.nil? ? nil : t.size_type.copy result else raise ArgumentError, "Internal Error: Only Array and Tuple can be given to copy_as_tuple" end end # Debugging to_s to reduce the amount of output def to_s '[a TypeCalculator]' end private NAME_SEGMENT_SEPARATOR = '::'.freeze def capitalize_segments(s) s.split(NAME_SEGMENT_SEPARATOR).map(&:capitalize).join(NAME_SEGMENT_SEPARATOR) end def class_from_string(str) begin str.split(NAME_SEGMENT_SEPARATOR).inject(Object) do |memo, name_segment| memo.const_get(name_segment) end rescue NameError return nil end end def common_data?(t1, t2) assignable?(@data_t, t1) && assignable?(@data_t, t2) end def common_scalar?(t1, t2) assignable?(@scalar_t, t1) && assignable?(@scalar_t, t2) end def common_numeric?(t1, t2) assignable?(@numeric_t, t1) && assignable?(@numeric_t, t2) end end diff --git a/spec/unit/functions4_spec.rb b/spec/unit/functions4_spec.rb index eaa42a1e9..bf7180106 100644 --- a/spec/unit/functions4_spec.rb +++ b/spec/unit/functions4_spec.rb @@ -1,660 +1,682 @@ require 'spec_helper' require 'puppet/pops' require 'puppet/loaders' require 'puppet_spec/pops' require 'puppet_spec/scope' module FunctionAPISpecModule class TestDuck end class TestFunctionLoader < Puppet::Pops::Loader::StaticLoader def initialize @functions = {} end def add_function(name, function) typed_name = Puppet::Pops::Loader::Loader::TypedName.new(:function, name) entry = Puppet::Pops::Loader::Loader::NamedEntry.new(typed_name, function, __FILE__) @functions[typed_name] = entry end # override StaticLoader def load_constant(typed_name) @functions[typed_name] end end end describe 'the 4x function api' do include FunctionAPISpecModule include PuppetSpec::Pops include PuppetSpec::Scope let(:loader) { FunctionAPISpecModule::TestFunctionLoader.new } it 'allows a simple function to be created without dispatch declaration' do f = Puppet::Functions.create_function('min') do def min(x,y) x <= y ? x : y end end # the produced result is a Class inheriting from Function expect(f.class).to be(Class) expect(f.superclass).to be(Puppet::Functions::Function) # and this class had the given name (not a real Ruby class name) expect(f.name).to eql('min') end it 'refuses to create functions that are not based on the Function class' do expect do Puppet::Functions.create_function('testing', Object) {} end.to raise_error(ArgumentError, 'Functions must be based on Puppet::Pops::Functions::Function. Got Object') end it 'refuses to create functions with parameters that are not named with a symbol' do expect do Puppet::Functions.create_function('testing') do dispatch :test do param 'Integer', 'not_symbol' end def test(x) end end end.to raise_error(ArgumentError, /Parameter name argument must be a Symbol/) end it 'a function without arguments can be defined and called without dispatch declaration' do f = create_noargs_function_class() func = f.new(:closure_scope, :loader) expect(func.call({})).to eql(10) end it 'an error is raised when calling a no arguments function with arguments' do f = create_noargs_function_class() func = f.new(:closure_scope, :loader) expect{func.call({}, 'surprise')}.to raise_error(ArgumentError, "function 'test' called with mis-matched arguments expected: test() - arg count {0} actual: test(String) - arg count {1}") end it 'a simple function can be called' do f = create_min_function_class() # TODO: Bogus parameters, not yet used func = f.new(:closure_scope, :loader) expect(func.is_a?(Puppet::Functions::Function)).to be_truthy expect(func.call({}, 10,20)).to eql(10) end it 'an error is raised if called with too few arguments' do f = create_min_function_class() # TODO: Bogus parameters, not yet used func = f.new(:closure_scope, :loader) expect(func.is_a?(Puppet::Functions::Function)).to be_truthy signature = 'Any x, Any y' expect do func.call({}, 10) end.to raise_error(ArgumentError, "function 'min' called with mis-matched arguments expected: min(#{signature}) - arg count {2} actual: min(Integer) - arg count {1}") end it 'an error is raised if called with too many arguments' do f = create_min_function_class() # TODO: Bogus parameters, not yet used func = f.new(:closure_scope, :loader) expect(func.is_a?(Puppet::Functions::Function)).to be_truthy signature = 'Any x, Any y' expect do func.call({}, 10, 10, 10) end.to raise_error(ArgumentError, Regexp.new(Regexp.escape( "function 'min' called with mis-matched arguments expected: min(#{signature}) - arg count {2} actual: min(Integer, Integer, Integer) - arg count {3}"))) end it 'an error is raised if simple function-name and method are not matched' do expect do f = create_badly_named_method_function_class() end.to raise_error(ArgumentError, /Function Creation Error, cannot create a default dispatcher for function 'mix', no method with this name found/) end it 'the implementation separates dispatchers for different functions' do # this tests that meta programming / construction puts class attributes in the correct class f1 = create_min_function_class() f2 = create_max_function_class() d1 = f1.dispatcher d2 = f2.dispatcher expect(d1).to_not eql(d2) expect(d1.dispatchers[0]).to_not eql(d2.dispatchers[0]) end context 'when using regular dispatch' do it 'a function can be created using dispatch and called' do f = create_min_function_class_using_dispatch() func = f.new(:closure_scope, :loader) expect(func.call({}, 3,4)).to eql(3) end it 'an error is raised with reference to given parameter names when called with mis-matched arguments' do f = create_min_function_class_using_dispatch() # TODO: Bogus parameters, not yet used func = f.new(:closure_scope, :loader) expect(func.is_a?(Puppet::Functions::Function)).to be_truthy expect do func.call({}, 10, 10, 10) end.to raise_error(ArgumentError, Regexp.new(Regexp.escape( "function 'min' called with mis-matched arguments expected: min(Numeric a, Numeric b) - arg count {2} actual: min(Integer, Integer, Integer) - arg count {3}"))) end it 'an error includes optional indicators and count for last element' do f = create_function_with_optionals_and_varargs() # TODO: Bogus parameters, not yet used func = f.new(:closure_scope, :loader) expect(func.is_a?(Puppet::Functions::Function)).to be_truthy signature = 'Any x, Any y, Any a?, Any b?, Any c{0,}' expect do func.call({}, 10) end.to raise_error(ArgumentError, "function 'min' called with mis-matched arguments expected: min(#{signature}) - arg count {2,} actual: min(Integer) - arg count {1}") end it 'an error includes optional indicators and count for last element when defined via dispatch' do f = create_function_with_optionals_and_varargs_via_dispatch() # TODO: Bogus parameters, not yet used func = f.new(:closure_scope, :loader) expect(func.is_a?(Puppet::Functions::Function)).to be_truthy expect do func.call({}, 10) end.to raise_error(ArgumentError, "function 'min' called with mis-matched arguments expected: min(Numeric x, Numeric y, Numeric a?, Numeric b?, Numeric c{0,}) - arg count {2,} actual: min(Integer) - arg count {1}") end it 'a function can be created using dispatch and called' do f = create_min_function_class_disptaching_to_two_methods() func = f.new(:closure_scope, :loader) expect(func.call({}, 3,4)).to eql(3) expect(func.call({}, 'Apple', 'Banana')).to eql('Apple') end it 'an error is raised with reference to multiple methods when called with mis-matched arguments' do f = create_min_function_class_disptaching_to_two_methods() # TODO: Bogus parameters, not yet used func = f.new(:closure_scope, :loader) expect(func.is_a?(Puppet::Functions::Function)).to be_truthy expect do func.call({}, 10, 10, 10) end.to raise_error(ArgumentError, "function 'min' called with mis-matched arguments expected one of: min(Numeric a, Numeric b) - arg count {2} min(String s1, String s2) - arg count {2} actual: min(Integer, Integer, Integer) - arg count {3}") end context 'can use injection' do before :all do injector = Puppet::Pops::Binder::Injector.create('test') do bind.name('a_string').to('evoe') bind.name('an_int').to(42) end Puppet.push_context({:injector => injector}, "injector for testing function API") end after :all do Puppet.pop_context() end it 'attributes can be injected' do f1 = create_function_with_class_injection() f = f1.new(:closure_scope, :loader) expect(f.test_attr2()).to eql("evoe") expect(f.serial().produce(nil)).to eql(42) expect(f.test_attr().class.name).to eql("FunctionAPISpecModule::TestDuck") end it 'parameters can be injected and woven with regular dispatch' do f1 = create_function_with_param_injection_regular() f = f1.new(:closure_scope, :loader) expect(f.call(nil, 10, 20)).to eql("evoe! 10, and 20 < 42 = true") expect(f.call(nil, 50, 20)).to eql("evoe! 50, and 20 < 42 = false") end end context 'when requesting a type' do it 'responds with a Callable for a single signature' do tf = Puppet::Pops::Types::TypeFactory fc = create_min_function_class_using_dispatch() t = fc.dispatcher.to_type expect(t.class).to be(Puppet::Pops::Types::PCallableType) expect(t.param_types.class).to be(Puppet::Pops::Types::PTupleType) expect(t.param_types.types).to eql([tf.numeric(), tf.numeric()]) expect(t.block_type).to be_nil end it 'responds with a Variant[Callable...] for multiple signatures' do tf = Puppet::Pops::Types::TypeFactory fc = create_min_function_class_disptaching_to_two_methods() t = fc.dispatcher.to_type expect(t.class).to be(Puppet::Pops::Types::PVariantType) expect(t.types.size).to eql(2) t1 = t.types[0] expect(t1.param_types.class).to be(Puppet::Pops::Types::PTupleType) expect(t1.param_types.types).to eql([tf.numeric(), tf.numeric()]) expect(t1.block_type).to be_nil t2 = t.types[1] expect(t2.param_types.class).to be(Puppet::Pops::Types::PTupleType) expect(t2.param_types.types).to eql([tf.string(), tf.string()]) expect(t2.block_type).to be_nil end end context 'supports lambdas' do it 'such that, a required block can be defined and given as an argument' do # use a Function as callable the_callable = create_min_function_class().new(:closure_scope, :loader) the_function = create_function_with_required_block_all_defaults().new(:closure_scope, :loader) result = the_function.call({}, 10, the_callable) expect(result).to be(the_callable) end it 'such that, a missing required block when called raises an error' do # use a Function as callable the_function = create_function_with_required_block_all_defaults().new(:closure_scope, :loader) expect do the_function.call({}, 10) end.to raise_error(ArgumentError, "function 'test' called with mis-matched arguments expected: test(Integer x, Callable block) - arg count {2} actual: test(Integer) - arg count {1}") end it 'such that, an optional block can be defined and given as an argument' do # use a Function as callable the_callable = create_min_function_class().new(:closure_scope, :loader) the_function = create_function_with_optional_block_all_defaults().new(:closure_scope, :loader) result = the_function.call({}, 10, the_callable) expect(result).to be(the_callable) end it 'such that, an optional block can be omitted when called and gets the value nil' do # use a Function as callable the_function = create_function_with_optional_block_all_defaults().new(:closure_scope, :loader) expect(the_function.call({}, 10)).to be_nil end + + it 'such that, a scope can be injected and a block can be used' do + # use a Function as callable + the_callable = create_min_function_class().new(:closure_scope, :loader) + the_function = create_function_with_scope_required_block_all_defaults().new(:closure_scope, :loader) + expect(the_function.call({}, 10, the_callable)).to be(the_callable) + end end context 'provides signature information' do it 'about capture rest (varargs)' do fc = create_function_with_optionals_and_varargs signatures = fc.signatures expect(signatures.size).to eql(1) signature = signatures[0] expect(signature.last_captures_rest?).to be_truthy end it 'about optional and required parameters' do fc = create_function_with_optionals_and_varargs signature = fc.signatures[0] expect(signature.args_range).to eql( [2, Float::INFINITY ] ) expect(signature.infinity?(signature.args_range[1])).to be_truthy end it 'about block not being allowed' do fc = create_function_with_optionals_and_varargs signature = fc.signatures[0] expect(signature.block_range).to eql( [ 0, 0 ] ) expect(signature.block_type).to be_nil end it 'about required block' do fc = create_function_with_required_block_all_defaults signature = fc.signatures[0] expect(signature.block_range).to eql( [ 1, 1 ] ) expect(signature.block_type).to_not be_nil end it 'about optional block' do fc = create_function_with_optional_block_all_defaults signature = fc.signatures[0] expect(signature.block_range).to eql( [ 0, 1 ] ) expect(signature.block_type).to_not be_nil end it 'about the type' do fc = create_function_with_optional_block_all_defaults signature = fc.signatures[0] expect(signature.type.class).to be(Puppet::Pops::Types::PCallableType) end it 'about parameter names obtained from ruby introspection' do fc = create_min_function_class signature = fc.signatures[0] expect(signature.parameter_names).to eql(['x', 'y']) end it 'about parameter names specified with dispatch' do fc = create_min_function_class_using_dispatch signature = fc.signatures[0] expect(signature.parameter_names).to eql([:a, :b]) end it 'about block_name when it is *not* given in the definition' do # neither type, nor name fc = create_function_with_required_block_all_defaults signature = fc.signatures[0] expect(signature.block_name).to eql(:block) # no name given, only type fc = create_function_with_required_block_given_type signature = fc.signatures[0] expect(signature.block_name).to eql(:block) end it 'about block_name when it *is* given in the definition' do # neither type, nor name fc = create_function_with_required_block_default_type signature = fc.signatures[0] expect(signature.block_name).to eql(:the_block) # no name given, only type fc = create_function_with_required_block_fully_specified signature = fc.signatures[0] expect(signature.block_name).to eql(:the_block) end end context 'supports calling other functions' do before(:all) do Puppet.push_context( {:loaders => Puppet::Pops::Loaders.new(Puppet::Node::Environment.create(:testing, []))}) end after(:all) do Puppet.pop_context() end it 'such that, other functions are callable by name' do fc = Puppet::Functions.create_function('test') do def test() # Call a function available in the puppet system call_function('assert_type', 'Integer', 10) end end # initiate the function the same way the loader initiates it f = fc.new(:closure_scope, Puppet.lookup(:loaders).puppet_system_loader) expect(f.call({})).to eql(10) end it 'such that, calling a non existing function raises an error' do fc = Puppet::Functions.create_function('test') do def test() # Call a function not available in the puppet system call_function('no_such_function', 'Integer', 'hello') end end # initiate the function the same way the loader initiates it f = fc.new(:closure_scope, Puppet.lookup(:loaders).puppet_system_loader) expect{f.call({})}.to raise_error(ArgumentError, "Function test(): cannot call function 'no_such_function' - not found") end end context 'supports calling ruby functions with lambda from puppet' do before(:all) do Puppet.push_context( {:loaders => Puppet::Pops::Loaders.new(Puppet::Node::Environment.create(:testing, []))}) end after(:all) do Puppet.pop_context() end before(:each) do Puppet[:strict_variables] = true end let(:parser) { Puppet::Pops::Parser::EvaluatingParser.new } let(:node) { 'node.example.com' } let(:scope) { s = create_test_scope_for_node(node); s } it 'function with required block can be called' do # construct ruby function to call fc = Puppet::Functions.create_function('testing::test') do dispatch :test do param 'Integer', :x # block called 'the_block', and using "all_callables" required_block_param #(all_callables(), 'the_block') end def test(x, block) # call the block with x block.call(x) end end # add the function to the loader (as if it had been loaded from somewhere) the_loader = loader() f = fc.new({}, the_loader) loader.add_function('testing::test', f) # evaluate a puppet call source = "testing::test(10) |$x| { $x+1 }" program = parser.parse_string(source, __FILE__) Puppet::Pops::Adapters::LoaderAdapter.adapt(program.model).loader = the_loader expect(parser.evaluate(scope, program)).to eql(11) end end end def create_noargs_function_class f = Puppet::Functions.create_function('test') do def test() 10 end end end def create_min_function_class f = Puppet::Functions.create_function('min') do def min(x,y) x <= y ? x : y end end end def create_max_function_class f = Puppet::Functions.create_function('max') do def max(x,y) x >= y ? x : y end end end def create_badly_named_method_function_class f = Puppet::Functions.create_function('mix') do def mix_up(x,y) x <= y ? x : y end end end def create_min_function_class_using_dispatch f = Puppet::Functions.create_function('min') do dispatch :min do param 'Numeric', :a param 'Numeric', :b end def min(x,y) x <= y ? x : y end end end def create_min_function_class_disptaching_to_two_methods f = Puppet::Functions.create_function('min') do dispatch :min do param 'Numeric', :a param 'Numeric', :b end dispatch :min_s do param 'String', :s1 param 'String', :s2 end def min(x,y) x <= y ? x : y end def min_s(x,y) cmp = (x.downcase <=> y.downcase) cmp <= 0 ? x : y end end end def create_function_with_optionals_and_varargs f = Puppet::Functions.create_function('min') do def min(x,y,a=1, b=1, *c) x <= y ? x : y end end end def create_function_with_optionals_and_varargs_via_dispatch f = Puppet::Functions.create_function('min') do dispatch :min do param 'Numeric', :x param 'Numeric', :y param 'Numeric', :a param 'Numeric', :b param 'Numeric', :c arg_count 2, :default end def min(x,y,a=1, b=1, *c) x <= y ? x : y end end end def create_function_with_class_injection f = Puppet::Functions.create_function('test', Puppet::Functions::InternalFunction) do attr_injected Puppet::Pops::Types::TypeFactory.type_of(FunctionAPISpecModule::TestDuck), :test_attr attr_injected Puppet::Pops::Types::TypeFactory.string(), :test_attr2, "a_string" attr_injected_producer Puppet::Pops::Types::TypeFactory.integer(), :serial, "an_int" def test(x,y,a=1, b=1, *c) x <= y ? x : y end end end def create_function_with_param_injection_regular f = Puppet::Functions.create_function('test', Puppet::Functions::InternalFunction) do attr_injected Puppet::Pops::Types::TypeFactory.type_of(FunctionAPISpecModule::TestDuck), :test_attr attr_injected Puppet::Pops::Types::TypeFactory.string(), :test_attr2, "a_string" attr_injected_producer Puppet::Pops::Types::TypeFactory.integer(), :serial, "an_int" dispatch :test do injected_param Puppet::Pops::Types::TypeFactory.string, :x, 'a_string' injected_producer_param Puppet::Pops::Types::TypeFactory.integer, :y, 'an_int' param 'Scalar', :a param 'Scalar', :b end def test(x,y,a,b) y_produced = y.produce(nil) "#{x}! #{a}, and #{b} < #{y_produced} = #{ !!(a < y_produced && b < y_produced)}" end end end def create_function_with_required_block_all_defaults f = Puppet::Functions.create_function('test') do dispatch :test do param 'Integer', :x # use defaults, any callable, name is 'block' required_block_param end def test(x, block) # returns the block to make it easy to test what it got when called block end end end + def create_function_with_scope_required_block_all_defaults + f = Puppet::Functions.create_function('test', Puppet::Functions::InternalFunction) do + dispatch :test do + scope_param + param 'Integer', :x + # use defaults, any callable, name is 'block' + required_block_param + end + def test(scope, x, block) + # returns the block to make it easy to test what it got when called + block + end + end + end + def create_function_with_required_block_default_type f = Puppet::Functions.create_function('test') do dispatch :test do param 'Integer', :x # use defaults, any callable, name is 'block' required_block_param :the_block end def test(x, block) # returns the block to make it easy to test what it got when called block end end end def create_function_with_required_block_given_type f = Puppet::Functions.create_function('test') do dispatch :test do param 'Integer', :x required_block_param end def test(x, block) # returns the block to make it easy to test what it got when called block end end end def create_function_with_required_block_fully_specified f = Puppet::Functions.create_function('test') do dispatch :test do param 'Integer', :x # use defaults, any callable, name is 'block' required_block_param('Callable', :the_block) end def test(x, block) # returns the block to make it easy to test what it got when called block end end end def create_function_with_optional_block_all_defaults f = Puppet::Functions.create_function('test') do dispatch :test do param 'Integer', :x # use defaults, any callable, name is 'block' optional_block_param end def test(x, block=nil) # returns the block to make it easy to test what it got when called # a default of nil must be used or the call will fail with a missing parameter block end end end end diff --git a/spec/unit/pops/types/type_calculator_spec.rb b/spec/unit/pops/types/type_calculator_spec.rb index 19c17f46c..007e9d53f 100644 --- a/spec/unit/pops/types/type_calculator_spec.rb +++ b/spec/unit/pops/types/type_calculator_spec.rb @@ -1,1877 +1,1920 @@ require 'spec_helper' require 'puppet/pops' describe 'The type calculator' do let(:calculator) { Puppet::Pops::Types::TypeCalculator.new() } def range_t(from, to) t = Puppet::Pops::Types::PIntegerType.new t.from = from t.to = to t end def constrained_t(t, from, to) Puppet::Pops::Types::TypeFactory.constrain_size(t, from, to) end def pattern_t(*patterns) Puppet::Pops::Types::TypeFactory.pattern(*patterns) end def regexp_t(pattern) Puppet::Pops::Types::TypeFactory.regexp(pattern) end def string_t(*strings) Puppet::Pops::Types::TypeFactory.string(*strings) end def callable_t(*params) Puppet::Pops::Types::TypeFactory.callable(*params) end def all_callables_t(*params) Puppet::Pops::Types::TypeFactory.all_callables() end def with_block_t(callable_t, *params) Puppet::Pops::Types::TypeFactory.with_block(callable_t, *params) end def with_optional_block_t(callable_t, *params) Puppet::Pops::Types::TypeFactory.with_optional_block(callable_t, *params) end def enum_t(*strings) Puppet::Pops::Types::TypeFactory.enum(*strings) end def variant_t(*types) Puppet::Pops::Types::TypeFactory.variant(*types) end def integer_t() Puppet::Pops::Types::TypeFactory.integer() end def array_t(t) Puppet::Pops::Types::TypeFactory.array_of(t) end def hash_t(k,v) Puppet::Pops::Types::TypeFactory.hash_of(v, k) end def data_t() Puppet::Pops::Types::TypeFactory.data() end def factory() Puppet::Pops::Types::TypeFactory end def collection_t() Puppet::Pops::Types::TypeFactory.collection() end def tuple_t(*types) Puppet::Pops::Types::TypeFactory.tuple(*types) end def struct_t(type_hash) Puppet::Pops::Types::TypeFactory.struct(type_hash) end def object_t Puppet::Pops::Types::TypeFactory.any() end def unit_t # Cannot be created via factory, the type is private to the type system Puppet::Pops::Types::PUnitType.new end def types Puppet::Pops::Types end shared_context "types_setup" do # Do not include the special type Unit in this list def all_types [ Puppet::Pops::Types::PAnyType, Puppet::Pops::Types::PNilType, Puppet::Pops::Types::PDataType, Puppet::Pops::Types::PScalarType, Puppet::Pops::Types::PStringType, Puppet::Pops::Types::PNumericType, Puppet::Pops::Types::PIntegerType, Puppet::Pops::Types::PFloatType, Puppet::Pops::Types::PRegexpType, Puppet::Pops::Types::PBooleanType, Puppet::Pops::Types::PCollectionType, Puppet::Pops::Types::PArrayType, Puppet::Pops::Types::PHashType, Puppet::Pops::Types::PRuntimeType, Puppet::Pops::Types::PHostClassType, Puppet::Pops::Types::PResourceType, Puppet::Pops::Types::PPatternType, Puppet::Pops::Types::PEnumType, Puppet::Pops::Types::PVariantType, Puppet::Pops::Types::PStructType, Puppet::Pops::Types::PTupleType, Puppet::Pops::Types::PCallableType, Puppet::Pops::Types::PType, Puppet::Pops::Types::POptionalType, Puppet::Pops::Types::PDefaultType, ] end def scalar_types # PVariantType is also scalar, if its types are all Scalar [ Puppet::Pops::Types::PScalarType, Puppet::Pops::Types::PStringType, Puppet::Pops::Types::PNumericType, Puppet::Pops::Types::PIntegerType, Puppet::Pops::Types::PFloatType, Puppet::Pops::Types::PRegexpType, Puppet::Pops::Types::PBooleanType, Puppet::Pops::Types::PPatternType, Puppet::Pops::Types::PEnumType, ] end def numeric_types # PVariantType is also numeric, if its types are all numeric [ Puppet::Pops::Types::PNumericType, Puppet::Pops::Types::PIntegerType, Puppet::Pops::Types::PFloatType, ] end def string_types # PVariantType is also string type, if its types are all compatible [ Puppet::Pops::Types::PStringType, Puppet::Pops::Types::PPatternType, Puppet::Pops::Types::PEnumType, ] end def collection_types # PVariantType is also string type, if its types are all compatible [ Puppet::Pops::Types::PCollectionType, Puppet::Pops::Types::PHashType, Puppet::Pops::Types::PArrayType, Puppet::Pops::Types::PStructType, Puppet::Pops::Types::PTupleType, ] end def data_compatible_types result = scalar_types result << Puppet::Pops::Types::PDataType result << array_t(types::PDataType.new) result << types::TypeFactory.hash_of_data result << Puppet::Pops::Types::PNilType tmp = tuple_t(types::PDataType.new) result << (tmp) tmp.size_type = range_t(0, nil) result end def type_from_class(c) c.is_a?(Class) ? c.new : c end end context 'when inferring ruby' do it 'fixnum translates to PIntegerType' do expect(calculator.infer(1).class).to eq(Puppet::Pops::Types::PIntegerType) end it 'large fixnum (or bignum depending on architecture) translates to PIntegerType' do expect(calculator.infer(2**33).class).to eq(Puppet::Pops::Types::PIntegerType) end it 'float translates to PFloatType' do expect(calculator.infer(1.3).class).to eq(Puppet::Pops::Types::PFloatType) end it 'string translates to PStringType' do expect(calculator.infer('foo').class).to eq(Puppet::Pops::Types::PStringType) end it 'inferred string type knows the string value' do t = calculator.infer('foo') expect(t.class).to eq(Puppet::Pops::Types::PStringType) expect(t.values).to eq(['foo']) end it 'boolean true translates to PBooleanType' do expect(calculator.infer(true).class).to eq(Puppet::Pops::Types::PBooleanType) end it 'boolean false translates to PBooleanType' do expect(calculator.infer(false).class).to eq(Puppet::Pops::Types::PBooleanType) end it 'regexp translates to PRegexpType' do expect(calculator.infer(/^a regular expression$/).class).to eq(Puppet::Pops::Types::PRegexpType) end it 'nil translates to PNilType' do expect(calculator.infer(nil).class).to eq(Puppet::Pops::Types::PNilType) end it ':undef translates to PRuntimeType' do expect(calculator.infer(:undef).class).to eq(Puppet::Pops::Types::PRuntimeType) end it 'an instance of class Foo translates to PRuntimeType[ruby, Foo]' do class Foo end t = calculator.infer(Foo.new) expect(t.class).to eq(Puppet::Pops::Types::PRuntimeType) expect(t.runtime).to eq(:ruby) expect(t.runtime_type_name).to eq('Foo') end context 'array' do it 'translates to PArrayType' do expect(calculator.infer([1,2]).class).to eq(Puppet::Pops::Types::PArrayType) end it 'with fixnum values translates to PArrayType[PIntegerType]' do expect(calculator.infer([1,2]).element_type.class).to eq(Puppet::Pops::Types::PIntegerType) end it 'with 32 and 64 bit integer values translates to PArrayType[PIntegerType]' do expect(calculator.infer([1,2**33]).element_type.class).to eq(Puppet::Pops::Types::PIntegerType) end it 'Range of integer values are computed' do t = calculator.infer([-3,0,42]).element_type expect(t.class).to eq(Puppet::Pops::Types::PIntegerType) expect(t.from).to eq(-3) expect(t.to).to eq(42) end it "Compound string values are computed" do t = calculator.infer(['a','b', 'c']).element_type expect(t.class).to eq(Puppet::Pops::Types::PStringType) expect(t.values).to eq(['a', 'b', 'c']) end it 'with fixnum and float values translates to PArrayType[PNumericType]' do expect(calculator.infer([1,2.0]).element_type.class).to eq(Puppet::Pops::Types::PNumericType) end it 'with fixnum and string values translates to PArrayType[PScalarType]' do expect(calculator.infer([1,'two']).element_type.class).to eq(Puppet::Pops::Types::PScalarType) end it 'with float and string values translates to PArrayType[PScalarType]' do expect(calculator.infer([1.0,'two']).element_type.class).to eq(Puppet::Pops::Types::PScalarType) end it 'with fixnum, float, and string values translates to PArrayType[PScalarType]' do expect(calculator.infer([1, 2.0,'two']).element_type.class).to eq(Puppet::Pops::Types::PScalarType) end it 'with fixnum and regexp values translates to PArrayType[PScalarType]' do expect(calculator.infer([1, /two/]).element_type.class).to eq(Puppet::Pops::Types::PScalarType) end it 'with string and regexp values translates to PArrayType[PScalarType]' do expect(calculator.infer(['one', /two/]).element_type.class).to eq(Puppet::Pops::Types::PScalarType) end it 'with string and symbol values translates to PArrayType[PAnyType]' do expect(calculator.infer(['one', :two]).element_type.class).to eq(Puppet::Pops::Types::PAnyType) end it 'with fixnum and nil values translates to PArrayType[PIntegerType]' do expect(calculator.infer([1, nil]).element_type.class).to eq(Puppet::Pops::Types::PIntegerType) end it 'with arrays of string values translates to PArrayType[PArrayType[PStringType]]' do et = calculator.infer([['first' 'array'], ['second','array']]) expect(et.class).to eq(Puppet::Pops::Types::PArrayType) et = et.element_type expect(et.class).to eq(Puppet::Pops::Types::PArrayType) et = et.element_type expect(et.class).to eq(Puppet::Pops::Types::PStringType) end it 'with array of string values and array of fixnums translates to PArrayType[PArrayType[PScalarType]]' do et = calculator.infer([['first' 'array'], [1,2]]) expect(et.class).to eq(Puppet::Pops::Types::PArrayType) et = et.element_type expect(et.class).to eq(Puppet::Pops::Types::PArrayType) et = et.element_type expect(et.class).to eq(Puppet::Pops::Types::PScalarType) end it 'with hashes of string values translates to PArrayType[PHashType[PStringType]]' do et = calculator.infer([{:first => 'first', :second => 'second' }, {:first => 'first', :second => 'second' }]) expect(et.class).to eq(Puppet::Pops::Types::PArrayType) et = et.element_type expect(et.class).to eq(Puppet::Pops::Types::PHashType) et = et.element_type expect(et.class).to eq(Puppet::Pops::Types::PStringType) end it 'with hash of string values and hash of fixnums translates to PArrayType[PHashType[PScalarType]]' do et = calculator.infer([{:first => 'first', :second => 'second' }, {:first => 1, :second => 2 }]) expect(et.class).to eq(Puppet::Pops::Types::PArrayType) et = et.element_type expect(et.class).to eq(Puppet::Pops::Types::PHashType) et = et.element_type expect(et.class).to eq(Puppet::Pops::Types::PScalarType) end end context 'hash' do it 'translates to PHashType' do expect(calculator.infer({:first => 1, :second => 2}).class).to eq(Puppet::Pops::Types::PHashType) end it 'with symbolic keys translates to PHashType[PRuntimeType[ruby, Symbol], value]' do k = calculator.infer({:first => 1, :second => 2}).key_type expect(k.class).to eq(Puppet::Pops::Types::PRuntimeType) expect(k.runtime).to eq(:ruby) expect(k.runtime_type_name).to eq('Symbol') end it 'with string keys translates to PHashType[PStringType, value]' do expect(calculator.infer({'first' => 1, 'second' => 2}).key_type.class).to eq(Puppet::Pops::Types::PStringType) end it 'with fixnum values translates to PHashType[key, PIntegerType]' do expect(calculator.infer({:first => 1, :second => 2}).element_type.class).to eq(Puppet::Pops::Types::PIntegerType) end - end + context 'using infer_set' do + it "with 'first' and 'second' keys translates to PStructType[{first=>value,second=>value}]" do + t = calculator.infer_set({'first' => 1, 'second' => 2}) + expect(t.class).to eq(Puppet::Pops::Types::PStructType) + expect(t.elements.size).to eq(2) + expect(t.elements.map { |e| e.name }.sort).to eq(['first', 'second']) + end + + it 'with string keys and string and array values translates to PStructType[{key1=>PStringType,key2=>PTupleType}]' do + t = calculator.infer_set({ 'mode' => 'read', 'path' => ['foo', 'fee' ] }) + expect(t.class).to eq(Puppet::Pops::Types::PStructType) + expect(t.elements.size).to eq(2) + els = t.elements.map { |e| e.type }.sort {|a,b| a.to_s <=> b.to_s } + els[0].class.should == Puppet::Pops::Types::PStringType + els[1].class.should == Puppet::Pops::Types::PTupleType + end + + it 'with mixed string and non-string keys translates to PHashType' do + t = calculator.infer_set({ 1 => 'first', 'second' => 'second' }) + expect(t.class).to eq(Puppet::Pops::Types::PHashType) + end + + it 'with empty string keys translates to PHashType' do + t = calculator.infer_set({ '' => 'first', 'second' => 'second' }) + expect(t.class).to eq(Puppet::Pops::Types::PHashType) + end + end + end end context 'patterns' do it "constructs a PPatternType" do t = pattern_t('a(b)c') expect(t.class).to eq(Puppet::Pops::Types::PPatternType) expect(t.patterns.size).to eq(1) expect(t.patterns[0].class).to eq(Puppet::Pops::Types::PRegexpType) expect(t.patterns[0].pattern).to eq('a(b)c') expect(t.patterns[0].regexp.match('abc')[1]).to eq('b') end it "constructs a PStringType with multiple strings" do t = string_t('a', 'b', 'c', 'abc') expect(t.values).to eq(['a', 'b', 'c', 'abc']) end end # Deal with cases not covered by computing common type context 'when computing common type' do it 'computes given resource type commonality' do r1 = Puppet::Pops::Types::PResourceType.new() r1.type_name = 'File' r2 = Puppet::Pops::Types::PResourceType.new() r2.type_name = 'File' expect(calculator.string(calculator.common_type(r1, r2))).to eq("File") r2 = Puppet::Pops::Types::PResourceType.new() r2.type_name = 'File' r2.title = '/tmp/foo' expect(calculator.string(calculator.common_type(r1, r2))).to eq("File") r1 = Puppet::Pops::Types::PResourceType.new() r1.type_name = 'File' r1.title = '/tmp/foo' expect(calculator.string(calculator.common_type(r1, r2))).to eq("File['/tmp/foo']") r1 = Puppet::Pops::Types::PResourceType.new() r1.type_name = 'File' r1.title = '/tmp/bar' expect(calculator.string(calculator.common_type(r1, r2))).to eq("File") r2 = Puppet::Pops::Types::PResourceType.new() r2.type_name = 'Package' r2.title = 'apache' expect(calculator.string(calculator.common_type(r1, r2))).to eq("Resource") end it 'computes given hostclass type commonality' do r1 = Puppet::Pops::Types::PHostClassType.new() r1.class_name = 'foo' r2 = Puppet::Pops::Types::PHostClassType.new() r2.class_name = 'foo' expect(calculator.string(calculator.common_type(r1, r2))).to eq("Class[foo]") r2 = Puppet::Pops::Types::PHostClassType.new() r2.class_name = 'bar' expect(calculator.string(calculator.common_type(r1, r2))).to eq("Class") r2 = Puppet::Pops::Types::PHostClassType.new() expect(calculator.string(calculator.common_type(r1, r2))).to eq("Class") r1 = Puppet::Pops::Types::PHostClassType.new() expect(calculator.string(calculator.common_type(r1, r2))).to eq("Class") end it 'computes pattern commonality' do t1 = pattern_t('abc') t2 = pattern_t('xyz') common_t = calculator.common_type(t1,t2) expect(common_t.class).to eq(Puppet::Pops::Types::PPatternType) expect(common_t.patterns.map { |pr| pr.pattern }).to eq(['abc', 'xyz']) expect(calculator.string(common_t)).to eq("Pattern[/abc/, /xyz/]") end it 'computes enum commonality to value set sum' do t1 = enum_t('a', 'b', 'c') t2 = enum_t('x', 'y', 'z') common_t = calculator.common_type(t1, t2) expect(common_t).to eq(enum_t('a', 'b', 'c', 'x', 'y', 'z')) end it 'computed variant commonality to type union where added types are not sub-types' do a_t1 = integer_t() a_t2 = enum_t('b') v_a = variant_t(a_t1, a_t2) b_t1 = enum_t('a') v_b = variant_t(b_t1) common_t = calculator.common_type(v_a, v_b) expect(common_t.class).to eq(Puppet::Pops::Types::PVariantType) expect(Set.new(common_t.types)).to eq(Set.new([a_t1, a_t2, b_t1])) end it 'computed variant commonality to type union where added types are sub-types' do a_t1 = integer_t() a_t2 = string_t() v_a = variant_t(a_t1, a_t2) b_t1 = enum_t('a') v_b = variant_t(b_t1) common_t = calculator.common_type(v_a, v_b) expect(common_t.class).to eq(Puppet::Pops::Types::PVariantType) expect(Set.new(common_t.types)).to eq(Set.new([a_t1, a_t2])) end context "of callables" do it 'incompatible instances => generic callable' do t1 = callable_t(String) t2 = callable_t(Integer) common_t = calculator.common_type(t1, t2) expect(common_t.class).to be(Puppet::Pops::Types::PCallableType) expect(common_t.param_types).to be_nil expect(common_t.block_type).to be_nil end it 'compatible instances => the most specific' do t1 = callable_t(String) scalar_t = Puppet::Pops::Types::PScalarType.new t2 = callable_t(scalar_t) common_t = calculator.common_type(t1, t2) expect(common_t.class).to be(Puppet::Pops::Types::PCallableType) expect(common_t.param_types.class).to be(Puppet::Pops::Types::PTupleType) expect(common_t.param_types.types).to eql([string_t]) expect(common_t.block_type).to be_nil end it 'block_type is included in the check (incompatible block)' do t1 = with_block_t(callable_t(String), String) t2 = with_block_t(callable_t(String), Integer) common_t = calculator.common_type(t1, t2) expect(common_t.class).to be(Puppet::Pops::Types::PCallableType) expect(common_t.param_types).to be_nil expect(common_t.block_type).to be_nil end it 'block_type is included in the check (compatible block)' do t1 = with_block_t(callable_t(String), String) scalar_t = Puppet::Pops::Types::PScalarType.new t2 = with_block_t(callable_t(String), scalar_t) common_t = calculator.common_type(t1, t2) expect(common_t.param_types.class).to be(Puppet::Pops::Types::PTupleType) expect(common_t.block_type).to eql(callable_t(scalar_t)) end end end context 'computes assignability' do include_context "types_setup" context 'for Unit, such that' do it 'all types are assignable to Unit' do t = Puppet::Pops::Types::PUnitType.new() all_types.each { |t2| expect(t2.new).to be_assignable_to(t) } end it 'Unit is assignable to all other types' do t = Puppet::Pops::Types::PUnitType.new() all_types.each { |t2| expect(t).to be_assignable_to(t2.new) } end it 'Unit is assignable to Unit' do t = Puppet::Pops::Types::PUnitType.new() t2 = Puppet::Pops::Types::PUnitType.new() expect(t).to be_assignable_to(t2) end end context "for Any, such that" do it 'all types are assignable to Any' do t = Puppet::Pops::Types::PAnyType.new() all_types.each { |t2| expect(t2.new).to be_assignable_to(t) } end it 'Any is not assignable to anything but Any' do tested_types = all_types() - [Puppet::Pops::Types::PAnyType] t = Puppet::Pops::Types::PAnyType.new() tested_types.each { |t2| expect(t).not_to be_assignable_to(t2.new) } end end context "for Data, such that" do it 'all scalars + array and hash are assignable to Data' do t = Puppet::Pops::Types::PDataType.new() data_compatible_types.each { |t2| expect(type_from_class(t2)).to be_assignable_to(t) } end it 'a Variant of scalar, hash, or array is assignable to Data' do t = Puppet::Pops::Types::PDataType.new() data_compatible_types.each { |t2| expect(variant_t(type_from_class(t2))).to be_assignable_to(t) } end it 'Data is not assignable to any of its subtypes' do t = Puppet::Pops::Types::PDataType.new() types_to_test = data_compatible_types- [Puppet::Pops::Types::PDataType] types_to_test.each {|t2| expect(t).not_to be_assignable_to(type_from_class(t2)) } end it 'Data is not assignable to a Variant of Data subtype' do t = Puppet::Pops::Types::PDataType.new() types_to_test = data_compatible_types- [Puppet::Pops::Types::PDataType] types_to_test.each { |t2| expect(t).not_to be_assignable_to(variant_t(type_from_class(t2))) } end it 'Data is not assignable to any disjunct type' do tested_types = all_types - [Puppet::Pops::Types::PAnyType, Puppet::Pops::Types::PDataType] - scalar_types t = Puppet::Pops::Types::PDataType.new() tested_types.each {|t2| expect(t).not_to be_assignable_to(t2.new) } end end context 'for Variant, such that' do it 'it is assignable to a type if all contained types are assignable to that type' do v = variant_t(range_t(10, 12),range_t(14, 20)) expect(v).to be_assignable_to(integer_t) expect(v).to be_assignable_to(range_t(10, 20)) # test that both types are assignable to one of the variants OK expect(v).to be_assignable_to(variant_t(range_t(10, 20), range_t(30, 40))) # test where each type is assignable to different types in a variant is OK expect(v).to be_assignable_to(variant_t(range_t(10, 13), range_t(14, 40))) # not acceptable expect(v).not_to be_assignable_to(range_t(0, 4)) expect(v).not_to be_assignable_to(string_t) end end context "for Scalar, such that" do it "all scalars are assignable to Scalar" do t = Puppet::Pops::Types::PScalarType.new() scalar_types.each {|t2| expect(t2.new).to be_assignable_to(t) } end it 'Scalar is not assignable to any of its subtypes' do t = Puppet::Pops::Types::PScalarType.new() types_to_test = scalar_types - [Puppet::Pops::Types::PScalarType] types_to_test.each {|t2| expect(t).not_to be_assignable_to(t2.new) } end it 'Scalar is not assignable to any disjunct type' do tested_types = all_types - [Puppet::Pops::Types::PAnyType, Puppet::Pops::Types::PDataType] - scalar_types t = Puppet::Pops::Types::PScalarType.new() tested_types.each {|t2| expect(t).not_to be_assignable_to(t2.new) } end end context "for Numeric, such that" do it "all numerics are assignable to Numeric" do t = Puppet::Pops::Types::PNumericType.new() numeric_types.each {|t2| expect(t2.new).to be_assignable_to(t) } end it 'Numeric is not assignable to any of its subtypes' do t = Puppet::Pops::Types::PNumericType.new() types_to_test = numeric_types - [Puppet::Pops::Types::PNumericType] types_to_test.each {|t2| expect(t).not_to be_assignable_to(t2.new) } end it 'Numeric is not assignable to any disjunct type' do tested_types = all_types - [ Puppet::Pops::Types::PAnyType, Puppet::Pops::Types::PDataType, Puppet::Pops::Types::PScalarType, ] - numeric_types t = Puppet::Pops::Types::PNumericType.new() tested_types.each {|t2| expect(t).not_to be_assignable_to(t2.new) } end end context "for Collection, such that" do it "all collections are assignable to Collection" do t = Puppet::Pops::Types::PCollectionType.new() collection_types.each {|t2| expect(t2.new).to be_assignable_to(t) } end it 'Collection is not assignable to any of its subtypes' do t = Puppet::Pops::Types::PCollectionType.new() types_to_test = collection_types - [Puppet::Pops::Types::PCollectionType] types_to_test.each {|t2| expect(t).not_to be_assignable_to(t2.new) } end it 'Collection is not assignable to any disjunct type' do tested_types = all_types - [Puppet::Pops::Types::PAnyType] - collection_types t = Puppet::Pops::Types::PCollectionType.new() tested_types.each {|t2| expect(t).not_to be_assignable_to(t2.new) } end end context "for Array, such that" do it "Array is not assignable to non Array based Collection type" do t = Puppet::Pops::Types::PArrayType.new() tested_types = collection_types - [ Puppet::Pops::Types::PCollectionType, Puppet::Pops::Types::PArrayType, Puppet::Pops::Types::PTupleType] tested_types.each {|t2| expect(t).not_to be_assignable_to(t2.new) } end it 'Array is not assignable to any disjunct type' do tested_types = all_types - [ Puppet::Pops::Types::PAnyType, Puppet::Pops::Types::PDataType] - collection_types t = Puppet::Pops::Types::PArrayType.new() tested_types.each {|t2| expect(t).not_to be_assignable_to(t2.new) } end end context "for Hash, such that" do it "Hash is not assignable to any other Collection type" do t = Puppet::Pops::Types::PHashType.new() tested_types = collection_types - [ Puppet::Pops::Types::PCollectionType, Puppet::Pops::Types::PStructType, Puppet::Pops::Types::PHashType] tested_types.each {|t2| expect(t).not_to be_assignable_to(t2.new) } end it 'Hash is not assignable to any disjunct type' do tested_types = all_types - [ Puppet::Pops::Types::PAnyType, Puppet::Pops::Types::PDataType] - collection_types t = Puppet::Pops::Types::PHashType.new() tested_types.each {|t2| expect(t).not_to be_assignable_to(t2.new) } end + + it 'Struct is assignable to Hash with Pattern that matches all keys' do + struct_t({'x' => integer_t, 'y' => integer_t}).should be_assignable_to(hash_t(pattern_t(/^\w+$/), factory.any)) + end + + it 'Struct is assignable to Hash with Enum that matches all keys' do + struct_t({'x' => integer_t, 'y' => integer_t}).should be_assignable_to(hash_t(enum_t('x', 'y', 'z'), factory.any)) + end + + it 'Struct is not assignable to Hash with Pattern unless all keys match' do + struct_t({'a' => integer_t, 'A' => integer_t}).should_not be_assignable_to(hash_t(pattern_t(/^[A-Z]+$/), factory.any)) + end + + it 'Struct is not assignable to Hash with Enum unless all keys match' do + struct_t({'a' => integer_t, 'y' => integer_t}).should_not be_assignable_to(hash_t(enum_t('x', 'y', 'z'), factory.any)) + end end context "for Tuple, such that" do it "Tuple is not assignable to any other non Array based Collection type" do t = Puppet::Pops::Types::PTupleType.new() tested_types = collection_types - [ Puppet::Pops::Types::PCollectionType, Puppet::Pops::Types::PTupleType, Puppet::Pops::Types::PArrayType] tested_types.each {|t2| expect(t).not_to be_assignable_to(t2.new) } end it 'Tuple is not assignable to any disjunct type' do tested_types = all_types - [ Puppet::Pops::Types::PAnyType, Puppet::Pops::Types::PDataType] - collection_types t = Puppet::Pops::Types::PTupleType.new() tested_types.each {|t2| expect(t).not_to be_assignable_to(t2.new) } end end context "for Struct, such that" do it "Struct is not assignable to any other non Hashed based Collection type" do t = Puppet::Pops::Types::PStructType.new() tested_types = collection_types - [ Puppet::Pops::Types::PCollectionType, Puppet::Pops::Types::PStructType, Puppet::Pops::Types::PHashType] tested_types.each {|t2| expect(t).not_to be_assignable_to(t2.new) } end it 'Struct is not assignable to any disjunct type' do tested_types = all_types - [ Puppet::Pops::Types::PAnyType, Puppet::Pops::Types::PDataType] - collection_types t = Puppet::Pops::Types::PStructType.new() tested_types.each {|t2| expect(t).not_to be_assignable_to(t2.new) } end end context "for Callable, such that" do it "Callable is not assignable to any disjunct type" do t = Puppet::Pops::Types::PCallableType.new() tested_types = all_types - [ Puppet::Pops::Types::PCallableType, Puppet::Pops::Types::PAnyType] tested_types.each {|t2| expect(t).not_to be_assignable_to(t2.new) } end end it 'should recognize mapped ruby types' do { Integer => Puppet::Pops::Types::PIntegerType.new, Fixnum => Puppet::Pops::Types::PIntegerType.new, Bignum => Puppet::Pops::Types::PIntegerType.new, Float => Puppet::Pops::Types::PFloatType.new, Numeric => Puppet::Pops::Types::PNumericType.new, NilClass => Puppet::Pops::Types::PNilType.new, TrueClass => Puppet::Pops::Types::PBooleanType.new, FalseClass => Puppet::Pops::Types::PBooleanType.new, String => Puppet::Pops::Types::PStringType.new, Regexp => Puppet::Pops::Types::PRegexpType.new, Regexp => Puppet::Pops::Types::PRegexpType.new, Array => Puppet::Pops::Types::TypeFactory.array_of_data(), Hash => Puppet::Pops::Types::TypeFactory.hash_of_data() }.each do |ruby_type, puppet_type | expect(ruby_type).to be_assignable_to(puppet_type) end end context 'when dealing with integer ranges' do it 'should accept an equal range' do expect(calculator.assignable?(range_t(2,5), range_t(2,5))).to eq(true) end it 'should accept an equal reverse range' do expect(calculator.assignable?(range_t(2,5), range_t(5,2))).to eq(true) end it 'should accept a narrower range' do expect(calculator.assignable?(range_t(2,10), range_t(3,5))).to eq(true) end it 'should accept a narrower reverse range' do expect(calculator.assignable?(range_t(2,10), range_t(5,3))).to eq(true) end it 'should reject a wider range' do expect(calculator.assignable?(range_t(3,5), range_t(2,10))).to eq(false) end it 'should reject a wider reverse range' do expect(calculator.assignable?(range_t(3,5), range_t(10,2))).to eq(false) end it 'should reject a partially overlapping range' do expect(calculator.assignable?(range_t(3,5), range_t(2,4))).to eq(false) expect(calculator.assignable?(range_t(3,5), range_t(4,6))).to eq(false) end it 'should reject a partially overlapping reverse range' do expect(calculator.assignable?(range_t(3,5), range_t(4,2))).to eq(false) expect(calculator.assignable?(range_t(3,5), range_t(6,4))).to eq(false) end end context 'when dealing with patterns' do it 'should accept a string matching a pattern' do p_t = pattern_t('abc') p_s = string_t('XabcY') expect(calculator.assignable?(p_t, p_s)).to eq(true) end it 'should accept a regexp matching a pattern' do p_t = pattern_t(/abc/) p_s = string_t('XabcY') expect(calculator.assignable?(p_t, p_s)).to eq(true) end it 'should accept a pattern matching a pattern' do p_t = pattern_t(pattern_t('abc')) p_s = string_t('XabcY') expect(calculator.assignable?(p_t, p_s)).to eq(true) end it 'should accept a regexp matching a pattern' do p_t = pattern_t(regexp_t('abc')) p_s = string_t('XabcY') expect(calculator.assignable?(p_t, p_s)).to eq(true) end it 'should accept a string matching all patterns' do p_t = pattern_t('abc', 'ab', 'c') p_s = string_t('XabcY') expect(calculator.assignable?(p_t, p_s)).to eq(true) end it 'should accept multiple strings if they all match any patterns' do p_t = pattern_t('X', 'Y', 'abc') p_s = string_t('Xa', 'aY', 'abc') expect(calculator.assignable?(p_t, p_s)).to eq(true) end it 'should reject a string not matching any patterns' do p_t = pattern_t('abc', 'ab', 'c') p_s = string_t('XqqqY') expect(calculator.assignable?(p_t, p_s)).to eq(false) end it 'should reject multiple strings if not all match any patterns' do p_t = pattern_t('abc', 'ab', 'c', 'q') p_s = string_t('X', 'Y', 'Z') expect(calculator.assignable?(p_t, p_s)).to eq(false) end it 'should accept enum matching patterns as instanceof' do enum = enum_t('XS', 'S', 'M', 'L' 'XL', 'XXL') pattern = pattern_t('S', 'M', 'L') expect(calculator.assignable?(pattern, enum)).to eq(true) end it 'pattern should accept a variant where all variants are acceptable' do pattern = pattern_t(/^\w+$/) expect(calculator.assignable?(pattern, variant_t(string_t('a'), string_t('b')))).to eq(true) end it 'pattern representing all patterns should accept any pattern' do expect(calculator.assignable?(pattern_t(), pattern_t('a'))).to eq(true) expect(calculator.assignable?(pattern_t(), pattern_t())).to eq(true) end it 'pattern representing all patterns should accept any enum' do expect(calculator.assignable?(pattern_t(), enum_t('a'))).to eq(true) expect(calculator.assignable?(pattern_t(), enum_t())).to eq(true) end it 'pattern representing all patterns should accept any string' do expect(calculator.assignable?(pattern_t(), string_t('a'))).to eq(true) expect(calculator.assignable?(pattern_t(), string_t())).to eq(true) end end context 'when dealing with enums' do it 'should accept a string with matching content' do expect(calculator.assignable?(enum_t('a', 'b'), string_t('a'))).to eq(true) expect(calculator.assignable?(enum_t('a', 'b'), string_t('b'))).to eq(true) expect(calculator.assignable?(enum_t('a', 'b'), string_t('c'))).to eq(false) end it 'should accept an enum with matching enum' do expect(calculator.assignable?(enum_t('a', 'b'), enum_t('a', 'b'))).to eq(true) expect(calculator.assignable?(enum_t('a', 'b'), enum_t('a'))).to eq(true) expect(calculator.assignable?(enum_t('a', 'b'), enum_t('c'))).to eq(false) end it 'non parameterized enum accepts any other enum but not the reverse' do expect(calculator.assignable?(enum_t(), enum_t('a'))).to eq(true) expect(calculator.assignable?(enum_t('a'), enum_t())).to eq(false) end it 'enum should accept a variant where all variants are acceptable' do enum = enum_t('a', 'b') expect(calculator.assignable?(enum, variant_t(string_t('a'), string_t('b')))).to eq(true) end end context 'when dealing with string and enum combinations' do it 'should accept assigning any enum to unrestricted string' do expect(calculator.assignable?(string_t(), enum_t('blue'))).to eq(true) expect(calculator.assignable?(string_t(), enum_t('blue', 'red'))).to eq(true) end it 'should not accept assigning longer enum value to size restricted string' do expect(calculator.assignable?(constrained_t(string_t(),2,2), enum_t('a','blue'))).to eq(false) end it 'should accept assigning any string to empty enum' do expect(calculator.assignable?(enum_t(), string_t())).to eq(true) end it 'should accept assigning empty enum to any string' do expect(calculator.assignable?(string_t(), enum_t())).to eq(true) end it 'should not accept assigning empty enum to size constrained string' do expect(calculator.assignable?(constrained_t(string_t(),2,2), enum_t())).to eq(false) end end context 'when dealing with string/pattern/enum combinations' do it 'any string is equal to any enum is equal to any pattern' do expect(calculator.assignable?(string_t(), enum_t())).to eq(true) expect(calculator.assignable?(string_t(), pattern_t())).to eq(true) expect(calculator.assignable?(enum_t(), string_t())).to eq(true) expect(calculator.assignable?(enum_t(), pattern_t())).to eq(true) expect(calculator.assignable?(pattern_t(), string_t())).to eq(true) expect(calculator.assignable?(pattern_t(), enum_t())).to eq(true) end end context 'when dealing with tuples' do it 'matches empty tuples' do tuple1 = tuple_t() tuple2 = tuple_t() expect(calculator.assignable?(tuple1, tuple2)).to eq(true) expect(calculator.assignable?(tuple2, tuple1)).to eq(true) end it 'accepts an empty tuple as assignable to a tuple with a min size of 0' do tuple1 = tuple_t(Object) factory.constrain_size(tuple1, 0, :default) tuple2 = tuple_t() expect(calculator.assignable?(tuple1, tuple2)).to eq(true) expect(calculator.assignable?(tuple2, tuple1)).to eq(false) end it 'should accept matching tuples' do tuple1 = tuple_t(1,2) tuple2 = tuple_t(Integer,Integer) expect(calculator.assignable?(tuple1, tuple2)).to eq(true) expect(calculator.assignable?(tuple2, tuple1)).to eq(true) end it 'should accept matching tuples where one is more general than the other' do tuple1 = tuple_t(1,2) tuple2 = tuple_t(Numeric,Numeric) expect(calculator.assignable?(tuple1, tuple2)).to eq(false) expect(calculator.assignable?(tuple2, tuple1)).to eq(true) end it 'should accept ranged tuples' do tuple1 = tuple_t(1) factory.constrain_size(tuple1, 5, 5) tuple2 = tuple_t(Integer,Integer, Integer, Integer, Integer) expect(calculator.assignable?(tuple1, tuple2)).to eq(true) expect(calculator.assignable?(tuple2, tuple1)).to eq(true) end it 'should reject ranged tuples when ranges does not match' do tuple1 = tuple_t(1) factory.constrain_size(tuple1, 4, 5) tuple2 = tuple_t(Integer,Integer, Integer, Integer, Integer) expect(calculator.assignable?(tuple1, tuple2)).to eq(true) expect(calculator.assignable?(tuple2, tuple1)).to eq(false) end it 'should reject ranged tuples when ranges does not match (using infinite upper bound)' do tuple1 = tuple_t(1) factory.constrain_size(tuple1, 4, :default) tuple2 = tuple_t(Integer,Integer, Integer, Integer, Integer) expect(calculator.assignable?(tuple1, tuple2)).to eq(true) expect(calculator.assignable?(tuple2, tuple1)).to eq(false) end it 'should accept matching tuples with optional entries by repeating last' do tuple1 = tuple_t(1,2) factory.constrain_size(tuple1, 0, :default) tuple2 = tuple_t(Numeric,Numeric) factory.constrain_size(tuple2, 0, :default) expect(calculator.assignable?(tuple1, tuple2)).to eq(false) expect(calculator.assignable?(tuple2, tuple1)).to eq(true) end it 'should accept matching tuples with optional entries' do tuple1 = tuple_t(Integer, Integer, String) factory.constrain_size(tuple1, 1, 3) array2 = factory.constrain_size(array_t(Integer),2,2) expect(calculator.assignable?(tuple1, array2)).to eq(true) factory.constrain_size(tuple1, 3, 3) expect(calculator.assignable?(tuple1, array2)).to eq(false) end it 'should accept matching array' do tuple1 = tuple_t(1,2) array = array_t(Integer) factory.constrain_size(array, 2, 2) expect(calculator.assignable?(tuple1, array)).to eq(true) expect(calculator.assignable?(array, tuple1)).to eq(true) end it 'should accept empty array when tuple allows min of 0' do tuple1 = tuple_t(Integer) factory.constrain_size(tuple1, 0, 1) array = array_t(Integer) factory.constrain_size(array, 0, 0) expect(calculator.assignable?(tuple1, array)).to eq(true) expect(calculator.assignable?(array, tuple1)).to eq(false) end end context 'when dealing with structs' do it 'should accept matching structs' do struct1 = struct_t({'a'=>Integer, 'b'=>Integer}) struct2 = struct_t({'a'=>Integer, 'b'=>Integer}) expect(calculator.assignable?(struct1, struct2)).to eq(true) expect(calculator.assignable?(struct2, struct1)).to eq(true) end it 'should accept matching structs where one is more general than the other' do struct1 = struct_t({'a'=>Integer, 'b'=>Integer}) struct2 = struct_t({'a'=>Numeric, 'b'=>Numeric}) expect(calculator.assignable?(struct1, struct2)).to eq(false) expect(calculator.assignable?(struct2, struct1)).to eq(true) end it 'should accept matching hash' do struct1 = struct_t({'a'=>Integer, 'b'=>Integer}) non_empty_string = string_t() non_empty_string.size_type = range_t(1, nil) hsh = hash_t(non_empty_string, Integer) factory.constrain_size(hsh, 2, 2) expect(calculator.assignable?(struct1, hsh)).to eq(true) expect(calculator.assignable?(hsh, struct1)).to eq(true) end end it 'should recognize ruby type inheritance' do class Foo end class Bar < Foo end fooType = calculator.infer(Foo.new) barType = calculator.infer(Bar.new) expect(calculator.assignable?(fooType, fooType)).to eq(true) expect(calculator.assignable?(Foo, fooType)).to eq(true) expect(calculator.assignable?(fooType, barType)).to eq(true) expect(calculator.assignable?(Foo, barType)).to eq(true) expect(calculator.assignable?(barType, fooType)).to eq(false) expect(calculator.assignable?(Bar, fooType)).to eq(false) end it "should allow host class with same name" do hc1 = Puppet::Pops::Types::TypeFactory.host_class('the_name') hc2 = Puppet::Pops::Types::TypeFactory.host_class('the_name') expect(calculator.assignable?(hc1, hc2)).to eq(true) end it "should allow host class with name assigned to hostclass without name" do hc1 = Puppet::Pops::Types::TypeFactory.host_class() hc2 = Puppet::Pops::Types::TypeFactory.host_class('the_name') expect(calculator.assignable?(hc1, hc2)).to eq(true) end it "should reject host classes with different names" do hc1 = Puppet::Pops::Types::TypeFactory.host_class('the_name') hc2 = Puppet::Pops::Types::TypeFactory.host_class('another_name') expect(calculator.assignable?(hc1, hc2)).to eq(false) end it "should reject host classes without name assigned to host class with name" do hc1 = Puppet::Pops::Types::TypeFactory.host_class('the_name') hc2 = Puppet::Pops::Types::TypeFactory.host_class() expect(calculator.assignable?(hc1, hc2)).to eq(false) end it "should allow resource with same type_name and title" do r1 = Puppet::Pops::Types::TypeFactory.resource('file', 'foo') r2 = Puppet::Pops::Types::TypeFactory.resource('file', 'foo') expect(calculator.assignable?(r1, r2)).to eq(true) end it "should allow more specific resource assignment" do r1 = Puppet::Pops::Types::TypeFactory.resource() r2 = Puppet::Pops::Types::TypeFactory.resource('file') expect(calculator.assignable?(r1, r2)).to eq(true) r2 = Puppet::Pops::Types::TypeFactory.resource('file', '/tmp/foo') expect(calculator.assignable?(r1, r2)).to eq(true) r1 = Puppet::Pops::Types::TypeFactory.resource('file') expect(calculator.assignable?(r1, r2)).to eq(true) end it "should reject less specific resource assignment" do r1 = Puppet::Pops::Types::TypeFactory.resource('file', '/tmp/foo') r2 = Puppet::Pops::Types::TypeFactory.resource('file') expect(calculator.assignable?(r1, r2)).to eq(false) r2 = Puppet::Pops::Types::TypeFactory.resource() expect(calculator.assignable?(r1, r2)).to eq(false) end end context 'when testing if x is instance of type t' do include_context "types_setup" it 'should consider undef to be instance of Any, NilType, and optional' do expect(calculator.instance?(Puppet::Pops::Types::PNilType.new(), nil)).to eq(true) expect(calculator.instance?(Puppet::Pops::Types::PAnyType.new(), nil)).to eq(true) expect(calculator.instance?(Puppet::Pops::Types::POptionalType.new(), nil)).to eq(true) end it 'all types should be (ruby) instance of PAnyType' do all_types.each do |t| expect(t.new.is_a?(Puppet::Pops::Types::PAnyType)).to eq(true) end end it "should consider :undef to be instance of Runtime['ruby', 'Symbol]" do expect(calculator.instance?(Puppet::Pops::Types::PRuntimeType.new(:runtime => :ruby, :runtime_type_name => 'Symbol'), :undef)).to eq(true) end it "should consider :undef to be instance of an Optional type" do expect(calculator.instance?(Puppet::Pops::Types::POptionalType.new(), :undef)).to eq(true) end it 'should not consider undef to be an instance of any other type than Any, NilType and Data' do types_to_test = all_types - [ Puppet::Pops::Types::PAnyType, Puppet::Pops::Types::PNilType, Puppet::Pops::Types::PDataType, Puppet::Pops::Types::POptionalType, ] types_to_test.each {|t| expect(calculator.instance?(t.new, nil)).to eq(false) } types_to_test.each {|t| expect(calculator.instance?(t.new, :undef)).to eq(false) } end it 'should consider default to be instance of Default and Any' do expect(calculator.instance?(Puppet::Pops::Types::PDefaultType.new(), :default)).to eq(true) expect(calculator.instance?(Puppet::Pops::Types::PAnyType.new(), :default)).to eq(true) end it 'should not consider "default" to be an instance of anything but Default, and Any' do types_to_test = all_types - [ Puppet::Pops::Types::PAnyType, Puppet::Pops::Types::PDefaultType, ] types_to_test.each {|t| expect(calculator.instance?(t.new, :default)).to eq(false) } end it 'should consider fixnum instanceof PIntegerType' do expect(calculator.instance?(Puppet::Pops::Types::PIntegerType.new(), 1)).to eq(true) end it 'should consider fixnum instanceof Fixnum' do expect(calculator.instance?(Fixnum, 1)).to eq(true) end it 'should consider integer in range' do range = range_t(0,10) expect(calculator.instance?(range, 1)).to eq(true) expect(calculator.instance?(range, 10)).to eq(true) expect(calculator.instance?(range, -1)).to eq(false) expect(calculator.instance?(range, 11)).to eq(false) end it 'should consider string in length range' do range = factory.constrain_size(string_t, 1,3) expect(calculator.instance?(range, 'a')).to eq(true) expect(calculator.instance?(range, 'abc')).to eq(true) expect(calculator.instance?(range, '')).to eq(false) expect(calculator.instance?(range, 'abcd')).to eq(false) end it 'should consider array in length range' do range = factory.constrain_size(array_t(integer_t), 1,3) expect(calculator.instance?(range, [1])).to eq(true) expect(calculator.instance?(range, [1,2,3])).to eq(true) expect(calculator.instance?(range, [])).to eq(false) expect(calculator.instance?(range, [1,2,3,4])).to eq(false) end it 'should consider hash in length range' do range = factory.constrain_size(hash_t(integer_t, integer_t), 1,2) expect(calculator.instance?(range, {1=>1})).to eq(true) expect(calculator.instance?(range, {1=>1, 2=>2})).to eq(true) expect(calculator.instance?(range, {})).to eq(false) expect(calculator.instance?(range, {1=>1, 2=>2, 3=>3})).to eq(false) end it 'should consider collection in length range for array ' do range = factory.constrain_size(collection_t, 1,3) expect(calculator.instance?(range, [1])).to eq(true) expect(calculator.instance?(range, [1,2,3])).to eq(true) expect(calculator.instance?(range, [])).to eq(false) expect(calculator.instance?(range, [1,2,3,4])).to eq(false) end it 'should consider collection in length range for hash' do range = factory.constrain_size(collection_t, 1,2) expect(calculator.instance?(range, {1=>1})).to eq(true) expect(calculator.instance?(range, {1=>1, 2=>2})).to eq(true) expect(calculator.instance?(range, {})).to eq(false) expect(calculator.instance?(range, {1=>1, 2=>2, 3=>3})).to eq(false) end it 'should consider string matching enum as instanceof' do enum = enum_t('XS', 'S', 'M', 'L', 'XL', '0') expect(calculator.instance?(enum, 'XS')).to eq(true) expect(calculator.instance?(enum, 'S')).to eq(true) expect(calculator.instance?(enum, 'XXL')).to eq(false) expect(calculator.instance?(enum, '')).to eq(false) expect(calculator.instance?(enum, '0')).to eq(true) expect(calculator.instance?(enum, 0)).to eq(false) end it 'should consider array[string] as instance of Array[Enum] when strings are instance of Enum' do enum = enum_t('XS', 'S', 'M', 'L', 'XL', '0') array = array_t(enum) expect(calculator.instance?(array, ['XS', 'S', 'XL'])).to eq(true) expect(calculator.instance?(array, ['XS', 'S', 'XXL'])).to eq(false) end it 'should consider array[mixed] as instance of Variant[mixed] when mixed types are listed in Variant' do enum = enum_t('XS', 'S', 'M', 'L', 'XL') sizes = range_t(30, 50) array = array_t(variant_t(enum, sizes)) expect(calculator.instance?(array, ['XS', 'S', 30, 50])).to eq(true) expect(calculator.instance?(array, ['XS', 'S', 'XXL'])).to eq(false) expect(calculator.instance?(array, ['XS', 'S', 29])).to eq(false) end it 'should consider array[seq] as instance of Tuple[seq] when elements of seq are instance of' do tuple = tuple_t(Integer, String, Float) expect(calculator.instance?(tuple, [1, 'a', 3.14])).to eq(true) expect(calculator.instance?(tuple, [1.2, 'a', 3.14])).to eq(false) expect(calculator.instance?(tuple, [1, 1, 3.14])).to eq(false) expect(calculator.instance?(tuple, [1, 'a', 1])).to eq(false) end it 'should consider hash[cont] as instance of Struct[cont-t]' do struct = struct_t({'a'=>Integer, 'b'=>String, 'c'=>Float}) expect(calculator.instance?(struct, {'a'=>1, 'b'=>'a', 'c'=>3.14})).to eq(true) expect(calculator.instance?(struct, {'a'=>1.2, 'b'=>'a', 'c'=>3.14})).to eq(false) expect(calculator.instance?(struct, {'a'=>1, 'b'=>1, 'c'=>3.14})).to eq(false) expect(calculator.instance?(struct, {'a'=>1, 'b'=>'a', 'c'=>1})).to eq(false) end context 'and t is Data' do it 'undef should be considered instance of Data' do expect(calculator.instance?(data_t, nil)).to eq(true) end it 'other symbols should not be considered instance of Data' do expect(calculator.instance?(data_t, :love)).to eq(false) end it 'an empty array should be considered instance of Data' do expect(calculator.instance?(data_t, [])).to eq(true) end it 'an empty hash should be considered instance of Data' do expect(calculator.instance?(data_t, {})).to eq(true) end it 'a hash with nil/undef data should be considered instance of Data' do expect(calculator.instance?(data_t, {'a' => nil})).to eq(true) end it 'a hash with nil/default key should not considered instance of Data' do expect(calculator.instance?(data_t, {nil => 10})).to eq(false) expect(calculator.instance?(data_t, {:default => 10})).to eq(false) end it 'an array with nil entries should be considered instance of Data' do expect(calculator.instance?(data_t, [nil])).to eq(true) end it 'an array with nil + data entries should be considered instance of Data' do expect(calculator.instance?(data_t, [1, nil, 'a'])).to eq(true) end end context "and t is something Callable" do it 'a Closure should be considered a Callable' do factory = Puppet::Pops::Model::Factory params = [factory.PARAM('a')] the_block = factory.LAMBDA(params,factory.literal(42)) the_closure = Puppet::Pops::Evaluator::Closure.new(:fake_evaluator, the_block, :fake_scope) expect(calculator.instance?(all_callables_t, the_closure)).to be_truthy expect(calculator.instance?(callable_t(object_t), the_closure)).to be_truthy expect(calculator.instance?(callable_t(object_t, object_t), the_closure)).to be_falsey end it 'a Function instance should be considered a Callable' do fc = Puppet::Functions.create_function(:foo) do dispatch :foo do param 'String', :a end def foo(a) a end end f = fc.new(:closure_scope, :loader) # Any callable expect(calculator.instance?(all_callables_t, f)).to be_truthy # Callable[String] expect(calculator.instance?(callable_t(String), f)).to be_truthy end end end context 'when converting a ruby class' do it 'should yield \'PIntegerType\' for Integer, Fixnum, and Bignum' do [Integer,Fixnum,Bignum].each do |c| expect(calculator.type(c).class).to eq(Puppet::Pops::Types::PIntegerType) end end it 'should yield \'PFloatType\' for Float' do expect(calculator.type(Float).class).to eq(Puppet::Pops::Types::PFloatType) end it 'should yield \'PBooleanType\' for FalseClass and TrueClass' do [FalseClass,TrueClass].each do |c| expect(calculator.type(c).class).to eq(Puppet::Pops::Types::PBooleanType) end end it 'should yield \'PNilType\' for NilClass' do expect(calculator.type(NilClass).class).to eq(Puppet::Pops::Types::PNilType) end it 'should yield \'PStringType\' for String' do expect(calculator.type(String).class).to eq(Puppet::Pops::Types::PStringType) end it 'should yield \'PRegexpType\' for Regexp' do expect(calculator.type(Regexp).class).to eq(Puppet::Pops::Types::PRegexpType) end it 'should yield \'PArrayType[PDataType]\' for Array' do t = calculator.type(Array) expect(t.class).to eq(Puppet::Pops::Types::PArrayType) expect(t.element_type.class).to eq(Puppet::Pops::Types::PDataType) end it 'should yield \'PHashType[PScalarType,PDataType]\' for Hash' do t = calculator.type(Hash) expect(t.class).to eq(Puppet::Pops::Types::PHashType) expect(t.key_type.class).to eq(Puppet::Pops::Types::PScalarType) expect(t.element_type.class).to eq(Puppet::Pops::Types::PDataType) end end context 'when representing the type as string' do it 'should yield \'Type\' for PType' do expect(calculator.string(Puppet::Pops::Types::PType.new())).to eq('Type') end it 'should yield \'Object\' for PAnyType' do expect(calculator.string(Puppet::Pops::Types::PAnyType.new())).to eq('Any') end it 'should yield \'Scalar\' for PScalarType' do expect(calculator.string(Puppet::Pops::Types::PScalarType.new())).to eq('Scalar') end it 'should yield \'Boolean\' for PBooleanType' do expect(calculator.string(Puppet::Pops::Types::PBooleanType.new())).to eq('Boolean') end it 'should yield \'Data\' for PDataType' do expect(calculator.string(Puppet::Pops::Types::PDataType.new())).to eq('Data') end it 'should yield \'Numeric\' for PNumericType' do expect(calculator.string(Puppet::Pops::Types::PNumericType.new())).to eq('Numeric') end it 'should yield \'Integer\' and from/to for PIntegerType' do int_T = Puppet::Pops::Types::PIntegerType expect(calculator.string(int_T.new())).to eq('Integer') int = int_T.new() int.from = 1 int.to = 1 expect(calculator.string(int)).to eq('Integer[1, 1]') int = int_T.new() int.from = 1 int.to = 2 expect(calculator.string(int)).to eq('Integer[1, 2]') int = int_T.new() int.from = nil int.to = 2 expect(calculator.string(int)).to eq('Integer[default, 2]') int = int_T.new() int.from = 2 int.to = nil expect(calculator.string(int)).to eq('Integer[2, default]') end it 'should yield \'Float\' for PFloatType' do expect(calculator.string(Puppet::Pops::Types::PFloatType.new())).to eq('Float') end it 'should yield \'Regexp\' for PRegexpType' do expect(calculator.string(Puppet::Pops::Types::PRegexpType.new())).to eq('Regexp') end it 'should yield \'Regexp[/pat/]\' for parameterized PRegexpType' do t = Puppet::Pops::Types::PRegexpType.new() t.pattern = ('a/b') expect(calculator.string(Puppet::Pops::Types::PRegexpType.new())).to eq('Regexp') end it 'should yield \'String\' for PStringType' do expect(calculator.string(Puppet::Pops::Types::PStringType.new())).to eq('String') end it 'should yield \'String\' for PStringType with multiple values' do expect(calculator.string(string_t('a', 'b', 'c'))).to eq('String') end it 'should yield \'String\' and from/to for PStringType' do string_T = Puppet::Pops::Types::PStringType expect(calculator.string(factory.constrain_size(string_T.new(), 1,1))).to eq('String[1, 1]') expect(calculator.string(factory.constrain_size(string_T.new(), 1,2))).to eq('String[1, 2]') expect(calculator.string(factory.constrain_size(string_T.new(), :default, 2))).to eq('String[default, 2]') expect(calculator.string(factory.constrain_size(string_T.new(), 2, :default))).to eq('String[2, default]') end it 'should yield \'Array[Integer]\' for PArrayType[PIntegerType]' do t = Puppet::Pops::Types::PArrayType.new() t.element_type = Puppet::Pops::Types::PIntegerType.new() expect(calculator.string(t)).to eq('Array[Integer]') end it 'should yield \'Collection\' and from/to for PCollectionType' do col = collection_t() expect(calculator.string(factory.constrain_size(col.copy, 1,1))).to eq('Collection[1, 1]') expect(calculator.string(factory.constrain_size(col.copy, 1,2))).to eq('Collection[1, 2]') expect(calculator.string(factory.constrain_size(col.copy, :default, 2))).to eq('Collection[default, 2]') expect(calculator.string(factory.constrain_size(col.copy, 2, :default))).to eq('Collection[2, default]') end it 'should yield \'Array\' and from/to for PArrayType' do arr = array_t(string_t) expect(calculator.string(factory.constrain_size(arr.copy, 1,1))).to eq('Array[String, 1, 1]') expect(calculator.string(factory.constrain_size(arr.copy, 1,2))).to eq('Array[String, 1, 2]') expect(calculator.string(factory.constrain_size(arr.copy, :default, 2))).to eq('Array[String, default, 2]') expect(calculator.string(factory.constrain_size(arr.copy, 2, :default))).to eq('Array[String, 2, default]') end it 'should yield \'Tuple[Integer]\' for PTupleType[PIntegerType]' do t = Puppet::Pops::Types::PTupleType.new() t.addTypes(Puppet::Pops::Types::PIntegerType.new()) expect(calculator.string(t)).to eq('Tuple[Integer]') end it 'should yield \'Tuple[T, T,..]\' for PTupleType[T, T, ...]' do t = Puppet::Pops::Types::PTupleType.new() t.addTypes(Puppet::Pops::Types::PIntegerType.new()) t.addTypes(Puppet::Pops::Types::PIntegerType.new()) t.addTypes(Puppet::Pops::Types::PStringType.new()) expect(calculator.string(t)).to eq('Tuple[Integer, Integer, String]') end it 'should yield \'Tuple\' and from/to for PTupleType' do tuple_t = tuple_t(string_t) expect(calculator.string(factory.constrain_size(tuple_t.copy, 1,1))).to eq('Tuple[String, 1, 1]') expect(calculator.string(factory.constrain_size(tuple_t.copy, 1,2))).to eq('Tuple[String, 1, 2]') expect(calculator.string(factory.constrain_size(tuple_t.copy, :default, 2))).to eq('Tuple[String, default, 2]') expect(calculator.string(factory.constrain_size(tuple_t.copy, 2, :default))).to eq('Tuple[String, 2, default]') end it 'should yield \'Struct\' and details for PStructType' do struct_t = struct_t({'a'=>Integer, 'b'=>String}) expect(calculator.string(struct_t)).to eq("Struct[{'a'=>Integer, 'b'=>String}]") struct_t = struct_t({}) expect(calculator.string(struct_t)).to eq("Struct") end it 'should yield \'Hash[String, Integer]\' for PHashType[PStringType, PIntegerType]' do t = Puppet::Pops::Types::PHashType.new() t.key_type = Puppet::Pops::Types::PStringType.new() t.element_type = Puppet::Pops::Types::PIntegerType.new() expect(calculator.string(t)).to eq('Hash[String, Integer]') end it 'should yield \'Hash\' and from/to for PHashType' do hsh = hash_t(string_t, string_t) expect(calculator.string(factory.constrain_size(hsh.copy, 1,1))).to eq('Hash[String, String, 1, 1]') expect(calculator.string(factory.constrain_size(hsh.copy, 1,2))).to eq('Hash[String, String, 1, 2]') expect(calculator.string(factory.constrain_size(hsh.copy, :default, 2))).to eq('Hash[String, String, default, 2]') expect(calculator.string(factory.constrain_size(hsh.copy, 2, :default))).to eq('Hash[String, String, 2, default]') end it "should yield 'Class' for a PHostClassType" do t = Puppet::Pops::Types::PHostClassType.new() expect(calculator.string(t)).to eq('Class') end it "should yield 'Class[x]' for a PHostClassType[x]" do t = Puppet::Pops::Types::PHostClassType.new() t.class_name = 'x' expect(calculator.string(t)).to eq('Class[x]') end it "should yield 'Resource' for a PResourceType" do t = Puppet::Pops::Types::PResourceType.new() expect(calculator.string(t)).to eq('Resource') end it 'should yield \'File\' for a PResourceType[\'File\']' do t = Puppet::Pops::Types::PResourceType.new() t.type_name = 'File' expect(calculator.string(t)).to eq('File') end it "should yield 'File['/tmp/foo']' for a PResourceType['File', '/tmp/foo']" do t = Puppet::Pops::Types::PResourceType.new() t.type_name = 'File' t.title = '/tmp/foo' expect(calculator.string(t)).to eq("File['/tmp/foo']") end it "should yield 'Enum[s,...]' for a PEnumType[s,...]" do t = enum_t('a', 'b', 'c') expect(calculator.string(t)).to eq("Enum['a', 'b', 'c']") end it "should yield 'Pattern[/pat/,...]' for a PPatternType['pat',...]" do t = pattern_t('a') t2 = pattern_t('a', 'b', 'c') expect(calculator.string(t)).to eq("Pattern[/a/]") expect(calculator.string(t2)).to eq("Pattern[/a/, /b/, /c/]") end it "should escape special characters in the string for a PPatternType['pat',...]" do t = pattern_t('a/b') expect(calculator.string(t)).to eq("Pattern[/a\\/b/]") end it "should yield 'Variant[t1,t2,...]' for a PVariantType[t1, t2,...]" do t1 = string_t() t2 = integer_t() t3 = pattern_t('a') t = variant_t(t1, t2, t3) expect(calculator.string(t)).to eq("Variant[String, Integer, Pattern[/a/]]") end it "should yield 'Callable' for generic callable" do expect(calculator.string(all_callables_t)).to eql("Callable") end it "should yield 'Callable[0,0]' for callable without params" do expect(calculator.string(callable_t)).to eql("Callable[0, 0]") end it "should yield 'Callable[t,t]' for callable with typed parameters" do expect(calculator.string(callable_t(String, Integer))).to eql("Callable[String, Integer]") end it "should yield 'Callable[t,min,max]' for callable with size constraint (infinite max)" do expect(calculator.string(callable_t(String, 0))).to eql("Callable[String, 0, default]") end it "should yield 'Callable[t,min,max]' for callable with size constraint (capped max)" do expect(calculator.string(callable_t(String, 0, 3))).to eql("Callable[String, 0, 3]") end it "should yield 'Callable[min,max]' callable with size > 0" do expect(calculator.string(callable_t(0, 0))).to eql("Callable[0, 0]") expect(calculator.string(callable_t(0, 1))).to eql("Callable[0, 1]") expect(calculator.string(callable_t(0, :default))).to eql("Callable[0, default]") end it "should yield 'Callable[Callable]' for callable with block" do expect(calculator.string(callable_t(all_callables_t))).to eql("Callable[0, 0, Callable]") expect(calculator.string(callable_t(string_t, all_callables_t))).to eql("Callable[String, Callable]") expect(calculator.string(callable_t(string_t, 1,1, all_callables_t))).to eql("Callable[String, 1, 1, Callable]") end it "should yield Unit for a Unit type" do expect(calculator.string(unit_t)).to eql('Unit') end end context 'when processing meta type' do it 'should infer PType as the type of all other types' do ptype = Puppet::Pops::Types::PType expect(calculator.infer(Puppet::Pops::Types::PNilType.new() ).is_a?(ptype)).to eq(true) expect(calculator.infer(Puppet::Pops::Types::PDataType.new() ).is_a?(ptype)).to eq(true) expect(calculator.infer(Puppet::Pops::Types::PScalarType.new() ).is_a?(ptype)).to eq(true) expect(calculator.infer(Puppet::Pops::Types::PStringType.new() ).is_a?(ptype)).to eq(true) expect(calculator.infer(Puppet::Pops::Types::PNumericType.new() ).is_a?(ptype)).to eq(true) expect(calculator.infer(Puppet::Pops::Types::PIntegerType.new() ).is_a?(ptype)).to eq(true) expect(calculator.infer(Puppet::Pops::Types::PFloatType.new() ).is_a?(ptype)).to eq(true) expect(calculator.infer(Puppet::Pops::Types::PRegexpType.new() ).is_a?(ptype)).to eq(true) expect(calculator.infer(Puppet::Pops::Types::PBooleanType.new() ).is_a?(ptype)).to eq(true) expect(calculator.infer(Puppet::Pops::Types::PCollectionType.new()).is_a?(ptype)).to eq(true) expect(calculator.infer(Puppet::Pops::Types::PArrayType.new() ).is_a?(ptype)).to eq(true) expect(calculator.infer(Puppet::Pops::Types::PHashType.new() ).is_a?(ptype)).to eq(true) expect(calculator.infer(Puppet::Pops::Types::PRuntimeType.new() ).is_a?(ptype)).to eq(true) expect(calculator.infer(Puppet::Pops::Types::PHostClassType.new() ).is_a?(ptype)).to eq(true) expect(calculator.infer(Puppet::Pops::Types::PResourceType.new() ).is_a?(ptype)).to eq(true) expect(calculator.infer(Puppet::Pops::Types::PEnumType.new() ).is_a?(ptype)).to eq(true) expect(calculator.infer(Puppet::Pops::Types::PPatternType.new() ).is_a?(ptype)).to eq(true) expect(calculator.infer(Puppet::Pops::Types::PVariantType.new() ).is_a?(ptype)).to eq(true) expect(calculator.infer(Puppet::Pops::Types::PTupleType.new() ).is_a?(ptype)).to eq(true) expect(calculator.infer(Puppet::Pops::Types::POptionalType.new() ).is_a?(ptype)).to eq(true) expect(calculator.infer(Puppet::Pops::Types::PCallableType.new() ).is_a?(ptype)).to eq(true) end it 'should infer PType as the type of all other types' do ptype = Puppet::Pops::Types::PType expect(calculator.string(calculator.infer(Puppet::Pops::Types::PNilType.new() ))).to eq("Type[Undef]") expect(calculator.string(calculator.infer(Puppet::Pops::Types::PDataType.new() ))).to eq("Type[Data]") expect(calculator.string(calculator.infer(Puppet::Pops::Types::PScalarType.new() ))).to eq("Type[Scalar]") expect(calculator.string(calculator.infer(Puppet::Pops::Types::PStringType.new() ))).to eq("Type[String]") expect(calculator.string(calculator.infer(Puppet::Pops::Types::PNumericType.new() ))).to eq("Type[Numeric]") expect(calculator.string(calculator.infer(Puppet::Pops::Types::PIntegerType.new() ))).to eq("Type[Integer]") expect(calculator.string(calculator.infer(Puppet::Pops::Types::PFloatType.new() ))).to eq("Type[Float]") expect(calculator.string(calculator.infer(Puppet::Pops::Types::PRegexpType.new() ))).to eq("Type[Regexp]") expect(calculator.string(calculator.infer(Puppet::Pops::Types::PBooleanType.new() ))).to eq("Type[Boolean]") expect(calculator.string(calculator.infer(Puppet::Pops::Types::PCollectionType.new()))).to eq("Type[Collection]") expect(calculator.string(calculator.infer(Puppet::Pops::Types::PArrayType.new() ))).to eq("Type[Array[?]]") expect(calculator.string(calculator.infer(Puppet::Pops::Types::PHashType.new() ))).to eq("Type[Hash[?, ?]]") expect(calculator.string(calculator.infer(Puppet::Pops::Types::PRuntimeType.new() ))).to eq("Type[Runtime[?, ?]]") expect(calculator.string(calculator.infer(Puppet::Pops::Types::PHostClassType.new() ))).to eq("Type[Class]") expect(calculator.string(calculator.infer(Puppet::Pops::Types::PResourceType.new() ))).to eq("Type[Resource]") expect(calculator.string(calculator.infer(Puppet::Pops::Types::PEnumType.new() ))).to eq("Type[Enum]") expect(calculator.string(calculator.infer(Puppet::Pops::Types::PVariantType.new() ))).to eq("Type[Variant]") expect(calculator.string(calculator.infer(Puppet::Pops::Types::PPatternType.new() ))).to eq("Type[Pattern]") expect(calculator.string(calculator.infer(Puppet::Pops::Types::PTupleType.new() ))).to eq("Type[Tuple]") expect(calculator.string(calculator.infer(Puppet::Pops::Types::POptionalType.new() ))).to eq("Type[Optional]") expect(calculator.string(calculator.infer(Puppet::Pops::Types::PCallableType.new() ))).to eq("Type[Callable]") expect(calculator.infer(Puppet::Pops::Types::PResourceType.new(:type_name => 'foo::fee::fum')).to_s).to eq("Type[Foo::Fee::Fum]") expect(calculator.string(calculator.infer(Puppet::Pops::Types::PResourceType.new(:type_name => 'foo::fee::fum')))).to eq("Type[Foo::Fee::Fum]") expect(calculator.infer(Puppet::Pops::Types::PResourceType.new(:type_name => 'Foo::Fee::Fum')).to_s).to eq("Type[Foo::Fee::Fum]") end it "computes the common type of PType's type parameter" do int_t = Puppet::Pops::Types::PIntegerType.new() string_t = Puppet::Pops::Types::PStringType.new() expect(calculator.string(calculator.infer([int_t]))).to eq("Array[Type[Integer], 1, 1]") expect(calculator.string(calculator.infer([int_t, string_t]))).to eq("Array[Type[Scalar], 2, 2]") end it 'should infer PType as the type of ruby classes' do class Foo end [Object, Numeric, Integer, Fixnum, Bignum, Float, String, Regexp, Array, Hash, Foo].each do |c| expect(calculator.infer(c).is_a?(Puppet::Pops::Types::PType)).to eq(true) end end it 'should infer PType as the type of PType (meta regression short-circuit)' do expect(calculator.infer(Puppet::Pops::Types::PType.new()).is_a?(Puppet::Pops::Types::PType)).to eq(true) end it 'computes instance? to be true if parameterized and type match' do int_t = Puppet::Pops::Types::PIntegerType.new() type_t = Puppet::Pops::Types::TypeFactory.type_type(int_t) type_type_t = Puppet::Pops::Types::TypeFactory.type_type(type_t) expect(calculator.instance?(type_type_t, type_t)).to eq(true) end it 'computes instance? to be false if parameterized and type do not match' do int_t = Puppet::Pops::Types::PIntegerType.new() string_t = Puppet::Pops::Types::PStringType.new() type_t = Puppet::Pops::Types::TypeFactory.type_type(int_t) type_t2 = Puppet::Pops::Types::TypeFactory.type_type(string_t) type_type_t = Puppet::Pops::Types::TypeFactory.type_type(type_t) # i.e. Type[Integer] =~ Type[Type[Integer]] # false expect(calculator.instance?(type_type_t, type_t2)).to eq(false) end it 'computes instance? to be true if unparameterized and matched against a type[?]' do int_t = Puppet::Pops::Types::PIntegerType.new() type_t = Puppet::Pops::Types::TypeFactory.type_type(int_t) expect(calculator.instance?(Puppet::Pops::Types::PType.new, type_t)).to eq(true) end end context "when asking for an enumerable " do it "should produce an enumerable for an Integer range that is not infinite" do t = Puppet::Pops::Types::PIntegerType.new() t.from = 1 t.to = 10 expect(calculator.enumerable(t).respond_to?(:each)).to eq(true) end it "should not produce an enumerable for an Integer range that has an infinite side" do t = Puppet::Pops::Types::PIntegerType.new() t.from = nil t.to = 10 expect(calculator.enumerable(t)).to eq(nil) t = Puppet::Pops::Types::PIntegerType.new() t.from = 1 t.to = nil expect(calculator.enumerable(t)).to eq(nil) end it "all but Integer range are not enumerable" do [Object, Numeric, Float, String, Regexp, Array, Hash].each do |t| expect(calculator.enumerable(calculator.type(t))).to eq(nil) end end end context "when dealing with different types of inference" do it "an instance specific inference is produced by infer" do expect(calculator.infer(['a','b']).element_type.values).to eq(['a', 'b']) end it "a generic inference is produced using infer_generic" do expect(calculator.infer_generic(['a','b']).element_type.values).to eq([]) end it "a generic result is created by generalize! given an instance specific result for an Array" do generic = calculator.infer(['a','b']) expect(generic.element_type.values).to eq(['a', 'b']) calculator.generalize!(generic) expect(generic.element_type.values).to eq([]) end it "a generic result is created by generalize! given an instance specific result for a Hash" do generic = calculator.infer({'a' =>1,'b' => 2}) expect(generic.key_type.values.sort).to eq(['a', 'b']) expect(generic.element_type.from).to eq(1) expect(generic.element_type.to).to eq(2) calculator.generalize!(generic) expect(generic.key_type.values).to eq([]) expect(generic.element_type.from).to eq(nil) expect(generic.element_type.to).to eq(nil) end it "does not reduce by combining types when using infer_set" do element_type = calculator.infer(['a','b',1,2]).element_type expect(element_type.class).to eq(Puppet::Pops::Types::PScalarType) inferred_type = calculator.infer_set(['a','b',1,2]) expect(inferred_type.class).to eq(Puppet::Pops::Types::PTupleType) element_types = inferred_type.types expect(element_types[0].class).to eq(Puppet::Pops::Types::PStringType) expect(element_types[1].class).to eq(Puppet::Pops::Types::PStringType) expect(element_types[2].class).to eq(Puppet::Pops::Types::PIntegerType) expect(element_types[3].class).to eq(Puppet::Pops::Types::PIntegerType) end it "does not reduce by combining types when using infer_set and values are undef" do element_type = calculator.infer(['a',nil]).element_type expect(element_type.class).to eq(Puppet::Pops::Types::PStringType) inferred_type = calculator.infer_set(['a',nil]) expect(inferred_type.class).to eq(Puppet::Pops::Types::PTupleType) element_types = inferred_type.types expect(element_types[0].class).to eq(Puppet::Pops::Types::PStringType) expect(element_types[1].class).to eq(Puppet::Pops::Types::PNilType) end end context 'when determening callability' do context 'and given is exact' do it 'with callable' do required = callable_t(string_t) given = callable_t(string_t) expect(calculator.callable?(required, given)).to eq(true) end it 'with args tuple' do required = callable_t(string_t) given = tuple_t(string_t) expect(calculator.callable?(required, given)).to eq(true) end it 'with args tuple having a block' do required = callable_t(string_t, callable_t(string_t)) given = tuple_t(string_t, callable_t(string_t)) expect(calculator.callable?(required, given)).to eq(true) end it 'with args array' do required = callable_t(string_t) given = array_t(string_t) factory.constrain_size(given, 1, 1) expect(calculator.callable?(required, given)).to eq(true) end end context 'and given is more generic' do it 'with callable' do required = callable_t(string_t) given = callable_t(object_t) expect(calculator.callable?(required, given)).to eq(true) end it 'with args tuple' do required = callable_t(string_t) given = tuple_t(object_t) expect(calculator.callable?(required, given)).to eq(false) end it 'with args tuple having a block' do required = callable_t(string_t, callable_t(string_t)) given = tuple_t(string_t, callable_t(object_t)) expect(calculator.callable?(required, given)).to eq(true) end it 'with args tuple having a block with captures rest' do required = callable_t(string_t, callable_t(string_t)) given = tuple_t(string_t, callable_t(object_t, 0, :default)) expect(calculator.callable?(required, given)).to eq(true) end end context 'and given is more specific' do it 'with callable' do required = callable_t(object_t) given = callable_t(string_t) expect(calculator.callable?(required, given)).to eq(false) end it 'with args tuple' do required = callable_t(object_t) given = tuple_t(string_t) expect(calculator.callable?(required, given)).to eq(true) end it 'with args tuple having a block' do required = callable_t(string_t, callable_t(object_t)) given = tuple_t(string_t, callable_t(string_t)) expect(calculator.callable?(required, given)).to eq(false) end it 'with args tuple having a block with captures rest' do required = callable_t(string_t, callable_t(object_t)) given = tuple_t(string_t, callable_t(string_t, 0, :default)) expect(calculator.callable?(required, given)).to eq(false) end end end matcher :be_assignable_to do |type| calc = Puppet::Pops::Types::TypeCalculator.new match do |actual| calc.assignable?(type, actual) end failure_message do |actual| "#{calc.string(actual)} should be assignable to #{calc.string(type)}" end failure_message_when_negated do |actual| "#{calc.string(actual)} is assignable to #{calc.string(type)} when it should not" end end end