diff --git a/lib/puppet/parser/functions/collect.rb b/lib/puppet/parser/functions/collect.rb index e30a80bb1..fea42a4df 100644 --- a/lib/puppet/parser/functions/collect.rb +++ b/lib/puppet/parser/functions/collect.rb @@ -1,15 +1,15 @@ Puppet::Parser::Functions::newfunction( :collect, :type => :rvalue, :arity => 2, :doc => <<-'ENDHEREDOC') do |args| The 'collect' function has been renamed to 'map'. Please update your manifests. The collect function is reserved for future use. - Removed as of 3.4 - requires `parser = future`. ENDHEREDOC raise NotImplementedError, "The 'collect' function has been renamed to 'map'. Please update your manifests." -end \ No newline at end of file +end diff --git a/lib/puppet/parser/functions/select.rb b/lib/puppet/parser/functions/select.rb index 659f2013c..93924f9d0 100644 --- a/lib/puppet/parser/functions/select.rb +++ b/lib/puppet/parser/functions/select.rb @@ -1,15 +1,15 @@ Puppet::Parser::Functions::newfunction( :select, :type => :rvalue, :arity => 2, :doc => <<-'ENDHEREDOC') do |args| The 'select' function has been renamed to 'filter'. Please update your manifests. The select function is reserved for future use. - Removed as of 3.4 - requires `parser = future`. ENDHEREDOC raise NotImplementedError, "The 'select' function has been renamed to 'filter'. Please update your manifests." -end \ No newline at end of file +end diff --git a/lib/puppet/pops/binder/binder_issues.rb b/lib/puppet/pops/binder/binder_issues.rb index a23e7735a..98171add5 100644 --- a/lib/puppet/pops/binder/binder_issues.rb +++ b/lib/puppet/pops/binder/binder_issues.rb @@ -1,122 +1,122 @@ # @api public module Puppet::Pops::Binder::BinderIssues # NOTE: The methods #issue and #hard_issue are done in a somewhat funny way # since the Puppet::Pops::Issues is a module with these methods defined on the module-class # This makes it hard to inherit them in this module. (Likewise if Issues was a class, and they # need to be defined for the class, and such methods are also not inherited, it becomes more # difficult to reuse these. It did not seem as a good idea to refactor Issues at this point # in time - they should both probably be refactored once bindings support is finished. # Meanwhile, they delegate to Issues. # (see Puppet::Pops::Issues#issue) def self.issue (issue_code, *args, &block) Puppet::Pops::Issues.issue(issue_code, *args, &block) end # (see Puppet::Pops::Issues#hard_issue) def self.hard_issue(issue_code, *args, &block) Puppet::Pops::Issues.hard_issue(issue_code, *args, &block) end # Producer issues (binding identified using :binding argument) # @api public MISSING_NAME = issue :MISSING_NAME, :binding do "#{label.a_an_uc(binding)} with #{label.a_an(semantic)} has no name" end # @api public MISSING_KEY = issue :MISSING_KEY, :binding do "#{label.a_an_uc(binding)} with #{label.a_an(semantic)} has no key" end # @api public MISSING_VALUE = issue :MISSING_VALUE, :binding do "#{label.a_an_uc(binding)} with #{label.a_an(semantic)} has no value" end # @api public MISSING_EXPRESSION = issue :MISSING_EXPRESSION, :binding do "#{label.a_an_uc(binding)} with #{label.a_an(semantic)} has no expression" end # @api public MISSING_CLASS_NAME = issue :MISSING_CLASS_NAME, :binding do "#{label.a_an_uc(binding)} with #{label.a_an(semantic)} has no class name" end # @api public CACHED_PRODUCER_MISSING_PRODUCER = issue :PRODUCER_MISSING_PRODUCER, :binding do "#{label.a_an_uc(binding)} with #{label.a_an(semantic)} has no producer" end # @api public INCOMPATIBLE_TYPE = issue :INCOMPATIBLE_TYPE, :binding, :expected_type, :actual_type do "#{label.a_an_uc(binding)} with #{label.a_an(semantic)} has an incompatible type: expected #{label.a_an(expected_type)}, but got #{label.a_an(actual_type)}." end # @api public MULTIBIND_INCOMPATIBLE_TYPE = issue :MULTIBIND_INCOMPATIBLE_TYPE, :binding, :actual_type do "#{label.a_an_uc(binding)} with #{label.a_an(semantic)} cannot bind #{label.a_an(actual_type)} value" end # @api public MODEL_OBJECT_IS_UNBOUND = issue :MODEL_OBJECT_IS_UNBOUND do "#{label.a_an_uc(semantic)} is not contained in a binding" end # Binding issues (binding identified using semantic) # @api public MISSING_PRODUCER = issue :MISSING_PRODUCER do "#{label.a_an_uc(semantic)} has no producer" end # @api public MISSING_TYPE = issue :MISSING_TYPE do "#{label.a_an_uc(semantic)} has no type" end # @api public MULTIBIND_NOT_COLLECTION_PRODUCER = issue :MULTIBIND_NOT_COLLECTION_PRODUCER, :actual_producer do "#{label.a_an_uc(semantic)} must have a MultibindProducerDescriptor, but got: #{label.a_an(actual_producer)}" end # @api public MULTIBIND_TYPE_ERROR = issue :MULTIBIND_TYPE_ERROR, :actual_type do "#{label.a_an_uc(semantic)} is expected to bind a collection type, but got: #{label.a_an(actual_type)}." end # @api public MISSING_BINDINGS = issue :MISSING_BINDINGS do "#{label.a_an_uc(semantic)} has zero bindings" end # @api public MISSING_BINDINGS_NAME = issue :MISSING_BINDINGS_NAME do "#{label.a_an_uc(semantic)} has no name" end # @api public MISSING_PREDICATES = issue :MISSING_PREDICATES do "#{label.a_an_uc(semantic)} has zero predicates" end # @api public MISSING_LAYERS = issue :MISSING_LAYERS do "#{label.a_an_uc(semantic)} has zero layers" end # @api public MISSING_LAYER_NAME = issue :MISSING_LAYER_NAME do "#{label.a_an_uc(semantic)} has a layer without name" end # @api public MISSING_BINDINGS_IN_LAYER = issue :MISSING_BINDINGS_IN_LAYER, :layer do "#{label.a_an_uc(semantic)} has zero bindings in #{label.label(layer)}" end -end \ No newline at end of file +end diff --git a/lib/puppet/pops/binder/bindings_loader.rb b/lib/puppet/pops/binder/bindings_loader.rb index ca9043f2c..84a4051b8 100644 --- a/lib/puppet/pops/binder/bindings_loader.rb +++ b/lib/puppet/pops/binder/bindings_loader.rb @@ -1,88 +1,88 @@ 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::Binder::BindingsLoader @confdir = Puppet.settings[:confdir] # @autoloader = Puppet::Util::Autoload.new("BindingsLoader", "puppet/bindings", :wrap => false) # Returns a XXXXX 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(scope, name) case name when String provide_from_string(scope, name) when Array provide_from_name_path(scope, name.join('::'), name) else raise ArgumentError, "Cannot provide a bindings from a '#{name.class.name}'" end end # If loadable name exists relative to a a basedir or not. Returns the loadable path as a side effect. # @return [String, nil] a loadable path for the given name, or nil # def self.loadable?(basedir, name) # note, "lib" is added by the autoloader # paths_for_name(name).find {|p| Puppet::FileSystem.exist?(File.join(basedir, "lib/puppet/bindings", p)+'.rb') } end private def self.loader() unless Puppet.settings[:confdir] == @confdir @confdir = Puppet.settings[:confdir] == @confdir @autoloader = Puppet::Util::Autoload.new("BindingsLoader", "puppet/bindings", :wrap => false) end @autoloader end def self.provide_from_string(scope, 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(scope, name, name_path) end def self.provide_from_name_path(scope, name, name_path) # If bindings is already loaded, try this first result = Puppet::Bindings.resolve(scope, name) unless result # Attempt to load it using the auto loader paths_for_name(name).find {|path| loader.load(path) } result = Puppet::Bindings.resolve(scope, name) end result 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 +end diff --git a/lib/puppet/pops/binder/injector.rb b/lib/puppet/pops/binder/injector.rb index 8087551a4..994394975 100644 --- a/lib/puppet/pops/binder/injector.rb +++ b/lib/puppet/pops/binder/injector.rb @@ -1,767 +1,767 @@ # The injector is the "lookup service" class # # Initialization # -------------- # The injector is initialized with a configured {Puppet::Pops::Binder::Binder Binder}. The Binder instance contains a resolved set of # `key => "binding information"` that is used to setup the injector. # # Lookup # ------ # It is possible to lookup either the value, or a producer of the value. The {#lookup} method looks up a value, and the # {#lookup_producer} looks up a producer. # Both of these methods can be called with three different signatures; `lookup(key)`, `lookup(type, name)`, and `lookup(name)`, # with the corresponding calls to obtain a producer; `lookup_producer(key)`, `lookup_producer(type, name)`, and `lookup_producer(name)`. # # It is possible to pass a block to {#lookup} and {#lookup_producer}, the block is passed the result of the lookup # and the result of the block is returned as the value of the lookup. This is useful in order to provide a default value. # # @example Lookup with default value # injector.lookup('favourite_food') {|x| x.nil? ? 'bacon' : x } # # Singleton or Not # ---------------- # The lookup of a value is always based on the lookup of a producer. For *singleton producers* this means that the value is # determined by the first value lookup. Subsequent lookups via `lookup` or `lookup_producer` will produce the same instance. # # *Non singleton producers* will produce a new instance on each request for a value. For constant value producers this # means that a new deep-clone is produced for mutable objects (but not for immutable objects as this is not needed). # Custom producers should have non singleton behavior, or if this is not possible ensure that the produced result is # immutable. (The behavior if a custom producer hands out a mutable value and this is mutated is undefined). # # Custom bound producers capable of producing a series of objects when bound as a singleton means that the producer # is a singleton, not the value it produces. If such a producer is bound as non singleton, each `lookup` will get a new # producer (hence, typically, restarting the series). However, the producer returned from `lookup_producer` will not # recreate the producer on each call to `produce`; i.e. each `lookup_producer` returns a producer capable of returning # a series of objects. # # @see Puppet::Pops::Binder::Binder Binder, for details about how to bind keys to producers # @see Puppet::Pops::Binder::BindingsFactory BindingsFactory, for a convenient way to create a Binder and bindings # # Assisted Inject # --------------- # The injector supports lookup of instances of classes *even if the requested class is not explicitly bound*. # This is possible for classes that have a zero argument `initialize` method, or that has a class method called # `inject` that takes two arguments; `injector`, and `scope`. # This is useful in ruby logic as a class can then use the given injector to inject details. # An `inject` class method wins over a zero argument `initialize` in all cases. # # @example Using assisted inject # # Class with assisted inject support # class Duck # attr_reader :name, :year_of_birth # # def self.inject(injector, scope, binding, *args) # # lookup default name and year of birth, and use defaults if not present # name = injector.lookup(scope,'default-duck-name') {|x| x ? x : 'Donald Duck' } # year_of_birth = injector.lookup(scope,'default-duck-year_of_birth') {|x| x ? x : 1934 } # self.new(name, year_of_birth) # end # # def initialize(name, year_of_birth) # @name = name # @year_of_birth = year_of_birth # end # end # # injector.lookup(scope, Duck) # # Produces a Duck named 'Donald Duck' or named after the binding 'default-duck-name' (and with similar treatment of # # year_of_birth). # @see Puppet::Pops::Binder::Producers::AssistedInjectProducer AssistedInjectProducer, for more details on assisted injection # # Access to key factory and type calculator # ----------------------------------------- # It is important to use the same key factory, and type calculator as the binder. It is therefor possible to obtain # these with the methods {#key_factory}, and {#type_calculator}. # # Special support for producers # ----------------------------- # There is one method specially designed for producers. The {#get_contributions} method returns an array of all contributions # to a given *contributions key*. This key is obtained from the {#key_factory} for a given multibinding. The returned set of # contributed bindings is sorted in descending precedence order. Any conflicts, merges, etc. is performed by the multibinding # producer configured for a multibinding. # # @api public # class Puppet::Pops::Binder::Injector Producers = Puppet::Pops::Binder::Producers def self.create_from_model(layered_bindings_model) self.new(Puppet::Pops::Binder::Binder.new(layered_bindings_model)) end def self.create_from_hash(name, key_value_hash) factory = Puppet::Pops::Binder::BindingsFactory named_bindings = factory.named_bindings(name) { key_value_hash.each {|k,v| bind.name(k).to(v) }} layered_bindings = factory.layered_bindings(factory.named_layer(name+'-layer',named_bindings.model)) self.new(Puppet::Pops::Binder::Binder.new(layered_bindings)) end # Creates an injector with a single bindings layer created with the given name, and the bindings # produced by the given block. The block is evaluated with self bound to a BindingsContainerBuilder. # # @example # Injector.create('mysettings') do # bind('name').to(42) # end # # @api public # def self.create(name, &block) factory = Puppet::Pops::Binder::BindingsFactory layered_bindings = factory.layered_bindings(factory.named_layer(name+'-layer',factory.named_bindings(name, &block).model)) self.new(Puppet::Pops::Binder::Binder.new(layered_bindings)) end # Creates an overriding injector with a single bindings layer # created with the given name, and the bindings produced by the given block. # The block is evaluated with self bound to a BindingsContainerBuilder. # # @example # an_injector.override('myoverrides') do # bind('name').to(43) # end # # @api public # def override(name, &block) factory = Puppet::Pops::Binder::BindingsFactory layered_bindings = factory.layered_bindings(factory.named_layer(name+'-layer',factory.named_bindings(name, &block).model)) self.class.new(Puppet::Pops::Binder::Binder.new(layered_bindings, @impl.binder)) end # Creates an overriding injector with bindings from a bindings model (a LayeredBindings) which # may consists of multiple layers of bindings. # # @api public # def override_with_model(layered_bindings) unless layered_bindings.is_a?(Puppet::Pops::Binder::Bindings::LayeredBindings) raise ArgumentError, "Expected a LayeredBindings model, got '#{bindings_model.class}'" end self.class.new(Puppet::Pops::Binder::Binder.new(layered_bindings, @impl.binder)) end # Creates an overriding injector with a single bindings layer # created with the given name, and the bindings given in the key_value_hash # @api public # def override_with_hash(name, key_value_hash) factory = Puppet::Pops::Binder::BindingsFactory named_bindings = factory.named_bindings(name) { key_value_hash.each {|k,v| bind.name(k).to(v) }} layered_bindings = factory.layered_bindings(factory.named_layer(name+'-layer',named_bindings.model)) self.class.new(Puppet::Pops::Binder::Binder.new(layered_bindings, @impl.binder)) end # An Injector is initialized with a configured {Puppet::Pops::Binder::Binder Binder}. # # @param configured_binder [Puppet::Pops::Binder::Binder,nil] The configured binder containing effective bindings. A given value # of nil creates an injector that returns or yields nil on all lookup requests. # @raise ArgumentError if the given binder is not fully configured # # @api public # def initialize(configured_binder, parent_injector = nil) if configured_binder.nil? @impl = Private::NullInjectorImpl.new() else @impl = Private::InjectorImpl.new(configured_binder, parent_injector) end end # The KeyFactory used to produce keys in this injector. # The factory is shared with the Binder to ensure consistent translation to keys. # A compatible type calculator can also be obtained from the key factory. # @return [Puppet::Pops::Binder::KeyFactory] the key factory in use # # @api public # def key_factory() @impl.key_factory end # Returns the TypeCalculator in use for keys. The same calculator (as used for keys) should be used if there is a need # to check type conformance, or infer the type of Ruby objects. # # @return [Puppet::Pops::Types::TypeCalculator] the type calculator that is in use for keys # @api public # def type_calculator() @impl.type_calculator() end # Lookup (a.k.a "inject") of a value given a key. # The lookup may be called with different parameters. This method is a convenience method that # dispatches to one of #lookup_key or #lookup_type depending on the arguments. It also provides # the ability to use an optional block that is called with the looked up value, or scope and value if the # block takes two parameters. This is useful to provide a default value or other transformations, calculations # based on the result of the lookup. # # @overload lookup(scope, key) # (see #lookup_key) # @param scope [Puppet::Parser::Scope] the scope to use for evaluation # @param key [Object] an opaque object being the full key # # @overload lookup(scope, type, name = '') # (see #lookup_type) # @param scope [Puppet::Parser::Scope] the scope to use for evaluation # @param type [Puppet::Pops::Types::PObjectType] the type of what to lookup # @param name [String] the name to use, defaults to empty string (for unnamed) # # @overload lookup(scope, name) # Lookup of Data type with given name. # @see #lookup_type # @param scope [Puppet::Parser::Scope] the scope to use for evaluation # @param name [String] the Data/name to lookup # # @yield [value] passes the looked up value to an optional block and returns what this block returns # @yield [scope, value] passes scope and value to the block and returns what this block returns # @yieldparam scope [Puppet::Parser::Scope] the scope given to lookup # @yieldparam value [Object, nil] the looked up value or nil if nothing was found # # @raise [ArgumentError] if the block has an arity that is not 1 or 2 # # @api public # def lookup(scope, *args, &block) @impl.lookup(scope, *args, &block) end # Looks up a (typesafe) value based on a type/name combination. # Creates a key for the type/name combination using a KeyFactory. Specialization of the Data type are transformed # to a Data key, and the result is type checked to conform with the given key. # # @param type [Puppet::Pops::Types::PObjectType] the type to lookup as defined by Puppet::Pops::Types::TypeFactory # @param name [String] the (optional for non `Data` types) name of the entry to lookup. # The name may be an empty String (the default), but not nil. The name is required for lookup for subtypes of # `Data`. # @return [Object, nil] the looked up bound object, or nil if not found (type conformance with given type is guaranteed) # @raise [ArgumentError] if the produced value does not conform with the given type # # @api public # def lookup_type(scope, type, name='') @impl.lookup_type(scope, type, name) end # Looks up the key and returns the entry, or nil if no entry is found. # Produced type is checked for type conformance with its binding, but not with the lookup key. # (This since all subtypes of PDataType are looked up using a key based on PDataType). # Use the Puppet::Pops::Types::TypeCalculator#instance? method to check for conformance of the result # if this is wanted, or use #lookup_type. # # @param key [Object] lookup of key as produced by the key factory # @return [Object, nil] produced value of type that conforms with bound type (type conformance with key not guaranteed). # @raise [ArgumentError] if the produced value does not conform with the bound type # # @api public # def lookup_key(scope, key) @impl.lookup_key(scope, key) end # Lookup (a.k.a "inject") producer of a value given a key. # The producer lookup may be called with different parameters. This method is a convenience method that # dispatches to one of #lookup_producer_key or #lookup_producer_type depending on the arguments. It also provides # the ability to use an optional block that is called with the looked up producer, or scope and producer if the # block takes two parameters. This is useful to provide a default value, call a custom producer method, # or other transformations, calculations based on the result of the lookup. # # @overload lookup_producer(scope, key) # (see #lookup_proudcer_key) # @param scope [Puppet::Parser::Scope] the scope to use for evaluation # @param key [Object] an opaque object being the full key # # @overload lookup_producer(scope, type, name = '') # (see #lookup_type) # @param scope [Puppet::Parser::Scope] the scope to use for evaluation # @param type [Puppet::Pops::Types::PObjectType], the type of what to lookup # @param name [String], the name to use, defaults to empty string (for unnamed) # # @overload lookup_producer(scope, name) # Lookup of Data type with given name. # @see #lookup_type # @param scope [Puppet::Parser::Scope] the scope to use for evaluation # @param name [String], the Data/name to lookup # # @return [Puppet::Pops::Binder::Producers::Producer, Object, nil] a producer, or what the optional block returns # # @yield [producer] passes the looked up producer to an optional block and returns what this block returns # @yield [scope, producer] passes scope and producer to the block and returns what this block returns # @yieldparam producer [Puppet::Pops::Binder::Producers::Producer, nil] the looked up producer or nil if nothing was bound # @yieldparam scope [Puppet::Parser::Scope] the scope given to lookup # # @raise [ArgumentError] if the block has an arity that is not 1 or 2 # # @api public # def lookup_producer(scope, *args, &block) @impl.lookup_producer(scope, *args, &block) end # Looks up a Producer given an opaque binder key. # @return [Puppet::Pops::Binder::Producers::Producer, nil] the bound producer, or nil if no such producer was found. # # @api public # def lookup_producer_key(scope, key) @impl.lookup_producer_key(scope, key) end # Looks up a Producer given a type/name key. # @note The result is not type checked (it cannot be until the producer has produced an instance). # @return [Puppet::Pops::Binder::Producers::Producer, nil] the bound producer, or nil if no such producer was found # # @api public # def lookup_producer_type(scope, type, name='') @impl.lookup_producer_type(scope, type, name) end # Returns the contributions to a multibind given its contribution key (as produced by the KeyFactory). # This method is typically used by multibind value producers, but may be used for introspection of the injector's state. # # @param scope [Puppet::Parser::Scope] the scope to use # @param contributions_key [Object] Opaque key as produced by KeyFactory as the contributions key for a multibinding # @return [Array] the contributions sorted in deecending order of precedence # # @api public # def get_contributions(scope, contributions_key) @impl.get_contributions(scope, contributions_key) end # Returns an Injector that returns (or yields) nil on all lookups, and produces an empty structure for contributions # This method is intended for testing purposes. # def self.null_injector self.new(nil) end # The implementation of the Injector is private. # @see Puppet::Pops::Binder::Injector The public API this module implements. # @api private # module Private # This is a mocking "Null" implementation of Injector. It never finds anything # @api private class NullInjectorImpl attr_reader :entries attr_reader :key_factory attr_reader :type_calculator def initialize @entries = [] @key_factory = Puppet::Pops::Binder::KeyFactory.new() @type_calculator = @key_factory.type_calculator end def lookup(scope, *args, &block) raise ArgumentError, "lookup should be called with two or three arguments, got: #{args.size()+1}" unless args.size.between?(1,2) # call block with result if given if block case block.arity when 1 block.call(:undef) when 2 block.call(scope, :undef) else raise ArgumentError, "The block should have arity 1 or 2" end else val end end # @api private def binder nil end # @api private def lookup_key(scope, key) nil end # @api private def lookup_producer(scope, *args, &block) lookup(scope, *args, &block) end # @api private def lookup_producer_key(scope, key) nil end # @api private def lookup_producer_type(scope, type, name='') nil end def get_contributions() [] end end # @api private # class InjectorImpl # Hash of key => InjectorEntry # @api private # attr_reader :entries attr_reader :key_factory attr_reader :type_calculator attr_reader :binder def initialize(configured_binder, parent_injector = nil) @binder = configured_binder @parent = parent_injector # TODO: Different error message raise ArgumentError, "Given Binder is not configured" unless configured_binder #&& configured_binder.configured?() @entries = configured_binder.injector_entries() # It is essential that the injector uses the same key factory as the binder since keys must be # represented the same (but still opaque) way. # @key_factory = configured_binder.key_factory() @type_calculator = key_factory.type_calculator() @@transform_visitor ||= Puppet::Pops::Visitor.new(nil,"transform", 2, 2) @recursion_lock = [ ] end # @api private def lookup(scope, *args, &block) raise ArgumentError, "lookup should be called with two or three arguments, got: #{args.size()+1}" unless args.size.between?(1,2) val = case args[ 0 ] when Puppet::Pops::Types::PObjectType lookup_type(scope, *args) when String raise ArgumentError, "lookup of name should only pass the name" unless args.size == 1 lookup_key(scope, key_factory.data_key(args[ 0 ])) else raise ArgumentError, 'lookup using a key should only pass a single key' unless args.size == 1 lookup_key(scope, args[ 0 ]) end # call block with result if given if block case block.arity when 1 block.call(val) when 2 block.call(scope, val) else raise ArgumentError, "The block should have arity 1 or 2" end else val end end # Produces a key for a type/name combination. # @api private def named_key(type, name) key_factory.named_key(type, name) end # Produces a key for a PDataType/name combination # @api private def data_key(name) key_factory.data_key(name) end # @api private def lookup_type(scope, type, name='') val = lookup_key(scope, named_key(type, name)) return nil if val.nil? unless key_factory.type_calculator.instance?(type, val) raise ArgumentError, "Type error: incompatible type, #{type_error_detail(type, val)}" end val end # @api private def type_error_detail(expected, actual) actual_t = type_calculator.infer(actual) "expected: #{type_calculator.string(expected)}, got: #{type_calculator.string(actual_t)}" end # @api private def lookup_key(scope, key) if @recursion_lock.include?(key) raise ArgumentError, "Lookup loop detected for key: #{key}" end begin @recursion_lock.push(key) case entry = get_entry(key) when NilClass @parent ? @parent.lookup_key(scope, key) : nil when Puppet::Pops::Binder::InjectorEntry val = produce(scope, entry) return nil if val.nil? unless key_factory.type_calculator.instance?(entry.binding.type, val) raise "Type error: incompatible type returned by producer, #{type_error_detail(entry.binding.type, val)}" end val when Producers::AssistedInjectProducer entry.produce(scope) else # internal, direct entries entry end ensure @recursion_lock.pop() end end # Should be used to get entries as it converts missing entries to NotFound entries or AssistedInject entries # # @api private def get_entry(key) case entry = entries[ key ] when NilClass # not found, is this an assisted inject? if clazz = assistable_injected_class(key) entry = Producers::AssistedInjectProducer.new(self, clazz) entries[ key ] = entry else entries[ key ] = NotFound.new() entry = nil end when NotFound entry = nil end entry end # Returns contributions to a multibind in precedence order; highest first. # Returns an Array on the form [ [key, entry], [key, entry]] where the key is intended to be used to lookup the value # (or a producer) for that entry. # @api private def get_contributions(scope, contributions_key) result = {} return [] unless contributions = lookup_key(scope, contributions_key) contributions.each { |k| result[k] = get_entry(k) } result.sort {|a, b| a[0] <=> b[0] } #result.sort_by {|key, entry| entry } end # Produces an injectable class given a key, or nil if key does not represent an injectable class # @api private # def assistable_injected_class(key) kt = key_factory.get_type(key) return nil unless kt.is_a?(Puppet::Pops::Types::PRubyType) && !key_factory.is_named?(key) type_calculator.injectable_class(kt) end def lookup_producer(scope, *args, &block) raise ArgumentError, "lookup_producer should be called with two or three arguments, got: #{args.size()+1}" unless args.size <= 2 p = case args[ 0 ] when Puppet::Pops::Types::PObjectType lookup_producer_type(scope, *args) when String raise ArgumentError, "lookup_producer of name should only pass the name" unless args.size == 1 lookup_producer_key(scope, key_factory.data_key(args[ 0 ])) else raise ArgumentError, "lookup_producer using a key should only pass a single key" unless args.size == 1 lookup_producer_key(scope, args[ 0 ]) end # call block with result if given if block case block.arity when 1 block.call(p) when 2 block.call(scope, p) else raise ArgumentError, "The block should have arity 1 or 2" end else p end end # @api private def lookup_producer_key(scope, key) if @recursion_lock.include?(key) raise ArgumentError, "Lookup loop detected for key: #{key}" end begin @recursion_lock.push(key) producer(scope, get_entry(key), :multiple_use) ensure @recursion_lock.pop() end end # @api private def lookup_producer_type(scope, type, name='') lookup_producer_key(scope, named_key(type, name)) end # Returns the producer for the entry # @return [Puppet::Pops::Binder::Producers::Producer] the entry's producer. # # @api private # def producer(scope, entry, use) return nil unless entry # not found return entry.producer(scope) if entry.is_a?(Producers::AssistedInjectProducer) unless entry.cached_producer entry.cached_producer = transform(entry.binding.producer, scope, entry) end unless entry.cached_producer raise ArgumentError, "Injector entry without a producer #{format_binding(entry.binding)}" end entry.cached_producer.producer(scope) end # @api private def transform(producer_descriptor, scope, entry) @@transform_visitor.visit_this(self, producer_descriptor, scope, entry) end # Returns the produced instance # @return [Object] the produced instance # @api private # def produce(scope, entry) return nil unless entry # not found producer(scope, entry, :single_use).produce(scope) end # @api private def named_arguments_to_hash(named_args) nb = named_args.nil? ? [] : named_args result = {} nb.each {|arg| result[ :"#{arg.name}" ] = arg.value } result end # @api private def merge_producer_options(binding, options) named_arguments_to_hash(binding.producer_args).merge(options) end # @api private def format_binding(b) Puppet::Pops::Binder::Binder.format_binding(b) end # Handles a missing producer (which is valid for a Multibinding where one is selected automatically) # @api private # def transform_NilClass(descriptor, scope, entry) unless entry.binding.is_a?(Puppet::Pops::Binder::Bindings::Multibinding) raise ArgumentError, "Binding without producer detected, #{format_binding(entry.binding)}" end case entry.binding.type when Puppet::Pops::Types::PArrayType transform(Puppet::Pops::Binder::Bindings::ArrayMultibindProducerDescriptor.new(), scope, entry) when Puppet::Pops::Types::PHashType transform(Puppet::Pops::Binder::Bindings::HashMultibindProducerDescriptor.new(), scope, entry) else raise ArgumentError, "Unsupported multibind type, must be an array or hash type, #{format_binding(entry.binding)}" end end # @api private def transform_ArrayMultibindProducerDescriptor(descriptor, scope, entry) make_producer(Producers::ArrayMultibindProducer, descriptor, scope, entry, named_arguments_to_hash(entry.binding.producer_args)) end # @api private def transform_HashMultibindProducerDescriptor(descriptor, scope, entry) make_producer(Producers::HashMultibindProducer, descriptor, scope, entry, named_arguments_to_hash(entry.binding.producer_args)) end # @api private def transform_ConstantProducerDescriptor(descriptor, scope, entry) producer_class = singleton?(descriptor) ? Producers::SingletonProducer : Producers::DeepCloningProducer producer_class.new(self, entry.binding, scope, merge_producer_options(entry.binding, {:value => descriptor.value})) end # @api private def transform_InstanceProducerDescriptor(descriptor, scope, entry) make_producer(Producers::InstantiatingProducer, descriptor, scope, entry, merge_producer_options(entry.binding, {:class_name => descriptor.class_name, :init_args => descriptor.arguments})) end # @api private def transform_EvaluatingProducerDescriptor(descriptor, scope, entry) make_producer(Producers::EvaluatingProducer, descriptor, scope, entry, merge_producer_options(entry.binding, {:expression => descriptor.expression})) end # @api private def make_producer(clazz, descriptor, scope, entry, options) singleton_wrapped(descriptor, scope, entry, clazz.new(self, entry.binding, scope, options)) end # @api private def singleton_wrapped(descriptor, scope, entry, producer) return producer unless singleton?(descriptor) Producers::SingletonProducer.new(self, entry.binding, scope, merge_producer_options(entry.binding, {:value => producer.produce(scope)})) end # @api private def transform_ProducerProducerDescriptor(descriptor, scope, entry) p = transform(descriptor.producer, scope, entry) clazz = singleton?(descriptor) ? Producers::SingletonProducerProducer : Producers::ProducerProducer clazz.new(self, entry.binding, scope, merge_producer_options(entry.binding, merge_producer_options(entry.binding, { :producer_producer => p }))) end # @api private def transform_LookupProducerDescriptor(descriptor, scope, entry) make_producer(Producers::LookupProducer, descriptor, scope, entry, merge_producer_options(entry.binding, {:type => descriptor.type, :name => descriptor.name})) end # @api private def transform_HashLookupProducerDescriptor(descriptor, scope, entry) make_producer(Producers::LookupKeyProducer, descriptor, scope, entry, merge_producer_options(entry.binding, {:type => descriptor.type, :name => descriptor.name, :key => descriptor.key})) end # @api private def transform_NonCachingProducerDescriptor(descriptor, scope, entry) # simply delegates to the wrapped producer transform(descriptor.producer, scope, entry) end # @api private def transform_FirstFoundProducerDescriptor(descriptor, scope, entry) make_producer(Producers::FirstFoundProducer, descriptor, scope, entry, merge_producer_options(entry.binding, {:producers => descriptor.producers.collect {|p| transform(p, scope, entry) }})) end # @api private def singleton?(descriptor) ! descriptor.eContainer().is_a?(Puppet::Pops::Binder::Bindings::NonCachingProducerDescriptor) end # Special marker class used in entries # @api private class NotFound end end end -end \ No newline at end of file +end diff --git a/lib/puppet/pops/binder/key_factory.rb b/lib/puppet/pops/binder/key_factory.rb index 5abff5fa1..0b45d4f02 100644 --- a/lib/puppet/pops/binder/key_factory.rb +++ b/lib/puppet/pops/binder/key_factory.rb @@ -1,67 +1,67 @@ # The KeyFactory is responsible for creating keys used for lookup of bindings. # @api public # class Puppet::Pops::Binder::KeyFactory attr_reader :type_calculator # @api public def initialize(type_calculator = Puppet::Pops::Types::TypeCalculator.new()) @type_calculator = type_calculator end # @api public def binding_key(binding) named_key(binding.type, binding.name) end # @api public def named_key(type, name) [(@type_calculator.assignable?(@type_calculator.data, type) ? @type_calculator.data : type), name] end # @api public def data_key(name) [@type_calculator.data, name] end # @api public def is_contributions_key?(s) return false unless s.is_a?(String) s.start_with?('mc_') end # @api public def multibind_contributions(multibind_id) "mc_#{multibind_id}" end # @api public def multibind_contribution_key_to_id(contributions_key) # removes the leading "mc_" from the key to get the multibind_id contributions_key[3..-1] end # @api public def is_named?(key) key.is_a?(Array) && key[1] && !key[1].empty? end # @api public def is_data?(key) return false unless key.is_a?(Array) && key[0].is_a?(Puppet::Pops::Types::PObjectType) type_calculator.assignable?(type_calculator.data(), key[0]) end # @api public def is_ruby?(key) return key.is_a?(Array) && key[0].is_a?(Puppet::Pops::Types::PRubyType) end # Returns the type of the key # @api public # def get_type(key) return nil unless key.is_a?(Array) key[0] end -end \ No newline at end of file +end diff --git a/lib/puppet/pops/binder/producers.rb b/lib/puppet/pops/binder/producers.rb index 2cd8fb6db..c7f57dd71 100644 --- a/lib/puppet/pops/binder/producers.rb +++ b/lib/puppet/pops/binder/producers.rb @@ -1,829 +1,829 @@ # This module contains the various producers used by Puppet Bindings. # The main (abstract) class is {Puppet::Pops::Binder::Producers::Producer} which documents the # Producer API and serves as a base class for all other producers. # It is required that custom producers inherit from this producer (directly or indirectly). # # The selection of a Producer is typically performed by the Innjector when it configures itself # from a Bindings model where a {Puppet::Pops::Binder::Bindings::ProducerDescriptor} describes # which producer to use. The configuration uses this to create the concrete producer. # It is possible to describe that a particular producer class is to be used, and also to describe that # a custom producer (derived from Producer) should be used. This is available for both regular # bindings as well as multi-bindings. # # # @api public # module Puppet::Pops::Binder::Producers # Producer is an abstract base class representing the base contract for a bound producer. # Typically, when a lookup is performed it is the value that is returned (via a producer), but # it is also possible to lookup the producer, and ask it to produce the value (the producer may # return a series of values, which makes this especially useful). # # When looking up a producer, it is of importance to only use the API of the Producer class # unless it is known that a particular custom producer class has been bound. # # Custom Producers # ---------------- # The intent is that this class is derived for custom producers that require additional # options/arguments when producing an instance. Such a custom producer may raise an error if called # with too few arguments, or may implement specific `produce` methods and always raise an # error on #produce indicating that this producer requires custom calls and that it can not # be used as an implicit producer. # # Features of Producer # -------------------- # The Producer class is abstract, but offers the ability to transform the produced result # by passing the option `:transformer` which should be a Puppet Lambda Expression taking one argument # and producing the transformed (wanted) result. # # @abstract # @api public # class Producer # A Puppet 3 AST Lambda Expression # @api public # attr_reader :transformer # Creates a Producer. # Derived classes should call this constructor to get support for transformer lambda. # # @param injector [Puppet::Pops::Binder::Injector] The injector where the lookup originates # @param binding [Puppet::Pops::Binder::Bindings::Binding, nil] The binding using this producer # @param scope [Puppet::Parser::Scope] The scope to use for evaluation # @option options [Puppet::Pops::Model::LambdaExpression] :transformer (nil) a transformer of produced value # @api public # def initialize(injector, binding, scope, options) if transformer_lambda = options[:transformer] if transformer_lambda.is_a?(Proc) raise ArgumentError, "Transformer Proc must take two arguments; scope, value." unless transformer_lambda.arity == 2 @transformer = transformer_lambda else raise ArgumentError, "Transformer must be a LambdaExpression" unless transformer_lambda.is_a?(Puppet::Pops::Model::LambdaExpression) raise ArgumentError, "Transformer lambda must take one argument; value." unless transformer_lambda.parameters.size() == 1 # NOTE: This depends on Puppet 3 AST Lambda @transformer = Puppet::Pops::Model::AstTransformer.new().transform(transformer_lambda) end end end # Produces an instance. # @param scope [Puppet::Parser:Scope] the scope to use for evaluation # @param args [Object] arguments to custom producers, always empty for implicit productions # @return [Object] the produced instance (should never be nil). # @api public # def produce(scope, *args) do_transformation(scope, internal_produce(scope)) end # Returns the producer after possibly having recreated an internal/wrapped producer. # This implementation returns `self`. A derived class may want to override this method # to perform initialization/refresh of its internal state. This method is called when # a producer is requested. # @see Puppet::Pops::Binder::ProducerProducer for an example of implementation. # @param scope [Puppet::Parser:Scope] the scope to use for evaluation # @return [Puppet::Pops::Binder::Producer] the producer to use # @api public # def producer(scope) self end protected # Derived classes should implement this method to do the production of a value # @param scope [Puppet::Parser::Scope] the scope to use when performing lookup and evaluation # @raise [NotImplementedError] this implementation always raises an error # @abstract # @api private # def internal_produce(scope) raise NotImplementedError, "Producer-class '#{self.class.name}' should implement #internal_produce(scope)" end # Transforms the produced value if a transformer has been defined. # @param scope [Puppet::Parser::Scope] the scope used for evaluation # @param produced_value [Object, nil] the produced value (possibly nil) # @return [Object] the transformed value if a transformer is defined, else the given `produced_value` # @api private # def do_transformation(scope, produced_value) return produced_value unless transformer produced_value = :undef if produced_value.nil? transformer.call(scope, produced_value) end end # Abstract Producer holding a value # @abstract # @api public # class AbstractValueProducer < Producer # @api public attr_reader :value # @param injector [Puppet::Pops::Binder::Injector] The injector where the lookup originates # @param binding [Puppet::Pops::Binder::Bindings::Binding, nil] The binding using this producer # @param scope [Puppet::Parser::Scope] The scope to use for evaluation # @option options [Puppet::Pops::Model::LambdaExpression] :transformer (nil) a transformer of produced value # @option options [Puppet::Pops::Model::LambdaExpression, nil] :value (nil) the value to produce # @api public # def initialize(injector, binding, scope, options) super # nil is ok here, as an abstract value producer may be used to signal "not found" @value = options[:value] end end # Produces the same/singleton value on each production # @api public # class SingletonProducer < AbstractValueProducer protected # @api private def internal_produce(scope) value() end end # Produces a deep clone of its value on each production. # @api public # class DeepCloningProducer < AbstractValueProducer protected # @api private def internal_produce(scope) case value when Integer, Float, TrueClass, FalseClass, Symbol # These are immutable return value when String # ok if frozen, else fall through to default return value() if value.frozen? end # The default: serialize/deserialize to get a deep copy Marshal.load(Marshal.dump(value())) end end # This abstract producer class remembers the injector and binding. # @abstract # @api public # class AbstractArgumentedProducer < Producer # @api public attr_reader :injector # @api public attr_reader :binding # @param injector [Puppet::Pops::Binder::Injector] The injector where the lookup originates # @param binding [Puppet::Pops::Binder::Bindings::Binding, nil] The binding using this producer # @param scope [Puppet::Parser::Scope] The scope to use for evaluation # @option options [Puppet::Pops::Model::LambdaExpression] :transformer (nil) a transformer of produced value # @api public # def initialize(injector, binding, scope, options) super @injector = injector @binding = binding end end # @api public class InstantiatingProducer < AbstractArgumentedProducer # @api public attr_reader :the_class # @api public attr_reader :init_args # @param injector [Puppet::Pops::Binder::Injector] The injector where the lookup originates # @param binding [Puppet::Pops::Binder::Bindings::Binding, nil] The binding using this producer # @param scope [Puppet::Parser::Scope] The scope to use for evaluation # @option options [Puppet::Pops::Model::LambdaExpression] :transformer (nil) a transformer of produced value # @option options [String] :class_name The name of the class to create instance of # @option options [Array] :init_args ([]) Optional arguments to class constructor # @api public # def initialize(injector, binding, scope, options) # Better do this, even if a transformation of a created instance is kind of an odd thing to do, one can imagine # sending it to a function for further detailing. # super class_name = options[:class_name] raise ArgumentError, "Option 'class_name' must be given for an InstantiatingProducer" unless class_name # get class by name @the_class = Puppet::Pops::Types::ClassLoader.provide(class_name) @init_args = options[:init_args] || [] raise ArgumentError, "Can not load the class #{class_name} specified in binding named: '#{binding.name}'" unless @the_class end protected # Performs initialization the same way as Assisted Inject does (but handle arguments to # constructor) # @api private # def internal_produce(scope) result = nil # A class :inject method wins over an instance :initialize if it is present, unless a more specific # constructor exists. (i.e do not pick :inject from superclass if class has a constructor). # if the_class.respond_to?(:inject) inject_method = the_class.method(:inject) initialize_method = the_class.instance_method(:initialize) if inject_method.owner <= initialize_method.owner result = the_class.inject(injector, scope, binding, *init_args) end end if result.nil? result = the_class.new(*init_args) end result end end # @api public class FirstFoundProducer < Producer # @api public attr_reader :producers # @param injector [Puppet::Pops::Binder::Injector] The injector where the lookup originates # @param binding [Puppet::Pops::Binder::Bindings::Binding, nil] The binding using this producer # @param scope [Puppet::Parser::Scope] The scope to use for evaluation # @option options [Puppet::Pops::Model::LambdaExpression] :transformer (nil) a transformer of produced value # @option options [Array] :producers list of producers to consult. Required. # @api public # def initialize(injector, binding, scope, options) super @producers = options[:producers] raise ArgumentError, "Option :producers' must be set to a list of producers." if @producers.nil? raise ArgumentError, "Given 'producers' option is not an Array" unless @producers.is_a?(Array) end protected # @api private def internal_produce(scope) # return the first produced value that is non-nil (unfortunately there is no such enumerable method) producers.reduce(nil) {|memo, p| break memo unless memo.nil?; p.produce(scope)} end end # Evaluates a Puppet Expression and returns the result. # This is typically used for strings with interpolated expressions. # @api public # class EvaluatingProducer < Producer # A Puppet 3 AST Expression # @api public # attr_reader :expression # @param injector [Puppet::Pops::Binder::Injector] The injector where the lookup originates # @param binding [Puppet::Pops::Binder::Bindings::Binding, nil] The binding using this producer # @param scope [Puppet::Parser::Scope] The scope to use for evaluation # @option options [Puppet::Pops::Model::LambdaExpression] :transformer (nil) a transformer of produced value # @option options [Array] :expression The expression to evaluate # @api public # def initialize(injector, binding, scope, options) super expr = options[:expression] raise ArgumentError, "Option 'expression' must be given to an EvaluatingProducer." unless expr @expression = Puppet::Pops::Model::AstTransformer.new().transform(expr) end # @api private def internal_produce(scope) expression.evaluate(scope) end end # @api public class LookupProducer < AbstractArgumentedProducer # @api public attr_reader :type # @api public attr_reader :name # @param injector [Puppet::Pops::Binder::Injector] The injector where the lookup originates # @param binding [Puppet::Pops::Binder::Bindings::Binding, nil] The binding using this producer # @param scope [Puppet::Parser::Scope] The scope to use for evaluation # @option options [Puppet::Pops::Model::LambdaExpression] :transformer (nil) a transformer of produced value # @option options [Puppet::Pops::Types::PObjectType] :type The type to lookup # @option options [String] :name ('') The name to lookup # @api public # def initialize(injector, binder, scope, options) super @type = options[:type] @name = options[:name] || '' raise ArgumentError, "Option 'type' must be given in a LookupProducer." unless @type end protected # @api private def internal_produce(scope) injector.lookup_type(scope, type, name) end end # @api public class LookupKeyProducer < LookupProducer # @api public attr_reader :key # @param injector [Puppet::Pops::Binder::Injector] The injector where the lookup originates # @param binding [Puppet::Pops::Binder::Bindings::Binding, nil] The binding using this producer # @param scope [Puppet::Parser::Scope] The scope to use for evaluation # @option options [Puppet::Pops::Model::LambdaExpression] :transformer (nil) a transformer of produced value # @option options [Puppet::Pops::Types::PObjectType] :type The type to lookup # @option options [String] :name ('') The name to lookup # @option options [Puppet::Pops::Types::PObjectType] :key The key to lookup in the hash # @api public # def initialize(injector, binder, scope, options) super @key = options[:key] raise ArgumentError, "Option 'key' must be given in a LookupKeyProducer." if key.nil? end protected # @api private def internal_produce(scope) result = super result.is_a?(Hash) ? result[key] : nil end end # Produces the given producer, then uses that producer. # @see ProducerProducer for the non singleton version # @api public # class SingletonProducerProducer < Producer # @api public attr_reader :value_producer # @param injector [Puppet::Pops::Binder::Injector] The injector where the lookup originates # @param binding [Puppet::Pops::Binder::Bindings::Binding, nil] The binding using this producer # @param scope [Puppet::Parser::Scope] The scope to use for evaluation # @option options [Puppet::Pops::Model::LambdaExpression] :transformer (nil) a transformer of produced value # @option options [Puppet::Pops::Model::LambdaExpression] :producer_producer a producer of a value producer (required) # @api public # def initialize(injector, binding, scope, options) super p = options[:producer_producer] raise ArgumentError, "Option :producer_producer must be given in a SingletonProducerProducer" unless p @value_producer = p.produce(scope) end protected # @api private def internal_produce(scope) value_producer.produce(scope) end end # A ProducerProducer creates a producer via another producer, and then uses this created producer # to produce values. This is useful for custom production of series of values. # On each request for a producer, this producer will reset its internal producer (i.e. restarting # the series). # # @param producer_producer [#produce(scope)] the producer of the producer # # @api public # class ProducerProducer < Producer # @api public attr_reader :producer_producer # @api public attr_reader :value_producer # Creates new ProducerProducer given a producer. # # @param injector [Puppet::Pops::Binder::Injector] The injector where the lookup originates # @param binding [Puppet::Pops::Binder::Bindings::Binding, nil] The binding using this producer # @param scope [Puppet::Parser::Scope] The scope to use for evaluation # @option options [Puppet::Pops::Model::LambdaExpression] :transformer (nil) a transformer of produced value # @option options [Puppet::Pops::Binder::Producer] :producer_producer a producer of a value producer (required) # # @api public # def initialize(injector, binding, scope, options) super unless producer_producer = options[:producer_producer] raise ArgumentError, "The option :producer_producer must be set in a ProducerProducer" end raise ArgumentError, "Argument must be a Producer" unless producer_producer.is_a?(Producer) @producer_producer = producer_producer @value_producer = nil end # Updates the internal state to use a new instance of the wrapped producer. # @api public # def producer(scope) @value_producer = @producer_producer.produce(scope) self end protected # Produces a value after having created an instance of the wrapped producer (if not already created). # @api private # def internal_produce(scope, *args) producer() unless value_producer value_producer.produce(scope) end end # This type of producer should only be created by the Injector. # # @api private # class AssistedInjectProducer < Producer # An Assisted Inject Producer is created when a lookup is made of a type that is # not bound. It does not support a transformer lambda. # @note This initializer has a different signature than all others. Do not use in regular logic. # @api private # def initialize(injector, clazz) raise ArgumentError, "class must be given" unless clazz.is_a?(Class) @injector = injector @clazz = clazz @inst = nil end def produce(scope, *args) producer(scope, *args) unless @inst @inst end # @api private def producer(scope, *args) @inst = nil # A class :inject method wins over an instance :initialize if it is present, unless a more specific zero args # constructor exists. (i.e do not pick :inject from superclass if class has a zero args constructor). # if @clazz.respond_to?(:inject) inject_method = @clazz.method(:inject) initialize_method = @clazz.instance_method(:initialize) if inject_method.owner <= initialize_method.owner || initialize_method.arity != 0 @inst = @clazz.inject(@injector, scope, nil, *args) end end if @inst.nil? unless args.empty? raise ArgumentError, "Assisted Inject can not pass arguments to no-args constructor when there is no class inject method." end @inst = @clazz.new() end self end end # Abstract base class for multibind producers. # Is suitable as base class for custom implementations of multibind producers. # @abstract # @api public # class MultibindProducer < AbstractArgumentedProducer attr_reader :contributions_key # @param injector [Puppet::Pops::Binder::Injector] The injector where the lookup originates # @param binding [Puppet::Pops::Binder::Bindings::Binding, nil] The binding using this producer # @param scope [Puppet::Parser::Scope] The scope to use for evaluation # @option options [Puppet::Pops::Model::LambdaExpression] :transformer (nil) a transformer of produced value # # @api public # def initialize(injector, binding, scope, options) super @contributions_key = injector.key_factory.multibind_contributions(binding.id) end # @param expected [Array, Puppet::Pops::Types::PObjectType] expected type or types # @param actual [Object, Puppet::Pops::Types::PObjectType> the actual value (or its type) # @return [String] a formatted string for inclusion as detail in an error message # @api private # def type_error_detail(expected, actual) tc = injector.type_calculator expected = [expected] unless expected.is_a?(Array) actual_t = tc.is_ptype?(actual) ? actual : tc.infer(actual) expstrs = expected.collect {|t| tc.string(t) } "expected: #{expstrs.join(', or ')}, got: #{tc.string(actual_t)}" end end # A configurable multibind producer for Array type multibindings. # # This implementation collects all contributions to the multibind and then combines them using the following rules: # # - all *unnamed* entries are added unless the option `:priority_on_unnamed` is set to true, in which case the unnamed # contribution with the highest priority is added, and the rest are ignored (unless they have the same priority in which # case an error is raised). # - all *named* entries are handled the same way as *unnamed* but the option `:priority_on_named` controls their handling. # - the option `:uniq` post processes the result to only contain unique entries # - the option `:flatten` post processes the result by flattening all nested arrays. # - If both `:flatten` and `:uniq` are true, flattening is done first. # # @note # Collection accepts elements that comply with the array's element type, or the entire type (i.e. Array[element_type]). # If the type is restrictive - e.g. Array[String] and an Array[String] is contributed, the result will not be type # compliant without also using the `:flatten` option, and a type error will be raised. For an array with relaxed typing # i.e. Array[Data], it it valid to produce a result such as `['a', ['b', 'c'], 'd']` and no flattening is required # and no error is raised (but using the array needs to be aware of potential array, non-array entries. # The use of the option `:flatten` controls how the result is flattened. # # @api public # class ArrayMultibindProducer < MultibindProducer # @return [Boolean] whether the result should be made contain unique (non-equal) entries or not # @api public attr_reader :uniq # @return [Boolean, Integer] If result should be flattened (true), or not (false), or flattened to given level (0 = none, -1 = all) # @api public attr_reader :flatten # @return [Boolean] whether priority should be considered for named contributions # @api public attr_reader :priority_on_named # @return [Boolean] whether priority should be considered for unnamed contributions # @api public attr_reader :priority_on_unnamed # @param injector [Puppet::Pops::Binder::Injector] The injector where the lookup originates # @param binding [Puppet::Pops::Binder::Bindings::Binding, nil] The binding using this producer # @param scope [Puppet::Parser::Scope] The scope to use for evaluation # @option options [Puppet::Pops::Model::LambdaExpression] :transformer (nil) a transformer of produced value # @option options [Boolean] :uniq (false) if collected result should be post-processed to contain only unique entries # @option options [Boolean, Integer] :flatten (false) if collected result should be post-processed so all contained arrays # are flattened. May be set to an Integer value to indicate the level of recursion (-1 is endless, 0 is none). # @option options [Boolean] :priority_on_named (true) if highest precedented named element should win or if all should be included # @option options [Boolean] :priority_on_unnamed (false) if highest precedented unnamed element should win or if all should be included # @api public # def initialize(injector, binding, scope, options) super @uniq = !!options[:uniq] @flatten = options[:flatten] @priority_on_named = options[:priority_on_named].nil? ? true : options[:priority_on_name] @priority_on_unnamed = !!options[:priority_on_unnamed] case @flatten when Integer when true @flatten = -1 when false @flatten = nil when NilClass @flatten = nil else raise ArgumentError, "Option :flatten must be nil, Boolean, or an integer value" unless @flatten.is_a?(Integer) end end protected # @api private def internal_produce(scope) seen = {} included_keys = [] injector.get_contributions(scope, contributions_key).each do |element| key = element[0] entry = element[1] name = entry.binding.name existing = seen[name] empty_name = name.nil? || name.empty? if existing if empty_name && priority_on_unnamed if (seen[name] <=> entry) >= 0 raise ArgumentError, "Duplicate key (same priority) contributed to Array Multibinding '#{binding.name}' with unnamed entry." end next elsif !empty_name && priority_on_named if (seen[name] <=> entry) >= 0 raise ArgumentError, "Duplicate key (same priority) contributed to Array Multibinding '#{binding.name}', key: '#{name}'." end next end else seen[name] = entry end included_keys << key end result = included_keys.collect do |k| x = injector.lookup_key(scope, k) assert_type(binding(), injector.type_calculator(), x) x end result.flatten!(flatten) if flatten result.uniq! if uniq result end # @api private def assert_type(binding, tc, value) infered = tc.infer(value) unless tc.assignable?(binding.type.element_type, infered) || tc.assignable?(binding.type, infered) raise ArgumentError, ["Type Error: contribution to '#{binding.name}' does not match type of multibind, ", "#{type_error_detail([binding.type.element_type, binding.type], value)}"].join() end end end # @api public class HashMultibindProducer < MultibindProducer # @return [Symbol] One of `:error`, `:merge`, `:append`, `:priority`, `:ignore` # @api public attr_reader :conflict_resolution # @return [Boolean] # @api public attr_reader :uniq # @return [Boolean, Integer] Flatten all if true, or none if false, or to given level (0 = none, -1 = all) # @api public attr_reader :flatten # The hash multibind producer provides options to control conflict resolution. # By default, the hash is produced using `:priority` resolution - the highest entry is selected, the rest are # ignored unless they have the same priority which is an error. # # @param injector [Puppet::Pops::Binder::Injector] The injector where the lookup originates # @param binding [Puppet::Pops::Binder::Bindings::Binding, nil] The binding using this producer # @param scope [Puppet::Parser::Scope] The scope to use for evaluation # @option options [Puppet::Pops::Model::LambdaExpression] :transformer (nil) a transformer of produced value # @option options [Symbol, String] :conflict_resolution (:priority) One of `:error`, `:merge`, `:append`, `:priority`, `:ignore` #
  • `ignore` the first found highest priority contribution is used, the rest are ignored
  • #
  • `error` any duplicate key is an error
  • #
  • `append` element type must be compatible with Array, makes elements be arrays and appends all found
  • #
  • `merge` element type must be compatible with hash, merges hashes with retention of highest priority hash content
  • #
  • `priority` the first found highest priority contribution is used, duplicates with same priority raises and error, the rest are # ignored.
