Open paracycle opened 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.
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]
Anyone have a solution for this?
Here's the first part anyway. It satisfies my code at this point.
Also, for anyone trying to run this on an engine that isn't mounted, make sure you use the --app-root
cli switch, i.e. tapioca dsl --app-root=test/dummy
Creates an engine.rbi
for each engine with a GeneratedPathHelpersModule
and GeneratedUrlHelpersModule
scoped to the Engine
.
# sorbet/tapioca/compilers/engine_url_helpers.rb
# typed: strict
# frozen_string_literal: true
module Tapioca
module Dsl
module Compilers
class EngineUrlHelpers < Tapioca::Dsl::Compiler
extend T::Sig
ConstantType = type_member { { fixed: Module } }
sig { override.returns(T::Enumerable[Module]) }
def self.gather_constants
# Gather all Rails::Engine subclasses, specifically targeting Engine(s)
Rails::Engine.subclasses.select { |engine| engine.name.include?("::Engine") }
end
sig { override.void }
def decorate
# Generate all the methods on the engine router.
has_routes = generate_helpers_rbi
# Generate the Engine.routes sig.
generate_engine_routes_rbi if has_routes
end
private
sig { void }
def generate_engine_routes_rbi
root.create_path(constant) do |klass|
klass.create_method(
"routes",
class_method: true,
return_type: "ActionDispatch::Routing::RouteSet"
)
end
end
sig { returns(T::Boolean) }
def generate_helpers_rbi
routes = constant.routes
path_helpers_module = routes.named_routes.path_helpers_module
url_helpers_module = routes.named_routes.url_helpers_module
path_instance_methods = path_helpers_module.instance_methods(false)
url_instance_methods = url_helpers_module.instance_methods(false)
unless path_instance_methods.empty?
# Define the Path Helpers Module
root.create_module("#{constant}::GeneratedPathHelpersModule") do |mod|
mod.create_include("::ActionDispatch::Routing::UrlFor")
mod.create_include("::ActionDispatch::Routing::PolymorphicRoutes")
path_instance_methods.each do |method_name|
mod.create_method(
method_name.to_s,
parameters: [create_rest_param("args", type: "T.untyped")],
return_type: "String"
)
end
end
end
unless url_instance_methods.empty?
# Define the URL Helpers Module
root.create_module("#{constant}::GeneratedUrlHelpersModule") do |mod|
mod.create_include("::ActionDispatch::Routing::UrlFor")
mod.create_include("::ActionDispatch::Routing::PolymorphicRoutes")
url_instance_methods.each do |method_name|
mod.create_method(
method_name.to_s,
parameters: [create_rest_param("args", type: "T.untyped")],
return_type: "String"
)
end
end
end
!path_instance_methods.empty? || !url_instance_methods.empty?
end
end
end
end
end
For example, this is what is generated for the Litestream engine.
# sorbet/rbi/dsl/litestream/engine.rbi
# typed: true
# DO NOT EDIT MANUALLY
# This is an autogenerated file for dynamic methods in `Litestream::Engine`.
# Please instead update this file by running `bin/tapioca dsl Litestream::Engine`.
class Litestream::Engine
class << self
sig { returns(ActionDispatch::Routing::RouteSet) }
def routes; end
end
end
module Litestream::Engine::GeneratedPathHelpersModule
include ::ActionDispatch::Routing::UrlFor
include ::ActionDispatch::Routing::PolymorphicRoutes
sig { params(args: T.untyped).returns(String) }
def process_path(*args); end
sig { params(args: T.untyped).returns(String) }
def restorations_path(*args); end
sig { params(args: T.untyped).returns(String) }
def root_path(*args); end
end
module Litestream::Engine::GeneratedUrlHelpersModule
include ::ActionDispatch::Routing::UrlFor
include ::ActionDispatch::Routing::PolymorphicRoutes
sig { params(args: T.untyped).returns(String) }
def process_url(*args); end
sig { params(args: T.untyped).returns(String) }
def restorations_url(*args); end
sig { params(args: T.untyped).returns(String) }
def root_url(*args); end
end
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 theRails.application
one.In order to be able to properly represent this, we need to do the following:
engine.routes.named_routes.url_helpers_module
andengine.routes.named_routes.path_helpers_module
and give them engine specific names, likeXXX::Engine::GeneratedUrlHelpersModule
.decorate
, create module definitions for the app and engine url helpers and add includes for them for the modules that include them.Test Case
should create:
and the corresponding includes to the modules that include the above helpers.