require 'pdk' module PDK module Generate class PuppetObject attr_reader :context, :object_name, :options # Initialises the PDK::Generate::PuppetObject object. # # In general, this object should never be instantiated directly. Instead, # one of the subclasses should be used e.g. PDK::Generate::Klass. # # New subclasses generally only need to inherit this class, set the # OBJECT_TYPE constant and implement the {#template_data}, # {#target_object_path} and {#target_spec_path} methods. # # @param module_dir [String] The path to the root of module that the # will contain the object. # @param object_name [String] The name of the object. # @param options [Hash{Symbol => Object}] def initialize(context, object_name, options) raise ArgumentError, format('Expected PDK::Context::AbstractContext but got \'%{klass}\' for context', klass: context.class) unless context.is_a?(PDK::Context::AbstractContext) @context = context @options = options @object_name = object_name end # Whether the generator should only return test (spec) files # @return [Boolean] def spec_only? @options[:spec_only] end # Subclass and implement {#friendly_name} to provide a nice name to show users in CLI # @abstract # @return String def friendly_name raise NotImplementedError end # Subclass and implement {#template_files} to provide the template files to # render. Implementations of this method should return a Hash!{String => String}. # @abstract # @return Hash{String => String} Hash key is the source template file and the Hash value is # the relative destination path def template_files raise NotImplementedError end # Subclass and implement {#template_data} to provide data to the templates during rendering. # @abstract # @return Hash{Symbol => Object} def template_data raise NotImplementedError end # Raises an error if any pre-conditions are not met # # @return [void] # @abstract def check_preconditions raise ArgumentError, format('Expected a module context but got %{context_name}', context_name: context.display_name) unless context.is_a?(PDK::Context::Module) end # Check the preconditions of this template group, behaving as a predicate rather than raising an exception. # # @return [Boolean] true if the generator is safe to run, otherwise false. def can_run? check_preconditions true rescue StandardError false end # Creates an instance of an update manager # @api private def update_manager_instance require 'pdk/module/update_manager' PDK::Module::UpdateManager.new end # Stages and then executes the changes for the templates to be rendereed. # This is the main entry point for the class. # # @see #stage_changes # @return [PDK::Module::UpdateManager] The update manager which implemented the changes # @api public def run(update_manager = update_manager_instance) stage_changes(update_manager).sync_changes! update_manager end # Check that the templates can be rendered. Find an appropriate template # and stages the target files from the template. This is the main entry # point for the class. # # @raise [PDK::CLI::ExitWithError] if the target files already exist. # @raise [PDK::CLI::FatalError] (see #render_file) # @return [PDK::Module::UpdateManager] The update manager with the staged changes # @api public def stage_changes(update_manager) check_preconditions with_templates do |template_dir| template_files.each do |source_file, relative_dest_path| new_content = template_dir.render_single_item(source_file, template_data) next if new_content.nil? stage_change(relative_dest_path, new_content, update_manager) end end non_template_files.each { |relative_dest_path, content| stage_change(relative_dest_path, content, update_manager) } update_manager end # Stages a single file into the Update Manager. # @return [void] # @api private def stage_change(relative_dest_path, content, update_manager) absolute_file_path = File.join(context.root_path, relative_dest_path) if PDK::Util::Filesystem.exist?(absolute_file_path) raise PDK::CLI::ExitWithError, format("Unable to generate %{object_type}; '%{file}' already exists.", file: absolute_file_path, object_type: spec_only? ? 'unit test' : friendly_name) end update_manager.add_file(absolute_file_path, content) end # A subclass may wish to stage files into the Update Manager, but the content is not templated. Subclasses # can override this method to stage arbitrary files # # @api private # @return [Hash{String => String}] A Hash with the relative file path as the key and the new file content as the value. # @abstract def non_template_files {} end # Search the possible template directories in order of preference to find # a template that can be used to render a new object of the specified # type. # # @yieldparam template_paths [Hash{Symbol => String}] :object contains # the path on disk to the template file for the object, :spec contains # the path on disk to the template file for the tests for the object # (if it exists). # @yieldparam config_hash [Hash{Object => Object}] the contents of the # :global key in the config_defaults.yml file. # # @raise [PDK::CLI::FatalError] if no suitable template could be found. # # @api private def with_templates require 'pdk/logger' require 'pdk/util/template_uri' templates.each do |template| if template[:uri].nil? PDK.logger.debug(format('No %{dir_type} template found; trying next template directory.', dir_type: template[:type])) next end PDK::Template.with(PDK::Util::TemplateURI.new(template[:uri]), context) do |template_dir| if template_files.any? { |source_file, _| template_dir.has_single_item?(source_file) } yield template_dir # TODO: refactor to a search-and-execute form instead return # work is done # rubocop:disable Lint/NonLocalExitFromIterator elsif template[:allow_fallback] PDK.logger.debug(format('Unable to find a %{type} template in %{url}; trying next template directory.', type: friendly_name, url: template[:uri])) else raise PDK::CLI::FatalError, format('Unable to find the %{type} template in %{url}.', type: friendly_name, url: template[:uri]) end end end rescue ArgumentError => e raise PDK::CLI::ExitWithError, e end # Provides the possible template directory locations in the order in # which they should be searched for a valid template. # # If a template-url has been specified on in the options hash (e.g. from # a CLI parameter), then this template directory will be checked first # and we do not fall back to the next possible template directory. # # If we have not been provided a specific template directory to use, we # try the template specified in the module metadata (as set during # PDK::Generate::Module) and fall back to the default template if # necessary. # # @return [Array Object}>] an array of hashes. Each hash # contains 3 keys: :type contains a String that describes the template # directory, :url contains a String with the URL to the template # directory, and :allow_fallback contains a Boolean that specifies if # the lookup process should proceed to the next template directory if # the template file is not in this template directory. # # @api private def templates require 'pdk/util/template_uri' @templates ||= PDK::Util::TemplateURI.templates(@options) end # Retrieves the name of the module (without the forge username) from the # module metadata. # # @return [String] The name of the module. # # @api private def module_name return nil unless context.is_a?(PDK::Context::Module) require 'pdk/util' @module_name ||= PDK::Util.module_metadata(context.root_path)['name'].rpartition('-').last rescue ArgumentError => e raise PDK::CLI::FatalError, e end private # Transform an object name into a ruby class name def class_name_from_object_name(object_name) object_name.to_s.split('_').map(&:capitalize).join end end end end