Skip to content

Commit 99f1d8b

Browse files
authored
Merge pull request #191 from kbrock/relation_delegate
Introduce delegation to virtual_has_many
2 parents 26640bb + e7f9ab9 commit 99f1d8b

File tree

9 files changed

+84
-26
lines changed

9 files changed

+84
-26
lines changed

CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ The versioning of this gem follows ActiveRecord versioning, and does not follow
44

55
## [Unreleased]
66

7+
## [7.1.2] - 2025-06-20
8+
9+
* Introduce virtual_has_many :through [#191](https://github.com/ManageIQ/activerecord-virtual_attributes/pull/191)
10+
711
## [7.1.1] - 2025-06-18
812

913
* Deprecate virtual_delegate without a type [#188](https://github.com/ManageIQ/activerecord-virtual_attributes/pull/188)
@@ -115,7 +119,8 @@ The versioning of this gem follows ActiveRecord versioning, and does not follow
115119
* Initial Release
116120
* Extracted from ManageIQ/manageiq
117121

118-
[Unreleased]: https://github.com/ManageIQ/activerecord-virtual_attributes/compare/v7.1.1...HEAD
122+
[Unreleased]: https://github.com/ManageIQ/activerecord-virtual_attributes/compare/v7.1.2...HEAD
123+
[7.1.2]: https://github.com/ManageIQ/activerecord-virtual_attributes/compare/v7.1.1...v7.1.2
119124
[7.1.1]: https://github.com/ManageIQ/activerecord-virtual_attributes/compare/v7.1.0...v7.1.1
120125
[7.1.0]: https://github.com/ManageIQ/activerecord-virtual_attributes/compare/v7.0.0...v7.1.0
121126
[7.0.0]: https://github.com/ManageIQ/activerecord-virtual_attributes/compare/v6.1.2...v7.0.0

lib/active_record/virtual_attributes.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ def load_schema!
102102
end
103103

104104
def define_virtual_attribute(name, cast_type, uses: nil, arel: nil)
105-
attribute_types[name] = cast_type
105+
attribute_types[name.to_s] = cast_type
106106
define_virtual_include(name, uses) if uses
107107
define_virtual_arel(name, arel) if arel
108108
end
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
module ActiveRecord
22
module VirtualAttributes
3-
VERSION = "7.1.1".freeze
3+
VERSION = "7.1.2".freeze
44
end
55
end

lib/active_record/virtual_attributes/virtual_arel.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ def arel_for_virtual_attribute(column_name, table) # :nodoc:
111111
private
112112

113113
def define_virtual_arel(name, arel) # :nodoc:
114-
self._virtual_arel = _virtual_arel.merge(name => arel)
114+
self._virtual_arel = _virtual_arel.merge(name.to_s => arel)
115115
end
116116
end
117117
end

lib/active_record/virtual_attributes/virtual_delegates.rb

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,8 @@ def virtual_delegate(*methods, to:, type: nil, prefix: nil, allow_nil: nil, defa
2424
end
2525

2626
to = to.to_s
27-
if to.include?(".") && methods.size > 1
28-
raise ArgumentError, 'Delegation only supports specifying a method name when defining a single virtual method'
27+
if to.include?(".") && (methods.size > 1 || prefix)
28+
raise ArgumentError, 'Delegation only supports specifying a target method name when defining a single virtual method with no prefix'
2929
end
3030

3131
if to.count(".") > 1
@@ -35,16 +35,11 @@ def virtual_delegate(*methods, to:, type: nil, prefix: nil, allow_nil: nil, defa
3535
# put method entry per method name.
3636
# This better supports reloading of the class and changing the definitions
3737
methods.each do |method|
38-
method_prefix = virtual_delegate_name_prefix(prefix, to)
39-
method_name = "#{method_prefix}#{method}"
40-
if to.include?(".") # to => "target.method"
41-
to, method = to.split(".").map(&:to_sym)
42-
end
43-
38+
method_name, to, method = determine_method_names(method, to, prefix)
4439
define_delegate(method_name, method, :to => to, :allow_nil => allow_nil, :default => default)
4540

4641
self.virtual_delegates_to_define =
47-
virtual_delegates_to_define.merge(method_name => [method, options.merge(:to => to, :type => type)])
42+
virtual_delegates_to_define.merge(method_name.to_s => [method, options.merge(:to => to, :type => type)])
4843
end
4944
end
5045

@@ -122,8 +117,25 @@ def #{method_name}(#{definition})
122117
end
123118
# rubocop:enable Style/TernaryParentheses
124119

125-
def virtual_delegate_name_prefix(prefix, to) # rubocop:disable Naming/MethodParameterName
126-
"#{prefix == true ? to : prefix}_" if prefix
120+
# Sometimes the `to` contains the column name target.column, split it up to the source method_name and target column
121+
# If `to` does specify the column name, `to` becomes the target (i.e.: association)
122+
#
123+
# @param column [Symbol|String] the name of the column
124+
# @param to [Symbol|String]
125+
# @param prefix [Boolean|Nil|Symbol]
126+
# @return [Symbol, Symbol, Symbol] method_name, relation, relation's column name
127+
def determine_method_names(column, to, prefix) # rubocop:disable Naming/MethodParameterName
128+
method_name = column = column.to_sym
129+
130+
tos = to.to_s
131+
if tos.include?(".") # to => "target.method"
132+
to, column = tos.split(".").map(&:to_sym)
133+
end
134+
135+
method_prefix = "#{prefix == true ? to : prefix}_" if prefix
136+
method_name = "#{method_prefix}#{method_name}".to_sym
137+
138+
[method_name, to.to_sym, column]
127139
end
128140

129141
# @param col [String] attribute name

lib/active_record/virtual_attributes/virtual_includes.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ def virtual_includes(name)
2525
private
2626

2727
def define_virtual_include(name, uses)
28-
self._virtual_includes = _virtual_includes.merge(name => uses)
28+
self._virtual_includes = _virtual_includes.merge(name.to_s => uses)
2929
end
3030
end
3131
end

lib/active_record/virtual_attributes/virtual_reflections.rb

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,26 +9,24 @@ module ClassMethods
99
# Definition
1010
#
1111

12-
def virtual_has_one(name, options = {})
13-
uses = options.delete(:uses)
12+
def virtual_has_one(name, uses: nil, **options)
1413
reflection = ActiveRecord::Associations::Builder::HasOne.build(self, name, nil, options)
15-
add_virtual_reflection(reflection, name, uses, options)
14+
add_virtual_reflection(reflection, name, uses)
1615
end
1716

18-
def virtual_has_many(name, options = {})
17+
def virtual_has_many(name, uses: nil, source: nil, through: nil, **options)
1918
define_method(:"#{name.to_s.singularize}_ids") do
2019
records = send(name)
2120
records.respond_to?(:ids) ? records.ids : records.collect(&:id)
2221
end
23-
uses = options.delete(:uses)
22+
define_delegate(name, source || name, :to => through, :allow_nil => true, :default => []) if through
2423
reflection = ActiveRecord::Associations::Builder::HasMany.build(self, name, nil, options)
25-
add_virtual_reflection(reflection, name, uses, options)
24+
add_virtual_reflection(reflection, name, uses)
2625
end
2726

28-
def virtual_belongs_to(name, options = {})
29-
uses = options.delete(:uses)
27+
def virtual_belongs_to(name, uses: nil, **options)
3028
reflection = ActiveRecord::Associations::Builder::BelongsTo.build(self, name, nil, options)
31-
add_virtual_reflection(reflection, name, uses, options)
29+
add_virtual_reflection(reflection, name, uses)
3230
end
3331

3432
def virtual_reflection?(name)
@@ -94,7 +92,7 @@ def collect_reflections_with_virtual(association_names)
9492

9593
private
9694

97-
def add_virtual_reflection(reflection, name, uses, _options)
95+
def add_virtual_reflection(reflection, name, uses)
9896
raise ArgumentError, "macro must be specified" unless reflection
9997

10098
reset_virtual_reflection_information

spec/db/models.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,10 @@ class Book < VirtualTotalTestBase
135135
has_many :photos, :as => :imageable, :class_name => "Photo"
136136
has_one :current_photo, -> { all.merge(Photo.order(:id => :desc)) }, :as => :imageable, :class_name => "Photo"
137137

138+
virtual_has_many :author_books, :through => :author, :source => :books
139+
# sorry. books.books doesn't totally make sense.
140+
virtual_has_many :books, :through => :author
141+
138142
scope :ordered, -> { order(:created_on => :desc) }
139143
scope :published, -> { where(:published => true) }
140144
scope :wip, -> { where(:published => false) }

spec/virtual_delegates_spec.rb

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,4 +299,43 @@ def self.connection
299299
expect(actual).to eq(author)
300300
end
301301
end
302+
303+
describe "virtual_has_many with :through" do
304+
it "with :source works" do
305+
Author.create_with_books(2)
306+
author = Author.create_with_books(3)
307+
books = author.books.order(:id)
308+
309+
expect(books.first.author_books.order(:id)).to eq(books)
310+
end
311+
312+
it "without :source works" do
313+
Author.create_with_books(2)
314+
author = Author.create_with_books(3)
315+
books = author.books.order(:id)
316+
317+
expect(books.first.books.order(:id)).to eq(books)
318+
end
319+
end
320+
321+
describe "#determine_method_names (private)" do
322+
it "works with column and to" do
323+
expect(determine_method_names("column", "relation", nil)).to eq([:column, :relation, :column])
324+
expect(determine_method_names("column", "relation", true)).to eq([:relation_column, :relation, :column])
325+
expect(determine_method_names("column", "relation", "pre")).to eq([:pre_column, :relation, :column])
326+
expect(determine_method_names("column", "relation.column2", false)).to eq([:column, :relation, :column2])
327+
expect(determine_method_names("column", "relation.column2", true)).to eq([:relation_column, :relation, :column2])
328+
329+
TestClass.virtual_delegate :str, :to => :ref1, :prefix => true, :type => :string
330+
expect(TestClass.new.respond_to?(:ref1_str)).to eq(true)
331+
332+
expect do
333+
TestClass.virtual_delegate :my_method, :to => "ref1.str", :prefix => true, :type => :string
334+
end.to raise_exception(ArgumentError)
335+
end
336+
end
337+
338+
def determine_method_names(*args)
339+
TestClass.send(:determine_method_names, *args)
340+
end
302341
end

0 commit comments

Comments
 (0)