From b6470b82c63bdfdf7e16d12ead70f18cf14ed380 Mon Sep 17 00:00:00 2001 From: Alexandre Terrasa Date: Thu, 28 Apr 2022 16:35:08 -0400 Subject: [PATCH 1/3] Move `abort_if_pending_migrations!` inside `load_application` Signed-off-by: Alexandre Terrasa Co-authored-by: Ufuk Kayserilioglu --- lib/tapioca/commands/dsl.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/tapioca/commands/dsl.rb b/lib/tapioca/commands/dsl.rb index ce6f515ef..887861a86 100644 --- a/lib/tapioca/commands/dsl.rb +++ b/lib/tapioca/commands/dsl.rb @@ -65,7 +65,6 @@ def initialize( def execute load_dsl_extensions load_application(eager_load: @requested_constants.empty?) - abort_if_pending_migrations! load_dsl_compilers if @should_verify @@ -141,6 +140,8 @@ def load_application(eager_load:) ) say("Done", :green) + + abort_if_pending_migrations! end sig { void } From 9a9631ba1a2dce61bf2cc7c9f19731517a1ceff8 Mon Sep 17 00:00:00 2001 From: Alexandre Terrasa Date: Thu, 28 Apr 2022 16:09:29 -0400 Subject: [PATCH 2/3] Extract `load_dsl_defaults` Signed-off-by: Alexandre Terrasa Co-authored-by: Ufuk Kayserilioglu --- lib/tapioca/commands/dsl.rb | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/lib/tapioca/commands/dsl.rb b/lib/tapioca/commands/dsl.rb index 887861a86..b41acee78 100644 --- a/lib/tapioca/commands/dsl.rb +++ b/lib/tapioca/commands/dsl.rb @@ -61,11 +61,16 @@ 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?) load_dsl_compilers + end + + sig { override.void } + def execute + load_dsl_defaults if @should_verify say("Checking for out-of-date RBIs...") From 12255bb93bf30a7dd32e5bbaf0e818ab1c9942ee Mon Sep 17 00:00:00 2001 From: Alexandre Terrasa Date: Thu, 28 Apr 2022 16:40:20 -0400 Subject: [PATCH 3/3] Allow users to pass a custom loader for non-rails applications Signed-off-by: Alexandre Terrasa Co-authored-by: Ufuk Kayserilioglu --- lib/tapioca/commands/dsl.rb | 9 +- lib/tapioca/runtime/loader.rb | 22 +++ spec/tapioca/cli/load_spec.rb | 304 ++++++++++++++++++++++++++++++++++ 3 files changed, 334 insertions(+), 1 deletion(-) create mode 100644 spec/tapioca/cli/load_spec.rb diff --git a/lib/tapioca/commands/dsl.rb b/lib/tapioca/commands/dsl.rb index b41acee78..5879c6b52 100644 --- a/lib/tapioca/commands/dsl.rb +++ b/lib/tapioca/commands/dsl.rb @@ -70,7 +70,14 @@ def load_dsl_defaults sig { override.void } def execute - load_dsl_defaults + 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...") diff --git a/lib/tapioca/runtime/loader.rb b/lib/tapioca/runtime/loader.rb index 294f26775..06330c7b5 100644 --- a/lib/tapioca/runtime/loader.rb +++ b/lib/tapioca/runtime/loader.rb @@ -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 diff --git a/spec/tapioca/cli/load_spec.rb b/spec/tapioca/cli/load_spec.rb new file mode 100644 index 000000000..a375583b4 --- /dev/null +++ b/spec/tapioca/cli/load_spec.rb @@ -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