Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Prototype for custom app loaders #912

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions lib/tapioca/commands/dsl.rb
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,23 @@ def initialize(
@loader = T.let(nil, T.nilable(Runtime::Loader))
end

sig { override.void }
def execute
sig { void }
def load_dsl_defaults
load_dsl_extensions
load_application(eager_load: @requested_constants.empty?)
abort_if_pending_migrations!
load_dsl_compilers
end

sig { override.void }
def execute
custom_load_file_path = File.expand_path("#{@tapioca_path}/load.rb")

if File.exist?(custom_load_file_path)
require custom_load_file_path
instance_exec(&Tapioca.dsl_loader_block)
else
load_dsl_defaults
end

if @should_verify
say("Checking for out-of-date RBIs...")
Expand Down Expand Up @@ -141,6 +152,8 @@ def load_application(eager_load:)
)

say("Done", :green)

abort_if_pending_migrations!
end

sig { void }
Expand Down
22 changes: 22 additions & 0 deletions lib/tapioca/runtime/loader.rb
Original file line number Diff line number Diff line change
Expand Up @@ -121,4 +121,26 @@ def load_rails_engines
end
end
end

@dsl_loader_block = T.let(nil, T.nilable(T.proc.void))

sig { params(block: T.proc.bind(Tapioca::Commands::Dsl).void).void }
def self.load_for_dsl(&block)
@dsl_loader_block = block
end

sig { returns(T.proc.params(arg: T.untyped).void) }
def self.dsl_loader_block
block = @dsl_loader_block

raise <<~ERR unless block
To provide a custom application loader, `Tapioca.load_for_dsl` must be called with a block

Tapioca.load_for_dsl do
# Add custom load instructions here
end
ERR

T.unsafe(block)
end
end
304 changes: 304 additions & 0 deletions spec/tapioca/cli/load_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,304 @@
# typed: strict
# frozen_string_literal: true

require "spec_helper"
require "yaml"

module Tapioca
class InitSpec < SpecWithProject
describe "cli::load" do
describe "generate dsl" do
before(:all) do
project.require_real_gem("smart_properties", "1.15.0")
project.bundle_install

project.write("lib/post.rb", <<~RB)
require "smart_properties"

class Post
include SmartProperties
property :title, accepts: String
end
RB
end

after do
@project.remove("sorbet/tapioca/load.rb")
@project.remove("sorbet/rbi/dsl")
end

describe "with rails app" do
before(:all) do
project.write("config/application.rb", <<~RB)
module Rails
class Application
attr_reader :config

def load_tasks; end
end

def self.application
Application.new
end
end

lib_dir = File.expand_path("../lib/", __dir__)

# Add lib directory to load path
$LOAD_PATH << lib_dir

# Require files from lib directory
Dir.glob("**/*.rb", base: lib_dir).sort.each do |file|
require(file)
end
RB

project.write("config/environment.rb", <<~RB)
require_relative "application.rb"
RB
end

it "loads the app correctly in a default rails app" do
result = @project.tapioca("dsl Post")

assert_success_status(result)
assert_project_file_exist("sorbet/rbi/dsl/post.rbi")
end

it "executes custom loaders found in sorbet/tapioca/load.rb" do
@project.write("sorbet/tapioca/load.rb", <<~RB)
puts "Custom loader file loaded!"
exit 0
RB

result = @project.tapioca("dsl Post")

assert_includes(result.out, "Custom loader file loaded!")
assert_success_status(result)
refute_project_file_exist("sorbet/rbi/dsl/post.rbi")
end

it "lets errors propagate from custom loaders propagate" do
@project.write("sorbet/tapioca/load.rb", <<~RB)
raise "Some kind of error!"
RB

result = @project.tapioca("dsl Post")

assert_includes(result.err, "Some kind of error!")
refute_success_status(result)
end

it "ensures custom loaders call Tapioca.load_for_dsl" do
@project.write("sorbet/tapioca/load.rb", "")

result = @project.tapioca("dsl Post")

assert_includes(result.err, <<~ERR)
To provide a custom application loader, `Tapioca.load_for_dsl` must be called with a block (RuntimeError)

Tapioca.load_for_dsl do
# Add custom load instructions here
end
ERR

refute_success_status(result)
refute_project_file_exist("sorbet/rbi/dsl/post.rbi")
end

it "ensures custom loaders are passed a block" do
@project.write("sorbet/tapioca/load.rb", <<~RB)
Tapioca.load_for_dsl
RB

result = @project.tapioca("dsl Post")

assert_includes(result.err, <<~ERR)
To provide a custom application loader, `Tapioca.load_for_dsl` must be called with a block (RuntimeError)