# @option options [Boolean, Integer] :flatten (false) If appended should be flattened. Also see {#flatten}. # @option options [Boolean] :uniq (false) If appended result should be made unique. # # @api public # def initialize(injector, binding, scope, options) super @conflict_resolution = options[:conflict_resolution].nil? ? :priority : options[:conflict_resolution] @uniq = !!options[:uniq] @flatten = options[:flatten] unless [:error, :merge, :append, :priority, :ignore].include?(@conflict_resolution) raise ArgumentError, "Unknown conflict_resolution for Multibind Hash: '#{@conflict_resolution}." end case @flatten when Integer when true @flatten = -1 when false @flatten = nil when NilClass @flatten = nil else raise ArgumentError, "Option :flatten must be nil, Boolean, or an integer value" unless @flatten.is_a?(Integer) end if uniq || flatten || conflict_resolution.to_s == 'append' etype = binding.type.element_type unless etype.class == Puppet::Pops::Types::PDataType || etype.is_a?(Puppet::Pops::Types::PArrayType) detail = [] detail << ":uniq" if uniq detail << ":flatten" if flatten detail << ":conflict_resolution => :append" if conflict_resolution.to_s == 'append' raise ArgumentError, ["Options #{detail.join(', and ')} cannot be used with a Multibind ", "of type #{injector.type_calculator.string(binding.type)}"].join() end end end protected # @api private def internal_produce(scope) seen = {} included_entries = [] injector.get_contributions(scope, contributions_key).each do |element| key = element[0] entry = element[1] name = entry.binding.name raise ArgumentError, "A Hash Multibind contribution to '#{binding.name}' must have a name." if name.nil? || name.empty? existing = seen[name] if existing case conflict_resolution.to_s when 'priority' # skip if duplicate has lower prio if (comparison = (seen[name] <=> entry)) <= 0 raise ArgumentError, "Internal Error: contributions not given in decreasing precedence order" unless comparison == 0 raise ArgumentError, "Duplicate key (same priority) contributed to Hash Multibinding '#{binding.name}', key: '#{name}'." end next when 'ignore' # skip, ignore conflict if prio is the same next when 'error' raise ArgumentError, "Duplicate key contributed to Hash Multibinding '#{binding.name}', key: '#{name}'." end else seen[name] = entry end included_entries << [key, entry] end result = {} included_entries.each do |element| k = element[ 0 ] entry = element[ 1 ] x = injector.lookup_key(scope, k) name = entry.binding.name assert_type(binding(), injector.type_calculator(), name, x) if result[ name ] merge(result, name, result[ name ], x) else result[ name ] = conflict_resolution().to_s == 'append' ? [x] : x end end result end # @api private def merge(result, name, higher, lower) case conflict_resolution.to_s when 'append' unless higher.is_a?(Array) higher = [higher] end tmp = higher + [lower] tmp.flatten!(flatten) if flatten tmp.uniq! if uniq result[name] = tmp when 'merge' result[name] = lower.merge(higher) end end # @api private def assert_type(binding, tc, key, value) unless tc.instance?(binding.type.key_type, key) raise ArgumentError, ["Type Error: key contribution to #{binding.name}['#{key}'] ", "is incompatible with key type: #{tc.label(binding.type)}, ", type_error_detail(binding.type.key_type, key)].join() end if key.nil? || !key.is_a?(String) || key.empty? raise ArgumentError, "Entry contributing to multibind hash with id '#{binding.id}' must have a name." end unless tc.instance?(binding.type.element_type, value) raise ArgumentError, ["Type Error: value contribution to #{binding.name}['#{key}'] ", "is incompatible, ", type_error_detail(binding.type.element_type, value)].join() end end end -end \ No newline at end of file +end diff --git a/lib/puppet/pops/evaluator/runtime3_support.rb b/lib/puppet/pops/evaluator/runtime3_support.rb index d7c1ee062..8c92c315a 100644 --- a/lib/puppet/pops/evaluator/runtime3_support.rb +++ b/lib/puppet/pops/evaluator/runtime3_support.rb @@ -1,489 +1,489 @@ # A module with bindings between the new evaluator and the 3x runtime. # The intention is to separate all calls into scope, compiler, resource, etc. in this module # to make it easier to later refactor the evaluator for better implementations of the 3x classes. # # @api private module Puppet::Pops::Evaluator::Runtime3Support # Fails the evaluation of _semantic_ with a given issue. # # @param issue [Puppet::Pops::Issue] the issue to report # @param semantic [Puppet::Pops::ModelPopsObject] the object for which evaluation failed in some way. Used to determine origin. # @param options [Hash] hash of optional named data elements for the given issue # @return [!] this method does not return # @raise [Puppet::ParseError] an evaluation error initialized from the arguments (TODO: Change to EvaluationError?) # def fail(issue, semantic, options={}, except=nil) if except.nil? # Want a stacktrace, and it must be passed as an exception begin raise EvaluationError.new() rescue EvaluationError => e except = e end end diagnostic_producer.accept(issue, semantic, options, except) end # Binds the given variable name to the given value in the given scope. # The reference object `o` is intended to be used for origin information - the 3x scope implementation # only makes use of location when there is an error. This is now handled by other mechanisms; first a check # is made if a variable exists and an error is raised if attempting to change an immutable value. Errors # in name, numeric variable assignment etc. have also been validated prior to this call. In the event the # scope.setvar still raises an error, the general exception handling for evaluation of the assignment # expression knows about its location. Because of this, there is no need to extract the location for each # setting (extraction is somewhat expensive since 3x requires line instead of offset). # def set_variable(name, value, o, scope) # Scope also checks this but requires that location information are passed as options. # Those are expensive to calculate and a test is instead made here to enable failing with better information. # The error is not specific enough to allow catching it - need to check the actual message text. # TODO: Improve the messy implementation in Scope. # if scope.bound?(name) if Puppet::Parser::Scope::RESERVED_VARIABLE_NAMES.include?(name) fail(Puppet::Pops::Issues::ILLEGAL_RESERVED_ASSIGNMENT, o, {:name => name} ) else fail(Puppet::Pops::Issues::ILLEGAL_REASSIGNMENT, o, {:name => name} ) end end scope.setvar(name, value) end # Returns the value of the variable (nil is returned if variable has no value, or if variable does not exist) # def get_variable_value(name, o, scope) # Puppet 3x stores all variables as strings (then converts them back to numeric with a regexp... to see if it is a match variable) # Not ideal, scope should support numeric lookup directly instead. # TODO: consider fixing scope catch(:undefined_variable) { return scope.lookupvar(name.to_s) } # It is always ok to reference numeric variables even if they are not assigned. They are always undef # if not set by a match expression. # unless name =~ Puppet::Pops::Patterns::NUMERIC_VAR_NAME fail(Puppet::Pops::Issues::UNKNOWN_VARIABLE, o, {:name => name}) end end # Returns true if the variable of the given name is set in the given most nested scope. True is returned even if # variable is bound to nil. # def variable_bound?(name, scope) scope.bound?(name.to_s) end # Returns true if the variable is bound to a value or nil, in the scope or it's parent scopes. # def variable_exists?(name, scope) scope.exist?(name.to_s) end def set_match_data(match_data, o, scope) # See set_variable for rationale for not passing file and line to ephemeral_from. # NOTE: The 3x scope adds one ephemeral(match) to its internal stack per match that succeeds ! It never # clears anything. Thus a context that performs many matches will get very deep (there simply is no way to # clear the match variables without rolling back the ephemeral stack.) # This implementation does not attempt to fix this, it behaves the same bad way. unless match_data.nil? scope.ephemeral_from(match_data) end end # Creates a local scope with vairalbes set from a hash of variable name to value # def create_local_scope_from(hash, scope) # two dummy values are needed since the scope tries to give an error message (can not happen in this # case - it is just wrong, the error should be reported by the caller who knows in more detail where it # is in the source. # raise ArgumentError, "Internal error - attempt to create a local scope without a hash" unless hash.is_a?(Hash) scope.ephemeral_from(hash) end # Creates a nested match scope def create_match_scope_from(scope) # Create a transparent match scope (for future matches) scope.new_match_scope(nil) end def get_scope_nesting_level(scope) scope.ephemeral_level end def set_scope_nesting_level(scope, level) # Yup, 3x uses this method to reset the level, it also supports passing :all to destroy all # ephemeral/local scopes - which is a sure way to create havoc. # scope.unset_ephemeral_var(level) end # Adds a relationship between the given `source` and `target` of the given `relationship_type` # @param source [Puppet:Pops::Types::PCatalogEntryType] the source end of the relationship (from) # @param target [Puppet:Pops::Types::PCatalogEntryType] the target end of the relationship (to) # @param relationship_type [:relationship, :subscription] the type of the relationship # def add_relationship(source, target, relationship_type, scope) # The 3x way is to record a Puppet::Parser::Relationship that is evaluated at the end of the compilation. # This means it is not possible to detect any duplicates at this point (and signal where an attempt is made to # add a duplicate. There is also no location information to signal the original place in the logic. The user will have # to go fish. # The 3.x implementation is based on Strings :-o, so the source and target must be transformed. The resolution is # done by Catalog#resource(type, title). To do that, it creates a Puppet::Resource since it is responsible for # translating the name/type/title and create index-keys used by the catalog. The Puppet::Resource has bizarre parsing of # the type and title (scan for [] that is interpreted as type/title (but it gets it wrong). # Moreover if the type is "" or "component", the type is Class, and if the type is :main, it is :main, all other cases # undergo capitalization of name-segments (foo::bar becomes Foo::Bar). (This was earlier done in the reverse by the parser). # Further, the title undergoes the same munging !!! # # That bug infested nest of messy logic needs serious Exorcism! # # Unfortunately it is not easy to simply call more intelligent methods at a lower level as the compiler evaluates the recorded # Relationship object at a much later point, and it is responsible for invoking all the messy logic. # # TODO: Revisit the below logic when there is a sane implementation of the catalog, compiler and resource. For now # concentrate on transforming the type references to what is expected by the wacky logic. # # HOWEVER, the Compiler only records the Relationships, and the only method it calls is @relationships.each{|x| x.evaluate(catalog) } # Which means a smarter Relationship class could do this right. Instead of obtaining the resource from the catalog using # the borked resource(type, title) which creates a resource for the purpose of looking it up, it needs to instead # scan the catalog's resources # # GAAAH, it is even worse! # It starts in the parser, which parses "File['foo']" into an AST::ResourceReference with type = File, and title = foo # This AST is evaluated by looking up the type/title in the scope - causing it to be loaded if it exists, and if not, the given # type name/title is used. It does not search for resource instances, only classes and types. It returns symbolic information # [type, [title, title]]. From this, instances of Puppet::Resource are created and returned. These only have type/title information # filled out. One or an array of resources are returned. # This set of evaluated (empty reference) Resource instances are then passed to the relationship operator. It creates a # Puppet::Parser::Relationship giving it a source and a target that are (empty reference) Resource instances. These are then remembered # until the relationship is evaluated by the compiler (at the end). When evaluation takes place, the (empty reference) Resource instances # are converted to String (!?! WTF) on the simple format "#{type}[#{title}]", and the catalog is told to find a resource, by giving # it this string. If it cannot find the resource it fails, else the before/notify parameter is appended with the target. # The search for the resource begin with (you guessed it) again creating an (empty reference) resource from type and title (WTF?!?!). # The catalog now uses the reference resource to compute a key [r.type, r.title.to_s] and also gets a uniqueness key from the # resource (This is only a reference type created from title and type). If it cannot find it with the first key, it uses the # uniqueness key to lookup. # # This is probably done to allow a resource type to munge/translate the title in some way (but it is quite unclear from the long # and convoluted path of evaluation. # In order to do this in a way that is similar to 3.x two resources are created to be used as keys. # # # TODO: logic that creates a PCatalogEntryType should resolve it to ensure it is loaded (to the best of known_resource_types knowledge). # If this is not done, the order in which things are done may be different? OTOH, it probably works anyway :-) # TODO: Not sure if references needs to be resolved via the scope? # # And if that is not enough, a source/target may be a Collector (a baked query that will be evaluated by the # compiler - it is simply passed through here for processing by the compiler at the right time). # if source.is_a?(Puppet::Parser::Collector) # use verbatim - behavior defined by 3x source_resource = source else # transform into the wonderful String representation in 3x type, title = catalog_type_to_split_type_title(source) source_resource = Puppet::Resource.new(type, title) end if target.is_a?(Puppet::Parser::Collector) # use verbatim - behavior defined by 3x target_resource = target else # transform into the wonderful String representation in 3x type, title = catalog_type_to_split_type_title(target) target_resource = Puppet::Resource.new(type, title) end # Add the relationship to the compiler for later evaluation. scope.compiler.add_relationship(Puppet::Parser::Relationship.new(source_resource, target_resource, relationship_type)) end # Coerce value `v` to numeric or fails. # The given value `v` is coerced to Numeric, and if that fails the operation # calls {#fail}. # @param v [Object] the value to convert # @param o [Object] originating instruction # @param scope [Object] the (runtime specific) scope where evaluation of o takes place # @return [Numeric] value `v` converted to Numeric. # def coerce_numeric(v, o, scope) unless n = Puppet::Pops::Utils.to_n(v) fail(Puppet::Pops::Issues::NOT_NUMERIC, o, {:value => v}) end n end # Asserts that the given function name resolves to an available function. The function is loaded # as a side effect. Fails if the function does not exist. # def assert_function_available(name, o, scope) fail(Puppet::Pops::Issues::UNKNOWN_FUNCTION, o, {:name => name}) unless Puppet::Parser::Functions.function(name) end def call_function(name, args, o, scope) # Arguments must be mapped since functions are unaware of the new and magical creatures in 4x. # NOTE: Passing an empty string last converts :undef to empty string mapped_args = args.map {|a| convert(a, scope, '') } scope.send("function_#{name}", mapped_args) end # Returns true if the function produces a value def rvalue_function?(name, o, scope) Puppet::Parser::Functions.rvalue?(name) end # The o is used for source reference def create_resource_parameter(o, scope, name, value, operator) file, line = extract_file_line(o) Puppet::Parser::Resource::Param.new( :name => name, :value => convert(value, scope, :undef), # converted to 3x since 4x supports additional objects / types :source => scope.source, :line => line, :file => file, :add => operator == :'+>' ) end def create_resources(o, scope, virtual, exported, type_name, resource_titles, evaluated_parameters) # TODO: Unknown resource causes creation of Resource to fail with ArgumentError, should give # a proper Issue. Now the result is "Error while evaluating a Resource Statement" with the message # from the raised exception. (It may be good enough). # resolve in scope. fully_qualified_type, resource_titles = scope.resolve_type_and_titles(type_name, resource_titles) # Not 100% accurate as this is the resource expression location and each title is processed separately # The titles are however the result of evaluation and they have no location at this point (an array # of positions for the source expressions are required for this to work). # TODO: Revisit and possible improve the accuracy. # file, line = extract_file_line(o) # Build a resource for each title resource_titles.map do |resource_title| resource = Puppet::Parser::Resource.new( fully_qualified_type, resource_title, :parameters => evaluated_parameters, :file => file, :line => line, :exported => exported, :virtual => virtual, # WTF is this? Which source is this? The file? The name of the context ? :source => scope.source, :scope => scope, :strict => true ) if resource.resource_type.is_a? Puppet::Resource::Type resource.resource_type.instantiate_resource(scope, resource) end scope.compiler.add_resource(scope, resource) scope.compiler.evaluate_classes([resource_title], scope, false, true) if fully_qualified_type == 'class' # Turn the resource into a PType (a reference to a resource type) # weed out nil's resource_to_ptype(resource) end end # Defines default parameters for a type with the given name. # def create_resource_defaults(o, scope, type_name, evaluated_parameters) # Note that name must be capitalized in this 3x call # The 3x impl creates a Resource instance with a bogus title and then asks the created resource # for the type of the name. # Note, locations are available per parameter. # scope.define_settings(type_name.capitalize, evaluated_parameters) end # Creates resource overrides for all resource type objects in evaluated_resources. The same set of # evaluated parameters are applied to all. # def create_resource_overrides(o, scope, evaluated_resources, evaluated_parameters) # Not 100% accurate as this is the resource expression location and each title is processed separately # The titles are however the result of evaluation and they have no location at this point (an array # of positions for the source expressions are required for this to work. # TODO: Revisit and possible improve the accuracy. # file, line = extract_file_line(o) evaluated_resources.each do |r| resource = Puppet::Parser::Resource.new( r.type_name, r.title, :parameters => evaluated_parameters, :file => file, :line => line, # WTF is this? Which source is this? The file? The name of the context ? :source => scope.source, :scope => scope ) scope.compiler.add_override(resource) end end # Finds a resource given a type and a title. # def find_resource(scope, type_name, title) scope.compiler.findresource(type_name, title) end # Returns the value of a resource's parameter by first looking up the parameter in the resource # and then in the defaults for the resource. Since the resource exists (it must in order to look up its # parameters, any overrides have already been applied). Defaults are not applied to a resource until it # has been finished (which typically has not taked place when this is evaluated; hence the dual lookup). # def get_resource_parameter_value(scope, resource, parameter_name) val = resource[parameter_name] if val.nil? && defaults = scope.lookupdefaults(resource.type) # NOTE: 3x resource keeps defaults as hash using symbol for name as key to Parameter which (again) holds # name and value. param = defaults[parameter_name.to_sym] val = param.value end val end # Returns true, if the given name is the name of a resource parameter. # def is_parameter_of_resource?(scope, resource, name) resource.valid_parameter?(name) end def resource_to_ptype(resource) nil if resource.nil? type_calculator.infer(resource) end # This is the same type of "truth" as used in the current Puppet DSL. # def is_true? o # Is the value true? This allows us to control the definition of truth # in one place. case o when '' false when :undef false else !!o end end # Utility method for TrueClass || FalseClass # @param x [Object] the object to test if it is instance of TrueClass or FalseClass def is_boolean? x x.is_a?(TrueClass) || x.is_a?(FalseClass) end def initialize @@convert_visitor ||= Puppet::Pops::Visitor.new(self, "convert", 2, 2) end # Converts 4x supported values to 3x values. This is required because # resources and other objects do not know about the new type system, and does not support # regular expressions. Unfortunately this has to be done for array and hash as well. # A complication is that catalog types needs to be resolved against the scope. # def convert(o, scope, undef_value) @@convert_visitor.visit_this_2(self, o, scope, undef_value) end def convert_NilClass(o, scope, undef_value) undef_value end def convert_Object(o, scope, undef_value) o end def convert_Array(o, scope, undef_value) o.map {|x| convert(x, scope, undef_value) } end def convert_Hash(o, scope, undef_value) result = {} o.each {|k,v| result[convert(k, scope, undef_value)] = convert(v, scope, undef_value) } result end def convert_Regexp(o, scope, undef_value) # Puppet 3x cannot handle parameter values that are reqular expressions. Turn into regexp string in # source form o.inspect end def convert_Symbol(o, scope, undef_value) case o when :undef undef_value # 3x wants :undef as empty string in function else o # :default, and all others are verbatim since they are new in future evaluator end end def convert_PAbstractType(o, scope, undef_value) o end def convert_PResourceType(o,scope, undef_value) # Needs conversion by calling scope to resolve the name and possibly return a different name # Resolution can only be called with an array, and returns an array. Here there is only one name type, titles = scope.resolve_type_and_titles(o.type_name, [o.title]) Puppet::Resource.new(type, titles[0]) end def convert_PHostClassType(o, scope, undef_value) # Needs conversion by calling scope to resolve the name and possibly return a different name # Resolution can only be called with an array, and returns an array. Here there is only one name type, titles = scope.resolve_type_and_titles('class', [o.class_name]) Puppet::Resource.new(type, titles[0]) end private # Produces an array with [type, title] from a PCatalogEntryType # Used to produce reference resource instances (used when 3x is operating on a resource). # def catalog_type_to_split_type_title(catalog_type) case catalog_type when Puppet::Pops::Types::PHostClassType return ['Class', catalog_type.class_name] when Puppet::Pops::Types::PResourceType return [catalog_type.type_name, catalog_type.title] else raise ArgumentError, "Cannot split the type #{catalog_type.class}, it is neither a PHostClassType, nor a PResourceClass." end end def extract_file_line(o) source_pos = Puppet::Pops::Utils.find_closest_positioned(o) return [nil, -1] unless source_pos [source_pos.locator.file, source_pos.line] end def find_closest_positioned(o) return nil if o.nil? || o.is_a?(Puppet::Pops::Model::Program) o.offset.nil? ? find_closest_positioned(o.eContainer) : Puppet::Pops::Adapters::SourcePosAdapter.adapt(o) end # Creates a diagnostic producer def diagnostic_producer Puppet::Pops::Validation::DiagnosticProducer.new( ExceptionRaisingAcceptor.new(), # Raises exception on all issues Puppet::Pops::Validation::SeverityProducer.new(), # All issues are errors Puppet::Pops::Model::ModelLabelProvider.new()) end # An acceptor of diagnostics that immediately raises an exception. class ExceptionRaisingAcceptor < Puppet::Pops::Validation::Acceptor def accept(diagnostic) super Puppet::Pops::IssueReporter.assert_and_report(self, {:message => "Evaluation Error:" }) raise ArgumentError, "Internal Error: Configuration of runtime error handling wrong: should have raised exception" end end class EvaluationError < StandardError end -end \ No newline at end of file +end diff --git a/lib/puppet/pops/issue_reporter.rb b/lib/puppet/pops/issue_reporter.rb index 28ee971fe..2ba3f1088 100644 --- a/lib/puppet/pops/issue_reporter.rb +++ b/lib/puppet/pops/issue_reporter.rb @@ -1,80 +1,80 @@ class Puppet::Pops::IssueReporter # @param acceptor [Puppet::Pops::Validation::Acceptor] the acceptor containing reported issues # @option options [String] :message (nil) A message text to use as prefix in a single Error message # @option options [Boolean] :emit_warnings (false) A message text to use as prefix in a single Error message # @option options [Boolean] :emit_errors (true) whether errors should be emitted or only given message # @option options [Exception] :exception_class (Puppet::ParseError) The exception to raise # def self.assert_and_report(acceptor, options) return unless acceptor max_errors = Puppet[:max_errors] max_warnings = Puppet[:max_warnings] + 1 max_deprecations = Puppet[:max_deprecations] + 1 emit_warnings = options[:emit_warnings] || false emit_errors = options[:emit_errors].nil? ? true : !!options[:emit_errors] emit_message = options[:message] emit_exception = options[:exception_class] || Puppet::ParseError # If there are warnings output them warnings = acceptor.warnings if emit_warnings && warnings.size > 0 formatter = Puppet::Pops::Validation::DiagnosticFormatterPuppetStyle.new emitted_w = 0 emitted_dw = 0 acceptor.warnings.each do |w| if w.severity == :deprecation # Do *not* call Puppet.deprecation_warning it is for internal deprecation, not # deprecation of constructs in manifests! (It is not designed for that purpose even if # used throughout the code base). # Puppet.warning(formatter.format(w)) if emitted_dw < max_deprecations emitted_dw += 1 else Puppet.warning(formatter.format(w)) if emitted_w < max_warnings emitted_w += 1 end break if emitted_w > max_warnings && emitted_dw > max_deprecations # but only then end end # If there were errors, report the first found. Use a puppet style formatter. errors = acceptor.errors if errors.size > 0 unless emit_errors raise emit_exception.new(emit_message) end formatter = Puppet::Pops::Validation::DiagnosticFormatterPuppetStyle.new if errors.size == 1 || max_errors <= 1 # raise immediately exception = emit_exception.new(format_with_prefix(emit_message, formatter.format(errors[0]))) # if an exception was given as cause, use it's backtrace instead of the one indicating "here" if errors[0].exception exception.set_backtrace(errors[0].exception.backtrace) end raise exception end emitted = 0 if emit_message Puppet.err(emit_message) end errors.each do |e| Puppet.err(formatter.format(e)) emitted += 1 break if emitted >= max_errors end warnings_message = (emit_warnings && warnings.size > 0) ? ", and #{warnings.size} warnings" : "" giving_up_message = "Found #{errors.size} errors#{warnings_message}. Giving up" exception = emit_exception.new(giving_up_message) exception.file = errors[0].file raise exception end parse_result end def self.format_with_prefix(prefix, message) return message unless prefix [prefix, message].join(' ') end -end \ No newline at end of file +end diff --git a/lib/puppet/pops/parser/epp_support.rb b/lib/puppet/pops/parser/epp_support.rb index 3d1013690..aaf6f281b 100644 --- a/lib/puppet/pops/parser/epp_support.rb +++ b/lib/puppet/pops/parser/epp_support.rb @@ -1,244 +1,244 @@ # This module is an integral part of the Lexer. # It handles scanning of EPP (Embedded Puppet), a form of string/expression interpolation similar to ERB. # require 'strscan' module Puppet::Pops::Parser::EppSupport TOKEN_RENDER_STRING = [:RENDER_STRING, nil, 0] TOKEN_RENDER_EXPR = [:RENDER_EXPR, nil, 0] # Scans all of the content and returns it in an array # Note that the terminating [false, false] token is included in the result. # def fullscan_epp result = [] scan_epp {|token, value| result.push([token, value]) } result end # A block must be passed to scan. It will be called with two arguments, a symbol for the token, # and an instance of LexerSupport::TokenValue # PERFORMANCE NOTE: The TokenValue is designed to reduce the amount of garbage / termporary data # and to only convert the lexer's internal tokens on demand. It is slightly mroe costly to create an # instance of a class defined in Ruby than an Array or Hash, but the gain is much bigger since transformation # logic is avoided for many of its memebers (most are never used (e.g. line/pos information which is only of # value in general for error messages, and for some expressions (which the lexer does not know about). # def scan_epp # PERFORMANCE note: it is faster to access local variables than instance variables. # This makes a small but notable difference since instance member access is avoided for # every token in the lexed content. # scn = @scanner ctx = @lexing_context queue = @token_queue lex_error "Internal Error: No string or file given to lexer to process." unless scn ctx[:epp_mode] = :text interpolate_epp # This is the lexer's main loop until queue.empty? && scn.eos? do if token = queue.shift || lex_token yield [ ctx[:after] = token[0], token[1] ] end end if ctx[:epp_position] lex_error("Unbalanced epp tag, reached without closing tag.", ctx[:epp_position]) end # Signals end of input yield [false, false] end def interpolate_epp(skip_leading=false) scn = @scanner ctx = @lexing_context eppscanner = EppScanner.new(scn) before = scn.pos s = eppscanner.scan(skip_leading) case eppscanner.mode when :text # Should be at end of scan, or something is terribly wrong lex_error("Internal error: template scanner returns text mode and is not and end of input") unless @scanner.eos? if s # s may be nil if scanned text ends with an epp tag (i.e. no trailing text). enqueue_completed([:RENDER_STRING, s, scn.pos - before], before) end ctx[:epp_open_position] = nil # do nothing else, scanner is at the end when :error lex_error(eppscanner.message()) when :epp # It is meaningless to render empty string segments, and it is harmful to do this at # the start of the scan as it prevents specification of parameters with <%- ($x, $y) -%> # if s && s.length > 0 enqueue_completed([:RENDER_STRING, s, scn.pos - before], before) end # switch epp_mode to general (embedded) pp logic (non rendered result) ctx[:epp_mode] = :epp ctx[:epp_open_position] = scn.pos when :expr # It is meaningless to render an empty string segment if s && s.length > 0 enqueue_completed([:RENDER_STRING, s, scn.pos - before], before) end enqueue_completed(TOKEN_RENDER_EXPR, before) # switch mode to "epp expr interpolation" ctx[:epp_mode] = :expr ctx[:epp_open_position] = scn.pos else lex_error("Internal Error, Unknown mode #{eppscanner.mode} returned by template scanner") end nil end # A scanner specialized in processing text with embedded EPP (Embedded Puppet) tags. # The scanner is initialized with a StringScanner which it mutates as scanning takes place. # The intent is to use one instance of EppScanner per wanted scan, and this instance represents # the state after the scan. # # @example Sample usage # a = "some text <% pp code %> some more text" # scan = StringScanner.new(a) # eppscan = EppScanner.new(scan) # str = eppscan.scan # eppscan.mode # => :epp # eppscan.lines # => 0 # eppscan # # The scanner supports # * scanning text until <%, <%-, <%= # * while scanning text: # * tokens <%% and %%> are translated to <% and %> respetively and is returned as text. # * tokens <%# and %> (or ending with -%>) and the enclosed text is a comment and is not included in the returned text # * text following a comment that ends with -%> gets trailing whitespace (up to and including a line break) trimmed # and this whitespace is not included in the returned text. # * The continuation {#mode} is set to one of: # * `:epp` - for a <% token # * `:expr` - for a <%= token # * `:text` - when there was no continuation mode (e.g. when input ends with text) # * ':error` - if the tokens are unbalanced (reaching the end without a closing matching token). An error message # is then also available via the method {#message}. # # Note that the intent is to use this specialized scanner to scan the text parts, when continuation mode is `:epp` or `:expr` # the pp lexer should advance scanning (using the string scanner) until it reaches and consumes a `-%>` or '%>ยด token. If it # finds a `-%> token it should pass this on as a `skip_leading` parameter when it performs the next {#scan}. # class EppScanner # The original scanner used by the lexer/container using EppScanner attr_reader :scanner # The resulting mode after the scan. # The mode is one of `:text` (the initial mode), `:epp` embedded code (no output), `:expr` (embedded # expression), or `:error` # attr_reader :mode # An error message if `mode == :error`, `nil` otherwise. attr_reader :message # If the first scan should skip leading whitespace (typically detected by the pp lexer when the # pp mode end-token is found (i.e. `-%>`) and then passed on to the scanner. # attr_reader :skip_leading # Creates an EppScanner based on a StringScanner that represents the state where EppScanner should start scanning. # The given scanner will be mutated (i.e. position moved) to reflect the EppScanner's end state after a scan. # def initialize(scanner) @scanner = scanner end # Scans from the current position in the configured scanner, advances this scanner's position until the end # of the input, or to the first position after a mode switching token (`<%`, `<%-` or `<%=`). Number of processed # lines and continuation mode can be obtained via {#lines}, and {#mode}. # # @return [String, nil] the scanned and processed text, or nil if at the end of the input. # def scan(skip_leading=false) @mode = :text @skip_leading = skip_leading return nil if scanner.eos? s = "" until scanner.eos? part = @scanner.scan_until(/(<%)|\z/) if @skip_leading part.gsub!(/^[ \t]*\r?\n?/,'') @skip_leading = false end # The spec for %%> is to transform it into a literal %>. This is done here, as %%> otherwise would go # undetected in text mode. (i.e. it is not really necessary to escape %> with %%> in text mode unless # adding checks stating that a literal %> is illegal in text (unbalanced). # part.gsub!(/%%>/, '%>') s += part case @scanner.peek(1) when "" # at the end # if s ends with <% then this is an error (unbalanced <% %>) if s.end_with? "<%" @mode = :error @message = "Unbalanced embedded expression - opening <% and reaching end of input" else mode = :epp end return s when "-" # trim trailing whitespace on same line from accumulated s # return text and signal switch to pp mode @scanner.getch # drop the - s.gsub!(/\r?\n?[ \t]*<%\z/, '') @mode = :epp return s when "%" # verbatim text # keep the scanned <%, and continue scanning after skipping one % # (i.e. do nothing here) @scanner.getch # drop the % to get a literal <% in the output when "=" # expression # return text and signal switch to expression mode # drop the scanned <%, and skip past -%>, or %>, but also skip %%> @scanner.getch # drop the = s.slice!(-2..-1) @mode = :expr return s when "#" # template comment # drop the scanned <%, and skip past -%>, or %>, but also skip %%> s.slice!(-2..-1) # unless there is an immediate termination i.e. <%#%> scan for the next %> that is not # preceded by a % (i.e. skip %%>) part = scanner.scan_until(/[^%]%>/) unless part @message = "Reaching end after opening <%# without seeing %>" @mode = :error return s end @skip_leading = true if part.end_with?("-%>") # Continue scanning for more text else # Switch to pp after having removed the <% s.slice!(-2..-1) @mode = :epp return s end end end end -end \ No newline at end of file +end diff --git a/lib/puppet/pops/parser/heredoc_support.rb b/lib/puppet/pops/parser/heredoc_support.rb index eda606569..341b18a4e 100644 --- a/lib/puppet/pops/parser/heredoc_support.rb +++ b/lib/puppet/pops/parser/heredoc_support.rb @@ -1,190 +1,190 @@ module Puppet::Pops::Parser::HeredocSupport # Pattern for heredoc `@(endtag[:syntax][/escapes]) # Produces groups for endtag (group 1), syntax (group 2), and escapes (group 3) # PATTERN_HEREDOC = %r{@\(([^:/\r\n\)]+)(?::[:blank:]*([a-z][a-zA-Z0-9_+]+)[:blank:]*)?(?:/((?:\w|[$])*)[:blank:]*)?\)} def heredoc scn = @scanner ctx = @lexing_context locator = @locator before = scn.pos # scanner is at position before @( # find end of the heredoc spec str = scn.scan_until(/\)/) || lexer.lex_error("Unclosed parenthesis after '@(' followed by '#{followed_by}'") pos_after_heredoc = scn.pos # Note: allows '+' as separator in syntax, but this needs validation as empty segments are not allowed unless md = str.match(PATTERN_HEREDOC) lex_error("Invalid syntax in heredoc expected @(endtag[:syntax][/escapes])") end endtag = md[1] syntax = md[2] || '' escapes = md[3] endtag.strip! # Is this a dq string style heredoc? (endtag enclosed in "") if endtag =~ /^"(.*)"$/ dqstring_style = true endtag = $1.strip end lexer.lex_error("Missing endtag in heredoc") unless endtag.length >= 1 resulting_escapes = [] if escapes escapes = "trnsuL$" if escapes.length < 1 escapes = escapes.split('') unless escapes.length == escapes.uniq.length lex_error("An escape char for @() may only appear once. Got '#{escapes.join(', ')}") end resulting_escapes = ["\\"] escapes.each do |e| case e when "t", "r", "n", "s", "u", "$" resulting_escapes << e when "L" resulting_escapes += ["\n", "\r\n"] else lex_error("Invalid heredoc escape char. Only t, r, n, s, u, L, $ allowed. Got '#{e}'") end end end # Produce a heredoc token to make the syntax available to the grammar enqueue_completed([:HEREDOC, syntax, pos_after_heredoc - before], before) # If this is the second or subsequent heredoc on the line, the lexing context's :newline_jump contains # the position after the \n where the next heredoc text should scan. If not set, this is the first # and it should start scanning after the first found \n (or if not found == error). if ctx[:newline_jump] scn.pos = lexing_context[:newline_jump] else scn.scan_until(/\n/) || lex_error("Heredoc without any following lines of text") end # offset 0 for the heredoc, and its line number heredoc_offset = scn.pos heredoc_line = locator.line_for_offset(heredoc_offset)-1 # Compute message to emit if there is no end (to make it refer to the opening heredoc position). eof_message = positioned_message("Heredoc without end-tagged line") # Text from this position (+ lexing contexts offset for any preceding heredoc) is heredoc until a line # that terminates the heredoc is found. # (Endline in EBNF form): WS* ('|' WS*)? ('-' WS*)? endtag WS* \r? (\n|$) endline_pattern = /([[:blank:]]*)(?:([|])[[:blank:]]*)?(?:(\-)[[:blank:]]*)?#{Regexp.escape(endtag)}[[:blank:]]*\r?(?:\n|\z)/ lines = [] while !scn.eos? do one_line = scn.scan_until(/(?:\n|\z)/) || lexer.lex_error_without_pos(eof_message) if md = one_line.match(endline_pattern) leading = md[1] has_margin = md[2] == '|' remove_break = md[3] == '-' # Record position where next heredoc (from same line as current @()) should start scanning for content ctx[:newline_jump] = scn.pos # Process captured lines - remove leading, and trailing newline str = heredoc_text(lines, leading, has_margin, remove_break) # Use a new lexer instance configured with a sub-locator to enable correct positioning sublexer = self.class.new() locator = SubLocator.sub_locator(str, locator.file, heredoc_line, heredoc_offset, leading.length()) sublexer.lex_unquoted_string(str, locator, resulting_escapes, dqstring_style) sublexer.interpolate_uq_to(self) # Continue scan after @(...) scn.pos = pos_after_heredoc return else lines << one_line end end lex_error_without_pos(eof_message) end # Produces the heredoc text string given the individual (unprocessed) lines as an array. # @param lines [Array] unprocessed lines of text in the heredoc w/o terminating line # @param leading [String] the leading text up (up to pipe or other terminating char) # @param has_margin [Boolean] if the left margin should be adjusted as indicated by `leading` # @param remove_break [Boolean] if the line break (\r?\n) at the end of the last line should be removed or not # def heredoc_text(lines, leading, has_margin, remove_break) if has_margin leading_pattern = /^#{Regexp.escape(leading)}/ lines = lines.collect {|s| s.gsub(leading_pattern, '') } end result = lines.join('') result.gsub!(/\r?\n$/, '') if remove_break result end # A Sublocator locates a concrete locator (subspace) in a virtual space. # The `leading_line_count` is the (virtual) number of lines preceding the first line in the concrete locator. # The `leading_offset` is the (virtual) byte offset of the first byte in the concrete locator. # The `leading_line_offset` is the (virtual) offset / margin in characters for each line. # # This illustrates characters in the sublocator (`.`) inside the subspace (`X`): # # 1:XXXXXXXX # 2:XXXX.... .. ... .. # 3:XXXX. . .... .. # 4:XXXX............ # # This sublocator would be configured with leading_line_count = 1, # leading_offset=8, and leading_line_offset=4 # # Note that leading_offset must be the same for all lines and measured in characters. # class SubLocator < Puppet::Pops::Parser::Locator def self.sub_locator(string, file, leading_line_count, leading_offset, leading_line_offset) self.new(Puppet::Pops::Parser::Locator.locator(string, file), leading_line_count, leading_offset, leading_line_offset) end def initialize(locator, leading_line_count, leading_offset, leading_line_offset) @locator = locator @leading_line_count = leading_line_count @leading_offset = leading_offset @leading_line_offset = leading_line_offset end def file @locator.file end def string @locator.string end # Given offset is offset in the subspace def line_for_offset(offset) @locator.line_for_offset(offset) + @leading_line_count end # Given offset is offset in the subspace def offset_on_line(offset) @locator.offset_on_line + @leading_line_offset end # Given offset is offset in the subspace def char_offset(offset) effective_line = @locator.line_for_offset(offset) locator.char_offset(offset) + (effective_line * @leading_line_offset) + @leading_offset end # Given offsets are offsets in the subspace def char_length(offset, end_offset) effective_line = @locator.line_for_offset(end_offset) - @locator.line_for_offset(offset) locator.char_length(offset, end_offset) + (effective_line * @leading_line_offset) end end -end \ No newline at end of file +end diff --git a/lib/puppet/pops/parser/interpolation_support.rb b/lib/puppet/pops/parser/interpolation_support.rb index 696709acb..951be392a 100644 --- a/lib/puppet/pops/parser/interpolation_support.rb +++ b/lib/puppet/pops/parser/interpolation_support.rb @@ -1,227 +1,227 @@ # This module is an integral part of the Lexer. # It defines interpolation support # PERFORMANCE NOTE: There are 4 very similar methods in this module that are designed to be as # performant as possible. While it is possible to parameterize them into one common method, the overhead # of passing parameters and evaluating conditional logic has a negative impact on performance. # module Puppet::Pops::Parser::InterpolationSupport PATTERN_VARIABLE = %r{(::)?(\w+::)*\w+} # This is the starting point for a double quoted string with possible interpolation # The structure mimics that of the grammar. # The logic is explicit (where the former implementation used parameters/strucures) given to a # generic handler. # (This is both easier to understand and faster). # def interpolate_dq scn = @scanner ctx = @lexing_context before = scn.pos # skip the leading " by doing a scan since the slurp_dqstring uses last matched when there is an error scn.scan(/"/) value,terminator = slurp_dqstring() text = value after = scn.pos while true case terminator when '"' # simple case, there was no interpolation, return directly return emit_completed([:STRING, text, scn.pos-before], before) when '${' count = ctx[:brace_count] ctx[:brace_count] += 1 # The ${ terminator is counted towards the string part enqueue_completed([:DQPRE, text, scn.pos-before], before) # Lex expression tokens until a closing (balanced) brace count is reached enqueue_until count break when '$' if varname = scn.scan(PATTERN_VARIABLE) # The $ is counted towards the variable enqueue_completed([:DQPRE, text, after-before-1], before) enqueue_completed([:VARIABLE, varname, scn.pos - after + 1], after -1) break else # false $ variable start text += value value,terminator = slurp_dqstring() after = scn.pos end end end interpolate_tail_dq # return the first enqueued token and shift the queue @token_queue.shift end def interpolate_tail_dq scn = @scanner ctx = @lexing_context before = scn.pos value,terminator = slurp_dqstring text = value after = scn.pos while true case terminator when '"' # simple case, there was no further interpolation, return directly enqueue_completed([:DQPOST, text, scn.pos-before], before) return when '${' count = ctx[:brace_count] ctx[:brace_count] += 1 # The ${ terminator is counted towards the string part enqueue_completed([:DQMID, text, scn.pos-before], before) # Lex expression tokens until a closing (balanced) brace count is reached enqueue_until count break when '$' if varname = scn.scan(PATTERN_VARIABLE) # The $ is counted towards the variable enqueue_completed([:DQMID, text, after-before-1], before) enqueue_completed([:VARIABLE, varname, scn.pos - after +1], after -1) break else # false $ variable start text += value value,terminator = self.send(slurpfunc) after = scn.pos end end end interpolate_tail_dq end # This is the starting point for a un-quoted string with possible interpolation # The logic is explicit (where the former implementation used parameters/strucures) given to a # generic handler. # (This is both easier to understand and faster). # def interpolate_uq scn = @scanner ctx = @lexing_context before = scn.pos value,terminator = slurp_uqstring() text = value after = scn.pos while true case terminator when '' # simple case, there was no interpolation, return directly enqueue_completed([:STRING, text, scn.pos-before], before) return when '${' count = ctx[:brace_count] ctx[:brace_count] += 1 # The ${ terminator is counted towards the string part enqueue_completed([:DQPRE, text, scn.pos-before], before) # Lex expression tokens until a closing (balanced) brace count is reached enqueue_until count break when '$' if varname = scn.scan(PATTERN_VARIABLE) # The $ is counted towards the variable enqueue_completed([:DQPRE, text, after-before-1], before) enqueue_completed([:VARIABLE, varname, scn.pos - after + 1], after -1) break else # false $ variable start text += value value,terminator = slurp_uqstring() after = scn.pos end end end interpolate_tail_uq nil end def interpolate_tail_uq scn = @scanner ctx = @lexing_context before = scn.pos value,terminator = slurp_uqstring text = value after = scn.pos while true case terminator when '' # simple case, there was no further interpolation, return directly enqueue_completed([:DQPOST, text, scn.pos-before], before) return when '${' count = ctx[:brace_count] ctx[:brace_count] += 1 # The ${ terminator is counted towards the string part enqueue_completed([:DQMID, text, scn.pos-before], before) # Lex expression tokens until a closing (balanced) brace count is reached enqueue_until count break when '$' if varname = scn.scan(PATTERN_VARIABLE) # The $ is counted towards the variable enqueue_completed([:DQMID, text, after-before-1], before) enqueue_completed([:VARIABLE, varname, scn.pos - after +1], after -1) break else # false $ variable start text += value value,terminator = slurp_uqstring after = scn.pos end end end interpolate_tail_uq end # Enqueues lexed tokens until either end of input, or the given brace_count is reached # def enqueue_until brace_count scn = @scanner ctx = @lexing_context queue = @token_queue scn.skip(self.class::PATTERN_WS) queue_size = queue.size until scn.eos? do if token = lex_token token_name = token[0] ctx[:after] = token_name if token_name == :RBRACE && ctx[:brace_count] == brace_count if queue.size - queue_size == 1 # Single token is subject to replacement queue[-1] = transform_to_variable(queue[-1]) end return end queue << token else scn.skip(self.class::PATTERN_WS) end end end def transform_to_variable(token) token_name = token[0] if [:NUMBER, :NAME].include?(token_name) || self.class::KEYWORD_NAMES[token_name] t = token[1] ta = t.token_array [:VARIABLE, self.class::TokenValue.new([:VARIABLE, ta[1], ta[2]], t.offset, t.locator)] else token end end # Interpolates unquoted string and transfers the result to the given lexer # (This is used when a second lexer instance is used to lex a substring) # def interpolate_uq_to(lexer) interpolate_uq queue = @token_queue until queue.empty? do lexer.enqueue(queue.shift) end end -end \ No newline at end of file +end diff --git a/lib/puppet/pops/parser/lexer_support.rb b/lib/puppet/pops/parser/lexer_support.rb index 22c5703ab..c769255a5 100644 --- a/lib/puppet/pops/parser/lexer_support.rb +++ b/lib/puppet/pops/parser/lexer_support.rb @@ -1,107 +1,107 @@ # This is an integral part of the Lexer. It is broken out into a separate module # for maintainability of the code, and making the various parts of the lexer focused. # module Puppet::Pops::Parser::LexerSupport # Formats given message by appending file, line and position if available. def positioned_message(msg, pos = nil) result = [msg] file = @locator.file line = @locator.line_for_offset(pos || @scanner.pos) pos = @locator.pos_on_line(pos || @scanner.pos) result << "in file #{file}" if file && file.is_a?(String) && !file.empty? result << "at line #{line}:#{pos}" result.join(" ") end # Returns "" if at end of input, else the following 5 characters with \n \r \t escaped def followed_by return "" if @scanner.eos? result = @scanner.rest[0,5] + "..." result.gsub!("\t", '\t') result.gsub!("\n", '\n') result.gsub!("\r", '\r') result end # Returns a quoted string using " or ' depending on the given a strings's content def format_quote(q) if q == "'" '"\'"' else "'#{q}'" end end # Raises a Puppet::LexError with the given message def lex_error_without_pos msg raise Puppet::LexError.new(msg) end # Raises a Puppet::LexError with the given message def lex_error(msg, pos=nil) raise Puppet::LexError.new(positioned_message(msg, pos)) end # Asserts that the given string value is a float, or an integer in decimal, octal or hex form. # An error is raised if the given value does not comply. # def assert_numeric(value, length) if value =~ /^0[xX].*$/ lex_error("Not a valid hex number #{value}", length) unless value =~ /^0[xX][0-9A-Fa-f]+$/ elsif value =~ /^0[^.].*$/ lex_error("Not a valid octal number #{value}", length) unless value =~ /^0[0-7]+$/ else lex_error("Not a valid decimal number #{value}", length) unless value =~ /0?\d+(?:\.\d+)?(?:[eE]-?\d+)?/ end end # A TokenValue keeps track of the token symbol, the lexed text for the token, its length # and its position in its source container. There is a cost associated with computing the # line and position on line information. # class TokenValue < Puppet::Pops::Parser::Locatable attr_reader :token_array attr_reader :offset attr_reader :locator def initialize(token_array, offset, locator) @token_array = token_array @offset = offset @locator = locator end def length @token_array[2] end def [](key) case key when :value @token_array[1] when :file @locator.file when :line @locator.line_for_offset(@offset) when :pos @locator.pos_on_line(@offset) when :length @token_array[2] when :locator @locator when :offset @offset else nil end end # TODO: Make this comparable for testing # vs symbolic, vs array with symbol and non hash, array with symbol and hash) # end -end \ No newline at end of file +end diff --git a/lib/puppet/pops/parser/locator.rb b/lib/puppet/pops/parser/locator.rb index eaada1e06..7e97004f0 100644 --- a/lib/puppet/pops/parser/locator.rb +++ b/lib/puppet/pops/parser/locator.rb @@ -1,219 +1,219 @@ # Helper class that keeps track of where line breaks are located and can answer questions about positions. # class Puppet::Pops::Parser::Locator RUBY_1_9_3 = (1 << 16 | 9 << 8 | 3) RUBY_2_0_0 = (2 << 16 | 0 << 8 | 0) RUBYVER_ARRAY = RUBY_VERSION.split(".").collect {|s| s.to_i } RUBYVER = (RUBYVER_ARRAY[0] << 16 | RUBYVER_ARRAY[1] << 8 | RUBYVER_ARRAY[2]) # Computes a symbol representing which ruby runtime this is running on # This implementation will fail if there are more than 255 minor or micro versions of ruby # def self.locator_version if RUBYVER >= RUBY_2_0_0 :ruby20 elsif RUBYVER >= RUBY_1_9_3 :ruby19 else :ruby18 end end LOCATOR_VERSION = locator_version # Constant set to true if multibyte is supported (includes multibyte extended regular expressions) MULTIBYTE = !!(LOCATOR_VERSION == :ruby19 || LOCATOR_VERSION == :ruby20) # Creates, or recreates a Locator. A Locator is created if index is not given (a scan is then # performed of the given source string. # def self.locator(string, file, index = nil) case LOCATOR_VERSION when :ruby20, :ruby19 Locator19.new(string, file, index) else Locator18.new(string, file, index) end end # Returns the file name associated with the string content def file end # Returns the string content def string end # Returns the position on line (first position on a line is 1) def pos_on_line(offset) end # Returns the line number (first line is 1) for the given offset def line_for_offset(offset) end # Returns the offset on line (first offset on a line is 0). # def offset_on_line(offset) end # Returns the character offset for a given reported offset def char_offset(byte_offset) end # Returns the length measured in number of characters from the given start and end reported offseta def char_length(offset, end_offset) end # Returns the line index - an array of line offsets for the start position of each line, starting at 0 for # the first line. # def line_index() end private class AbstractLocator < Puppet::Pops::Parser::Locator attr_accessor :line_index attr_accessor :string attr_accessor :prev_offset attr_accessor :prev_line attr_reader :string attr_reader :file # Create a locator based on a content string, and a boolean indicating if ruby version support multi-byte strings # or not. # def initialize(string, file, index = nil) @string = string.freeze @file = file.freeze @prev_offset = nil @prev_line = nil @line_index = index compute_line_index unless !index.nil? end # Returns the position on line (first position on a line is 1) def pos_on_line(offset) offset_on_line(offset) +1 end def to_location_hash(reported_offset, end_offset) pos = pos_on_line(reported_offset) offset = char_offset(reported_offset) length = char_length(reported_offset, end_offset) start_line = line_for_offset(reported_offset) { :line => start_line, :pos => pos, :offset => offset, :length => length} end # Returns the index of the smallest item for which the item > the given value # This is a min binary search. Although written in Ruby it is only slightly slower than # the corresponding method in C in Ruby 2.0.0 - the main benefit to use this method over # the Ruby C version is that it returns the index (not the value) which means there is not need # to have an additional structure to get the index (or record the index in the structure). This # saves both memory and CPU. It also does not require passing a block that is called since this # method is specialized to search the line index. # def ary_bsearch_i(ary, value) low = 0 high = ary.length mid = nil smaller = false satisfied = false v = nil while low < high do mid = low + ((high - low) / 2) v = (ary[mid] > value) if v == true satisfied = true smaller = true elsif !v smaller = false else raise TypeError, "wrong argument, must be boolean or nil, got '#{v.class}'" end if smaller high = mid else low = mid + 1; end end return nil if low == ary.length return nil if !satisfied return low end # Common impl for 18 and 19 since scanner is byte based def compute_line_index scanner = StringScanner.new(string) result = [0] # first line starts at 0 while scanner.scan_until(/\n/) result << scanner.pos end self.line_index = result.freeze end # Returns the line number (first line is 1) for the given offset def line_for_offset(offset) if prev_offset == offset # use cache return prev_line end if line_nbr = ary_bsearch_i(line_index, offset) # cache prev_offset = offset prev_line = line_nbr return line_nbr end # If not found it is after last # clear cache prev_offset = prev_line = nil return line_index.size end end class Locator18 < AbstractLocator def offset_on_line(offset) line_offset = line_index[ line_for_offset(offset)-1 ] offset - line_offset end def char_offset(char_offset) char_offset end def char_length(offset, end_offset) end_offset - offset end end # This implementation is for Ruby19 and Ruby20. It uses byteslice to get strings from byte based offsets. # For Ruby20 this is faster than using the Stringscanner.charpos method (byteslice outperforms it, when # strings are frozen). # class Locator19 < AbstractLocator # Returns the offset on line (first offset on a line is 0). # Ruby 19 is multibyte but has no character position methods, must use byteslice def offset_on_line(offset) line_offset = line_index[ line_for_offset(offset)-1 ] string.byteslice(line_offset, offset-line_offset).length end # Returns the character offset for a given byte offset # Ruby 19 is multibyte but has no character position methods, must use byteslice def char_offset(byte_offset) string.byteslice(0, byte_offset).length end # Returns the length measured in number of characters from the given start and end byte offseta def char_length(offset, end_offset) string.byteslice(offset, end_offset - offset).length end end -end \ No newline at end of file +end diff --git a/lib/puppet/pops/parser/makefile b/lib/puppet/pops/parser/makefile index 0e59f55ba..802382dd8 100644 --- a/lib/puppet/pops/parser/makefile +++ b/lib/puppet/pops/parser/makefile @@ -1,6 +1,6 @@ eparser.rb: egrammar.ra racc -o$@ egrammar.ra egrammar.output: egrammar.ra - racc -v -o$@ egrammar.ra \ No newline at end of file + racc -v -o$@ egrammar.ra diff --git a/lib/puppet/pops/parser/slurp_support.rb b/lib/puppet/pops/parser/slurp_support.rb index 0f258c83b..17658c156 100644 --- a/lib/puppet/pops/parser/slurp_support.rb +++ b/lib/puppet/pops/parser/slurp_support.rb @@ -1,95 +1,95 @@ # This module is an integral part of the Lexer. # It defines the string slurping behavior - finding the string and non string parts in interpolated # strings, translating escape sequences in strings to their single character equivalence. # # PERFORMANCE NOTE: The various kinds of slurping could be made even more generic, but requires # additional parameter passing and evaluation of conditional logic. # TODO: More detailed performance analysis of excessive character escaping and interpolation. # module Puppet::Pops::Parser::SlurpSupport SLURP_SQ_PATTERN = /(?:[^\\]|^|[^\\])(?:[\\]{2})*[']/ SLURP_DQ_PATTERN = /(?:[^\\]|^|[^\\])(?:[\\]{2})*(["]|[$]\{?)/ SLURP_UQ_PATTERN = /(?:[^\\]|^|[^\\])(?:[\\]{2})*([$]\{?|\z)/ SLURP_ALL_PATTERN = /.*(\z)/ SQ_ESCAPES = %w{ ' } DQ_ESCAPES = %w{ \\ $ ' " r n t s u}+["\r\n", "\n"] UQ_ESCAPES = %w{ \\ $ r n t s u}+["\r\n", "\n"] def slurp_sqstring # skip the leading ' @scanner.pos += 1 str = slurp(@scanner, SLURP_SQ_PATTERN, SQ_ESCAPES, :ignore_invalid_escapes) || lex_error("Unclosed quote after \"'\" followed by '#{followed_by}'") str[0..-2] # strip closing "'" from result end def slurp_dqstring scn = @scanner last = scn.matched str = slurp(scn, SLURP_DQ_PATTERN, DQ_ESCAPES, false) unless str lex_error("Unclosed quote after #{format_quote(last)} followed by '#{followed_by}'") end # Terminator may be a single char '"', '$', or two characters '${' group match 1 (scn[1]) from the last slurp holds this terminator = scn[1] [str[0..(-1 - terminator.length)], terminator] end # Copy from old lexer - can do much better def slurp_uqstring scn = @scanner last = scn.matched ignore = true str = slurp(scn, @lexing_context[:uq_slurp_pattern], @lexing_context[:escapes], :ignore_invalid_escapes) # Terminator may be a single char '$', two characters '${', or empty string '' at the end of intput. # Group match 1 holds this. # The exceptional case is found by looking at the subgroup 1 of the most recent match made by the scanner (i.e. @scanner[1]). # This is the last match made by the slurp method (having called scan_until on the scanner). # If there is a terminating character is must be stripped and returned separately. # terminator = scn[1] [str[0..(-1 - terminator.length)], terminator] end # Slurps a string from the given scanner until the given pattern and then replaces any escaped # characters given by escapes into their control-character equivalent or in case of line breaks, replaces the # pattern \r?\n with an empty string. # The returned string contains the terminating character. Returns nil if the scanner can not scan until the given # pattern. # def slurp(scanner, pattern, escapes, ignore_invalid_escapes) str = scanner.scan_until(pattern) || return # Process unicode escapes first as they require getting 4 hex digits # If later a \u is found it is warned not to be a unicode escape if escapes.include?('u') str.gsub!(/\\u([\da-fAF]{4})/m) { [$1.hex].pack("U") } end str.gsub!(/\\([^\r\n]|(?:\r?\n))/m) { ch = $1 if escapes.include? ch case ch when 'r' ; "\r" when 'n' ; "\n" when 't' ; "\t" when 's' ; " " when 'u' Puppet.warning(positioned_message("Unicode escape '\\u' was not followed by 4 hex digits")) "\\u" when "\n" ; '' when "\r\n"; '' else ch end else Puppet.warning(positioned_message("Unrecognized escape sequence '\\#{ch}'")) unless ignore_invalid_escapes "\\#{ch}" end } str end -end \ No newline at end of file +end diff --git a/lib/puppet/pops/types/class_loader.rb b/lib/puppet/pops/types/class_loader.rb index d2b3a327b..0cd1b8c2f 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::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 +end diff --git a/lib/puppet/pops/types/enumeration.rb b/lib/puppet/pops/types/enumeration.rb index cf354b9cb..76dc59b67 100644 --- a/lib/puppet/pops/types/enumeration.rb +++ b/lib/puppet/pops/types/enumeration.rb @@ -1,34 +1,34 @@ # The Enumeration class provides default Enumerable::Enumerator creation for Puppet Programming Language # runtime objects that supports the concept of enumeration. # class Puppet::Pops::Types::Enumeration # Produces an Enumerable::Enumerator for Array, Hash, Integer, Integer Range, and String. # def self.enumerator(o) @@singleton ||= new @@singleton.enumerator(o) end # Produces an Enumerator for Array, Hash, Integer, Integer Range, and String. # def enumerator(o) case o when String x = o.chars # Ruby 1.8.7 returns Enumerable::Enumerator, Ruby 1.8.9 Enumerator, and 2.0.0 an Array x.is_a?(Array) ? x.each : x when Integer o.times when Array o.each when Hash o.each when Puppet::Pops::Types::PIntegerType # Not enumerable if representing an infinite range return nil if o.to.nil? || o.from.nil? o.each else nil end end -end \ No newline at end of file +end diff --git a/lib/puppet/vendor/safe_yaml/CHANGES.md b/lib/puppet/vendor/safe_yaml/CHANGES.md index 1efd80cba..4dff37d5c 100644 --- a/lib/puppet/vendor/safe_yaml/CHANGES.md +++ b/lib/puppet/vendor/safe_yaml/CHANGES.md @@ -1,104 +1,104 @@ 0.9.2 ----- - fixed error w/ parsing "!" when whitelisting tags - fixed parsing of the number 0 (d'oh!) 0.9.1 ----- - added Yecht support (JRuby) - more bug fixes 0.9.0 ----- - added `whitelist!` method for easily whitelisting tags - added support for call-specific options - removed deprecated methods 0.8.6 ----- - fixed bug in float matcher 0.8.5 ----- - performance improvements - made less verbose by default - bug fixes 0.8.4 ----- - enhancements to parsing of integers, floats, and dates - updated built-in whitelist - more bug fixes 0.8.3 ----- - fixed exception on parsing empty document - fixed handling of octal & hexadecimal numbers 0.8.2 ----- - bug fixes 0.8.1 ----- - added `:raise_on_unknown_tag` option - renamed `reset_defaults!` to `restore_defaults!` 0.8 --- - added tag whitelisting - more API changes 0.7 --- - separated YAML engine support from Ruby version - added support for binary scalars - numerous bug fixes and enhancements 0.6 --- - several API changes - added `SafeYAML::OPTIONS` for specifying default behavior 0.5 --- Added support for dates 0.4 --- - efficiency improvements - made `YAML.load` use `YAML.safe_load` by default - made symbol deserialization optional 0.3 --- Added Syck support 0.2 --- Added support for: - anchors & aliases - booleans - nils 0.1 --- -Initial release \ No newline at end of file +Initial release diff --git a/lib/puppetx.rb b/lib/puppetx.rb index 884e2b8f2..ad779add1 100644 --- a/lib/puppetx.rb +++ b/lib/puppetx.rb @@ -1,89 +1,89 @@ # The Puppet Extensions Module. # # Submodules of this module should be named after the publisher (e.g. 'user' part of a Puppet Module name). # The submodule {Puppetx::Puppet} contains the puppet extension points. # # This module also contains constants that are used when defining extensions. # # @api public # module Puppetx # The lookup **key** for the multibind containing syntax checkers used to syntax check embedded string in non # puppet DSL syntax. # @api public SYNTAX_CHECKERS = 'puppetx::puppet::syntaxcheckers' # The lookup **type** for the multibind containing syntax checkers used to syntax check embedded string in non # puppet DSL syntax. # @api public SYNTAX_CHECKERS_TYPE = 'Puppetx::Puppet::SyntaxChecker' # The lookup **key** for the multibind containing a map from scheme name to scheme handler class for bindings schemes. # @api public BINDINGS_SCHEMES = 'puppetx::puppet::bindings::schemes' # The lookup **type** for the multibind containing a map from scheme name to scheme handler class for bindings schemes. # @api public BINDINGS_SCHEMES_TYPE = 'Puppetx::Puppet::BindingsSchemeHandler' # This module is the name space for extension points # @api public module Puppet if ::Puppet[:binder] || ::Puppet[:parser] == 'future' # Extension-points are registered here: # - If in a Ruby submodule it is best to create it here # - The class does not have to be required; it will be auto required when the binder # needs it. # - If the extension is a multibind, it can be registered here; either with a required # class or a class reference in string form. # Register extension points # ------------------------- system_bindings = ::Puppet::Pops::Binder::SystemBindings extensions = system_bindings.extensions() extensions.multibind(SYNTAX_CHECKERS).name(SYNTAX_CHECKERS).hash_of(SYNTAX_CHECKERS_TYPE) extensions.multibind(BINDINGS_SCHEMES).name(BINDINGS_SCHEMES).hash_of(BINDINGS_SCHEMES_TYPE) # Register injector boot bindings # ------------------------------- boot_bindings = system_bindings.injector_boot_bindings() # Register the default bindings scheme handlers require 'puppetx/puppet/bindings_scheme_handler' { 'module' => 'ModuleScheme', 'confdir' => 'ConfdirScheme', }.each do |scheme, class_name| boot_bindings.bind.name(scheme).instance_of(BINDINGS_SCHEMES_TYPE).in_multibind(BINDINGS_SCHEMES). to_instance("Puppet::Pops::Binder::SchemeHandler::#{class_name}") end end end # Module with implementations of various extensions # @api public module Puppetlabs # Default extensions delivered in Puppet Core are included here # @api public module SyntaxCheckers if ::Puppet[:binder] || ::Puppet[:parser] == 'future' # Classes in this name-space are lazily loaded as they may be overridden and/or never used # (Lazy loading is done by binding to the name of a class instead of a Class instance). # Register extensions # ------------------- system_bindings = ::Puppet::Pops::Binder::SystemBindings bindings = system_bindings.default_bindings() bindings.bind do name('json') instance_of(SYNTAX_CHECKERS_TYPE) in_multibind(SYNTAX_CHECKERS) to_instance('Puppetx::Puppetlabs::SyntaxCheckers::Json') end end end end -end \ No newline at end of file +end diff --git a/lib/puppetx/puppet/syntax_checker.rb b/lib/puppetx/puppet/syntax_checker.rb index 6baa1479d..d46ac8fc8 100644 --- a/lib/puppetx/puppet/syntax_checker.rb +++ b/lib/puppetx/puppet/syntax_checker.rb @@ -1,91 +1,91 @@ module Puppetx::Puppet # SyntaxChecker is a Puppet Extension Point for the purpose of extending Puppet with syntax checkers. # The intended use is to create a class derived from this class and then register it with the # Puppet Binder. # # Creating the Extension Class # ---------------------------- # As an example, a class for checking custom xml (aware of some custom schemes) may be authored in # say a puppet module called 'exampleorg/xmldata'. The name of the class should start with `Puppetx::::`, # e.g. 'Puppetx::Exampleorg::XmlData::XmlChecker" and # be located in `lib/puppetx/exampleorg/xml_data/xml_checker.rb`. The Puppet Binder will auto-load this file when it # has a binding to the class `Puppetx::Exampleorg::XmlData::XmlChecker' # The Ruby Module `Puppetx` is created by Puppet, the remaining modules should be created by the loaded logic - e.g.: # # @example Defining an XmlChecker # module Puppetx::Exampleorg # module XmlData # class XmlChecker < Puppetx::Puppetlabs::SyntaxCheckers::SyntaxChecker # def check(text, syntax_identifier, acceptor, location_hash) # # do the checking # end # end # end # end # # Implementing the check method # ----------------------------- # The implementation of the {#check} method should naturally perform syntax checking of the given text/string and # produce found issues on the given `acceptor`. These can be warnings or errors. The method should return `false` if # any warnings or errors were produced (it is up to the caller to check for error/warning conditions and report them # to the user). # # Issues are reported by calling the given `acceptor`, which takes a severity (e.g. `:error`, # or `:warning), an {Puppet::Pops::Issues::Issue} instance, and a {Puppet::Pops::Adapters::SourcePosAdapter} # (which describes details about linenumber, position, and length of the problem area). Note that the # `location_info` given to the check method holds information about the location of the string in its *container* # (e.g. the source position of a Heredoc); this information can be used if more detailed information is not # available, or combined if there are more details (relative to the start of the checked string). # # @example Reporting an issue # # create an issue with a symbolic name (that can serve as a reference to more details about the problem), # # make the name unique # issue = Puppet::Pops::Issues::issue(:EXAMPLEORG_XMLDATA_ILLEGAL_XML) { "syntax error found in xml text" } # source_pos = Puppet::Pops::Adapters::SourcePosAdapter.new() # source_pos.line = info[:line] # use this if there is no detail from the used parser # source_pos.pos = info[:pos] # use this pos if there is no detail from used parser # # # report it # acceptor.accept(Puppet::Pops::Validation::Diagnostic.new(:error, issue, info[:file], source_pos, {})) # # There is usually a cap on the number of errors/warnings that are presented to the user, this is handled by the # reporting logic, but care should be taken to not generate too many as the issues are kept in memory until # the checker returns. The acceptor may set a limit and simply ignore issues past a certain (high) number of reported # issues (this number is typically higher than the cap on issues reported to the user). # # The `syntax_identifier` # ----------------------- # The extension makes use of a syntax identifier written in mime-style. This identifier can be something simple # as 'xml', or 'json', but can also consist of several segments joined with '+' where the most specific syntax variant # is placed first. When searching for a syntax checker; say for JSON having some special traits, say 'userdata', the # author of the text may indicate this as the text having the syntax "userdata+json" - when a checker is looked up it is # first checked if there is a checker for "userdata+json", if none is found, a lookup is made for "json" (since the text # must at least be valid json). The given identifier is passed to the checker (to allow the same checker to check for # several dialects/specializations). # # Use in Puppet DSL # ----------------- # The Puppet DSL Heredoc support and Puppet Templates makes use of the syntax checker extension. A user of a # heredoc can specify the syntax in the heredoc tag, e.g.`@(END:userdata+json)`. # # # @abstract # class SyntaxChecker # Checks the text for syntax issues and reports them to the given acceptor. # This implementation is abstract, it raises {NotImplementedError} since a subclass should have implemented the # method. # # @param text [String] The text to check # @param syntax_identifier [String] The syntax identifier in mime style (e.g. 'json', 'json-patch+json', 'xml', 'myapp+xml' # @option location_info [String] :file The filename where the string originates # @option location_info [Integer] :line The line number identifying the location where the string is being used/checked # @option location_info [Integer] :position The position on the line identifying the location where the string is being used/checked # @return [Boolean] Whether the checked string had issues (warnings and/or errors) or not. # @api public # def check(text, syntax_identifier, acceptor, location_info) raise NotImplementedError, "The class #{self.class.name} should have implemented the method check()" end end -end \ No newline at end of file +end