diff --git a/lib/puppet/pops/evaluator/access_operator.rb b/lib/puppet/pops/evaluator/access_operator.rb index 6c3615ec0..cfc586706 100644 --- a/lib/puppet/pops/evaluator/access_operator.rb +++ b/lib/puppet/pops/evaluator/access_operator.rb @@ -1,543 +1,548 @@ # AccessOperator handles operator [] # This operator is part of evaluation. # class Puppet::Pops::Evaluator::AccessOperator # Provides access to the Puppet 3.x runtime (scope, etc.) # This separation has been made to make it easier to later migrate the evaluator to an improved runtime. # include Puppet::Pops::Evaluator::Runtime3Support Issues = Puppet::Pops::Issues TYPEFACTORY = Puppet::Pops::Types::TypeFactory attr_reader :semantic # Initialize with AccessExpression to enable reporting issues # @param access_expression [Puppet::Pops::Model::AccessExpression] the semantic object being evaluated # @return [void] # def initialize(access_expression) @@access_visitor ||= Puppet::Pops::Visitor.new(self, "access", 2, nil) @semantic = access_expression end def access (o, scope, *keys) @@access_visitor.visit_this_2(self, o, scope, keys) end protected def access_Object(o, scope, keys) fail(Issues::OPERATOR_NOT_APPLICABLE, @semantic.left_expr, :operator=>'[]', :left_value => o) end def access_String(o, scope, keys) keys.flatten! result = case keys.size when 0 fail(Puppet::Pops::Issues::BAD_STRING_SLICE_ARITY, @semantic.left_expr, {:actual => keys.size}) when 1 # Note that Ruby 1.8.7 requires a length of 1 to produce a String k1 = coerce_numeric(keys[0], @semantic.keys, scope) bad_access_key_type(o, 0, k1, Integer) unless k1.is_a?(Integer) k2 = 1 k1 = k1 < 0 ? o.length + k1 : k1 # abs pos # if k1 is outside, a length of 1 always produces an empty string if k1 < 0 '' else o[ k1, k2 ] end when 2 k1 = coerce_numeric(keys[0], @semantic.keys, scope) k2 = coerce_numeric(keys[1], @semantic.keys, scope) [k1, k2].each_with_index { |k,i| bad_access_key_type(o, i, k, Integer) unless k.is_a?(Integer) } k1 = k1 < 0 ? o.length + k1 : k1 # abs pos (negative is count from end) k2 = k2 < 0 ? o.length - k1 + k2 + 1 : k2 # abs length (negative k2 is length from pos to end count) # if k1 is outside, adjust to first position, and adjust length if k1 < 0 k2 = k2 + k1 k1 = 0 end o[ k1, k2 ] else fail(Puppet::Pops::Issues::BAD_STRING_SLICE_ARITY, @semantic.left_expr, {:actual => keys.size}) end # Specified as: an index outside of range, or empty result == empty string (result.nil? || result.empty?) ? '' : result end # Parameterizes a PRegexp Type with a pattern string or r ruby egexp # def access_PRegexpType(o, scope, keys) keys.flatten! unless keys.size == 1 blamed = keys.size == 0 ? @semantic : @semantic.keys[2] fail(Puppet::Pops::Issues::BAD_TYPE_SLICE_ARITY, blamed, :base_type => o, :min=>1, :actual => keys.size) end assert_keys(keys, o, 1, 1, String, Regexp) Puppet::Pops::Types::TypeFactory.regexp(*keys) end # Evaluates [] with 1 or 2 arguments. One argument is an index lookup, two arguments is a slice from/to. # def access_Array(o, scope, keys) keys.flatten! case keys.size when 0 fail(Puppet::Pops::Issues::BAD_ARRAY_SLICE_ARITY, @semantic.left_expr, {:actual => keys.size}) when 1 k = coerce_numeric(keys[0], @semantic.keys[0], scope) unless k.is_a?(Integer) bad_access_key_type(o, 0, k, Integer) end o[k] when 2 # A slice [from, to] with support for -1 to mean start, or end respectively. k1 = coerce_numeric(keys[0], @semantic.keys[0], scope) k2 = coerce_numeric(keys[1], @semantic.keys[1], scope) [k1, k2].each_with_index { |k,i| bad_access_key_type(o, i, k, Integer) unless k.is_a?(Integer) } # Help confused Ruby do the right thing (it truncates to the right, but negative index + length can never overlap # the available range. k1 = k1 < 0 ? o.length + k1 : k1 # abs pos (negative is count from end) k2 = k2 < 0 ? o.length - k1 + k2 + 1 : k2 # abs length (negative k2 is length from pos to end count) # if k1 is outside, adjust to first position, and adjust length if k1 < 0 k2 = k2 + k1 k1 = 0 end # Help ruby always return empty array when asking for a sub array result = o[ k1, k2 ] result.nil? ? [] : result else fail(Puppet::Pops::Issues::BAD_ARRAY_SLICE_ARITY, @semantic.left_expr, {:actual => keys.size}) end end # Evaluates [] with support for one or more arguments. If more than one argument is used, the result # is an array with each lookup. # @note # Does not flatten its keys to enable looking up with a structure # def access_Hash(o, scope, keys) # Look up key in hash, if key is nil or :undef, try alternate form before giving up. # This makes :undef and nil "be the same key". (The alternative is to always only write one or the other # in all hashes - that is much harder to guarantee since the Hash is a regular Ruby hash. # result = keys.collect do |k| o.fetch(k) do |key| case key when nil o[:undef] when :undef o[:nil] else nil end end end case result.size when 0 fail(Puppet::Pops::Issues::BAD_HASH_SLICE_ARITY, @semantic.left_expr, {:actual => keys.size}) when 1 result.pop else # remove nil elements and return result.compact! result end end # Ruby does not have an infinity constant. TODO: Consider having one constant in Puppet. Now it is in several places. INFINITY = 1.0 / 0.0 def access_PEnumType(o, scope, keys) keys.flatten! assert_keys(keys, o, 1, INFINITY, String) Puppet::Pops::Types::TypeFactory.enum(*keys) end def access_PVariantType(o, scope, keys) keys.flatten! assert_keys(keys, o, 1, INFINITY, Puppet::Pops::Types::PAbstractType) Puppet::Pops::Types::TypeFactory.variant(*keys) end def access_PTupleType(o, scope, keys) keys.flatten! if TYPEFACTORY.is_range_parameter?(keys[-2]) && TYPEFACTORY.is_range_parameter?(keys[-1]) size_type = TYPEFACTORY.range(keys[-2], keys[-1]) keys = keys[0, keys.size - 2] elsif TYPEFACTORY.is_range_parameter?(keys[-1]) size_type = TYPEFACTORY.range(keys[-1], :default) keys = keys[0, keys.size - 1] end assert_keys(keys, o, 1, INFINITY, Puppet::Pops::Types::PAbstractType) t = Puppet::Pops::Types::TypeFactory.tuple(*keys) # set size type, or nil for default (exactly 1) t.size_type = size_type t end + def access_PStructType(o, scope, keys) + assert_keys(keys, o, 1, 1, Hash) + TYPEFACTORY.struct(keys[0]) + end + def access_PStringType(o, scope, keys) keys.flatten! case keys.size when 1 size_t = collection_size_t(0, keys[0]) when 2 size_t = collection_size_t(0, keys[0], keys[1]) else fail(Puppet::Pops::Issues::BAD_STRING_SLICE_ARITY, @semantic, {:actual => keys.size}) end string_t = Puppet::Pops::Types::TypeFactory.string() string_t.size_type = size_t string_t end # Asserts type of each key and calls fail with BAD_TYPE_SPECIFICATION # @param keys [Array] the evaluated keys # @param o [Object] evaluated LHS reported as :base_type # @param min [Integer] the minimum number of keys (typically 1) # @param max [Numeric] the maximum number of keys (use same as min, specific number, or INFINITY) # @param allowed_classes [Class] a variable number of classes that each key must be an instance of (any) # @api private # def assert_keys(keys, o, min, max, *allowed_classes) size = keys.size unless size.between?(min, max || INFINITY) fail(Puppet::Pops::Issues::BAD_TYPE_SLICE_ARITY, blamed, :base_type => o, :min=>1, :max => max, :actual => keys.size) end keys.each_with_index do |k, i| unless allowed_classes.any? {|clazz| k.is_a?(clazz) } bad_type_specialization_key_type(o, i, k, *allowed_classes) end end end def bad_access_key_type(lhs, key_index, actual, *expected_classes) fail(Puppet::Pops::Issues::BAD_SLICE_KEY_TYPE, @semantic.keys[key_index], { :left_value => lhs, :actual => bad_key_type_name(actual), :expected_classes => expected_classes }) end def bad_key_type_name(actual) case actual when nil, :undef 'Undef' when :default 'Default' else actual.class.name end end def bad_type_specialization_key_type(type, key_index, actual, *expected_classes) label_provider = Puppet::Pops::Model::ModelLabelProvider.new() expected = expected_classes.map {|c| label_provider.label(c) }.join(' or ') fail(Puppet::Pops::Issues::BAD_TYPE_SPECIALIZATION, @semantic.keys[key_index], { :type => type, :message => "Cannot use #{bad_key_type_name(actual)} where #{expected} is expected" }) end def access_PPatternType(o, scope, keys) keys.flatten! assert_keys(keys, o, 1, INFINITY, String, Regexp, Puppet::Pops::Types::PPatternType, Puppet::Pops::Types::PRegexpType) Puppet::Pops::Types::TypeFactory.pattern(*keys) end def access_POptionalType(o, scope, keys) keys.flatten! if keys.size == 1 unless keys[0].is_a?(Puppet::Pops::Types::PAbstractType) fail(Puppet::Pops::Issues::BAD_TYPE_SLICE_TYPE, @semantic.keys[0], {:base_type => 'Optional-Type', :actual => keys[0].class}) end result = Puppet::Pops::Types::POptionalType.new() result.optional_type = keys[0] result else fail(Puppet::Pops::Issues::BAD_TYPE_SLICE_ARITY, @semantic, {:base_type => 'Optional-Type', :min => 1, :actual => keys.size}) end end def access_PType(o, scope, keys) keys.flatten! if keys.size == 1 unless keys[0].is_a?(Puppet::Pops::Types::PAbstractType) fail(Puppet::Pops::Issues::BAD_TYPE_SLICE_TYPE, @semantic.keys[0], {:base_type => 'Type-Type', :actual => keys[0].class}) end result = Puppet::Pops::Types::PType.new() result.type = keys[0] result else fail(Puppet::Pops::Issues::BAD_TYPE_SLICE_ARITY, @semantic, {:base_type => 'Type-Type', :min => 1, :actual => keys.size}) end end def access_PRubyType(o, scope, keys) keys.flatten! assert_keys(keys, o, 1, 1, String) # create ruby type based on name of class, not inference of key's type Puppet::Pops::Types::TypeFactory.ruby_type(keys[0]) end def access_PIntegerType(o, scope, keys) keys.flatten! unless keys.size.between?(1, 2) fail(Puppet::Pops::Issues::BAD_INTEGER_SLICE_ARITY, @semantic, {:actual => keys.size}) end keys.each_with_index do |x, index| fail(Puppet::Pops::Issues::BAD_INTEGER_SLICE_TYPE, @semantic.keys[index], {:actual => x.class}) unless (x.is_a?(Integer) || x == :default) end ranged_integer = Puppet::Pops::Types::PIntegerType.new() from, to = keys ranged_integer.from = from == :default ? nil : from ranged_integer.to = to == :default ? nil : to ranged_integer end def access_PFloatType(o, scope, keys) keys.flatten! unless keys.size.between?(1, 2) fail(Puppet::Pops::Issues::BAD_FLOAT_SLICE_ARITY, @semantic, {:actual => keys.size}) end keys.each_with_index do |x, index| fail(Puppet::Pops::Issues::BAD_FLOAT_SLICE_TYPE, @semantic.keys[index], {:actual => x.class}) unless (x.is_a?(Float) || x.is_a?(Integer) || x == :default) end ranged_float = Puppet::Pops::Types::PFloatType.new() from, to = keys ranged_float.from = from == :default || from.nil? ? nil : Float(from) ranged_float.to = to == :default || to.nil? ? nil : Float(to) ranged_float end # A Hash can create a new Hash type, one arg sets value type, two args sets key and value type in new type. # With 3 or 4 arguments, these are used to create a size constraint. # It is not possible to create a collection of Hash types directly. # def access_PHashType(o, scope, keys) keys.flatten! keys[0,2].each_with_index do |k, index| unless k.is_a?(Puppet::Pops::Types::PAbstractType) fail(Puppet::Pops::Issues::BAD_TYPE_SLICE_TYPE, @semantic.keys[index], {:base_type => 'Hash-Type', :actual => k.class}) end end case keys.size when 1 result = Puppet::Pops::Types::PHashType.new() result.key_type = o.key_type.copy result.element_type = keys[0] result when 2 result = Puppet::Pops::Types::PHashType.new() result.key_type = keys[0] result.element_type = keys[1] result when 3 result = Puppet::Pops::Types::PHashType.new() result.key_type = keys[0] result.element_type = keys[1] size_t = collection_size_t(1, keys[2]) result when 4 result = Puppet::Pops::Types::PHashType.new() result.key_type = keys[0] result.element_type = keys[1] size_t = collection_size_t(1, keys[2], keys[3]) result else fail(Puppet::Pops::Issues::BAD_TYPE_SLICE_ARITY, @semantic, { :base_type => 'Hash-Type', :min => 1, :max => 4, :actual => keys.size }) end result.size_type = size_t if size_t result end # CollectionType is parameterized with a range def access_PCollectionType(o, scope, keys) keys.flatten! case keys.size when 1 size_t = collection_size_t(1, keys[0]) when 2 size_t = collection_size_t(1, keys[0], keys[1]) else fail(Puppet::Pops::Issues::BAD_TYPE_SLICE_ARITY, @semantic, {:base_type => 'Collection-Type', :min => 1, :max => 2, :actual => keys.size}) end result = Puppet::Pops::Types::PCollectionType.new() result.size_type = size_t result end # An Array can create a new Array type. It is not possible to create a collection of Array types. # def access_PArrayType(o, scope, keys) keys.flatten! case keys.size when 1 size_t = nil when 2 size_t = collection_size_t(1, keys[1]) when 3 size_t = collection_size_t(1, keys[1], keys[2]) else fail(Puppet::Pops::Issues::BAD_TYPE_SLICE_ARITY, @semantic, {:base_type => 'Array-Type', :min => 1, :max => 3, :actual => keys.size}) end unless keys[0].is_a?(Puppet::Pops::Types::PAbstractType) fail(Puppet::Pops::Issues::BAD_TYPE_SLICE_TYPE, @semantic.keys[0], {:base_type => 'Array-Type', :actual => keys[0].class}) end result = Puppet::Pops::Types::PArrayType.new() result.element_type = keys[0] result.size_type = size_t result end # Produces an PIntegerType (range) given one or two keys. def collection_size_t(start_index, *keys) if keys.size == 1 && keys[0].is_a?(Puppet::Pops::Types::PIntegerType) keys[0].copy else keys.each_with_index do |x, index| fail(Puppet::Pops::Issues::BAD_COLLECTION_SLICE_TYPE, @semantic.keys[start_index + index], {:actual => x.class}) unless (x.is_a?(Integer) || x == :default) end ranged_integer = Puppet::Pops::Types::PIntegerType.new() from, to = keys ranged_integer.from = from == :default ? nil : from ranged_integer.to = to == :default ? nil : to ranged_integer end end # A Resource can create a new more specific Resource type, and/or an array of resource types # If the given type has title set, it can not be specified further. # @example # Resource[File] # => File # Resource[File, 'foo'] # => File[foo] # Resource[File. 'foo', 'bar'] # => [File[foo], File[bar]] # File['foo', 'bar'] # => [File[foo], File[bar]] # File['foo']['bar'] # => Value of the 'bar' parameter in the File['foo'] resource # Resource[File]['foo', 'bar'] # => [File[Foo], File[bar]] # Resource[File, 'foo', 'bar'] # => [File[foo], File[bar]] # Resource[File, 'foo']['bar'] # => Value of the 'bar' parameter in the File['foo'] resource # def access_PResourceType(o, scope, keys) keys.flatten! if keys.size == 0 fail(Puppet::Pops::Issues::BAD_TYPE_SLICE_ARITY, o, :base_type => Puppet::Pops::Types::TypeCalculator.new().string(o), :min => 1, :actual => 0) end if !o.title.nil? # lookup resource and return one or more parameter values resource = find_resource(scope, o.type_name, o.title) unless resource fail(Puppet::Pops::Issues::UNKNOWN_RESOURCE, @semantic, {:type_name => o.type_name, :title => o.title}) end result = keys.map do |k| unless is_parameter_of_resource?(scope, resource, k) fail(Puppet::Pops::Issues::UNKNOWN_RESOURCE_PARAMETER, @semantic, {:type_name => o.type_name, :title => o.title, :param_name=>k}) end get_resource_parameter_value(scope, resource, k) end return result.size <= 1 ? result.pop : result end # type_name is LHS type_name if set, else the first given arg keys_orig_size = keys.size type_name = o.type_name || keys.shift type_name = case type_name when Puppet::Pops::Types::PResourceType type_name.type_name when String type_name.downcase else blame = keys_orig_size != keys.size ? @semantic.keys[0] : @semantic.left_expr fail(Puppet::Pops::Issues::ILLEGAL_RESOURCE_SPECIALIZATION, blame, {:actual => type_name.class}) end keys = [:no_title] if keys.size < 1 # if there was only a type_name and it was consumed result = keys.each_with_index.map do |t, i| unless t.is_a?(String) || t == :no_title type_to_report = case t when nil, :undef 'Undef' when :default 'Default' else t.class.name end index = keys_orig_size != keys.size ? i+1 : i fail(Puppet::Pops::Issues::BAD_TYPE_SPECIALIZATION, @semantic.keys[index], { :type => o, :message => "Cannot use #{type_to_report} where String is expected" }) end rtype = Puppet::Pops::Types::PResourceType.new() rtype.type_name = type_name rtype.title = (t == :no_title ? nil : t) rtype end # returns single type as type, else an array of types result.size == 1 ? result.pop : result end def access_PHostClassType(o, scope, keys) keys.flatten! if keys.size == 0 fail(Puppet::Pops::Issues::BAD_TYPE_SLICE_ARITY, o, :base_type => Puppet::Pops::Types::TypeCalculator.new().string(o), :min => 1, :actual => 0) end if ! o.class_name.nil? # lookup class resource and return one or more parameter values resource = find_resource(scope, 'class', o.class_name) unless resource fail(Puppet::Pops::Issues::UNKNOWN_RESOURCE, @semantic, {:type_name => 'Class', :title => o.class_name}) end result = keys.map do |k| unless is_parameter_of_resource?(scope, resource, k) fail(Puppet::Pops::Issues::UNKNOWN_RESOURCE_PARAMETER, @semantic, {:type_name => 'Class', :title => o.class_name, :param_name=>k}) end get_resource_parameter_value(scope, resource, k) end return result.size <= 1 ? result.pop : result # TODO: if [] is applied to specific class, it should be treated the same as getting # a resource parameter. Now it fails the operation # fail(Puppet::Pops::Issues::ILLEGAL_TYPE_SPECIALIZATION, semantic.left_expr, {:kind => 'Class'}) end # The type argument may be a Resource Type - the Puppet Language allows a reference such as # Class[Foo], and this is interpreted as Class[Resource[Foo]] - which is ok as long as the resource # does not have a title. This should probably be deprecated. # result = keys.each_with_index.map do |c, i| ctype = Puppet::Pops::Types::PHostClassType.new() if c.is_a?(Puppet::Pops::Types::PResourceType) && !c.type_name.nil? && c.title.nil? c = c.type_name.downcase end unless c.is_a?(String) fail(Puppet::Pops::Issues::ILLEGAL_HOSTCLASS_NAME, @semantic.keys[i], {:name => c}) end if c !~ Puppet::Pops::Patterns::NAME fail(Issues::ILLEGAL_NAME, @semantic.keys[i], {:name=>c}) end ctype.class_name = c ctype end # returns single type as type, else an array of types result.size == 1 ? result.pop : result end end diff --git a/lib/puppet/pops/types/type_calculator.rb b/lib/puppet/pops/types/type_calculator.rb index 70584d5bc..d09963acd 100644 --- a/lib/puppet/pops/types/type_calculator.rb +++ b/lib/puppet/pops/types/type_calculator.rb @@ -1,1400 +1,1471 @@ # 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 PRubyType # ------------- # The PRubyType corresponds to a Ruby Class, except for the puppet types that are specialized (i.e. PRubyType should not be # used for Integer, String, etc. since there are specialized types for those). # When the type calculator deals with PRubyTypes 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 PRubyType 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 Object 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 infering 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 TheInfinity = 1.0 / 0.0 # because the Infinity symbol is not defined # @api public def self.assignable?(t1, t2) singleton.assignable?(t1,t2) end # @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) 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::PObjectType.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 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, PRubyType] 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::PRubyType) klazz = Puppet::Pops::Types::ClassLoader.provide(klazz) end # data types can not be injected (check again, it is not safe to assume that given RubyType klazz arg was ok) return false unless type(klazz).is_a?(Types::PRubyType) 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' # @api public # def assignable?(t, t2) # nil is assignable to anything except to required types return true if is_pnil?(t2) if t.is_a?(Class) t = type(t) end if t2.is_a?(Class) t2 = type(t2) end @@assignable_visitor.visit_this_1(self, t, t2) 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 if the two given types describe the same type def equals(left, right) return false unless left.is_a?(Types::PAbstractType) && right.is_a?(Types::PAbstractType) # 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::PRubyType.new() type.ruby_class = 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) # return true if o.nil? && !t.is_a?(Types::PRequiredType) @@instance_of_visitor.visit_this_1(self, t, o) end def instance_of_Object(t, o) # Undef is Undef and Object, but nothing else when checking instance? return false if (o.nil? || o == :undef) && t.class != Types::PObjectType assignable?(t, infer(o)) 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 size_t2 = size_as_type(o) assignable?(size_t, size_t2) 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 from, to = size_range(t.size_type) size_t = Types::PIntegerType.new() size_t.from = t2.types.size - 1 + from size_t.to = t2.types.size - 1 + to # compute the array's size as type size_t2 = size_as_type(o) return false unless assignable?(size_t, size_t2) o.each_with_index do |element, index| return false unless instance_of(t.types[index] || t.types[-1], element) end 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) + return false unless o.size == h.size && h.all? { |k,v| o.has_key?(k) && 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 size_t2 = size_as_type(o) assignable?(size_t, size_t2) end def instance_of_PDataType(t, o) #require 'debugger'; debugger instance_of(@data_variant_t, o) end def instance_of_PNilType(t, o) return o.nil? || o == :undef end def instance_of_POptionalType(t, o) return true if (o.nil? || o == :undef) 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 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::PAbstractType) 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)) # if either is nil, the common type is the other if is_pnil?(t1) return t2 elsif is_pnil?(t2) 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 == TheInfinity t.to = to unless to == TheInfinity 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 == TheInfinity t.to = to unless to == TheInfinity 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 {|p| p.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 {|opt_t| opt_t.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 # 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 t1.is_a?(Types::PRubyType) && t2.is_a?(Types::PRubyType) if t1.ruby_class == t2.ruby_class return t1 end # finding the common super class requires that names are resolved to class 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 result = Types::PRubyType.new() result.ruby_class = c1_super.name return result end end end end end # If both are RubyObjects if common_pobject?(t1, t2) return Types::PObjectType.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_Object(o) type = Types::PRubyType.new() type.ruby_class = o.class.name type end # The type of all types is PType # @api private # def infer_PObjectType(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 :undef as PNilType, all other are Ruby[Symbol] # @api private def infer_Symbol(o) o == :undef ? infer_NilClass(o) : infer_Object(o) 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 t.title = o.title t 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) type = Types::PArrayType.new() type.element_type = if o.empty? Types::PNilType.new() else t = Types::PVariantType.new() t.types = o.map() {|x| infer_set(x) } t.types.size == 1 ? t.types[0] : t end type.size_type = size_as_type(o) type end def infer_set_Hash(o) type = Types::PHashType.new() if o.empty? ktype = Types::PNilType.new() etype = Types::PNilType.new() 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) } end type.key_type = unwrap_single_variant(ktype) type.element_type = unwrap_single_variant(vtype) 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_PObjectType(t, t2) t2.is_a?(Types::PObjectType) end # @api private def assignable_PNilType(t, t2) # Only undef/nil is assignable to nil type t2.is_a?(Types::PNilType) 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? ? TheInfinity : to if x < y [x, y] else [y, x] end end # @api private def from_to_ordered(from, to) x = (from.nil? || from == :default) ? -TheInfinity : from y = (to.nil? || to == :default) ? TheInfinity : 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 if 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 def assignable_PTupleType(t, t2) return true if t == t2 || t.types.empty? && (t2.is_a?(Types::PArrayType)) t_regular = t.types[0..-2] t_ranged = t.types[-1] t_from, t_to = size_range(t.size_type) t_required = t_regular.size + t_from if t2.is_a?(Types::PTupleType) 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 # tuples with fewer required entries can not be assigned return false if t_required > t2_required # tuples with more optionally available entries can not be assigned return false if t_regular.size + t_to < t2_regular.size + t2_to t_required.times do |index| t_entry = tuple_entry_at(t, t_from, t_to, index) t2_entry = tuple_entry_at(t2, t2_from, t2_to, index) return false if t2_entry.nil? || !assignable?(t_entry, t2_entry) end # Handle remainder in t2's required (t2_required - t_required).times do |index| t_entry = tuple_entry_at(t, t_from, t_to, t_required + index) t2_entry = tuple_entry_at(t2, t2_from, t2_to, t_required + index) return false if t2_entry.nil? || !assignable?(t_entry, t2_entry) end # Now only a trailing optional type remains - the last type must always be compatible # irrespective of optionality and count # return assignable?(t_ranged, t2_ranged) 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? # array type may be size constrained size_t = t2.size_type || @collection_default_size_t sz = size_t.range # Array with fewer min entries can not be assigned return false if t_required > sz.min # Array with more optionally available entries can not be assigned return false if t_regular.size + t_to < sz.max # each tuple type must be assignable to the element type t_required.times do |index| t_entry = tuple_entry_at(t, t_from, t_to, index) return false unless assignable?(t_entry, t2_entry) end # ... and so must the last, possibly optional (ranged) type return assignable?(t_ranged, t2_entry) 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?(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 || (t.values.empty? && (t2.is_a?(Types::PStringType) || t2.is_a?(Types::PEnumType))) if t2.is_a?(Types::PStringType) # if the set of strings are all found in the set of enums 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?(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?(size_t, @collection_default_size_t) when Types::PEnumType if t2.values # 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 # no string can match this enum anyway since it does not accept anything false end 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 return false unless t2.is_a?(Types::PStringType) || t2.is_a?(Types::PEnumType) 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 regexps = t.patterns.map {|p| p.regexp } 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_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?(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?(size_t, t2s) + when Types::PStructType + from = to = t2.elements.size + t2s = Types::PIntegerType.new() + t2s.from = from + t2s.to = to + assignable?(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 # where # @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 # 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 t_entry.nil? # array type may be size constrained size_t = t.size_type || @collection_default_size_t sz = size_t.range # Tuple with fewer min entries can not be assigned return false if t2_required < sz.min # Tuple with more optionally available entries can not be assigned return false if t2_regular.size + t2_to > sz.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) - return false unless t2.is_a?(Types::PHashType) - return false unless assignable?(t.key_type, t2.key_type) && assignable?(t.element_type, t2.element_type) - assignable_PCollectionType(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 + sz = size_t.range + struct_size = t2.elements.size + element_type = t.element_type + ( struct_size >= sz.min && struct_size <= sz.max && + assignable?(t.key_type, @non_emptry_string_t) && + t2.hashed_elements.all? {|k,v| assignable?(element_type, v) }) + 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 ruby class is same or subclass of t1's ruby class # @api private def assignable_PRubyType(t1, t2) return false unless t2.is_a?(Types::PRubyType) return true if t1.ruby_class.nil? # t1 is wider return false if t2.ruby_class.nil? # t1 not nil, so t2 can not be wider c1 = class_from_string(t1.ruby_class) c2 = class_from_string(t2.ruby_class) 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_PObjectType(t) ; "Object" ; end # @api private def string_PNilType(t) ; 'Undef' ; 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_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_PRubyType(t) ; "Ruby[#{string(t.ruby_class)}]" ; 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 "#{t.type_name.capitalize}['#{t.title}']" else "#{t.type_name.capitalize}" end else "Resource" end end def string_POptionalType(t) "Optional[#{string(t.optional_type)}]" 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 == TheInfinity t end private def class_from_string(str) begin str.split('::').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 def common_pobject?(t1, t2) assignable?(@t, t1) && assignable?(@t, t2) end end diff --git a/lib/puppet/pops/types/type_factory.rb b/lib/puppet/pops/types/type_factory.rb index 2c94c656d..9f5860206 100644 --- a/lib/puppet/pops/types/type_factory.rb +++ b/lib/puppet/pops/types/type_factory.rb @@ -1,332 +1,335 @@ # Helper module that makes creation of type objects simpler. # @api public # module Puppet::Pops::Types::TypeFactory @type_calculator = Puppet::Pops::Types::TypeCalculator.new() Types = Puppet::Pops::Types # Produces the Integer type # @api public # def self.integer() Types::PIntegerType.new() end # Produces an Integer range type # @api public # def self.range(from, to) t = Types::PIntegerType.new() t.from = from unless (from == :default || from == 'default') t.to = to unless (to == :default || to == 'default') t end # Produces a Float range type # @api public # def self.float_range(from, to) t = Types::PFloatType.new() t.from = Float(from) unless from == :default || from.nil? t.to = Float(to) unless to == :default || to.nil? t end # Produces the Float type # @api public # def self.float() Types::PFloatType.new() end # Produces the Numeric type # @api public # def self.numeric() Types::PNumericType.new() end # Produces a string representation of the type # @api public # def self.label(t) @type_calculator.string(t) end # Produces the String type, optionally with specific string values # @api public # def self.string(*values) t = Types::PStringType.new() values.each {|v| t.addValues(v) } t end # Produces the Optional type, i.e. a short hand for Variant[T, Undef] def self.optional(optional_type = nil) t = Types::POptionalType.new t.optional_type = optional_type t end # Produces the Enum type, optionally with specific string values # @api public # def self.enum(*values) t = Types::PEnumType.new() values.each {|v| t.addValues(v) } t end # Produces the Variant type, optionally with the "one of" types # @api public # def self.variant(*types) t = Types::PVariantType.new() types.each {|v| t.addTypes(type_of(v)) } t end # Produces the Struct type, either a non parameterized instance representing all structs (i.e. all hashes) # or a hash with a given set of keys of String type (names), bound to a value of a given type. Type may be # a Ruby Class, a Puppet Type, or an instance from which the type is inferred. # def self.struct(name_type_hash = {}) t = Types::PStructType.new name_type_hash.map do |name, type| elem = Types::PStructElement.new + if name.is_a?(String) && name.empty? + raise ArgumentError, "An empty String can not be used where a String[1, default] is expected" + end elem.name = name elem.type = type_of(type) elem end.each {|elem| t.addElements(elem) } t end def self.tuple(*types) t = Types::PTupleType.new types.each {|elem| t.addTypes(type_of(elem)) } t end # Produces the Boolean type # @api public # def self.boolean() Types::PBooleanType.new() end # Produces the Object type # @api public # def self.object() Types::PObjectType.new() end # Produces the Regexp type # @param pattern [Regexp, String, nil] (nil) The regular expression object or a regexp source string, or nil for bare type # @api public # def self.regexp(pattern = nil) t = Types::PRegexpType.new() if pattern t.pattern = pattern.is_a?(Regexp) ? pattern.inspect[1..-2] : pattern end t end def self.pattern(*regular_expressions) t = Types::PPatternType.new() regular_expressions.each do |re| case re when String re_T = Types::PRegexpType.new() re_T.pattern = re t.addPatterns(re_T) when Regexp re_T = Types::PRegexpType.new() # Regep.to_s includes options user did not enter and does not escape source # to work either as a string or as a // regexp. The inspect method does a better # job, but includes the // re_T.pattern = re.inspect[1..-2] t.addPatterns(re_T) when Types::PRegexpType t.addPatterns(re.copy) when Types::PPatternType re.patterns.each do |p| t.addPatterns(p.copy) end else raise ArgumentError, "Only String, Regexp, Pattern-Type, and Regexp-Type are allowed: got '#{re.class}" end end t end # Produces the Literal type # @api public # def self.scalar() Types::PScalarType.new() end # Produces the abstract type Collection # @api public # def self.collection() Types::PCollectionType.new() end # Produces the Data type # @api public # def self.data() Types::PDataType.new() end # Creates an instance of the Undef type # @api public def self.undef() Types::PNilType.new() end # Produces an instance of the abstract type PCatalogEntryType def self.catalog_entry() Types::PCatalogEntryType.new() end # Produces a PResourceType with a String type_name # A PResourceType with a nil or empty name is compatible with any other PResourceType. # A PResourceType with a given name is only compatible with a PResourceType with the same name. # (There is no resource-type subtyping in Puppet (yet)). # def self.resource(type_name = nil, title = nil) type = Types::PResourceType.new() type_name = type_name.type_name if type_name.is_a?(Types::PResourceType) type.type_name = type_name.downcase unless type_name.nil? type.title = title type end # Produces PHostClassType with a string class_name. # A PHostClassType with nil or empty name is compatible with any other PHostClassType. # A PHostClassType with a given name is only compatible with a PHostClassType with the same name. # def self.host_class(class_name = nil) type = Types::PHostClassType.new() type.class_name = class_name type end # Produces a type for Array[o] where o is either a type, or an instance for which a type is inferred. # @api public # def self.array_of(o) type = Types::PArrayType.new() type.element_type = type_of(o) type end # Produces a type for Hash[Scalar, o] where o is either a type, or an instance for which a type is inferred. # @api public # def self.hash_of(value, key = scalar()) type = Types::PHashType.new() type.key_type = type_of(key) type.element_type = type_of(value) type end # Produces a type for Array[Data] # @api public # def self.array_of_data() type = Types::PArrayType.new() type.element_type = data() type end # Produces a type for Hash[Scalar, Data] # @api public # def self.hash_of_data() type = Types::PHashType.new() type.key_type = scalar() type.element_type = data() type end # Produces a type for Type[T] # @api public # def self.type_type(inst_type = nil) type = Types::PType.new() type.type = inst_type type end # Produce a type corresponding to the class of given unless given is a String, Class or a PObjectType. # When a String is given this is taken as a classname. # def self.type_of(o) if o.is_a?(Class) @type_calculator.type(o) elsif o.is_a?(Types::PObjectType) o elsif o.is_a?(String) type = Types::PRubyType.new() type.ruby_class = o type else @type_calculator.infer_generic(o) end end # Produces a type for a class or infers a type for something that is not a class # @note # To get the type for the class' class use `TypeCalculator.infer(c)` # # @overload ruby(o) # @param o [Class] produces the type corresponding to the class (e.g. Integer becomes PIntegerType) # @overload ruby(o) # @param o [Object] produces the type corresponding to the instance class (e.g. 3 becomes PIntegerType) # # @api public # def self.ruby(o) if o.is_a?(Class) @type_calculator.type(o) else type = Types::PRubyType.new() type.ruby_class = o.class.name type end end # Generic creator of a RubyType - allows creating the Ruby type with nil name, or String name. # Also see ruby(o) which performs inference, or mapps a Ruby Class to its name. # def self.ruby_type(class_name = nil) type = Types::PRubyType.new() type.ruby_class = class_name type end # Sets the accepted size range of a collection if something other than the default 0 to Infinity # is wanted. The semantics for from/to are the same as for #range # def self.constrain_size(collection_t, from, to) collection_t.size_type = range(from, to) collection_t end # Returns true if the given type t is of valid range parameter type (integer or literal default). def self.is_range_parameter?(t) t.is_a?(Integer) || t == 'default' || t == :default end end diff --git a/lib/puppet/pops/types/type_parser.rb b/lib/puppet/pops/types/type_parser.rb index fe6206bb1..cea59689a 100644 --- a/lib/puppet/pops/types/type_parser.rb +++ b/lib/puppet/pops/types/type_parser.rb @@ -1,430 +1,433 @@ # This class provides parsing of Type Specification from a string into the Type # Model that is produced by the Puppet::Pops::Types::TypeFactory. # # The Type Specifications that are parsed are the same as the stringified forms # of types produced by the {Puppet::Pops::Types::TypeCalculator TypeCalculator}. # # @api public class Puppet::Pops::Types::TypeParser # @api private TYPES = Puppet::Pops::Types::TypeFactory # @api public def initialize @parser = Puppet::Pops::Parser::Parser.new() @type_transformer = Puppet::Pops::Visitor.new(nil, "interpret", 0, 0) end # Produces a *puppet type* based on the given string. # # @example # parser.parse('Integer') # parser.parse('Array[String]') # parser.parse('Hash[Integer, Array[String]]') # # @param string [String] a string with the type expressed in stringified form as produced by the # {Puppet::Pops::Types::TypeCalculator#string TypeCalculator#string} method. # @return [Puppet::Pops::Types::PObjectType] a specialization of the PObjectType representing the type. # # @api public # def parse(string) # TODO: This state (@string) can be removed since the parse result of newer future parser # contains a Locator in its SourcePosAdapter and the Locator keeps the the string. # This way, there is no difference between a parsed "string" and something that has been parsed # earlier and fed to 'interpret' # @string = string model = @parser.parse_string(@string) if model interpret(model.current) else raise_invalid_type_specification_error end end # @api private def interpret(ast) result = @type_transformer.visit_this_0(self, ast) result = result.body if result.is_a?(Puppet::Pops::Model::Program) raise_invalid_type_specification_error unless result.is_a?(Puppet::Pops::Types::PAbstractType) result end # @api private def interpret_any(ast) @type_transformer.visit_this_0(self, ast) end # @api private def interpret_Object(o) raise_invalid_type_specification_error end # @api private def interpret_Program(o) interpret(o.body) end # @api private def interpret_QualifiedName(o) o.value end # @api private def interpret_LiteralString(o) o.value end # @api private def interpret_String(o) o end # @api private def interpret_LiteralDefault(o) :default end def interpret_LiteralInteger(o) o.value end def interpret_LiteralFloat(o) o.value end # @api private def interpret_QualifiedReference(name_ast) case name_ast.value when "integer" TYPES.integer when "float" TYPES.float when "numeric" TYPES.numeric when "string" TYPES.string when "enum" TYPES.enum when "boolean" TYPES.boolean when "pattern" TYPES.pattern when "regexp" TYPES.regexp when "data" TYPES.data when "array" TYPES.array_of_data when "hash" TYPES.hash_of_data when "class" TYPES.host_class() when "resource" TYPES.resource() when "collection" TYPES.collection() when "scalar" TYPES.scalar() when "catalogentry" TYPES.catalog_entry() when "undef" # Should not be interpreted as Resource type TYPES.undef() when "object" TYPES.object() when "variant" TYPES.variant() when "optional" TYPES.optional() when "ruby" TYPES.ruby_type() when "type" TYPES.type_type() when "tuple" TYPES.tuple() + + when "struct" + TYPES.struct() else TYPES.resource(name_ast.value) end end # @api private def interpret_AccessExpression(parameterized_ast) parameters = parameterized_ast.keys.collect { |param| interpret_any(param) } unless parameterized_ast.left_expr.is_a?(Puppet::Pops::Model::QualifiedReference) raise_invalid_type_specification_error end case parameterized_ast.left_expr.value when "array" case parameters.size when 1 when 2 size_type = if parameters[1].is_a?(Puppet::Pops::Types::PIntegerType) parameters[1].copy else assert_range_parameter(parameters[1]) TYPES.range(parameters[1], :default) end when 3 assert_range_parameter(parameters[1]) assert_range_parameter(parameters[2]) size_type = TYPES.range(parameters[1], parameters[2]) else raise_invalid_parameters_error("Array", "1 to 3", parameters.size) end assert_type(parameters[0]) t = TYPES.array_of(parameters[0]) t.size_type = size_type if size_type t when "hash" result = case parameters.size when 1 assert_type(parameters[0]) TYPES.hash_of(parameters[0]) when 2 assert_type(parameters[0]) assert_type(parameters[1]) TYPES.hash_of(parameters[1], parameters[0]) when 3 size_type = if parameters[2].is_a?(Puppet::Pops::Types::PIntegerType) parameters[2].copy else assert_range_parameter(parameters[2]) TYPES.range(parameters[2], :default) end assert_type(parameters[0]) assert_type(parameters[1]) TYPES.hash_of(parameters[1], parameters[0]) when 4 assert_range_parameter(parameters[2]) assert_range_parameter(parameters[3]) size_type = TYPES.range(parameters[2], parameters[3]) assert_type(parameters[0]) assert_type(parameters[1]) TYPES.hash_of(parameters[1], parameters[0]) else raise_invalid_parameters_error("Hash", "1 to 4", parameters.size) end result.size_type = size_type if size_type result when "collection" size_type = case parameters.size when 1 if parameters[0].is_a?(Puppet::Pops::Types::PIntegerType) parameters[0].copy else assert_range_parameter(parameters[0]) TYPES.range(parameters[0], :default) end when 2 assert_range_parameter(parameters[0]) assert_range_parameter(parameters[1]) TYPES.range(parameters[0], parameters[1]) else raise_invalid_parameters_error("Collection", "1 to 2", parameters.size) end result = TYPES.collection result.size_type = size_type result when "class" if parameters.size != 1 raise_invalid_parameters_error("Class", 1, parameters.size) end TYPES.host_class(parameters[0]) when "resource" if parameters.size == 1 TYPES.resource(parameters[0]) elsif parameters.size != 2 raise_invalid_parameters_error("Resource", "1 or 2", parameters.size) else TYPES.resource(parameters[0], parameters[1]) end when "regexp" # 1 parameter being a string, or regular expression raise_invalid_parameters_error("Regexp", "1", parameters.size) unless parameters.size == 1 TYPES.regexp(parameters[0]) when "enum" # 1..m parameters being strings raise_invalid_parameters_error("Enum", "1 or more", parameters.size) unless parameters.size > 1 TYPES.enum(*parameters) when "pattern" # 1..m parameters being strings or regular expressions raise_invalid_parameters_error("Pattern", "1 or more", parameters.size) unless parameters.size > 1 TYPES.pattern(*parameters) when "variant" # 1..m parameters being strings or regular expressions raise_invalid_parameters_error("Variant", "1 or more", parameters.size) unless parameters.size > 1 TYPES.variant(*parameters) when "tuple" # 1..m parameters being types (last two optionally integer or literal default raise_invalid_parameters_error("Tuple", "1 or more", parameters.size) unless parameters.size > 1 length = parameters.size if is_range_parameter?(parameters[-2]) # min, max specification min = parameters[-2] min = (min == :default || min == 'default') ? 0 : min assert_range_parameter(parameters[-1]) max = parameters[-1] max = max == :default ? nil : max parameters = parameters[0, length-2] elsif is_range_parameter?(parameters[-1]) min = parameters[-1] min = (min == :default || min == 'default') ? 0 : min max = nil parameters = parameters[0, length-1] end t = TYPES.tuple(*parameters) if min || max TYPES.constrain_size(t, min, max) end t when "integer" if parameters.size == 1 case parameters[0] when Integer TYPES.range(parameters[0], parameters[0]) when :default TYPES.integer # unbound end elsif parameters.size != 2 raise_invalid_parameters_error("Integer", "1 or 2", parameters.size) else TYPES.range(parameters[0] == :default ? nil : parameters[0], parameters[1] == :default ? nil : parameters[1]) end when "float" if parameters.size == 1 case parameters[0] when Integer, Float TYPES.float_range(parameters[0], parameters[0]) when :default TYPES.float # unbound end elsif parameters.size != 2 raise_invalid_parameters_error("Float", "1 or 2", parameters.size) else TYPES.float_range(parameters[0] == :default ? nil : parameters[0], parameters[1] == :default ? nil : parameters[1]) end when "string" size_type = case parameters.size when 1 if parameters[0].is_a?(Puppet::Pops::Types::PIntegerType) parameters[0].copy else assert_range_parameter(parameters[0]) TYPES.range(parameters[0], :default) end when 2 assert_range_parameter(parameters[0]) assert_range_parameter(parameters[1]) TYPES.range(parameters[0], parameters[1]) else raise_invalid_parameters_error("String", "1 to 2", parameters.size) end result = TYPES.string result.size_type = size_type result when "optional" if parameters.size != 1 raise_invalid_parameters_error("Optional", 1, parameters.size) end assert_type(parameters[0]) TYPES.optional(parameters[0]) when "object", "data", "catalogentry", "boolean", "scalar", "undef", "numeric" raise_unparameterized_type_error(parameterized_ast.left_expr) when "type" if parameters.size != 1 raise_invalid_parameters_error("Type", 1, parameters.size) end assert_type(parameters[0]) TYPES.type_type(parameters[0]) when "ruby" raise_invalid_parameters_error("Ruby", "1", parameters.size) unless parameters.size == 1 TYPES.ruby_type(parameters[0]) else # It is a resource such a File['/tmp/foo'] type_name = parameterized_ast.left_expr.value if parameters.size != 1 raise_invalid_parameters_error(type_name.capitalize, 1, parameters.size) end TYPES.resource(type_name, parameters[0]) end end private def assert_type(t) raise_invalid_type_specification_error unless t.is_a?(Puppet::Pops::Types::PObjectType) end def assert_range_parameter(t) raise_invalid_type_speification_error unless TYPES.is_range_parameter?(t) end def raise_invalid_type_specification_error raise Puppet::ParseError, "The expression <#{@string}> is not a valid type specification." end def raise_invalid_parameters_error(type, required, given) raise Puppet::ParseError, "Invalid number of type parameters specified: #{type} requires #{required}, #{given} provided" end def raise_unparameterized_type_error(ast) raise Puppet::ParseError, "Not a parameterized type <#{original_text_of(ast)}>" end def raise_unknown_type_error(ast) raise Puppet::ParseError, "Unknown type <#{original_text_of(ast)}>" end def original_text_of(ast) position = Puppet::Pops::Adapters::SourcePosAdapter.adapt(ast) position.extract_text() end end diff --git a/lib/puppet/pops/types/types.rb b/lib/puppet/pops/types/types.rb index 2f93b00f4..14b54843c 100644 --- a/lib/puppet/pops/types/types.rb +++ b/lib/puppet/pops/types/types.rb @@ -1,420 +1,420 @@ require 'rgen/metamodel_builder' # The Types model is a model of Puppet Language types. # # The exact relationship between types is not visible in this model wrt. the PDataType which is an abstraction # of Scalar, Array[Data], and Hash[Scalar, Data] nested to any depth. This means it is not possible to # infer the type by simply looking at the inheritance hierarchy. The {Puppet::Pops::Types::TypeCalculator} should # be used to answer questions about types. The {Puppet::Pops::Types::TypeFactory} should be used to create an instance # of a type whenever one is needed. # # The implementation of the Types model contains methods that are required for the type objects to behave as # expected when comparing them and using them as keys in hashes. (No other logic is, or should be included directly in # the model's classes). # # @api public # module Puppet::Pops::Types class PAbstractType < Puppet::Pops::Model::PopsObject abstract module ClassModule # Produce a deep copy of the type def copy Marshal.load(Marshal.dump(self)) end def hash self.class.hash end def ==(o) self.class == o.class end alias eql? == def to_s Puppet::Pops::Types::TypeCalculator.string(self) end end end # The type of types. # @api public class PType < PAbstractType contains_one_uni 'type', PAbstractType module ClassModule def hash [self.class, type].hash end def ==(o) self.class == o.class && type == o.type end end end # Base type for all types except {Puppet::Pops::Types::PType PType}, the type of types. # @api public class PObjectType < PAbstractType module ClassModule end end # @api public class PNilType < PObjectType end # A flexible data type, being assignable to its subtypes as well as PArrayType and PHashType with element type assignable to PDataType. # # @api public class PDataType < PObjectType module ClassModule def ==(o) self.class == o.class || o.class == PVariantType && o == Puppet::Pops::Types::TypeCalculator.data_variant() end end end # A flexible type describing an any? of other types # @api public class PVariantType < PObjectType contains_many_uni 'types', PAbstractType, :lowerBound => 1 module ClassModule def hash [self.class, Set.new(self.types)].hash end def ==(o) (self.class == o.class && Set.new(types) == Set.new(o.types)) || (o.class == PDataType && self == Puppet::Pops::Types::TypeCalculator.data_variant()) end end end # Type that is PDataType compatible, but is not a PCollectionType. # @api public class PScalarType < PObjectType end # A string type describing the set of strings having one of the given values # class PEnumType < PScalarType has_many_attr 'values', String, :lowerBound => 1 module ClassModule def hash [self.class, Set.new(self.values)].hash end def ==(o) self.class == o.class && Set.new(values) == Set.new(o.values) end end end # @api public class PNumericType < PScalarType end # @api public class PIntegerType < PNumericType has_attr 'from', Integer, :lowerBound => 0 has_attr 'to', Integer, :lowerBound => 0 module ClassModule # The integer type is enumerable when it defines a range include Enumerable # Returns Float.Infinity if one end of the range is unbound def size return 1.0 / 0.0 if from.nil? || to.nil? 1+(to-from).abs end def range f = from || -(1.0 / 0.0) t = to || (1.0 / 0.0) if f < t (f..t) else (t..f) end end # Returns Enumerator if no block is given # Returns self if size is infinity (does not yield) def each return self.to_enum unless block_given? return nil if from.nil? || to.nil? if to < from from.downto(to) {|x| yield x } else from.upto(to) {|x| yield x } end end def hash [self.class, from, to].hash end def ==(o) self.class == o.class && from == o.from && to == o.to end end end # @api public class PFloatType < PNumericType has_attr 'from', Float, :lowerBound => 0 has_attr 'to', Float, :lowerBound => 0 module ClassModule def hash [self.class, from, to].hash end def ==(o) self.class == o.class && from == o.from && to == o.to end end end # @api public class PStringType < PScalarType has_many_attr 'values', String, :lowerBound => 0, :upperBound => -1, :unique => true contains_one_uni 'size_type', PIntegerType module ClassModule def hash [self.class, self.size_type, Set.new(self.values)].hash end def ==(o) self.class == o.class && self.size_type == o.size_type && Set.new(values) == Set.new(o.values) end end end # @api public class PRegexpType < PScalarType has_attr 'pattern', String, :lowerBound => 1 has_attr 'regexp', Object, :derived => true module ClassModule def regexp_derived @_regexp = Regexp.new(pattern) unless @_regexp && @_regexp.source == pattern @_regexp end def hash [self.class, pattern].hash end def ==(o) self.class == o.class && pattern == o.pattern end end end # Represents a subtype of String that narrows the string to those matching the patterns # If specified without a pattern it is basically the same as the String type. # # @api public class PPatternType < PScalarType contains_many_uni 'patterns', PRegexpType module ClassModule def hash [self.class, Set.new(patterns)].hash end def ==(o) self.class == o.class && Set.new(patterns) == Set.new(o.patterns) end end end # @api public class PBooleanType < PScalarType end # @api public class PCollectionType < PObjectType contains_one_uni 'element_type', PAbstractType contains_one_uni 'size_type', PIntegerType module ClassModule def hash [self.class, element_type, size].hash end def ==(o) self.class == o.class && element_type == o.element_type && size_type == o.size_type end end end class PStructElement < Puppet::Pops::Model::PopsObject has_attr 'name', String, :lowerBound => 1 contains_one_uni 'type', PAbstractType module ClassModule def hash [self.class, type, name].hash end def ==(o) self.class == o.class && type == o.type && name == o.name end end end # @api public class PStructType < PObjectType contains_many_uni 'elements', PStructElement, :lowerBound => 1 has_attr 'hashed_elements', Object, :derived => true module ClassModule - def derived_hashed_elements + def hashed_elements_derived @_hashed ||= elements.reduce({}) {|memo, e| memo[e.name] = e.type; memo } @_hashed end def clear_hashed_elements @_hashed = nil end def hash [self.class, Set.new(elements)].hash end def ==(o) self.class == o.class && hashed_elements == o.hashed_elements end end end # @api public class PTupleType < PObjectType contains_many_uni 'types', PAbstractType, :lowerBound => 1 # If set, describes repetition of the last type in types contains_one_uni 'size_type', PIntegerType, :lowerBound => 0 module ClassModule def hash [self.class, size_type, Set.new(types)].hash end def ==(o) self.class == o.class && types == o.types && size_type == o.size_type end end end # @api public class PArrayType < PCollectionType module ClassModule def hash [self.class, self.element_type, self.size_type].hash end def ==(o) self.class == o.class && self.element_type == o.element_type && self.size_type == o.size_type end end end # @api public class PHashType < PCollectionType contains_one_uni 'key_type', PAbstractType module ClassModule def hash [self.class, key_type, self.element_type, self.size_type].hash end def ==(o) self.class == o.class && key_type == o.key_type && self.element_type == o.element_type && self.size_type == o.size_type end end end # @api public class PRubyType < PObjectType has_attr 'ruby_class', String module ClassModule def hash [self.class, ruby_class].hash end def ==(o) self.class == o.class && ruby_class == o.ruby_class end end end # Abstract representation of a type that can be placed in a Catalog. # @api public # class PCatalogEntryType < PObjectType end # Represents a (host-) class in the Puppet Language. # @api public # class PHostClassType < PCatalogEntryType has_attr 'class_name', String # contains_one_uni 'super_type', PHostClassType module ClassModule def hash [self.class, host_class].hash end def ==(o) self.class == o.class && class_name == o.class_name end end end # Represents a Resource Type in the Puppet Language # @api public # class PResourceType < PCatalogEntryType has_attr 'type_name', String has_attr 'title', String module ClassModule def hash [self.class, type_name, title].hash end def ==(o) self.class == o.class && type_name == o.type_name && title == o.title end end end # Represents a type that accept PNilType instead of the type parameter # required_type - is a short hand for Variant[T, Undef] # class POptionalType < PAbstractType contains_one_uni 'optional_type', PAbstractType module ClassModule def hash [self.class, optional_type].hash end def ==(o) self.class == o.class && optional_type == o.optional_type 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 430c2a9a4..c2d90f300 100644 --- a/spec/unit/pops/types/type_calculator_spec.rb +++ b/spec/unit/pops/types/type_calculator_spec.rb @@ -1,1382 +1,1438 @@ require 'spec_helper' require 'puppet/pops' describe 'The type calculator' do let(:calculator) { Puppet::Pops::Types::TypeCalculator.new() } - def int_range(from, to) + def range_t(from, to) t = Puppet::Pops::Types::PIntegerType.new t.from = from t.to = to t 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 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 types Puppet::Pops::Types end shared_context "types_setup" do def all_types [ Puppet::Pops::Types::PObjectType, 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::PRubyType, 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::PStructType, Puppet::Pops::Types::PTupleType, ] 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::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 result << (tmp = tuple_t(types::PDataType.new)) - tmp.size_type = int_range(0, nil) + 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 calculator.infer(1).class.should == Puppet::Pops::Types::PIntegerType end it 'large fixnum (or bignum depending on architecture) translates to PIntegerType' do calculator.infer(2**33).class.should == Puppet::Pops::Types::PIntegerType end it 'float translates to PFloatType' do calculator.infer(1.3).class.should == Puppet::Pops::Types::PFloatType end it 'string translates to PStringType' do calculator.infer('foo').class.should == Puppet::Pops::Types::PStringType end it 'inferred string type knows the string value' do t = calculator.infer('foo') t.class.should == Puppet::Pops::Types::PStringType t.values.should == ['foo'] end it 'boolean true translates to PBooleanType' do calculator.infer(true).class.should == Puppet::Pops::Types::PBooleanType end it 'boolean false translates to PBooleanType' do calculator.infer(false).class.should == Puppet::Pops::Types::PBooleanType end it 'regexp translates to PRegexpType' do calculator.infer(/^a regular expression$/).class.should == Puppet::Pops::Types::PRegexpType end it 'nil translates to PNilType' do calculator.infer(nil).class.should == Puppet::Pops::Types::PNilType end it ':undef translates to PNilType' do calculator.infer(:undef).class.should == Puppet::Pops::Types::PNilType end it 'an instance of class Foo translates to PRubyType[Foo]' do class Foo end t = calculator.infer(Foo.new) t.class.should == Puppet::Pops::Types::PRubyType t.ruby_class.should == 'Foo' end context 'array' do it 'translates to PArrayType' do calculator.infer([1,2]).class.should == Puppet::Pops::Types::PArrayType end it 'with fixnum values translates to PArrayType[PIntegerType]' do calculator.infer([1,2]).element_type.class.should == Puppet::Pops::Types::PIntegerType end it 'with 32 and 64 bit integer values translates to PArrayType[PIntegerType]' do calculator.infer([1,2**33]).element_type.class.should == Puppet::Pops::Types::PIntegerType end it 'Range of integer values are computed' do t = calculator.infer([-3,0,42]).element_type t.class.should == Puppet::Pops::Types::PIntegerType t.from.should == -3 t.to.should == 42 end it "Compound string values are computed" do t = calculator.infer(['a','b', 'c']).element_type t.class.should == Puppet::Pops::Types::PStringType t.values.should == ['a', 'b', 'c'] end it 'with fixnum and float values translates to PArrayType[PNumericType]' do calculator.infer([1,2.0]).element_type.class.should == Puppet::Pops::Types::PNumericType end it 'with fixnum and string values translates to PArrayType[PScalarType]' do calculator.infer([1,'two']).element_type.class.should == Puppet::Pops::Types::PScalarType end it 'with float and string values translates to PArrayType[PScalarType]' do calculator.infer([1.0,'two']).element_type.class.should == Puppet::Pops::Types::PScalarType end it 'with fixnum, float, and string values translates to PArrayType[PScalarType]' do calculator.infer([1, 2.0,'two']).element_type.class.should == Puppet::Pops::Types::PScalarType end it 'with fixnum and regexp values translates to PArrayType[PScalarType]' do calculator.infer([1, /two/]).element_type.class.should == Puppet::Pops::Types::PScalarType end it 'with string and regexp values translates to PArrayType[PScalarType]' do calculator.infer(['one', /two/]).element_type.class.should == Puppet::Pops::Types::PScalarType end it 'with string and symbol values translates to PArrayType[PObjectType]' do calculator.infer(['one', :two]).element_type.class.should == Puppet::Pops::Types::PObjectType end it 'with fixnum and nil values translates to PArrayType[PIntegerType]' do calculator.infer([1, nil]).element_type.class.should == Puppet::Pops::Types::PIntegerType end it 'with arrays of string values translates to PArrayType[PArrayType[PStringType]]' do et = calculator.infer([['first' 'array'], ['second','array']]) et.class.should == Puppet::Pops::Types::PArrayType et = et.element_type et.class.should == Puppet::Pops::Types::PArrayType et = et.element_type et.class.should == 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]]) et.class.should == Puppet::Pops::Types::PArrayType et = et.element_type et.class.should == Puppet::Pops::Types::PArrayType et = et.element_type et.class.should == 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' }]) et.class.should == Puppet::Pops::Types::PArrayType et = et.element_type et.class.should == Puppet::Pops::Types::PHashType et = et.element_type et.class.should == 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 }]) et.class.should == Puppet::Pops::Types::PArrayType et = et.element_type et.class.should == Puppet::Pops::Types::PHashType et = et.element_type et.class.should == Puppet::Pops::Types::PScalarType end end context 'hash' do it 'translates to PHashType' do calculator.infer({:first => 1, :second => 2}).class.should == Puppet::Pops::Types::PHashType end it 'with symbolic keys translates to PHashType[PRubyType[Symbol],value]' do k = calculator.infer({:first => 1, :second => 2}).key_type k.class.should == Puppet::Pops::Types::PRubyType k.ruby_class.should == 'Symbol' end it 'with string keys translates to PHashType[PStringType,value]' do calculator.infer({'first' => 1, 'second' => 2}).key_type.class.should == Puppet::Pops::Types::PStringType end it 'with fixnum values translates to PHashType[key,PIntegerType]' do calculator.infer({:first => 1, :second => 2}).element_type.class.should == Puppet::Pops::Types::PIntegerType end end end context 'patterns' do it "constructs a PPatternType" do t = pattern_t('a(b)c') t.class.should == Puppet::Pops::Types::PPatternType t.patterns.size.should == 1 t.patterns[0].class.should == Puppet::Pops::Types::PRegexpType t.patterns[0].pattern.should == 'a(b)c' t.patterns[0].regexp.match('abc')[1].should == 'b' end it "constructs a PStringType with multiple strings" do t = string_t('a', 'b', 'c', 'abc') t.values.should == ['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' calculator.string(calculator.common_type(r1, r2)).should == "File" r2 = Puppet::Pops::Types::PResourceType.new() r2.type_name = 'File' r2.title = '/tmp/foo' calculator.string(calculator.common_type(r1, r2)).should == "File" r1 = Puppet::Pops::Types::PResourceType.new() r1.type_name = 'File' r1.title = '/tmp/foo' calculator.string(calculator.common_type(r1, r2)).should == "File['/tmp/foo']" r1 = Puppet::Pops::Types::PResourceType.new() r1.type_name = 'File' r1.title = '/tmp/bar' calculator.string(calculator.common_type(r1, r2)).should == "File" r2 = Puppet::Pops::Types::PResourceType.new() r2.type_name = 'Package' r2.title = 'apache' calculator.string(calculator.common_type(r1, r2)).should == "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' calculator.string(calculator.common_type(r1, r2)).should == "Class[foo]" r2 = Puppet::Pops::Types::PHostClassType.new() r2.class_name = 'bar' calculator.string(calculator.common_type(r1, r2)).should == "Class" r2 = Puppet::Pops::Types::PHostClassType.new() calculator.string(calculator.common_type(r1, r2)).should == "Class" r1 = Puppet::Pops::Types::PHostClassType.new() calculator.string(calculator.common_type(r1, r2)).should == "Class" end it 'computes pattern commonality' do t1 = pattern_t('abc') t2 = pattern_t('xyz') common_t = calculator.common_type(t1,t2) common_t.class.should == Puppet::Pops::Types::PPatternType common_t.patterns.map { |pr| pr.pattern }.should == ['abc', 'xyz'] calculator.string(common_t).should == "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) common_t.should == 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) common_t.class.should == Puppet::Pops::Types::PVariantType Set.new(common_t.types).should == 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) common_t.class.should == Puppet::Pops::Types::PVariantType Set.new(common_t.types).should == Set.new([a_t1, a_t2]) end end context 'computes assignability' do include_context "types_setup" context "for Object, such that" do it 'all types are assignable to Object' do t = Puppet::Pops::Types::PObjectType.new() all_types.each { |t2| t2.new.should be_assignable_to(t) } end it 'Object is not assignable to anything but Object' do tested_types = all_types() - [Puppet::Pops::Types::PObjectType] t = Puppet::Pops::Types::PObjectType.new() tested_types.each { |t2| t.should_not 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| type_from_class(t2).should 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| variant_t(type_from_class(t2)).should 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| t.should_not 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| t.should_not 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::PObjectType, Puppet::Pops::Types::PDataType] - scalar_types t = Puppet::Pops::Types::PDataType.new() tested_types.each {|t2| t.should_not be_assignable_to(t2.new) } 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| t2.new.should 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| t.should_not be_assignable_to(t2.new) } end it 'Scalar is not assignable to any disjunct type' do tested_types = all_types - [Puppet::Pops::Types::PObjectType, Puppet::Pops::Types::PDataType] - scalar_types t = Puppet::Pops::Types::PScalarType.new() tested_types.each {|t2| t.should_not 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| t2.new.should 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| t.should_not be_assignable_to(t2.new) } end it 'Numeric is not assignable to any disjunct type' do tested_types = all_types - [ Puppet::Pops::Types::PObjectType, Puppet::Pops::Types::PDataType, Puppet::Pops::Types::PScalarType, ] - numeric_types t = Puppet::Pops::Types::PNumericType.new() tested_types.each {|t2| t.should_not 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| t2.new.should 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| t.should_not be_assignable_to(t2.new) } end it 'Collection is not assignable to any disjunct type' do tested_types = all_types - [Puppet::Pops::Types::PObjectType] - collection_types t = Puppet::Pops::Types::PCollectionType.new() tested_types.each {|t2| t.should_not 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| t.should_not be_assignable_to(t2.new) } end it 'Array is not assignable to any disjunct type' do tested_types = all_types - [ Puppet::Pops::Types::PObjectType, Puppet::Pops::Types::PDataType] - collection_types t = Puppet::Pops::Types::PArrayType.new() tested_types.each {|t2| t.should_not 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| t.should_not be_assignable_to(t2.new) } end it 'Hash is not assignable to any disjunct type' do tested_types = all_types - [ Puppet::Pops::Types::PObjectType, Puppet::Pops::Types::PDataType] - collection_types t = Puppet::Pops::Types::PHashType.new() tested_types.each {|t2| t.should_not be_assignable_to(t2.new) } 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| t.should_not be_assignable_to(t2.new) } end it 'Tuple is not assignable to any disjunct type' do tested_types = all_types - [ Puppet::Pops::Types::PObjectType, Puppet::Pops::Types::PDataType] - collection_types t = Puppet::Pops::Types::PTupleType.new() tested_types.each {|t2| t.should_not 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| t.should_not be_assignable_to(t2.new) } + end + + it 'Struct is not assignable to any disjunct type' do + tested_types = all_types - [ + Puppet::Pops::Types::PObjectType, + Puppet::Pops::Types::PDataType] - collection_types + t = Puppet::Pops::Types::PStructType.new() + tested_types.each {|t2| t.should_not 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 | ruby_type.should be_assignable_to(puppet_type) end end context 'when dealing with integer ranges' do it 'should accept an equal range' do - calculator.assignable?(int_range(2,5), int_range(2,5)).should == true + calculator.assignable?(range_t(2,5), range_t(2,5)).should == true end it 'should accept an equal reverse range' do - calculator.assignable?(int_range(2,5), int_range(5,2)).should == true + calculator.assignable?(range_t(2,5), range_t(5,2)).should == true end it 'should accept a narrower range' do - calculator.assignable?(int_range(2,10), int_range(3,5)).should == true + calculator.assignable?(range_t(2,10), range_t(3,5)).should == true end it 'should accept a narrower reverse range' do - calculator.assignable?(int_range(2,10), int_range(5,3)).should == true + calculator.assignable?(range_t(2,10), range_t(5,3)).should == true end it 'should reject a wider range' do - calculator.assignable?(int_range(3,5), int_range(2,10)).should == false + calculator.assignable?(range_t(3,5), range_t(2,10)).should == false end it 'should reject a wider reverse range' do - calculator.assignable?(int_range(3,5), int_range(10,2)).should == false + calculator.assignable?(range_t(3,5), range_t(10,2)).should == false end it 'should reject a partially overlapping range' do - calculator.assignable?(int_range(3,5), int_range(2,4)).should == false - calculator.assignable?(int_range(3,5), int_range(4,6)).should == false + calculator.assignable?(range_t(3,5), range_t(2,4)).should == false + calculator.assignable?(range_t(3,5), range_t(4,6)).should == false end it 'should reject a partially overlapping reverse range' do - calculator.assignable?(int_range(3,5), int_range(4,2)).should == false - calculator.assignable?(int_range(3,5), int_range(6,4)).should == false + calculator.assignable?(range_t(3,5), range_t(4,2)).should == false + calculator.assignable?(range_t(3,5), range_t(6,4)).should == 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') calculator.assignable?(p_t, p_s).should == true end it 'should accept a regexp matching a pattern' do p_t = pattern_t(/abc/) p_s = string_t('XabcY') calculator.assignable?(p_t, p_s).should == true end it 'should accept a pattern matching a pattern' do p_t = pattern_t(pattern_t('abc')) p_s = string_t('XabcY') calculator.assignable?(p_t, p_s).should == true end it 'should accept a regexp matching a pattern' do p_t = pattern_t(regexp_t('abc')) p_s = string_t('XabcY') calculator.assignable?(p_t, p_s).should == true end it 'should accept a string matching all patterns' do p_t = pattern_t('abc', 'ab', 'c') p_s = string_t('XabcY') calculator.assignable?(p_t, p_s).should == 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') calculator.assignable?(p_t, p_s).should == true end it 'should reject a string not matching any patterns' do p_t = pattern_t('abc', 'ab', 'c') p_s = string_t('XqqqY') calculator.assignable?(p_t, p_s).should == 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') calculator.assignable?(p_t, p_s).should == 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') calculator.assignable?(pattern, enum).should == true end end context 'when dealing with tuples' do it 'should accept matching tuples' do tuple1 = tuple_t(1,2) tuple2 = tuple_t(Integer,Integer) calculator.assignable?(tuple1, tuple2).should == true calculator.assignable?(tuple2, tuple1).should == 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) calculator.assignable?(tuple1, tuple2).should == false calculator.assignable?(tuple2, tuple1).should == 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) calculator.assignable?(tuple1, tuple2).should == true calculator.assignable?(tuple2, tuple1).should == 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) calculator.assignable?(tuple1, tuple2).should == true calculator.assignable?(tuple2, tuple1).should == 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) calculator.assignable?(tuple1, tuple2).should == true calculator.assignable?(tuple2, tuple1).should == false end it 'should accept matching tuples with optional entries' do tuple1 = tuple_t(1,2) factory.constrain_size(tuple1, 0, :default) tuple2 = tuple_t(Numeric,Numeric) factory.constrain_size(tuple2, 0, :default) calculator.assignable?(tuple1, tuple2).should == false calculator.assignable?(tuple2, tuple1).should == true end it 'should accept matching array' do tuple1 = tuple_t(1,2) array = array_t(Integer) factory.constrain_size(array, 2, 2) calculator.assignable?(tuple1, array).should == true calculator.assignable?(array, tuple1).should == true 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}) + calculator.assignable?(struct1, struct2).should == true + calculator.assignable?(struct2, struct1).should == 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}) + calculator.assignable?(struct1, struct2).should == false + calculator.assignable?(struct2, struct1).should == 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) + calculator.assignable?(struct1, hsh).should == true + calculator.assignable?(hsh, struct1).should == 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) calculator.assignable?(fooType, fooType).should == true calculator.assignable?(Foo, fooType).should == true calculator.assignable?(fooType, barType).should == true calculator.assignable?(Foo, barType).should == true calculator.assignable?(barType, fooType).should == false calculator.assignable?(Bar, fooType).should == 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') calculator.assignable?(hc1, hc2).should == 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') calculator.assignable?(hc1, hc2).should == 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') calculator.assignable?(hc1, hc2).should == 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() calculator.assignable?(hc1, hc2).should == 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') calculator.assignable?(r1, r2).should == true end it "should allow more specific resource assignment" do r1 = Puppet::Pops::Types::TypeFactory.resource() r2 = Puppet::Pops::Types::TypeFactory.resource('file') calculator.assignable?(r1, r2).should == true r2 = Puppet::Pops::Types::TypeFactory.resource('file', '/tmp/foo') calculator.assignable?(r1, r2).should == true r1 = Puppet::Pops::Types::TypeFactory.resource('file') calculator.assignable?(r1, r2).should == 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') calculator.assignable?(r1, r2).should == false r2 = Puppet::Pops::Types::TypeFactory.resource() calculator.assignable?(r1, r2).should == 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 Object and NilType' do calculator.instance?(Puppet::Pops::Types::PNilType.new(), nil).should == true calculator.instance?(Puppet::Pops::Types::PObjectType.new(), nil).should == true end it 'should not consider undef to be an instance of any other type than Object and NilType and Data' do types_to_test = all_types - [ Puppet::Pops::Types::PObjectType, Puppet::Pops::Types::PNilType, Puppet::Pops::Types::PDataType] types_to_test.each {|t| calculator.instance?(t.new, nil).should == false } types_to_test.each {|t| calculator.instance?(t.new, :undef).should == false } end it 'should consider fixnum instanceof PIntegerType' do calculator.instance?(Puppet::Pops::Types::PIntegerType.new(), 1).should == true end it 'should consider fixnum instanceof Fixnum' do calculator.instance?(Fixnum, 1).should == true end it 'should consider integer in range' do - range = int_range(0,10) + range = range_t(0,10) calculator.instance?(range, 1).should == true calculator.instance?(range, 10).should == true calculator.instance?(range, -1).should == false calculator.instance?(range, 11).should == false end it 'should consider string in length range' do range = factory.constrain_size(string_t, 1,3) calculator.instance?(range, 'a').should == true calculator.instance?(range, 'abc').should == true calculator.instance?(range, '').should == false calculator.instance?(range, 'abcd').should == false end it 'should consider array in length range' do range = factory.constrain_size(array_t(integer_t), 1,3) calculator.instance?(range, [1]).should == true calculator.instance?(range, [1,2,3]).should == true calculator.instance?(range, []).should == false calculator.instance?(range, [1,2,3,4]).should == false end it 'should consider hash in length range' do range = factory.constrain_size(hash_t(integer_t, integer_t), 1,2) calculator.instance?(range, {1=>1}).should == true calculator.instance?(range, {1=>1, 2=>2}).should == true calculator.instance?(range, {}).should == false calculator.instance?(range, {1=>1, 2=>2, 3=>3}).should == false end it 'should consider collection in length range for array ' do range = factory.constrain_size(collection_t, 1,3) calculator.instance?(range, [1]).should == true calculator.instance?(range, [1,2,3]).should == true calculator.instance?(range, []).should == false calculator.instance?(range, [1,2,3,4]).should == false end it 'should consider collection in length range for hash' do range = factory.constrain_size(collection_t, 1,2) calculator.instance?(range, {1=>1}).should == true calculator.instance?(range, {1=>1, 2=>2}).should == true calculator.instance?(range, {}).should == false calculator.instance?(range, {1=>1, 2=>2, 3=>3}).should == false end it 'should consider string matching enum as instanceof' do enum = enum_t('XS', 'S', 'M', 'L', 'XL', '0') calculator.instance?(enum, 'XS').should == true calculator.instance?(enum, 'S').should == true calculator.instance?(enum, 'XXL').should == false calculator.instance?(enum, '').should == false calculator.instance?(enum, '0').should == true calculator.instance?(enum, 0).should == 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) calculator.instance?(array, ['XS', 'S', 'XL']).should == true calculator.instance?(array, ['XS', 'S', 'XXL']).should == 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 = int_range(30, 50) + sizes = range_t(30, 50) array = array_t(variant_t(enum, sizes)) calculator.instance?(array, ['XS', 'S', 30, 50]).should == true calculator.instance?(array, ['XS', 'S', 'XXL']).should == false calculator.instance?(array, ['XS', 'S', 29]).should == false end context 'and t is Data' do it 'undef should be considered instance of Data' do calculator.instance?(data_t, :undef).should == true end it 'other symbols should not be considered instance of Data' do calculator.instance?(data_t, :love).should == false end it 'an empty array should be considered instance of Data' do calculator.instance?(data_t, []).should == true end it 'an empty hash should be considered instance of Data' do calculator.instance?(data_t, {}).should == true end it 'a hash with nil/undef data should be considered instance of Data' do calculator.instance?(data_t, {'a' => nil}).should == true calculator.instance?(data_t, {'a' => :undef}).should == true end it 'a hash with nil/undef key should not considered instance of Data' do calculator.instance?(data_t, {nil => 10}).should == false calculator.instance?(data_t, {:undef => 10}).should == false end it 'an array with undef entries should be considered instance of Data' do calculator.instance?(data_t, [:undef]).should == true calculator.instance?(data_t, [nil]).should == true end it 'an array with undef / data entries should be considered instance of Data' do calculator.instance?(data_t, [1, :undef, 'a']).should == true calculator.instance?(data_t, [1, nil, 'a']).should == true 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| calculator.type(c).class.should == Puppet::Pops::Types::PIntegerType end end it 'should yield \'PFloatType\' for Float' do calculator.type(Float).class.should == Puppet::Pops::Types::PFloatType end it 'should yield \'PBooleanType\' for FalseClass and TrueClass' do [FalseClass,TrueClass].each do |c| calculator.type(c).class.should == Puppet::Pops::Types::PBooleanType end end it 'should yield \'PNilType\' for NilClass' do calculator.type(NilClass).class.should == Puppet::Pops::Types::PNilType end it 'should yield \'PStringType\' for String' do calculator.type(String).class.should == Puppet::Pops::Types::PStringType end it 'should yield \'PRegexpType\' for Regexp' do calculator.type(Regexp).class.should == Puppet::Pops::Types::PRegexpType end it 'should yield \'PArrayType[PDataType]\' for Array' do t = calculator.type(Array) t.class.should == Puppet::Pops::Types::PArrayType t.element_type.class.should == Puppet::Pops::Types::PDataType end it 'should yield \'PHashType[PScalarType,PDataType]\' for Hash' do t = calculator.type(Hash) t.class.should == Puppet::Pops::Types::PHashType t.key_type.class.should == Puppet::Pops::Types::PScalarType t.element_type.class.should == Puppet::Pops::Types::PDataType end end context 'when representing the type as string' do it 'should yield \'Type\' for PType' do calculator.string(Puppet::Pops::Types::PType.new()).should == 'Type' end it 'should yield \'Object\' for PObjectType' do calculator.string(Puppet::Pops::Types::PObjectType.new()).should == 'Object' end it 'should yield \'Scalar\' for PScalarType' do calculator.string(Puppet::Pops::Types::PScalarType.new()).should == 'Scalar' end it 'should yield \'Boolean\' for PBooleanType' do calculator.string(Puppet::Pops::Types::PBooleanType.new()).should == 'Boolean' end it 'should yield \'Data\' for PDataType' do calculator.string(Puppet::Pops::Types::PDataType.new()).should == 'Data' end it 'should yield \'Numeric\' for PNumericType' do calculator.string(Puppet::Pops::Types::PNumericType.new()).should == 'Numeric' end it 'should yield \'Integer\' and from/to for PIntegerType' do int_T = Puppet::Pops::Types::PIntegerType calculator.string(int_T.new()).should == 'Integer' int = int_T.new() int.from = 1 int.to = 1 calculator.string(int).should == 'Integer[1, 1]' int = int_T.new() int.from = 1 int.to = 2 calculator.string(int).should == 'Integer[1, 2]' int = int_T.new() int.from = nil int.to = 2 calculator.string(int).should == 'Integer[default, 2]' int = int_T.new() int.from = 2 int.to = nil calculator.string(int).should == 'Integer[2, default]' end it 'should yield \'Float\' for PFloatType' do calculator.string(Puppet::Pops::Types::PFloatType.new()).should == 'Float' end it 'should yield \'Regexp\' for PRegexpType' do calculator.string(Puppet::Pops::Types::PRegexpType.new()).should == 'Regexp' end it 'should yield \'Regexp[/pat/]\' for parameterized PRegexpType' do t = Puppet::Pops::Types::PRegexpType.new() t.pattern = ('a/b') calculator.string(Puppet::Pops::Types::PRegexpType.new()).should == 'Regexp' end it 'should yield \'String\' for PStringType' do calculator.string(Puppet::Pops::Types::PStringType.new()).should == 'String' end it 'should yield \'String\' for PStringType with multiple values' do calculator.string(string_t('a', 'b', 'c')).should == 'String' end it 'should yield \'String\' and from/to for PStringType' do string_T = Puppet::Pops::Types::PStringType calculator.string(factory.constrain_size(string_T.new(), 1,1)).should == 'String[1, 1]' calculator.string(factory.constrain_size(string_T.new(), 1,2)).should == 'String[1, 2]' calculator.string(factory.constrain_size(string_T.new(), :default, 2)).should == 'String[default, 2]' calculator.string(factory.constrain_size(string_T.new(), 2, :default)).should == '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() calculator.string(t).should == 'Array[Integer]' end it 'should yield \'Collection\' and from/to for PCollectionType' do col = collection_t() calculator.string(factory.constrain_size(col.copy, 1,1)).should == 'Collection[1, 1]' calculator.string(factory.constrain_size(col.copy, 1,2)).should == 'Collection[1, 2]' calculator.string(factory.constrain_size(col.copy, :default, 2)).should == 'Collection[default, 2]' calculator.string(factory.constrain_size(col.copy, 2, :default)).should == 'Collection[2, default]' end it 'should yield \'Array\' and from/to for PArrayType' do arr = array_t(string_t) calculator.string(factory.constrain_size(arr.copy, 1,1)).should == 'Array[String, 1, 1]' calculator.string(factory.constrain_size(arr.copy, 1,2)).should == 'Array[String, 1, 2]' calculator.string(factory.constrain_size(arr.copy, :default, 2)).should == 'Array[String, default, 2]' calculator.string(factory.constrain_size(arr.copy, 2, :default)).should == '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()) calculator.string(t).should == '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()) calculator.string(t).should == 'Tuple[Integer, Integer, String]' end it 'should yield \'Tuple\' and from/to for PTupleType' do tuple_t = tuple_t(string_t) calculator.string(factory.constrain_size(tuple_t.copy, 1,1)).should == 'Tuple[String, 1, 1]' calculator.string(factory.constrain_size(tuple_t.copy, 1,2)).should == 'Tuple[String, 1, 2]' calculator.string(factory.constrain_size(tuple_t.copy, :default, 2)).should == 'Tuple[String, default, 2]' calculator.string(factory.constrain_size(tuple_t.copy, 2, :default)).should == 'Tuple[String, 2, default]' end + it 'should yield \'Struct\' and details for PStructType' do + struct_t = struct_t({'a'=>Integer, 'b'=>String}) + calculator.string(struct_t).should == "Struct[{'a'=>Integer, 'b'=>String}]" + struct_t = struct_t({}) + calculator.string(struct_t).should == "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() calculator.string(t).should == 'Hash[String, Integer]' end it 'should yield \'Hash\' and from/to for PHashType' do hsh = hash_t(string_t, string_t) calculator.string(factory.constrain_size(hsh.copy, 1,1)).should == 'Hash[String, String, 1, 1]' calculator.string(factory.constrain_size(hsh.copy, 1,2)).should == 'Hash[String, String, 1, 2]' calculator.string(factory.constrain_size(hsh.copy, :default, 2)).should == 'Hash[String, String, default, 2]' calculator.string(factory.constrain_size(hsh.copy, 2, :default)).should == 'Hash[String, String, 2, default]' end it "should yield 'Class' for a PHostClassType" do t = Puppet::Pops::Types::PHostClassType.new() calculator.string(t).should == 'Class' end it "should yield 'Class[x]' for a PHostClassType[x]" do t = Puppet::Pops::Types::PHostClassType.new() t.class_name = 'x' calculator.string(t).should == 'Class[x]' end it "should yield 'Resource' for a PResourceType" do t = Puppet::Pops::Types::PResourceType.new() calculator.string(t).should == 'Resource' end it 'should yield \'File\' for a PResourceType[\'File\']' do t = Puppet::Pops::Types::PResourceType.new() t.type_name = 'File' calculator.string(t).should == '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' calculator.string(t).should == "File['/tmp/foo']" end it "should yield 'Enum[s,...]' for a PEnumType[s,...]" do t = enum_t('a', 'b', 'c') calculator.string(t).should == "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') calculator.string(t).should == "Pattern[/a/]" calculator.string(t2).should == "Pattern[/a/, /b/, /c/]" end it "should escape special characters in the string for a PPatternType['pat',...]" do t = pattern_t('a/b') calculator.string(t).should == "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) calculator.string(t).should == "Variant[String, Integer, Pattern[/a/]]" 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 calculator.infer(Puppet::Pops::Types::PNilType.new() ).is_a?(ptype).should() == true calculator.infer(Puppet::Pops::Types::PDataType.new() ).is_a?(ptype).should() == true calculator.infer(Puppet::Pops::Types::PScalarType.new() ).is_a?(ptype).should() == true calculator.infer(Puppet::Pops::Types::PStringType.new() ).is_a?(ptype).should() == true calculator.infer(Puppet::Pops::Types::PNumericType.new() ).is_a?(ptype).should() == true calculator.infer(Puppet::Pops::Types::PIntegerType.new() ).is_a?(ptype).should() == true calculator.infer(Puppet::Pops::Types::PFloatType.new() ).is_a?(ptype).should() == true calculator.infer(Puppet::Pops::Types::PRegexpType.new() ).is_a?(ptype).should() == true calculator.infer(Puppet::Pops::Types::PBooleanType.new() ).is_a?(ptype).should() == true calculator.infer(Puppet::Pops::Types::PCollectionType.new()).is_a?(ptype).should() == true calculator.infer(Puppet::Pops::Types::PArrayType.new() ).is_a?(ptype).should() == true calculator.infer(Puppet::Pops::Types::PHashType.new() ).is_a?(ptype).should() == true calculator.infer(Puppet::Pops::Types::PRubyType.new() ).is_a?(ptype).should() == true calculator.infer(Puppet::Pops::Types::PHostClassType.new() ).is_a?(ptype).should() == true calculator.infer(Puppet::Pops::Types::PResourceType.new() ).is_a?(ptype).should() == true calculator.infer(Puppet::Pops::Types::PEnumType.new() ).is_a?(ptype).should() == true calculator.infer(Puppet::Pops::Types::PPatternType.new() ).is_a?(ptype).should() == true calculator.infer(Puppet::Pops::Types::PVariantType.new() ).is_a?(ptype).should() == true calculator.infer(Puppet::Pops::Types::PTupleType.new() ).is_a?(ptype).should() == true end it 'should infer PType as the type of all other types' do ptype = Puppet::Pops::Types::PType calculator.string(calculator.infer(Puppet::Pops::Types::PNilType.new() )).should == "Type[Undef]" calculator.string(calculator.infer(Puppet::Pops::Types::PDataType.new() )).should == "Type[Data]" calculator.string(calculator.infer(Puppet::Pops::Types::PScalarType.new() )).should == "Type[Scalar]" calculator.string(calculator.infer(Puppet::Pops::Types::PStringType.new() )).should == "Type[String]" calculator.string(calculator.infer(Puppet::Pops::Types::PNumericType.new() )).should == "Type[Numeric]" calculator.string(calculator.infer(Puppet::Pops::Types::PIntegerType.new() )).should == "Type[Integer]" calculator.string(calculator.infer(Puppet::Pops::Types::PFloatType.new() )).should == "Type[Float]" calculator.string(calculator.infer(Puppet::Pops::Types::PRegexpType.new() )).should == "Type[Regexp]" calculator.string(calculator.infer(Puppet::Pops::Types::PBooleanType.new() )).should == "Type[Boolean]" calculator.string(calculator.infer(Puppet::Pops::Types::PCollectionType.new())).should == "Type[Collection]" calculator.string(calculator.infer(Puppet::Pops::Types::PArrayType.new() )).should == "Type[Array[?]]" calculator.string(calculator.infer(Puppet::Pops::Types::PHashType.new() )).should == "Type[Hash[?, ?]]" calculator.string(calculator.infer(Puppet::Pops::Types::PRubyType.new() )).should == "Type[Ruby[?]]" calculator.string(calculator.infer(Puppet::Pops::Types::PHostClassType.new() )).should == "Type[Class]" calculator.string(calculator.infer(Puppet::Pops::Types::PResourceType.new() )).should == "Type[Resource]" calculator.string(calculator.infer(Puppet::Pops::Types::PEnumType.new() )).should == "Type[Enum]" calculator.string(calculator.infer(Puppet::Pops::Types::PVariantType.new() )).should == "Type[Variant]" calculator.string(calculator.infer(Puppet::Pops::Types::PPatternType.new() )).should == "Type[Pattern]" calculator.string(calculator.infer(Puppet::Pops::Types::PTupleType.new() )).should == "Type[Tuple]" 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() calculator.string(calculator.infer([int_t])).should == "Array[Type[Integer], 1, 1]" calculator.string(calculator.infer([int_t, string_t])).should == "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| calculator.infer(c).is_a?(Puppet::Pops::Types::PType).should() == true end end it 'should infer PType as the type of PType (meta regression short-circuit)' do calculator.infer(Puppet::Pops::Types::PType.new()).is_a?(Puppet::Pops::Types::PType).should() == 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) calculator.instance?(type_type_t, type_t).should == 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 calculator.instance?(type_type_t, type_t2).should == 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) calculator.instance?(Puppet::Pops::Types::PType.new, type_t).should == 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 calculator.enumerable(t).respond_to?(:each).should == 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 calculator.enumerable(t).should == nil t = Puppet::Pops::Types::PIntegerType.new() t.from = 1 t.to = nil calculator.enumerable(t).should == nil end it "all but Integer range are not enumerable" do [Object, Numeric, Float, String, Regexp, Array, Hash].each do |t| calculator.enumerable(calculator.type(t)).should == nil end end end context "when dealing with different types of inference" do it "an instance specific inference is produced by infer" do calculator.infer(['a','b']).element_type.values.should == ['a', 'b'] end it "a generic inference is produced using infer_generic" do calculator.infer_generic(['a','b']).element_type.values.should == [] end it "a generic result is created by generalize! given an instance specific result for an Array" do generic = calculator.infer(['a','b']) generic.element_type.values.should == ['a', 'b'] calculator.generalize!(generic) generic.element_type.values.should == [] 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}) generic.key_type.values.sort.should == ['a', 'b'] generic.element_type.from.should == 1 generic.element_type.to.should == 2 calculator.generalize!(generic) generic.key_type.values.should == [] generic.element_type.from.should == nil generic.element_type.to.should == nil end it "does not reduce by combining types when using infer_set" do element_type = calculator.infer(['a','b',1,2]).element_type element_type.class.should == Puppet::Pops::Types::PScalarType element_type = calculator.infer_set(['a','b',1,2]).element_type element_type.class.should == Puppet::Pops::Types::PVariantType element_type.types[0].class.should == Puppet::Pops::Types::PStringType element_type.types[1].class.should == Puppet::Pops::Types::PStringType element_type.types[2].class.should == Puppet::Pops::Types::PIntegerType element_type.types[3].class.should == 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 element_type.class.should == Puppet::Pops::Types::PStringType element_type = calculator.infer_set(['a',nil]).element_type element_type.class.should == Puppet::Pops::Types::PVariantType element_type.types[0].class.should == Puppet::Pops::Types::PStringType element_type.types[1].class.should == Puppet::Pops::Types::PNilType end end matcher :be_assignable_to do |type| calc = Puppet::Pops::Types::TypeCalculator.new match do |actual| calc.assignable?(type, actual) end failure_message_for_should do |actual| "#{calc.string(actual)} should be assignable to #{calc.string(type)}" end failure_message_for_should_not do |actual| "#{calc.string(actual)} is assignable to #{calc.string(type)} when it should not" end end end