fix specs, use relation hooks

This commit is contained in:
Dylan Knutson
2025-07-15 06:16:01 +00:00
parent fd91328334
commit 239afcbadb
6 changed files with 190 additions and 54 deletions

View File

@@ -8,6 +8,7 @@ require "active_support"
require "active_support/concern"
require "active_model/attribute_set"
require_relative "has_aux_table/relation_extensions"
require_relative "has_aux_table/aux_table_config"
require_relative "has_aux_table/query_extensions"
require_relative "has_aux_table/migration_extensions"
@@ -22,6 +23,7 @@ module HasAuxTable
module ClassMethods
extend T::Sig
include QueryExtensions
include RelationExtensions
# Main DSL method for defining auxiliary tables
sig { params(aux_name: T.any(String, Symbol)).returns(AuxTableConfig) }
@@ -39,8 +41,9 @@ module HasAuxTable
@aux_table_configs[aux_table_name] = aux_config =
generate_aux_config(aux_table_name)
setup_schema_loading_hook!(aux_table_name)
setup_query_extensions!(self, aux_config, with_bind_attribute: false)
setup_schema_loading_hook!(aux_config)
# setup_query_extensions!(self, aux_config, with_bind_attribute: false)
setup_relation_extensions!(aux_config)
aux_config
end
@@ -58,11 +61,9 @@ module HasAuxTable
private
# Hook into schema loading to generate attribute accessors when schema is loaded
sig { params(aux_table_name: Symbol).void }
def setup_schema_loading_hook!(aux_table_name)
aux_config =
@aux_table_configs[aux_table_name] ||
raise("no aux_config for #{aux_table_name}")
sig { params(aux_config: AuxTableConfig).void }
def setup_schema_loading_hook!(aux_config)
aux_table_name = aux_config.table_name
# Override load_schema to also generate auxiliary attribute accessors when schema is loaded
load_schema_method = self.method(:load_schema!)
@@ -127,7 +128,12 @@ module HasAuxTable
end
end
%i[_read_attribute read_attribute write_attribute].each do |method_name|
%i[
_read_attribute
read_attribute
_write_attribute
write_attribute
].each do |method_name|
read_attribute_method = self.instance_method(method_name)
self.define_method(method_name) do |name, *args, **kwargs|
if aux_config.is_aux_column?(name)
@@ -255,6 +261,7 @@ module HasAuxTable
AuxTableConfig.new(
table_name:,
model_class: aux_class,
main_class:,
aux_association_name:,
main_association_name:,
foreign_key:,

View File

@@ -9,6 +9,7 @@ module HasAuxTable
const :table_name, Symbol
const :aux_association_name, Symbol
const :main_association_name, Symbol
const :main_class, T.class_of(ActiveRecord::Base)
const :model_class, T.class_of(ActiveRecord::Base)
const :foreign_key, T.any(Symbol, T::Array[Symbol])
const :primary_key, T.any(Symbol, T::Array[Symbol])
@@ -65,7 +66,6 @@ module HasAuxTable
if aux_conditions.any?
relation = relation.where(aux_association_name => aux_conditions)
end
puts "conditions: #{main_conditions} / #{aux_conditions}"
relation
end
@@ -78,6 +78,17 @@ module HasAuxTable
conditions.partition { |k, _| !self.is_aux_column?(k) }.map(&:to_h)
end
sig do
params(conditions: T::Hash[String, T.untyped]).returns(
T::Hash[String, T.untyped]
)
end
def remap_conditions(conditions)
main, aux = split_conditions(conditions)
main.merge!(aux_association_name => aux) if aux.any?
main
end
sig do
params(
main_model: ActiveRecord::Base,

View File

@@ -4,23 +4,6 @@
module HasAuxTable
module QueryExtensions
extend T::Sig
# Split conditions into main table and aux table conditions
def split_conditions(conditions, aux_config)
main_conditions = {}
aux_conditions = {}
conditions.each do |key, value|
if aux_config.is_aux_column?(key)
aux_conditions[key] = value
else
main_conditions[key] = value
end
end
[main_conditions, aux_conditions]
end
sig do
params(
on: T.any(ActiveRecord::Relation, T.class_of(ActiveRecord::Base)),

View File

@@ -0,0 +1,90 @@
# typed: false
# frozen_string_literal: true
module HasAuxTable
module RelationExtensions
extend T::Sig
sig { params(aux_config: AuxTableConfig).void }
def setup_relation_extensions!(aux_config)
setup_main_class_extensions!(aux_config)
end
def hook_method(target, method_name, is_instance_method, &hook_block)
define_method =
is_instance_method ? :define_method : :define_singleton_method
target_method =
(
if is_instance_method
target.instance_method(method_name)
else
target.method(method_name)
end
)
target.send(define_method, method_name) do |*args, **kwargs, &block|
method = is_instance_method ? target_method.bind(self) : target_method
hook_block.call(method, *args, **kwargs, &block)
end
end
sig { params(aux_config: AuxTableConfig).void }
def setup_main_class_extensions!(aux_config)
main_class = aux_config.main_class
hook_method(main_class, :where, false) do |original, *args|
if args.length == 1 && args.first.is_a?(Hash)
opts_remapped = aux_config.remap_conditions(args.first)
original.call(opts_remapped)
else
original.call(*args)
end
end
hook_method(
main_class,
:all,
false
) do |original, *args, **kwargs, &block|
original.call(*args, **kwargs, &block).eager_load(
aux_config.aux_association_name
)
end
hook_method(main_class, :unscoped, false) do |original, *args, **kwargs|
original.call(*args, **kwargs).eager_load(
aux_config.aux_association_name
)
end
hook_method(main_class, :find, false) do |original, arg|
original.call(arg)
end
relation_class =
main_class.relation_delegate_class(ActiveRecord::Relation)
hook_method(relation_class, :where!, true) do |original, opts, *rest|
if opts.is_a?(Hash)
opts_remapped = aux_config.remap_conditions(opts)
original.call(opts_remapped, *rest)
else
original.call(opts, *rest)
end
end
hook_method(
relation_class,
:bind_attribute,
true
) do |original, name, value, &block|
if aux_config.is_aux_column?(name)
aux_config.aux_bind_attribute(name, value, &block)
else
original.call(name, value, &block)
end
end
end
end
end