Shopify / tapioca

The swiss army knife of RBI generation
MIT License
732 stars 122 forks source link

Make `UrlHelpers` generator Rails Engine aware #474

Open paracycle opened 3 years ago

paracycle commented 3 years ago

It turns out that Rails Engines have their own router and the controllers, etc inside a Rails Engine get an engine specific url_helper module included, not the Rails.application one.

In order to be able to properly represent this, we need to do the following:

Test Case

module Article
  class Engine < ::Rails::Engine
    isolate_namespace Article

    routes.draw do
      resource :articles
    end
  end
end

class Application < Rails::Application
  routes.draw do
    resource :index
    mount Article::Engine, at: "/", as: "articles"
  end
end

should create:

# generated_path_helpers_module.rbi
# typed: strong

module GeneratedPathHelpersModule
  include ::ActionDispatch::Routing::UrlFor
  include ::ActionDispatch::Routing::PolymorphicRoutes

  sig { params(args: T.untyped).returns(String) }
  def edit_index_path(*args); end

  sig { params(args: T.untyped).returns(String) }
  def index_path(*args); end

  sig { params(args: T.untyped).returns(String) }
  def new_index_path(*args); end

  sig { params(args: T.untyped).returns(String) }
  def articles_path(*args); end
end
# generated_url_helpers_module.rbi
# typed: strong

module GeneratedUrlHelpersModule
  include ::ActionDispatch::Routing::UrlFor
  include ::ActionDispatch::Routing::PolymorphicRoutes

  sig { params(args: T.untyped).returns(String) }
  def edit_index_url(*args); end

  sig { params(args: T.untyped).returns(String) }
  def index_url(*args); end

  sig { params(args: T.untyped).returns(String) }
  def new_index_url(*args); end

  sig { params(args: T.untyped).returns(String) }
  def articles_url(*args); end
end
# article/engine/generated_path_helpers_module.rbi
# typed: strong

module Article::Engine::GeneratedPathHelpersModule
  include ::ActionDispatch::Routing::UrlFor
  include ::ActionDispatch::Routing::PolymorphicRoutes

  sig { params(args: T.untyped).returns(String) }
  def edit_article_path(*args); end

  sig { params(args: T.untyped).returns(String) }
  def article_path(*args); end

  sig { params(args: T.untyped).returns(String) }
  def new_article_path(*args); end

  sig { params(args: T.untyped).returns(String) }
  def articles_path(*args); end
end
# article/engine/generated_url_helpers_module.rbi
# typed: strong

module Article::Engine::GeneratedUrlHelpersModule
  include ::ActionDispatch::Routing::UrlFor
  include ::ActionDispatch::Routing::PolymorphicRoutes

  sig { params(args: T.untyped).returns(String) }
  def edit_article_url(*args); end

  sig { params(args: T.untyped).returns(String) }
  def article_url(*args); end

  sig { params(args: T.untyped).returns(String) }
  def new_article_url(*args); end

  sig { params(args: T.untyped).returns(String) }
  def articles_url(*args); end
end

and the corresponding includes to the modules that include the above helpers.

paracycle commented 3 years ago

It turns out that for the above test case also need to generate:

module GeneratedMountedHelpers < ActionDispatch::Routing::RouteSet::MountedHelpers
  sig { returns(Article::Engine::RoutesProxy) }
  def articles; end
end

with:

class Article::Engine::GeneratedRoutesProxy < ActionDispatch::Routing::Proxy
  include Article::Engine::GeneratedPathHelpersModule
  include Article::Engine::GeneratedUrlHelpersModule
end

and the GeneratedMountedHelpers module being mixed into all controllers as a helper module.

paracycle commented 3 years ago

Btw, the way to discover all this info about Rails engines is:

$ dev c
👩‍💻  Running bin/console from dev.yml
[1] pry(main)> require "rails"
=> true
[2] pry(main)> module Article
  class Engine < ::Rails::Engine
    isolate_namespace Article

    routes.draw do
      resource :articles
    end
  end
end

class Application < Rails::Application
  routes.draw do
    resource :index
    mount Article::Engine, at: "/", as: "articles"
  end
end
=> nil
[3] pry(main)> Rails.application.railties.grep(Rails::Engine)
=> [#<Article::Engine:0x00007f80193c0d48
  @_all_autoload_paths=nil,
  @_all_load_paths=nil,
  @app=nil,
  @app_build_lock=#<Thread::Mutex:0x00007f80193c0be0>,
  @config=
   #<Rails::Engine::Configuration:0x00007f80194f3c10
    @generators=
     #<Rails::Configuration::Generators:0x00007f80194f38a0
      @after_generate_callbacks=[],
      @aliases={},
      @api_only=false,
      @colorize_logging=true,
      @fallbacks={},
      @hidden_namespaces=[],
      @options={},
      @templates=[]>,
    @javascript_path="javascript",
    @middleware=#<Rails::Configuration::MiddlewareStackProxy:0x00007f80194f3648 @delete_operations=[], @operations=[]>,
    @root=#<Pathname:/Users/ufuk/src/github.com/Shopify/shopify-types>>,
  @env_config=nil,
  @helpers=nil,
  @routes=#<ActionDispatch::Routing::RouteSet:0x00007f80194f3490>>]
[4] pry(main)> Rails.application.railties.grep(Rails::Engine).first.routes.named_routes.path_helpers_module.instance_methods(false)
=> [:edit_articles_path, :new_articles_path, :articles_path]
[5] pry(main)> Rails.application.railties.grep(Rails::Engine).first.routes.named_routes.url_helpers_module.instance_methods(false)
=> [:articles_url, :edit_articles_url, :new_articles_url]
[6] pry(main)> Rails.application.routes.mounted_helpers.instance_methods(false)
=> [:_articles, :articles]