Charyf/charyf-core

View on GitHub
lib/charyf/utils/generator/base.rb

Summary

Maintainability
B
6 hrs
Test Coverage
# frozen_string_literal: true

begin
  require 'thor/group'
rescue LoadError
  puts "Thor is not available.\nIf you ran this command from a git checkout " \
       "of Charyf, please make sure thor is installed,\nand run this command " \
       "as `ruby #{$0} #{(ARGV | ['--dev']).join(" ")}`"
  exit
end

require_relative 'actions'

module Charyf
  module Generators
    class Error < Thor::Error # :nodoc:
    end

    class Base < Thor::Group
      include Thor::Actions
      include Charyf::Generators::Actions

      add_runtime_options!
      strict_args_position!

      # Cache source root and add lib/generators/base/generator/templates to
      # source paths.
      def self.inherited(base) #:nodoc:
        super

        # Invoke source_root so the default_source_root is set.
        base.source_root

        if base.name && base.name !~ /Base$/
          Charyf::Generators.subclasses << base
        end

      end

      # Tries to get the description from a USAGE file one folder above the command
      # root.
      def self.desc(usage = nil, description = nil, options = {})
        if usage
          super
        else
          @desc ||= ERB.new(File.read(desc_file)).result(binding) if desc_file
        end
      end

      def self.desc_file(desc_file = nil)
        @desc_file = desc_file if desc_file

        @desc_file if @desc_file && File.exist?(@desc_file)
      end

      def self.hook_for(*names, &block)
        options = names.last.is_a?(Hash) ? names.pop : {}
        in_base = options.delete(:in) || base_name
        as_hook = options.delete(:as) || generator_name

        names.each do |name|
          unless class_options.key?(name)
            defaults = if options[:type] == :boolean
                         {}
                       elsif [true, false].include?(default_value_for_option(name, options))
                         { banner: "", type: :boolean }
                       elsif default_value_for_option(name, options).is_a? Array
                         { desc: "#{name.to_s} to be invoked", banner: "NAMES", type: :array }
                       else
                         { desc: "#{name.to_s} to be invoked", banner: "NAME" }
                       end

            class_option(name, defaults.merge!(options))
          end

          hooks[name] = [ in_base, as_hook ]
          invoke_from_option(name, options, &block)
        end
      end

      # # Remove a previously added hook.
      # #
      # #   remove_hook_for :orm
      # def self.remove_hook_for(*names)
      #   remove_invocation(*names)
      #
      #   names.each do |name|
      #     hooks.delete(name)
      #   end
      # end
      #
      # Make class option aware of Charyf::Generators.options
      def self.class_option(name, options = {}) #:nodoc:
        options[:desc]    = "Indicates when to generate #{name.to_s.downcase}" unless options.key?(:desc)
        options[:default] = default_value_for_option(name, options)
        super(name, options)
      end

      # Returns the source root for this generator using default_source_root as default.
      def self.source_root(path = nil)
        @_source_root = path if path
        @_source_root ||= default_source_root || super
      end

      # Convenience method to get the namespace from the class name. It's the
      # same as Thor default except that the Generator at the end of the class
      # is removed.
      def self.namespace(name = nil)
        return super if name
        @namespace ||= super.sub(/_generator$/, "").sub(/:generators:/, ":")
      end

      # Convenience method to hide this generator from the available ones when
      # running charyf generator command.
      def self.hide!
        Charyf::Generators.hide_namespace(namespace)
      end

      protected

      # Shortcut to invoke with padding and block handling. Use internally by
      # invoke and invoke_from_option class methods.
      def _invoke_for_class_method(klass, command = nil, *args, &block) #:nodoc:
        with_padding do
          if block
            case block.arity
              when 3
                yield(self, klass, command)
              when 2
                yield(self, klass)
              when 1
                instance_exec(klass, &block)
            end
          else
            invoke klass, command, *args
          end
        end
      end

      # Prepare class invocation to search on Charyf namespace if a previous
      # added hook is being used.
      def self.prepare_for_invocation(name, value) #:nodoc:
        return super unless value.is_a?(String) || value.is_a?(Symbol) #|| value.is_a?(Array)

        if value && constants = hooks[name]
          value = name if TrueClass === value
          Charyf::Generators.find_by_namespace(value, *constants)
        elsif klass = Charyf::Generators.find_by_namespace(value)
          klass
        else
          super
        end
      end

      private

      # Check whether the given class names are already taken by user
      # application or Charyf.
      def class_collisions(*class_names)
        return unless behavior == :invoke

        class_names.flatten.each do |class_name|
          class_name = class_name.to_s
          next if class_name.strip.empty?

          # Split the class from its module nesting
          nesting = class_name.split("::")
          last_name = nesting.pop
          last = extract_last_module(nesting)

          if last && last.const_defined?(last_name, false)
            raise Error, "The name '#{class_name}' is either already used in your application " \
                           "or reserved by Charyf. Please choose an alternative and run "  \
                           "this generator again."
          end
        end
      end

      # Takes in an array of nested modules and extracts the last module
      def extract_last_module(nesting) # :doc:
        nesting.inject(Object) do |last_module, nest|
          break unless last_module.const_defined?(nest, false)
          last_module.const_get(nest)
        end
      end

      # Wrap block with namespace of current application
      # if namespace exists and is not skipped
      def module_namespacing(&block) # :doc:
        content = capture(&block)
        content = wrap_with_namespace(content) if namespaced?
        concat(content)
      end

      # Keep hooks configuration that are used on prepare_for_invocation.
      def self.hooks #:nodoc:
        @hooks ||= from_superclass(:hooks, {})
      end


      # Returns the default value for the option name given doing a lookup in
      # Charyf::Generators.options.
      def self.default_value_for_option(name, options) # :doc:
        default_for_option(Charyf::Generators.options, name, options, options[:default])
      end

      # Returns default for the option name given doing a lookup in config.
      def self.default_for_option(config, name, options, default) # :doc:
        if generator_name && (c = config[generator_name.to_sym]) && c.key?(name)
          c[name]
        elsif base_name && (c = config[base_name.to_sym]) && c.key?(name)
          c[name]
        elsif config[:charyf].key?(name)
          config[:charyf][name]
        else
          default
        end
      end

      # Returns the default source root for a given generator. This is used internally
      # by Charyf to set its generators source root. If you want to customize your source
      # root, you should use source_root.
      def self.default_source_root
        return unless base_name && generator_name
        return unless default_generator_root
        path = File.join(default_generator_root, "templates")
        path if File.exist?(path)
      end

      # Returns the base root for a common set of generators. This is used to dynamically
      # guess the default source root.
      def self.base_root
        __dir__
      end

      # Use Charyf default banner.
      def self.banner # :doc:
        "charyf generate #{namespace.sub(/^charyf:/, '')} #{arguments.map(&:usage).join(' ')} [options]".gsub(/\s+/, " ")
      end

      # Sets the base_name taking into account the current class namespace.
      def self.base_name # :doc:
        @base_name ||= begin
          if base = name.to_s.split("::").first
            base.underscore
          end
        end
      end

      # Removes the namespaces and get the generator name. For example,
      # Charyf::Generators::SkillGenerator will return "skill" as generator name.
      def self.generator_name # :doc:
        @generator_name ||= begin
          if generator = name.to_s.split("::").last
            generator.sub!(/Generator$/, "")
            generator.underscore
          end
        end
      end

      # # Small macro to add ruby as an option to the generator with proper
      # # default value plus an instance helper method called shebang.
      def self.add_shebang_option! # :doc:
        class_option :ruby, type: :string, aliases: "-r", default: Thor::Util.ruby_command,
                     desc: "Path to the Ruby binary of your choice", banner: "PATH"

        no_tasks {
          define_method :shebang do
            @shebang ||= begin
              command = if options[:ruby] == Thor::Util.ruby_command
                          "/usr/bin/env #{File.basename(Thor::Util.ruby_command)}"
                        else
                          options[:ruby]
                        end
              "#!#{command}"
            end
          end
        }
      end

      def self.default_generator_root # :doc:
        path = File.expand_path(File.join('..', 'generators', generator_name), base_root)
        path if File.exist?(path)
      end

    end
  end
end