more typing
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
PATH
|
PATH
|
||||||
remote: .
|
remote: .
|
||||||
specs:
|
specs:
|
||||||
active-record-aux-table (0.1.0)
|
has_aux_table (0.1.0)
|
||||||
activerecord (>= 7.0)
|
activerecord (>= 7.0)
|
||||||
activesupport (>= 7.0)
|
activesupport (>= 7.0)
|
||||||
sorbet-runtime (~> 0.5)
|
sorbet-runtime (~> 0.5)
|
||||||
@@ -141,9 +141,9 @@ PLATFORMS
|
|||||||
aarch64-linux
|
aarch64-linux
|
||||||
|
|
||||||
DEPENDENCIES
|
DEPENDENCIES
|
||||||
active-record-aux-table!
|
|
||||||
bundler-audit
|
bundler-audit
|
||||||
debug
|
debug
|
||||||
|
has_aux_table!
|
||||||
lefthook
|
lefthook
|
||||||
pry
|
pry
|
||||||
rake (~> 13.0)
|
rake (~> 13.0)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# typed: false
|
# typed: true
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require "sorbet-runtime"
|
require "sorbet-runtime"
|
||||||
@@ -24,6 +24,12 @@ module HasAuxTable
|
|||||||
module ClassMethods
|
module ClassMethods
|
||||||
extend T::Sig
|
extend T::Sig
|
||||||
extend T::Helpers
|
extend T::Helpers
|
||||||
|
requires_ancestor { Kernel }
|
||||||
|
requires_ancestor { T.class_of(BasicObject) }
|
||||||
|
requires_ancestor { T.class_of(ActiveRecord::Base) }
|
||||||
|
requires_ancestor { ActiveRecord::ModelSchema::ClassMethods }
|
||||||
|
requires_ancestor { ActiveRecord::Associations::ClassMethods }
|
||||||
|
|
||||||
include RelationExtensions
|
include RelationExtensions
|
||||||
|
|
||||||
# Main DSL method for defining auxiliary tables
|
# Main DSL method for defining auxiliary tables
|
||||||
@@ -32,34 +38,20 @@ module HasAuxTable
|
|||||||
@aux_table_configs ||=
|
@aux_table_configs ||=
|
||||||
T.let({}, T.nilable(T::Hash[Symbol, AuxTableConfig]))
|
T.let({}, T.nilable(T::Hash[Symbol, AuxTableConfig]))
|
||||||
|
|
||||||
base_table = self.table_name
|
aux_name = aux_name.to_sym
|
||||||
aux_table_name = :"#{base_table}_#{aux_name}_aux"
|
if @aux_table_configs.key?(aux_name)
|
||||||
|
|
||||||
if @aux_table_configs.key?(aux_table_name)
|
|
||||||
Kernel.raise ArgumentError,
|
Kernel.raise ArgumentError,
|
||||||
"Auxiliary '#{aux_name}' (table '#{aux_table_name}') is already defined"
|
"Auxiliary '#{aux_name}' on #{self.name} (table '#{self.table_name}') already exists"
|
||||||
end
|
end
|
||||||
|
|
||||||
@aux_table_configs[aux_table_name] = aux_config =
|
@aux_table_configs[aux_name] = aux_config = generate_aux_config(aux_name)
|
||||||
generate_aux_config(aux_table_name)
|
|
||||||
setup_attribute_types_hook!(aux_config)
|
setup_attribute_types_hook!(aux_config)
|
||||||
setup_columns_hook!(aux_config)
|
|
||||||
setup_schema_loading_hook!(aux_config)
|
setup_schema_loading_hook!(aux_config)
|
||||||
setup_relation_extensions!(aux_config)
|
setup_relation_extensions!(aux_config)
|
||||||
|
|
||||||
aux_config
|
aux_config
|
||||||
end
|
end
|
||||||
|
|
||||||
# Helper method to get auxiliary table configuration
|
|
||||||
sig do
|
|
||||||
params(table_name: T.any(String, Symbol)).returns(
|
|
||||||
T.nilable(AuxTableConfig)
|
|
||||||
)
|
|
||||||
end
|
|
||||||
def aux_table_configuration(table_name)
|
|
||||||
@aux_table_configs[table_name.to_sym]
|
|
||||||
end
|
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
sig { params(aux_config: AuxTableConfig).void }
|
sig { params(aux_config: AuxTableConfig).void }
|
||||||
@@ -69,7 +61,7 @@ module HasAuxTable
|
|||||||
.main_class
|
.main_class
|
||||||
.define_singleton_method(:attribute_types) do
|
.define_singleton_method(:attribute_types) do
|
||||||
@aux_config_attribute_types_cache ||= {}
|
@aux_config_attribute_types_cache ||= {}
|
||||||
@aux_config_attribute_types_cache[aux_config.table_name] ||= begin
|
@aux_config_attribute_types_cache[aux_config.aux_table_name] ||= begin
|
||||||
original_types = original_method.call.dup
|
original_types = original_method.call.dup
|
||||||
|
|
||||||
aux_types =
|
aux_types =
|
||||||
@@ -83,29 +75,19 @@ module HasAuxTable
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
sig { params(aux_config: AuxTableConfig).void }
|
|
||||||
def setup_columns_hook!(aux_config)
|
|
||||||
# original_method = aux_config.main_class.method(:columns)
|
|
||||||
# aux_config
|
|
||||||
# .main_class
|
|
||||||
# .define_singleton_method(:columns) do
|
|
||||||
# original_columns = original_method.call
|
|
||||||
# aux_columns =
|
|
||||||
# aux_config.model_class.columns.filter do |col|
|
|
||||||
# aux_config.is_aux_column?(col.name)
|
|
||||||
# end
|
|
||||||
# original_columns + aux_columns
|
|
||||||
# end
|
|
||||||
end
|
|
||||||
|
|
||||||
# Hook into schema loading to generate attribute accessors when schema is loaded
|
# Hook into schema loading to generate attribute accessors when schema is loaded
|
||||||
sig { params(aux_config: AuxTableConfig).void }
|
sig { params(aux_config: AuxTableConfig).void }
|
||||||
def setup_schema_loading_hook!(aux_config)
|
def setup_schema_loading_hook!(aux_config)
|
||||||
aux_table_name = aux_config.table_name
|
aux_table_name = aux_config.aux_table_name
|
||||||
|
|
||||||
# Override load_schema to also generate auxiliary attribute accessors when schema is loaded
|
# Override load_schema to also generate auxiliary attribute accessors when schema is loaded
|
||||||
load_schema_method = self.method(:load_schema!)
|
load_schema_method = self.method(:load_schema!)
|
||||||
self.define_singleton_method(:load_schema!) do
|
self.define_singleton_method(:load_schema!) do
|
||||||
|
T.bind(
|
||||||
|
self,
|
||||||
|
T.all(T.class_of(ActiveRecord::Base), HasAuxTable::ClassMethods)
|
||||||
|
)
|
||||||
|
|
||||||
# first, load the main and aux table schemas like normal
|
# first, load the main and aux table schemas like normal
|
||||||
result = load_schema_method.call
|
result = load_schema_method.call
|
||||||
aux_config.load_aux_schema
|
aux_config.load_aux_schema
|
||||||
@@ -154,6 +136,7 @@ module HasAuxTable
|
|||||||
%i[save save!].each do |method_name|
|
%i[save save!].each do |method_name|
|
||||||
save_method = self.instance_method(method_name)
|
save_method = self.instance_method(method_name)
|
||||||
self.define_method(method_name) do |*args, **kwargs|
|
self.define_method(method_name) do |*args, **kwargs|
|
||||||
|
T.bind(self, ActiveRecord::Base)
|
||||||
result = save_method.bind(self).call(*args, **kwargs)
|
result = save_method.bind(self).call(*args, **kwargs)
|
||||||
result &&=
|
result &&=
|
||||||
self
|
self
|
||||||
@@ -172,17 +155,22 @@ module HasAuxTable
|
|||||||
].each do |method_name|
|
].each do |method_name|
|
||||||
read_attribute_method = self.instance_method(method_name)
|
read_attribute_method = self.instance_method(method_name)
|
||||||
self.define_method(method_name) do |name, *args, **kwargs|
|
self.define_method(method_name) do |name, *args, **kwargs|
|
||||||
|
T.bind(self, ActiveRecord::Base)
|
||||||
if aux_config.is_aux_column?(name)
|
if aux_config.is_aux_column?(name)
|
||||||
target = aux_config.ensure_aux_target(self)
|
target = aux_config.ensure_aux_target(self)
|
||||||
target.send(method_name, name, *args, **kwargs)
|
T.unsafe(target).send(method_name, name, *args, **kwargs)
|
||||||
else
|
else
|
||||||
read_attribute_method.bind(self).call(name, *args, **kwargs)
|
T
|
||||||
|
.unsafe(read_attribute_method)
|
||||||
|
.bind(self)
|
||||||
|
.call(name, *args, **kwargs)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
initialize_method = self.instance_method(:initialize)
|
initialize_method = self.instance_method(:initialize)
|
||||||
self.define_method(:initialize) do |args|
|
self.define_method(:initialize) do |args|
|
||||||
|
T.bind(self, ActiveRecord::Base)
|
||||||
aux_args, main_args =
|
aux_args, main_args =
|
||||||
args.partition { |k, _| aux_config.is_aux_column?(k) }.map(&:to_h)
|
args.partition { |k, _| aux_config.is_aux_column?(k) }.map(&:to_h)
|
||||||
initialize_method.bind(self).call(main_args)
|
initialize_method.bind(self).call(main_args)
|
||||||
@@ -190,6 +178,7 @@ module HasAuxTable
|
|||||||
end
|
end
|
||||||
|
|
||||||
self.define_method(:reload) do |*args|
|
self.define_method(:reload) do |*args|
|
||||||
|
T.bind(self, ActiveRecord::Base)
|
||||||
aux_model = aux_config.ensure_aux_target(self)
|
aux_model = aux_config.ensure_aux_target(self)
|
||||||
fresh_model = self.class.find(id)
|
fresh_model = self.class.find(id)
|
||||||
@attributes = fresh_model.instance_variable_get(:@attributes)
|
@attributes = fresh_model.instance_variable_get(:@attributes)
|
||||||
@@ -235,20 +224,25 @@ module HasAuxTable
|
|||||||
# Generate auxiliary model class dynamically
|
# Generate auxiliary model class dynamically
|
||||||
sig do
|
sig do
|
||||||
params(
|
params(
|
||||||
table_name: Symbol,
|
aux_name: Symbol,
|
||||||
foreign_key: KeyType,
|
foreign_key: KeyType,
|
||||||
primary_key: KeyType
|
primary_key: KeyType
|
||||||
).returns(AuxTableConfig)
|
).returns(AuxTableConfig)
|
||||||
end
|
end
|
||||||
def generate_aux_config(
|
def generate_aux_config(
|
||||||
table_name,
|
aux_name,
|
||||||
# The column on the aux table that points to the main table
|
# The column on the aux table that points to the main table
|
||||||
foreign_key: :base_table_id,
|
foreign_key: :base_table_id,
|
||||||
primary_key: self.primary_key
|
primary_key: self.primary_key
|
||||||
)
|
)
|
||||||
|
T.bind(self, T.all(T.class_of(ActiveRecord::Base), Class))
|
||||||
|
|
||||||
|
base_table = self.table_name
|
||||||
|
aux_table_name = :"#{base_table}_#{aux_name}_aux"
|
||||||
|
|
||||||
# Generate class name (e.g., :car_aux => "CarAux")
|
# Generate class name (e.g., :car_aux => "CarAux")
|
||||||
aux_class_name = table_name.to_s.camelize
|
aux_class_name = aux_table_name.to_s.camelize
|
||||||
aux_association_name = table_name.to_s.singularize.to_sym
|
aux_association_name = aux_table_name.to_s.singularize.to_sym
|
||||||
|
|
||||||
# Ensure the class name doesn't conflict with existing constants
|
# Ensure the class name doesn't conflict with existing constants
|
||||||
if Object.const_defined?(aux_class_name)
|
if Object.const_defined?(aux_class_name)
|
||||||
@@ -257,12 +251,12 @@ module HasAuxTable
|
|||||||
|
|
||||||
# Get the current class for the association
|
# Get the current class for the association
|
||||||
main_class = self
|
main_class = self
|
||||||
main_association_name = main_class.name.underscore.to_sym
|
main_association_name = T.must(main_class.name&.underscore&.to_sym)
|
||||||
|
|
||||||
# Create the auxiliary model class
|
# Create the auxiliary model class
|
||||||
aux_class =
|
aux_class =
|
||||||
Class.new(ActiveRecord::Base) do
|
Class.new(ActiveRecord::Base) do
|
||||||
self.table_name = table_name.to_s
|
self.table_name = aux_table_name.to_s
|
||||||
self.primary_key = foreign_key
|
self.primary_key = foreign_key
|
||||||
|
|
||||||
# Define the association back to the specific STI subclass
|
# Define the association back to the specific STI subclass
|
||||||
@@ -294,7 +288,7 @@ module HasAuxTable
|
|||||||
Object.const_set(aux_class_name, aux_class)
|
Object.const_set(aux_class_name, aux_class)
|
||||||
|
|
||||||
AuxTableConfig.new(
|
AuxTableConfig.new(
|
||||||
table_name:,
|
aux_table_name:,
|
||||||
model_class: aux_class,
|
model_class: aux_class,
|
||||||
main_class:,
|
main_class:,
|
||||||
aux_association_name:,
|
aux_association_name:,
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ module HasAuxTable
|
|||||||
class AuxTableConfig < T::Struct
|
class AuxTableConfig < T::Struct
|
||||||
extend T::Sig
|
extend T::Sig
|
||||||
|
|
||||||
const :table_name, Symbol
|
const :aux_table_name, Symbol
|
||||||
const :aux_association_name, Symbol
|
const :aux_association_name, Symbol
|
||||||
const :main_association_name, Symbol
|
const :main_association_name, Symbol
|
||||||
const :main_class, T.class_of(ActiveRecord::Base)
|
const :main_class, T.class_of(ActiveRecord::Base)
|
||||||
@@ -47,7 +47,12 @@ module HasAuxTable
|
|||||||
|
|
||||||
sig do
|
sig do
|
||||||
params(
|
params(
|
||||||
main_class: T.class_of(ActiveRecord::Base),
|
main_class:
|
||||||
|
T.all(
|
||||||
|
T.class_of(ActiveRecord::Base),
|
||||||
|
ActiveRecord::Associations::ClassMethods,
|
||||||
|
Module
|
||||||
|
),
|
||||||
method_name: Symbol
|
method_name: Symbol
|
||||||
).void
|
).void
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# typed: false
|
# typed: true
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module HasAuxTable
|
module HasAuxTable
|
||||||
@@ -21,6 +21,10 @@ module HasAuxTable
|
|||||||
end
|
end
|
||||||
|
|
||||||
module MigrationExtensions
|
module MigrationExtensions
|
||||||
|
extend T::Sig
|
||||||
|
extend T::Helpers
|
||||||
|
requires_ancestor { ActiveRecord::Migration }
|
||||||
|
|
||||||
def create_base_table(name, type: :string, **options)
|
def create_base_table(name, type: :string, **options)
|
||||||
create_table(name, **options) do |t|
|
create_table(name, **options) do |t|
|
||||||
t.column :type, type, null: false
|
t.column :type, type, null: false
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# typed: false
|
# typed: true
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
module HasAuxTable
|
module HasAuxTable
|
||||||
@@ -44,19 +44,15 @@ module HasAuxTable
|
|||||||
)
|
)
|
||||||
end
|
end
|
||||||
|
|
||||||
Util.hook_method(main_class, :find, false) do |original, arg|
|
|
||||||
original.call(arg)
|
|
||||||
end
|
|
||||||
|
|
||||||
relation_class =
|
relation_class =
|
||||||
main_class.relation_delegate_class(ActiveRecord::Relation)
|
main_class.relation_delegate_class(ActiveRecord::Relation)
|
||||||
|
|
||||||
Util.hook_method(relation_class, :where!, true) do |original, opts, *rest|
|
Util.hook_method(relation_class, :where!, true) do |original, opts, *rest|
|
||||||
if opts.is_a?(Hash)
|
if opts.is_a?(Hash)
|
||||||
opts_remapped = aux_config.remap_conditions(opts)
|
opts_remapped = aux_config.remap_conditions(opts)
|
||||||
original.call(opts_remapped, *rest)
|
T.unsafe(original).call(opts_remapped, *rest)
|
||||||
else
|
else
|
||||||
original.call(opts, *rest)
|
T.unsafe(original).call(opts, *rest)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ module HasAuxTable
|
|||||||
),
|
),
|
||||||
method_name: Symbol,
|
method_name: Symbol,
|
||||||
is_instance_method: T::Boolean,
|
is_instance_method: T::Boolean,
|
||||||
hook_block: T.proc.void
|
hook_block: T.proc.params(args: T.untyped).void
|
||||||
).void
|
).void
|
||||||
end
|
end
|
||||||
def self.hook_method(target, method_name, is_instance_method, &hook_block)
|
def self.hook_method(target, method_name, is_instance_method, &hook_block)
|
||||||
@@ -35,5 +35,30 @@ module HasAuxTable
|
|||||||
T.unsafe(hook_block).call(method, *args, **kwargs, &block)
|
T.unsafe(hook_block).call(method, *args, **kwargs, &block)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# sig do
|
||||||
|
# type_parameters(:T)
|
||||||
|
# .params(
|
||||||
|
# obj: T::Class[T.type_parameter(:T)],
|
||||||
|
# name: Symbol,
|
||||||
|
# block: T.proc.bind(:T).void
|
||||||
|
# )
|
||||||
|
# .returns(Symbol)
|
||||||
|
# end
|
||||||
|
# def self.safe_define_method(obj, name, &block)
|
||||||
|
# end
|
||||||
|
|
||||||
|
# sig do
|
||||||
|
# type_parameters(:T)
|
||||||
|
# .params(
|
||||||
|
# obj: T::Class[T.type_parameter(:T)],
|
||||||
|
# name: Symbol,
|
||||||
|
# block: T.proc.bind(T.attached_class).void
|
||||||
|
# )
|
||||||
|
# .returns(Symbol)
|
||||||
|
# end
|
||||||
|
# def self.safe_define_singleton_method(obj, name, &block)
|
||||||
|
# Module.safe_define_singleton_method(name, &block)
|
||||||
|
# end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -2,3 +2,4 @@
|
|||||||
.
|
.
|
||||||
--ignore=/tmp/
|
--ignore=/tmp/
|
||||||
--ignore=/vendor/bundle
|
--ignore=/vendor/bundle
|
||||||
|
--enable-experimental-requires-ancestor
|
||||||
@@ -661,7 +661,7 @@ RSpec.describe HasAuxTable do
|
|||||||
|
|
||||||
it "can be destroyed through the association" do
|
it "can be destroyed through the association" do
|
||||||
driver = @car.drivers.create!(name: "John Doe")
|
driver = @car.drivers.create!(name: "John Doe")
|
||||||
expect { driver.destroy }.to change { @car.drivers.count }.by(-1)
|
expect { driver.destroy }.to change { @car.reload.drivers.count }.by(-1)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user