Invoking additional generator in Rails default scaffold

Viewed 301

In our project, we are currently using Policy classes to allow authorization for users. This is present for every model, so I want to add the policy_generator and policy_spec_generator to the existing rails g scaffold command so it will create the relevant models, controllers, views and my new policy files (both the Policy and spec) all in a single command.

How do I go about doing this? My initial thought would be to look into Railties and edit the files using the lib folder, but I can't seem to figure out what or where to add the code. Thanks!

UPDATE

So I've been trial and testing for a good part of the day and came up with a solution. What I did was to copy the code for rails' scaffold_generator.rb into my lib BUT it must be with the correct namespacing (in my case, lib/rails/generators/rails/scaffold_controller/scaffold_controller_generator.rb). I'm sure it'd be the same path in any rails project but give it a few go to make sure.

Also, I notice that the patterns for modifying generator files have been like so lib/rails/generators/rails/<generator_folder>/<generator_file>_generator.rb and it's template in this path lib/templates/rails/<generator_folder>/<generator_file>.rb. It's pretty confusing at first since it doesn't match the lib path in Railties or other gems.

As for the actual method itself, here's a copy of the scaffold_controller_generator.rb. I've added a comment where I added my create_policy_file method. The rest are unchanged.

module Rails
  module Generators
    class ScaffoldControllerGenerator < NamedBase # :nodoc:
      include ResourceHelpers

      check_class_collision suffix: "Controller"

      class_option :helper, type: :boolean
      class_option :orm, banner: "NAME", type: :string, required: true,
                         desc: "ORM to generate the controller for"
      class_option :api, type: :boolean,
                         desc: "Generates API controller"

      class_option :skip_routes, type: :boolean, desc: "Don't add routes to config/routes.rb."

      argument :attributes, type: :array, default: [], banner: "field:type field:type"

      def create_controller_files
        template_file = options.api? ? "api_controller.rb" : "controller.rb"
        template template_file, File.join("app/controllers", controller_class_path, "#{controller_file_name}_controller.rb")
      end
      
      # My new method to generate policy files
      def create_policy_files
        template "policy.rb", File.join("app/policies", controller_class_path, "#{singular_name}_policy.rb")
      end

      hook_for :template_engine, as: :scaffold do |template_engine|
        invoke template_engine unless options.api?
      end

      hook_for :resource_route, required: true do |route|
        invoke route unless options.skip_routes?
      end

      hook_for :test_framework, as: :scaffold

      # Invoke the helper using the controller name (pluralized)
      hook_for :helper, as: :scaffold do |invoked|
        invoke invoked, [ controller_name ]
      end

      private
        def permitted_params
          attachments, others = attributes_names.partition { |name| attachments?(name) }
          params = others.map { |name| ":#{name}" }
          params += attachments.map { |name| "#{name}: []" }
          params.join(", ")
        end

        def attachments?(name)
          attribute = attributes.find { |attr| attr.name == name }
          attribute&.attachments?
        end
    end
  end
end

For generatig the spec files, you can just add it inside the file above in the same method even but it's not that pretty. What I did then is to add it inside rspec's scaffold_generator.rb.

Here's the scaffold_generator.rb. Namespacing it is important! Mine's at lib/rails/generatots/rspec/scaffold/scaffold_generator.rb. To find out where I get this path, look into RSpec's lib folder in its repo and follow the same pattern to add it to your project.

require 'generators/rspec'
require 'rails/generators/resource_helpers'

