diff --git a/lib/puppet/pops/loader/loader.rb b/lib/puppet/pops/loader/loader.rb index 68d5cd17c..f162604f5 100644 --- a/lib/puppet/pops/loader/loader.rb +++ b/lib/puppet/pops/loader/loader.rb @@ -1,172 +1,172 @@ # Loader # === # A Loader is responsible for loading "entities" ("instantiable and executable objects in the puppet language" which # are type, hostclass, definition, function, and bindings. # # The main method for users of a Loader is the `load` or `load_typed methods`, which returns a previously loaded entity # of a given type/name, and searches and loads the entity if not already loaded. # # private entities # --- # TODO: handle loading of entities that are private. Suggest that all calls pass an origin_loader (the loader # where request originated (or symbol :public). A module loader has one (or possibly a list) of what is # considered to represent private loader - i.e. the dependency loader for a module. If an entity is private # it should be stored with this status, and an error should be raised if the origin_loader is not on the list # of accepted "private" loaders. # The private loaders can not be given at creation time (they are parented by the loader in question). Another # alternative is to check if the origin_loader is a child loader, but this requires bidirectional links # between loaders or a search if loader with private entity is a parent of the origin_loader). # # @api public # class Puppet::Pops::Loader::Loader # Produces the value associated with the given name if already loaded, or available for loading # by this loader, one of its parents, or other loaders visible to this loader. # This is the method an external party should use to "get" the named element. # # An implementor of this method should first check if the given name is already loaded by self, or a parent # loader, and if so return that result. If not, it should call `find` to perform the loading. # # @param type [:Symbol] the type to load # @param name [String, Symbol] the name of the entity to load # @return [Object, nil] the value or nil if not found # # @api public # def load(type, name) - if result = load_typed(TypedName.new(type, name)) + if result = load_typed(TypedName.new(type, name.to_s)) result.value end end # Loads the given typed name, and returns a NamedEntry if found, else returns nil. # This the same a `load`, but returns a NamedEntry with origin/value information. # # @param typed_name [TypedName] - the type, name combination to lookup # @return [NamedEntry, nil] the entry containing the loaded value, or nil if not found # # @api public # def load_typed(typed_name) raise NotImplementedError.new end # Produces the value associated with the given name if defined **in this loader**, or nil if not defined. # This lookup does not trigger any loading, or search of the given name. # An implementor of this method may not search or look up in any other loader, and it may not # define the name. # # @param typed_name [TypedName] - the type, name combination to lookup # # @api private # def [] (typed_name) if found = get_entry(typed_name) found.value else nil end end # Searches for the given name in this loader's context (parents should already have searched their context(s) without # producing a result when this method is called). # An implementation of find typically caches the result. # # @param typed_name [TypedName] the type, name combination to lookup # @return [NamedEntry, nil] the entry for the loaded entry, or nil if not found # # @api private # def find(typed_name) raise NotImplementedError.new end # Returns the parent of the loader, or nil, if this is the top most loader. This implementation returns nil. def parent nil end # Binds a value to a name. The name should not start with '::', but may contain multiple segments. # # @param type [:Symbol] the type of the entity being set # @param name [String, Symbol] the name of the entity being set # @param origin [URI, #uri, String] the origin of the set entity, a URI, or provider of URI, or URI in string form # @return [NamedEntry, nil] the created entry # # @api private # def set_entry(type, name, value, origin = nil) raise NotImplementedError.new end # Produces a NamedEntry if a value is bound to the given name, or nil if nothing is bound. # # @param typed_name [TypedName] the type, name combination to lookup # @return [NamedEntry, nil] the value bound in an entry # # @api private # def get_entry(typed_name) raise NotImplementedError.new end # An entry for one entity loaded by the loader. # class NamedEntry attr_reader :typed_name attr_reader :value attr_reader :origin def initialize(typed_name, value, origin) @name = typed_name @value = value @origin = origin freeze() end end # A name/type combination that can be used as a compound hash key # class TypedName attr_reader :type attr_reader :name attr_reader :name_parts # True if name is qualified (more than a single segment) attr_reader :qualified def initialize(type, name) @type = type # relativize the name (get rid of leading ::), and make the split string available - @name_parts = name.split(/::/) + @name_parts = name.to_s.split(/::/) @name_parts.shift if name_parts[0].empty? @name = name_parts.join('::') @qualified = name_parts.size > 1 # precompute hash - the name is frozen, so this is safe to do @hash = [self.class, type, @name].hash # Not allowed to have numeric names - 0, 010, 0x10, 1.2 etc if Puppet::Pops::Utils.is_numeric?(@name) raise ArgumentError, "Illegal attempt to use a numeric name '#{name}' at #{origin_label(origin)}." end freeze() end def hash @hash end def ==(o) o.class == self.class && type == o.type && name == o.name end alias eql? == def to_s "#{type}/#{name}" end end end diff --git a/lib/puppet/pops/loader/static_loader.rb b/lib/puppet/pops/loader/static_loader.rb index 88ba86d85..27cfbe462 100644 --- a/lib/puppet/pops/loader/static_loader.rb +++ b/lib/puppet/pops/loader/static_loader.rb @@ -1,32 +1,69 @@ # Static Loader contains constants, basic data types and other types required for the system # to boot. # class Puppet::Pops::Loader::StaticLoader < Puppet::Pops::Loader::Loader + attr_reader :loaded + def initialize + @loaded = {} + create_logging_functions() + end + def load_typed(typed_name) load_constant(typed_name) end def get_entry(typed_name) load_constant(typed_name) end def find(name) # There is nothing to search for, everything this loader knows about is already available nil end def parent nil # at top of the hierarchy end def to_s() "(StaticLoader)" end private def load_constant(typed_name) - # Move along, nothing to see here a.t.m... - nil + @loaded[typed_name] + end + + private + + # Creates a function for each of the specified log levels + # + def create_logging_functions() + Puppet::Util::Log.levels.each do |level| + + fc = Puppet::Functions.create_function(level) do + # create empty dispatcher to stop it from complaining about missing method since + # an override of :call is made instead of using dispatch. + dispatch(:log) { } + + # Logs per the specified level, outputs formatted information for arrays, hashes etc. + # Overrides the implementation in Function that uses dispatching. This is not needed here + # since it accepts 0-n Optional[Object] + # + define_method(:call) do |scope, *vals| + # NOTE: 3x, does this: vals.join(" ") + # New implementation uses the evaluator to get proper formatting per type + # TODO: uses a fake scope (nil) - fix when :scopes are available via settings + mapped = vals.map {|v| Puppet::Pops::Evaluator::EvaluatorImpl.new.string(v, nil) } + Puppet.send(level, mapped.join(" ")) + end + end + + typed_name = Puppet::Pops::Loader::Loader::TypedName.new(:function, level) + # TODO:closure scope is fake (an empty hash) - waiting for new global scope to be available via lookup of :scopes + func = fc.new({},self) + @loaded[ typed_name ] = Puppet::Pops::Loader::Loader::NamedEntry.new(typed_name, func, __FILE__) + end end end diff --git a/spec/unit/pops/loaders/static_loader_spec.rb b/spec/unit/pops/loaders/static_loader_spec.rb index 6d8f516a6..e1a73273e 100644 --- a/spec/unit/pops/loaders/static_loader_spec.rb +++ b/spec/unit/pops/loaders/static_loader_spec.rb @@ -1,26 +1,46 @@ require 'spec_helper' require 'puppet/pops' require 'puppet/loaders' -describe 'static loader' do +describe 'the static loader' do it 'has no parent' do expect(Puppet::Pops::Loader::StaticLoader.new.parent).to be(nil) end it 'identifies itself in string form' do expect(Puppet::Pops::Loader::StaticLoader.new.to_s).to be_eql('(StaticLoader)') end it 'support the Loader API' do # it may produce things later, this is just to test that calls work as they should - now all lookups are nil. loader = Puppet::Pops::Loader::StaticLoader.new() a_typed_name = typed_name(:function, 'foo') expect(loader[a_typed_name]).to be(nil) expect(loader.load_typed(a_typed_name)).to be(nil) expect(loader.find(a_typed_name)).to be(nil) end + context 'provides access to logging functions' do + let(:loader) { loader = Puppet::Pops::Loader::StaticLoader.new() } + # Ensure all logging functions produce output + before(:each) { Puppet::Util::Log.level = :debug } + + Puppet::Util::Log.levels.each do |level| + it "defines the function #{level.to_s}" do + expect(loader.load(:function, level).class.name).to eql(level.to_s) + end + + it 'and #{level.to_s} can be called' do + expect(loader.load(:function, level).call({}, 'yay').to_s).to eql('yay') + end + + it "uses the evaluator to format output" do + expect(loader.load(:function, level).call({}, ['yay', 'surprise']).to_s).to eql('[yay, surprise]') + end + end + end + def typed_name(type, name) Puppet::Pops::Loader::Loader::TypedName.new(type, name) end end \ No newline at end of file