diff --git a/lib/puppet/pops/binder/binder.rb b/lib/puppet/pops/binder/binder.rb index f5807fb5c..930781424 100644 --- a/lib/puppet/pops/binder/binder.rb +++ b/lib/puppet/pops/binder/binder.rb @@ -1,393 +1,394 @@ # The Binder is responsible for processing layered bindings that can be used to setup an Injector. # # An instance should be created and a call to {#define_layers} should be made which will process the layered bindings # (handle overrides, abstract entries etc.). # The constructed hash with `key => InjectorEntry` mappings is obtained as {#injector_entries}, and is used to initialize an # {Puppet::Pops::Binder::Injector Injector}. # # @api public # class Puppet::Pops::Binder::Binder # @api private attr_reader :injector_entries # @api private attr :id_index # @api private attr_reader :key_factory # A parent Binder or nil # @api private attr_reader :parent # The next anonymous key to use # @api private attr_reader :anonymous_key # This binder's precedence # @api private attr_reader :binder_precedence # @api public def initialize(layered_bindings, parent_binder=nil) @parent = parent_binder @id_index = Hash.new() { |k, v| [] } @key_factory = Puppet::Pops::Binder::KeyFactory.new() # Resulting hash of all key -> binding @injector_entries = {} if @parent.nil? @anonymous_key = 0 @binder_precedence = 0 else # First anonymous key is the parent's next (non incremented key). (The parent can not change, it is # the final, free key). @anonymous_key = @parent.anonymous_key @binder_precedence = @parent.binder_precedence + 1 end define_layers(layered_bindings) end # Binds layers from highest to lowest as defined by the given LayeredBindings. # @note # The model should have been # validated to get better error messages if the model is invalid. This implementation expects the model # to be valid, and any errors raised will be more technical runtime errors. # # @param layered_bindings [Puppet::Pops::Binder::Bindings::LayeredBindings] the named and ordered layers # @raise ArgumentError if this binder is already configured # @raise ArgumentError if bindings with unresolved 'override' surfaces as an effective binding # @raise ArgumentError if the given argument has the wrong type, or if model is invalid in some way # @return [Puppet::Pops::Binder::Binder] self # @api public # def define_layers(layered_bindings) LayerProcessor.new(self, key_factory).bind(layered_bindings) contribution_keys = [] # make one pass over entries to collect contributions, and check overrides injector_entries.each do |k,v| if key_factory.is_contributions_key?(k) contribution_keys << [k,v] elsif !v.is_resolved?() raise ArgumentError, "Binding with unresolved 'override' detected: #{self.class.format_binding(v.binding)}}" else # if binding has an id, add it to the index add_id_to_index(v.binding) end end # If a lower level binder has contributions for a key also contributed to in this binder # they must included in the higher shadowing contribution. # If a contribution is made to an id that is defined in a parent # contribute to an id that is defined in a lower binder, it must be promoted to this binder (copied) or # there is risk of making the lower level injector dirty. # contribution_keys.each do |kv| parent_contribution = lookup_in_parent(kv[0]) next unless parent_contribution injector_entries[kv[0]] = kv[1] + parent_contributions # key the multibind_id from the contribution key multibind_id = key_factory.multibind_contribution_key_to_id(kv[0]) promote_matching_bindings(self, @parent, multibind_id) end end private :define_layers # @api private def next_anonymous_key tmp = @anonymous_key @anonymous_key += 1 tmp end def add_id_to_index(binding) return unless binding.is_a?(Puppet::Pops::Binder::Bindings::Multibinding) && !(id = binding.id).nil? @id_index[id] = @id_index[id] << binding end def promote_matching_bindings(to_binder, from_binder, multibind_id) return if from_binder.nil? from_binder.id_index[ multibind_id ].each do |binding| key = key_factory.binding_key(binding) entry = lookup(key) unless entry.precedence == @binder_precedence # it is from a lower layer it must be promoted injector_entries[ key ] = Puppet::Pops::Binder::InjectorEntry.new(binding, binder_precedence) end end # recursive "up the parent chain" to promote all promote_matching_bindings(to_binder, from_binder.parent, multibind_id) end def lookup_in_parent(key) @parent.nil? ? nil : @parent.lookup(key) end def lookup(key) if x = injector_entries[key] return x end @parent ? @parent.lookup(key) : nil end # @api private def self.format_binding(b) type_name = Puppet::Pops::Types::TypeCalculator.new().string(b.type) layer_name, bindings_name = get_named_binding_layer_and_name(b) "binding: '#{type_name}/#{b.name}' in: '#{bindings_name}' in layer: '#{layer_name}'" end # @api private def self.format_contribution_source(b) layer_name, bindings_name = get_named_binding_layer_and_name(b) "(layer: #{layer_name}, bindings: #{bindings_name})" end # @api private def self.get_named_binding_layer_and_name(b) return ['', ''] if b.nil? return [get_named_layer(b), b.name] if b.is_a?(Puppet::Pops::Binder::Bindings::NamedBindings) get_named_binding_layer_and_name(b.eContainer) end # @api private def self.get_named_layer(b) return '' if b.nil? return b.name if b.is_a?(Puppet::Pops::Binder::Bindings::NamedLayer) get_named_layer(b.eContainer) end # Processes the information in a layer, aggregating it to the injector_entries hash in its parent binder. # A LayerProcessor holds the intermediate state required while processing one layer. # # @api private # class LayerProcessor attr :bindings attr :binder attr :key_factory attr :contributions attr :binder_precedence def initialize(binder, key_factory) @binder = binder @binder_precedence = binder.binder_precedence @key_factory = key_factory @bindings = [] @contributions = [] @@bind_visitor ||= Puppet::Pops::Visitor.new(nil,"bind",0,0) end # Add the binding to the list of potentially effective bindings from this layer # @api private # def add(b) bindings << Puppet::Pops::Binder::InjectorEntry.new(b, binder_precedence) end # Add a multibind contribution # @api private # def add_contribution(b) contributions << Puppet::Pops::Binder::InjectorEntry.new(b, binder_precedence) end # Bind given abstract binding # @api private # def bind(binding) @@bind_visitor.visit_this(self, binding) end # @return [Puppet::Pops::Binder::InjectorEntry] the entry with the highest precedence # @api private def highest(b1, b2) if b1.is_abstract? != b2.is_abstract? # if one is abstract and the other is not, the non abstract wins b1.is_abstract? ? b2 : b1 else case b1.precedence <=> b2.precedence when 1 b1 when -1 b2 when 0 raise_conflicting_binding(b1, b2) end end end # Raises a conflicting bindings error given two InjectorEntry's with same precedence in the same layer # (if they are in different layers, something is seriously wrong) def raise_conflicting_binding(b1, b2) b1_layer_name, b1_bindings_name = binder.class.get_named_binding_layer_and_name(b1.binding) b2_layer_name, b2_bindings_name = binder.class.get_named_binding_layer_and_name(b2.binding) finality_msg = (b1.is_final? || b2.is_final?) ? ". Override of final binding not allowed" : '' # TODO: Use of layer_name is not very good, it is not guaranteed to be unique unless b1_layer_name == b2_layer_name raise ArgumentError, [ 'Conflicting binding for', "'#{b1.binding.name}'", 'being resolved across layers', "'#{b1_layer_name}' and", "'#{b2_layer_name}'" ].join(' ')+finality_msg end # Conflicting bindings made from the same source if b1_bindings_name == b2_bindings_name raise ArgumentError, [ 'Conflicting binding for name:', "'#{b1.binding.name}'", 'in layer:', "'#{b1_layer_name}', ", 'both from:', "'#{b1_bindings_name}'" ].join(' ')+finality_msg end # Conflicting bindings from different sources raise ArgumentError, [ 'Conflicting binding for name:', "'#{b1.binding.name}'", 'in layer:', "'#{b1_layer_name}',", 'from:', "'#{b1_bindings_name}', and", "'#{b2_bindings_name}'" ].join(' ')+finality_msg end # Produces the key for the given Binding. # @param binding [Puppet::Pops::Binder::Bindings::Binding] the binding to get a key for # @return [Object] an opaque key # @api private # def key(binding) k = if is_contribution?(binding) # contributions get a unique (sequential) key binder.next_anonymous_key() else key_factory.binding_key(binding) end end # @api private def is_contribution?(binding) ! binding.multibind_id.nil? end # @api private def bind_Binding(o) if is_contribution?(o) add_contribution(o) else add(o) end end # @api private def bind_Bindings(o) o.bindings.each {|b| bind(b) } end # @api private def bind_NamedBindings(o) # Name is ignored here, it should be introspected when needed (in case of errors) o.bindings.each {|b| bind(b) } end # Process layered bindings from highest to lowest layer # @api private # def bind_LayeredBindings(o) o.layers.each do |layer| processor = LayerProcessor.new(binder, key_factory) # All except abstract (==error) are transferred to injector_entries processor.bind(layer).each do |k, v| entry = binder.injector_entries[k] unless key_factory.is_contributions_key?(k) if v.is_abstract?() layer_name, bindings_name = Puppet::Pops::Binder::Binder.get_named_binding_layer_and_name(v.binding) type_name = key_factory.type_calculator.string(v.binding.type) raise ArgumentError, "The abstract binding '#{type_name}/#{v.binding.name}' in '#{bindings_name}' in layer '#{layer_name}' was not overridden" end raise ArgumentError, "Internal Error - redefinition of key: #{k}, (should never happen)" if entry binder.injector_entries[k] = v else - entry ? entry << v : binder.injector_entries[k] = v + # add contributions to existing contributions, else set them + binder.injector_entries[k] = entry ? entry + v : v end end end end # Processes one named ("top level") layer consisting of a list of NamedBindings # @api private # def bind_NamedLayer(o) o.bindings.each {|b| bind(b) } this_layer = {} # process regular bindings bindings.each do |b| bkey = key(b.binding) # ignore if a higher layer defined it (unless the lower is final), but ensure override gets resolved # (override is not resolved across binders) if x = binder.injector_entries[bkey] if b.is_final? raise_conflicting_binding(x, b) end x.mark_override_resolved() next end # If a lower (parent) binder exposes a final binding it may not be overridden # if (x = binder.lookup_in_parent(bkey)) && x.is_final? raise_conflicting_binding(x, b) end # if already found in this layer, one wins (and resolves override), or it is an error existing = this_layer[bkey] winner = existing ? highest(existing, b) : b this_layer[bkey] = winner if existing winner.mark_override_resolved() end end # Process contributions # - organize map multibind_id to bindings with this id # - for each id, create an array with the unique anonymous keys to the contributed bindings # - bind the index to a special multibind contributions key (these are aggregated) # c_hash = Hash.new {|hash, key| hash[ key ] = [] } contributions.each {|b| c_hash[ b.binding.multibind_id ] << b } # - for each id c_hash.each do |k, v| index = v.collect do |b| bkey = key(b.binding) this_layer[bkey] = b bkey - end + end.flatten contributions_key = key_factory.multibind_contributions(k) unless this_layer[contributions_key] this_layer[contributions_key] = [] end this_layer[contributions_key] += index end this_layer end end end diff --git a/lib/puppet/pops/binder/bindings_loader.rb b/lib/puppet/pops/binder/bindings_loader.rb index e3019cac2..353f82e0d 100644 --- a/lib/puppet/pops/binder/bindings_loader.rb +++ b/lib/puppet/pops/binder/bindings_loader.rb @@ -1,88 +1,83 @@ 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::PAnyType] A fully qualified # class name String (e.g. '::Foo::Bar', 'Foo::Bar'), a PAnyType, 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() @autoloader ||= Puppet::Util::Autoload.new("BindingsLoader", "puppet/bindings") -# unless Puppet.settings[:confdir] == @confdir -# @confdir = Puppet.settings[:confdir] == @confdir -# 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) } + paths_for_name(name).find {|path| loader.load(path, Puppet.lookup(:current_environment)) } result = Puppet::Bindings.resolve(scope, name) end result end def self.paths_for_name(fq_name) [de_camel(fq_name), downcased_path(fq_name)].uniq 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 diff --git a/lib/puppet/pops/binder/bindings_model_dumper.rb b/lib/puppet/pops/binder/bindings_model_dumper.rb index 4883f4dd6..513b27963 100644 --- a/lib/puppet/pops/binder/bindings_model_dumper.rb +++ b/lib/puppet/pops/binder/bindings_model_dumper.rb @@ -1,179 +1,215 @@ # Dumps a Pops::Binder::Bindings model in reverse polish notation; i.e. LISP style # The intention is to use this for debugging output # TODO: BAD NAME - A DUMP is a Ruby Serialization # NOTE: use :break, :indent, :dedent in lists to do just that # class Puppet::Pops::Binder::BindingsModelDumper < Puppet::Pops::Model::TreeDumper Bindings = Puppet::Pops::Binder::Bindings attr_reader :type_calculator attr_reader :expression_dumper def initialize super @type_calculator = Puppet::Pops::Types::TypeCalculator.new() @expression_dumper = Puppet::Pops::Model::ModelTreeDumper.new() end def dump_BindingsFactory o do_dump(o.model) end def dump_BindingsBuilder o do_dump(o.model) end def dump_BindingsContainerBuilder o do_dump(o.model) end def dump_NamedLayer o result = ['named-layer', (o.name.nil? ? '': o.name), :indent] if o.bindings o.bindings.each do |b| result << :break result << do_dump(b) end end result << :dedent result end + def dump_Injector o + result = ['injector', :indent, + :break, + ['entries', do_dump(o.instance_variable_get('@impl').instance_variable_get('@entries'))], + :dedent + ] + result + end + + def dump_InjectorEntry o + result = ['entry', :indent] + result << :break + result << ['precedence', o.precedence] + result << :break + result << ['binding', do_dump(o.binding)] + result << :break + result << ['producer', do_dump(o.cached_producer)] + result << :dedent + result + end def dump_Array o o.collect {|e| do_dump(e) } end + def dump_Hash o + result = ["hash", :indent] + o.each do |elem| + result << :break + result << ["=>", :indent, do_dump(elem[0]), :break, do_dump(elem[1]), :dedent] + end + result << :dedent + result + end + def dump_Integer o o.to_s end # Dump a Ruby String in single quotes unless it is a number. def dump_String o "'#{o}'" end def dump_NilClass o "()" end def dump_Object o ['dev-error-no-polymorph-dump-for:', o.class.to_s, o.to_s] end def is_nop? o o.nil? || o.is_a?(Model::Nop) || o.is_a?(AST::Nop) end def dump_ProducerDescriptor o result = [o.class.name] result << expression_dumper.dump(o.transformer) if o.transformer result end def dump_NonCachingProducerDescriptor o dump_ProducerDescriptor(o) + do_dump(o.producer) end def dump_ConstantProducerDescriptor o ['constant', do_dump(o.value)] end def dump_EvaluatingProducerDescriptor o result = dump_ProducerDescriptor(o) result << expression_dumper.dump(o.expression) end def dump_InstanceProducerDescriptor o # TODO: o.arguments, o. transformer ['instance', o.class_name] end def dump_ProducerProducerDescriptor o # skip the transformer lambda... result = ['producer-producer', do_dump(o.producer)] result << expression_dumper.dump(o.transformer) if o.transformer result end def dump_LookupProducerDescriptor o ['lookup', do_dump(o.type), o.name] end def dump_PAnyType o type_calculator.string(o) end def dump_HashLookupProducerDescriptor o # TODO: transformer lambda result = ['hash-lookup', do_dump(o.type), o.name, "[#{do_dump(o.key)}]"] result << expression_dumper.dump(o.transformer) if o.transformer result end def dump_FirstFoundProducerDescriptor o # TODO: transformer lambda ['first-found', do_dump(o.producers)] end def dump_ArrayMultibindProducerDescriptor o ['multibind-array'] end def dump_HashMultibindProducerDescriptor o ['multibind-hash'] end def dump_NamedArgument o "#{o.name} => #{do_dump(o.value)}" end def dump_Binding o - result = ['bind'] + result = ['bind', :indent] result << 'override' if o.override result << 'abstract' if o.abstract result.concat([do_dump(o.type), o.name]) + result << :break result << "(in #{o.multibind_id})" if o.multibind_id + result << :break result << ['to', do_dump(o.producer)] + do_dump(o.producer_args) + result << :dedent result end def dump_Multibinding o - result = ['multibind', o.id] + result = ['multibind', o.id, :indent] result << 'override' if o.override result << 'abstract' if o.abstract result.concat([do_dump(o.type), o.name]) + result << :break result << "(in #{o.multibind_id})" if o.multibind_id + result << :break result << ['to', do_dump(o.producer)] + do_dump(o.producer_args) + result << :dedent result end def dump_Bindings o do_dump(o.bindings) end def dump_NamedBindings o result = ['named-bindings', o.name, :indent] o.bindings.each do |b| result << :break result << do_dump(b) end result << :dedent result end def dump_LayeredBindings o result = ['layers', :indent] o.layers.each do |layer| result << :break result << do_dump(layer) end result << :dedent result end def dump_ContributedBindings o ['contributed', o.name, do_dump(o.bindings)] end end diff --git a/lib/puppet/pops/binder/injector.rb b/lib/puppet/pops/binder/injector.rb index fb11c3cc1..24e97e1d5 100644 --- a/lib/puppet/pops/binder/injector.rb +++ b/lib/puppet/pops/binder/injector.rb @@ -1,767 +1,766 @@ # 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::PAnyType] 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::PAnyType] 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::PAnyType], 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(nil) when 2 block.call(scope, nil) 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::PAnyType 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::PRuntimeType) && kt.runtime == :ruby && !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::PAnyType 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_2(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 diff --git a/lib/puppet/pops/types/class_loader.rb b/lib/puppet/pops/types/class_loader.rb index 21ce5c4bd..d97cc6908 100644 --- a/lib/puppet/pops/types/class_loader.rb +++ b/lib/puppet/pops/types/class_loader.rb @@ -1,129 +1,129 @@ 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", "") # 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::PAnyType] A fully qualified # class name String (e.g. '::Foo::Bar', 'Foo::Bar'), a PAnyType, 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::PAnyType, 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::PRuntimeType raise ArgumentError.new("Only Runtime type 'ruby' is supported, got #{type.runtime}") unless type.runtime == :ruby provide_from_string(type.runtime_type_name) 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 has a type argument (a PAnyType) so the Class' class could be returned # (but this only matters in special circumstances when meta programming has been used). Class when Puppet::Pops::Type::POptionalType # cannot make a distinction between optional and its type provide_from_type(type.optional_type) # 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::PTupleType ; Array when Puppet::Pops::Types::PHashType ; Hash when Puppet::Pops::Types::PStructType ; Hash when Puppet::Pops::Types::PRegexpType ; Regexp when Puppet::Pops::Types::PIntegerType ; Integer when Puppet::Pops::Types::PStringType ; String when Puppet::Pops::Types::PPatternType ; String when Puppet::Pops::Types::PEnumType ; String when Puppet::Pops::Types::PFloatType ; Float when Puppet::Pops::Types::PNilType ; NilClass when Puppet::Pops::Types::PCallableType ; Proc 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) } + if paths_for_name(name_path).find {|path| loaded_path = path; @autoloader.load(path, Puppet.lookup(:current_environment)) } 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 + def self.paths_for_name(fq_named_parts) + # search two entries, one where all parts are decamelized, and one with names just downcased + # TODO:this is not perfect - it will not produce the correct mix if a mix of styles are used + # The alternative is to test many additional paths. + # + [fq_named_parts.map {|part| de_camel(part)}.join('/'), fq_named_parts.join('/').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 diff --git a/lib/puppetx.rb b/lib/puppetx.rb index f68abb4a7..4127c2454 100644 --- a/lib/puppetx.rb +++ b/lib/puppetx.rb @@ -1,8 +1,8 @@ # The Puppet Extensions Module. # # Submodules of this module should be named after the publisher (e.g. 'user' part of a Puppet Module name). # # @api public # -module Puppetx +module PuppetX end diff --git a/spec/fixtures/unit/data_providers/environments/sample/environment.conf b/spec/fixtures/unit/data_providers/environments/sample/environment.conf new file mode 100644 index 000000000..a5e17cf24 --- /dev/null +++ b/spec/fixtures/unit/data_providers/environments/sample/environment.conf @@ -0,0 +1,2 @@ +# Use the 'sample' env data provider (in this fixture) +environment_data_provider=sample diff --git a/spec/fixtures/unit/data_providers/environments/sample/manifests/site.pp b/spec/fixtures/unit/data_providers/environments/sample/manifests/site.pp new file mode 100644 index 000000000..7124b82be --- /dev/null +++ b/spec/fixtures/unit/data_providers/environments/sample/manifests/site.pp @@ -0,0 +1,6 @@ +class test($param_a = 1, $param_b = 2, $param_c = 3) { + notify { "$param_a, $param_b, $param_c": } +} + +include test +include dataprovider::test diff --git a/spec/fixtures/unit/data_providers/environments/sample/modules/dataprovider/lib/puppet/bindings/dataprovider/default.rb b/spec/fixtures/unit/data_providers/environments/sample/modules/dataprovider/lib/puppet/bindings/dataprovider/default.rb new file mode 100644 index 000000000..55cb0121f --- /dev/null +++ b/spec/fixtures/unit/data_providers/environments/sample/modules/dataprovider/lib/puppet/bindings/dataprovider/default.rb @@ -0,0 +1,54 @@ +# This registers the default bindings for this module. +# These bidnings are loaded at the start of a Puppet +# catalog production. +# +# The registrations makes 'sample' available as both a +# data provider for an environment, and individually +# selectable in modules. +# +# Note that there are two different implementation classes +# registered, one for environment, and one for modules. +# +# Also note that all data are strings including the names +# of the classes that implement the provider logic. This +# is to not cause loading of those classes until they +# are needed. +# +Puppet::Bindings.newbindings('dataprovider::default') do + + # Make the SampleEnvData provider available for use in environments + # as 'sample'. + # + bind { + name 'sample' + in_multibind 'puppet::environment_data_providers' + to_instance 'PuppetX::Helindbe::SampleEnvData' + } + + # Make the SampleModuleData provider available for use in environments + # as 'sample'. + # + bind { + name 'sample' + in_multibind 'puppet::module_data_providers' + to_instance 'PuppetX::Helindbe::SampleModuleData' + } + + # This is what users of the 'sample' module data provider should + # use in its default.rb bindings. The module providing the implementation + # of this data provider typically does not have any puppet logic, so it + # would not have this binding. This example module has this however since + # it would otherwise require an additional module with just some puppet code + # and this binding to demonstrate the functionality. + # + # This binding declares that this module wants to use the 'sample' data provider + # for this module. (Thus ending up using the SampleModuleData implementation + # bound above in this example). + # + bind { + name 'dataprovider' + to 'sample' + in_multibind 'puppet::module_data' + } +end + diff --git a/spec/fixtures/unit/data_providers/environments/sample/modules/dataprovider/lib/puppet_x/helindbe/sample_env_data.rb b/spec/fixtures/unit/data_providers/environments/sample/modules/dataprovider/lib/puppet_x/helindbe/sample_env_data.rb new file mode 100644 index 000000000..3ceea9af0 --- /dev/null +++ b/spec/fixtures/unit/data_providers/environments/sample/modules/dataprovider/lib/puppet_x/helindbe/sample_env_data.rb @@ -0,0 +1,31 @@ +# The module is named after the author, to ensure that names under PuppetX namespace +# does not clash. +# +require 'puppetx' +module PuppetX::Helindbe + + # An env data provider that is hardcoded and provides data for + # the two names 'test::param_a' and 'test::param_b'. + # + # A real implementation would read the data from somewhere or invoke some + # other service to obtain the data. When doing so caching may be performance + # critical, and it is important that a cache is associated with the apropriate + # object to not cause memory leaks. See more details in the documentation + # for how to write a data provider and use adapters. + # + class SampleEnvData < Puppet::Plugins::DataProviders::EnvironmentDataProvider + def initialize() + @data = { + 'test::param_a' => 'env data param_a is 10', + 'test::param_b' => 'env data param_b is 20', + # demo: this overrides a parameter for a class in the dataprovider module + 'dataprovider::test::param_c' => 'env data param_c is 300', + } + end + + def lookup(name, scope) + @data[name] + end + end +end + diff --git a/spec/fixtures/unit/data_providers/environments/sample/modules/dataprovider/lib/puppet_x/helindbe/sample_module_data.rb b/spec/fixtures/unit/data_providers/environments/sample/modules/dataprovider/lib/puppet_x/helindbe/sample_module_data.rb new file mode 100644 index 000000000..483ca147c --- /dev/null +++ b/spec/fixtures/unit/data_providers/environments/sample/modules/dataprovider/lib/puppet_x/helindbe/sample_module_data.rb @@ -0,0 +1,32 @@ +# The module is named after the author, to ensure that names under PuppetX namespace +# does not clash. +# +require 'puppetx' +module PuppetX::Helindbe + + # A module data provider that is hardcoded and provides data for + # the three names 'test::param_a', 'test::param_b', and 'test::param_c' + # + # A real implementation would read the data from somewhere or invoke some + # other service to obtain the data. When doing so caching may be performance + # critical, and it is important that a cache is associated with the apropriate + # object to not cause memory leaks. See more details in the documentation + # for how to write a data provider and use adapters. + # + class SampleModuleData < Puppet::Plugins::DataProviders::ModuleDataProvider + def initialize() + @data = { + 'dataprovider::test::param_a' => 'module data param_a is 100', + 'dataprovider::test::param_b' => 'module data param_b is 200', + + # demo: uncomment the entry below to make it override the environment provided data + #'dataprovider::test::param_c' => 'env data param_c is 300', + } + end + + def lookup(name, scope) + @data[name] + end + end +end + diff --git a/spec/fixtures/unit/data_providers/environments/sample/modules/dataprovider/manifests/init.pp b/spec/fixtures/unit/data_providers/environments/sample/modules/dataprovider/manifests/init.pp new file mode 100644 index 000000000..fff6240b7 --- /dev/null +++ b/spec/fixtures/unit/data_providers/environments/sample/modules/dataprovider/manifests/init.pp @@ -0,0 +1,5 @@ +class dataprovider { + class test($param_a = 1, $param_b = 2, $param_c = 3) { + notify { "$param_a, $param_b, $param_c": } + } +} diff --git a/spec/fixtures/unit/pops/binder/bindings_composer/ok/binder_config.yaml b/spec/fixtures/unit/pops/binder/bindings_composer/ok/binder_config.yaml index f7a6f8c4d..f1b9d1636 100644 --- a/spec/fixtures/unit/pops/binder/bindings_composer/ok/binder_config.yaml +++ b/spec/fixtures/unit/pops/binder/bindings_composer/ok/binder_config.yaml @@ -1,10 +1,10 @@ --- version: 1 layers: [{name: site, include: 'confdir:/confdirtest'}, {name: test, include: 'echo:/quick/brown/fox'}, {name: modules, include: ['module:/*::default'], exclude: 'module:/bad::default/' } ] extensions: scheme_handlers: - echo: 'Puppetx::Awesome2::EchoSchemeHandler' + echo: 'PuppetX::Awesome2::EchoSchemeHandler' diff --git a/spec/fixtures/unit/pops/binder/bindings_composer/ok/modules/awesome2/lib/puppetx/awesome2/echo_scheme_handler.rb b/spec/fixtures/unit/pops/binder/bindings_composer/ok/modules/awesome2/lib/puppet_x/awesome2/echo_scheme_handler.rb similarity index 97% rename from spec/fixtures/unit/pops/binder/bindings_composer/ok/modules/awesome2/lib/puppetx/awesome2/echo_scheme_handler.rb rename to spec/fixtures/unit/pops/binder/bindings_composer/ok/modules/awesome2/lib/puppet_x/awesome2/echo_scheme_handler.rb index 33159fcaa..4edcd437b 100644 --- a/spec/fixtures/unit/pops/binder/bindings_composer/ok/modules/awesome2/lib/puppetx/awesome2/echo_scheme_handler.rb +++ b/spec/fixtures/unit/pops/binder/bindings_composer/ok/modules/awesome2/lib/puppet_x/awesome2/echo_scheme_handler.rb @@ -1,18 +1,18 @@ require 'puppet/plugins/binding_schemes' -module Puppetx +module PuppetX module Awesome2 # A binding scheme that echos its path # 'echo:/quick/brown/fox' becomes key '::quick::brown::fox' => 'echo: quick brown fox'. # (silly class for testing loading of extension) # class EchoSchemeHandler < Puppet::Plugins::BindingSchemes::BindingsSchemeHandler def contributed_bindings(uri, scope, composer) factory = ::Puppet::Pops::Binder::BindingsFactory bindings = factory.named_bindings("echo") bindings.bind.name(uri.path.gsub(/\//, '::')).to("echo: #{uri.path.gsub(/\//, ' ').strip!}") result = factory.contributed_bindings("echo", bindings.model) ### , nil) end end end end \ No newline at end of file diff --git a/spec/unit/data_providers/sample_data_provider_spec.rb b/spec/unit/data_providers/sample_data_provider_spec.rb new file mode 100644 index 000000000..89c617bfb --- /dev/null +++ b/spec/unit/data_providers/sample_data_provider_spec.rb @@ -0,0 +1,52 @@ +#! /usr/bin/env ruby +require 'spec_helper' +require 'puppet_spec/compiler' + +describe "when using a sample data provider from an external module" do + include PuppetSpec::Compiler + + # There is a fully configured 'sample' environment in fixtures at this location + let(:environmentpath) { parent_fixture('environments') } + + around(:each) do |example| + # Initialize settings to get a full compile as close as possible to a real + # environment load + Puppet.settings.initialize_global_settings + # Initialize loaders based on the environmentpath. It does not work to + # just set the setting environmentpath for some reason - this achieves the same: + # - first a loader is created, loading directory environments from the fixture (there is + # one environment, 'sample', which will be loaded since the node references this + # environment by name). + # - secondly, the created env loader is set as 'environments' in the puppet context. + # + loader = Puppet::Environments::Directories.new(environmentpath, []) + Puppet.override(:environments => loader) do + example.run + end + end + + it 'the environment data loader is used to set parameters' do + node = Puppet::Node.new("testnode", :facts => Puppet::Node::Facts.new("facts", {}), :environment => 'sample') + compiler = Puppet::Parser::Compiler.new(node) + catalog = compiler.compile() + resources_created_in_fixture = ["Notify[env data param_a is 10, env data param_b is 20, 3]"] + expect(resources_in(catalog)).to include(*resources_created_in_fixture) + end + + it 'the module and environment data loader is used to set parameters' do + node = Puppet::Node.new("testnode", :facts => Puppet::Node::Facts.new("facts", {}), :environment => 'sample') + compiler = Puppet::Parser::Compiler.new(node) + catalog = compiler.compile() + resources_created_in_fixture = ["Notify[module data param_a is 100, module data param_b is 200, env data param_c is 300]"] + expect(resources_in(catalog)).to include(*resources_created_in_fixture) + end + + def parent_fixture(dir_name) + File.absolute_path(File.join(my_fixture_dir(), "../#{dir_name}")) + end + + def resources_in(catalog) + catalog.resources.map(&:ref) + end + +end diff --git a/spec/unit/pops/binder/bindings_composer_spec.rb b/spec/unit/pops/binder/bindings_composer_spec.rb index 8f39b8eeb..123e33c7e 100644 --- a/spec/unit/pops/binder/bindings_composer_spec.rb +++ b/spec/unit/pops/binder/bindings_composer_spec.rb @@ -1,63 +1,64 @@ require 'spec_helper' require 'puppet/pops' require 'puppet_spec/pops' require 'puppet/plugins' describe 'BinderComposer' do include PuppetSpec::Pops def config_dir(config_name) my_fixture(config_name) end let(:acceptor) { Puppet::Pops::Validation::Acceptor.new() } let(:diag) { Puppet::Pops::Binder::Config::DiagnosticProducer.new(acceptor) } let(:issues) { Puppet::Pops::Binder::Config::Issues } let(:node) { Puppet::Node.new('localhost') } let(:compiler) { Puppet::Parser::Compiler.new(node)} let(:scope) { Puppet::Parser::Scope.new(compiler) } let(:parser) { Puppet::Pops::Parser::Parser.new() } let(:factory) { Puppet::Pops::Binder::BindingsFactory } it 'should load default config if no config file exists' do diagnostics = diag composer = Puppet::Pops::Binder::BindingsComposer.new() composer.compose(scope) end context "when loading a complete configuration with modules" do let(:config_directory) { config_dir('ok') } it 'should load everything without errors' do Puppet.settings[:confdir] = config_directory Puppet.settings[:libdir] = File.join(config_directory, 'lib') - Puppet.override(:environments => Puppet::Environments::Static.new(Puppet::Node::Environment.create(:production, [File.join(config_directory, 'modules')]))) do + environments = Puppet::Environments::Static.new(Puppet::Node::Environment.create(:production, [File.join(config_directory, 'modules')])) + Puppet.override(:environments => environments, :current_environment => environments.get('production')) do # this ensure the binder is active at the right time # (issues with getting a /dev/null path for "confdir" / "libdir") raise "Binder not active" unless scope.compiler.activate_binder diagnostics = diag composer = Puppet::Pops::Binder::BindingsComposer.new() the_scope = scope the_scope['fqdn'] = 'localhost' the_scope['environment'] = 'production' layered_bindings = composer.compose(scope) # puts Puppet::Pops::Binder::BindingsModelDumper.new().dump(layered_bindings) binder = Puppet::Pops::Binder::Binder.new(layered_bindings) injector = Puppet::Pops::Binder::Injector.new(binder) expect(injector.lookup(scope, 'awesome_x')).to be == 'golden' expect(injector.lookup(scope, 'good_x')).to be == 'golden' expect(injector.lookup(scope, 'rotten_x')).to be == nil expect(injector.lookup(scope, 'the_meaning_of_life')).to be == 42 expect(injector.lookup(scope, 'has_funny_hat')).to be == 'the pope' expect(injector.lookup(scope, 'all your base')).to be == 'are belong to us' expect(injector.lookup(scope, 'env_meaning_of_life')).to be == 'production thinks it is 42' expect(injector.lookup(scope, '::quick::brown::fox')).to be == 'echo: quick brown fox' end end end # TODO: test error conditions (see BinderConfigChecker for what to test) end diff --git a/spec/unit/pops/types/class_loader_spec.rb b/spec/unit/pops/types/class_loader_spec.rb new file mode 100644 index 000000000..9f7f9b7a1 --- /dev/null +++ b/spec/unit/pops/types/class_loader_spec.rb @@ -0,0 +1,10 @@ +require 'spec_helper' +require 'puppet/pops' + +describe 'the Puppet::Pops::Types::ClassLoader' do + it 'should produce path alternatives for CamelCase classes' do + expected_paths = ['puppet_x/some_thing', 'puppetx/something'] + # path_for_name method is private + expect(Puppet::Pops::Types::ClassLoader.send(:paths_for_name, ['PuppetX', 'SomeThing'])).to include(*expected_paths) + end +end