module Rspec
  module Generators
    # @private
    class ScaffoldGenerator < Base
      include ::Rails::Generators::ResourceHelpers
      source_paths << File.expand_path('../helper/templates', __dir__)
      argument :attributes, type: :array, default: [], banner: "field:type field:type"

      class_option :orm, desc: "ORM used to generate the controller"
      class_option :template_engine, desc: "Template engine to generate view files"
      class_option :singleton, type: :boolean, desc: "Supply to create a singleton controller"
      class_option :api, type: :boolean, desc: "Skip specs unnecessary for API-only apps"

      class_option :controller_specs, type: :boolean, default: false, desc: "Generate controller specs"
      class_option :request_specs,    type: :boolean, default: true,  desc: "Generate request specs"
      class_option :view_specs,       type: :boolean, default: true,  desc: "Generate view specs"
      class_option :helper_specs,     type: :boolean, default: true,  desc: "Generate helper specs"
      class_option :routing_specs,    type: :boolean, default: true,  desc: "Generate routing specs"
      class_option :policy_specs,     type: :boolean, default: true,  desc: "Generate policy specs"

      def initialize(*args, &blk)
        @generator_args = args.first
        super(*args, &blk)
      end

      def generate_controller_spec
        return unless options[:controller_specs]

        if options[:api]
          template 'api_controller_spec.rb', template_file(folder: 'controllers', suffix: '_controller')
        else
          template 'controller_spec.rb', template_file(folder: 'controllers', suffix: '_controller')
        end
      end

      def generate_request_spec
        return unless options[:request_specs]

        if options[:api]
          template 'api_request_spec.rb', template_file(folder: 'requests')
        else
          template 'request_spec.rb', template_file(folder: 'requests')
        end
      end

      def generate_view_specs
        return if options[:api]
        return unless options[:view_specs] && options[:template_engine]

        copy_view :edit
        copy_view :index unless options[:singleton]
        copy_view :new
        copy_view :show
      end

      def generate_routing_spec
        return unless options[:routing_specs]

        template_file = File.join(
          'spec/routing',
          controller_class_path,
          "#{controller_file_name}_routing_spec.rb"
        )
        template 'routing_spec.rb', template_file
      end

      # My new method to generate policy spec files
      def generate_policy_spec
        return unless options[:policy_specs]

        template_file = File.join(
          'spec/policies',
          controller_class_path,
          "#{singular_name}_policy_spec.rb"
        )
        template 'policy_spec.rb', template_file
      end

    protected

      attr_reader :generator_args

      def copy_view(view)
        template "#{view}_spec.rb",
                 File.join("spec/views", controller_file_path, "#{view}.html.#{options[:template_engine]}_spec.rb")
      end

      # support for namespaced-resources
      def ns_file_name
        return file_name if ns_parts.empty?

        "#{ns_prefix.map(&:underscore).join('/')}_#{ns_suffix.singularize.underscore}"
      end

      # support for namespaced-resources
      def ns_table_name
        return table_name if ns_parts.empty?

        "#{ns_prefix.map(&:underscore).join('/')}/#{ns_suffix.tableize}"
      end

      def ns_parts
        @ns_parts ||= begin
                        parts = generator_args[0].split(/\/|::/)
                        parts.size > 1 ? parts : []
                      end
      end

      def ns_prefix
        @ns_prefix ||= ns_parts[0..-2]
      end

      def ns_suffix
        @ns_suffix ||= ns_parts[-1]
      end

      def value_for(attribute)
        raw_value_for(attribute).inspect
      end

      def raw_value_for(attribute)
        case attribute.type
        when :string
          attribute.name.titleize
        when :integer, :float
          @attribute_id_map ||= {}
          @attribute_id_map[attribute] ||= @attribute_id_map.keys.size.next + attribute.default
        else
          attribute.default
        end
      end

      def template_file(folder:, suffix: '')
        File.join('spec', folder, controller_class_path, "#{controller_file_name}#{suffix}_spec.rb")
      end

      def banner
        self.class.banner
      end
    end
  end
end

Hope this helps others who's battling code generation too!

1 Answers

Please have a look into the official guide here https://guides.rubyonrails.org/generators.html#creating-your-first-generator.

Short summary, create a file in lib/generators/initializer_generator.rb with this content:

class InitializerGenerator < Rails::Generators::Base
  def create_initializer_file
    create_file "config/initializers/initializer.rb", "# Add initialization content here"
  end
end

and execute it with rails generate initializer.

There is even a generator which can do this for you https://guides.rubyonrails.org/generators.html#creating-generators-with-generators.

Related