Skip to content

Instantly share code, notes, and snippets.

@annikoff
Last active August 18, 2024 17:48
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
@deepakmahakale
Copy link

deepakmahakale commented May 11, 2021

@annikoff
There's an issue with this approach. I am currently using this to generate policy files.

This for some reason is not creating model. It is skipping the model generator.

Did you face the same issue and did you add any workaround for the same?

@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?

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