Skip to content

Instantly share code, notes, and snippets.

@annikoff
Last active December 17, 2024 11:38
Show Gist options
  • Save annikoff/331f785aa7a207a7945b1eca6eff526b to your computer and use it in GitHub Desktop.
Save annikoff/331f785aa7a207a7945b1eca6eff526b to your computer and use it in GitHub Desktop.
Custom generators

The main generator

# lib/generators/rails/policy/policy_generator.rb

module Rails
  module Generators
    class PolicyGenerator < NamedBase
      source_root File.expand_path('templates', __dir__)

      def copy_policy_file
        template 'policy.erb', File.join("app/policies", class_path, "#{file_name}_policy.rb")
      end

      hook_for :test_framework
    end
  end
end

The generator's template

# lib/generators/rails/policy/templates/policy.erb

class <%= class_name %>Policy
  # Add default methods
end

A hook to invoke the custom generator with scaffolding or with controller's generators

# lib/generators/rails/policy/hooks.rb

require 'rails/generators'
require 'rails/generators/rails/scaffold/scaffold_generator'
require 'rails/generators/rails/controller/controller_generator'

Rails::Generators::ScaffoldGenerator.hook_for :policy, default: true, type: :boolean # invoke with scaffolding generators
Rails::Generators::ControllerGenerator.hook_for :policy, default: true, type: :boolean # invoke with the controllers' generator.

The generator of a spec file

# lib/generators/rspec/policy/policy_generator.rb

module Rspec
  module Generators
    class PolicyGenerator < Rails::Generators::NamedBase
      source_root File.expand_path('templates', __dir__)

      def copy_policy_spec_file
        template 'policy_spec.erb',  File.join("spec/policies", class_path, "#{file_name}_policy_spec.rb")
      end
    end
  end
end

The spec's template

# lib/generators/rspec/policy/templates/policy_spec.erb

require 'spec_helper'

describe <%= class_name %>Policy do
  pending "add some examples to (or delete) #{__FILE__}"
end

Application config to hook up custom generators

# config/application.rb

module YourAppName
  class Application < Rails::Application
    # ...

    config.generators do |g|
      g.test_framework  :rspec
      require './lib/generators/rails/policy/hooks'
    end
  end
end

Generate scaffolding

$ rails g scaffold user
      invoke  resource_route
       route    resources :users
      invoke  scaffold_controller
      create    app/controllers/users_controller.rb
      invoke    erb
      create      app/views/users
      create      app/views/users/index.html.erb
      create      app/views/users/edit.html.erb
      create      app/views/users/show.html.erb
      create      app/views/users/new.html.erb
      create      app/views/users/_form.html.erb
      invoke    rspec
      create      spec/requests/users_spec.rb
      create      spec/views/users/edit.html.erb_spec.rb
      create      spec/views/users/index.html.erb_spec.rb
      create      spec/views/users/new.html.erb_spec.rb
      create      spec/views/users/show.html.erb_spec.rb
      create      spec/routing/users_routing_spec.rb
      invoke    helper
      create      app/helpers/users_helper.rb
      invoke      rspec
      create        spec/helpers/users_helper_spec.rb
      invoke    jbuilder
      create      app/views/users/index.json.jbuilder
      create      app/views/users/show.json.jbuilder
      create      app/views/users/_user.json.jbuilder
      invoke  assets
      invoke    scss
      create      app/assets/stylesheets/users.scss
      invoke  css
   identical    app/assets/stylesheets/scaffold.css
      invoke  policy
      create    app/policies/user_policy.rb
      invoke    rspec
      create      spec/policies/user_policy_spec.rb

$ cat app/policies/user_policy.rb
class UserPolicy
  # Add default methods
end

$ cat spec/policies/user_policy_spec.rb
require 'spec_helper'

describe UserPolicy do
  pending "add some examples to (or delete) #{__FILE__}"
end

Generate a controller inside a module

$ rails g controller api/project
      create  app/controllers/api/project_controller.rb
      invoke  erb
      create    app/views/api/project
      invoke  helper
      create    app/helpers/api/project_helper.rb
      invoke    rspec
      create      spec/helpers/api/project_helper_spec.rb
      invoke  assets
      invoke    scss
      create      app/assets/stylesheets/api/project.scss
      invoke  policy
      create    app/policies/api/project_policy.rb
      invoke    rspec
      create      spec/policies/api/project_policy_spec.rb

$ cat app/policies/api/project_policy.rb
class Api::ProjectPolicy
  # Add default methods
end

$ cat spec/policies/api/project_policy_spec.rb
require 'spec_helper'

describe Api::ProjectPolicy do
  pending "add some examples to (or delete) #{__FILE__}"
