Skip to content

Commit 524ccc8

Browse files
committed
Allow users to pass a custom loader for non-rails applications
Signed-off-by: Alexandre Terrasa <[email protected]>
1 parent 5b86cbc commit 524ccc8

File tree

3 files changed

+334
-1
lines changed

3 files changed

+334
-1
lines changed

lib/tapioca/commands/dsl.rb

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,14 @@ def load_dsl_defaults
7070

7171
sig { override.void }
7272
def execute
73-
load_dsl_defaults
73+
custom_load_file_path = File.expand_path("#{@tapioca_path}/load.rb")
74+
75+
if File.exist?(custom_load_file_path)
76+
require custom_load_file_path
77+
instance_exec(&Tapioca.dsl_loader_block)
78+
else
79+
load_dsl_defaults
80+
end
7481

7582
if @should_verify
7683
say("Checking for out-of-date RBIs...")

lib/tapioca/runtime/loader.rb

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,4 +121,26 @@ def load_rails_engines
121121
end
122122
end
123123
end
124+
125+
@dsl_loader_block = T.let(nil, T.nilable(T.proc.void))
126+
127+
sig { params(block: T.proc.bind(Tapioca::Commands::Dsl).void).void }
128+
def self.load_for_dsl(&block)
129+
@dsl_loader_block = block
130+
end
131+
132+
sig { returns(T.proc.params(arg: T.untyped).void) }
133+
def self.dsl_loader_block
134+
block = @dsl_loader_block
135+
136+
raise <<~ERR unless block
137+
To provide a custom application loader, `Tapioca.load_for_dsl` must be called with a block
138+
139+
Tapioca.load_for_dsl do
140+
# Add custom load instructions here
141+
end
142+
ERR
143+
144+
T.unsafe(block)
145+
end
124146
end

