Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F16571154
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Flag For Later
Award Token
Size
93 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/lib/puppet/pops/binder/binder.rb b/lib/puppet/pops/binder/binder.rb
index c0a6bd6d3..866e02cbd 100644
--- a/lib/puppet/pops/binder/binder.rb
+++ b/lib/puppet/pops/binder/binder.rb
@@ -1,404 +1,407 @@
# The Binder is responsible for processing layered bindings that can be used to setup an Injector.
#
# An instance should be created, and calls should then be made to #define_categories to define the available categories, and
# their precedence. This should be followed by a call to #define_layers which will match the layered bindings against the
# effective categories (filtering out everything that does not apply, handle overrides, abstract entries etc.).
# The constructed hash with key => InjectorEntry mappings is obtained as #injector_entries, and is used to initialize an Injector.
#
# @api public
#
class Puppet::Pops::Binder::Binder
# This limits the number of available categorizations, including "common".
# @api private
PRECEDENCE_MAX = 1000
# @api private
attr_reader :category_precedences
# @api private
attr_reader :category_values
# @api private
attr_reader :injector_entries
# @api private
attr_reader :key_factory
# Whether the binder is fully configured or not
# @api public
#
attr_reader :configured
# @api public
def initialize
@category_precedences = {}
@category_values = {}
@key_factory = Puppet::Pops::Binder::KeyFactory.new()
# Resulting hash of all key -> binding
@injector_entries = {}
# Not configured until the fat lady sings
@configured = false
@next_anonymous_key = 0
end
# Answers the question 'is this binder configured?' to the point it can be used to instantiate an Injector
# @api public
def configured?()
configured()
end
# Defines the effective categories in precedence order (highest precedence first).
# The 'common' (lowest precedence) category should not be included in the list.
# A sanity check is made that there are no more than 1000 categorizations (which is pretty wild).
#
# The term 'effective categories' refers to the evaluated list of tuples (categorization, category-value) represented with
# an instance of Puppet::Pops::Binder::Bindings::EffectiveCategories.
#
# @param effective_categories [Puppet::Pops::Binder::Bindings::EffectiveCategories] effective categories (i.e. with evaluated values)
# @raises ArgumentError if this binder is already configured
# @raises ArgumentError if the argument is not an EffectiveCategories
# @raises ArgumentError if there is an attempt to redefine a category (non unique, or 'common').
# @return [Puppet::Pops::Binder::Binder] self
# @api public
#
def define_categories(effective_categories)
raise ArgumentError, "This categories are already defined. Cannot redefine." unless @category_precedences.empty?
# Note: a model instance is used since a Hash does not have a defined order in all Rubies.
unless effective_categories.is_a?(Puppet::Pops::Binder::Bindings::EffectiveCategories)
raise ArgumentError, "Expected Puppet::Pops::Binder::Bindings::EffectiveCategories, but got a: #{effective_categories.class}"
end
categories = effective_categories.categories
raise ArgumentError, "Category limit (#{PRECEDENCE_MAX}) exceeded" unless categories.size <= PRECEDENCE_MAX
# Automatically add the 'common' category with lowest precedence
@category_precedences['common'] = 0
# if categories contains "common", it should be last - simply drop it if present
if last = categories[-1]
if last.categorization == 'common'
categories.delete_at(-1)
end
end
# Process the given categories (highest precedence is first in the list)
categories.each_with_index do |c, index|
cname = c.categorization
raise ArgumentError, "Attempt to redefine categorization: #{cname}" if @category_precedences[cname]
@category_precedences[cname] = PRECEDENCE_MAX - index
@category_values[cname] = c.value
end
self
end
# Binds layers from highest to lowest as defined by the given LayeredBindings.
# @note
# Categories must be set with #define_categories before calling this method. 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
# @raises ArgumentError if categories have not been defined
# @raises ArgumentError if this binder is already configured
# @raises ArgumentError if bindings with unresolved 'override' surfaces as an effective binding
# @raises 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)
raise ArgumentError, "This binder is already configured. Cannot redefine its content." if configured?()
raise ArgumentError, "Categories must be defined first" if @category_precedences.empty?
LayerProcessor.new(self, key_factory).bind(layered_bindings)
injector_entries.each do |k,v|
unless key_factory.is_contributions_key?(k) || v.is_resolved?()
raise ArgumentError, "Binding with unresolved 'override' detected: #{k}"
end
end
# and the fat lady has sung
@configured = true
self
end
# @api private
def next_anonymous_key
tmp = @next_anonymous_key
@next_anonymous_key += 1
tmp
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 :effective_prec
attr :prec_stack
attr :bindings
attr :binder
attr :key_factory
attr :contributions
def initialize(binder, key_factory)
@binder = binder
@key_factory = key_factory
@prec_stack = []
@effective_prec = nil
@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(effective_prec, b)
end
# Add a multibind contribution
# @api private
#
def add_contribution(b)
contributions << Puppet::Pops::Binder::InjectorEntry.new(effective_prec, b)
end
# Bind given abstract binding
# @api private
#
def bind(binding)
@@bind_visitor.visit_this(self, binding)
end
# @returns [Puppet::Pops::Binder::InjectorEntry] the entry with the highest (category) precedence
# @api private
def highest(b1, b2)
case b1.precedence <=> b2.precedence
when 1
b1
when -1
b2
when 0
# TODO: This is too crude for conflict errors
raise_conflicting_binding(b1, b2)
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)
formatter = lambda {|layer, name| }
b1_layer_name, b1_bindings_name = get_named_binding_layer_and_name(b1.binding)
b2_layer_name, b2_bindings_name = get_named_binding_layer_and_name(b2.binding)
# The resolution is per layer, and if they differ something is serious wrong as a higher layer
# overrides a lower; so no such conflict should be possible:
unless b1_layer_name == b2_layer_name
raise ArgumentError, [
'Internal Error: Conflicting binding for',
"'#{b1.binding.name}'",
'being resolved across layers',
"'#{b1_layer_name}' and",
"'#{b2_layer_name}'"
].join(' ')
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(' ')
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(' ')
end
def format_contribution_source(b)
layer_name, bindings_name = get_named_binding_layer_and_name(b)
"(layer: #{layer_name}, bindings: #{bindings_name})"
end
def get_named_binding_layer_and_name(b)
return ['<unknown>', '<unknown>'] 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
def get_named_layer(b)
return '<unknown>' if b.nil?
return b.name if b.is_a?(Puppet::Pops::Binder::Bindings::NamedLayer)
get_named_layer(b.eContainer)
end
# Produces the key for the given Binding.
- # @param binding [Puppet::Pops::Binder::Bindings::Binding] they binding to get a key for
+ # @param binding [Puppet::Pops::Binder::Bindings::Binding] the binding to get a key for
# @returns [Object] an opaque key
# @api private
#
def key(binding)
- k = unless binding.is_a?(Puppet::Pops::Binder::Bindings::MultibindContribution)
- key_factory.binding_key(binding)
- else
- # contributions get a unique (sequencial) key
+ 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 push_precedences(precedences)
prec_stack.push(precedences)
@effective_prec = nil # clear cache
end
# @api private
def pop_precedences()
prec_stack.pop()
@effective_prec = nil # clear cache
end
# Returns the effective precedence as an array with highest precedence first.
# Internally the precedence is an array with the highest precedence first.
#
# @api private
#
def effective_prec()
unless @effective_prec
@effective_prec = prec_stack.flatten.uniq.sort.reverse
if @effective_prec.size == 0
@effective_prec = [ 0 ] # i.e. "common"
end
end
@effective_prec
end
# @api private
def bind_Binding(o)
- add(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 CategorizedBindings by calculating precedence, and then if satisfying the predicates, process the contained
# bindings.
# @api private
#
def bind_CategorizedBindings(o)
precedences = o.predicates.collect do |p|
prec = binder.category_precedences[p.categorization]
# Skip bindings if the categorization is not present, or
# if the category value is not the effective value for the categorization
# Ignore the value for the common category (it is not possible to state common 'false' etc.)
#
return unless prec
return unless binder.category_values[p.categorization] == p.value.downcase || p.categorization == 'common'
prec
end
push_precedences(precedences)
o.bindings.each {|b| bind(b) }
pop_precedences()
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 transfered to injector_entries
processor.bind(layer).each do |k, v|
entry = binder.injector_entries[k]
unless key_factory.is_contributions_key?(k)
raise ArgumentError, "The abstract binding TODO: was not overridden" unless !v.is_abstract?()
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
end
end
end
end
- # @api private
- #
- def bind_MultibindContribution(o)
- add_contribution(o)
- 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, but ensure override gets resolved
if x = binder.injector_entries[bkey]
x.mark_override_resolved()
next
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
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_factory.rb b/lib/puppet/pops/binder/bindings_factory.rb
index 65edcd804..0c3f52bba 100644
--- a/lib/puppet/pops/binder/bindings_factory.rb
+++ b/lib/puppet/pops/binder/bindings_factory.rb
@@ -1,608 +1,602 @@
# A helper class that makes it easier to construct a Bindings model
#
# @example Usage of the factory
# result = Puppet::Pops::Binder::BindingsFactory.named_bindings("mymodule::mybindings")
# result.bind().name("foo").to(42)
# result.when_in_category("node", "kermit.example.com").bind().name("foo").to(43)
# result.bind().string().name("site url").to("http://www.example.com")
# result.model()
#
# @api public
#
module Puppet::Pops::Binder::BindingsFactory
class AbstractBuilder
+ # The built model object.
+ attr_reader :model
+
+ # @api public
+ def initialize(binding)
+ @model = binding
+ end
+
# Provides convenient access to the Bindings Factory class methods. The intent is to provide access to the
# methods that return producers for the purpose of composing more elaborate things that the convenient methods
# directly supports.
#
def method_missing(meth, *args, &block)
- Puppet::Pops::Binder::BindingsFactory.send(meth, *args, &block)
+ factory = Puppet::Pops::Binder::BindingsFactory
+ if factory.respond_to?(meth)
+ factory.send(meth, *args, &block)
+ else
+ super
+ end
end
end
# @api public
class BindingsContainerBuilder < AbstractBuilder
- # The built model object.
- attr_reader :model
-
- # @api public
- def initialize(binding)
- @model = binding
- end
-
-
# Adds an empty binding to the container, and returns a builder for it for further detailing.
# @api public
#
def bind(&block)
binding = Puppet::Pops::Binder::Bindings::Binding.new()
model.addBindings(binding)
builder = BindingsBuilder.new(binding)
builder.instance_eval(&block) if block_given?
builder
end
# Binds an (almost) empty multibind where later, the looked up result contains all contributions to this key
# @param id [String] the multibind's id used when adding contributions
# @api public
#
def multibind(id, &block)
binding = Puppet::Pops::Binder::Bindings::Multibinding.new()
binding.id = id
model.addBindings(binding)
builder = MultibindingsBuilder.new(binding)
builder.instance_eval(&block) if block_given?
builder
end
- # Binds a type/name key in a multibind given by id.
- # @param type [Puppet::Pops::Types::PObjectType] the type (must be compatible with the multibind type argument)
- # @param name [String] the name of the binding (appears as key in a Hash multibind, ignored in an Array multibind
- # @param id [String] the multibind id of the multibind where this binding should be made
- # @api public
- #
- def bind_in_multibind(id, &block)
- binding = Puppet::Pops::Binder::Bindings::MultibindContribution.new()
- binding.multibind_id = id
- model.addBindings(binding)
- builder = BindingsBuilder.new(binding)
- builder.instance_eval(&block) if block_given?
- builder
- end
-
# Adds a categorized bindings to this container. Returns a BindingsContainerBuilder to allow adding
# bindings in that container.
# @param categorixation [String] the name of the categorization e.g. 'node'
# @param category_vale [String] the calue in that category e.g. 'kermit.example.com'
# @api public
#
def when_in_category(categorization, category_value, &block)
when_in_categories({categorization => category_value}, &block)
end
# Adds a categorized bindings to this container. Returns a BindingsContainerBuilder to allow adding
# bindings in that container. The result is that a processed request must be in all the listed categorizations
# with the given values.
# @param categories_hash Hash[String, String] a hash with categorization and categorization value entries
# @api public
#
def when_in_categories(categories_hash, &block)
binding = Puppet::Pops::Binder::Bindings::CategorizedBindings.new()
categories_hash.each do |k,v|
pred = Puppet::Pops::Binder::Bindings::Category.new()
pred.categorization = k
pred.value = v
binding.addPredicates(pred)
end
model.addBindings(binding)
builder = BindingsContainerBuilder.new(binding)
builder.instance_eval(&block) if block_given?
builder
end
end
# Builds a Binding via convenience methods.
#
# @api public
#
class BindingsBuilder < AbstractBuilder
- attr_reader :model
# @api public
def initialize(binding)
- @model = binding
+ super binding
data()
end
# Sets the name of the binding.
# @param name [String] the name to bind.
# @api public
def name(name)
- @model.name = name
+ model.name = name
+ self
+ end
+
+ # Makes the binding a multibind contribution to the given multibind id
+ # @api public
+ def in_multibind(id)
+ model.multibind_id = id
self
end
# (#name)
# @api public
def named(name)
- @model.name = name
+ model.name = name
self
end
# @api public
def type(type)
- @model.type = type
+ model.type = type
self
end
# @api public
def integer()
type(type_factory.integer())
end
# @api public
def float()
type(type_factory.float())
end
# @api public
def boolean()
type(type_factory.boolean())
end
# @api public
def string()
type(type_factory.string())
end
# @api public
def pattern()
type(type_factory.pattern())
end
# @api public
def literal()
type(type_factory.literal())
end
# @api public
def data()
type(type_factory.data())
end
# @api public
def array_of_data()
type(type_factory.array_of_data())
end
# @api public
def array_of(t)
type(type_factory.array_of(t))
end
# @api public
def hash_of_data()
type(type_factory.hash_of_data())
end
# Sets type of binding to `Hash[Literal, t]`. To limit the key type, use {#type} and give it a fully specified
# hash using {#type_factory} and then `hash_of(value_type, key_type)`.
# @api public
def hash_of(t)
type(type_factory.hash_of(t))
end
# @api public
def instance_of(t)
type(type_factory.type_of(t))
end
# Provides convenient access to the type factory.
# This is intended to be used when methods taking a type as argument i.e. {#type}, #{array_of}, {#hash_of}, and {#instance_of}.
#
# @api public
def type_factory
Puppet::Pops::Types::TypeFactory
end
# to a singleton producer, if producer is a value, a literal producer is created for it
# @overload to(a_literal)
# a constant producer
# @overload to(a_class, *args)
# Instantiating producer
# @overload to(a_producer_descriptor)
# a given producer
# @api public
#
def to(producer, *args)
case producer
when Class
producer = Puppet::Pops::Binder::BindingsFactory.instance_producer(producer.name, *args)
when Puppet::Pops::Model::Expression
producer = Puppet::Pops::Binder::BindingsFactory.evaluating_producer(producer)
when Puppet::Pops::Binder::Bindings::ProducerDescriptor
else
# If given producer is not a producer, create a literal producer
producer = Puppet::Pops::Binder::BindingsFactory.literal_producer(producer)
end
- @model.producer = producer
+ model.producer = producer
self
end
# To a producer of an instance of given class (a String class name, or a Class instance)
# @overload to_instance(class_name, *args)
# @param class_name [String] the name of the class to instantiate
# @param args [Object] optional arguments to the constructor
# @overload to_instance(a_class)
# @param a_class [Class] the class to instantiate
# @param args [Object] optional arguments to the constructor
#
def to_instance(type, *args)
class_name = case type
when Class
type.name
when String
type
else
raise ArgumentError, "to_instance accepts String (a class name), or a Class.*args got: #{type.class}."
end
- @model.producer = Puppet::Pops::Binder::BindingsFactory.instance_producer(class_name, *args)
+ model.producer = Puppet::Pops::Binder::BindingsFactory.instance_producer(class_name, *args)
end
# to a singleton producer
# @overload to_producer(a_producer)
# @param a_producer [Puppet::Pops::Binder::Producers::Producer] an instantiated producer, not serializeable !
#
# @overload to_producer(a_class, *args)
# @param a_class [Class] the class to create an instance of
# @param args [Object] the arguments to the given class' new
#
# @overload to_producer(a_producer_descriptor)
# @param a_producer_descriptor [Puppet::Pops::Binder::Bindings::ProducerDescriptor] a descriptor
# producing Puppet::Pops::Binder::Producers::Producer
#
# @api public
#
def to_producer(producer, *args)
case producer
when Class
producer = Puppet::Pops::Binder::BindingsFactory.instance_producer(producer.name, *args)
when Puppet::Pops::Binder::Bindings::ProducerDescriptor
when Puppet::Pops::Binder::Producers::Producer
# a custom producer instance
producer = Puppet::Pops::Binder::BindingsFactory.literal_producer(producer)
else
raise ArgumentError, "Given producer argument is neither a producer descriptor, a class, nor a producer"
end
metaproducer = Puppet::Pops::Binder::BindingsFactory.producer_producer(producer)
- @model.producer = metaproducer
+ model.producer = metaproducer
self
end
# to a series of producers
# @overload to_producer(a_producer)
# @param a_producer [Puppet::Pops::Binder::Producers::Producer] an instantiated producer, not serializeable !
#
# @overload to_producer(a_class, *args)
# @param a_class [Class] the class to create an instance of
# @param args [Object] the arguments to the given class' new
#
# @overload to_producer(a_producer_descriptor)
# @param a_producer_descriptor [Puppet::Pops::Binder::Bindings::ProducerDescriptor] a descriptor
# producing Puppet::Pops::Binder::Producers::Producer
#
# @api public
#
def to_producer_series(producer, *args)
case producer
when Class
producer = Puppet::Pops::Binder::BindingsFactory.instance_producer(producer.name, *args)
when Puppet::Pops::Binder::Bindings::ProducerDescriptor
when Puppet::Pops::Binder::Producers::Producer
# a custom producer instance
producer = Puppet::Pops::Binder::BindingsFactory.literal_producer(producer)
else
raise ArgumentError, "Given producer argument is neither a producer descriptor, a class, nor a producer"
end
non_caching = Puppet::Pops::Binder::Bindings::NonCachingProducerDescriptor.new()
non_caching.producer = producer
metaproducer = Puppet::Pops::Binder::BindingsFactory.producer_producer(non_caching)
non_caching = Puppet::Pops::Binder::Bindings::NonCachingProducerDescriptor.new()
non_caching.producer = metaproducer
- @model.producer = non_caching
+ model.producer = non_caching
self
end
# to a "non singleton" producer (each produce produces a new copy).
# @overload to_series_of(a_literal)
# a constant producer
# @overload toto_series_of(a_class, *args)
# Instantiating producer
# @overload toto_series_of(a_producer_descriptor)
# a given producer
#
# @api public
#
def to_series_of(producer)
case producer
when Class
producer = Puppet::Pops::Binder::BindingsFactory.instance_producer(producer.name, *args)
when Puppet::Pops::Binder::Bindings::ProducerDescriptor
else
# If given producer is not a producer, create a literal producer
producer = Puppet::Pops::Binder::BindingsFactory.literal_producer(producer)
end
non_caching = Puppet::Pops::Binder::Bindings::NonCachingProducerDescriptor.new()
non_caching.producer = producer
- @model.producer = non_caching
+ model.producer = non_caching
self
end
-
# to a lookup of another key
# @overload to_lookup_of(type, name)
# @overload to_lookup_of(name)
# @api public
#
def to_lookup_of(type, name=nil)
unless name
name = type
type = Puppet::Pops::Types::TypeFactory.data()
end
- @model.producer = Puppet::Pops::Binder::BindingsFactory.lookup_producer(type, name)
+ model.producer = Puppet::Pops::Binder::BindingsFactory.lookup_producer(type, name)
self
end
# to a lookup of another key
# @overload to_lookup_of(type, name)
# @overload to_lookup_of(name)
# @api public
#
def to_hash_lookup_of(type, name, key)
- @model.producer = Puppet::Pops::Binder::BindingsFactory.hash_lookup_producer(type, name, key)
+ model.producer = Puppet::Pops::Binder::BindingsFactory.hash_lookup_producer(type, name, key)
self
end
# to first found lookup of another key
# @param list_of_lookups [Array] array of arrays [type name], or just name (implies data)
# @example
# binder.bind().name('foo').to_first_found(['fee', 'fum', 'extended-bar'])
# binder.bind().name('foo').to_first_found([
# [TypeFactory.ruby(ThisClass), 'fee'],
# [TypeFactory.ruby(ThatClass), 'fum'],
# 'extended-bar'])
# @api public
#
def to_first_found(list_of_lookups)
producers = list_of_lookups.collect do |entry|
if entry.is_a?(Array)
case entry.size
when 2
Puppet::Pops::Binder::BindingsFactory.lookup_producer(entry[0], entry[1])
when 1
Puppet::Pops::Binder::BindingsFactory.lookup_producer(Puppet::Pops::Types::TypeFactory.data(), entry[0])
else
raise ArgumentError, "Not an array of [type, name], name, or [name]"
end
else
Puppet::Pops::Binder::BindingsFactory.lookup_producer(Puppet::Pops::Types::TypeFactory.data(), entry)
end
end
- @model.producer = Puppet::Pops::Binder::BindingsFactory.first_found_producer(producers)
+ model.producer = Puppet::Pops::Binder::BindingsFactory.first_found_producer(producers)
self
end
# @api public
def producer_options(options)
options.each do |k, v|
arg = Puppet::Pops::Binder::Bindings::NamedArgument.new()
arg.name = k.to_s
arg.value = v
- @model.addProducer_args(arg)
+ model.addProducer_args(arg)
end
self
end
end
# @api public
class MultibindingsBuilder < BindingsBuilder
# Constraints type to be one of {Puppet::Pops::Types::PArrayType PArrayType}, or {Puppet::Pops::Types:PHashType PHashType}.
# @raise [ArgumentError] if type constraint is not met.
# @api public
def type(type)
unless type.class == Puppet::Pops::Types::PArrayType || type.class == Puppet::Pops::Types::PHashType
raise ArgumentError, "Wrong type; only PArrayType, or PHashType allowed, got '#{type.to_s}'"
end
- @model.type = type
+ model.type = type
self
end
# Overrides the default implementation that will raise an exception as a multibind requires a hash type.
# Thus, if nothing else is requested, a multibind will be configured as Hash[Data].
#
def data()
hash_of_data()
end
end
# Produces a ContributedBindings
# @param name [String] the name of the contributed bindings (for human use in messages/logs only)
# @param named_bindings [Puppet::Pops::Binder::Bindings::NamedBindings, Array<Puppet::Pops::Binder::Bindings::NamedBindings>] the
# named bindings to include
# @parm effective_categories [Puppet::Pops::Binder::Bindings::EffectiveCategories] the contributors opinion about categorization
# this is used to ensure consistent use of categories.
#
def self.contributed_bindings(name, named_bindings, effective_categories)
cb = Puppet::Pops::Binder::Bindings::ContributedBindings.new()
cb.name = name
named_bindings = [named_bindings] unless named_bindings.is_a?(Array)
named_bindings.each {|b| cb.addBindings(b) }
cb.effective_categories = effective_categories
cb
end
# Creates a named binding container, the top bindings model object.
# The created container is wrapped in a BindingsContainerBuilder for further detailing.
# Unwrap the built result when done.
# @api public
#
def self.named_bindings(name, &block)
binding = Puppet::Pops::Binder::Bindings::NamedBindings.new()
binding.name = name
builder = BindingsContainerBuilder.new(binding)
builder.instance_eval(&block) if block_given?
builder
end
# This variant of named_binding evaluates the given block as a method on an anonymous class,
# thus, if the block defines methods or do something with the class itself, this does not pollute
# the base class (BindingsContainerBuilder).
#
def self.safe_named_bindings(name, scope, &block)
binding = Puppet::Pops::Binder::Bindings::NamedBindings.new()
binding.name = name
anon = Class.new(BindingsContainerBuilder) do
def initialize(b)
super b
end
end
anon.send(:define_method, :_produce, block)
builder = anon.new(binding)
case block.arity
when 0
builder._produce()
when 1
builder._produce(scope)
end
builder
end
# Creates a literal producer
# @api public
#
def self.literal_producer(value)
producer = Puppet::Pops::Binder::Bindings::ConstantProducerDescriptor.new()
producer.value = value
producer
end
# Creates a literal producer
# @api public
#
def self.non_caching_producer(producer)
p = Puppet::Pops::Binder::Bindings::NonCachingProducerDescriptor.new()
p.producer = producer
p
end
# Creates a producer producer
# @api public
#
def self.producer_producer(producer)
p = Puppet::Pops::Binder::Bindings::ProducerProducerDescriptor.new()
p.producer = producer
p
end
# @api public
#
def self.instance_producer(class_name, *args)
p = Puppet::Pops::Binder::Bindings::InstanceProducerDescriptor.new()
p.class_name = class_name
args.each {|a| p.addArguments(a) }
p
end
# @api public
def self.lookup_producer(type, name)
p = Puppet::Pops::Binder::Bindings::LookupProducerDescriptor.new()
p.type = type
p.name = name
p
end
# @api public
def self.hash_lookup_producer(type, name, key)
p = Puppet::Pops::Binder::Bindings::HashLookupProducerDescriptor.new()
p.type = type
p.name = name
p.key = key
p
end
# @api public
def self.first_found_producer(producers)
p = Puppet::Pops::Binder::Bindings::FirstFoundProducerDescriptor.new()
producers.each {|p2| p.addProducers(p2) }
p
end
# @api public
def self.evaluating_producer(expression)
p = Puppet::Pops::Binder::Bindings::EvaluatingProducerDescriptor.new()
p.expression = expression
p
end
# Creates an EffectiveCategories from a list of tuples `[categorizxation category ...]`, or ´[[categorization category] ...]`
# @api public
#
def self.categories(tuple_array)
result = Puppet::Pops::Binder::Bindings::EffectiveCategories.new()
tuple_array.flatten.each_slice(2) do |c|
cat = Puppet::Pops::Binder::Bindings::Category.new()
cat.categorization = c[0]
cat.value = c[1]
result.addCategories(cat)
end
result
end
# @api public
def self.named_layer(name, *bindings)
result = Puppet::Pops::Binder::Bindings::NamedLayer.new()
result.name = name
bindings.each { |b| result.addBindings(b) }
result
end
# @api public
def self.layered_bindings(*named_layers)
result = Puppet::Pops::Binder::Bindings::LayeredBindings.new()
named_layers.each {|b| result.addLayers(b) }
result
end
def self.parser
@parser ||= Puppet::Pops::Parser::EvaluatingParser.new()
end
# Parses and produces a puppet expression from the given string.
# @param string [String] puppet source e.g. "1 + 2"
# @param source_file [String] the source location, typically `__File__`
# @return [Puppet::Pops::Model::Expression] an expression (that can be bound)
# @api public
#
def self.puppet_expression(string, source_file)
parser.parse_string(string, source_file).current
end
# Parses and produces a puppet string expression from the given string.
# The string will automatically be quoted and special characters escaped.
# As an example if given the (ruby) string "Hi\nMary" it is transformed to
# the puppet string (illustrated with a ruby string) "\"Hi\\nMary\”" before being
# parsed.
#
# @param string [String] puppet source e.g. "On node ${fqdn}"
# @param source_file [String] the source location, typically `__File__`
# @return [Puppet::Pops::Model::Expression] an expression (that can be bound)
# @api public
#
def self.puppet_string(string, source_file)
parser.parse_string(parser.quote(string), source_file).current
end
end
diff --git a/lib/puppet/pops/binder/bindings_label_provider.rb b/lib/puppet/pops/binder/bindings_label_provider.rb
index ae38643eb..f7555468a 100644
--- a/lib/puppet/pops/binder/bindings_label_provider.rb
+++ b/lib/puppet/pops/binder/bindings_label_provider.rb
@@ -1,42 +1,46 @@
# A provider of labels for bindings model object, producing a human name for the model object.
# @api private
#
class Puppet::Pops::Binder::BindingsLabelProvider < Puppet::Pops::LabelProvider
def initialize
@@label_visitor ||= Puppet::Pops::Visitor.new(self,"label",0,0)
end
# Produces a label for the given object without article.
# @return [String] a human readable label
#
def label o
@@label_visitor.visit(o)
end
def label_PObjectType o ; "#{Puppet::Pops::Types::TypeFactory.label(o)}" end
def label_ProducerDescriptor o ; "Producer" end
def label_NonCachingProducerDescriptor o ; "Non Caching Producer" end
def label_ConstantProducerDescriptor o ; "Producer['#{o.value}']" end
def label_EvaluatingProducerDescriptor o ; "Evaluating Producer" end
def label_InstanceProducerDescriptor o ; "Producer[#{o.class_name}]" end
def label_LookupProducerDescriptor o ; "Lookup Producer[#{o.name}]" end
def label_HashLookupProducerDescriptor o ; "Hash Lookup Producer[#{o.name}][#{o.key}]" end
def label_FirstFoundProducerDescriptor o ; "First Found Producer" end
def label_ProducerProducerDescriptor o ; "Producer[Producer]" end
def label_MultibindProducerDescriptor o ; "Multibind Producer" end
def label_ArrayMultibindProducerDescriptor o ; "Array Multibind Producer" end
def label_HashMultibindProducerDescriptor o ; "Hash Multibind Producer" end
- def label_Binding o ; "Binding" end
- def label_Multibinding o ; "Multibinding" end
- def label_MultibindContribution o ; "Multibind Contribution" end
def label_Bindings o ; "Bindings" end
def label_NamedBindings o ; "Named Bindings" end
def label_Category o ; "Category '#{o.categorization}/#{o.value}'" end
def label_CategorizedBindings o ; "Categorized Bindings" end
def label_LayeredBindings o ; "Layered Bindings" end
def label_NamedLayer o ; "Layer '#{o.name}'" end
def label_EffectiveCategories o ; "Effective Categories" end
def label_ContributedBindings o ; "Contributed Bindings" end
def label_NamedArgument o ; "Named Argument" end
+ def label_Binding(o)
+ 'Binding' + (o.multibind_id.nil? ? '' : ' In Multibind')
+ end
+ def label_Multibinding(o)
+ 'Multibinding' + (o.multibind_id.nil? ? '' : ' In Multibind')
+ end
+
end
diff --git a/lib/puppet/pops/binder/bindings_model.rb b/lib/puppet/pops/binder/bindings_model.rb
index fb7b3808c..92e13df7e 100644
--- a/lib/puppet/pops/binder/bindings_model.rb
+++ b/lib/puppet/pops/binder/bindings_model.rb
@@ -1,220 +1,215 @@
require 'rgen/metamodel_builder'
# The Bindings model is a model of Key to Producer mappings (bindings).
# The central concept is that a Bindings is a nested structure of bindings.
# A top level Bindings should be a NamedBindings (the name is used primarily
# in error messages). A Key is a Type/Name combination.
#
# TODO: In this version, references to "any object" uses the class Object.
# this is only temporarily. The intent is to use specific Puppet Objects
# that are typed using the Puppet Type System. (This to enable serialization)
#
# @see Puppet::Pops::Binder::BindingsFactory The BindingsFactory for more details on how to create model instances.
# @api public
module Puppet::Pops::Binder::Bindings
# @abstract
# @api public
#
class AbstractBinding < Puppet::Pops::Model::PopsObject
abstract
end
# An abstract producer
# @abstract
# @api public
#
class ProducerDescriptor < Puppet::Pops::Model::PopsObject
abstract
contains_one_uni 'transformer', Puppet::Pops::Model::LambdaExpression
end
# All producers are singleton producers unless wrapped in a non caching producer
# where each lookup produces a new instance. It is an error to have a nesting level > 1
# and to nest a NonCachingProducerDescriptor.
#
# @api public
#
class NonCachingProducerDescriptor < ProducerDescriptor
contains_one_uni 'producer', ProducerDescriptor
end
# Produces a constant value (i.e. something of {Puppet::Pops::Types::PDataType PDataType})
# @api public
#
class ConstantProducerDescriptor < ProducerDescriptor
# TODO: This should be a typed Puppet Object
has_attr 'value', Object
end
# Produces a value by evaluating a Puppet DSL expression
# @api public
#
class EvaluatingProducerDescriptor < ProducerDescriptor
contains_one_uni 'expression', Puppet::Pops::Model::Expression
end
# An InstanceProducer creates an instance of the given class
# Arguments are passed to the class' `new` operator in the order they are given.
# @api public
#
class InstanceProducerDescriptor < ProducerDescriptor
# TODO: This should be a typed Puppet Object ??
has_many_attr 'arguments', Object, :upperBound => -1
has_attr 'class_name', String
end
# A ProducerProducerDescriptor, describes that the produced instance is itself a Producer
# that should be used to produce the value.
# @api public
#
class ProducerProducerDescriptor < ProducerDescriptor
contains_one_uni 'producer', ProducerDescriptor, :lowerBound => 1
end
# Produces a value by looking up another key (type/name)
# @api public
#
class LookupProducerDescriptor < ProducerDescriptor
contains_one_uni 'type', Puppet::Pops::Types::PObjectType
has_attr 'name', String
end
# Produces a value by looking up another multibound key, and then looking up
# the detail using a detail_key.
# This is used to produce a specific service of a given type (such as a SyntaxChecker for the syntax "json").
# @api public
#
class HashLookupProducerDescriptor < LookupProducerDescriptor
has_attr 'key', String
end
# Produces a value by looking up each producer in turn. The first existing producer wins.
# @api public
#
class FirstFoundProducerDescriptor < ProducerDescriptor
contains_many_uni 'producers', LookupProducerDescriptor
end
# @api public
# @abstract
class MultibindProducerDescriptor < ProducerDescriptor
abstract
end
# Used in a Multibind of Array type unless it has a producer. May explicitly be used as well.
# @api public
#
class ArrayMultibindProducerDescriptor < MultibindProducerDescriptor
end
# Used in a Multibind of Hash type unless it has a producer. May explicitly be used as well.
# @api public
#
class HashMultibindProducerDescriptor < MultibindProducerDescriptor
end
# Plays the role of "Hash[String, Object] entry" but with keys in defined order.
#
# @api public
#
class NamedArgument < Puppet::Pops::Model::PopsObject
has_attr 'name', String, :lowerBound => 1
has_attr 'value', Object, :lowerBound => 1
end
# Binds a type/name combination to a producer. Optionally marking the bindidng as being abstract, or being an
# override of another binding. Optionally, the binding defines producer arguments passed to the producer when
# it is created.
#
# @api public
class Binding < AbstractBinding
contains_one_uni 'type', Puppet::Pops::Types::PObjectType
has_attr 'name', String
has_attr 'override', Boolean
has_attr 'abstract', Boolean
+ # If set is a contribution in a multibind
+ has_attr 'multibind_id', String, :lowerBound => 0
# Invariant: Only multibinds may have lowerBound 0, all regular Binding must have a producer.
contains_one_uni 'producer', ProducerDescriptor, :lowerBound => 0
contains_many_uni 'producer_args', NamedArgument, :lowerBound => 0
end
# A multibinding is a binding other bindings can contribute to.
#
# @api public
class Multibinding < Binding
has_attr 'id', String
end
- # A binding in a multibind
- # @api public
- #
- class MultibindContribution < Binding
- has_attr 'multibind_id', String, :lowerBound => 1
- end
-
# A container of Binding instances
# @api public
#
class Bindings < AbstractBinding
contains_many_uni 'bindings', AbstractBinding
end
# The top level container of bindings can have a name (for error messages, logging, tracing).
# May be nested.
# @api public
#
class NamedBindings < Bindings
has_attr 'name', String
end
# A category predicate (the request has to be in this category).
# @api public
#
class Category < Puppet::Pops::Model::PopsObject
has_attr 'categorization', String, :lowerBound => 1
has_attr 'value', String, :lowerBound => 1
end
# A container of Binding instances that are in effect when the
# predicates (min one) evaluates to true. Multiple predicates are handles as an 'and'.
# Note that 'or' semantics are handled by repeating the same rules.
# @api public
#
class CategorizedBindings < Bindings
contains_many_uni 'predicates', Category, :lowerBound => 1
end
# A named layer of bindings having the same priority.
# @api public
class NamedLayer < Puppet::Pops::Model::PopsObject
has_attr 'name', String, :lowerBound => 1
contains_many_uni 'bindings', NamedBindings
end
# A list of layers with bindings in descending priority order.
# @api public
#
class LayeredBindings < Puppet::Pops::Model::PopsObject
contains_many_uni 'layers', NamedLayer
end
# A list of categories consisting of categroization name and category value (i.e. the *state of the request*)
# @api public
#
class EffectiveCategories < Puppet::Pops::Model::PopsObject
# The order is from highest precedence to lowest
contains_many_uni 'categories', Category
end
# ContributedBindings is a named container of one or more NamedBindings.
# The intent is that a bindings producer returns a ContributedBindings which in addition to the bindings
# may optionally contain provider's opinion about the precedence of categories, and their category values.
# This enables merging of bindings, and validation of consistency.
#
# @api public
#
class ContributedBindings < NamedLayer
contains_one_uni 'effective_categories', EffectiveCategories
end
end
diff --git a/lib/puppet/pops/binder/bindings_model_dumper.rb b/lib/puppet/pops/binder/bindings_model_dumper.rb
index dbfa1f9a5..8416dc651 100644
--- a/lib/puppet/pops/binder/bindings_model_dumper.rb
+++ b/lib/puppet/pops/binder/bindings_model_dumper.rb
@@ -1,226 +1,205 @@
# 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? ? '<no-name>': 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_Array o
o.collect {|e| do_dump(e) }
end
def dump_ASTArray o
["[]"] + o.children.collect {|x| do_dump(x)}
end
def dump_ASTHash o
["{}"] + o.value.sort_by{|k,v| k.to_s}.collect {|x| [do_dump(x[0]), do_dump(x[1])]}
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_Hostclass o
-# # ok, this is kind of crazy stuff in the AST, information in a context instead of in AST, and
-# # parameters are in a Ruby Array with each parameter being an Array...
-# #
-# context = o.context
-# args = context[:arguments]
-# parent = context[:parent]
-# result = ["class", o.name]
-# result << ["inherits", parent] if parent
-# result << ["parameters"] + args.collect {|p| _dump_ParameterArray(p) } if args && args.size() > 0
-# if is_nop?(o.code)
-# result << []
-# else
-# result << do_dump(o.code)
-# end
-# result
-# 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
- # TODO: delegate to Pops Model Tree dumper and dump the transformer if it exists
- # o.transformer
- [o.class.name ]
+ 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
- # TODO: puppet pops model transformer dump o.expression
- dump_ProducerDescriptor(o)
+ result = dump_ProducerDescriptor(o)
+ result << expression_dumper.dump(o.expression)
end
def dump_InstanceProducerDescriptor
# TODO: o.arguments, o. transformer
['instance', o.class_name]
end
def dump_ProducerProducerDescriptor o
# skip the transformer lambda...
- ['producer-producer', do_dump(o.producer)]
+ 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_PObjectType o
type_calculator.string(o)
end
def dump_HashLookupProducerDescriptor o
# TODO: transformer lambda
- ['hash-lookup', do_dump(o.type), o.name, "[#{do_dump(o.key)}]"]
+ 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 << 'override' if o.override
result << 'abstract' if o.abstract
result.concat([do_dump(o.type), o.name])
+ result << "(in #{o.multibind_id})" if o.multibind_id
result << ['to', do_dump(o.producer)] + do_dump(o.producer_args)
result
end
def dump_Multibinding o
result = ['multibind', o.id]
result << 'override' if o.override
result << 'abstract' if o.abstract
result.concat([do_dump(o.type), o.name])
- result << ['to', do_dump(o.producer)] + do_dump(o.producer_args)
- result
- end
-
- def dump_MultibindContribution o
- result = ['contribute-to', o.multibind_id]
- result << 'override' if o.override
- result << 'abstract' if o.abstract
- result.concat([do_dump(o.type), o.name])
+ result << "(in #{o.multibind_id})" if o.multibind_id
result << ['to', do_dump(o.producer)] + do_dump(o.producer_args)
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_Category o
['category', o.categorization, do_dump(o.value)]
end
def dump_CategorizedBindings o
result = ['when', do_dump(o.predicates), :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_EffectiveCategories o
['categories', do_dump(o.categories)]
end
def dump_ContributedBindings o
['contributed', o.name, do_dump(o.effective_categories), do_dump(o.bindings)]
end
end
diff --git a/lib/puppetx.rb b/lib/puppetx.rb
index cc1df16e8..3945fe34f 100644
--- a/lib/puppetx.rb
+++ b/lib/puppetx.rb
@@ -1,45 +1,46 @@
# The Puppet Extensions Module
# Submodules of this module should be named after the publisher (e.g. 'user' part of a Puppet Module name).
# The submodule `Puppetx::Puppet` contains the puppet extension points.
#
module Puppetx
SYNTAX_CHECKERS = 'puppetx::puppet::syntaxcheckers'
SYNTAX_CHECKERS_TYPE = 'Puppetx::Puppet::SyntaxChecker'
module Puppet
# Extension-points are registered here:
# - If in a Ruby submodule it is best to create it here
# - The class does not have to be required; it will be auto required when the binder
# needs it.
# - If the extension is a multibind, it can be registered here; either with a required
# class or a class reference in string form.
# Register extension points
# -------------------------
system_bindings = ::Puppet::Pops::Binder::SystemBindings
extensions = system_bindings.extensions()
extensions.multibind(SYNTAX_CHECKERS).name(SYNTAX_CHECKERS).hash_of(SYNTAX_CHECKERS_TYPE)
end
# Module with implementations of various extensions
module Puppetlabs
# Default extensions delivered in Puppet Core are included here
module SyntaxCheckers
# Classes in this name-space are lazily loaded as they may be overridden and/or never used
# (Lazy loading is done by binding to the name of a class instead of a Class instance).
# Register extensions
# -------------------
system_bindings = ::Puppet::Pops::Binder::SystemBindings
bindings = system_bindings.default_bindings()
- bindings.
- bind_in_multibind(SYNTAX_CHECKERS ).name('json').
- instance_of(SYNTAX_CHECKERS_TYPE).
- to_instance('Puppetx::Puppetlabs::SyntaxCheckers::Json')
+ bindings.bind.
+ name('json').
+ instance_of(SYNTAX_CHECKERS_TYPE).
+ in_multibind(SYNTAX_CHECKERS).
+ to_instance('Puppetx::Puppetlabs::SyntaxCheckers::Json')
end
end
end
\ No newline at end of file
diff --git a/spec/unit/pops/binder/injector_spec.rb b/spec/unit/pops/binder/injector_spec.rb
index db54b0d37..e9cad905c 100644
--- a/spec/unit/pops/binder/injector_spec.rb
+++ b/spec/unit/pops/binder/injector_spec.rb
@@ -1,735 +1,751 @@
require 'spec_helper'
require 'puppet/pops'
module InjectorSpecModule
def injector(binder)
Puppet::Pops::Binder::Injector.new(binder)
end
def factory
Puppet::Pops::Binder::BindingsFactory
end
def test_layer_with_empty_bindings
factory.named_layer('test-layer', factory.named_bindings('test').model)
end
def test_layer_with_bindings(*bindings)
factory.named_layer('test-layer', *bindings)
end
def null_scope()
nil
end
def type_calculator
Puppet::Pops::Types::TypeCalculator
end
def type_factory
Puppet::Pops::Types::TypeFactory
end
# Returns a binder with the effective categories highest/test, node/kermit, environment/dev (and implicit 'common')
#
def binder_with_categories
b = Puppet::Pops::Binder::Binder.new()
b.define_categories(factory.categories(['highest', 'test', 'node', 'kermit', 'environment','dev']))
b
end
class TestDuck
end
class Daffy < TestDuck
end
class AngryDuck < TestDuck
# Supports assisted inject, returning a Donald duck as the default impl of Duck
def self.inject(injector, scope, binding, *args)
Donald.new()
end
end
class Donald < AngryDuck
end
class ArneAnka < AngryDuck
attr_reader :label
def initialize()
@label = 'A Swedish angry cartoon duck'
end
end
class ScroogeMcDuck < TestDuck
attr_reader :fortune
# Supports assisted inject, returning an ScroogeMcDuck with 1$ fortune or first arg in args
# Note that when injected (via instance producer, or implict assisted inject, the inject method
# always wins.
def self.inject(injector, scope, binding, *args)
self.new(args[0].nil? ? 1 : args[0])
end
def initialize(fortune)
@fortune = fortune
end
end
class NamedDuck < TestDuck
attr_reader :name
def initialize(name)
@name = name
end
end
# Test custom producer that on each produce returns a duck that is twice as rich as its predecessor
class ScroogeProducer < Puppet::Pops::Binder::Producers::Producer
attr_reader :next_capital
def initialize
@next_capital = 100
end
def produce(scope)
ScroogeMcDuck.new(@next_capital *= 2)
end
end
end
describe 'Injector' do
include InjectorSpecModule
let(:bindings) { factory.named_bindings('test') }
let(:scope) { null_scope()}
let(:duck_type) { type_factory.ruby(InjectorSpecModule::TestDuck) }
let(:binder) { Puppet::Pops::Binder::Binder.new()}
let(:cbinder) do
b = Puppet::Pops::Binder::Binder.new()
b.define_categories(factory.categories([]))
b
end
let(:lbinder) do
cbinder.define_layers(layered_bindings)
end
let(:layered_bindings) { factory.layered_bindings(test_layer_with_bindings(bindings.model)) }
let(:xinjector) { Puppet::Pops::Binder::Injector.new(lbinder) }
context 'When created' do
it 'should raise an error when given binder is not configured at all' do
expect { Puppet::Pops::Binder::Injector.new(binder()) }.to raise_error(/Given Binder is not configured/)
end
it 'should raise an error if binder has categories, but is not completely configured' do
expect { Puppet::Pops::Binder::Injector.new(cbinder) }.to raise_error(/Given Binder is not configured/)
end
it 'should not raise an error if binder is configured' do
lbinder.configured?().should == true # of something is very wrong
expect { injector(lbinder) }.to_not raise_error
end
it 'should create an empty injector given an empty binder' do
expect { cbinder.define_layers(layered_bindings) }.to_not raise_exception
end
it "should be possible to reference the TypeCalculator" do
injector(lbinder).type_calculator.is_a?(Puppet::Pops::Types::TypeCalculator).should == true
end
it "should be possible to reference the KeyFactory" do
injector(lbinder).key_factory.is_a?(Puppet::Pops::Binder::KeyFactory).should == true
end
end
context "When looking up objects" do
it 'lookup(scope, name) finds bound object of type Data with given name' do
bindings.bind().name('a_string').to('42')
injector(lbinder).lookup(scope, 'a_string').should == '42'
end
context 'a block transforming the result can be given' do
it 'that transform a found value given scope and value' do
bindings.bind().name('a_string').to('42')
injector(lbinder).lookup(scope, 'a_string') {|zcope, val| val + '42' }.should == '4242'
end
it 'that transform a found value given only value' do
bindings.bind().name('a_string').to('42')
injector(lbinder).lookup(scope, 'a_string') {|val| val + '42' }.should == '4242'
end
it 'that produces a default value when entry is missing' do
bindings.bind().name('a_string').to('42')
injector(lbinder).lookup(scope, 'a_non_existing_string') {|val| val ? (raise Error, "Should not happen") : '4242' }.should == '4242'
end
end
context "and class is not bound" do
it "assisted inject kicks in for classes with zero args constructor" do
duck_type = type_factory.ruby(InjectorSpecModule::Daffy)
injector = injector(lbinder)
injector.lookup(scope, duck_type).is_a?(InjectorSpecModule::Daffy).should == true
injector.lookup_producer(scope, duck_type).produce(scope).is_a?(InjectorSpecModule::Daffy).should == true
end
it "assisted inject produces same instance on lookup but not on lookup producer" do
duck_type = type_factory.ruby(InjectorSpecModule::Daffy)
injector = injector(lbinder)
d1 = injector.lookup(scope, duck_type)
d2 = injector.lookup(scope, duck_type)
d1.equal?(d2).should == true
d1 = injector.lookup_producer(scope, duck_type).produce(scope)
d2 = injector.lookup_producer(scope, duck_type).produce(scope)
d1.equal?(d2).should == false
end
it "assisted inject kicks in for classes with a class inject method" do
duck_type = type_factory.ruby(InjectorSpecModule::ScroogeMcDuck)
injector = injector(lbinder)
# Do not pass any arguments, the ScroogeMcDuck :inject method should pick 1 by default
# This tests zero args passed
injector.lookup(scope, duck_type).fortune.should == 1
injector.lookup_producer(scope, duck_type).produce(scope).fortune.should == 1
end
it "assisted inject selects the inject method if it exists over a zero args constructor" do
injector = injector(lbinder)
duck_type = type_factory.ruby(InjectorSpecModule::AngryDuck)
injector.lookup(scope, duck_type).is_a?(InjectorSpecModule::Donald).should == true
injector.lookup_producer(scope, duck_type).produce(scope).is_a?(InjectorSpecModule::Donald).should == true
end
it "assisted inject selects the zero args constructor if injector is from a superclass" do
injector = injector(lbinder)
duck_type = type_factory.ruby(InjectorSpecModule::ArneAnka)
injector.lookup(scope, duck_type).is_a?(InjectorSpecModule::ArneAnka).should == true
injector.lookup_producer(scope, duck_type).produce(scope).is_a?(InjectorSpecModule::ArneAnka).should == true
end
end
context 'and conditionals are in use' do
let(:binder) { binder_with_categories()}
let(:lbinder) { binder.define_layers(layered_bindings) }
it "should be possible to shadow a bound value in a higher precedented category" do
bindings.bind().name('a_string').to('42')
bindings.when_in_category('environment', 'dev').bind().name('a_string').to('43')
bindings.when_in_category('node', 'kermit').bind().name('a_string').to('being green')
injector(lbinder).lookup(scope,'a_string').should == 'being green'
end
it "shadowing should not happen when not in a category" do
bindings.bind().name('a_string').to('42')
bindings.when_in_category('environment', 'dev').bind().name('a_string').to('43')
bindings.when_in_category('node', 'piggy').bind().name('a_string').to('being green')
injector(lbinder).lookup(scope,'a_string').should == '43'
end
it "multiple predicates makes binding more specific" do
bindings.bind().name('a_string').to('42')
bindings.when_in_category('environment', 'dev').bind().name('a_string').to('43')
bindings.when_in_category('node', 'kermit').bind().name('a_string').to('being green')
bindings.when_in_categories({'node'=>'kermit', 'environment'=>'dev'}).bind().name('a_string').to('being dev green')
injector(lbinder).lookup(scope,'a_string').should == 'being dev green'
end
it "multiple predicates makes binding more specific, but not more specific than higher precedence" do
bindings.bind().name('a_string').to('42')
bindings.when_in_category('environment', 'dev').bind().name('a_string').to('43')
bindings.when_in_category('node', 'kermit').bind().name('a_string').to('being green')
bindings.when_in_categories({'node'=>'kermit', 'environment'=>'dev'}).bind().name('a_string').to('being dev green')
bindings.when_in_category('highest', 'test').bind().name('a_string').to('bazinga')
injector(lbinder).lookup(scope,'a_string').should == 'bazinga'
end
end
context "and multiple layers are in use" do
let(:binder) { binder_with_categories()}
it "a higher layer shadows anything in a lower layer" do
bindings1 = factory.named_bindings('test1')
bindings1.when_in_category("highest", "test").bind().name('a_string').to('bad stuff')
lower_layer = factory.named_layer('lower-layer', bindings1.model)
bindings2 = factory.named_bindings('test2')
bindings2.bind().name('a_string').to('good stuff')
higher_layer = factory.named_layer('higher-layer', bindings2.model)
binder.define_layers(factory.layered_bindings(higher_layer, lower_layer))
injector = injector(binder)
injector.lookup(scope,'a_string').should == 'good stuff'
end
end
context "and dealing with Data types" do
let(:binder) { binder_with_categories()}
let(:lbinder) { binder.define_layers(layered_bindings) }
it "should treat all data as same type w.r.t. key" do
bindings.bind().name('a_string').to('42')
bindings.bind().name('an_int').to(43)
bindings.bind().name('a_float').to(3.14)
bindings.bind().name('a_boolean').to(true)
bindings.bind().name('an_array').to([1,2,3])
bindings.bind().name('a_hash').to({'a'=>1,'b'=>2,'c'=>3})
injector = injector(lbinder)
injector.lookup(scope,'a_string').should == '42'
injector.lookup(scope,'an_int').should == 43
injector.lookup(scope,'a_float').should == 3.14
injector.lookup(scope,'a_boolean').should == true
injector.lookup(scope,'an_array').should == [1,2,3]
injector.lookup(scope,'a_hash').should == {'a'=>1,'b'=>2,'c'=>3}
end
it "should provide type-safe lookup of given type/name" do
bindings.bind().string().name('a_string').to('42')
bindings.bind().integer().name('an_int').to(43)
bindings.bind().float().name('a_float').to(3.14)
bindings.bind().boolean().name('a_boolean').to(true)
bindings.bind().array_of_data().name('an_array').to([1,2,3])
bindings.bind().hash_of_data().name('a_hash').to({'a'=>1,'b'=>2,'c'=>3})
injector = injector(lbinder)
# Check lookup using implied Data type
injector.lookup(scope,'a_string').should == '42'
injector.lookup(scope,'an_int').should == 43
injector.lookup(scope,'a_float').should == 3.14
injector.lookup(scope,'a_boolean').should == true
injector.lookup(scope,'an_array').should == [1,2,3]
injector.lookup(scope,'a_hash').should == {'a'=>1,'b'=>2,'c'=>3}
# Check lookup using expected type
injector.lookup(scope,type_factory.string(), 'a_string').should == '42'
injector.lookup(scope,type_factory.integer(), 'an_int').should == 43
injector.lookup(scope,type_factory.float(),'a_float').should == 3.14
injector.lookup(scope,type_factory.boolean(),'a_boolean').should == true
injector.lookup(scope,type_factory.array_of_data(),'an_array').should == [1,2,3]
injector.lookup(scope,type_factory.hash_of_data(),'a_hash').should == {'a'=>1,'b'=>2,'c'=>3}
# Check lookup using wrong type
expect { injector.lookup(scope,type_factory.integer(), 'a_string')}.to raise_error(/Type error/)
expect { injector.lookup(scope,type_factory.string(), 'an_int')}.to raise_error(/Type error/)
expect { injector.lookup(scope,type_factory.string(),'a_float')}.to raise_error(/Type error/)
expect { injector.lookup(scope,type_factory.string(),'a_boolean')}.to raise_error(/Type error/)
expect { injector.lookup(scope,type_factory.string(),'an_array')}.to raise_error(/Type error/)
expect { injector.lookup(scope,type_factory.string(),'a_hash')}.to raise_error(/Type error/)
end
end
end
context "When looking up producer" do
it 'the value is produced by calling produce(scope)' do
bindings.bind().name('a_string').to('42')
injector(lbinder).lookup_producer(scope, 'a_string').produce(scope).should == '42'
end
context 'a block transforming the result can be given' do
it 'that transform a found value given scope and producer' do
bindings.bind().name('a_string').to('42')
injector(lbinder).lookup_producer(scope, 'a_string') {|zcope, p| p.produce(zcope) + '42' }.should == '4242'
end
it 'that transform a found value given only producer' do
bindings.bind().name('a_string').to('42')
injector(lbinder).lookup_producer(scope, 'a_string') {|p| p.produce(scope) + '42' }.should == '4242'
end
it 'that can produce a default value when entry is not found' do
bindings.bind().name('a_string').to('42')
injector(lbinder).lookup_producer(scope, 'a_non_existing_string') {|p| p ? (raise Error,"Should not happen") : '4242' }.should == '4242'
end
end
end
context "When dealing with singleton vs. non singleton" do
it "should produce the same instance when producer is a singleton" do
bindings.bind().name('a_string').to('42')
injector = injector(lbinder)
a = injector.lookup(scope, 'a_string')
b = injector.lookup(scope, 'a_string')
a.equal?(b).should == true
end
it "should produce different instances when producer is a non singleton producer" do
bindings.bind().name('a_string').to_series_of('42')
injector = injector(lbinder)
a = injector.lookup(scope, 'a_string')
b = injector.lookup(scope, 'a_string')
a.should == '42'
b.should == '42'
a.equal?(b).should == false
end
end
context "When using the lookup producer" do
it "should lookup again to produce a value" do
bindings.bind().name('a_string').to_lookup_of('another_string')
bindings.bind().name('another_string').to('hello')
injector(lbinder).lookup(scope, 'a_string').should == 'hello'
end
it "should produce nil if looked up key does not exist" do
bindings.bind().name('a_string').to_lookup_of('non_existing')
injector(lbinder).lookup(scope, 'a_string').should == nil
end
it "should report an error if lookup loop is detected" do
bindings.bind().name('a_string').to_lookup_of('a_string')
expect { injector(lbinder).lookup(scope, 'a_string') }.to raise_error(/Lookup loop/)
end
end
context "When using the hash lookup producer" do
it "should lookup a key in looked up hash" do
data_hash = type_factory.hash_of_data()
bindings.bind().name('a_string').to_hash_lookup_of(data_hash, 'a_hash', 'huey')
bindings.bind().name('a_hash').to({'huey' => 'red', 'dewey' => 'blue', 'louie' => 'green'})
injector(lbinder).lookup(scope, 'a_string').should == 'red'
end
it "should produce nil if looked up entry does not exist" do
data_hash = type_factory.hash_of_data()
bindings.bind().name('a_string').to_hash_lookup_of(data_hash, 'non_existing_entry', 'huey')
bindings.bind().name('a_hash').to({'huey' => 'red', 'dewey' => 'blue', 'louie' => 'green'})
injector(lbinder).lookup(scope, 'a_string').should == nil
end
end
context "When using the first found producer" do
it "should lookup until it finds a value, but no further" do
bindings.bind().name('a_string').to_first_found(['b_string', 'c_string', 'g_string'])
bindings.bind().name('c_string').to('hello')
bindings.bind().name('g_string').to('Oh, mrs. Smith...')
injector(lbinder).lookup(scope, 'a_string').should == 'hello'
end
end
context "When producing instances" do
it "should lookup an instance of a class without arguments" do
bindings.bind().type(duck_type).name('the_duck').to(InjectorSpecModule::Daffy)
injector(lbinder).lookup(scope, duck_type, 'the_duck').is_a?(InjectorSpecModule::Daffy).should == true
end
it "should lookup an instance of a class with arguments" do
bindings.bind().type(duck_type).name('the_duck').to(InjectorSpecModule::ScroogeMcDuck, 1234)
injector = injector(lbinder)
the_duck = injector.lookup(scope, duck_type, 'the_duck')
the_duck.is_a?(InjectorSpecModule::ScroogeMcDuck).should == true
the_duck.fortune.should == 1234
end
it "singleton producer should not be recreated between lookups" do
bindings.bind().type(duck_type).name('the_duck').to_producer(InjectorSpecModule::ScroogeProducer)
injector = injector(lbinder)
the_duck = injector.lookup(scope, duck_type, 'the_duck')
the_duck.is_a?(InjectorSpecModule::ScroogeMcDuck).should == true
the_duck.fortune.should == 200
# singleton, do it again to get next value in series - it is the producer that is a singleton
# not the produced value
the_duck = injector.lookup(scope, duck_type, 'the_duck')
the_duck.is_a?(InjectorSpecModule::ScroogeMcDuck).should == true
the_duck.fortune.should == 400
duck_producer = injector.lookup_producer(scope, duck_type, 'the_duck')
duck_producer.produce(scope).fortune.should == 800
end
it "series of producers should recreate producer on each lookup and lookup_producer" do
bindings.bind().type(duck_type).name('the_duck').to_producer_series(InjectorSpecModule::ScroogeProducer)
injector = injector(lbinder)
duck_producer = injector.lookup_producer(scope, duck_type, 'the_duck')
duck_producer.produce(scope).fortune().should == 200
duck_producer.produce(scope).fortune().should == 400
# series, each lookup gets a new producer (initialized to produce 200)
duck_producer = injector.lookup_producer(scope, duck_type, 'the_duck')
duck_producer.produce(scope).fortune().should == 200
duck_producer.produce(scope).fortune().should == 400
injector.lookup(scope, duck_type, 'the_duck').fortune().should == 200
injector.lookup(scope, duck_type, 'the_duck').fortune().should == 200
end
end
context "When working with multibind" do
context "of hash kind" do
it "a multibind produces contributed items keyed by their bound key-name" do
hash_of_duck = type_factory.hash_of(duck_type)
multibind_id = "ducks"
bindings.multibind(multibind_id).type(hash_of_duck).name('donalds_nephews')
- bindings.bind_in_multibind(multibind_id).type(duck_type).name('nephew1').to(InjectorSpecModule::NamedDuck, 'Huey')
- bindings.bind_in_multibind(multibind_id).type(duck_type).name('nephew2').to(InjectorSpecModule::NamedDuck, 'Dewey')
- bindings.bind_in_multibind(multibind_id).type(duck_type).name('nephew3').to(InjectorSpecModule::NamedDuck, 'Louie')
+ bindings.bind.in_multibind(multibind_id).type(duck_type).name('nephew1').to(InjectorSpecModule::NamedDuck, 'Huey')
+ bindings.bind.in_multibind(multibind_id).type(duck_type).name('nephew2').to(InjectorSpecModule::NamedDuck, 'Dewey')
+ bindings.bind.in_multibind(multibind_id).type(duck_type).name('nephew3').to(InjectorSpecModule::NamedDuck, 'Louie')
injector = injector(lbinder)
the_ducks = injector.lookup(scope, hash_of_duck, "donalds_nephews")
the_ducks.size.should == 3
the_ducks['nephew1'].name.should == 'Huey'
the_ducks['nephew2'].name.should == 'Dewey'
the_ducks['nephew3'].name.should == 'Louie'
end
it "is an error to not bind contribution with a name" do
hash_of_duck = type_factory.hash_of(duck_type)
multibind_id = "ducks"
bindings.multibind(multibind_id).type(hash_of_duck).name('donalds_nephews')
# missing name
- bindings.bind_in_multibind(multibind_id).type(duck_type).to(InjectorSpecModule::NamedDuck, 'Huey')
- bindings.bind_in_multibind(multibind_id).type(duck_type).to(InjectorSpecModule::NamedDuck, 'Dewey')
+ bindings.bind.in_multibind(multibind_id).type(duck_type).to(InjectorSpecModule::NamedDuck, 'Huey')
+ bindings.bind.in_multibind(multibind_id).type(duck_type).to(InjectorSpecModule::NamedDuck, 'Dewey')
expect {
the_ducks = injector(lbinder).lookup(scope, hash_of_duck, "donalds_nephews")
}.to raise_error(/must have a name/)
end
it "is an error to bind with duplicate key when using default (priority) conflict resolution" do
hash_of_duck = type_factory.hash_of(duck_type)
multibind_id = "ducks"
bindings.multibind(multibind_id).type(hash_of_duck).name('donalds_nephews')
# missing name
- bindings.bind_in_multibind(multibind_id).type(duck_type).name('foo').to(InjectorSpecModule::NamedDuck, 'Huey')
- bindings.bind_in_multibind(multibind_id).type(duck_type).name('foo').to(InjectorSpecModule::NamedDuck, 'Dewey')
+ bindings.bind.in_multibind(multibind_id).type(duck_type).name('foo').to(InjectorSpecModule::NamedDuck, 'Huey')
+ bindings.bind.in_multibind(multibind_id).type(duck_type).name('foo').to(InjectorSpecModule::NamedDuck, 'Dewey')
expect {
the_ducks = injector(lbinder).lookup(scope, hash_of_duck, "donalds_nephews")
}.to raise_error(/Duplicate key/)
end
it "is not an error to bind with duplicate key when using (ignore) conflict resolution" do
hash_of_duck = type_factory.hash_of(duck_type)
multibind_id = "ducks"
bindings.multibind(multibind_id).type(hash_of_duck).name('donalds_nephews').producer_options(:conflict_resolution => :ignore)
- bindings.bind_in_multibind(multibind_id).type(duck_type).name('foo').to(InjectorSpecModule::NamedDuck, 'Huey')
- bindings.bind_in_multibind(multibind_id).type(duck_type).name('foo').to(InjectorSpecModule::NamedDuck, 'Dewey')
+ bindings.bind.in_multibind(multibind_id).type(duck_type).name('foo').to(InjectorSpecModule::NamedDuck, 'Huey')
+ bindings.bind.in_multibind(multibind_id).type(duck_type).name('foo').to(InjectorSpecModule::NamedDuck, 'Dewey')
expect {
the_ducks = injector(lbinder).lookup(scope, hash_of_duck, "donalds_nephews")
}.to_not raise_error(/Duplicate key/)
end
it "should produce detailed type error message" do
hash_of_integer = type_factory.hash_of(type_factory.integer())
multibind_id = "ints"
mb = bindings.multibind(multibind_id).type(hash_of_integer).name('donalds_family')
- bindings.bind_in_multibind(multibind_id).name('nephew').to('Huey')
+ bindings.bind.in_multibind(multibind_id).name('nephew').to('Huey')
expect { ducks = injector(lbinder).lookup(scope, 'donalds_family')
}.to raise_error(%r{expected: Integer, got: String})
end
it "should be possible to combine hash multibind contributions with append on conflict" do
# This case uses a multibind of individual strings, but combines them
# into an array bound to a hash key
# (There are other ways to do this - e.g. have the multibind lookup a multibind
# of array type to which nephews are contributed).
#
hash_of_data = type_factory.hash_of_data()
multibind_id = "ducks"
mb = bindings.multibind(multibind_id).type(hash_of_data).name('donalds_family')
mb.producer_options(:conflict_resolution => :append)
- bindings.bind_in_multibind(multibind_id).name('nephews').to('Huey')
- bindings.bind_in_multibind(multibind_id).name('nephews').to('Dewey')
- bindings.bind_in_multibind(multibind_id).name('nephews').to('Louie')
- bindings.bind_in_multibind(multibind_id).name('uncles').to('Scrooge McDuck')
- bindings.bind_in_multibind(multibind_id).name('uncles').to('Ludwig Von Drake')
+ bindings.bind.in_multibind(multibind_id).name('nephews').to('Huey')
+ bindings.bind.in_multibind(multibind_id).name('nephews').to('Dewey')
+ bindings.bind.in_multibind(multibind_id).name('nephews').to('Louie')
+ bindings.bind.in_multibind(multibind_id).name('uncles').to('Scrooge McDuck')
+ bindings.bind.in_multibind(multibind_id).name('uncles').to('Ludwig Von Drake')
ducks = injector(lbinder).lookup(scope, 'donalds_family')
ducks['nephews'].should == ['Huey', 'Dewey', 'Louie']
ducks['uncles'].should == ['Scrooge McDuck', 'Ludwig Von Drake']
end
it "should be possible to combine hash multibind contributions with append, flat, and uniq, on conflict" do
# This case uses a multibind of individual strings, but combines them
# into an array bound to a hash key
# (There are other ways to do this - e.g. have the multibind lookup a multibind
# of array type to which nephews are contributed).
#
hash_of_data = type_factory.hash_of_data()
multibind_id = "ducks"
mb = bindings.multibind(multibind_id).type(hash_of_data).name('donalds_family')
mb.producer_options(:conflict_resolution => :append, :flatten => true, :uniq => true)
- bindings.bind_in_multibind(multibind_id).name('nephews').to('Huey')
- bindings.bind_in_multibind(multibind_id).name('nephews').to('Huey')
- bindings.bind_in_multibind(multibind_id).name('nephews').to('Dewey')
- bindings.bind_in_multibind(multibind_id).name('nephews').to(['Huey', ['Louie'], 'Dewey'])
- bindings.bind_in_multibind(multibind_id).name('uncles').to('Scrooge McDuck')
- bindings.bind_in_multibind(multibind_id).name('uncles').to('Ludwig Von Drake')
+ bindings.bind.in_multibind(multibind_id).name('nephews').to('Huey')
+ bindings.bind.in_multibind(multibind_id).name('nephews').to('Huey')
+ bindings.bind.in_multibind(multibind_id).name('nephews').to('Dewey')
+ bindings.bind.in_multibind(multibind_id).name('nephews').to(['Huey', ['Louie'], 'Dewey'])
+ bindings.bind.in_multibind(multibind_id).name('uncles').to('Scrooge McDuck')
+ bindings.bind.in_multibind(multibind_id).name('uncles').to('Ludwig Von Drake')
ducks = injector(lbinder).lookup(scope, 'donalds_family')
ducks['nephews'].should == ['Huey', 'Dewey', 'Louie']
ducks['uncles'].should == ['Scrooge McDuck', 'Ludwig Von Drake']
end
it "should fail attempts to append, perform uniq or flatten on type incompatible multibind hash" do
hash_of_integer = type_factory.hash_of(type_factory.integer())
ids = ["ducks1", "ducks2", "ducks3"]
mb = bindings.multibind(ids[0]).type(hash_of_integer).name('broken_family0')
mb.producer_options(:conflict_resolution => :append)
mb = bindings.multibind(ids[1]).type(hash_of_integer).name('broken_family1')
mb.producer_options(:flatten => :true)
mb = bindings.multibind(ids[2]).type(hash_of_integer).name('broken_family2')
mb.producer_options(:uniq => :true)
binder.define_categories(factory.categories([]))
binder.define_layers(factory.layered_bindings(test_layer_with_bindings(bindings.model)))
injector = injector(binder)
expect { injector.lookup(scope, 'broken_family0')}.to raise_error(/:conflict_resolution => :append/)
expect { injector.lookup(scope, 'broken_family1')}.to raise_error(/:flatten/)
expect { injector.lookup(scope, 'broken_family2')}.to raise_error(/:uniq/)
end
it "a higher priority contribution is selected when resolution is :priority" do
hash_of_duck = type_factory.hash_of(duck_type)
multibind_id = "ducks"
bindings.multibind(multibind_id).type(hash_of_duck).name('donalds_nephews')
- mb1 = bindings.when_in_category("highest", "test").bind_in_multibind(multibind_id)
+ mb1 = bindings.when_in_category("highest", "test").bind.in_multibind(multibind_id)
mb1.type(duck_type).name('nephew').to(InjectorSpecModule::NamedDuck, 'Huey')
- mb2 = bindings.bind_in_multibind(multibind_id)
+ mb2 = bindings.bind.in_multibind(multibind_id)
mb2.type(duck_type).name('nephew').to(InjectorSpecModule::NamedDuck, 'Dewey')
binder.define_categories(factory.categories(['highest', 'test']))
binder.define_layers(layered_bindings)
injector(binder).lookup(scope, hash_of_duck, "donalds_nephews")['nephew'].name.should == 'Huey'
end
it "a higher priority contribution wins when resolution is :merge" do
hash_of_data = type_factory.hash_of_data()
multibind_id = "hashed_ducks"
bindings.multibind(multibind_id).type(hash_of_data).name('donalds_nephews').producer_options(:conflict_resolution => :merge)
- mb1 = bindings.when_in_category("highest", "test").bind_in_multibind(multibind_id)
+ mb1 = bindings.when_in_category("highest", "test").bind.in_multibind(multibind_id)
mb1.name('nephew').to({'name' => 'Huey', 'is' => 'winner'})
- mb2 = bindings.bind_in_multibind(multibind_id)
+ mb2 = bindings.bind.in_multibind(multibind_id)
mb2.name('nephew').to({'name' => 'Dewey', 'is' => 'looser', 'has' => 'cap'})
binder.define_categories(factory.categories(['highest', 'test']))
binder.define_layers(layered_bindings)
the_ducks = injector(binder).lookup(scope, "donalds_nephews");
the_ducks['nephew']['name'].should == 'Huey'
the_ducks['nephew']['is'].should == 'winner'
the_ducks['nephew']['has'].should == 'cap'
end
end
context "of array kind" do
it "an array multibind produces contributed items, names are allowed but ignored" do
array_of_duck = type_factory.array_of(duck_type)
multibind_id = "ducks"
bindings.multibind(multibind_id).type(array_of_duck).name('donalds_nephews')
# one with name (ignored, expect no error)
- bindings.bind_in_multibind(multibind_id).type(duck_type).name('nephew1').to(InjectorSpecModule::NamedDuck, 'Huey')
+ bindings.bind.in_multibind(multibind_id).type(duck_type).name('nephew1').to(InjectorSpecModule::NamedDuck, 'Huey')
# two without name
- bindings.bind_in_multibind(multibind_id).type(duck_type).to(InjectorSpecModule::NamedDuck, 'Dewey')
- bindings.bind_in_multibind(multibind_id).type(duck_type).to(InjectorSpecModule::NamedDuck, 'Louie')
+ bindings.bind.in_multibind(multibind_id).type(duck_type).to(InjectorSpecModule::NamedDuck, 'Dewey')
+ bindings.bind.in_multibind(multibind_id).type(duck_type).to(InjectorSpecModule::NamedDuck, 'Louie')
the_ducks = injector(lbinder).lookup(scope, array_of_duck, "donalds_nephews")
the_ducks.size.should == 3
the_ducks.collect {|d| d.name }.sort.should == ['Dewey', 'Huey', 'Louie']
end
it "should be able to make result contain only unique entries" do
# This case uses a multibind of individual strings, and combines them
# into an array of unique values
#
array_of_data = type_factory.array_of_data()
multibind_id = "ducks"
mb = bindings.multibind(multibind_id).type(array_of_data).name('donalds_family')
# turn off priority on named to not trigger conflict as all additions have the same precedence
# (could have used the default for unnamed and add unnamed entries).
mb.producer_options(:priority_on_named => false, :uniq => true)
- bindings.bind_in_multibind(multibind_id).name('nephews').to('Huey')
- bindings.bind_in_multibind(multibind_id).name('nephews').to('Dewey')
- bindings.bind_in_multibind(multibind_id).name('nephews').to('Dewey') # duplicate
- bindings.bind_in_multibind(multibind_id).name('nephews').to('Louie')
- bindings.bind_in_multibind(multibind_id).name('nephews').to('Louie') # duplicate
- bindings.bind_in_multibind(multibind_id).name('nephews').to('Louie') # duplicate
+ bindings.bind.in_multibind(multibind_id).name('nephews').to('Huey')
+ bindings.bind.in_multibind(multibind_id).name('nephews').to('Dewey')
+ bindings.bind.in_multibind(multibind_id).name('nephews').to('Dewey') # duplicate
+ bindings.bind.in_multibind(multibind_id).name('nephews').to('Louie')
+ bindings.bind.in_multibind(multibind_id).name('nephews').to('Louie') # duplicate
+ bindings.bind.in_multibind(multibind_id).name('nephews').to('Louie') # duplicate
ducks = injector(lbinder).lookup(scope, 'donalds_family')
ducks.should == ['Huey', 'Dewey', 'Louie']
end
it "should be able to contribute elements and arrays of elements and flatten 1 level" do
# This case uses a multibind of individual strings and arrays, and combines them
# into an array of flattened
#
array_of_string = type_factory.array_of(type_factory.string())
multibind_id = "ducks"
mb = bindings.multibind(multibind_id).type(array_of_string).name('donalds_family')
# flatten one level
mb.producer_options(:flatten => 1)
- bindings.bind_in_multibind(multibind_id).to('Huey')
- bindings.bind_in_multibind(multibind_id).to('Dewey')
- bindings.bind_in_multibind(multibind_id).to('Louie') # duplicate
- bindings.bind_in_multibind(multibind_id).to(['Huey', 'Dewey', 'Louie'])
+ bindings.bind.in_multibind(multibind_id).to('Huey')
+ bindings.bind.in_multibind(multibind_id).to('Dewey')
+ bindings.bind.in_multibind(multibind_id).to('Louie') # duplicate
+ bindings.bind.in_multibind(multibind_id).to(['Huey', 'Dewey', 'Louie'])
ducks = injector(lbinder).lookup(scope, 'donalds_family')
ducks.should == ['Huey', 'Dewey', 'Louie', 'Huey', 'Dewey', 'Louie']
end
it "should produce detailed type error message" do
array_of_integer = type_factory.array_of(type_factory.integer())
multibind_id = "ints"
mb = bindings.multibind(multibind_id).type(array_of_integer).name('donalds_family')
- bindings.bind_in_multibind(multibind_id).to('Huey')
+ bindings.bind.in_multibind(multibind_id).to('Huey')
expect { ducks = injector(lbinder).lookup(scope, 'donalds_family')
}.to raise_error(%r{expected: Integer, or Array\[Integer\], got: String})
end
end
+ context "When using multibind in multibind" do
+ it "a hash multibind can be contributed to another" do
+ hash_of_data = type_factory.hash_of_data()
+ mb1_id = 'data1'
+ mb2_id = 'data2'
+ top = bindings.multibind(mb1_id).type(hash_of_data).name("top")
+ detail = bindings.multibind(mb2_id).type(hash_of_data).name("detail").in_multibind(mb1_id)
+
+ bindings.bind.in_multibind(mb1_id).name('a').to(10)
+ bindings.bind.in_multibind(mb1_id).name('b').to(20)
+ bindings.bind.in_multibind(mb2_id).name('a').to(30)
+ bindings.bind.in_multibind(mb2_id).name('b').to(40)
+ expect( injector(lbinder).lookup(scope, "top") ).to eql({'detail' => {'a' => 30, 'b' => 40}, 'a' => 10, 'b' => 20})
+ end
+ end
+
context "When looking up entries requiring evaluation" do
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() }
it "should be possible to lookup a concatenated string" do
scope['duck'] = 'Donald Fauntleroy Duck'
expr = parser.parse_string('"Hello $duck"').current()
bindings.bind.name('the_duck').to(expr)
injector(lbinder).lookup(scope, 'the_duck').should == 'Hello Donald Fauntleroy Duck'
end
it "should be possible to post process lookup with a puppet lambda" do
model = parser.parse_string('fake() |$value| {$value + 1 }').current
bindings.bind.name('an_int').to(42).producer_options( :transformer => model.lambda)
injector(lbinder).lookup(scope, 'an_int').should == 43
end
end
end
end
\ No newline at end of file
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Fri, Nov 1, 9:59 AM (1 d, 21 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
10076268
Default Alt Text
(93 KB)
Attached To
Mode
rPU puppet
Attached
Detach File
Event Timeline
Log In to Comment