diff --git a/lib/puppet/pops/binder/bindings_factory.rb b/lib/puppet/pops/binder/bindings_factory.rb index a5d3ca46a..7c8f02ac7 100644 --- a/lib/puppet/pops/binder/bindings_factory.rb +++ b/lib/puppet/pops/binder/bindings_factory.rb @@ -1,847 +1,847 @@ # A helper class that makes it easier to construct a Bindings model. # # The Bindings Model # ------------------ # The BindingsModel (defined in {Puppet::Pops::Binder::Bindings} is a model that is intended to be generally free from Ruby concerns. # This means that it is possible for system integrators to create and serialize such models using other technologies than # Ruby. This manifests itself in the model in that producers are described using instances of a `ProducerDescriptor` rather than # describing Ruby classes directly. This is also true of the type system where type is expressed using the {Puppet::Pops::Types} model # to describe all types. # # This class, the `BindingsFactory` is a concrete Ruby API for constructing instances of classes in the model. # # Named Bindings # -------------- # The typical usage of the factory is to call {named_bindings} which creates a container of bindings wrapped in a *build object* # equipped with convenience methods to define the details of the just created named bindings. # The returned builder is an instance of {Puppet::Pops::Binder::BindingsFactory::BindingsContainerBuilder BindingsContainerBuilder}. # # Binding # ------- # A Binding binds a type/name key to a producer of a value. A binding is conveniently created by calling `bind` on a # `BindingsContainerBuilder`. The call to bind, produces a binding wrapped in a build object equipped with convenience methods # to define the details of the just created binding. The returned builder is an instance of # {Puppet::Pops::Binder::BindingsFactory::BindingsBuilder BindingsBuilder}. # # Multibinding # ------------ # A multibinding works like a binding, but it requires an additional ID. It also places constraints on the type of the binding; # it must be a collection type (Hash or Array). # # Constructing and Contributing Bindings from Ruby # ------------------------------------------------ # The bindings system is used by referencing bindings symbolically; these are then specified in a Ruby file which is autoloaded # by Puppet. The entry point for user code that creates bindings is described in {Puppet::Bindings Bindings}. # That class makes use of a BindingsFactory, and the builder objects to make it easy to construct bindings. # # It is intended that a user defining bindings in Ruby should be able to use the builder object methods for the majority of tasks. # If something advanced is wanted, use of one of the helper class methods on the BuildingsFactory, and/or the # {Puppet::Pops::Types::TypeCalculator TypeCalculator} will be required to create and configure objects that are not handled by # the methods in the builder objects. # # Chaining of calls # ------------------ # Since all the build methods return the build object it is easy to stack on additional calls. The intention is to # do this in an order that is readable from left to right: `bind.string.name('thename').to(42)`, but there is nothing preventing # making the calls in some other order e.g. `bind.to(42).name('thename').string`, the second is quite unreadable but produces # the same result. # # For sake of human readability, the method `name` is alsp available as `named`, with the intention that it is used after a type, # e.g. `bind.integer.named('the meaning of life').to(42)` # # Methods taking blocks # ---------------------- # Several methods take an optional block. The block evaluates with the builder object as `self`. This means that there is no # need to chain the methods calls, they can instead be made in sequence - e.g. # # bind do # integer # named 'the meaning of life' # to 42 # end # # or mix the two styles # # bind do # integer.named 'the meaning of life' # to 42 # end # # Unwrapping the result # --------------------- # The result from all methods is a builder object. Call the method `model` to unwrap the constructed bindings model object. # # bindings = BindingsFactory.named_bindings('my named bindings') do # # bind things # end.model # # @example Create a NamedBinding with content # result = Puppet::Pops::Binder::BindingsFactory.named_bindings("mymodule::mybindings") do # bind.name("foo").to(42) # when_in_category("node", "kermit.example.com").bind.name("foo").to(43) # bind.string.name("site url").to("http://www.example.com") # end # result.model() # # @api public # module Puppet::Pops::Binder::BindingsFactory # Alias for the {Puppet::Pops::Types::TypeFactory TypeFactory}. This is also available as the method # `type_factory`. # T = Puppet::Pops::Types::TypeFactory # Abstract base class for bindings object builders. # Supports delegation of method calls to the BindingsFactory class methods for all methods not implemented # by a concrete builder. # # @abstract # class AbstractBuilder # The built model object. attr_reader :model # @param binding [Puppet::Pops::Binder::Bindings::AbstractBinding] The binding to build. # @api public def initialize(binding) @model = binding end # Provides convenient access to the Bindings Factory class methods. The intent is to provide access to the # methods that return producers for the purpose of composing more elaborate things than the builder convenience # methods support directly. # @api private # def method_missing(meth, *args, &block) factory = Puppet::Pops::Binder::BindingsFactory if factory.respond_to?(meth) factory.send(meth, *args, &block) else super end end end # A bindings builder for an AbstractBinding containing other AbstractBinding instances. # @api public class BindingsContainerBuilder < AbstractBuilder # Adds an empty binding to the container, and returns a builder for it for further detailing. # An optional block may be given which is evaluated using `instance_eval`. # @return [BindingsBuilder] the builder for the created binding # @api public # def bind(&block) binding = Puppet::Pops::Binder::Bindings::Binding.new() model.addBindings(binding) builder = BindingsBuilder.new(binding) builder.instance_eval(&block) if block_given? builder end # Binds a multibind with the given identity where later, the looked up result contains all # contributions to this key. An optional block may be given which is evaluated using `instance_eval`. # @param id [String] the multibind's id used when adding contributions # @return [MultibindingsBuilder] the builder for the created multibinding # @api public # def multibind(id, &block) binding = Puppet::Pops::Binder::Bindings::Multibinding.new() binding.id = id model.addBindings(binding) builder = MultibindingsBuilder.new(binding) builder.instance_eval(&block) if block_given? builder end # Adds a categorized bindings to this container. Returns a BindingsContainerBuilder to allow adding # bindings in the newly created container. An optional block may be given which is evaluated using `instance_eval`. # @param categorization [String] the name of the categorization e.g. 'node' # @param category_value [String] the value in that category e.g. 'kermit.example.com' # @return [BindingsContainerBuilder] the builder for the created categorized bindings container # @api public # def when_in_category(categorization, category_value, &block) when_in_categories({categorization => category_value}, &block) end # Adds a categorized bindings to this container. Returns a BindingsContainerBuilder to allow adding # bindings in the newly created container. # The result is that a processed request must match all the given categorizations # with the given values. An optional block may be given which is evaluated using `instance_eval`. # @param categories_hash Hash[String, String] a hash with categorization and categorization value entries # @return [BindingsContainerBuilder] the builder for the created categorized bindings container # @api public # def when_in_categories(categories_hash, &block) binding = Puppet::Pops::Binder::Bindings::CategorizedBindings.new() categories_hash.each do |k,v| pred = Puppet::Pops::Binder::Bindings::Category.new() pred.categorization = k pred.value = v binding.addPredicates(pred) end model.addBindings(binding) builder = BindingsContainerBuilder.new(binding) builder.instance_eval(&block) if block_given? builder end end # Builds a Binding via convenience methods. # # @api public # class BindingsBuilder < AbstractBuilder # @param binding [Puppet::Pops::Binder::Bindings::AbstractBinding] the binding to build. # @api public def initialize(binding) super binding data() end # Sets the name of the binding. # @param name [String] the name to bind. # @api public def name(name) model.name = name self end # Same as {#name}, but reads better in certain combinations. # @api public alias_method :named, :name # Sets the binding to be abstract (it must be overridden) # @api public def abstract model.abstract = true self end # Sets the binding to be override (it must override something) # @api public def override model.override = true self end # Makes the binding a multibind contribution to the given multibind id # @param id [String] the multibind id to contribute this binding to # @api public def in_multibind(id) model.multibind_id = id self end # Sets the type of the binding to the given type. # @note # This is only needed if something other than the default type `Data` is wanted, or if the wanted type is # not provided by one of the convenience methods {#array_of_data}, {#boolean}, {#float}, {#hash_of_data}, # {#integer}, {#literal}, {#pattern}, {#string}, or one of the collection methods {#array_of}, or {#hash_of}. # # To create a type, use the method {#type_factory}, to obtain the type. # @example creating a Hash with Integer key and Array[Integer] element type # tc = type_factory # type(tc.hash(tc.array_of(tc.integer), tc.integer) # @param type [Puppet::Pops::Types::PObjectType] the type to set for the binding # @api public # def type(type) model.type = type self end # Sets the type of the binding to Integer. # @return [Puppet::Pops::Types::PIntegerType] the type # @api public def integer() type(T.integer()) end # Sets the type of the binding to Float. # @return [Puppet::Pops::Types::PFloatType] the type # @api public def float() type(T.float()) end # Sets the type of the binding to Boolean. # @return [Puppet::Pops::Types::PBooleanType] the type # @api public def boolean() type(T.boolean()) end # Sets the type of the binding to String. # @return [Puppet::Pops::Types::PStringType] the type # @api public def string() type(T.string()) end # Sets the type of the binding to Pattern. - # @return [Puppet::Pops::Types::PPatternType] the type + # @return [Puppet::Pops::Types::PRegexpType] the type # @api public def pattern() type(T.pattern()) end # Sets the type of the binding to the abstract type Literal. # @return [Puppet::Pops::Types::PLiteralType] the type # @api public def literal() type(T.literal()) end # Sets the type of the binding to the abstract type Data. # @return [Puppet::Pops::Types::PDataType] the type # @api public def data() type(T.data()) end # Sets the type of the binding to Array[Data]. # @return [Puppet::Pops::Types::PArrayType] the type # @api public def array_of_data() type(T.array_of_data()) end # Sets the type of the binding to Array[T], where T is given. # @param t [Puppet::Pops::Types::PObjectType] the type of the elements of the array # @return [Puppet::Pops::Types::PArrayType] the type # @api public def array_of(t) type(T.array_of(t)) end # Sets the type of the binding to Hash[Literal, Data]. # @return [Puppet::Pops::Types::PHashType] the type # @api public def hash_of_data() type(T.hash_of_data()) end # Sets type of the binding to `Hash[Literal, t]`. # To also limit the key type, use {#type} and give it a fully specified # hash using {#type_factory} and then `hash_of(value_type, key_type)`. # @return [Puppet::Pops::Types::PHashType] the type # @api public def hash_of(t) type(T.hash_of(t)) end # Sets the type of the binding based on the given argument. # @overload instance_of(t) # The same as calling {#type} with `t`. # @param t [Puppet::Pops::Types::PObjectType] the type # @overload instance_of(o) # Infers the type from the given Ruby object and sets that as the type - i.e. "set the type # of the binding to be that of the given data object". # @param o [Object] the object to infer the type from # @overload instance_of(c) # @param c [Class] the Class to base the type on. # Sets the type based on the given ruby class. The result is one of the specific puppet types # if the class can be represented by a specific type, or the open ended PRubyType otherwise. # @overload instance_of(s) # The same as using a class, but instead of giving a class instance, the class is expressed using its fully # qualified name. This method of specifying the type allows late binding (the class does not have to be loaded # before it can be used in a binding). # @param s [String] the fully qualified classname to base the type on. # @return the resulting type # @api public # def instance_of(t) type(T.type_of(t)) end # Provides convenient access to the type factory. # This is intended to be used when methods taking a type as argument i.e. {#type}, {#array_of}, {#hash_of}, and {#instance_of}. # @note # The type factory is also available via the constant {T}. # @api public def type_factory Puppet::Pops::Types::TypeFactory end # Sets the binding's producer to a singleton producer, if given argument is a value, a literal producer is created for it. # To create a producer producing an instance of a class with lazy loading of the class, use {#to_instance}. # # @overload to(a_literal) # Sets a constant producer in the binding. # @overload to(a_class, *args) # Sets an Instantiating producer (producing an instance of the given class) # @overload to(a_producer_descriptor) # Sets the producer from the given producer descriptor # @return [BindingsBuilder] self # @api public # def to(producer, *args) case producer when Class producer = Puppet::Pops::Binder::BindingsFactory.instance_producer(producer.name, *args) when Puppet::Pops::Model::Expression producer = Puppet::Pops::Binder::BindingsFactory.evaluating_producer(producer) when Puppet::Pops::Binder::Bindings::ProducerDescriptor else # If given producer is not a producer, create a literal producer producer = Puppet::Pops::Binder::BindingsFactory.literal_producer(producer) end model.producer = producer self end # Sets the binding's producer to a producer of an instance of given class (a String class name, or a Class instance). # Use a string class name when lazy loading of the class is wanted. # # @overload to_instance(class_name, *args) # @param class_name [String] the name of the class to instantiate # @param args [Object] optional arguments to the constructor # @overload to_instance(a_class) # @param a_class [Class] the class to instantiate # @param args [Object] optional arguments to the constructor # def to_instance(type, *args) class_name = case type when Class type.name when String type else raise ArgumentError, "to_instance accepts String (a class name), or a Class.*args got: #{type.class}." end model.producer = Puppet::Pops::Binder::BindingsFactory.instance_producer(class_name, *args) end # Sets the binding's producer to a singleton producer # @overload to_producer(a_producer) # Sets the producer to an instantiated producer. The resulting model can not be serialized as a consequence as there # is no meta-model describing the specialized producer. Use this only in exceptional cases, or where there is never the # need to serialize the model. # @param a_producer [Puppet::Pops::Binder::Producers::Producer] an instantiated producer, not serializeable ! # # @overload to_producer(a_class, *args) # @param a_class [Class] the class to create an instance of # @param args [Object] the arguments to the given class' new # # @overload to_producer(a_producer_descriptor) # @param a_producer_descriptor [Puppet::Pops::Binder::Bindings::ProducerDescriptor] a descriptor # producing Puppet::Pops::Binder::Producers::Producer # # @api public # def to_producer(producer, *args) case producer when Class producer = Puppet::Pops::Binder::BindingsFactory.instance_producer(producer.name, *args) when Puppet::Pops::Binder::Bindings::ProducerDescriptor when Puppet::Pops::Binder::Producers::Producer # a custom producer instance producer = Puppet::Pops::Binder::BindingsFactory.literal_producer(producer) else raise ArgumentError, "Given producer argument is none of a producer descriptor, a class, or a producer" end metaproducer = Puppet::Pops::Binder::BindingsFactory.producer_producer(producer) model.producer = metaproducer self end # Sets the binding's producer to a series of producers. # Use this when you want to produce a different producer on each request for a producer # # @overload to_producer(a_producer) # Sets the producer to an instantiated producer. The resulting model can not be serialized as a consequence as there # is no meta-model describing the specialized producer. Use this only in exceptional cases, or where there is never the # need to serialize the model. # @param a_producer [Puppet::Pops::Binder::Producers::Producer] an instantiated producer, not serializeable ! # # @overload to_producer(a_class, *args) # @param a_class [Class] the class to create an instance of # @param args [Object] the arguments to the given class' new # # @overload to_producer(a_producer_descriptor) # @param a_producer_descriptor [Puppet::Pops::Binder::Bindings::ProducerDescriptor] a descriptor # producing Puppet::Pops::Binder::Producers::Producer # # @api public # def to_producer_series(producer, *args) case producer when Class producer = Puppet::Pops::Binder::BindingsFactory.instance_producer(producer.name, *args) when Puppet::Pops::Binder::Bindings::ProducerDescriptor when Puppet::Pops::Binder::Producers::Producer # a custom producer instance producer = Puppet::Pops::Binder::BindingsFactory.literal_producer(producer) else raise ArgumentError, "Given producer argument is none of a producer descriptor, a class, or a producer" end non_caching = Puppet::Pops::Binder::Bindings::NonCachingProducerDescriptor.new() non_caching.producer = producer metaproducer = Puppet::Pops::Binder::BindingsFactory.producer_producer(non_caching) non_caching = Puppet::Pops::Binder::Bindings::NonCachingProducerDescriptor.new() non_caching.producer = metaproducer model.producer = non_caching self end # Sets the binding's producer to a "non singleton" producer (each call to produce produces a new instance/copy). # @overload to_series_of(a_literal) # a constant producer # @overload to_series_of(a_class, *args) # Instantiating producer # @overload to_series_of(a_producer_descriptor) # a given producer # # @api public # def to_series_of(producer, *args) case producer when Class producer = Puppet::Pops::Binder::BindingsFactory.instance_producer(producer.name, *args) when Puppet::Pops::Binder::Bindings::ProducerDescriptor else # If given producer is not a producer, create a literal producer producer = Puppet::Pops::Binder::BindingsFactory.literal_producer(producer) end non_caching = Puppet::Pops::Binder::Bindings::NonCachingProducerDescriptor.new() non_caching.producer = producer model.producer = non_caching self end # Sets the binding's producer to one that performs a lookup of another key # @overload to_lookup_of(type, name) # @overload to_lookup_of(name) # @api public # def to_lookup_of(type, name=nil) unless name name = type type = Puppet::Pops::Types::TypeFactory.data() end model.producer = Puppet::Pops::Binder::BindingsFactory.lookup_producer(type, name) self end # Sets the binding's producer to a one that performs a lookup of another key and they applies hash lookup on # the result. # # @overload to_lookup_of(type, name) # @overload to_lookup_of(name) # @api public # def to_hash_lookup_of(type, name, key) model.producer = Puppet::Pops::Binder::BindingsFactory.hash_lookup_producer(type, name, key) self end # Sets the binding's producer to one that produces the first found lookup of another key # @param list_of_lookups [Array] array of arrays [type name], or just name (implies data) # @example # binder.bind().name('foo').to_first_found('fee', 'fum', 'extended-bar') # binder.bind().name('foo').to_first_found( # [T.ruby(ThisClass), 'fee'], # [T.ruby(ThatClass), 'fum'], # 'extended-bar') # @api public # def to_first_found(*list_of_lookups) producers = list_of_lookups.collect do |entry| if entry.is_a?(Array) case entry.size when 2 Puppet::Pops::Binder::BindingsFactory.lookup_producer(entry[0], entry[1]) when 1 Puppet::Pops::Binder::BindingsFactory.lookup_producer(Puppet::Pops::Types::TypeFactory.data(), entry[0]) else raise ArgumentError, "Not an array of [type, name], name, or [name]" end else Puppet::Pops::Binder::BindingsFactory.lookup_producer(T.data(), entry) end end model.producer = Puppet::Pops::Binder::BindingsFactory.first_found_producer(*producers) self end # Sets options to the producer. # See the respective producer for the options it supports. All producers supports the option `:transformer`, a # puppet or ruby lambda that is evaluated with the produced result as an argument. The ruby lambda gets scope and # value as arguments. # @note # A Ruby lambda is not cross platform safe. Use a puppet lambda if you want a bindings model that is. # # @api public def producer_options(options) options.each do |k, v| arg = Puppet::Pops::Binder::Bindings::NamedArgument.new() arg.name = k.to_s arg.value = v model.addProducer_args(arg) end self end end # A builder specialized for multibind - checks that type is Array or Hash based. A new builder sets the # multibinding to be of type Hash[Data]. # # @api public class MultibindingsBuilder < BindingsBuilder # Constraints type to be one of {Puppet::Pops::Types::PArrayType PArrayType}, or {Puppet::Pops::Types::PHashType PHashType}. # @raise [ArgumentError] if type constraint is not met. # @api public def type(type) unless type.class == Puppet::Pops::Types::PArrayType || type.class == Puppet::Pops::Types::PHashType raise ArgumentError, "Wrong type; only PArrayType, or PHashType allowed, got '#{type.to_s}'" end model.type = type self end # Overrides the default implementation that will raise an exception as a multibind requires a hash type. # Thus, if nothing else is requested, a multibind will be configured as Hash[Data]. # def data() hash_of_data() end end # Produces a ContributedBindings. # A ContributedBindings is used by bindings providers to return a set of named bindings. # # @param name [String] the name of the contributed bindings (for human use in messages/logs only) # @param named_bindings [Puppet::Pops::Binder::Bindings::NamedBindings, Array] the # named bindings to include # @param effective_categories [Puppet::Pops::Binder::Bindings::EffectiveCategories] the contributors opinion about categorization # this is used to ensure consistent use of categories. # def self.contributed_bindings(name, named_bindings, effective_categories) cb = Puppet::Pops::Binder::Bindings::ContributedBindings.new() cb.name = name named_bindings = [named_bindings] unless named_bindings.is_a?(Array) named_bindings.each {|b| cb.addBindings(b) } cb.effective_categories = effective_categories cb end # Creates a named binding container, the top bindings model object. # A NamedBindings is typically produced by a bindings provider. # # The created container is wrapped in a BindingsContainerBuilder for further detailing. # Unwrap the built result when done. # @api public # def self.named_bindings(name, &block) binding = Puppet::Pops::Binder::Bindings::NamedBindings.new() binding.name = name builder = BindingsContainerBuilder.new(binding) builder.instance_eval(&block) if block_given? builder end # This variant of {named_bindings} evaluates the given block as a method on an anonymous class, # thus, if the block defines methods or do something with the class itself, this does not pollute # the base class (BindingsContainerBuilder). # @api private # def self.safe_named_bindings(name, scope, &block) binding = Puppet::Pops::Binder::Bindings::NamedBindings.new() binding.name = name anon = Class.new(BindingsContainerBuilder) do def initialize(b) super b end end anon.send(:define_method, :_produce, block) builder = anon.new(binding) case block.arity when 0 builder._produce() when 1 builder._produce(scope) end builder end # Creates a literal/constant producer # @param value [Object] the value to produce # @return [Puppet::Pops::Binder::Bindings::ProducerDescriptor] a producer description # @api public # def self.literal_producer(value) producer = Puppet::Pops::Binder::Bindings::ConstantProducerDescriptor.new() producer.value = value producer end # Creates a non caching producer # @param producer [Puppet::Pops::Binder::Bindings::Producer] the producer to make non caching # @return [Puppet::Pops::Binder::Bindings::ProducerDescriptor] a producer description # @api public # def self.non_caching_producer(producer) p = Puppet::Pops::Binder::Bindings::NonCachingProducerDescriptor.new() p.producer = producer p end # Creates a producer producer # @param producer [Puppet::Pops::Binder::Bindings::Producer] a producer producing a Producer. # @return [Puppet::Pops::Binder::Bindings::ProducerDescriptor] a producer description # @api public # def self.producer_producer(producer) p = Puppet::Pops::Binder::Bindings::ProducerProducerDescriptor.new() p.producer = producer p end # Creates an instance producer # An instance producer creates a new instance of a class. # If the class implements the class method `inject` this method is called instead of `new` to allow further lookups # to take place. This is referred to as *assisted inject*. If the class method `inject` is missing, the regular `new` method # is called. # # @param class_name [String] the name of the class # @param args[Object] arguments to the class' `new` method. # @return [Puppet::Pops::Binder::Bindings::ProducerDescriptor] a producer description # @api public # def self.instance_producer(class_name, *args) p = Puppet::Pops::Binder::Bindings::InstanceProducerDescriptor.new() p.class_name = class_name args.each {|a| p.addArguments(a) } p end # Creates a Producer that looks up a value. # @param type [Puppet::Pops::Types::PObjectType] the type to lookup # @param name [String] the name to lookup # @return [Puppet::Pops::Binder::Bindings::ProducerDescriptor] a producer description # @api public def self.lookup_producer(type, name) p = Puppet::Pops::Binder::Bindings::LookupProducerDescriptor.new() p.type = type p.name = name p end # Creates a Hash lookup producer that looks up a hash value, and then a key in the hash. # # @return [Puppet::Pops::Binder::Bindings::ProducerDescriptor] a producer description # @param type [Puppet::Pops::Types::PObjectType] the type to lookup (i.e. a Hash of some key/value type). # @param name [String] the name to lookup # @param key [Object] the key to lookup in the looked up hash (type should comply with given key type). # @api public # def self.hash_lookup_producer(type, name, key) p = Puppet::Pops::Binder::Bindings::HashLookupProducerDescriptor.new() p.type = type p.name = name p.key = key p end # Creates a first-found producer that looks up from a given series of keys. The first found looked up # value will be produced. # @param producers [Array] the producers to consult in given order # @return [Puppet::Pops::Binder::Bindings::ProducerDescriptor] a producer descriptor # @api public def self.first_found_producer(*producers) p = Puppet::Pops::Binder::Bindings::FirstFoundProducerDescriptor.new() producers.each {|p2| p.addProducers(p2) } p end # Creates an evaluating producer that evaluates a puppet expression. # A puppet expression is most conveniently created by using the {Puppet::Pops::Parser::EvaluatingParser EvaluatingParser} as it performs # all set up and validation of the parsed source. Two convenience methods are used to parse an expression, or parse a ruby string # as a puppet string. See methods {puppet_expression}, {puppet_string} and {parser} for more information. # # @example producing a puppet expression # expr = puppet_string("Interpolated $fqdn", __FILE__) # # @param expression [Puppet::Pops::Model::Expression] a puppet DSL expression as producer by the eparser. # @return [Puppet::Pops::Binder::Bindings::ProducerDescriptor] a producer descriptor # @api public # def self.evaluating_producer(expression) p = Puppet::Pops::Binder::Bindings::EvaluatingProducerDescriptor.new() p.expression = expression p end # Creates an EffectiveCategories from a list of tuples `[categorization category ...]`, or `[[categorization category] ...]` # This method is used by backends to create a model of the effective categories. # @api public # def self.categories(tuple_array) result = Puppet::Pops::Binder::Bindings::EffectiveCategories.new() tuple_array.flatten.each_slice(2) do |c| cat = Puppet::Pops::Binder::Bindings::Category.new() cat.categorization = c[0] cat.value = c[1] result.addCategories(cat) end result end # Creates a NamedLayer. This is used by the bindings system to create a model of the layers. # # @api public # def self.named_layer(name, *bindings) result = Puppet::Pops::Binder::Bindings::NamedLayer.new() result.name = name bindings.each { |b| result.addBindings(b) } result end # Create a LayeredBindings. This is used by the bindings system to create a model of all given layers. # @param named_layers [Puppet::Pops::Binder::Bindings::NamedLayer] one or more named layers # @return [Puppet::Pops::Binder::Bindings::LayeredBindings] the constructed layered bindings. # @api public # def self.layered_bindings(*named_layers) result = Puppet::Pops::Binder::Bindings::LayeredBindings.new() named_layers.each {|b| result.addLayers(b) } result end # @return [Puppet::Pops::Parser::EvaluatingParser] a parser for puppet expressions def self.parser @parser ||= Puppet::Pops::Parser::EvaluatingParser.new() end # Parses and produces a puppet expression from the given string. # @param string [String] puppet source e.g. "1 + 2" # @param source_file [String] the source location, typically `__File__` # @return [Puppet::Pops::Model::Expression] an expression (that can be bound) # @api public # def self.puppet_expression(string, source_file) parser.parse_string(string, source_file).current end # Parses and produces a puppet string expression from the given string. # The string will automatically be quoted and special characters escaped. # As an example if given the (ruby) string "Hi\nMary" it is transformed to # the puppet string (illustrated with a ruby string) "\"Hi\\nMary\”" before being # parsed. # # @param string [String] puppet source e.g. "On node $!{fqdn}" # @param source_file [String] the source location, typically `__File__` # @return [Puppet::Pops::Model::Expression] an expression (that can be bound) # @api public # def self.puppet_string(string, source_file) parser.parse_string(parser.quote(string), source_file).current end end diff --git a/lib/puppet/pops/evaluator/access_operator.rb b/lib/puppet/pops/evaluator/access_operator.rb index 827a85155..3e0d3a86d 100644 --- a/lib/puppet/pops/evaluator/access_operator.rb +++ b/lib/puppet/pops/evaluator/access_operator.rb @@ -1,267 +1,287 @@ # 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 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("The [] operator is not applicable to the result of the LHS expression: #{o.class}", semantic.left_expr, scope) end def access_String(o, scope, keys) 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) 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 = 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 - # Speciaizes the Pattern p into itself p[], one regexp instance p[], or array of regexp instances - # p[, ]. + # Parameterizes a PRegexp Type with a pattern string or r ruby egexp # - def access_PPatternType(o, scope, keys) - if keys.size == 0 - return Marshal.load(Marshal.dump(o)) - end - result = keys.collect {|p| Regexp.new(keys[0]) } - result.size == 1 ? result.pop : result + def access_PRegexpType(o, scope, keys) + fail(Puppet::Pops::Issues::BAD_TYPE_SLICE_ARITY, o, :min=>1, :actual => keys.size) unless keys.size == 1 + 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) 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) 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) # 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. # def access_Hash(o, scope, keys) result = keys.collect {|k| o[k] } 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 + def access_PEnumType(o, scope, keys) + # TODO: Nice error handling + Puppet::Pops::Types::TypeFactory.enum(*keys) + end + + def access_PVariantType(o, scope, keys) + # TODO: Nice error handling + Puppet::Pops::Types::TypeFactory.variant(*keys) + end + + def access_PStringType(o, scope, keys) + # TODO: Nice error handling + begin + Puppet::Pops::Types::TypeFactory.string(*keys) + rescue StandardError => e + fail(Puppet::Pops::Issues::BAD_TYPE_SPECIALIZATION, o, :message => e.message) + end + end + + def access_PPatternType(o, scope, keys) + # TODO: Nice error handling + Puppet::Pops::Types::TypeFactory.pattern(*keys) + end + def access_PIntegerType(o, scope, keys) 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?(Numeric) || 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 # A Hash can create a new Hash type, one arg sets value type, two args sets key and value type in new type # It is not possible to create a collection of Hash types. # def access_PHashType(o, scope, keys) keys.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', :actual => k.class}) end end case keys.size when 1 result = Puppet::Pops::Types::PHashType.new() result.key_type = Marshal.load(Marshal.dump(o.key_type)) 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 else fail(Puppet::Pops::Issues::BAD_TYPE_SLICE_ARITY, @semantic, {:base_type => 'Hash', :min => 1, :max => 2, :actual => keys.size}) end 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) 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 => 'Array', :actual => keys[0].class}) end result = Puppet::Pops::Types::PArrayType.new() result.element_type = keys[0] result else fail(Puppet::Pops::Issues::BAD_TYPE_SLICE_ARITY, @semantic, {:base_type => 'Array', :min => 1, :actual => keys.size}) 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'] # => ERROR # Resource[File]['foo', 'bar'] # => [File[Foo], File[bar]] # Resource[File, 'foo', 'bar'] # => [File[foo], File[bar]] # Resource[???][] # => deep copy of the type # def access_PResourceType(o, scope, keys) 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 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 fail(Puppet::Pops::Issues::ILLEGAL_RESOURCE_SPECIALIZATION, @semantic.keys, {:actual => type_name.class}) end keys = [nil] if keys.size < 1 # if there was only a type_name and it was consumed result = keys.collect do |t| rtype = Puppet::Pops::Types::PResourceType.new() rtype.type_name = type_name rtype.title = 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) 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 result = keys.collect do |c| ctype = Puppet::Pops::Types::PHostClassType.new() 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/evaluator/evaluator_impl.rb b/lib/puppet/pops/evaluator/evaluator_impl.rb index 15760dae8..0174448c8 100644 --- a/lib/puppet/pops/evaluator/evaluator_impl.rb +++ b/lib/puppet/pops/evaluator/evaluator_impl.rb @@ -1,943 +1,952 @@ require 'rgen/ecore/ecore' require 'puppet/pops/evaluator/compare_operator' require 'puppet/pops/evaluator/relationship_operator' require 'puppet/pops/evaluator/access_operator' require 'puppet/pops/evaluator/closure' # This implementation of {Puppet::Pops::Evaluator} performs evaluation using the puppet 3.x runtime system # in a manner largely compatible with Puppet 3.x, but adds new features and introduces constraints. # # The evaluation uses _polymorphic dispatch_ which works by dispatching to the first found method named after # the class or one of its super-classes. The EvaluatorImpl itself mainly deals with evaluation (it currently # also handles assignment), and it uses a delegation pattern to more specialized handlers of some operators # that in turn use polymorphic dispatch; this to not clutter EvaluatorImpl with too much responsibility). # # Since a pattern is used, only the main entry points are fully documented. The parameters _o_ and _scope_ are # the same in all the polymorphic methods, (the type of the parameter _o_ is reflected in the method's name; # either the actual class, or one of its super classes). The _scope_ parameter is always the scope in which # the evaluation takes place. If nothing else is mentioned, the return is always the result of evaluation. # # See {Puppet::Pops::Visitable} and {Puppet::Pops::Visitor} for more information about # polymorphic calling. # class Puppet::Pops::Evaluator::EvaluatorImpl include Puppet::Pops::Utils # 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 # Reference to Issues name space makes it easier to refer to issues # (Issues are shared with the validator). # Issues = Puppet::Pops::Issues def initialize @@eval_visitor ||= Puppet::Pops::Visitor.new(self, "eval", 1, 1) @@lvalue_visitor ||= Puppet::Pops::Visitor.new(self, "lvalue", 1, 1) @@assign_visitor ||= Puppet::Pops::Visitor.new(self, "assign", 3, 3) @@string_visitor ||= Puppet::Pops::Visitor.new(self, "string", 1, 1) @@type_calculator ||= Puppet::Pops::Types::TypeCalculator.new() @@type_parser ||= Puppet::Pops::Types::TypeParser.new() @@compare_operator ||= Puppet::Pops::Evaluator::CompareOperator.new() @@relationship_operator ||= Puppet::Pops::Evaluator::RelationshipOperator.new() end # @api private def type_calculator @@type_calculator end # Polymorphic evaluate - calls eval_TYPE # # ## Polymorphic evaluate # Polymorphic evaluate calls a method on the format eval_TYPE where classname is the last # part of the class of the given _target_. A search is performed starting with the actual class, continuing # with each of the _target_ class's super classes until a matching method is found. # # # Description # Evaluates the given _target_ object in the given scope, optionally passing a block which will be # called with the result of the evaluation. # # @overload evaluate(target, scope, {|result| block}) # @param target [Object] evaluation target - see methods on the pattern assign_TYPE for actual supported types. # @param scope [Object] the runtime specific scope class where evaluation should take place # @return [Object] the result of the evaluation # # @api # def evaluate(target, scope) @@eval_visitor.visit_this_1(self, target, scope) end # Polymorphic assign - calls assign_TYPE # # ## Polymorphic assign # Polymorphic assign calls a method on the format assign_TYPE where TYPE is the last # part of the class of the given _target_. A search is performed starting with the actual class, continuing # with each of the _target_ class's super classes until a matching method is found. # # # Description # Assigns the given _value_ to the given _target_. The additional argument _o_ is the instruction that # produced the target/value tuple and it is used to set the origin of the result. # @param target [Object] assignment target - see methods on the pattern assign_TYPE for actual supported types. # @param value [Object] the value to assign to `target` # @param o [Puppet::Pops::Model::PopsObject] originating instruction # @param scope [Object] the runtime specific scope where evaluation should take place # # @api # def assign(target, value, o, scope) @@assign_visitor.visit_this_3(self, target, value, o, scope) end def lvalue(o, scope) @@lvalue_visitor.visit_this_1(self, o, scope) end def string(o, scope) @@string_visitor.visit_this_1(self, o, scope) end # Call a closure - Can only be called with a Closure (for now), may be refactored later # to also handle other types of calls (function calls are also handled by CallNamedFunction and CallMethod, they # could create similar objects to Closure, wait until other types of defines are instantiated - they may behave # as special cases of calls - i.e. 'new') # # @raise ArgumentError, if there are to many or too few arguments # @raise ArgumentError, if given closure is not a Puppet::Pops::Evaluator::Closure # def call(closure, args, scope) raise ArgumentError, "Can only call a Lambda" unless closure.is_a?(Puppet::Pops::Evaluator::Closure) pblock = closure.model parameters = pblock.parameters || [] raise ArgumentError, "Too many arguments: #{args.size} for #{parameters.size}" unless args.size <= parameters.size # associate values with parameters merged = parameters.zip(args) # calculate missing arguments missing = parameters.slice(args.size, parameters.size - args.size).select {|p| p.value.nil? } unless missing.empty? optional = parameters.count { |p| !p.value.nil? } raise ArgumentError, "Too few arguments; #{args.size} for #{optional > 0 ? ' min ' : ''}#{parameters.size - optional}" end evaluated = merged.collect do |m| # m can be one of # m = [Parameter{name => "name", value => nil], "given"] # | [Parameter{name => "name", value => Expression}, "given"] # # "given" is always an optional entry. If a parameter was provided then # the entry will be in the array, otherwise the m array will be a # single element.a = [] given_argument = m[1] argument_name = m[0].name default_expression = m[0].value value = if default_expression evaluate(default_expression, scope) else given_argument end [argument_name, value] end # Store the evaluated name => value associations in a new inner/local/ephemeral scope # (This is made complicated due to the fact that the implementation of scope is overloaded with # functionality and an inner ephemeral scope must be used (as opposed to just pushing a local scope # on a scope "stack"). # Ensure variable exists with nil value if error occurs. # Some ruby implementations does not like creating variable on return result = nil begin scope_memo = get_scope_nesting_level(scope) # change to create local scope_from - cannot give it file and line - that is the place of the call, not # "here" create_local_scope_from(Hash[evaluated], scope) result = evaluate(pblock.body, scope) ensure set_scope_nesting_level(scope, scope_memo) end result end protected def lvalue_VariableExpression(o, scope) # evaluate the name evaluate(o.expr, scope) end # Catches all illegal lvalues # def lvalue_Object(o, scope) fail(Issues::ILLEGAL_ASSIGNMENT, o) end # Assign value to named variable. # The '$' sign is never part of the name. # @example In Puppet DSL # $name = value # @param name [String] name of variable without $ # @param value [Object] value to assign to the variable # @param o [Puppet::Pops::Model::PopsObject] originating instruction # @param scope [Object] the runtime specific scope where evaluation should take place # @return [value] # def assign_String(name, value, o, scope) if name =~ /::/ fail(Issues::CROSS_SCOPE_ASSIGNMENT, o.left_expr, {:name => name}) end set_variable(name, value, o, scope) value end def assign_Numeric(n, value, o, scope) fail(Issues::ILLEGAL_NUMERIC_ASSIGNMENT, o.left_expr, {:varname => n.to_s}) end # Catches all illegal assignment (e.g. 1 = 2, {'a'=>1} = 2, etc) # def assign_Object(name, value, o, scope) fail(Issues::ILLEGAL_ASSIGNMENT, o) end def eval_Factory(o, scope) evaluate(o.current, scope) end # Evaluates any object not evaluated to something else to itself. def eval_Object o, scope o end # Allows nil to be used as a Nop. # Evaluates to nil # TODO: What is the difference between literal undef, nil, and nop? # def eval_NilClass(o, scope) nil end # Evaluates Nop to nil. # TODO: or is this the same as :undef # TODO: is this even needed as a separate instruction when there is a literal undef? def eval_Nop(o, scope) nil end # Captures all LiteralValues not handled elsewhere. # def eval_LiteralValue(o, scope) o.value end def eval_LiteralDefault(o, scope) :default end def eval_LiteralUndef(o, scope) :undef # TODO: or just use nil for this? end # A QualifiedReference (i.e. a capitalized qualified name such as Foo, or Foo::Bar) evaluates to a PType # def eval_QualifiedReference(o, scope) @@type_parser.interpret(o) end def eval_NotExpression(o, scope) ! is_true?(evaluate(o.expr, scope)) end def eval_UnaryMinusExpression(o, scope) - coerce_numeric(evaluate(o.expr, scope), o, scope) end # Abstract evaluation, returns array [left, right] with the evaluated result of left_expr and # right_expr # @return > array with result of evaluating left and right expressions # def eval_BinaryExpression o, scope [ evaluate(o.left_expr, scope), evaluate(o.right_expr, scope) ] end # Evaluates assignment with operators =, +=, -= and # # @example Puppet DSL # $a = 1 # $a += 1 # $a -= 1 # def eval_AssignmentExpression(o, scope) name = lvalue(o.left_expr, scope) value = evaluate(o.right_expr, scope) case o.operator when :'=' # regular assignment assign(name, value, o, scope) when :'+=' # if value does not exist, return RHS (note that type check has already been made so correct type is ensured) if !variable_exists?(name, scope) return value end begin # Delegate to calculate function to deal with check of LHS, and perform ´+´ as arithmetic or concatenation the # same way as ArithmeticExpression performs `+`. assign(name, calculate(get_variable_value(name, o, scope), value, :'+', o.left_expr, o.right_expr, scope), o, scope) rescue ArgumentError => e fail(Issues::APPEND_FAILED, o, {:message => e.message}) end when :'-=' # If an attempt is made to delete values from something that does not exists, the value is :undef (it is guaranteed to not # include any values the user wants deleted anyway :-) # if !variable_exists?(name, scope) return nil end begin # Delegate to delete function to deal with check of LHS, and perform deletion assign(name, delete(get_variable_value(name, o, scope), value), o, scope) rescue ArgumentError => e fail(Issues::APPEND_FAILED, o, {:message => e.message}) end else fail(Issues::UNSUPPORTED_OPERATOR, o, {:operator => o.operator}) end value end ARITHMETIC_OPERATORS = [:'+', :'-', :'*', :'/', :'%', :'<<', :'>>'] COLLECTION_OPERATORS = [:'+', :'-', :'<<'] # Handles binary expression where lhs and rhs are array/hash or numeric and operator is +, - , *, % / << >> # def eval_ArithmeticExpression(o, scope) unless ARITHMETIC_OPERATORS.include?(o.operator) fail(Issues::UNSUPPORTED_OPERATOR, o, {:operator => o.operator}) end left, right = eval_BinaryExpression(o, scope) begin result = calculate(left, right, o.operator, o.left_expr, o.right_expr, scope) rescue ArgumentError => e fail(Issues::RUNTIME_ERROR, o, {:detail => e.message}, e) end result end # Handles binary expression where lhs and rhs are array/hash or numeric and operator is +, - , *, % / << >> # def calculate(left, right, operator, left_o, right_o, scope) unless ARITHMETIC_OPERATORS.include?(operator) fail(Issues::UNSUPPORTED_OPERATOR, left_o.eContainer, {:operator => o.operator}) end if (left.is_a?(Array) || left.is_a?(Hash)) && COLLECTION_OPERATORS.include?(operator) # Handle operation on collections case operator when :'+' concatenate(left, right) when :'-' delete(left, right) when :'<<' unless left.is_a?(Array) fail(Issues::OPERATOR_NOT_APPLICABLE, left_o, {:operator => operator, :left_value => left}) end left + [right] end else # Handle operation on numeric left = coerce_numeric(left, left_o, scope) right = coerce_numeric(right, right_o, scope) begin if operator == :'%' && (left.is_a?(Float) || right.is_a?(Float)) # Deny users the fun of seeing severe rounding errors and confusing results fail(Issues::OPERATOR_NOT_APPLICABLE, left_o, {:operator => operator, :left_value => left}) end result = left.send(operator, right) rescue NoMethodError => e fail(Issues::OPERATOR_NOT_APPLICABLE, left_o, {:operator => operator, :left_value => left}) end result end end # Evaluates Puppet DSL ->, ~>, <-, and <~ def eval_RelationshipExpression(o, scope) # First level evaluation, reduction to basic data types or puppet types, the relationship operator then translates this # to the final set of references (turning strings into references, which can not naturally be done by the main evaluator since # all strings should not be turned into references. # real = eval_BinaryExpression(o, scope) @@relationship_operator.evaluate(real, o, scope) end # Evaluates x[key, key, ...] # def eval_AccessExpression(o, scope) left = evaluate(o.left_expr, scope) keys = o.keys.nil? ? [] : o.keys.collect {|key| evaluate(key, scope) } Puppet::Pops::Evaluator::AccessOperator.new(o).access(left, scope, *keys) end # Evaluates <, <=, >, >=, and == # def eval_ComparisonExpression o, scope left, right = eval_BinaryExpression o, scope begin # Left is a type if left.is_a?(Puppet::Pops::Types::PAbstractType) case o.operator when :'==' @@compare_operator.equals(left,right) when :'!=' ! @@compare_operator.equals(left,right) when :'<' # left can be assigned to right, but they are not equal @@type_calculator.assignable?(right, left) && ! @@compare_operator.equals(left,right) when :'<=' # left can be assigned to right @@type_calculator.assignable?(right, left) when :'>' # right can be assigned to left, but they are not equal @@type_calculator.assignable?(left,right) && ! @@compare_operator.equals(left,right) when :'>=' # right can be assigned to left @@type_calculator.assignable?(left, right) else fail(Issues::UNSUPPORTED_OPERATOR, o, {:operator => o.operator}) end else case o.operator when :'==' @@compare_operator.equals(left,right) when :'!=' ! @@compare_operator.equals(left,right) when :'<' @@compare_operator.compare(left,right) < 0 when :'<=' @@compare_operator.compare(left,right) <= 0 when :'>' @@compare_operator.compare(left,right) > 0 when :'>=' @@compare_operator.compare(left,right) >= 0 else fail(Issues::UNSUPPORTED_OPERATOR, o, {:operator => o.operator}) end end rescue ArgumentError => e fail(Issues::COMPARISON_NOT_POSSIBLE, o, { :operator => o.operator, :left_value => left, :right_value => right, :detail => e.message}) end end # Evaluates matching expressions with type, string or regexp rhs expression. # If RHS is a type, the =~ matches compatible (assignable?) type. # # @example # x =~ /abc.*/ # @example # x =~ "abc.*/" # @example # y = "abc" # x =~ "${y}.*" # @example # [1,2,3] =~ Array[Integer[1,10]] # @return [Boolean] if a match was made or not. Also sets $0..$n to matchdata in current scope. # def eval_MatchExpression o, scope left, pattern = eval_BinaryExpression o, scope + # matches RHS types as instance of for all types except a parameterized Regexp[R] if pattern.is_a?(Puppet::Pops::Types::PAbstractType) - matched = @@type_calculator.instance?(pattern, left) - else - begin - pattern = Regexp.new(pattern) unless pattern.is_a?(Regexp) - rescue StandardError => e - fail(Issues::MATCH_NOT_REGEXP, o.right_expr, {:detail => e.message}) - end - unless left.is_a?(String) - fail(Issues::MATCH_NOT_STRING, o.left_expr, {:left_value => left}) + if pattern.is_a?(Puppet::Pops::Types::PRegexpType) && pattern.pattern + # A qualified PRegexpType, get its ruby regexp + pattern = pattern.regexp + else + # evaluate as instance? + matched = @@type_calculator.instance?(pattern, left) + # convert match result to Boolean true, or false + return o.operator == :'=~' ? !!matched : !matched end + end - matched = pattern.match(left) # nil, or MatchData - set_match_data(matched, o, scope) # creates ephemeral + begin + pattern = Regexp.new(pattern) unless pattern.is_a?(Regexp) + rescue StandardError => e + fail(Issues::MATCH_NOT_REGEXP, o.right_expr, {:detail => e.message}) end + unless left.is_a?(String) + fail(Issues::MATCH_NOT_STRING, o.left_expr, {:left_value => left}) + end + + matched = pattern.match(left) # nil, or MatchData + set_match_data(matched, o, scope) # creates ephemeral # convert match result to Boolean true, or false o.operator == :'=~' ? !!matched : !matched end # Evaluates Puppet DSL `in` expression # def eval_InExpression o, scope left, right = eval_BinaryExpression o, scope @@compare_operator.include?(right, left) end # @example # $a and $b # b is only evaluated if a is true # def eval_AndExpression o, scope is_true?(evaluate(o.left_expr, scope)) ? is_true?(evaluate(o.right_expr, scope)) : false end # @example # a or b # b is only evaluated if a is false # def eval_OrExpression o, scope is_true?(evaluate(o.left_expr, scope)) ? true : is_true?(evaluate(o.right_expr, scope)) end # Evaluates each entry of the literal list and creates a new Array # @return [Array] with the evaluated content # def eval_LiteralList o, scope o.values.collect {|expr| evaluate(expr, scope)} end # Evaluates each entry of the literal hash and creates a new Hash. # @return [Hash] with the evaluated content # def eval_LiteralHash o, scope h = Hash.new o.entries.each {|entry| h[ evaluate(entry.key, scope)]= evaluate(entry.value, scope)} h end # Evaluates all statements and produces the last evaluated value # def eval_BlockExpression o, scope r = nil o.statements.each {|s| r = evaluate(s, scope)} r end # Performs optimized search over case option values, lazily evaluating each # until there is a match. If no match is found, the case expression's default expression # is evaluated (it may be nil or Nop if there is no default, thus producing nil). # If an option matches, the result of evaluating that option is returned. # @return [Object, nil] what a matched option returns, or nil if nothing matched. # def eval_CaseExpression(o, scope) # memo scope level before evaluating test - don't want a match in the case test to leak $n match vars # to expressions after the case expression. # with_guarded_scope(scope) do test = evaluate(o.test, scope) result = nil the_default = nil if o.options.find do |co| # the first case option that matches if co.values.find do |c| the_default = co.then_expr if c.is_a? Puppet::Pops::Model::LiteralDefault is_match?(test, evaluate(c, scope), c, scope) end result = evaluate(co.then_expr, scope) true # the option was picked end end result # an option was picked, and produced a result else evaluate(the_default, scope) # evaluate the default (should be a nop/nil) if there is no default). end end end # Evaluates a CollectExpression by transforming it into a 3x AST::Collection and then evaluating that. # This is done because of the complex API between compiler, indirector, backends, and difference between # collecting virtual resources and exported resources. # def eval_CollectExpression o, scope # The Collect Expression and its contained query expressions are implemented in such a way in # 3x that it is almost impossible to do anything about them (the AST objects are lazily evaluated, # and the built structure consists of both higher order functions and arrays with query expressions # that are either used as a predicate filter, or given to an indirection terminus (such as the Puppet DB # resource terminus). Unfortunately, the 3x implementation has many inconsistencies that the implementation # below carries forward. # collect_3x = Puppet::Pops::Model::AstTransformer.new().transform(o) collect_3x.evaluate(scope) # the 3x returns an instance of Collector (but it is only registered with the compiler at this # point and does not contain any valuable information (like the result, count of the result etc.) # Ensure that this object does not leak to the Puppet Program being evaluated. # nil end def eval_ParenthesizedExpression(o, scope) evaluate(o.expr, scope) end # This evaluates classes, nodes and resource type definitions to nil, since 3x: # instantiates them, and evaluates thwir parameters and body. This is acheived by # providing bridge AST classes in Puppet::Parser::AST::PopsBridge that bridges a # Pops Program and a Pops Expression. # # Since all Definitions are handled "out of band", they are treated as a no-op when # evaluated. # def eval_Definition(o, scope) nil end def eval_Program(o, scope) evaluate(o.body, scope) end # Produces Array[PObjectType], an array of resource references # def eval_ResourceExpression(o, scope) exported = o.exported virtual = o.virtual type_name = evaluate(o.type_name, scope) o.bodies.map do |body| titles = [evaluate(body.title, scope)].flatten evaluated_parameters = body.operations.map {|op| evaluate(op, scope) } create_resources(o, scope, virtual, exported, type_name, titles, evaluated_parameters) end.flatten.compact end def eval_ResourceOverrideExpression(o, scope) evaluated_resources = evaluate(o.resources, scope) evaluated_parameters = o.operations.map { |op| evaluate(op, scope) } create_resource_overrides(o, scope, [evaluated_resources].flatten, evaluated_parameters) evaluated_resources end # Produces 3x array of parameters def eval_AttributeOperation(o, scope) create_resource_parameter(o, scope, o.attribute_name, evaluate(o.value_expr, scope), o.operator) end # Sets default parameter values for a type, produces the type # def eval_ResourceDefaultsExpression(o, scope) type_name = o.type_ref.value # a QualifiedName's string value evaluated_parameters = o.operations.map {|op| evaluate(op, scope) } create_resource_defaults(o, scope, type_name, evaluated_parameters) # Produce the type evaluate(o.type_ref, scope) end # Evaluates function call by name. # def eval_CallNamedFunctionExpression(o, scope) # The functor expression is not evaluated, it is not possible to select the function to call # via an expression like $a() unless o.functor_expr.is_a? Puppet::Pops::Model::QualifiedName fail(Issues::ILLEGAL_EXPRESSION, o.functor_expr, {:feature=>'function name', :container => o}) end name = o.functor_expr.value assert_function_available(name, o, scope) evaluated_arguments = o.arguments.collect {|arg| evaluate(arg, scope) } # wrap lambda in a callable block if it is present evaluated_arguments << Puppet::Pops::Evaluator::Closure.new(self, o.lambda, scope) if o.lambda call_function(name, evaluated_arguments, o, scope) do |result| # prevent functions that are not r-value from leaking its return value rvalue_function?(name, o, scope) ? result : nil end end # Evaluation of CallMethodExpression handles a NamedAccessExpression functor (receiver.function_name) # def eval_CallMethodExpression(o, scope) unless o.functor_expr.is_a? Puppet::Pops::Model::NamedAccessExpression fail(Issues::ILLEGAL_EXPRESSION, o.functor_expr, {:feature=>'function accessor', :container => o}) end receiver = evaluate(o.functor_expr.left_expr, scope) name = o.functor_expr.right_expr unless name.is_a? Puppet::Pops::Model::QualifiedName fail(Issues::ILLEGAL_EXPRESSION, o.functor_expr, {:feature=>'function name', :container => o}) end name = name.value # the string function name assert_function_available(name, o, scope) evaluated_arguments = [receiver] + (o.arguments || []).collect {|arg| evaluate(arg, scope) } evaluated_arguments << Puppet::Pops::Evaluator::Closure.new(self, o.lambda, scope) if o.lambda call_function(name, evaluated_arguments, o, scope) do |result| # prevent functions that are not r-value from leaking its return value rvalue_function?(name, o, scope) ? result : nil end end # @example # $x ? { 10 => true, 20 => false, default => 0 } # def eval_SelectorExpression o, scope # memo scope level before evaluating test - don't want a match in the case test to leak $n match vars # to expressions after the selector expression. # with_guarded_scope(scope) do test = evaluate(o.left_expr, scope) selected = o.selectors.find do |s| candidate = evaluate(s.matching_expr, scope) candidate == :default || is_match?(test, candidate, s.matching_expr, scope) end if selected evaluate(selected.value_expr, scope) else nil end end end # Evaluates Puppet DSL `if` def eval_IfExpression o, scope with_guarded_scope(scope) do if is_true?(evaluate(o.test, scope)) evaluate(o.then_expr, scope) else evaluate(o.else_expr, scope) end end end # Evaluates Puppet DSL `unless` def eval_UnlessExpression o, scope with_guarded_scope(scope) do unless is_true?(evaluate(o.test, scope)) evaluate(o.then_expr, scope) else evaluate(o.else_expr, scope) end end end # Evaluates a variable (getting its value) # The evaluator is lenient; any expression producing a String is used as a name # of a variable. # def eval_VariableExpression o, scope # Evaluator is not too fussy about what constitutes a name as long as the result # is a String and a valid variable name # name = evaluate(o.expr, scope) # Should be caught by validation, but make this explicit here as well, or mysterious evaluation issues # may occur. case name when String when Numeric else fail(Issues::ILLEGAL_VARIABLE_EXPRESSION, o.expr) end # TODO: Check for valid variable name (Task for validator) # TODO: semantics of undefined variable in scope, this just returns what scope does == value or nil get_variable_value(name, o, scope) end # Evaluates double quoted strings that may contain interpolation # def eval_ConcatenatedString o, scope o.segments.collect {|expr| string(evaluate(expr, scope), scope)}.join end # If the wrapped expression is a QualifiedName, it is taken as the name of a variable in scope. # Note that this is different from the 3.x implementation, where an initial qualified name # is accepted. (e.g. `"---${var + 1}---"` is legal. This implementation requires such concrete # syntax to be expressed in a model as `(TextExpression (+ (Variable var) 1)` - i.e. moving the decision to # the parser. # # Semantics; the result of an expression is turned into a string, nil is silently transformed to empty # string. # @return [String] the interpolated result # def eval_TextExpression o, scope if o.expr.is_a?(Puppet::Pops::Model::QualifiedName) # TODO: formalize, when scope returns nil, vs error string(get_variable_value(o.expr.value, o, scope), scope) else string(evaluate(o.expr, scope), scope) end end def string_Object(o, scope) o.to_s end def string_Symbol(o, scope) case o when :undef '' else o.to_s end end def string_Array(o, scope) ['[', o.map {|e| string(e, scope)}.join(', '), ']'].join() end def string_Hash(o, scope) ['{', o.map {|k,v| string(k, scope) + " => " + string(v, scope)}.join(', '), '}'].join() end def string_Regexp(o, scope) ['/', o.source, '/'].join() end def string_PAbstractType(o, scope) @@type_calculator.string(o) end # Produces concatenation / merge of x and y. # # When x is an Array, y of type produces: # # * Array => concatenation `[1,2], [3,4] => [1,2,3,4]` # * Hash => concatenation of hash as array `[key, value, key, value, ...]` # * any other => concatenation of single value # # When x is a Hash, y of type produces: # # * Array => merge of array interpreted as `[key, value, key, value,...]` # * Hash => a merge, where entries in `y` overrides # * any other => error # # When x is something else, wrap it in an array first. # # When x is nil, an empty array is used instead. # # @note to concatenate an Array, nest the array - i.e. `[1,2], [[2,3]]` # # @overload concatenate(obj_x, obj_y) # @param obj_x [Object] object to wrap in an array and concatenate to; see other overloaded methods for return type # @param ary_y [Object] array to concatenate at end of `ary_x` # @return [Object] wraps obj_x in array before using other overloaded option based on type of obj_y # @overload concatenate(ary_x, ary_y) # @param ary_x [Array] array to concatenate to # @param ary_y [Array] array to concatenate at end of `ary_x` # @return [Array] new array with `ary_x` + `ary_y` # @overload concatenate(ary_x, hsh_y) # @param ary_x [Array] array to concatenate to # @param hsh_y [Hash] converted to array form, and concatenated to array # @return [Array] new array with `ary_x` + `hsh_y` converted to array # @overload concatenate (ary_x, obj_y) # @param ary_x [Array] array to concatenate to # @param obj_y [Object] non array or hash object to add to array # @return [Array] new array with `ary_x` + `obj_y` added as last entry # @overload concatenate(hsh_x, ary_y) # @param hsh_x [Hash] the hash to merge with # @param ary_y [Array] array interpreted as even numbered sequence of key, value merged with `hsh_x` # @return [Hash] new hash with `hsh_x` merged with `ary_y` interpreted as hash in array form # @overload concatenate(hsh_x, hsh_y) # @param hsh_x [Hash] the hash to merge to # @param hsh_y [Hash] hash merged with `hsh_x` # @return [Hash] new hash with `hsh_x` merged with `hsh_y` # @raise [ArgumentError] when `xxx_x` is neither an Array nor a Hash # @raise [ArgumentError] when `xxx_x` is a Hash, and `xxx_y` is neither Array nor Hash. # def concatenate(x, y) x = [x] unless x.is_a?(Array) || x.is_a?(Hash) case x when Array y = case y when Array then y when Hash then y.to_a else [y] end x + y # new array with concatenation when Hash y = case y when Hash then y when Array # Hash[[a, 1, b, 2]] => {} # Hash[a,1,b,2] => {a => 1, b => 2} # Hash[[a,1], [b,2]] => {[a,1] => [b,2]} # Hash[[[a,1], [b,2]]] => {a => 1, b => 2} # Use type calcultor to determine if array is Array[Array[?]], and if so use second form # of call t = @@type_calculator.infer(y) if t.element_type.is_a? Puppet::Pops::Types::PArrayType Hash[y] else Hash[*y] end else raise ArgumentError.new("Can only append Array or Hash to a Hash") end x.merge y # new hash with overwrite else raise ArgumentError.new("Can only append to an Array or a Hash.") end end # Produces the result x \ y (set difference) # When `x` is an Array, `y` is transformed to an array and then all matching elements removed from x. # When `x` is a Hash, all contained keys are removed from x as listed in `y` if it is an Array, or all its keys if it is a Hash. # The difference is returned. The given `x` and `y` are not modified by this operation. # @raise [ArgumentError] when `x` is neither an Array nor a Hash # def delete(x, y) result = x.dup case x when Array y = case y when Array then y when Hash then y.to_a else [y] end y.each {|e| result.delete(e) } when Hash y = case y when Array then y when Hash then y.keys else [y] end y.each {|e| result.delete(e) } else raise ArgumentError.new("Can only delete from an Array or Hash.") end result end # Implementation of case option matching. # # This is the type of matching performed in a case option, using == for every type # of value except regular expression where a match is performed. # def is_match? left, right, o, scope if right.is_a?(Regexp) return false unless left.is_a? String matched = right.match(left) set_match_data(matched, o, scope) # creates or clears ephemeral !!matched # convert to boolean elsif right.is_a?(Puppet::Pops::Types::PAbstractType) && !left.is_a?(Puppet::Pops::Types::PAbstractType) # right is a type and left is not - check if left is an instance of the given type # (The reverse is not terribly meaningful - computing which of the case options that first produces # an instance of a given type). # @@type_calculator.instance?(right, left) else # Handle equality the same way as the language '==' operator (case insensitive etc.) @@compare_operator.equals(left,right) end end def with_guarded_scope(scope) scope_memo = get_scope_nesting_level(scope) begin yield ensure set_scope_nesting_level(scope, scope_memo) end end end diff --git a/lib/puppet/pops/issues.rb b/lib/puppet/pops/issues.rb index bbb50cb14..4337708b7 100644 --- a/lib/puppet/pops/issues.rb +++ b/lib/puppet/pops/issues.rb @@ -1,388 +1,392 @@ # Defines classes to deal with issues, and message formatting and defines constants with Issues. # @api public # module Puppet::Pops::Issues # Describes an issue, and can produce a message for an occurrence of the issue. # class Issue # The issue code # @return [Symbol] attr_reader :issue_code # A block producing the message # @return [Proc] attr_reader :message_block # Names that must be bound in an occurrence of the issue to be able to produce a message. # These are the names in addition to requirements stipulated by the Issue formatter contract; i.e. :label`, # and `:semantic`. # attr_reader :arg_names # If this issue can have its severity lowered to :warning, :deprecation, or :ignored attr_writer :demotable # Configures the Issue with required arguments (bound by occurrence), and a block producing a message. def initialize issue_code, *args, &block @issue_code = issue_code @message_block = block @arg_names = args @demotable = true end # Returns true if it is allowed to demote this issue def demotable? @demotable end # Formats a message for an occurrence of the issue with argument bindings passed in a hash. # The hash must contain a LabelProvider bound to the key `label` and the semantic model element # bound to the key `semantic`. All required arguments as specified by `arg_names` must be bound # in the given `hash`. # @api public # def format(hash ={}) # Create a Message Data where all hash keys become methods for convenient interpolation # in issue text. msgdata = MessageData.new(*arg_names) begin # Evaluate the message block in the msg data's binding msgdata.format(hash, &message_block) rescue StandardError => e raise RuntimeError, "Error while reporting issue: #{issue_code}. #{e.message}", caller end end end # Provides a binding of arguments passed to Issue.format to method names available # in the issue's message producing block. # @api private # class MessageData def initialize *argnames singleton = class << self; self end argnames.each do |name| singleton.send(:define_method, name) do @data[name] end end end def format(hash, &block) @data = hash instance_eval &block end # Returns the label provider given as a key in the hash passed to #format. # If given an argument, calls #label on the label provider (caller would otherwise have to # call label.label(it) # def label(it = nil) raise "Label provider key :label must be set to produce the text of the message!" unless @data[:label] it.nil? ? @data[:label] : @data[:label].label(it) end # Returns the label provider given as a key in the hash passed to #format. # def semantic raise "Label provider key :semantic must be set to produce the text of the message!" unless @data[:semantic] @data[:semantic] end end # Defines an issue with the given `issue_code`, additional required parameters, and a block producing a message. # The block is evaluated in the context of a MessageData which provides convenient access to all required arguments # via accessor methods. In addition to accessors for specified arguments, these are also available: # * `label` - a `LabelProvider` that provides human understandable names for model elements and production of article (a/an/the). # * `semantic` - the model element for which the issue is reported # # @param issue_code [Symbol] the issue code for the issue used as an identifier, should be the same as the constant # the issue is bound to. # @param args [Symbol] required arguments that must be passed when formatting the message, may be empty # @param block [Proc] a block producing the message string, evaluated in a MessageData scope. The produced string # should not end with a period as additional information may be appended. # # @see MessageData # @api public # def self.issue (issue_code, *args, &block) Issue.new(issue_code, *args, &block) end # Creates a non demotable issue. # @see Issue.issue # def self.hard_issue(issue_code, *args, &block) result = Issue.new(issue_code, *args, &block) result.demotable = false result end # @comment Here follows definitions of issues. The intent is to provide a list from which yardoc can be generated # containing more detailed information / explanation of the issue. # These issues are set as constants, but it is unfortunately not possible for the created object to easily know which # name it is bound to. Instead the constant has to be repeated. (Alternatively, it could be done by instead calling # #const_set on the module, but the extra work required to get yardoc output vs. the extra effort to repeat the name # twice makes it not worth it (if doable at all, since there is no tag to artificially construct a constant, and # the parse tag does not produce any result for a constant assignment). # This is allowed (3.1) and has not yet been deprecated. # @todo configuration # NAME_WITH_HYPHEN = issue :NAME_WITH_HYPHEN, :name do "#{label.a_an_uc(semantic)} may not have a name containing a hyphen. The name '#{name}' is not legal" end # When a variable name contains a hyphen and these are illegal. # It is possible to control if a hyphen is legal in a name or not using the setting TODO # @todo describe the setting # @api public # @todo configuration if this is error or warning # VAR_WITH_HYPHEN = issue :VAR_WITH_HYPHEN, :name do "A variable name may not contain a hyphen. The name '#{name}' is not legal" end # A class, definition, or node may only appear at top level or inside other classes # @todo Is this really true for nodes? Can they be inside classes? Isn't that too late? # @api public # NOT_TOP_LEVEL = hard_issue :NOT_TOP_LEVEL do "Classes, definitions, and nodes may only appear at toplevel or inside other classes" end CROSS_SCOPE_ASSIGNMENT = hard_issue :CROSS_SCOPE_ASSIGNMENT, :name do "Illegal attempt to assign to '#{name}'. Cannot assign to variables in other namespaces" end # Assignment can only be made to certain types of left hand expressions such as variables. ILLEGAL_ASSIGNMENT = hard_issue :ILLEGAL_ASSIGNMENT do "Illegal attempt to assign to '#{label.a_an(semantic)}'. Not an assignable reference" end # Assignment cannot be made to numeric match result variables ILLEGAL_NUMERIC_ASSIGNMENT = issue :ILLEGAL_NUMERIC_ASSIGNMENT, :varname do "Illegal attempt to assign to the numeric match result variable '$#{varname}'. Numeric variables are not assignable" end APPEND_FAILED = issue :APPEND_FAILED, :message do "Append assignment += failed with error: #{message}" end DELETE_FAILED = issue :DELETE_FAILED, :message do "'Delete' assignment -= failed with error: #{message}" end # parameters cannot have numeric names, clashes with match result variables ILLEGAL_NUMERIC_PARAMETER = issue :ILLEGAL_NUMERIC_PARAMETER, :name do "The numeric parameter name '$#{varname}' cannot be used (clashes with numeric match result variables)" end # In certain versions of Puppet it may be allowed to assign to a not already assigned key # in an array or a hash. This is an optional validation that may be turned on to prevent accidental # mutation. # ILLEGAL_INDEXED_ASSIGNMENT = issue :ILLEGAL_INDEXED_ASSIGNMENT do "Illegal attempt to assign via [index/key]. Not an assignable reference" end # When indexed assignment ($x[]=) is allowed, the leftmost expression must be # a variable expression. # ILLEGAL_ASSIGNMENT_VIA_INDEX = hard_issue :ILLEGAL_ASSIGNMENT_VIA_INDEX do "Illegal attempt to assign to #{label.a_an(semantic)} via [index/key]. Not an assignable reference" end # For unsupported operators (e.g. -= in puppet 3). # UNSUPPORTED_OPERATOR = hard_issue :UNSUPPORTED_OPERATOR, :operator do "The operator '#{operator}' in #{label.a_an(semantic)} is not supported." end # For non applicable operators (e.g. << on Hash). # OPERATOR_NOT_APPLICABLE = hard_issue :OPERATOR_NOT_APPLICABLE, :operator, :left_value do "Operator '#{operator}' is not applicable to #{label.a_an(left_value)}." end COMPARISON_NOT_POSSIBLE = hard_issue :COMPARISON_NOT_POSSIBLE, :operator, :left_value, :right_value, :detail do "Comparison of: #{label(left_value)} #{operator} #{label(right_value)}, is not possible. Caused by '#{detail}'." end MATCH_NOT_REGEXP = hard_issue :MATCH_NOT_REGEXP, :detail do "Can not convert right match operand to a regular expression. Caused by '#{detail}'." end MATCH_NOT_STRING = hard_issue :MATCH_NOT_STRING, :left_value do "Left match operand must result in a String value. Got #{label.a_an(left_value)}." end # Some expressions/statements may not produce a value (known as right-value, or rvalue). # This may vary between puppet versions. # NOT_RVALUE = issue :NOT_RVALUE do "Invalid use of expression. #{label.a_an_uc(semantic)} does not produce a value" end # Appending to attributes is only allowed in certain types of resource expressions. # ILLEGAL_ATTRIBUTE_APPEND = hard_issue :ILLEGAL_ATTRIBUTE_APPEND, :name, :parent do "Illegal +> operation on attribute #{name}. This operator can not be used in #{label.a_an(parent)}" end ILLEGAL_NAME = hard_issue :ILLEGAL_NAME, :name do "Illegal name. The given name #{name} does not conform to the naming rule \\A((::)?[a-z0-9]\w*)(::[a-z0-9]\w*)*\\z" end # In case a model is constructed programmatically, it must create valid type references. # ILLEGAL_CLASSREF = hard_issue :ILLEGAL_CLASSREF, :name do "Illegal type reference. The given name '#{name}' does not conform to the naming rule" end # This is a runtime issue - storeconfigs must be on in order to collect exported. This issue should be # set to :ignore when just checking syntax. # @todo should be a :warning by default # RT_NO_STORECONFIGS = issue :RT_NO_STORECONFIGS do "You cannot collect exported resources without storeconfigs being set; the collection will be ignored" end # This is a runtime issue - storeconfigs must be on in order to export a resource. This issue should be # set to :ignore when just checking syntax. # @todo should be a :warning by default # RT_NO_STORECONFIGS_EXPORT = issue :RT_NO_STORECONFIGS_EXPORT do "You cannot collect exported resources without storeconfigs being set; the export is ignored" end # A hostname may only contain letters, digits, '_', '-', and '.'. # ILLEGAL_HOSTNAME_CHARS = hard_issue :ILLEGAL_HOSTNAME_CHARS, :hostname do "The hostname '#{hostname}' contains illegal characters (only letters, digits, '_', '-', and '.' are allowed)" end # A hostname may only contain letters, digits, '_', '-', and '.'. # ILLEGAL_HOSTNAME_INTERPOLATION = hard_issue :ILLEGAL_HOSTNAME_INTERPOLATION do "An interpolated expression is not allowed in a hostname of a node" end # Issues when an expression is used where it is not legal. # E.g. an arithmetic expression where a hostname is expected. # ILLEGAL_EXPRESSION = hard_issue :ILLEGAL_EXPRESSION, :feature, :container do "Illegal expression. #{label.a_an_uc(semantic)} is unacceptable as #{feature} in #{label.a_an(container)}" end # Issues when an expression is used where it is not legal. # E.g. an arithmetic expression where a hostname is expected. # ILLEGAL_VARIABLE_EXPRESSION = hard_issue :ILLEGAL_VARIABLE_EXPRESSION do "Illegal variable expression. #{label.a_an_uc(semantic)} did not produce a variable name (String or Numeric)." end # Issues when an expression is used illegaly in a query. # query only supports == and !=, and not <, > etc. # ILLEGAL_QUERY_EXPRESSION = hard_issue :ILLEGAL_QUERY_EXPRESSION do "Illegal query expression. #{label.a_an_uc(semantic)} cannot be used in a query" end # If an attempt is made to make a resource default virtual or exported. # NOT_VIRTUALIZEABLE = hard_issue :NOT_VIRTUALIZEABLE do "Resource Defaults are not virtualizable" end # When an attempt is made to use multiple keys (to produce a range in Ruby - e.g. $arr[2,-1]). # This is currently not supported, but may be in future versions # UNSUPPORTED_RANGE = issue :UNSUPPORTED_RANGE, :count do "Attempt to use unsupported range in #{label.a_an(semantic)}, #{count} values given for max 1" end DEPRECATED_NAME_AS_TYPE = issue :DEPRECATED_NAME_AS_TYPE, :name do "Resource references should now be capitalized. The given '#{name}' does not have the correct form" end ILLEGAL_RELATIONSHIP_OPERAND_TYPE = issue :ILLEGAL_RELATIONSHIP_OPERAND_TYPE, :operand do "Illegal relationship operand, can not form a relationship with #{label.a_an(operand)}. A Catalog type is required." end NOT_CATALOG_TYPE = issue :NOT_CATALOG_TYPE, :type do "Illegal relationship operand, can not form a relationship with something of type #{type}. A Catalog type is required." end BAD_STRING_SLICE_ARITY = issue :BAD_STRING_SLICE_ARITY, :actual do "String supports [] with one or two arguments. Got #{actual}" end BAD_ARRAY_SLICE_ARITY = issue :BAD_ARRAY_SLICE_ARITY, :actual do "Array supports [] with one or two arguments. Got #{actual}" end BAD_HASH_SLICE_ARITY = issue :BAD_HASH_SLICE_ARITY, :actual do "Hash supports [] with one or more arguments. Got #{actual}" end BAD_INTEGER_SLICE_ARITY = issue :BAD_INTEGER_SLICE_ARITY, :actual do "Integer supports [] with one or two arguments (from, to). Got #{actual}" end BAD_INTEGER_SLICE_TYPE = issue :BAD_INTEGER_SLICE_TYPE, :actual do "Integer [] requires all arguments to be integers (or default). Got #{actual}" end INTEGER_STEP_0 = issue :INTEGER_STEP_0 do "Integer [] can not step with a '0' increment." end BAD_TYPE_SLICE_TYPE = issue :BAD_TYPE_SLICE_TYPE, :base_type, :actual do "#{base_type}[] arguments must be types. Got #{actual}" end BAD_TYPE_SLICE_ARITY = issue :BAD_TYPE_SLICE_ARITY, :base_type, :min, :max, :actual do if max "#{base_type}[] requires #{min} to #{max} arguments. Got #{actual}" else "#{base_type}[] requires #{min} arguments. Got #{actual}" end end + BAD_TYPE_SPECIALIZATION = hard_issue :BAD_TYPE_SPECIALIZATION, :message do + "Error creating type specialization of #{label.a_an(semantic)}, #{message}" + end + ILLEGAL_TYPE_SPECIALIZATION = issue :ILLEGAL_TYPE_SPECIALIZATION, :kind do "Cannot specialize an already specialized #{kind} type" end ILLEGAL_RESOURCE_SPECIALIZATION = issue :ILLEGAL_RESOURCE_SPECIALIZATION, :actual do "First argument to Resource[] must be a resource type or a string. Got #{actual}." end NOT_NUMERIC = issue :NOT_NUMERIC, :value do "The value '#{value}' cannot be converted to Numeric." end UNKNOWN_FUNCTION = issue :UNKNOWN_FUNCTION, :name do "Unknown function: '#{name}'." end UNKNOWN_VARIABLE = issue :UNKNOWN_VARIABLE, :name do "Unknown variable: '#{name}'." end RUNTIME_ERROR = issue :RUNTIME_ERROR, :detail do "Error while evaluating #{label.a_an(semantic)}, #{detail}" end UNKNOWN_RESOURCE_TYPE = issue :UNKNOWN_RESOURCE_TYPE, :type_name do "Resource type not found: #{type_name.capitalize}" end UNKNOWN_RESOURCE = issue :UNKNOWN_RESOURCE, :type_name, :title do "Resource not found: #{type_name.capitalize}['#{title}']" end UNKNOWN_RESOURCE_PARAMETER = issue :UNKNOWN_RESOURCE_PARAMETER, :type_name, :title, :param_name do "The resource #{type_name.capitalize}['#{title}'] does not have a parameter called '#{param_name}'" end end diff --git a/lib/puppet/pops/types/class_loader.rb b/lib/puppet/pops/types/class_loader.rb index abacd704e..d2b3a327b 100644 --- a/lib/puppet/pops/types/class_loader.rb +++ b/lib/puppet/pops/types/class_loader.rb @@ -1,118 +1,118 @@ require 'rgen/metamodel_builder' # The ClassLoader provides a Class instance given a class name or a meta-type. # If the class is not already loaded, it is loaded using the Puppet Autoloader. # This means it can load a class from a gem, or from puppet modules. # class Puppet::Pops::Types::ClassLoader @autoloader = Puppet::Util::Autoload.new("ClassLoader", "", :wrap => false) # Returns a Class given a fully qualified class name. # Lookup of class is never relative to the calling namespace. # @param name [String, Array, Array, Puppet::Pops::Types::PObjectType] A fully qualified # class name String (e.g. '::Foo::Bar', 'Foo::Bar'), a PObjectType, or a fully qualified name in Array form where each part # is either a String or a Symbol, e.g. `%w{Puppetx Puppetlabs SomeExtension}`. # @return [Class, nil] the looked up class or nil if no such class is loaded # @raise ArgumentError If the given argument has the wrong type # @api public # def self.provide(name) case name when String provide_from_string(name) when Array provide_from_name_path(name.join('::'), name) when Puppet::Pops::Types::PObjectType, Puppet::Pops::Types::PType provide_from_type(name) else raise ArgumentError, "Cannot provide a class from a '#{name.class.name}'" end end private def self.provide_from_type(type) case type when Puppet::Pops::Types::PRubyType provide_from_string(type.ruby_class) when Puppet::Pops::Types::PBooleanType # There is no other thing to load except this Enum meta type RGen::MetamodelBuilder::MMBase::Boolean when Puppet::Pops::Types::PType # TODO: PType should have a type argument (a PObjectType) Class # Although not expected to be the first choice for getting a concrete class for these # types, these are of value if the calling logic just has a reference to type. # when Puppet::Pops::Types::PArrayType ; Array when Puppet::Pops::Types::PHashType ; Hash - when Puppet::Pops::Types::PPatternType ; Regexp + when Puppet::Pops::Types::PRegexpType ; Regexp when Puppet::Pops::Types::PIntegerType ; Integer when Puppet::Pops::Types::PStringType ; String when Puppet::Pops::Types::PFloatType ; Float when Puppet::Pops::Types::PNilType ; NilClass else nil end end def self.provide_from_string(name) name_path = name.split('::') # always from the root, so remove an empty first segment if name_path[0].empty? name_path = name_path[1..-1] end provide_from_name_path(name, name_path) end def self.provide_from_name_path(name, name_path) # If class is already loaded, try this first result = find_class(name_path) unless result.is_a?(Class) # Attempt to load it using the auto loader loaded_path = nil if paths_for_name(name).find {|path| loaded_path = path; @autoloader.load(path) } result = find_class(name_path) unless result.is_a?(Class) raise RuntimeError, "Loading of #{name} using relative path: '#{loaded_path}' did not create expected class" end end end return nil unless result.is_a?(Class) result end def self.find_class(name_path) name_path.reduce(Object) do |ns, name| begin ns.const_get(name) rescue NameError return nil end end end def self.paths_for_name(fq_name) [de_camel(fq_name), downcased_path(fq_name)] end def self.downcased_path(fq_name) fq_name.to_s.gsub(/::/, '/').downcase end def self.de_camel(fq_name) fq_name.to_s.gsub(/::/, '/'). gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2'). gsub(/([a-z\d])([A-Z])/,'\1_\2'). tr("-", "_"). downcase end end \ No newline at end of file diff --git a/lib/puppet/pops/types/type_calculator.rb b/lib/puppet/pops/types/type_calculator.rb index 3d60bd367..34ab49246 100644 --- a/lib/puppet/pops/types/type_calculator.rb +++ b/lib/puppet/pops/types/type_calculator.rb @@ -1,732 +1,859 @@ # 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 literal types, as well as Array with an element type compatible with Data, and Hash with key # compatible with Literal 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 literal. # # Inference # --------- # The `infer(o)` method infers a Puppet type for literal Ruby objects, and for Arrays and Hashes. # # 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. # # 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 PLiteralType. 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 # # @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 initialize @@assignable_visitor ||= Puppet::Pops::Visitor.new(nil,"assignable",1,1) @@infer_visitor ||= Puppet::Pops::Visitor.new(nil,"infer",0,0) @@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) 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::PLiteralType.new() @data_hash = h @data_t = Types::PDataType.new() @literal_t = Types::PLiteralType.new() @numeric_t = Types::PNumericType.new() @t = Types::PObjectType.new() end # Convenience method to get a data type for comparisons # @api private the returned value may not be contained in another element # def data @data_t end # 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 if is_pnil?(t2) return true end 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 '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::PPatternType.new() + 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 literal keys and data values type = Types::PHashType.new() type.key_type = Types::PLiteralType.new() type.element_type = Types::PDataType.new() else type = Types::PRubyType.new() type.ruby_class = c.name end type end # Answers 'what is the Puppet Type of o' # @api public # def infer(o) @@infer_visitor.visit_this_0(self, o) end # Answers 'is o an instance of type t' # @api public # def instance?(t, o) assignable?(t, infer(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 hot-classes, reduce to PHostClass[] (since one was not assignable to the other) + # 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, narrow 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 + 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() + t.patterns = t1.patterns | t2.patterns + 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_literal?(t1, t2) return Types::PLiteralType.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) - Types::PStringType.new() + t = Types::PStringType.new() + t.addValues(o) + t end # @api private def infer_Float(o) Types::PFloatType.new() 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) - Types::PPatternType.new() + t = Types::PRegexpType.new() + t.pattern = o.source + t end # @api private def infer_NilClass(o) Types::PNilType.new() 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 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 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_PLiteralType(t, t2) t2.is_a?(Types::PLiteralType) 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 # @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) + # A variant is assignable if t2 is assignable to any of its types + t.types.any? { |option_t| assignable?(option_t, t2) } + end + + 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) - t2.is_a?(Types::PStringType) + if t.values.empty? + # A general string is assignable by any other string, or pattern restricted string + t2.is_a?(Types::PStringType) || t2.is_a?(Types::PPatternType) || t2.is_a?(Types::PEnumType) + elsif t2.is_a?(Types::PStringType) + # A specific string acts as a set of strings - must have exactly the same strings + 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 + + if t2.values.empty? + # Strings (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 type must match all patterns in Pattern type + t.patterns.all? do |p| + re = p.regexp + t2.values.all? {|v| re.match(v) } + end end # @api private def assignable_PFloatType(t, t2) t2.is_a?(Types::PFloatType) end # @api private def assignable_PBooleanType(t, t2) t2.is_a?(Types::PBooleanType) end # @api private - def assignable_PPatternType(t, t2) - t2.is_a?(Types::PPatternType) + 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) t2.is_a?(Types::PCollectionType) end # @api private def assignable_PType(t, t2) return false unless t2.is_a?(Types::PType) assignable?(t.type, t2.type) end # Array is assignable if t2 is an Array and t2's element type is assignable # @api private def assignable_PArrayType(t, t2) return false unless t2.is_a?(Types::PArrayType) assignable?(t.element_type, t2.element_type) 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) assignable?(t.key_type, t2.key_type) && assignable?(t.element_type, t2.element_type) end def assignable_PCatalogEntryType(t1, t2) t2.is_a?(Types::PCatalogEntryType) end 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 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[Literal, Data] # @api private def assignable_PDataType(t, t2) - t2.is_a?(Types::PDataType) || t2.is_a?(Types::PLiteralType) || assignable?(@data_array, t2) || assignable?(@data_hash, t2) + t2.is_a?(Types::PDataType) || + t2.is_a?(Types::PLiteralType) || + assignable?(@data_array, t2) || + assignable?(@data_hash, t2) || + (t2.is_a?(Types::PVariantType) && !t2.types.empty? && t2.types.all? {|t| assignable?(data, t) } ) 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) 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 + 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_PLiteralType(t) ; "Literal" ; end # @api private def string_PDataType(t) ; "Data" ; end # @api private def string_PNumericType(t) ; "Numeric" ; end # @api private def string_PIntegerType(t) result = ["Integer"] unless t.from.nil? && t.to.nil? from = t.from.nil? ? 'default' : t.from to = t.to.nil? ? 'default' : t.to if from == to "Integer[#{from}]" else "Integer[#{from}, #{to}]" end else "Integer" end end # @api private def string_PFloatType(t) ; "Float" ; end # @api private - def string_PPatternType(t) ; "Pattern" ; end + 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 + return "String" + end + + # @api private + def debug_string_PStringType(t) + return "String" # if t.values.empty? + "String[" << (t.values.map {|s| "'#{s}'" }).join(', ') << ']' + end # @api private - def string_PStringType(t) ; "String" ; end + 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_PPatternType(t) + return "Pattern" if t.patterns.empty? + "Pattern[" << t.patterns.map {|s| "#{s.regexp.inspect}" }.join(', ') << ']' + end # @api private def string_PCollectionType(t) ; "Collection" ; end # @api private def string_PRubyType(t) ; "Ruby[#{string(t.ruby_class)}]" ; end # @api private def string_PArrayType(t) "Array[#{string(t.element_type)}]" end # @api private def string_PHashType(t) "Hash[#{string(t.key_type)}, #{string(t.element_type)}]" 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 # 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) str.split('::').inject(Object) do |memo, name_segment| memo.const_get(name_segment) end end def common_data?(t1, t2) assignable?(@data_t, t1) && assignable?(@data_t, t2) end def common_literal?(t1, t2) assignable?(@literal_t, t1) && assignable?(@literal_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 059617621..88f88ecbd 100644 --- a/lib/puppet/pops/types/type_factory.rb +++ b/lib/puppet/pops/types/type_factory.rb @@ -1,205 +1,252 @@ # 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 the Integer type # @api public # def self.range(from, to) t = Types::PIntegerType.new() t.from = from unless from == :default t.to = to unless to == :default 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 + # Produces the String type, optionally with specific string values # @api public # - def self.string() - Types::PStringType.new() + def self.string(*values) + t = Types::PStringType.new() + values.each {|v| t.addValues(v) } + 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 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 Pattern type + # 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.pattern() - Types::PPatternType.new() + def self.regexp(pattern = nil) + t = Types::PRegexpType.new() + if pattern + t.pattern = pattern.is_a?(Regexp) ? pattern.inspect[1..-1] : 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 = Type::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) + else + raise ArgumentError, "Only String and Regexp are allowed: got '#{re.class}" + end + end + t end # Produces the Literal type # @api public # def self.literal() Types::PLiteralType.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[Literal, o] where o is either a type, or an instance for which a type is inferred. # @api public # def self.hash_of(value, key = literal()) 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[Literal, Data] # @api public # def self.hash_of_data() type = Types::PHashType.new() type.key_type = literal() type.element_type = data() 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(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 end diff --git a/lib/puppet/pops/types/type_parser.rb b/lib/puppet/pops/types/type_parser.rb index 1280fc67e..bc4a8e2ca 100644 --- a/lib/puppet/pops/types/type_parser.rb +++ b/lib/puppet/pops/types/type_parser.rb @@ -1,234 +1,279 @@ # 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) 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_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 # @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 "literal" TYPES.literal() + 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 "ruby", "type" # should not be interpreted as Resource type # TODO: these should not be errors raise_unknown_type_error(name_ast) 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" if parameters.size != 1 raise_invalid_parameters_error("Array", 1, parameters.size) end assert_type(parameters[0]) TYPES.array_of(parameters[0]) when "hash" if parameters.size == 1 assert_type(parameters[0]) TYPES.hash_of(parameters[0]) elsif parameters.size != 2 raise_invalid_parameters_error("Hash", "1 or 2", parameters.size) else assert_type(parameters[0]) assert_type(parameters[1]) TYPES.hash_of(parameters[1], parameters[0]) end 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 "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 "object", "collection", "data", "catalogentry", "boolean", "float", "literal", "undef", "numeric", "pattern" + when "object", "collection", "data", "catalogentry", "boolean", "float", "literal", "undef", "numeric", "pattern", "string" raise_unparameterized_type_error(parameterized_ast.left_expr) when "ruby", "type" # TODO: Add Stage, Node (they are not Resource Type) # should not be interpreted as Resource type raise_unknown_type_error(parameterized_ast.left_expr) 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 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_from_string(@string || position.locator.string) end end diff --git a/lib/puppet/pops/types/types.rb b/lib/puppet/pops/types/types.rb index b0cdcce7d..1dceb4515 100644 --- a/lib/puppet/pops/types/types.rb +++ b/lib/puppet/pops/types/types.rb @@ -1,232 +1,313 @@ 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 Literal, Array[Data], and Hash[Literal, 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.new.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 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) + end + end + end + # Type that is PDataType compatible, but is not a PCollectionType. # @api public class PLiteralType < PDataType end + # A string type describing the set of strings having one of the given values + # + class PEnumType < PLiteralType + 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 PStringType < PLiteralType + has_many_attr 'values', String, :lowerBound => 0, :upperBound => -1, :unique => true + + 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 < PLiteralType 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? (to-from).abs 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 end + # @api public + class PRegexpType < PLiteralType + 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 < PLiteralType + 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 < PLiteralType end # @api public class PCollectionType < PObjectType contains_one_uni 'element_type', PAbstractType module ClassModule def hash [self.class, element_type].hash end def ==(o) self.class == o.class && element_type == o.element_type end end end # @api public class PArrayType < PCollectionType module ClassModule def hash [self.class, self.element_type].hash end def ==(o) self.class == o.class && self.element_type == o.element_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].hash end def ==(o) self.class == o.class && key_type == o.key_type && self.element_type == o.element_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 end diff --git a/spec/unit/pops/evaluator/access_ops_spec.rb b/spec/unit/pops/evaluator/access_ops_spec.rb index ab0db0655..a70dc6390 100644 --- a/spec/unit/pops/evaluator/access_ops_spec.rb +++ b/spec/unit/pops/evaluator/access_ops_spec.rb @@ -1,231 +1,236 @@ #! /usr/bin/env ruby require 'spec_helper' require 'puppet/pops' require 'puppet/pops/evaluator/evaluator_impl' require 'puppet/pops/types/type_factory' # relative to this spec file (./) does not work as this file is loaded by rspec require File.join(File.dirname(__FILE__), '/evaluator_rspec_helper') describe 'Puppet::Pops::Evaluator::EvaluatorImpl/AccessOperator' do include EvaluatorRspecHelper def range(from, to) Puppet::Pops::Types::TypeFactory.range(from, to) end context 'The evaluator when operating on a String' do it 'can get a single character using a single key index to []' do expect(evaluate(literal('abc')[1])).to eql('b') end it 'can get the last character using the key -1 in []' do expect(evaluate(literal('abc')[-1])).to eql('c') end it 'can get a substring by giving two keys' do expect(evaluate(literal('abcd')[1,2])).to eql('bc') end it 'produces empty string for a substring out of range' do expect(evaluate(literal('abc')[100])).to eql('') end it 'raises an error if arity is wrong for []' do expect{evaluate(literal('abc')[])}.to raise_error(/String supports \[\] with one or two arguments\. Got 0/) expect{evaluate(literal('abc')[1,2,3])}.to raise_error(/String supports \[\] with one or two arguments\. Got 3/) end end context 'The evaluator when operating on an Array' do it 'is tested with the correct assumptions' do expect(literal([1,2,3])[1].current.is_a?(Puppet::Pops::Model::AccessExpression)).to eql(true) end it 'can get an element using a single key index to []' do expect(evaluate(literal([1,2,3])[1])).to eql(2) end it 'can get the last element using the key -1 in []' do expect(evaluate(literal([1,2,3])[-1])).to eql(3) end it 'can get a slice of elements using two keys' do expect(evaluate(literal([1,2,3,4])[1,2])).to eql([2,3]) end it 'produces nil for a missing entry' do expect(evaluate(literal([1,2,3])[100])).to eql(nil) end it 'raises an error if arity is wrong for []' do expect{evaluate(literal([1,2,3,4])[])}.to raise_error(/Array supports \[\] with one or two arguments\. Got 0/) expect{evaluate(literal([1,2,3,4])[1,2,3])}.to raise_error(/Array supports \[\] with one or two arguments\. Got 3/) end end context 'The evaluator when operating on a Hash' do it 'can get a single element giving a single key to []' do expect(evaluate(literal({'a'=>1,'b'=>2,'c'=>3})['b'])).to eql(2) end it 'produces nil for a missing key' do expect(evaluate(literal({'a'=>1,'b'=>2,'c'=>3})['x'])).to eql(nil) end it 'can get multiple elements by giving multiple keys to []' do expect(evaluate(literal({'a'=>1,'b'=>2,'c'=>3, 'd'=>4})['b', 'd'])).to eql([2, 4]) end it 'compacts the result when using multiple keys' do expect(evaluate(literal({'a'=>1,'b'=>2,'c'=>3, 'd'=>4})['b', 'x'])).to eql([2]) end it 'produces an empty array if none of multiple given keys were missing' do expect(evaluate(literal({'a'=>1,'b'=>2,'c'=>3, 'd'=>4})['x', 'y'])).to eql([]) end it 'raises an error if arity is wrong for []' do expect{evaluate(literal({'a'=>1,'b'=>2,'c'=>3})[])}.to raise_error(/Hash supports \[\] with one or more arguments\. Got 0/) end end context "When applied to a type it" do let(:types) { Puppet::Pops::Types::TypeFactory } # Integer # it 'produces an Integer[from, to]' do expr = fqr('Integer')[1, 3] expect(evaluate(expr)).to eql(range(1,3)) end it 'produces an Integer[1]' do expr = fqr('Integer')[1] expect(evaluate(expr)).to eql(range(1,1)) end it 'produces an Integer[from, 1, "010" => 8, "0x10" => 16, "3.14" => 3.14, "0.314e1" => 3.14, "31.4e-1" => 3.14, "'1'" => '1', "'banana'" => 'banana', '"banana"' => 'banana', "banana" => 'banana', "banana::split" => 'banana::split', "false" => false, "true" => true, "Array" => types.array_of_data(), "/.*/" => /.*/ }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end end context "When the evaluator evaluates Lists and Hashes" do { "[]" => [], "[1,2,3]" => [1,2,3], "[1,[2.0, 2.1, [2.2]],[3.0, 3.1]]" => [1,[2.0, 2.1, [2.2]],[3.0, 3.1]], "[2 + 2]" => [4], "[1,2,3] == [1,2,3]" => true, "[1,2,3] != [2,3,4]" => true, "[1,2,3] == [2,2,3]" => false, "[1,2,3] != [1,2,3]" => false, "[1,2,3][2]" => 3, "[1,2,3] + [4,5]" => [1,2,3,4,5], "[1,2,3] + [[4,5]]" => [1,2,3,[4,5]], "[1,2,3] + 4" => [1,2,3,4], "[1,2,3] << [4,5]" => [1,2,3,[4,5]], "[1,2,3] << {'a' => 1, 'b'=>2}" => [1,2,3,{'a' => 1, 'b'=>2}], "[1,2,3] << 4" => [1,2,3,4], "[1,2,3,4] - [2,3]" => [1,4], "[1,2,3,4] - [2,5]" => [1,3,4], "[1,2,3,4] - 2" => [1,3,4], "[1,2,3,[2],4] - 2" => [1,3,[2],4], "[1,2,3,[2,3],4] - [[2,3]]" => [1,2,3,4], "[1,2,3,3,2,4,2,3] - [2,3]" => [1,4], "[1,2,3,['a',1],['b',2]] - {'a' => 1, 'b'=>2}" => [1,2,3], "[1,2,3,{'a'=>1,'b'=>2}] - [{'a' => 1, 'b'=>2}]" => [1,2,3], }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { "[1,2,3] + {'a' => 1, 'b'=>2}" => [1,2,3,['a',1],['b',2]], }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do # This test must be done with match_array since the order of the hash # is undefined and Ruby 1.8.7 and 1.9.3 produce different results. expect(parser.evaluate_string(scope, source, __FILE__)).to match_array(result) end end { "[1,2,3][a]" => :error, }.each do |source, result| it "should parse and raise error for '#{source}'" do expect { parser.evaluate_string(scope, source, __FILE__) }.to raise_error(Puppet::ParseError) end end { "{}" => {}, "{'a'=>1,'b'=>2}" => {'a'=>1,'b'=>2}, "{'a'=>1,'b'=>{'x'=>2.1,'y'=>2.2}}" => {'a'=>1,'b'=>{'x'=>2.1,'y'=>2.2}}, "{'a'=> 2 + 2}" => {'a'=> 4}, "{'a'=> 1, 'b'=>2} == {'a'=> 1, 'b'=>2}" => true, "{'a'=> 1, 'b'=>2} != {'x'=> 1, 'b'=>2}" => true, "{'a'=> 1, 'b'=>2} == {'a'=> 2, 'b'=>3}" => false, "{'a'=> 1, 'b'=>2} != {'a'=> 1, 'b'=>2}" => false, "{a => 1, b => 2}[b]" => 2, "{2+2 => sum, b => 2}[4]" => 'sum', "{'a'=>1, 'b'=>2} + {'c'=>3}" => {'a'=>1,'b'=>2,'c'=>3}, "{'a'=>1, 'b'=>2} + {'b'=>3}" => {'a'=>1,'b'=>3}, "{'a'=>1, 'b'=>2} + ['c', 3, 'b', 3]" => {'a'=>1,'b'=>3, 'c'=>3}, "{'a'=>1, 'b'=>2} + [['c', 3], ['b', 3]]" => {'a'=>1,'b'=>3, 'c'=>3}, "{'a'=>1, 'b'=>2} - {'b' => 3}" => {'a'=>1}, "{'a'=>1, 'b'=>2, 'c'=>3} - ['b', 'c']" => {'a'=>1}, "{'a'=>1, 'b'=>2, 'c'=>3} - 'c'" => {'a'=>1, 'b'=>2}, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { "{'a' => 1, 'b'=>2} << 1" => :error, }.each do |source, result| it "should parse and raise error for '#{source}'" do expect { parser.evaluate_string(scope, source, __FILE__) }.to raise_error(Puppet::ParseError) end end end context "When the evaluator perform comparisons" do { "'a' == 'a'" => true, "'a' == 'b'" => false, "'a' != 'a'" => false, "'a' != 'b'" => true, "'a' < 'b' " => true, "'a' < 'a' " => false, "'b' < 'a' " => false, "'a' <= 'b'" => true, "'a' <= 'a'" => true, "'b' <= 'a'" => false, "'a' > 'b' " => false, "'a' > 'a' " => false, "'b' > 'a' " => true, "'a' >= 'b'" => false, "'a' >= 'a'" => true, "'b' >= 'a'" => true, "'a' == 'A'" => true, "'a' != 'A'" => false, "'a' > 'A'" => false, "'a' >= 'A'" => true, "'A' < 'a'" => false, "'A' <= 'a'" => true, "1 == 1" => true, "1 == 2" => false, "1 != 1" => false, "1 != 2" => true, "1 < 2 " => true, "1 < 1 " => false, "2 < 1 " => false, "1 <= 2" => true, "1 <= 1" => true, "2 <= 1" => false, "1 > 2 " => false, "1 > 1 " => false, "2 > 1 " => true, "1 >= 2" => false, "1 >= 1" => true, "2 >= 1" => true, "1 == 1.0 " => true, "1 < 1.1 " => true, "'1' < 1.1" => true, "1.0 == 1 " => true, "1.0 < 2 " => true, "'1.0' < 1.1" => true, "'1.0' < 'a'" => true, "'1.0' < '' " => true, "'1.0' < ' '" => true, "'a' > '1.0'" => true, "/.*/ == /.*/ " => true, "/.*/ != /a.*/" => true, "true == true " => true, "false == false" => true, "true == false" => false, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { "'a' =~ /.*/" => true, "'a' =~ '.*'" => true, "/.*/ != /a.*/" => true, "'a' !~ /b.*/" => true, "'a' !~ 'b.*'" => true, '$x = a; a =~ "$x.*"' => true, "a =~ Pattern['a.*']" => true, + "a =~ Regexp['a.*']" => true, "$x = /a.*/ a =~ $x" => true, "$x = Pattern['a.*'] a =~ $x" => true, "1 =~ Integer" => true, "1 !~ Integer" => false, "[1,2,3] =~ Array[Integer[1,10]]" => true, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { "666 =~ /6/" => :error, "[a] =~ /a/" => :error, "{a=>1} =~ /a/" => :error, "/a/ =~ /a/" => :error, "Array =~ /A/" => :error, }.each do |source, result| it "should parse and raise error for '#{source}'" do expect { parser.evaluate_string(scope, source, __FILE__) }.to raise_error(Puppet::ParseError) end end { "1 in [1,2,3]" => true, "4 in [1,2,3]" => false, "a in {x=>1, a=>2}" => true, "z in {x=>1, a=>2}" => false, "ana in bananas" => true, "xxx in bananas" => false, "/ana/ in bananas" => true, "/xxx/ in bananas" => false, "ANA in bananas" => false, # ANA is a type, not a String "'ANA' in bananas" => true, "ana in 'BANANAS'" => true, "/ana/ in 'BANANAS'" => false, "/ANA/ in 'BANANAS'" => true, "xxx in 'BANANAS'" => false, "[2,3] in [1,[2,3],4]" => true, "[2,4] in [1,[2,3],4]" => false, "[a,b] in ['A',['A','B'],'C']" => true, "[x,y] in ['A',['A','B'],'C']" => false, "a in {a=>1}" => true, "x in {a=>1}" => false, "'A' in {a=>1}" => true, "'X' in {a=>1}" => false, "a in {'A'=>1}" => true, "x in {'A'=>1}" => false, "/xxx/ in {'aaaxxxbbb'=>1}" => true, "/yyy/ in {'aaaxxxbbb'=>1}" => false, "15 in [1, 0xf]" => true, "15 in [1, '0xf']" => true, "'15' in [1, 0xf]" => true, "15 in [1, 115]" => false, "1 in [11, '111']" => false, "'1' in [11, '111']" => false, "Array[Integer] in [2, 3]" => false, "Array[Integer] in [2, [3, 4]]" => true, "Array[Integer] in [2, [a, 4]]" => false, "Integer in { 2 =>'a'}" => true, "Integer[5,10] in [1,5,3]" => true, "Integer[5,10] in [1,2,3]" => false, "Integer in {'a'=>'a'}" => false, "Integer in {'a'=>1}" => false, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { 'Object' => ['Data', 'Literal', 'Numeric', 'Integer', 'Float', 'Boolean', 'String', 'Pattern', 'Collection', 'Array', 'Hash', 'CatalogEntry', 'Resource', 'Class', 'Undef', 'File', 'NotYetKnownResourceType'], # Note, Data > Collection is false (so not included) 'Data' => ['Literal', 'Numeric', 'Integer', 'Float', 'Boolean', 'String', 'Pattern', 'Array', 'Hash',], 'Literal' => ['Numeric', 'Integer', 'Float', 'Boolean', 'String', 'Pattern'], 'Numeric' => ['Integer', 'Float'], 'CatalogEntry' => ['Class', 'Resource', 'File', 'NotYetKnownResourceType'], 'Integer[1,10]' => ['Integer[2,3]'], }.each do |general, specials| specials.each do |special | it "should compute that #{general} > #{special}" do parser.evaluate_string(scope, "#{general} > #{special}", __FILE__).should == true end it "should compute that #{special} < #{general}" do parser.evaluate_string(scope, "#{special} < #{general}", __FILE__).should == true end it "should compute that #{general} != #{special}" do parser.evaluate_string(scope, "#{special} != #{general}", __FILE__).should == true end end end { 'Integer[1,10] > Integer[2,3]' => true, 'Integer[1,10] == Integer[2,3]' => false, 'Integer[1,10] > Integer[0,5]' => false, 'Integer[1,10] > Integer[1,10]' => false, 'Integer[1,10] >= Integer[1,10]' => true, 'Integer[1,10] == Integer[1,10]' => true, }.each do |source, result| it "should parse and evaluate the integer range comparison expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end end context "When the evaluator performs arithmetic" do context "on Integers" do { "2+2" => 4, "2 + 2" => 4, "7 - 3" => 4, "6 * 3" => 18, "6 / 3" => 2, "6 % 3" => 0, "10 % 3" => 1, "-(6/3)" => -2, "-6/3 " => -2, "8 >> 1" => 4, "8 << 1" => 16, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end context "on Floats" do { "2.2 + 2.2" => 4.4, "7.7 - 3.3" => 4.4, "6.1 * 3.1" => 18.91, "6.6 / 3.3" => 2.0, "-(6.0/3.0)" => -2.0, "-6.0/3.0 " => -2.0, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { "3.14 << 2" => :error, "3.14 >> 2" => :error, "6.6 % 3.3" => 0.0, "10.0 % 3.0" => 1.0, }.each do |source, result| it "should parse and raise error for '#{source}'" do expect { parser.evaluate_string(scope, source, __FILE__) }.to raise_error(Puppet::ParseError) end end end context "on strings requiring boxing to Numeric" do { "'2' + '2'" => 4, "'2.2' + '2.2'" => 4.4, "'0xF7' + '010'" => 0xFF, "'0xF7' + '0x8'" => 0xFF, "'0367' + '010'" => 0xFF, "'012.3' + '010'" => 20.3, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { "'0888' + '010'" => :error, "'0xWTF' + '010'" => :error, "'0x12.3' + '010'" => :error, "'0x12.3' + '010'" => :error, }.each do |source, result| it "should parse and raise error for '#{source}'" do expect { parser.evaluate_string(scope, source, __FILE__) }.to raise_error(Puppet::ParseError) end end end end end # arithmetic context "When the evaluator evaluates assignment" do { "$a = 5" => 5, "$a = 5; $a" => 5, "$a = 5; $b = 6; $a" => 5, "$a = $b = 5; $a == $b" => true, "$a = [1,2,3]; [x].map |$x| { $a += x; $a }" => [[1,2,3,'x']], "$a = [a,x,c]; [x].map |$x| { $a -= x; $a }" => [['a','c']], }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { "[a,b,c] = [1,2,3]; $a == 1 and $b == 2 and $c == 3" => :error, "[a,b,c] = {b=>2,c=>3,a=>1}; $a == 1 and $b == 2 and $c == 3" => :error, "$a = [1,2,3]; [x].collect |$x| { [a] += x; $a }" => :error, "$a = [a,x,c]; [x].collect |$x| { [a] -= x; $a }" => :error, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do expect { parser.evaluate_string(scope, source, __FILE__)}.to raise_error(Puppet::ParseError) end end end context "When the evaluator evaluates conditionals" do { "if true {5}" => 5, "if false {5}" => nil, "if false {2} else {5}" => 5, "if false {2} elsif true {5}" => 5, "if false {2} elsif false {5}" => nil, "unless false {5}" => 5, "unless true {5}" => nil, "unless true {2} else {5}" => 5, "$a = if true {5} $a" => 5, "$a = if false {5} $a" => nil, "$a = if false {2} else {5} $a" => 5, "$a = if false {2} elsif true {5} $a" => 5, "$a = if false {2} elsif false {5} $a" => nil, "$a = unless false {5} $a" => 5, "$a = unless true {5} $a" => nil, "$a = unless true {2} else {5} $a" => 5, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { "case 1 { 1 : { yes } }" => 'yes', "case 2 { 1,2,3 : { yes} }" => 'yes', "case 2 { 1,3 : { no } 2: { yes} }" => 'yes', "case 2 { 1,3 : { no } 5: { no } default: { yes }}" => 'yes', "case 2 { 1,3 : { no } 5: { no } }" => nil, "case 'banana' { 1,3 : { no } /.*ana.*/: { yes } }" => 'yes', "case 'banana' { /.*(ana).*/: { $1 } }" => 'ana', "case [1] { Array : { yes } }" => 'yes', "case [1] { Array[String] : { no } Array[Integer]: { yes } }" => 'yes', }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { "2 ? { 1 => no, 2 => yes}" => 'yes', "3 ? { 1 => no, 2 => no}" => nil, "3 ? { 1 => no, 2 => no, default => yes }" => 'yes', "3 ? { 1 => no, default => yes, 3 => no }" => 'yes', "'banana' ? { /.*(ana).*/ => $1 }" => 'ana', "[2] ? { Array[String] => yes, Array => yes}" => 'yes', }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end end context "When evaluator performs [] operations" do { "[1,2,3][0]" => 1, "[1,2,3][2]" => 3, "[1,2,3][3]" => nil, "[1,2,3][-1]" => 3, "[1,2,3][-2]" => 2, "[1,2,3][-4]" => nil, "[1,2,3,4][0,2]" => [1,2], "[1,2,3,4][1,3]" => [2,3,4], "[1,2,3,4][-2,2]" => [3,4], "[1,2,3,4][-3,2]" => [2,3], "[1,2,3,4][3,5]" => [4], "[1,2,3,4][5,2]" => [], "[1,2,3,4][0,-1]" => [1,2,3,4], "[1,2,3,4][0,-2]" => [1,2,3], "[1,2,3,4][0,-4]" => [1], "[1,2,3,4][0,-5]" => [], "[1,2,3,4][-5,2]" => [1], "[1,2,3,4][-5,-3]" => [1,2], "[1,2,3,4][-6,-3]" => [1,2], "[1,2,3,4][2,-3]" => [], }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { "{a=>1, b=>2, c=>3}[a]" => 1, "{a=>1, b=>2, c=>3}[c]" => 3, "{a=>1, b=>2, c=>3}[x]" => nil, "{a=>1, b=>2, c=>3}[c,b]" => [3,2], "{a=>1, b=>2, c=>3}[a,b,c]" => [1,2,3], }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { "'abc'[0]" => 'a', "'abc'[2]" => 'c', "'abc'[-1]" => 'c', "'abc'[-2]" => 'b', "'abc'[-3]" => 'a', "'abc'[-4]" => '', "'abc'[3]" => '', "abc[0]" => 'a', "abc[2]" => 'c', "abc[-1]" => 'c', "abc[-2]" => 'b', "abc[-3]" => 'a', "abc[-4]" => '', "abc[3]" => '', "'abcd'[0,2]" => 'ab', "'abcd'[1,3]" => 'bcd', "'abcd'[-2,2]" => 'cd', "'abcd'[-3,2]" => 'bc', "'abcd'[3,5]" => 'd', "'abcd'[5,2]" => '', "'abcd'[0,-1]" => 'abcd', "'abcd'[0,-2]" => 'abc', "'abcd'[0,-4]" => 'a', "'abcd'[0,-5]" => '', "'abcd'[-5,2]" => 'a', "'abcd'[-5,-3]" => 'ab', "'abcd'[-6,-3]" => 'ab', "'abcd'[2,-3]" => '', }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end # Type operations (full set tested by tests covering type calculator) { "Array[Integer]" => types.array_of(types.integer), "Hash[Integer,Integer]" => types.hash_of(types.integer, types.integer), "Resource[File]" => types.resource('File'), "Resource['File']" => types.resource(types.resource('File')), "File[foo]" => types.resource('file', 'foo'), "File[foo, bar]" => [types.resource('file', 'foo'), types.resource('file', 'bar')], }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end # LHS where [] not supported, and missing key(s) { "Array[]" => :error, "'abc'[]" => :error, "Resource[]" => :error, "File[]" => :error, "String[]" => :error, "String[0]" => :error, "1[]" => :error, "1[0]" => :error, "3.14[]" => :error, "3.14[0]" => :error, "/.*/[]" => :error, "/.*/[0]" => :error, "$a=[1] $a[]" => :error, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do expect { parser.evaluate_string(scope, source, __FILE__)}.to raise_error(Puppet::ParseError) end end context "on catalog types" do it "[n] gets resource parameter [n]" do source = "notify { 'hello': message=>'yo'} Notify[hello][message]" parser.evaluate_string(scope, source, __FILE__).should == 'yo' end it "[n] gets class parameter [n]" do source = "class wonka($produces='chocolate'){ } include wonka Class[wonka][produces]" # This is more complicated since it needs to run like 3.x and do an import_ast adapted_parser = Puppet::Parser::E4ParserAdapter.new adapted_parser.file = __FILE__ ast = adapted_parser.parse(source) scope.known_resource_types.import_ast(ast, '') ast.code.safeevaluate(scope).should == 'chocolate' end # Resource default and override expressions and resource parameter access with [] { "notify { id: message=>explicit} Notify[id][message]" => "explicit", "Notify { message=>by_default} notify {foo:} Notify[foo][message]" => "by_default", "notify {foo:} Notify[foo]{message =>by_override} Notify[foo][message]" => "by_override", }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end # Resource default and override expressions and resource parameter access error conditions { "notify { xid: message=>explicit} Notify[id][message]" => /Resource not found/, "notify { id: message=>explicit} Notify[id][mustard]" => /does not have a parameter called 'mustard'/, }.each do |source, result| it "should parse '#{source}' and raise error matching #{result}" do expect { parser.evaluate_string(scope, source, __FILE__)}.to raise_error(result) end end end # end [] operations end context "When the evaluator performs boolean operations" do { "true and true" => true, "false and true" => false, "true and false" => false, "false and false" => false, "true or true" => true, "false or true" => true, "true or false" => true, "false or false" => false, "! true" => false, "!! true" => true, "!! false" => false, "! 'x'" => false, "! ''" => true, "! undef" => true, "! [a]" => false, "! []" => false, "! {a=>1}" => false, "! {}" => false, "true and false and '0xwtf' + 1" => false, "false or true or '0xwtf' + 1" => true, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do parser.evaluate_string(scope, source, __FILE__).should == result end end { "false || false || '0xwtf' + 1" => :error, }.each do |source, result| it "should parse and raise error for '#{source}'" do expect { parser.evaluate_string(scope, source, __FILE__) }.to raise_error(Puppet::ParseError) end end end context "When evaluator performs calls" do let(:populate) do parser.evaluate_string(scope, "$a = 10 $b = [1,2,3]") end { 'sprintf( "x%iy", $a )' => "x10y", '"x%iy".sprintf( $a )' => "x10y", '$b.reduce |$memo,$x| { $memo + $x }' => 6, 'reduce($b) |$memo,$x| { $memo + $x }' => 6, }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do populate parser.evaluate_string(scope, source, __FILE__).should == result end end { '"value is ${a*2} yo"' => :error, }.each do |source, result| it "should parse and raise error for '#{source}'" do expect { parser.evaluate_string(scope, source, __FILE__) }.to raise_error(Puppet::ParseError) end end end context "When evaluator performs string interpolation" do let(:populate) do parser.evaluate_string(scope, "$a = 10 $b = [1,2,3]") end { '"value is $a yo"' => "value is 10 yo", '"value is \$a yo"' => "value is $a yo", '"value is ${a} yo"' => "value is 10 yo", '"value is \${a} yo"' => "value is ${a} yo", '"value is ${$a} yo"' => "value is 10 yo", '"value is ${$a*2} yo"' => "value is 20 yo", '"value is ${sprintf("x%iy",$a)} yo"' => "value is x10y yo", '"value is ${"x%iy".sprintf($a)} yo"' => "value is x10y yo", '"value is ${[1,2,3]} yo"' => "value is [1, 2, 3] yo", '"value is ${/.*/} yo"' => "value is /.*/ yo", '$x = undef "value is $x yo"' => "value is yo", '$x = default "value is $x yo"' => "value is default yo", '$x = Array[Integer] "value is $x yo"' => "value is Array[Integer] yo", '"value is ${Array[Integer]} yo"' => "value is Array[Integer] yo", }.each do |source, result| it "should parse and evaluate the expression '#{source}' to #{result}" do populate parser.evaluate_string(scope, source, __FILE__).should == result end end it "should parse and evaluate an interpolation of a hash" do source = '"value is ${{a=>1,b=>2}} yo"' # This test requires testing against two options because a hash to string # produces a result that is unordered hashstr = {'a' => 1, 'b' => 2}.to_s alt_results = ["value is {a => 1, b => 2} yo", "value is {b => 2, a => 1} yo" ] populate parse_result = parser.evaluate_string(scope, source, __FILE__) alt_results.include?(parse_result).should == true end { '"value is ${a*2} yo"' => :error, }.each do |source, result| it "should parse and raise error for '#{source}'" do expect { parser.evaluate_string(scope, source, __FILE__) }.to raise_error(Puppet::ParseError) end end end context "When evaluating variables" do context "that are non existing an error is raised for" do it "unqualified variable" do expect { parser.evaluate_string(scope, "$quantum_gravity", __FILE__) }.to raise_error(/Unknown variable/) end it "qualified variable" do expect { parser.evaluate_string(scope, "$quantum_gravity::graviton", __FILE__) }.to raise_error(/Unknown variable/) end end end context "When evaluating relationships" do it 'should form a relation with ->' do source = "File[a] -> File[b]" parser.evaluate_string(scope, source, __FILE__) scope.compiler.should have_relationship(['File', 'a', '->', 'File', 'b']) end it 'should form a relation with <-' do source = "File[a] <- File[b]" parser.evaluate_string(scope, source, __FILE__) scope.compiler.should have_relationship(['File', 'b', '->', 'File', 'a']) end it 'should form a relation with <-' do source = "File[a] <~ File[b]" parser.evaluate_string(scope, source, __FILE__) scope.compiler.should have_relationship(['File', 'b', '~>', 'File', 'a']) end end matcher :have_relationship do |expected| calc = Puppet::Pops::Types::TypeCalculator.new match do |compiler| op_name = {'->' => :relationship, '~>' => :subscription} compiler.relationships.any? do | relation | relation.source.type == expected[0] && relation.source.title == expected[1] && relation.type == op_name[expected[2]] && relation.target.type == expected[3] && relation.target.title == expected[4] end end failure_message_for_should do |actual| "Relationship #{expected[0]}[#{expected[1]}] #{expected[2]} #{expected[3]}[#{expected[4]}] but was unknown to compiler" end end end diff --git a/spec/unit/pops/types/type_calculator_spec.rb b/spec/unit/pops/types/type_calculator_spec.rb index 47ce5c1d4..64a3b75e4 100644 --- a/spec/unit/pops/types/type_calculator_spec.rb +++ b/spec/unit/pops/types/type_calculator_spec.rb @@ -1,721 +1,979 @@ require 'spec_helper' require 'puppet/pops' describe 'The type calculator' do let(:calculator) { Puppet::Pops::Types::TypeCalculator.new() } def int_range(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 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 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::PLiteralType, + 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, + ] + end + + def literal_types + # PVariantType is also literal, if its types are all Literal + [ + Puppet::Pops::Types::PLiteralType, + 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, + ] + end + + def data_compatible_types + literal_types + [Puppet::Pops::Types::PHashType, Puppet::Pops::Types::PArrayType, Puppet::Pops::Types::PDataType] + 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 PPatternType' do - calculator.infer(/^a regular exception$/).class.should == Puppet::Pops::Types::PPatternType + 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 '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[PLiteralType]' do calculator.infer([1,'two']).element_type.class.should == Puppet::Pops::Types::PLiteralType end it 'with float and string values translates to PArrayType[PLiteralType]' do calculator.infer([1.0,'two']).element_type.class.should == Puppet::Pops::Types::PLiteralType end it 'with fixnum, float, and string values translates to PArrayType[PLiteralType]' do calculator.infer([1, 2.0,'two']).element_type.class.should == Puppet::Pops::Types::PLiteralType end it 'with fixnum and regexp values translates to PArrayType[PLiteralType]' do calculator.infer([1, /two/]).element_type.class.should == Puppet::Pops::Types::PLiteralType end it 'with string and regexp values translates to PArrayType[PLiteralType]' do calculator.infer(['one', /two/]).element_type.class.should == Puppet::Pops::Types::PLiteralType 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[PLiteralType]]' 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::PLiteralType 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[PLiteralType]]' 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::PLiteralType 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 - # Deal with cases not covered by infer computing common type + 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 diff' 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' 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, b_t1]) + end end - context 'when testing if x is assignable to y' do - it 'should allow all object types to PObjectType' do - t = Puppet::Pops::Types::PObjectType.new() - calculator.assignable?(t, t).should() == true - calculator.assignable?(t,Puppet::Pops::Types::PNilType.new()).should() == true - calculator.assignable?(t,Puppet::Pops::Types::PDataType.new()).should() == true - calculator.assignable?(t,Puppet::Pops::Types::PLiteralType.new()).should() == true - calculator.assignable?(t,Puppet::Pops::Types::PStringType.new()).should() == true - calculator.assignable?(t,Puppet::Pops::Types::PNumericType.new()).should() == true - calculator.assignable?(t,Puppet::Pops::Types::PIntegerType.new()).should() == true - calculator.assignable?(t,Puppet::Pops::Types::PFloatType.new()).should() == true - calculator.assignable?(t,Puppet::Pops::Types::PPatternType.new()).should() == true - calculator.assignable?(t,Puppet::Pops::Types::PBooleanType.new()).should() == true - calculator.assignable?(t,Puppet::Pops::Types::PCollectionType.new()).should() == true - calculator.assignable?(t,Puppet::Pops::Types::PArrayType.new()).should() == true - calculator.assignable?(t,Puppet::Pops::Types::PHashType.new()).should() == true - calculator.assignable?(t,Puppet::Pops::Types::PRubyType.new()).should() == true - calculator.assignable?(t,Puppet::Pops::Types::PHostClassType.new()).should() == true - calculator.assignable?(t,Puppet::Pops::Types::PResourceType.new()).should() == true - end - - it 'should reject PObjectType to less generic types' do - t = Puppet::Pops::Types::PObjectType.new() - calculator.assignable?(Puppet::Pops::Types::PDataType.new(), t).should() == false - calculator.assignable?(Puppet::Pops::Types::PLiteralType.new(), t).should() == false - calculator.assignable?(Puppet::Pops::Types::PStringType.new(), t).should() == false - calculator.assignable?(Puppet::Pops::Types::PNumericType.new(), t).should() == false - calculator.assignable?(Puppet::Pops::Types::PIntegerType.new(), t).should() == false - calculator.assignable?(Puppet::Pops::Types::PFloatType.new(), t).should() == false - calculator.assignable?(Puppet::Pops::Types::PPatternType.new(), t).should() == false - calculator.assignable?(Puppet::Pops::Types::PBooleanType.new(), t).should() == false - calculator.assignable?(Puppet::Pops::Types::PCollectionType.new(), t).should() == false - calculator.assignable?(Puppet::Pops::Types::PArrayType.new(), t).should() == false - calculator.assignable?(Puppet::Pops::Types::PHashType.new(), t).should() == false - calculator.assignable?(Puppet::Pops::Types::PRubyType.new(), t).should() == false - calculator.assignable?(Puppet::Pops::Types::PHostClassType.new(), t).should() == false - calculator.assignable?(Puppet::Pops::Types::PResourceType.new(), t).should() == false - end - - it 'should allow all data types, array, and hash to PDataType' do - t = Puppet::Pops::Types::PDataType.new() - calculator.assignable?(t, t).should() == true - calculator.assignable?(t,Puppet::Pops::Types::PLiteralType.new()).should() == true - calculator.assignable?(t,Puppet::Pops::Types::PStringType.new()).should() == true - calculator.assignable?(t,Puppet::Pops::Types::PNumericType.new()).should() == true - calculator.assignable?(t,Puppet::Pops::Types::PIntegerType.new()).should() == true - calculator.assignable?(t,Puppet::Pops::Types::PFloatType.new()).should() == true - calculator.assignable?(t,Puppet::Pops::Types::PPatternType.new()).should() == true - calculator.assignable?(t,Puppet::Pops::Types::PBooleanType.new()).should() == true - calculator.assignable?(t,Puppet::Pops::Types::PArrayType.new()).should() == true - calculator.assignable?(t,Puppet::Pops::Types::PHashType.new()).should() == true - end - - it 'should reject PDataType to less generic data types' do - t = Puppet::Pops::Types::PDataType.new() - calculator.assignable?(Puppet::Pops::Types::PLiteralType.new(), t).should() == false - calculator.assignable?(Puppet::Pops::Types::PStringType.new(), t).should() == false - calculator.assignable?(Puppet::Pops::Types::PNumericType.new(), t).should() == false - calculator.assignable?(Puppet::Pops::Types::PIntegerType.new(), t).should() == false - calculator.assignable?(Puppet::Pops::Types::PFloatType.new(), t).should() == false - calculator.assignable?(Puppet::Pops::Types::PPatternType.new(), t).should() == false - calculator.assignable?(Puppet::Pops::Types::PBooleanType.new(), t).should() == false - end - - it 'should reject PDataType to non data types' do - t = Puppet::Pops::Types::PDataType.new() - calculator.assignable?(Puppet::Pops::Types::PCollectionType.new(),t).should() == false - calculator.assignable?(Puppet::Pops::Types::PArrayType.new(),t).should() == false - calculator.assignable?(Puppet::Pops::Types::PHashType.new(),t).should() == false - calculator.assignable?(Puppet::Pops::Types::PRubyType.new(), t).should() == false - end - - it 'should allow all literal types to PLiteralType' do - t = Puppet::Pops::Types::PLiteralType.new() - calculator.assignable?(t, t).should() == true - calculator.assignable?(t,Puppet::Pops::Types::PStringType.new()).should() == true - calculator.assignable?(t,Puppet::Pops::Types::PNumericType.new()).should() == true - calculator.assignable?(t,Puppet::Pops::Types::PIntegerType.new()).should() == true - calculator.assignable?(t,Puppet::Pops::Types::PFloatType.new()).should() == true - calculator.assignable?(t,Puppet::Pops::Types::PPatternType.new()).should() == true - calculator.assignable?(t,Puppet::Pops::Types::PBooleanType.new()).should() == true - end - - it 'should reject PLiteralType to less generic literal types' do - t = Puppet::Pops::Types::PLiteralType.new() - calculator.assignable?(Puppet::Pops::Types::PStringType.new(), t).should() == false - calculator.assignable?(Puppet::Pops::Types::PNumericType.new(), t).should() == false - calculator.assignable?(Puppet::Pops::Types::PIntegerType.new(), t).should() == false - calculator.assignable?(Puppet::Pops::Types::PFloatType.new(), t).should() == false - calculator.assignable?(Puppet::Pops::Types::PPatternType.new(), t).should() == false - calculator.assignable?(Puppet::Pops::Types::PBooleanType.new(), t).should() == false - end - - it 'should reject PLiteralType to non literal types' do - t = Puppet::Pops::Types::PLiteralType.new() - calculator.assignable?(Puppet::Pops::Types::PCollectionType.new(), t).should() == false - calculator.assignable?(Puppet::Pops::Types::PArrayType.new(), t).should() == false - calculator.assignable?(Puppet::Pops::Types::PHashType.new(), t).should() == false - calculator.assignable?(Puppet::Pops::Types::PRubyType.new(), t).should() == false - end - - it 'should allow all numeric types to PNumericType' do - t = Puppet::Pops::Types::PNumericType.new() - calculator.assignable?(t, t).should() == true - calculator.assignable?(t, Puppet::Pops::Types::PIntegerType.new()).should() == true - calculator.assignable?(t, Puppet::Pops::Types::PFloatType.new()).should() == true - end - - it 'should reject PNumericType to less generic numeric types' do - t = Puppet::Pops::Types::PNumericType.new() - calculator.assignable?(Puppet::Pops::Types::PIntegerType.new(), t).should() == false - calculator.assignable?(Puppet::Pops::Types::PFloatType.new(), t).should() == false - end - - it 'should reject PNumericType to non numeric types' do - t = Puppet::Pops::Types::PNumericType.new() - calculator.assignable?(Puppet::Pops::Types::PStringType.new(), t).should() == false - calculator.assignable?(Puppet::Pops::Types::PPatternType.new(), t).should() == false - calculator.assignable?(Puppet::Pops::Types::PBooleanType.new(), t).should() == false - calculator.assignable?(Puppet::Pops::Types::PCollectionType.new(), t).should() == false - calculator.assignable?(Puppet::Pops::Types::PArrayType.new(), t).should() == false - calculator.assignable?(Puppet::Pops::Types::PHashType.new(), t).should() == false - calculator.assignable?(Puppet::Pops::Types::PRubyType.new(), t).should() == false - end - - it 'should allow all collection types to PCollectionType' do - t = Puppet::Pops::Types::PCollectionType.new() - calculator.assignable?(t, t).should() == true - calculator.assignable?(t, Puppet::Pops::Types::PArrayType.new()).should() == true - calculator.assignable?(t, Puppet::Pops::Types::PHashType.new()).should() == true - end - - it 'should reject PCollectionType to less generic collection types' do - t = Puppet::Pops::Types::PCollectionType.new() - calculator.assignable?(Puppet::Pops::Types::PArrayType.new(), t).should() == false - calculator.assignable?(Puppet::Pops::Types::PHashType.new(), t).should() == false - end - - it 'should reject PCollectionType to non collection types' do - t = Puppet::Pops::Types::PCollectionType.new() - calculator.assignable?(Puppet::Pops::Types::PDataType.new(), t).should() == false - calculator.assignable?(Puppet::Pops::Types::PLiteralType.new(), t).should() == false - calculator.assignable?(Puppet::Pops::Types::PStringType.new(), t).should() == false - calculator.assignable?(Puppet::Pops::Types::PNumericType.new(), t).should() == false - calculator.assignable?(Puppet::Pops::Types::PIntegerType.new(), t).should() == false - calculator.assignable?(Puppet::Pops::Types::PFloatType.new(), t).should() == false - calculator.assignable?(Puppet::Pops::Types::PPatternType.new(), t).should() == false - calculator.assignable?(Puppet::Pops::Types::PBooleanType.new(), t).should() == false - calculator.assignable?(Puppet::Pops::Types::PRubyType.new(), t).should() == false - calculator.assignable?(Puppet::Pops::Types::PHostClassType.new(), t).should() == false - calculator.assignable?(Puppet::Pops::Types::PResourceType.new(), t).should() == false - end - - it 'should reject PArrayType to non array type collections' do - t = Puppet::Pops::Types::PArrayType.new() - calculator.assignable?(Puppet::Pops::Types::PHashType.new(), t).should() == false + 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 - it 'should reject PHashType to non hash type collections' do - t = Puppet::Pops::Types::PHashType.new() - calculator.assignable?(Puppet::Pops::Types::PArrayType.new(), t).should() == false + context "for Data, such that" do + it 'all literals + array and hash are assignable to Data' do + t = Puppet::Pops::Types::PDataType.new() + data_compatible_types.each { |t2| t2.new.should be_assignable_to(t) } + end + + it 'a Variant of literal, hash, or array is assignable to Data' do + t = Puppet::Pops::Types::PDataType.new() + data_compatible_types.each { |t2| variant_t(t2.new).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(t2.new) } + 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(t2.new)) } + end + + it 'Data is not assignable to any disjunct type' do + tested_types = all_types - [Puppet::Pops::Types::PObjectType, Puppet::Pops::Types::PDataType] - literal_types + t = Puppet::Pops::Types::PDataType.new() + tested_types.each {|t2| t.should_not be_assignable_to(t2.new) } + end end - it 'should recognize mapped ruby types' do - calculator.assignable?(Puppet::Pops::Types::PIntegerType.new(), Integer).should == true - calculator.assignable?(Puppet::Pops::Types::PIntegerType.new(), Fixnum).should == true - calculator.assignable?(Puppet::Pops::Types::PIntegerType.new(), Bignum).should == true - calculator.assignable?(Puppet::Pops::Types::PFloatType.new(), Float).should == true - calculator.assignable?(Puppet::Pops::Types::PNumericType.new(), Numeric).should == true - calculator.assignable?(Puppet::Pops::Types::PNilType.new(), NilClass).should == true - calculator.assignable?(Puppet::Pops::Types::PBooleanType.new(), FalseClass).should == true - calculator.assignable?(Puppet::Pops::Types::PBooleanType.new(), TrueClass).should == true - calculator.assignable?(Puppet::Pops::Types::PStringType.new(), String).should == true - calculator.assignable?(Puppet::Pops::Types::PPatternType.new(), Regexp).should == true - calculator.assignable?(Puppet::Pops::Types::TypeFactory.array_of_data(), Array).should == true - calculator.assignable?(Puppet::Pops::Types::TypeFactory.hash_of_data(), Hash).should == true + context "for Literal, such that" do + it "all literals are assignable to Literal" do + t = Puppet::Pops::Types::PLiteralType.new() + literal_types.each {|t2| t2.new.should be_assignable_to(t) } + end + + it 'Literal is not assignable to any of its subtypes' do + t = Puppet::Pops::Types::PLiteralType.new() + types_to_test = literal_types - [Puppet::Pops::Types::PLiteralType] + types_to_test.each {|t2| t.should_not be_assignable_to(t2.new) } + end + + it 'Literal is not assignable to any disjunct type' do + tested_types = all_types - [Puppet::Pops::Types::PObjectType, Puppet::Pops::Types::PDataType] - literal_types + t = Puppet::Pops::Types::PLiteralType.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::PLiteralType, + ] - 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 any other Collection type" do + t = Puppet::Pops::Types::PArrayType.new() + tested_types = collection_types - [ + Puppet::Pops::Types::PCollectionType, + Puppet::Pops::Types::PArrayType] + 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::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::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::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 + + + 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 end it 'should accept an equal reverse range' do calculator.assignable?(int_range(2,5), int_range(5,2)).should == true end it 'should accept a narrower range' do calculator.assignable?(int_range(2,10), int_range(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 end it 'should reject a wider range' do calculator.assignable?(int_range(3,5), int_range(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 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 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 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 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 all patterns' do + p_t = pattern_t('abc', 'ab', 'c') + p_s = string_t('XabcY', 'abcde') + calculator.assignable?(p_t, p_s).should == true + end + + it 'should reject a string not matching all patterns' do + p_t = pattern_t('abc', 'ab', 'c', 'q') + p_s = string_t('XqqqY') + calculator.assignable?(p_t, p_s).should == false + end + + it 'should reject multiple strings if not all match all patterns' do + p_t = pattern_t('abc', 'ab', 'c', 'q') + p_s = string_t('abc', 'XqqqY') + calculator.assignable?(p_t, p_s).should == false + 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 it 'should consider fixnum instanceof PIntegerType' do calculator.instance?(Puppet::Pops::Types::PIntegerType.new(), 1) == true end it 'should consider fixnum instanceof Fixnum' do calculator.instance?(Fixnum, 1) == true end it 'should consider integer in range' do range = int_range(0,10) calculator.instance?(range, 1) == true calculator.instance?(range, 10) == true calculator.instance?(range, -1) == false calculator.instance?(range, 11) == false end + + it 'should consider string matching enum as instanceof' do + enum = enum_t('XS', 'S', 'M', 'L', 'XL', '0') + calculator.instance?(enum, 'XS') == true + calculator.instance?(enum, 'S') == true + calculator.instance?(enum, 'XXL') == false + calculator.instance?(enum, '') == false + calculator.instance?(enum, '0') == true + calculator.instance?(enum, 0) == 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']) == true + calculator.instance?(array, ['XS', 'S', 'XXL']) == 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) + array = variant_t(enum, sizes) + calculator.instance?(array, ['XS', 'S', 30, 50]) == true + calculator.instance?(array, ['XS', 'S', 'XXL']) == false + calculator.instance?(array, ['XS', 'S', 29]) == false + 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 \'PPatternType\' for Regexp' do - calculator.type(Regexp).class.should == Puppet::Pops::Types::PPatternType + 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[PLiteralType,PDataType]\' for Hash' do t = calculator.type(Hash) t.class.should == Puppet::Pops::Types::PHashType t.key_type.class.should == Puppet::Pops::Types::PLiteralType 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 \'Literal\' for PLiteralType' do calculator.string(Puppet::Pops::Types::PLiteralType.new()).should == 'Literal' 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]' 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 \'Pattern\' for PPatternType' do - calculator.string(Puppet::Pops::Types::PPatternType.new()).should == 'Pattern' + 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 \'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 \'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 '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::PLiteralType.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::PPatternType.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.string(calculator.infer(Puppet::Pops::Types::PIntegerType.new())).should == "Type[Integer]" + 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 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::PLiteralType.new() )).should == "Type[Literal]" 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::PPatternType.new() )).should == "Type[Pattern]" + 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]" 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]]" calculator.string(calculator.infer([int_t, string_t])).should == "Array[Type[Literal]]" 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 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 diff --git a/spec/unit/pops/types/type_factory_spec.rb b/spec/unit/pops/types/type_factory_spec.rb index e4524d23b..682a97f1b 100644 --- a/spec/unit/pops/types/type_factory_spec.rb +++ b/spec/unit/pops/types/type_factory_spec.rb @@ -1,89 +1,101 @@ require 'spec_helper' require 'puppet/pops' describe 'The type factory' do context 'when creating' do it 'integer() returns PIntegerType' do Puppet::Pops::Types::TypeFactory.integer().class().should == Puppet::Pops::Types::PIntegerType end it 'float() returns PFloatType' do Puppet::Pops::Types::TypeFactory.float().class().should == Puppet::Pops::Types::PFloatType end it 'string() returns PStringType' do Puppet::Pops::Types::TypeFactory.string().class().should == Puppet::Pops::Types::PStringType end it 'boolean() returns PBooleanType' do Puppet::Pops::Types::TypeFactory.boolean().class().should == Puppet::Pops::Types::PBooleanType end it 'pattern() returns PPatternType' do Puppet::Pops::Types::TypeFactory.pattern().class().should == Puppet::Pops::Types::PPatternType end + it 'regexp() returns PRegexpType' do + Puppet::Pops::Types::TypeFactory.regexp().class().should == Puppet::Pops::Types::PRegexpType + end + + it 'enum() returns PEnumType' do + Puppet::Pops::Types::TypeFactory.enum().class().should == Puppet::Pops::Types::PEnumType + end + + it 'variant() returns PVariantType' do + Puppet::Pops::Types::TypeFactory.variant().class().should == Puppet::Pops::Types::PVariantType + end + it 'literal() returns PLiteralType' do Puppet::Pops::Types::TypeFactory.literal().class().should == Puppet::Pops::Types::PLiteralType end it 'data() returns PDataType' do Puppet::Pops::Types::TypeFactory.data().class().should == Puppet::Pops::Types::PDataType end it 'resource() creates a generic PResourceType' do pr = Puppet::Pops::Types::TypeFactory.resource() pr.class().should == Puppet::Pops::Types::PResourceType pr.type_name.should == nil end it 'resource(x) creates a PResourceType[x]' do pr = Puppet::Pops::Types::TypeFactory.resource('x') pr.class().should == Puppet::Pops::Types::PResourceType pr.type_name.should == 'x' end it 'host_class() creates a generic PHostClassType' do hc = Puppet::Pops::Types::TypeFactory.host_class() hc.class().should == Puppet::Pops::Types::PHostClassType hc.class_name.should == nil end it 'host_class(x) creates a PHostClassType[x]' do hc = Puppet::Pops::Types::TypeFactory.host_class('x') hc.class().should == Puppet::Pops::Types::PHostClassType hc.class_name.should == 'x' end it 'array_of(fixnum) returns PArrayType[PIntegerType]' do at = Puppet::Pops::Types::TypeFactory.array_of(1) at.class().should == Puppet::Pops::Types::PArrayType at.element_type.class.should == Puppet::Pops::Types::PIntegerType end it 'array_of(PIntegerType) returns PArrayType[PIntegerType]' do at = Puppet::Pops::Types::TypeFactory.array_of(Puppet::Pops::Types::PIntegerType.new()) at.class().should == Puppet::Pops::Types::PArrayType at.element_type.class.should == Puppet::Pops::Types::PIntegerType end it 'array_of_data returns PArrayType[PDataType]' do at = Puppet::Pops::Types::TypeFactory.array_of_data at.class().should == Puppet::Pops::Types::PArrayType at.element_type.class.should == Puppet::Pops::Types::PDataType end it 'hash_of_data returns PHashType[PLiteralType,PDataType]' do ht = Puppet::Pops::Types::TypeFactory.hash_of_data ht.class().should == Puppet::Pops::Types::PHashType ht.key_type.class.should == Puppet::Pops::Types::PLiteralType ht.element_type.class.should == Puppet::Pops::Types::PDataType end it 'ruby(1) returns PRubyType[\'Fixnum\']' do ht = Puppet::Pops::Types::TypeFactory.ruby(1) ht.class().should == Puppet::Pops::Types::PRubyType ht.ruby_class.should == 'Fixnum' end end end diff --git a/spec/unit/pops/types/type_parser_spec.rb b/spec/unit/pops/types/type_parser_spec.rb index 9c6250ef7..e1b516cee 100644 --- a/spec/unit/pops/types/type_parser_spec.rb +++ b/spec/unit/pops/types/type_parser_spec.rb @@ -1,140 +1,139 @@ require 'spec_helper' require 'puppet/pops' describe Puppet::Pops::Types::TypeParser do extend RSpec::Matchers::DSL let(:parser) { Puppet::Pops::Types::TypeParser.new } let(:types) { Puppet::Pops::Types::TypeFactory } it "rejects a puppet expression" do expect { parser.parse("1 + 1") }.to raise_error(Puppet::ParseError, /The expression <1 \+ 1> is not a valid type specification/) end it "rejects a empty type specification" do expect { parser.parse("") }.to raise_error(Puppet::ParseError, /The expression <> is not a valid type specification/) end it "rejects an invalid type simple type" do expect { parser.parse("notAType") }.to raise_error(Puppet::ParseError, /The expression is not a valid type specification/) end it "rejects an unknown parameterized type" do expect { parser.parse("notAType[Integer]") }.to raise_error(Puppet::ParseError, /The expression is not a valid type specification/) end it "rejects an unknown type parameter" do expect { parser.parse("Array[notAType]") }.to raise_error(Puppet::ParseError, /The expression is not a valid type specification/) end it "does not support types that do not make sense in the puppet language" do # These will/may make sense later, but are not yet implemented and should not be interpreted as a PResourceType expect { parser.parse("Ruby") }.to raise_type_error_for("Ruby") expect { parser.parse("Type") }.to raise_type_error_for("Type") end [ 'Object', 'Float', 'Collection', 'Data', 'CatalogEntry', 'Boolean', 'Float', 'Literal', 'Undef', 'Numeric', - 'Pattern' ].each do |name| it "does not support parameterizing unparameterized type <#{name}" do expect { parser.parse("#{name}[Integer]") }.to raise_unparameterized_error_for(name) end end it "parses a simple, unparameterized type into the type object" do expect(the_type_parsed_from(types.object)).to be_the_type(types.object) expect(the_type_parsed_from(types.integer)).to be_the_type(types.integer) expect(the_type_parsed_from(types.float)).to be_the_type(types.float) expect(the_type_parsed_from(types.string)).to be_the_type(types.string) expect(the_type_parsed_from(types.boolean)).to be_the_type(types.boolean) expect(the_type_parsed_from(types.pattern)).to be_the_type(types.pattern) expect(the_type_parsed_from(types.data)).to be_the_type(types.data) expect(the_type_parsed_from(types.catalog_entry)).to be_the_type(types.catalog_entry) expect(the_type_parsed_from(types.collection)).to be_the_type(types.collection) end it "interprets an unparameterized Array as an Array of Data" do expect(parser.parse("Array")).to be_the_type(types.array_of_data) end it "interprets an unparameterized Hash as a Hash of Literal to Data" do expect(parser.parse("Hash")).to be_the_type(types.hash_of_data) end it "interprets a parameterized Hash[t] as a Hash of Literal to t" do expect(parser.parse("Hash[Integer]")).to be_the_type(types.hash_of(types.integer)) end it "parses a parameterized type into the type object" do parameterized_array = types.array_of(types.integer) parameterized_hash = types.hash_of(types.integer, types.boolean) expect(the_type_parsed_from(parameterized_array)).to be_the_type(parameterized_array) expect(the_type_parsed_from(parameterized_hash)).to be_the_type(parameterized_hash) end it "rejects an array spec with the wrong number of parameters" do expect { parser.parse("Array[Integer, Integer]") }.to raise_the_parameter_error("Array", 1, 2) expect { parser.parse("Hash[Integer, Integer, Integer]") }.to raise_the_parameter_error("Hash", "1 or 2", 3) end it "interprets anything that is not a built in type to be a resource type" do expect(parser.parse("File")).to be_the_type(types.resource('file')) end it "parses a resource type with title" do expect(parser.parse("File['/tmp/foo']")).to be_the_type(types.resource('file', '/tmp/foo')) end it "parses a resource type using 'Resource[type]' form" do expect(parser.parse("Resource[File]")).to be_the_type(types.resource('file')) end it "parses a resource type with title using 'Resource[type, title]'" do expect(parser.parse("Resource[File, '/tmp/foo']")).to be_the_type(types.resource('file', '/tmp/foo')) end it "parses a host class type" do expect(parser.parse("Class")).to be_the_type(types.host_class()) end it "parses a parameterized host class type" do expect(parser.parse("Class[foo::bar]")).to be_the_type(types.host_class('foo::bar')) end matcher :be_the_type do |type| calc = Puppet::Pops::Types::TypeCalculator.new match do |actual| calc.assignable?(actual, type) && calc.assignable?(type, actual) end failure_message_for_should do |actual| "expected #{calc.string(type)}, but was #{calc.string(actual)}" end end def raise_the_parameter_error(type, required, given) raise_error(Puppet::ParseError, /#{type} requires #{required}, #{given} provided/) end def raise_type_error_for(type_name) raise_error(Puppet::ParseError, /Unknown type <#{type_name}>/) end def raise_unparameterized_error_for(type_name) raise_error(Puppet::ParseError, /Not a parameterized type <#{type_name}>/) end def the_type_parsed_from(type) parser.parse(the_type_spec_for(type)) end def the_type_spec_for(type) calc = Puppet::Pops::Types::TypeCalculator.new calc.string(type) end end