end
@james-em
Copy link

@annikoff Indeed. It's skipping migration/model generators. :(

@annikoff
Copy link
Author

annikoff commented Dec 27, 2021

@deepakmahakale @james-em sorry guys I don't even remember why I've created this gist :) I gues it was some kind of experiment.

@deepakmahakale
Copy link

@annikoff Thanks for actually pointing toward the solution.

I forgot I made it work with some tweaks and the changed code is working with ruby 3.0.0 and rails 6

config/initializers/pundit_policy_generator.rb

# frozen_string_literal: true

require 'rails/generators'

# NOTE: This is the only approach that works without skipping any existing
# generators.
# Other approaches tried:
# - Added hook in an initializer by opening class
#   - Missing `jbuilder`
# - Added whole files at the same generator locations
#   - Missing `controller_test` and `system test`
# - The existing approach https://gist.github.com/annikoff/331f785aa7a207a7945b1eca6eff526b
#   - Missing model
module Rails
  module Generators
    class ScaffoldControllerGenerator < NamedBase
      # invoke with scaffolding controllers' generators
      hook_for :policy, in: :pundit, default: true, type: :boolean
    end
    class ControllerGenerator < NamedBase
      # invoke with the controllers' generator.
      hook_for :policy, in: :pundit, default: true, type: :boolean
    end
  end
end

lib/generators/rails/controller/controller_generator.rb

# frozen_string_literal: true

# NOTE: Please do not remove this.
#
# This is required in order to invoke custom generators with the following
# command:
#   rails g controller [controller name]
#
# We have already tried different approaches mentioned in:
#   config/initializers/pundit_policy_generator.rb
#
module Rails
  module Generators
    class ControllerGenerator < NamedBase # :nodoc:
    end
  end
end

lib/generators/rails/scaffold_controller/scaffold_controller_generator.rb

# frozen_string_literal: true

# NOTE: Please do not remove this.
#
# This is required in order to invoke custom generators with the following
# command:
#   rails g scaffold [resource name]
#
# We have already tried different approaches mentioned in:
#   config/initializers/pundit_policy_generator.rb
#
module Rails
  module Generators
    class ScaffoldControllerGenerator < NamedBase # :nodoc:
    end
  end
end

@annikoff
Copy link
Author

@deepakmahakale Great. Thank you.

@james-em
Copy link

james-em commented May 16, 2022

@deepakmahakale

Your solution only half worked for me. While it did not skip any scaffolding and did scaffold policies, it stopped picking my custom scaffold templates in lib/templates/rails/scaffold_controller/controller.rb.tt

I've come to a perfect solution heavily inspired by

I only need 1 initializer to get everything working:

# Inspired by:
# - https://gist.github.com/annikoff/331f785aa7a207a7945b1eca6eff526b
# - https://github.com/drapergem/draper/blob/master/lib/generators/controller_override.rb
# - https://github.com/drapergem/draper/blob/master/lib/draper/railtie.rb

require "rails/railtie"
require "rails/generators"
require "generators/pundit/policy/policy_generator"

module CustomPunditGenerator
  module ControllerGenerator
    extend ActiveSupport::Concern

    included do
      hook_for :policy, in: :pundit, default: true, type: :boolean do |generator|
        invoke generator, [name.singularize]
      end
    end
  end

  module ScaffoldControllerGenerator
    extend ActiveSupport::Concern

    included do
      hook_for :policy, in: :pundit, default: true, type: :boolean
    end
  end

  module PolicyGenerator
    extend ActiveSupport::Concern

    included do
      source_root File.expand_path("templates", __dir__)

      def create_policy
        template(
          Rails.root.join("lib/templates/pundit/scaffold/policy.rb.tt"),
          File.join("app/policies", class_path, "#{file_name}_policy.rb")
        )
      end
    end
  end
end

module ActiveModel
  class Railtie < Rails::Railtie
    generators do |app|
      Rails::Generators.configure! app.config.generators

      Rails::Generators::ControllerGenerator.include CustomPunditGenerator::ControllerGenerator
      Rails::Generators::ScaffoldControllerGenerator.include CustomPunditGenerator::ScaffoldControllerGenerator
      Pundit::Generators::PolicyGenerator.include CustomPunditGenerator::PolicyGenerator
    end
  end
end

Edit: Specs for policies are no longer generated. It seems as soon as we do

require "generators/pundit/policy/policy_generator"

it breaks it. Can't figure out why. Anyone having an explanation?

@annikoff
Copy link
Author

@james-em There might be some issues with require when the default loader is ZeitWerk.

@james-em
Copy link

