diff --git a/lib/puppet/pops/binder/bindings_loader.rb b/lib/puppet/pops/binder/bindings_loader.rb index 171e454ea..353f82e0d 100644 --- a/lib/puppet/pops/binder/bindings_loader.rb +++ b/lib/puppet/pops/binder/bindings_loader.rb @@ -1,83 +1,83 @@ require 'rgen/metamodel_builder' # The ClassLoader provides a Class instance given a class name or a meta-type. # If the class is not already loaded, it is loaded using the Puppet Autoloader. # This means it can load a class from a gem, or from puppet modules. # class Puppet::Pops::Binder::BindingsLoader @confdir = Puppet.settings[:confdir] # Returns a XXXXX given a fully qualified class name. # Lookup of class is never relative to the calling namespace. # @param name [String, Array, Array, Puppet::Pops::Types::PAnyType] A fully qualified # class name String (e.g. '::Foo::Bar', 'Foo::Bar'), a PAnyType, or a fully qualified name in Array form where each part # is either a String or a Symbol, e.g. `%w{Puppetx Puppetlabs SomeExtension}`. # @return [Class, nil] the looked up class or nil if no such class is loaded # @raise ArgumentError If the given argument has the wrong type # @api public # def self.provide(scope, name) case name when String provide_from_string(scope, name) when Array provide_from_name_path(scope, name.join('::'), name) else raise ArgumentError, "Cannot provide a bindings from a '#{name.class.name}'" end end # If loadable name exists relative to a a basedir or not. Returns the loadable path as a side effect. # @return [String, nil] a loadable path for the given name, or nil # def self.loadable?(basedir, name) # note, "lib" is added by the autoloader # paths_for_name(name).find {|p| Puppet::FileSystem.exist?(File.join(basedir, "lib/puppet/bindings", p)+'.rb') } end private def self.loader() @autoloader ||= Puppet::Util::Autoload.new("BindingsLoader", "puppet/bindings") end def self.provide_from_string(scope, name) name_path = name.split('::') # always from the root, so remove an empty first segment if name_path[0].empty? name_path = name_path[1..-1] end provide_from_name_path(scope, name, name_path) end def self.provide_from_name_path(scope, name, name_path) # If bindings is already loaded, try this first result = Puppet::Bindings.resolve(scope, name) unless result # Attempt to load it using the auto loader - paths_for_name(name).find {|path| loader.load(path) } + paths_for_name(name).find {|path| loader.load(path, Puppet.lookup(:current_environment)) } result = Puppet::Bindings.resolve(scope, name) end result end def self.paths_for_name(fq_name) [de_camel(fq_name), downcased_path(fq_name)].uniq end def self.downcased_path(fq_name) fq_name.to_s.gsub(/::/, '/').downcase end def self.de_camel(fq_name) fq_name.to_s.gsub(/::/, '/'). gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2'). gsub(/([a-z\d])([A-Z])/,'\1_\2'). tr("-", "_"). downcase end end diff --git a/lib/puppet/pops/types/class_loader.rb b/lib/puppet/pops/types/class_loader.rb index bd22db280..b291d49b6 100644 --- a/lib/puppet/pops/types/class_loader.rb +++ b/lib/puppet/pops/types/class_loader.rb @@ -1,133 +1,133 @@ require 'rgen/metamodel_builder' # The ClassLoader provides a Class instance given a class name or a meta-type. # If the class is not already loaded, it is loaded using the Puppet Autoloader. # This means it can load a class from a gem, or from puppet modules. # class Puppet::Pops::Types::ClassLoader @autoloader = Puppet::Util::Autoload.new("ClassLoader", "") # Returns a Class given a fully qualified class name. # Lookup of class is never relative to the calling namespace. # @param name [String, Array, Array, Puppet::Pops::Types::PAnyType] A fully qualified # class name String (e.g. '::Foo::Bar', 'Foo::Bar'), a PAnyType, or a fully qualified name in Array form where each part # is either a String or a Symbol, e.g. `%w{Puppetx Puppetlabs SomeExtension}`. # @return [Class, nil] the looked up class or nil if no such class is loaded # @raise ArgumentError If the given argument has the wrong type # @api public # def self.provide(name) case name when String provide_from_string(name) when Array provide_from_name_path(name.join('::'), name) when Puppet::Pops::Types::PAnyType, Puppet::Pops::Types::PType provide_from_type(name) else raise ArgumentError, "Cannot provide a class from a '#{name.class.name}'" end end private def self.provide_from_type(type) case type when Puppet::Pops::Types::PRuntimeType raise ArgumentError.new("Only Runtime type 'ruby' is supported, got #{type.runtime}") unless type.runtime == :ruby provide_from_string(type.runtime_type_name) when Puppet::Pops::Types::PBooleanType # There is no other thing to load except this Enum meta type RGen::MetamodelBuilder::MMBase::Boolean when Puppet::Pops::Types::PType # TODO: PType should has a type argument (a PAnyType) so the Class' class could be returned # (but this only matters in special circumstances when meta programming has been used). Class when Puppet::Pops::Type::POptionalType # cannot make a distinction between optional and its type provide_from_type(type.optional_type) # Although not expected to be the first choice for getting a concrete class for these # types, these are of value if the calling logic just has a reference to type. # when Puppet::Pops::Types::PArrayType ; Array when Puppet::Pops::Types::PTupleType ; Array when Puppet::Pops::Types::PHashType ; Hash when Puppet::Pops::Types::PStructType ; Hash when Puppet::Pops::Types::PRegexpType ; Regexp when Puppet::Pops::Types::PIntegerType ; Integer when Puppet::Pops::Types::PStringType ; String when Puppet::Pops::Types::PPatternType ; String when Puppet::Pops::Types::PEnumType ; String when Puppet::Pops::Types::PFloatType ; Float when Puppet::Pops::Types::PNilType ; NilClass when Puppet::Pops::Types::PCallableType ; Proc else nil end end def self.provide_from_string(name) name_path = name.split('::') # always from the root, so remove an empty first segment if name_path[0].empty? name_path = name_path[1..-1] end provide_from_name_path(name, name_path) end def self.provide_from_name_path(name, name_path) # If class is already loaded, try this first result = find_class(name_path) unless result.is_a?(Class) # Attempt to load it using the auto loader loaded_path = nil - if paths_for_name(name_path).find {|path| loaded_path = path; @autoloader.load(path) } + if paths_for_name(name_path).find {|path| loaded_path = path; @autoloader.load(path, Puppet.lookup(:current_environment)) } result = find_class(name_path) unless result.is_a?(Class) raise RuntimeError, "Loading of #{name} using relative path: '#{loaded_path}' did not create expected class" end end end return nil unless result.is_a?(Class) result end def self.find_class(name_path) name_path.reduce(Object) do |ns, name| begin ns.const_get(name) rescue NameError return nil end end end def self.paths_for_name(fq_named_parts) # search two entries, one where all parts are decamelized, and one with names just downcased # TODO:this is not perfect - it will not produce the correct mix if a mix of styles are used # The alternative is to test many additional paths. # [fq_named_parts.map {|part| de_camel(part)}.join('/'), fq_named_parts.join('/').downcase ] end # def self.downcased_path(fq_name) # fq_name.to_s.gsub(/::/, '/').downcase # end def self.de_camel(fq_name) fq_name.to_s.gsub(/::/, '/'). gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2'). gsub(/([a-z\d])([A-Z])/,'\1_\2'). tr("-", "_"). downcase end end diff --git a/spec/fixtures/unit/pops/binder/bindings_composer/ok/binder_config.yaml b/spec/fixtures/unit/pops/binder/bindings_composer/ok/binder_config.yaml index f7a6f8c4d..f1b9d1636 100644 --- a/spec/fixtures/unit/pops/binder/bindings_composer/ok/binder_config.yaml +++ b/spec/fixtures/unit/pops/binder/bindings_composer/ok/binder_config.yaml @@ -1,10 +1,10 @@ --- version: 1 layers: [{name: site, include: 'confdir:/confdirtest'}, {name: test, include: 'echo:/quick/brown/fox'}, {name: modules, include: ['module:/*::default'], exclude: 'module:/bad::default/' } ] extensions: scheme_handlers: - echo: 'Puppetx::Awesome2::EchoSchemeHandler' + echo: 'PuppetX::Awesome2::EchoSchemeHandler' diff --git a/spec/fixtures/unit/pops/binder/bindings_composer/ok/modules/awesome2/lib/puppetx/awesome2/echo_scheme_handler.rb b/spec/fixtures/unit/pops/binder/bindings_composer/ok/modules/awesome2/lib/puppet_x/awesome2/echo_scheme_handler.rb similarity index 97% rename from spec/fixtures/unit/pops/binder/bindings_composer/ok/modules/awesome2/lib/puppetx/awesome2/echo_scheme_handler.rb rename to spec/fixtures/unit/pops/binder/bindings_composer/ok/modules/awesome2/lib/puppet_x/awesome2/echo_scheme_handler.rb index 33159fcaa..4edcd437b 100644 --- a/spec/fixtures/unit/pops/binder/bindings_composer/ok/modules/awesome2/lib/puppetx/awesome2/echo_scheme_handler.rb +++ b/spec/fixtures/unit/pops/binder/bindings_composer/ok/modules/awesome2/lib/puppet_x/awesome2/echo_scheme_handler.rb @@ -1,18 +1,18 @@ require 'puppet/plugins/binding_schemes' -module Puppetx +module PuppetX module Awesome2 # A binding scheme that echos its path # 'echo:/quick/brown/fox' becomes key '::quick::brown::fox' => 'echo: quick brown fox'. # (silly class for testing loading of extension) # class EchoSchemeHandler < Puppet::Plugins::BindingSchemes::BindingsSchemeHandler def contributed_bindings(uri, scope, composer) factory = ::Puppet::Pops::Binder::BindingsFactory bindings = factory.named_bindings("echo") bindings.bind.name(uri.path.gsub(/\//, '::')).to("echo: #{uri.path.gsub(/\//, ' ').strip!}") result = factory.contributed_bindings("echo", bindings.model) ### , nil) end end end end \ No newline at end of file diff --git a/spec/unit/pops/binder/bindings_composer_spec.rb b/spec/unit/pops/binder/bindings_composer_spec.rb index 8f39b8eeb..123e33c7e 100644 --- a/spec/unit/pops/binder/bindings_composer_spec.rb +++ b/spec/unit/pops/binder/bindings_composer_spec.rb @@ -1,63 +1,64 @@ require 'spec_helper' require 'puppet/pops' require 'puppet_spec/pops' require 'puppet/plugins' describe 'BinderComposer' do include PuppetSpec::Pops def config_dir(config_name) my_fixture(config_name) end let(:acceptor) { Puppet::Pops::Validation::Acceptor.new() } let(:diag) { Puppet::Pops::Binder::Config::DiagnosticProducer.new(acceptor) } let(:issues) { Puppet::Pops::Binder::Config::Issues } let(:node) { Puppet::Node.new('localhost') } let(:compiler) { Puppet::Parser::Compiler.new(node)} let(:scope) { Puppet::Parser::Scope.new(compiler) } let(:parser) { Puppet::Pops::Parser::Parser.new() } let(:factory) { Puppet::Pops::Binder::BindingsFactory } it 'should load default config if no config file exists' do diagnostics = diag composer = Puppet::Pops::Binder::BindingsComposer.new() composer.compose(scope) end context "when loading a complete configuration with modules" do let(:config_directory) { config_dir('ok') } it 'should load everything without errors' do Puppet.settings[:confdir] = config_directory Puppet.settings[:libdir] = File.join(config_directory, 'lib') - Puppet.override(:environments => Puppet::Environments::Static.new(Puppet::Node::Environment.create(:production, [File.join(config_directory, 'modules')]))) do + environments = Puppet::Environments::Static.new(Puppet::Node::Environment.create(:production, [File.join(config_directory, 'modules')])) + Puppet.override(:environments => environments, :current_environment => environments.get('production')) do # this ensure the binder is active at the right time # (issues with getting a /dev/null path for "confdir" / "libdir") raise "Binder not active" unless scope.compiler.activate_binder diagnostics = diag composer = Puppet::Pops::Binder::BindingsComposer.new() the_scope = scope the_scope['fqdn'] = 'localhost' the_scope['environment'] = 'production' layered_bindings = composer.compose(scope) # puts Puppet::Pops::Binder::BindingsModelDumper.new().dump(layered_bindings) binder = Puppet::Pops::Binder::Binder.new(layered_bindings) injector = Puppet::Pops::Binder::Injector.new(binder) expect(injector.lookup(scope, 'awesome_x')).to be == 'golden' expect(injector.lookup(scope, 'good_x')).to be == 'golden' expect(injector.lookup(scope, 'rotten_x')).to be == nil expect(injector.lookup(scope, 'the_meaning_of_life')).to be == 42 expect(injector.lookup(scope, 'has_funny_hat')).to be == 'the pope' expect(injector.lookup(scope, 'all your base')).to be == 'are belong to us' expect(injector.lookup(scope, 'env_meaning_of_life')).to be == 'production thinks it is 42' expect(injector.lookup(scope, '::quick::brown::fox')).to be == 'echo: quick brown fox' end end end # TODO: test error conditions (see BinderConfigChecker for what to test) end