Skip to content

Commit

Permalink
Refactor DSL compiler options to be a flat namespaced hash
Browse files Browse the repository at this point in the history
I've been thinking about this for a while, and I think namespacing compiler options was a little heavy handed. I've been thinking about cases where a single option might need to be consumed by multiple Active Record compilers, so I think it makes sense to have a flat hash of options that can be consumed by any compiler.

This makes the code simpler and makes it easier to pass these options on the command line interface, as well.
  • Loading branch information
paracycle committed Jun 18, 2024
1 parent 0c6796d commit d3c6028
Show file tree
Hide file tree
Showing 8 changed files with 92 additions and 46 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -501,6 +501,7 @@ Options:
[--halt-upon-load-error], [--no-halt-upon-load-error], [--skip-halt-upon-load-error] # Halt upon a load error while loading the Rails application
# Default: true
[--skip-constant=constant [constant ...]] # Do not generate RBI definitions for the given application constant(s)
[--compiler-options=key:value] # Options to pass to the DSL compilers
-c, [--config=<config file path>] # Path to the Tapioca configuration file
# Default: sorbet/tapioca/config.yml
-V, [--verbose], [--no-verbose], [--skip-verbose] # Verbose output for debugging purposes
Expand Down
26 changes: 1 addition & 25 deletions lib/tapioca/cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -143,17 +143,13 @@ def todo
option :compiler_options,
type: :hash,
desc: "Options to pass to the DSL compilers",
hide: true,
default: {}
def dsl(*constant_or_paths)
set_environment(options)

# Assume anything starting with a capital letter or colon is a class, otherwise a path
constants, paths = constant_or_paths.partition { |c| c =~ /\A[A-Z:]/ }

# Make sure compiler options are received as a hash
compiler_options = process_compiler_options

command_args = {
requested_constants: constants,
requested_paths: paths.map { |p| Pathname.new(p) },
Expand All @@ -169,7 +165,7 @@ def dsl(*constant_or_paths)
rbi_formatter: rbi_formatter(options),
app_root: options[:app_root],
halt_upon_load_error: options[:halt_upon_load_error],
compiler_options: compiler_options,
compiler_options: options[:compiler_options],
}

command = if options[:verify]
Expand Down Expand Up @@ -381,26 +377,6 @@ def exit_on_failure?

private

def process_compiler_options
compiler_options = options[:compiler_options]

# Parse all compiler option hash values as YAML if they are Strings
compiler_options.transform_values! do |value|
value = YAML.safe_load(value) if String === value
value
rescue YAML::Exception
raise MalformattedArgumentError,
"Option '--compiler-options' should have well-formatted YAML strings, but received: '#{value}'"
end

unless compiler_options.values.all? { |v| Hash === v }
raise MalformattedArgumentError,
"Option '--compiler-options' should be a hash of hashes, but received: '#{compiler_options}'"
end

compiler_options
end

def print_init_next_steps
say(<<~OUTPUT)
#{set_color("This project is now set up for use with Sorbet and Tapioca", :bold)}
Expand Down
18 changes: 10 additions & 8 deletions lib/tapioca/dsl/compilers/active_record_columns.rb
Original file line number Diff line number Diff line change
Expand Up @@ -147,16 +147,18 @@ def gather_constants

private

sig { returns(Helpers::ActiveRecordColumnTypeHelper::ColumnTypeOption) }
ColumnTypeOption = Helpers::ActiveRecordColumnTypeHelper::ColumnTypeOption

sig { returns(ColumnTypeOption) }
def column_type_option
@column_type_option ||= T.let(
Helpers::ActiveRecordColumnTypeHelper::ColumnTypeOption.from_serialized(
options.fetch(
"types",
Helpers::ActiveRecordColumnTypeHelper::ColumnTypeOption::Persisted.serialize,
),
),
T.nilable(Helpers::ActiveRecordColumnTypeHelper::ColumnTypeOption),
ColumnTypeOption.from_options(options) do |value, default_column_type_option|
add_error(<<~MSG.strip)
Unknown value for compiler option `ActiveRecordColumnTypes` given: `#{value}`.
Proceeding with the default value: `#{default_column_type_option.serialize}`.
MSG
end,
T.nilable(ColumnTypeOption),
)
end

Expand Down
26 changes: 26 additions & 0 deletions lib/tapioca/dsl/helpers/active_record_column_type_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,38 @@ class ActiveRecordColumnTypeHelper

class ColumnTypeOption < T::Enum
extend T::Sig

enums do
Untyped = new("untyped")
Nilable = new("nilable")
Persisted = new("persisted")
end

class << self
extend T::Sig

sig do
params(
options: T::Hash[String, T.untyped],
block: T.proc.params(value: String, default_column_type_option: ColumnTypeOption).void,
).returns(ColumnTypeOption)
end
def from_options(options, &block)
column_type_option = Persisted
value = options["ActiveRecordColumnTypes"]

if value
if has_serialized?(value)
column_type_option = from_serialized(value)
else
block.call(value, column_type_option)
end
end

column_type_option
end
end