@james-em There might be some issues with require when the default loader is ZeitWerk.

That is my guess too. What should be done instead?

@annikoff
Copy link
Author

Maybe remove generators/pundit/policy/policy_generator, zeitwerk should find and load Pundit::Generators::PolicyGenerator automatically, I guess.

@james-em
Copy link

Maybe remove generators/pundit/policy/policy_generator, zeitwerk should find and load Pundit::Generators::PolicyGenerator automatically, I guess.

I have tested without the require already since I know issues are coming from it.

I have tested all sort of events too but there is one more I haven't tried yet and it's the loader.on_load from Zeitwork. I will give it a try later

The only file I have is the initializer

Edit: Didn't work. Got any idea?

@james-em
Copy link

james-em commented May 17, 2022

I gave up and disabled pundit generator. Created my own generators and no more issue.

bundle exec rails generate generator rails/custom_policy
bundle exec rails generate generator rails/custom_policy

Initializer

require "rails/railtie"
require "rails/generators"

module CustomPolicyGenerator
  module ControllerGenerator
    extend ActiveSupport::Concern

    included do
      hook_for :custom_policy, in: nil, default: true, type: :boolean do |generator|
        invoke generator, [name.singularize]
      end
    end
  end

  module ScaffoldControllerGenerator
    extend ActiveSupport::Concern

    included do
      hook_for :custom_policy, in: nil, default: true, type: :boolean
    end
  end
end

module ActiveModel
  class Railtie < Rails::Railtie
    generators do |app|
      Rails::Generators.configure! app.config.generators
      Rails::Generators::ControllerGenerator.include CustomPolicyGenerator::ControllerGenerator
      Rails::Generators::ScaffoldControllerGenerator.include CustomPolicyGenerator::ScaffoldControllerGenerator
    end
  end
end

Rails.application.config.generators do |g|
  g.policy false
  g.custom_policy true
end
module Rails
  class CustomPolicyGenerator < Rails::Generators::NamedBase
    # source_root File.expand_path("templates", __dir__)

    def create_policy
      template "policy.rb", File.join("app/policies", class_path, "#{file_name}_policy.rb")
    end

    hook_for :test_framework
  end
end
module Rspec
  class CustomPolicyGenerator < Rails::Generators::NamedBase
    # source_root File.expand_path("templates", __dir__)

    def create_policy_spec
      template "policy_spec.rb", File.join("spec/policies", class_path, "#{file_name}_policy_spec.rb")
    end
  end
end

Placed template files in
lib/templates/rails/custom_policy/policy.rb.tt
lib/templates/rspec/custom_policy/policy_spec.rb.tt

@annikoff
Copy link
Author

Thanks for the effort.

@james-em
Copy link

Np !

Unsure if this

module ActiveModel
  class Railtie < Rails::Railtie
    generators do |app|
      Rails::Generators.configure! app.config.generators
      Rails::Generators::ControllerGenerator.include CustomPolicyGenerator::ControllerGenerator
      Rails::Generators::ScaffoldControllerGenerator.include CustomPolicyGenerator::ScaffoldControllerGenerator
    end
  end
end

is the best way but outside of that, Rails::Generators::ScaffoldControllerGenerator is not defined and I am forced to use a require. If I use the requires:

require "rails/generators"
require "rails/generators/rails/controller/controller_generator"
require "rails/generators/rails/scaffold_controller/scaffold_controller_generator"

my custom templates for controller are ignored.

@dyeje
Copy link

dyeje commented Jun 18, 2022

Thanks yall. I wrote a blogpost to cover what ended up working for me. I did end up needing the requires, but I wasn't using custom templates so not sure if it breaks that like you mentioned.

Adding a Custom Generator to Rails Scaffold

@annikoff
Copy link
Author

@dyeje That's cool. A lot of attention to this gist :)

@dyeje
Copy link

dyeje commented Jun 20, 2022

I assumed it would be as simple as plugging in a config, so I was surprised when this turned into a multi-hour adventure.

@tbrammar
Copy link

@james-em I'm not too sure that your controller scaffold templates are supposed to end with .tt 🤔

Have you tried simply ending them with .rb and seeing whether rails picks them up?

@davedkg
Copy link

davedkg commented Dec 16, 2024

Thread was super helpful. Finally got this working.

rails@8.0, rspec-rails@7.0, pundit@2.4

Override Pundit generators:

lib/templates/pundit/policy/policy.rb.tt
lib/templates/rspec/policy/policy_spec.rb.tt

Add to custom scaffold generator:

hook_for :policy, in: :pundit, default: true, type: :boolean

@annikoff
Copy link
Author

Great, thank you

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment