diff --git a/lib/puppet/pops/binder/injector.rb b/lib/puppet/pops/binder/injector.rb index 0cf129875..fb11c3cc1 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::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(self, 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