Skip to content

Commit dcd4e10

Browse files
authored
Merge pull request rails#50280 from seanpdoyle/delegated_type_inverse_of
Infer default `:inverse_of` option for `delegated_type`
2 parents edd298d + e9ee137 commit dcd4e10

File tree

11 files changed

+79
-11
lines changed

11 files changed

+79
-11
lines changed

activerecord/CHANGELOG.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
1+
* Infer default `:inverse_of` option for `delegated_type` definitions.
2+
3+
```ruby
4+
class Entry < ApplicationRecord
5+
delegated_type :entryable, types: %w[ Message ]
6+
# => defaults to inverse_of: :entry
7+
end
8+
```
9+
10+
*Sean Doyle*
11+
112
* Add support for `ActiveRecord::Point` type casts using `Hash` values
213

314
This allows `ActiveRecord::Point` to be cast or serialized from a hash

activerecord/lib/active_record/delegated_type.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,14 @@ module DelegatedType
220220
# [:primary_key]
221221
# Specify the method that returns the primary key of associated object used for the convenience methods.
222222
# By default this is +id+.
223+
# [+:inverse_of+]
224+
# Specifies the name of the #belongs_to association on the associated object
225+
# that is the inverse of this #has_one association. By default, the class
226+
# singularized class name is used unless a <tt>:foreign_key</tt> option is
227+
# also provided. For example, a call to
228+
# <tt>Entry.delegated_type</tt> will default to <tt>inverse_of: :entry</tt>.
229+
# See ActiveRecord::Associations::ClassMethods's overview on Bi-directional
230+
# associations for more detail.
223231
#
224232
# Option examples:
225233
# class Entry < ApplicationRecord
@@ -229,6 +237,8 @@ module DelegatedType
229237
# Entry#message_uuid # => returns entryable_uuid, when entryable_type == "Message", otherwise nil
230238
# Entry#comment_uuid # => returns entryable_uuid, when entryable_type == "Comment", otherwise nil
231239
def delegated_type(role, types:, **options)
240+
options[:inverse_of] = model_name.singular unless options.key?(:inverse_of) || options.key?(:foreign_key)
241+
232242
belongs_to role, options.delete(:scope), **options.merge(polymorphic: true)
233243
define_delegated_type_methods role, types: types, options: options
234244
end

activerecord/test/cases/delegated_type_test.rb

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
require "cases/helper"
44
require "models/account"
55
require "models/entry"
6+
require "models/essay"
67
require "models/message"
78
require "models/recipient"
89
require "models/comment"
@@ -11,12 +12,12 @@
1112
require "models/uuid_comment"
1213

1314
class DelegatedTypeTest < ActiveRecord::TestCase
14-
fixtures :comments, :accounts, :posts
15+
fixtures :comments, :accounts, :essays
1516

1617
setup do
1718
@entry_with_message = Entry.create! entryable: Message.new(subject: "Hello world!"), account: accounts(:signals37)
1819
@entry_with_comment = Entry.create! entryable: comments(:greetings), account: accounts(:signals37)
19-
@entry_with_post = Entry.create! thing: posts(:welcome), account: accounts(:signals37)
20+
@entry_with_essay = Entry.create! thing: essays(:mary_stay_home), account: accounts(:signals37)
2021

2122
if current_adapter?(:PostgreSQLAdapter)
2223
@uuid_entry_with_message = UuidEntry.create! uuid: SecureRandom.uuid, entryable: UuidMessage.new(uuid: SecureRandom.uuid, subject: "Hello world!")
@@ -36,7 +37,7 @@ class DelegatedTypeTest < ActiveRecord::TestCase
3637
test "delegated class with custom foreign_type" do
3738
assert_equal Message, @entry_with_message.thing_class
3839
assert_equal Comment, @entry_with_comment.thing_class
39-
assert_equal Post, @entry_with_post.thing_class
40+
assert_equal Essay, @entry_with_essay.thing_class
4041
end
4142

