lib/chef-cli/command/generator_commands/generator_generator.rb



#
# Copyright:: Copyright (c) 2014-2019 Chef Software Inc.
# License:: Apache License, Version 2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

require "fileutils" unless defined?(FileUtils)

require_relative "../../configurable"
require_relative "../../ui"
require_relative "base"
require_relative "../../dist"

module ChefCLI
  module Command
    module GeneratorCommands

      # chef generate generator [NAME]
      # --
      # There is already a `Generator` class a few levels up that other classes
      # are referring to via relative constant, so name this
      # `GeneratorGenerator` to avoid causing a conflict.
      class GeneratorGenerator < Base

        banner "Usage: #{ChefCLI::Dist::EXEC} generate generator [ PATH ] [options]"

        attr_reader :destination_dir
        attr_accessor :ui

        def initialize(*args)
          super
          @destination_dir = nil
          @ui = UI.new
          @custom_cookbook_name = false
        end

        def run
          return 1 unless verify_params!

          FileUtils.cp_r(source, destination_dir)
          update_metadata_rb
          ui.msg("Copied built-in generator cookbook to #{created_cookbook_path}")
          ui.msg("Add the following to your config file to enable it:")
          ui.msg("  chefcli.generator_cookbook \"#{created_cookbook_path}\"")
          0
        end

        # @api private
        def cookbook_name
          if custom_cookbook_name?
            File.basename(destination_dir)
          else
            "code_generator"
          end
        end

        # @api private
        def verify_params!
          case params.size
          when 0
            @destination_dir = Dir.pwd
            true
          when 1
            set_destination_dir_from_args(params.first)
          else
            ui.err("ERROR: Too many arguments.")
            ui.err(opt_parser)
            false
          end
        end

        # @api private
        def source
          # Hard-coded to the built-in generator, because otherwise setting
          # chefcli.generator_cookbook would make this command copy the custom
          # generator, but that doesn't make sense because the user can easily
          # do that anyway.
          File.expand_path("../../../skeletons/code_generator", __FILE__)
        end

        private

        # @api private
        def update_metadata_rb
          File.open(File.join(created_cookbook_path, "metadata.rb"), "w+") do |f|
            f.print(metadata_rb)
          end
        end

        def created_cookbook_path
          if custom_cookbook_name?
            destination_dir
          else
            File.join(destination_dir, "code_generator")
          end
        end

        # @api private
        def metadata_rb
          <<~METADATA
            name             '#{cookbook_name}'
            description      'Custom code generator cookbook for use with #{ChefCLI::Dist::PRODUCT}'
            version          '0.1.0'

          METADATA
        end

        def custom_cookbook_name?
          @custom_cookbook_name
        end

        def set_destination_dir_from_args(given_path)
          path = File.expand_path(given_path)
          if check_for_conflicting_dir(path) ||
              check_for_conflicting_file(path) ||
              check_for_missing_parent_dir(path)
            false
          else
            @destination_dir = File.expand_path(path)
            @custom_cookbook_name = !File.exist?(destination_dir)
            true
          end
        end

        def conflicting_file_exists?(path)
          File.exist?(path) && File.file?(path)
        end

        def check_for_conflicting_dir(path)
          conflicting_subdir_path = File.join(path, "code_generator")
          if File.exist?(path) &&
              File.directory?(path) &&
              File.exist?(conflicting_subdir_path)
            ui.err("ERROR: file or directory #{conflicting_subdir_path} exists.")
            true
          else
            false
          end
        end

        def check_for_conflicting_file(path)
          if File.exist?(path) && !File.directory?(path)
            ui.err("ERROR: #{path} exists and is not a directory.")
            true
          else
            false
          end
        end

        def check_for_missing_parent_dir(path)
          parent = File.dirname(path)
          if !File.exist?(parent)
            ui.err("ERROR: enclosing directory #{parent} does not exist.")
            true
          else
            false
          end
        end

      end

    end
  end
end