sig { returns(T::Boolean) }
def persisted?
self == ColumnTypeOption::Persisted
Expand Down
9 changes: 2 additions & 7 deletions lib/tapioca/dsl/pipeline.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ class Pipeline
error_handler: T.proc.params(error: String).void,
skipped_constants: T::Array[Module],
number_of_workers: T.nilable(Integer),
compiler_options: T::Hash[String, T::Hash[String, T.untyped]],
compiler_options: T::Hash[String, T.untyped],
).void
end
def initialize(
Expand Down Expand Up @@ -200,12 +200,7 @@ def rbi_for_constant(constant)
active_compilers.each do |compiler_class|
next unless compiler_class.handles?(constant)

compiler_key = T.must(compiler_class.name).dup
Tapioca::Dsl::Compilers::NAMESPACES.each do |namespace|
compiler_key.delete_prefix!(namespace)
end
options = @compiler_options.fetch(compiler_key, {})
compiler = compiler_class.new(self, file.root, constant, options)
compiler = compiler_class.new(self, file.root, constant, @compiler_options)
compiler.decorate
rescue
$stderr.puts("Error: `#{compiler_class.name}` failed to generate RBI for `#{constant}`")
Expand Down
2 changes: 1 addition & 1 deletion lib/tapioca/helpers/config_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,7 @@ def validate_config_options(command_options, config_key, config_options)
error_msg = "invalid value for option `#{config_option_key}` for key `#{config_key}` - expected " \
"`Hash[String, String]` but found `#{config_option_value}`"
values_to_validate = config_option_value.keys
values_to_validate += config_option_value.values unless config_option_key == "compiler_options"
values_to_validate += config_option_value.values
all_strings = values_to_validate.all? { |v| v.is_a?(String) }
next build_error(error_msg) unless all_strings
end
Expand Down
2 changes: 1 addition & 1 deletion spec/tapioca/cli/dsl_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2920,7 +2920,7 @@ class Post < ActiveRecord::Base
RB

result = @project.tapioca(
"dsl Post --only=ActiveRecordColumns --compiler-options='ActiveRecordColumns:{types: untyped}'",
"dsl Post --only=ActiveRecordColumns --compiler-options='ActiveRecordColumnTypes:untyped'",
)

assert_stdout_equals(<<~OUT, result)
Expand Down
54 changes: 50 additions & 4 deletions spec/tapioca/dsl/compilers/active_record_columns_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1038,7 +1038,7 @@ def body=(value); end
def body?; end
RBI

assert_includes(rbi_for(:Post, compiler_options: { types: "untyped" }), expected)
assert_includes(rbi_for(:Post, compiler_options: { ActiveRecordColumnTypes: "untyped" }), expected)
end

it "generates default columns with untyped" do
Expand Down Expand Up @@ -1189,7 +1189,7 @@ def will_save_change_to_id?; end
end
RBI

assert_equal(expected, rbi_for(:Post, compiler_options: { types: "untyped" }))
assert_equal(expected, rbi_for(:Post, compiler_options: { ActiveRecordColumnTypes: "untyped" }))
end
end

Expand Down Expand Up @@ -1224,7 +1224,7 @@ def body=(value); end
def body?; end
RBI

assert_includes(rbi_for(:Post, compiler_options: { types: "nilable" }), expected)
assert_includes(rbi_for(:Post, compiler_options: { ActiveRecordColumnTypes: "nilable" }), expected)
end

it "generates default columns with nilable" do
Expand Down Expand Up @@ -1375,7 +1375,53 @@ def will_save_change_to_id?; end
end
RBI

assert_equal(expected, rbi_for(:Post, compiler_options: { types: "nilable" }))
assert_equal(expected, rbi_for(:Post, compiler_options: { ActiveRecordColumnTypes: "nilable" }))
end
end

describe "when compiled with invalid column types" do
it "shows an error but generates columns with 'persisted' column types" do
expect_dsl_compiler_errors!

add_ruby_file("schema.rb", <<~RUBY)
ActiveRecord::Migration.suppress_messages do
ActiveRecord::Schema.define do
create_table :posts do |t|
# explicitly setting null to false to test that we always generate
# nilable column types despite this setting
t.string :body, null: false
end
end
end
RUBY

add_ruby_file("post.rb", <<~RUBY)
class Post < ActiveRecord::Base
end
RUBY

expected = indented(<<~RBI, 2)
module GeneratedAttributeMethods
sig { returns(::String) }
def body; end
sig { params(value: ::String).returns(::String) }
def body=(value); end
sig { returns(T::Boolean) }
def body?; end
RBI

assert_includes(
rbi_for(:Post, compiler_options: { ActiveRecordColumnTypes: "non_existent_wrong_option_value" }),
expected,
)

assert_equal(1, generated_errors.size)
assert_equal(<<~MSG.strip, generated_errors.first)
Unknown value for compiler option `ActiveRecordColumnTypes` given: `non_existent_wrong_option_value`.
Proceeding with the default value: `persisted`.
MSG
end
end
end
Expand Down

0 comments on commit d3c6028

Please sign in to comment.