Tapioca.load_for_dsl do
# Add custom load instructions here
end
ERR

refute_success_status(result)
refute_project_file_exist("sorbet/rbi/dsl/post.rbi")
end

it "loads the rails app correctly with a custom loader" do
@project.write("sorbet/tapioca/load.rb", <<~RB)
Tapioca.load_for_dsl do
load_dsl_extensions
load_application(eager_load: @requested_constants.empty?)
load_dsl_compilers
end
RB

result = @project.tapioca("dsl Post")

assert_includes(result.out, "Loading Rails application... Done")
assert_success_status(result)
assert_project_file_exist("sorbet/rbi/dsl/post.rbi")
end

it "loads the rails app correctly with a custom loader using the default loader" do
@project.write("sorbet/tapioca/load.rb", <<~RB)
Tapioca.load_for_dsl do
load_dsl_defaults
end
RB

result = @project.tapioca("dsl Post")

assert_includes(result.out, "Loading Rails application... Done")
assert_success_status(result)
assert_project_file_exist("sorbet/rbi/dsl/post.rbi")
end

it "loads the rails app and the custom compilers correctly using the default loader" do
@project.write("sorbet/tapioca/load.rb", <<~RB)
Tapioca.load_for_dsl do
load_dsl_defaults

require_relative "custom_compiler.rb"
end
RB

@project.write("sorbet/tapioca/custom_compiler.rb", <<~RB)
require "post"

class CustomCompiler < Tapioca::Dsl::Compiler
extend T::Sig

ConstantType = type_member(fixed: T.class_of(::Post))

sig { override.void }
def decorate
root.create_path(constant) do |klass|
klass.create_method(:custom_method)
end
end

sig { override.returns(T::Enumerable[Module]) }
def self.gather_constants
[::Post]
end
end
RB

result = @project.tapioca("dsl Post")

@project.remove("sorbet/tapioca/custom_compiler.rb")

assert_includes(result.out, "Loading Rails application... Done")
assert_success_status(result)
assert_project_file_equal("sorbet/rbi/dsl/post.rbi", <<~RBI)
# typed: true

# DO NOT EDIT MANUALLY
# This is an autogenerated file for dynamic methods in `Post`.
# Please instead update this file by running `bin/tapioca dsl Post`.

class Post
include SmartPropertiesGeneratedMethods

sig { returns(T.untyped) }
def custom_method; end

module SmartPropertiesGeneratedMethods
sig { returns(T.nilable(::String)) }
def title; end

sig { params(title: T.nilable(::String)).returns(T.nilable(::String)) }
def title=(title); end
end
end
RBI
end
end

describe "load non-rails app" do
it "loads the app correctly with a custom loader" do
@project.write("sorbet/tapioca/load.rb", <<~RB)
Tapioca.load_for_dsl do
print "Loading application..."
require_relative "../../lib/post.rb"
puts " Done"

load_dsl_compilers
end
RB

result = @project.tapioca("dsl Post")

assert_includes(result.out, "Loading application... Done")
assert_success_status(result)
assert_project_file_exist("sorbet/rbi/dsl/post.rbi")
end

it "loads the app and custom compilers correctly with a custom loader" do
@project.write("sorbet/tapioca/load.rb", <<~RB)
Tapioca.load_for_dsl do
print "Loading application..."
require_relative "../../lib/post.rb"
puts " Done"

load_dsl_compilers
require_relative "../../lib/compilers/custom_compiler.rb"
end
RB

@project.write("lib/compilers/custom_compiler.rb", <<~RB)
class CustomCompiler < Tapioca::Dsl::Compiler
extend T::Sig

ConstantType = type_member(fixed: T.class_of(::Post))

sig { override.void }
def decorate
root.create_path(constant) do |klass|
klass.create_method(:custom_method)
end
end

sig { override.returns(T::Enumerable[Module]) }
def self.gather_constants
[::Post]
end
end
RB

result = @project.tapioca("dsl Post")

@project.remove("lib/compilers/custom_compiler.rb")

assert_includes(result.out, "Loading application... Done")
assert_success_status(result)
assert_project_file_equal("sorbet/rbi/dsl/post.rbi", <<~RBI)
# typed: true

# DO NOT EDIT MANUALLY
# This is an autogenerated file for dynamic methods in `Post`.
# Please instead update this file by running `bin/tapioca dsl Post`.

class Post
include SmartPropertiesGeneratedMethods

sig { returns(T.untyped) }
def custom_method; end

module SmartPropertiesGeneratedMethods
sig { returns(T.nilable(::String)) }
def title; end

sig { params(title: T.nilable(::String)).returns(T.nilable(::String)) }
def title=(title); end
end
end
RBI
end
end
end
end
end
end