more rspec
This commit is contained in:
@@ -107,6 +107,8 @@ class E621Post < Post
|
||||
belongs_to :creator, class_name: "E621User", inverse_of: :created_posts
|
||||
end
|
||||
|
||||
puts FaPost.inspect
|
||||
|
||||
fa_user = FaUser.create!(username: "Alice", url_name: "alice")
|
||||
fa_user_id = fa_user.id
|
||||
raise if fa_user.id.nil?
|
||||
|
||||
@@ -8,9 +8,9 @@ require "active_support"
|
||||
require "active_support/concern"
|
||||
require "active_model/attribute_set"
|
||||
|
||||
require_relative "has_aux_table/util"
|
||||
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"
|
||||
|
||||
module HasAuxTable
|
||||
@@ -22,7 +22,6 @@ module HasAuxTable
|
||||
|
||||
module ClassMethods
|
||||
extend T::Sig
|
||||
include QueryExtensions
|
||||
include RelationExtensions
|
||||
|
||||
# Main DSL method for defining auxiliary tables
|
||||
@@ -41,8 +40,9 @@ module HasAuxTable
|
||||
|
||||
@aux_table_configs[aux_table_name] = aux_config =
|
||||
generate_aux_config(aux_table_name)
|
||||
setup_attribute_types_hook!(aux_config)
|
||||
setup_columns_hook!(aux_config)
|
||||
setup_schema_loading_hook!(aux_config)
|
||||
# setup_query_extensions!(self, aux_config, with_bind_attribute: false)
|
||||
setup_relation_extensions!(aux_config)
|
||||
|
||||
aux_config
|
||||
@@ -60,6 +60,42 @@ module HasAuxTable
|
||||
|
||||
private
|
||||
|
||||
sig { params(aux_config: AuxTableConfig).void }
|
||||
def setup_attribute_types_hook!(aux_config)
|
||||
original_method = aux_config.main_class.method(:attribute_types)
|
||||
aux_config
|
||||
.main_class
|
||||
.define_singleton_method(:attribute_types) do
|
||||
@aux_config_attribute_types_cache ||= {}
|
||||
@aux_config_attribute_types_cache[aux_config.table_name] ||= begin
|
||||
original_types = original_method.call.dup
|
||||
|
||||
aux_types =
|
||||
aux_config.model_class.attribute_types.filter do |k, _|
|
||||
aux_config.is_aux_column?(k)
|
||||
end
|
||||
|
||||
original_types.merge!(aux_types)
|
||||
original_types
|
||||
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
|
||||
sig { params(aux_config: AuxTableConfig).void }
|
||||
def setup_schema_loading_hook!(aux_config)
|
||||
@@ -72,13 +108,10 @@ module HasAuxTable
|
||||
result = load_schema_method.call
|
||||
aux_config.load_aux_schema
|
||||
|
||||
# `columns_hash` is populated by `load_schema!` so we can use it to
|
||||
# validate no column overlaps between main table and auxiliary table
|
||||
main_columns_hash = self.columns_hash
|
||||
aux_columns_hash =
|
||||
aux_config.model_class.columns_hash.reject do |col|
|
||||
%w[id created_at updated_at].include?(col) ||
|
||||
col == aux_config.foreign_key.to_s
|
||||
aux_config.model_class.columns_hash.select do |col|
|
||||
aux_config.is_aux_column?(col)
|
||||
end
|
||||
|
||||
main_column_names = main_columns_hash.keys
|
||||
@@ -99,6 +132,7 @@ module HasAuxTable
|
||||
end
|
||||
.to_h
|
||||
|
||||
# set attributes that exist on the aux table to also exist on this table
|
||||
aux_table_filtered_attributes.each do |name, attr|
|
||||
@default_attributes[name] = attr
|
||||
end
|
||||
@@ -210,7 +244,7 @@ module HasAuxTable
|
||||
|
||||
# Get the base class name for the foreign key (e.g., Vehicle -> vehicle_id)
|
||||
# In STI, all subclasses share the same table, so we need the base class
|
||||
foreign_key = "base_table_id".to_sym
|
||||
foreign_key = :base_table_id
|
||||
|
||||
# Get the current class for the association
|
||||
main_class = self
|
||||
@@ -222,7 +256,7 @@ module HasAuxTable
|
||||
Class.new(ActiveRecord::Base) do
|
||||
# Set the table name
|
||||
self.table_name = table_name.to_s
|
||||
self.primary_key = "base_table_id"
|
||||
self.primary_key = foreign_key
|
||||
|
||||
# Define the association back to the specific STI subclass
|
||||
# Foreign key points to base STI table (e.g., vehicle_id)
|
||||
@@ -246,15 +280,9 @@ module HasAuxTable
|
||||
dependent: :destroy
|
||||
)
|
||||
|
||||
# so the aux table is joined against the main table
|
||||
self.default_scope { eager_load(aux_association_name) }
|
||||
|
||||
after_create do
|
||||
aux_model = association(aux_association_name).target
|
||||
aux_model.base_table_id = self.id
|
||||
aux_model.save!
|
||||
true
|
||||
end
|
||||
|
||||
# Set the constant to make the class accessible
|
||||
Object.const_set(aux_class_name, aux_class)
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module HasAuxTable
|
||||
# AuxTable class to store auxiliary table definition
|
||||
class AuxTableConfig < T::Struct
|
||||
extend T::Sig
|
||||
|
||||
@@ -61,7 +60,8 @@ module HasAuxTable
|
||||
).returns(ActiveRecord::Relation)
|
||||
end
|
||||
def apply_split_conditions!(relation, conditions)
|
||||
main_conditions, aux_conditions = self.split_conditions(conditions)
|
||||
main_conditions, aux_conditions =
|
||||
self.partition_by_aux_columns(conditions)
|
||||
relation = relation.where(main_conditions) if main_conditions.any?
|
||||
if aux_conditions.any?
|
||||
relation = relation.where(aux_association_name => aux_conditions)
|
||||
@@ -69,22 +69,13 @@ module HasAuxTable
|
||||
relation
|
||||
end
|
||||
|
||||
sig do
|
||||
params(conditions: T::Hash[String, T.untyped]).returns(
|
||||
[T::Hash[String, T.untyped], T::Hash[String, T.untyped]]
|
||||
)
|
||||
end
|
||||
def split_conditions(conditions)
|
||||
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, aux = partition_by_aux_columns(conditions)
|
||||
main.merge!(aux_association_name => aux) if aux.any?
|
||||
main
|
||||
end
|
||||
@@ -100,25 +91,38 @@ module HasAuxTable
|
||||
aux_model.assign_attributes(aux_args)
|
||||
end
|
||||
|
||||
sig { returns(T::Array[Symbol]) }
|
||||
sig { returns(T::Array[String]) }
|
||||
def aux_column_names
|
||||
@aux_column_names ||=
|
||||
begin
|
||||
rejected_columns = [
|
||||
self.foreign_key,
|
||||
self.primary_key,
|
||||
"created_at",
|
||||
"updated_at"
|
||||
].flatten.map(&:to_s)
|
||||
|
||||
model_class
|
||||
.column_names
|
||||
.reject do |col|
|
||||
[
|
||||
self.foreign_key,
|
||||
:base_table_id,
|
||||
:created_at,
|
||||
:updated_at
|
||||
].flatten.include?(col.to_sym)
|
||||
.reject { |col| rejected_columns.include?(col.to_s) }
|
||||
.map(&:to_s)
|
||||
end
|
||||
.map(&:to_sym)
|
||||
end
|
||||
|
||||
sig { params(name: T.any(Symbol, String)).returns(T::Boolean) }
|
||||
def is_aux_column?(name)
|
||||
aux_column_names.include?(name.to_sym)
|
||||
aux_column_names.include?(name.to_s)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
sig do
|
||||
params(hash: T::Hash[String, T.untyped]).returns(
|
||||
[T::Hash[String, T.untyped], T::Hash[String, T.untyped]]
|
||||
)
|
||||
end
|
||||
def partition_by_aux_columns(hash)
|
||||
hash.partition { |k, _| !self.is_aux_column?(k) }.map(&:to_h)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -1,73 +0,0 @@
|
||||
# typed: false
|
||||
# frozen_string_literal: true
|
||||
|
||||
module HasAuxTable
|
||||
module QueryExtensions
|
||||
extend T::Sig
|
||||
sig do
|
||||
params(
|
||||
on: T.any(ActiveRecord::Relation, T.class_of(ActiveRecord::Base)),
|
||||
aux_config: AuxTableConfig,
|
||||
with_bind_attribute: T::Boolean
|
||||
).void
|
||||
end
|
||||
def setup_query_extensions!(on, aux_config, with_bind_attribute: true)
|
||||
# association_name = aux_config.aux_association_name
|
||||
|
||||
where_method = on.method(:where)
|
||||
on.define_singleton_method(:where) do |args|
|
||||
if args.is_a?(Hash)
|
||||
main_conditions, aux_conditions = aux_config.split_conditions(args)
|
||||
combined_conditions =
|
||||
main_conditions.merge(
|
||||
aux_config.aux_association_name => aux_conditions
|
||||
)
|
||||
where_method.call(combined_conditions)
|
||||
else
|
||||
super(args)
|
||||
end
|
||||
end
|
||||
|
||||
all_method = on.method(:all)
|
||||
on.define_singleton_method(:all) do
|
||||
all_method.call.eager_load(aux_config.aux_association_name)
|
||||
end
|
||||
|
||||
unscoped_method = on.method(:unscoped)
|
||||
on.define_singleton_method(:unscoped) do
|
||||
relation = unscoped_method.call
|
||||
self.setup_query_extensions!(relation, aux_config)
|
||||
relation
|
||||
end
|
||||
|
||||
if with_bind_attribute
|
||||
bind_attribute_method = on.method(:bind_attribute)
|
||||
on.define_singleton_method(:bind_attribute) do |name, value, &block|
|
||||
if aux_config.is_aux_column?(name)
|
||||
aux_config.aux_bind_attribute(name, value, &block)
|
||||
else
|
||||
bind_attribute_method.call(name, value, &block)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
find_by_method = on.method(:find_by)
|
||||
on.define_singleton_method(:find_by) do |args|
|
||||
main_conditions, aux_conditions = aux_config.split_conditions(args)
|
||||
combined_conditions = main_conditions
|
||||
if aux_conditions.any?
|
||||
combined_conditions.merge!(
|
||||
aux_config.aux_association_name => aux_conditions
|
||||
)
|
||||
end
|
||||
find_by_method.call(combined_conditions)
|
||||
end
|
||||
|
||||
on.define_singleton_method(:exists?) do |args|
|
||||
relation = self.select("1").joins(aux_config.aux_association_name)
|
||||
relation = aux_config.apply_split_conditions!(relation, args)
|
||||
relation.first.present?
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -4,36 +4,18 @@
|
||||
module HasAuxTable
|
||||
module RelationExtensions
|
||||
extend T::Sig
|
||||
Util = HasAuxTable::Util
|
||||
|
||||
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|
|
||||
Util.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)
|
||||
@@ -42,7 +24,7 @@ module HasAuxTable
|
||||
end
|
||||
end
|
||||
|
||||
hook_method(
|
||||
Util.hook_method(
|
||||
main_class,
|
||||
:all,
|
||||
false
|
||||
@@ -52,20 +34,24 @@ module HasAuxTable
|
||||
)
|
||||
end
|
||||
|
||||
hook_method(main_class, :unscoped, false) do |original, *args, **kwargs|
|
||||
Util.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|
|
||||
Util.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|
|
||||
Util.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)
|
||||
@@ -74,7 +60,7 @@ module HasAuxTable
|
||||
end
|
||||
end
|
||||
|
||||
hook_method(
|
||||
Util.hook_method(
|
||||
relation_class,
|
||||
:bind_attribute,
|
||||
true
|
||||
|
||||
39
lib/has_aux_table/util.rb
Normal file
39
lib/has_aux_table/util.rb
Normal file
@@ -0,0 +1,39 @@
|
||||
# typed: true
|
||||
# frozen_string_literal: true
|
||||
|
||||
module HasAuxTable
|
||||
module Util
|
||||
extend T::Sig
|
||||
|
||||
sig do
|
||||
params(
|
||||
target:
|
||||
T.any(
|
||||
T.class_of(ActiveRecord::Base),
|
||||
T.class_of(ActiveRecord::Relation)
|
||||
),
|
||||
method_name: Symbol,
|
||||
is_instance_method: T::Boolean,
|
||||
hook_block: T.proc.void
|
||||
).void
|
||||
end
|
||||
def self.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
|
||||
T.unsafe(hook_block).call(method, *args, **kwargs, &block)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -129,6 +129,16 @@ RSpec.describe HasAuxTable do
|
||||
expect(Vehicle.count).to eq(0)
|
||||
end
|
||||
|
||||
describe "column reporting" do
|
||||
it "reports the correct columns on the string repr of the class" do
|
||||
expect(Car.inspect).to include("fuel_type")
|
||||
end
|
||||
|
||||
it "does not include the aux table foreign key" do
|
||||
expect(Car.inspect).not_to include("base_table_id")
|
||||
end
|
||||
end
|
||||
|
||||
describe "database integration" do
|
||||
it "provides automatic attribute accessors for auxiliary table columns" do
|
||||
vehicle = Car.create!(name: "Honda Civic")
|
||||
|
||||
Reference in New Issue
Block a user