spec/tapioca/cli/load_spec.rb

Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
1+
# typed: strict
2+
# frozen_string_literal: true
3+
4+
require "spec_helper"
5+
require "yaml"
6+
7+
module Tapioca
8+
class InitSpec < SpecWithProject
9+
describe "cli::load" do
10+
describe "generate dsl" do
11+
before(:all) do
12+
project.require_real_gem("smart_properties", "1.15.0")
13+
project.bundle_install
14+
15+
project.write("lib/post.rb", <<~RB)
16+
require "smart_properties"
17+
18+
class Post
19+
include SmartProperties
20+
property :title, accepts: String
21+
end
22+
RB
23+
end
24+
25+
after do
26+
@project.remove("sorbet/tapioca/load.rb")
27+
@project.remove("sorbet/rbi/dsl")
28+
end
29+
30+
describe "with rails app" do
31+
before(:all) do
32+
project.write("config/application.rb", <<~RB)
33+
module Rails
34+
class Application
35+
attr_reader :config
36+
37+
def load_tasks; end
38+
end
39+
40+
def self.application
41+
Application.new
42+
end
43+
end
44+
45+
lib_dir = File.expand_path("../lib/", __dir__)
46+
47+
# Add lib directory to load path
48+
$LOAD_PATH << lib_dir
49+
50+
# Require files from lib directory
51+
Dir.glob("**/*.rb", base: lib_dir).sort.each do |file|
52+
require(file)
53+
end
54+
RB
55+
56+
project.write("config/environment.rb", <<~RB)
57+
require_relative "application.rb"
58+
RB
59+
end
60+
61+
it "loads the app correctly in a default rails app" do
62+
result = @project.tapioca("dsl Post")
63+
64+
assert_success_status(result)
65+
assert_project_file_exist("sorbet/rbi/dsl/post.rbi")
66+
end
67+
68+
it "executes custom loaders found in sorbet/tapioca/load.rb" do
69+
@project.write("sorbet/tapioca/load.rb", <<~RB)
70+
puts "Custom loader file loaded!"
71+
exit 0
72+
RB
73+
74+
result = @project.tapioca("dsl Post")
75+
76+
assert_includes(result.out, "Custom loader file loaded!")
77+
assert_success_status(result)
78+
refute_project_file_exist("sorbet/rbi/dsl/post.rbi")
79+
end
80+
81+
it "lets errors propagate from custom loaders propagate" do
82+
@project.write("sorbet/tapioca/load.rb", <<~RB)
83+
raise "Some kind of error!"
84+
RB
85+
86+
result = @project.tapioca("dsl Post")
87+
88+
assert_includes(result.err, "Some kind of error!")
89+
refute_success_status(result)
90+
end
91+
92+
it "ensures custom loaders call Tapioca.load_for_dsl" do
93+
@project.write("sorbet/tapioca/load.rb", "")
94+
95+
result = @project.tapioca("dsl Post")
96+
97+
assert_includes(result.err, <<~ERR)
98+
To provide a custom application loader, `Tapioca.load_for_dsl` must be called with a block (RuntimeError)
99+
100+
Tapioca.load_for_dsl do
101+
# Add custom load instructions here
102+
end
103+
ERR
104+
105+
refute_success_status(result)
106+
refute_project_file_exist("sorbet/rbi/dsl/post.rbi")
107+
end
108+
109+
it "ensures custom loaders are passed a block" do
110+
@project.write("sorbet/tapioca/load.rb", <<~RB)
111+
Tapioca.load_for_dsl
112+
RB
113+
114+
result = @project.tapioca("dsl Post")
115+
116+
assert_includes(result.err, <<~ERR)
117+
To provide a custom application loader, `Tapioca.load_for_dsl` must be called with a block (RuntimeError)
118+
119+
Tapioca.load_for_dsl do
120+
# Add custom load instructions here
121+
end
122+
ERR
123+
124+
refute_success_status(result)
125+
refute_project_file_exist("sorbet/rbi/dsl/post.rbi")
126+
end
127+
128+
it "loads the rails app correctly with a custom loader" do
129+
@project.write("sorbet/tapioca/load.rb", <<~RB)
130+
Tapioca.load_for_dsl do
131+
load_dsl_extensions
132+
load_application(eager_load: @requested_constants.empty?)
133+
load_dsl_compilers
134+
end
135+
RB
136+
137+
result = @project.tapioca("dsl Post")
138+
139+
assert_includes(result.out, "Loading Rails application... Done")
140+
assert_success_status(result)
141+
assert_project_file_exist("sorbet/rbi/dsl/post.rbi")
142+
end
143+
144+
it "loads the rails app correctly with a custom loader using the default loader" do
145+
@project.write("sorbet/tapioca/load.rb", <<~RB)
146+
Tapioca.load_for_dsl do
147+
load_dsl_defaults
148+
end
149+
RB
150+
151+
result = @project.tapioca("dsl Post")
152+
153+
assert_includes(result.out, "Loading Rails application... Done")
154+
assert_success_status(result)
155+
assert_project_file_exist("sorbet/rbi/dsl/post.rbi")
156+
end
157+
158+
it "loads the rails app and the custom compilers correctly using the default loader" do
159+
@project.write("sorbet/tapioca/load.rb", <<~RB)
160+
Tapioca.load_for_dsl do
161+
load_dsl_defaults
162+
163+
require_relative "custom_compiler.rb"
164+
end
165+
RB
166+
167+
@project.write("sorbet/tapioca/custom_compiler.rb", <<~RB)
168+
require "post"
169+
170+
class CustomCompiler < Tapioca::Dsl::Compiler
171+
extend T::Sig
172+
173+
ConstantType = type_member(fixed: T.class_of(::Post))
174+
175+
sig { override.void }
176+
def decorate
177+
root.create_path(constant) do |klass|
178+
klass.create_method(:custom_method)
179+
end
180+
end
181+
182+
sig { override.returns(T::Enumerable[Module]) }
183+
def self.gather_constants
184+
[::Post]
185+
end
186+
end
187+
RB
188+
189+
result = @project.tapioca("dsl Post")
190+
191+
@project.remove("sorbet/tapioca/custom_compiler.rb")
192+
193+
assert_includes(result.out, "Loading Rails application... Done")
194+
assert_success_status(result)
195+
assert_project_file_equal("sorbet/rbi/dsl/post.rbi", <<~RBI)
196+
# typed: true
197+
198+
# DO NOT EDIT MANUALLY
199+
# This is an autogenerated file for dynamic methods in `Post`.
200+
# Please instead update this file by running `bin/tapioca dsl Post`.
201+
202+
class Post
203+
include SmartPropertiesGeneratedMethods
204+
205+
sig { returns(T.untyped) }
206+
def custom_method; end
207+
208+
module SmartPropertiesGeneratedMethods
209+
sig { returns(T.nilable(::String)) }
210+
def title; end
211+
212+
sig { params(title: T.nilable(::String)).returns(T.nilable(::String)) }
213+
def title=(title); end
214+
end
215+
end
216+
RBI
217+
end
218+
end
219+
220+
describe "load non-rails app" do
221+
it "loads the app correctly with a custom loader" do
222+
@project.write("sorbet/tapioca/load.rb", <<~RB)
223+
Tapioca.load_for_dsl do
224+
print "Loading application..."
225+
require_relative "../../lib/post.rb"
226+
puts " Done"
227+
228+
load_dsl_compilers
229+
end
230+
RB
231+
232+
result = @project.tapioca("dsl Post")
233+
234+
assert_includes(result.out, "Loading application... Done")
235+
assert_success_status(result)
236+
assert_project_file_exist("sorbet/rbi/dsl/post.rbi")
237+
end
238+
239+
it "loads the app and custom compilers correctly with a custom loader" do
240+
@project.write("sorbet/tapioca/load.rb", <<~RB)
241+
Tapioca.load_for_dsl do
242+
print "Loading application..."
243+
require_relative "../../lib/post.rb"
244+
puts " Done"
245+
246+
load_dsl_compilers
247+
require_relative "../../lib/compilers/custom_compiler.rb"
248+
end
249+
RB
250+
251+
@project.write("lib/compilers/custom_compiler.rb", <<~RB)
252+
class CustomCompiler < Tapioca::Dsl::Compiler
253+
extend T::Sig
254+
255+
ConstantType = type_member(fixed: T.class_of(::Post))
256+
257+
sig { override.void }
258+
def decorate
259+
root.create_path(constant) do |klass|
260+
klass.create_method(:custom_method)
261+
end
262+
end
263+
264+
sig { override.returns(T::Enumerable[Module]) }
265+
def self.gather_constants
266+
[::Post]
267+
end
268+
end
269+
RB
270+
271+
result = @project.tapioca("dsl Post")
272+
273+
@project.remove("lib/compilers/custom_compiler.rb")
274+
275+
assert_includes(result.out, "Loading application... Done")
276+
assert_success_status(result)
277+
assert_project_file_equal("sorbet/rbi/dsl/post.rbi", <<~RBI)
278+
# typed: true
279+
280+
# DO NOT EDIT MANUALLY
281+
# This is an autogenerated file for dynamic methods in `Post`.
282+
# Please instead update this file by running `bin/tapioca dsl Post`.
283+
284+
class Post
285+
include SmartPropertiesGeneratedMethods
286+
287+
sig { returns(T.untyped) }
288+
def custom_method; end
289+
290+
module SmartPropertiesGeneratedMethods
291+
sig { returns(T.nilable(::String)) }
292+
def title; end
293+
294+
sig { params(title: T.nilable(::String)).returns(T.nilable(::String)) }
295+
def title=(title); end
296+
end
297+
end
298+
RBI
299+
end
300+
end
301+
end
302+
end
303+
end
304+
end

0 commit comments

Comments
 (0)