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 Jan 31, 2023
1 parent 447ecbe commit 37d8dab
Show file tree
Hide file tree
Showing 33 changed files with 369 additions and 42 deletions.
60 changes: 38 additions & 22 deletions lib/sorbet-rails/model_plugins/active_record_attribute.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# typed: strict

require ('sorbet-rails/model_plugins/base')

class SorbetRails::ModelPlugins::ActiveRecordAttribute < SorbetRails::ModelPlugins::Base

sig { override.params(root: Parlour::RbiGenerator::Namespace).void }
Expand All @@ -12,28 +14,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 = T.unsafe(@model_class).attribute_aliases
aliases_hash = model_class_columns_to_aliases(model_defined_aliases)
attributes_and_aliases_hash = model_class_attributes_and_aliases(columns_hash, aliases_hash)

columns_hash.sort.each do |column_name, column_def|
if model_defined_enums.has_key?(column_name)
attributes_and_aliases_hash.each do |attribute_name, column_def|
if model_defined_enums.key?(attribute_name) ||
((column_name = model_defined_aliases[attribute_name]).present? && model_defined_enums.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)
elsif serialization_coder_for_column(attribute_name) ||
((column_name = model_defined_aliases[attribute_name]).present? &&
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 +54,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 +107,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 +128,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,30 +13,35 @@ def generate(root)
model_class_rbi = root.create_class(self.model_class_name)
model_class_rbi.create_include(serialize_module_name)

model_defined_aliases = T.unsafe(@model_class).attribute_aliases
aliases_hash = model_class_columns_to_aliases(model_defined_aliases)

columns_hash.sort.each do |column_name, column_def|
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,
return_type: ColumnType.new(base_type: attr_type, nilable: nilable).to_s,
)
[column_name].union(aliases_hash[column_name] || []).each do |attribute_name|
serialize_module_rbi.create_method(
attribute_name.to_s,
return_type: ColumnType.new(base_type: attr_type, nilable: nilable).to_s,
)

serialize_module_rbi.create_method(
"#{column_name}=",
parameters: [
Parameter.new('value', type: ColumnType.new(base_type: attr_type, nilable: nilable).to_s)
],
return_type: nil,
)
serialize_module_rbi.create_method(
"#{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}?",
return_type: 'T::Boolean',
)
serialize_module_rbi.create_method(
"#{attribute_name}?",
return_type: 'T::Boolean',
)
end
end
end

Expand Down
27 changes: 27 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,30 @@ def serialization_coder_for_column(column_name)
Object
end
end

sig { params(aliases: T::Hash[String, String]).returns(T::Hash[String, T::Array[String]]) }
def model_class_columns_to_aliases(aliases)
aliases.each_with_object({}) do |(column_alias, column), hash|
hash[column] ||= []
hash[column] << column_alias
end
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
.sort
.each_with_object({}) do |(column_name, column_def), hash|
attributes = [column_name].union(aliases_hash[column_name] || [])
attributes.each do |attribute_name|
hash[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
3 changes: 3 additions & 0 deletions spec/support/v5.2/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/v5.2/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
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
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
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
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
2 changes: 2 additions & 0 deletions spec/support/v6.1/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
12 changes: 11 additions & 1 deletion spec/support/v6.1/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 @@ -163,7 +174,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
Loading

0 comments on commit 37d8dab

Please sign in to comment.