4243
test "delegated type name" do
@@ -56,9 +57,9 @@ class DelegatedTypeTest < ActiveRecord::TestCase
5657
end
5758

5859
test "delegated type predicates with custom foreign_type" do
59-
assert_predicate @entry_with_post, :post?
60-
assert_not @entry_with_message.post?
61-
assert_not @entry_with_comment.post?
60+
assert_predicate @entry_with_essay, :essay?
61+
assert_not @entry_with_message.essay?
62+
assert_not @entry_with_comment.essay?
6263
end
6364

6465
test "scope" do
@@ -67,7 +68,7 @@ class DelegatedTypeTest < ActiveRecord::TestCase
6768
end
6869

6970
test "scope with custom foreign_type" do
70-
assert_predicate Entry.posts.first, :post?
71+
assert_predicate Entry.essays.first, :essay?
7172
end
7273

7374
test "accessor" do
@@ -96,6 +97,15 @@ class DelegatedTypeTest < ActiveRecord::TestCase
9697
assert_nil @uuid_entry_with_comment.uuid_message_uuid
9798
end
9899

100+
test "association inverse_of" do
101+
created = @entry_with_message.message
102+
updated = @entry_with_message.build_entryable subject: "Goodbye world!"
103+
104+
assert_changes -> { @entry_with_message.reload.message }, from: created, to: updated do
105+
updated.save!
106+
end
107+
end
108+
99109
test "touch account" do
100110
previous_account_updated_at = @entry_with_message.account.updated_at
101111
previous_entry_updated_at = @entry_with_message.updated_at

activerecord/test/models/comment.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
# frozen_string_literal: true
22

3+
require "models/entryable"
4+
35
# `counter_cache` requires association class before `attr_readonly`.
46
class Post < ActiveRecord::Base; end
57

68
class Comment < ActiveRecord::Base
9+
include Entryable
10+
711
scope :limit_by, lambda { |l| limit(l) }
812
scope :containing_the_letter_e, -> { where("comments.body LIKE '%e%'") }
913
scope :not_again, -> { where("comments.body NOT LIKE '%again%'") }

activerecord/test/models/entry.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,6 @@ class Entry < ActiveRecord::Base
55
belongs_to :account, touch: true
66

77
# alternate delegation for custom foreign_key/foreign_type
8-
delegated_type :thing, types: %w[ Post ],
8+
delegated_type :thing, types: %w[ Essay ],
99
foreign_key: :entryable_id, foreign_type: :entryable_type
1010
end

activerecord/test/models/entryable.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# frozen_string_literal: true
2+
3+
require "models/entry"
4+
5+
module Entryable
6+
extend ActiveSupport::Concern
7+
8+
included do
9+
has_one :entry, as: :entryable, touch: true
10+
end
11+
end

activerecord/test/models/essay.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
# frozen_string_literal: true
22

3+
require "models/entryable"
4+
35
class Essay < ActiveRecord::Base
6+
include Entryable
7+
48
belongs_to :author, primary_key: :name
59
belongs_to :writer, primary_key: :name, polymorphic: true
610
belongs_to :category, primary_key: :name

activerecord/test/models/message.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
# frozen_string_literal: true
22

3+
require "models/entryable"
4+
35
class Message < ActiveRecord::Base
4-
has_one :entry, as: :entryable, touch: true
6+
include Entryable
7+
58
has_many :recipients
69
end
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# frozen_string_literal: true
22

3+
require "models/uuid_entryable"
4+
35
class UuidComment < ActiveRecord::Base
4-
has_one :uuid_entry, as: :entryable
6+
include UuidEntryable
57
end
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# frozen_string_literal: true
2+
3+
require "models/uuid_entry"
4+
5+
module UuidEntryable
6+
extend ActiveSupport::Concern
7+
8+
included do
9+
has_one :uuid_entry, as: :entryable
10+
end
11+
end

0 commit comments

Comments
 (0)