Skip to content
This repository has been archived by the owner on Jul 5, 2023. It is now read-only.

Commit

Permalink
Add support for alias_attribute
Browse files Browse the repository at this point in the history
  • Loading branch information
joshuay03 committed May 27, 2023
1 parent 447ecbe commit 5817096
Show file tree
Hide file tree
Showing 44 changed files with 383 additions and 44 deletions.
54 changes: 34 additions & 20 deletions lib/sorbet-rails/model_plugins/active_record_attribute.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,28 +12,38 @@ def generate(root)

model_class_rbi = root.create_class(self.model_class_name)
model_class_rbi.create_include(attribute_module_name)

model_defined_enums = @model_class.defined_enums
model_defined_aliases = @model_class.send(:attribute_aliases)
column_names = columns_hash.keys
aliases_hash = model_class_columns_to_aliases(column_names, model_defined_aliases)
attributes_and_aliases_hash = model_class_attributes_and_aliases(columns_hash, aliases_hash)

attributes_and_aliases_hash.sort.each do |attribute_name, column_def|
column_name = column_def.name

columns_hash.sort.each do |column_name, column_def|
if model_defined_enums.has_key?(column_name)
generate_enum_methods(
root,
model_class_rbi,
attribute_module_rbi,
model_defined_enums,
column_name,
column_def,
root: root,
model_class_rbi: model_class_rbi,
attribute_module_rbi: attribute_module_rbi,
model_defined_enums: model_defined_enums,
attribute_name: attribute_name,
column_name: column_name.presence || attribute_name,
column_def: column_def,
)
elsif serialization_coder_for_column(column_name)
next # handled by the ActiveRecordSerializedAttribute plugin
else
column_type = type_for_column_def(column_def)

attribute_module_rbi.create_method(
column_name.to_s,
attribute_name.to_s,
return_type: column_type.to_s,
)

attribute_module_rbi.create_method(
"#{column_name}=",
"#{attribute_name}=",
parameters: [
Parameter.new("value", type: value_type_for_attr_writer(column_type))
],
Expand All @@ -42,29 +52,33 @@ def generate(root)
end

attribute_module_rbi.create_method(
"#{column_name}?",
"#{attribute_name}?",
return_type: "T::Boolean",
)
end
end

private

sig {
params(
root: Parlour::RbiGenerator::Namespace,
model_class_rbi: Parlour::RbiGenerator::Namespace,
attribute_module_rbi: Parlour::RbiGenerator::Namespace,
model_defined_enums: T::Hash[String, T::Hash[String, T.untyped]],
attribute_name: String,
column_name: String,
column_def: T.untyped,
).void
}
def generate_enum_methods(
root,
model_class_rbi,
attribute_module_rbi,
model_defined_enums,
column_name,
column_def
root:,
model_class_rbi:,
attribute_module_rbi:,
model_defined_enums:,
attribute_name:,
column_name:,
column_def:
)
should_skip_setter_getter = false
nilable_column = nilable_column?(column_def)
Expand All @@ -91,11 +105,11 @@ def generate_enum_methods(
# add directly to model_class_rbi because they are included
# by sorbet's hidden.rbi
model_class_rbi.create_method(
"typed_#{column_name}",
"typed_#{attribute_name}",
return_type: assignable_type,
)
model_class_rbi.create_method(
"typed_#{column_name}=",
"typed_#{attribute_name}=",
parameters: [
Parameter.new("value", type: assignable_type)
],
Expand All @@ -112,11 +126,11 @@ def generate_enum_methods(
return_type = "T.nilable(#{return_type})" if nilable_column

attribute_module_rbi.create_method(
column_name.to_s,
attribute_name.to_s,
return_type: return_type,
)
attribute_module_rbi.create_method(
"#{column_name}=",
"#{attribute_name}=",
parameters: [
Parameter.new("value", type: assignable_type)
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,28 +13,34 @@ def generate(root)
model_class_rbi = root.create_class(self.model_class_name)
model_class_rbi.create_include(serialize_module_name)

columns_hash.sort.each do |column_name, column_def|
model_defined_aliases = @model_class.send(:attribute_aliases)
column_names = columns_hash.keys
aliases_hash = model_class_columns_to_aliases(column_names, model_defined_aliases)
attributes_and_aliases_hash = model_class_attributes_and_aliases(columns_hash, aliases_hash)

attributes_and_aliases_hash.sort.each do |attribute_name, column_def|
column_name = column_def.name || attribute_name
serialization_coder = serialization_coder_for_column(column_name)
next unless serialization_coder

nilable = nilable_column?(column_def)
attr_type = attr_types_for_coder(serialization_coder)

serialize_module_rbi.create_method(
column_name.to_s,
attribute_name.to_s,
return_type: ColumnType.new(base_type: attr_type, nilable: nilable).to_s,
)

serialize_module_rbi.create_method(
"#{column_name}=",
"#{attribute_name}=",
parameters: [
Parameter.new('value', type: ColumnType.new(base_type: attr_type, nilable: nilable).to_s)
],
return_type: nil,
)

serialize_module_rbi.create_method(
"#{column_name}?",
"#{attribute_name}?",
return_type: 'T::Boolean',
)
end
Expand Down
40 changes: 40 additions & 0 deletions lib/sorbet-rails/model_plugins/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ def initialize(model_class, available_classes)
@available_classes = T.let(available_classes, T::Set[String])
end

private

sig { params(column_name: String).returns(T.nilable(Class)) }
def serialization_coder_for_column(column_name)
column_type = @model_class.type_for_attribute(column_name)
Expand All @@ -44,5 +46,43 @@ def serialization_coder_for_column(column_name)
Object
end
end

sig {
params(column_names: T::Array[String], aliases: T::Hash[String, String])
.returns(T::Hash[String, T::Array[String]])
}
def model_class_columns_to_aliases(column_names, aliases)
column_names.each_with_object({}) do |column_name, columns_to_aliases|
direct_aliases = aliases.select do |_alias_name, attribute_name|
attribute_name == column_name
end.keys
all_aliases = direct_aliases.flat_map do |direct_alias_name|
recursive_keys_for_value(aliases, direct_alias_name).push(direct_alias_name)
end

columns_to_aliases[column_name] = all_aliases unless all_aliases.empty?
end
end

sig { params(hash: T::Hash[String, String], initial_value: String).returns(T::Array[String]) }
def recursive_keys_for_value(hash, initial_value)
initial_keys = hash.select { |_key, value| value == initial_value }.keys
initial_keys.flat_map { |key| [key].concat(recursive_keys_for_value(hash, key)) }
end

sig {
params(
columns_hash: T::Hash[String, T.untyped],
aliases_hash: T::Hash[String, T::Array[String]],
).returns(T::Hash[String, T.untyped])
}
def model_class_attributes_and_aliases(columns_hash, aliases_hash)
columns_hash
.each_with_object({}) do |(column_name, column_def), attributes_and_aliases|
[column_name].concat(aliases_hash[column_name] || []).each do |attribute_name|
attributes_and_aliases[attribute_name] = column_def
end
end
end
end
end
5 changes: 5 additions & 0 deletions spec/generators/rails-template.rb
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@ class SpellBook < ApplicationRecord
dark_art: 999,
}
alias_attribute :title, :name
alias_attribute :book_category, :book_type
scope :recent, -> { where('created_at > ?', 1.month.ago) }
end
RUBY
Expand Down Expand Up @@ -179,6 +182,8 @@ class Professor; end
serialize :pets, Array
serialize :patronus_characteristics, JSON
alias_attribute :ordinary_wizarding_level_results, :owl_results
has_one :wand
has_many :spell_books
# habtm which is optional at the db level
Expand Down
12 changes: 11 additions & 1 deletion spec/generators/sorbet_test_cases.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@
# -- model columns
T.assert_type!(wizard.name, T.nilable(String))

spell_book = wizard.spell_books.first!
T.assert_type!(spell_book, SpellBook)

# -- alias attributes
T.assert_type!(spell_book.title, String) # standard alias attribute
T.assert_type!(spell_book.book_category, String) # enum alias attribute
T.assert_type!(
wizard.ordinary_wizarding_level_results, # serialized alias attribute
T.nilable(T::Hash[T.untyped, T.untyped])
)

# -- time/date columns
T.assert_type!(wizard.created_at, ActiveSupport::TimeWithZone)
T.assert_type!(wand.broken_at, T.nilable(Time))
Expand Down Expand Up @@ -157,7 +168,6 @@
T.assert_type!(Wizard.all.empty?, T::Boolean)

# Finder methods -- CollectionProxy
spell_book = wizard.spell_books.first!
spell_books = wizard.spell_books
T.assert_type!(spell_books.exists?(name: 'Fantastic Beasts'), T::Boolean)
T.assert_type!(spell_books.find(spell_book.id), SpellBook)
Expand Down
5 changes: 4 additions & 1 deletion spec/support/v5.2/app/models/spell_book.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# typed: false
# typed: strict
class SpellBook < ApplicationRecord
validates :name, length: { minimum: 5 }, presence: true

Expand All @@ -14,5 +14,8 @@ class SpellBook < ApplicationRecord
dark_art: 999,
}

alias_attribute :title, :name
alias_attribute :book_category, :book_type

scope :recent, -> { where('created_at > ?', 1.month.ago) }
end
2 changes: 1 addition & 1 deletion spec/support/v5.2/app/models/wand.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# typed: false
# typed: true
class Wand < ApplicationRecord
include Mythical

Expand Down
4 changes: 3 additions & 1 deletion spec/support/v5.2/app/models/wizard.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# typed: false
# typed: strict
class Wizard < ApplicationRecord
validates :name, length: { minimum: 5 }, presence: true
# simulate conditional validation
Expand Down Expand Up @@ -52,6 +52,8 @@ class Professor; end
serialize :pets, Array
serialize :patronus_characteristics, JSON

alias_attribute :ordinary_wizarding_level_results, :owl_results

has_one :wand
has_many :spell_books
# habtm which is optional at the db level
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# typed: strict
# typed: false
# Be sure to restart your server when you modify this file.

# Specify a serializer for the signed and encrypted cookie jars.
Expand Down
12 changes: 11 additions & 1 deletion spec/support/v5.2/sorbet_test_cases.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@
# -- model columns
T.assert_type!(wizard.name, T.nilable(String))

spell_book = wizard.spell_books.first!
T.assert_type!(spell_book, SpellBook)

# -- alias attributes
T.assert_type!(spell_book.title, String) # standard alias attribute
T.assert_type!(spell_book.book_category, String) # enum alias attribute
T.assert_type!(
wizard.ordinary_wizarding_level_results, # serialized alias attribute
T.nilable(T::Hash[T.untyped, T.untyped])
)

# -- time/date columns
T.assert_type!(wizard.created_at, ActiveSupport::TimeWithZone)
T.assert_type!(wand.broken_at, T.nilable(Time))
Expand Down Expand Up @@ -161,7 +172,6 @@
T.assert_type!(Wizard.all.empty?, T::Boolean)

# Finder methods -- CollectionProxy
spell_book = wizard.spell_books.first!
spell_books = wizard.spell_books
T.assert_type!(spell_books.exists?(name: 'Fantastic Beasts'), T::Boolean)
T.assert_type!(spell_books.find(spell_book.id), SpellBook)
Expand Down
2 changes: 1 addition & 1 deletion spec/support/v6.0/app/mailers/application_mailer.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# typed: false
# typed: strict
class ApplicationMailer < ActionMailer::Base
default from: 'from@example.com'
layout 'mailer'
Expand Down
3 changes: 3 additions & 0 deletions spec/support/v6.0/app/models/spell_book.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,8 @@ class SpellBook < ApplicationRecord
dark_art: 999,
}

alias_attribute :title, :name
alias_attribute :book_category, :book_type

scope :recent, -> { where('created_at > ?', 1.month.ago) }
end
2 changes: 2 additions & 0 deletions spec/support/v6.0/app/models/wizard.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ class Professor; end
serialize :pets, Array
serialize :patronus_characteristics, JSON

alias_attribute :ordinary_wizarding_level_results, :owl_results

has_one :wand
has_many :spell_books
# habtm which is optional at the db level
Expand Down
2 changes: 1 addition & 1 deletion spec/support/v6.0/config/environments/production.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# typed: false
# typed: strict
Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb.

Expand Down
2 changes: 1 addition & 1 deletion spec/support/v6.0/config/initializers/sorbet_rails.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
# typed: false
# typed: strict
require(Rails.root.join('lib/mythical_rbi_plugin'))
SorbetRails::ModelRbiFormatter.register_plugin(MythicalRbiPlugin)
12 changes: 11 additions & 1 deletion spec/support/v6.0/sorbet_test_cases.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,17 @@
# -- model columns
T.assert_type!(wizard.name, T.nilable(String))

spell_book = wizard.spell_books.first!
T.assert_type!(spell_book, SpellBook)

# -- alias attributes
T.assert_type!(spell_book.title, String) # standard alias attribute
T.assert_type!(spell_book.book_category, String) # enum alias attribute
T.assert_type!(
wizard.ordinary_wizarding_level_results, # serialized alias attribute
T.nilable(T::Hash[T.untyped, T.untyped])
)

# -- time/date columns
T.assert_type!(wizard.created_at, ActiveSupport::TimeWithZone)
T.assert_type!(wand.broken_at, T.nilable(Time))
Expand Down Expand Up @@ -161,7 +172,6 @@
T.assert_type!(Wizard.all.empty?, T::Boolean)

# Finder methods -- CollectionProxy
spell_book = wizard.spell_books.first!
spell_books = wizard.spell_books
T.assert_type!(spell_books.exists?(name: 'Fantastic Beasts'), T::Boolean)
T.assert_type!(spell_books.find(spell_book.id), SpellBook)
Expand Down
2 changes: 1 addition & 1 deletion spec/support/v6.1/app/mailers/application_mailer.rb
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# typed: false
# typed: strict
class ApplicationMailer < ActionMailer::Base
default from: 'from@example.com'
layout 'mailer'
Expand Down
3 changes: 3 additions & 0 deletions spec/support/v6.1/app/models/spell_book.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,8 @@ class SpellBook < ApplicationRecord
dark_art: 999,
}

alias_attribute :title, :name
alias_attribute :book_category, :book_type

scope :recent, -> { where('created_at > ?', 1.month.ago) }
end
Loading

0 comments on commit 5817096

Please sign in to comment.