Skip to content

Commit 7a299a2

Browse files
committed
Add deprecated_associations plugin, for warning/raising for deprecated association access
This makes it possible to gradually phase out a current association. If an association is deprecated, it warns when a public association method is called, or when the association reflection is accessed.
1 parent 480d620 commit 7a299a2

File tree

4 files changed

+301
-0
lines changed

4 files changed

+301
-0
lines changed

CHANGELOG

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
=== master
22

3+
* Add deprecated_associations plugin, for warning/raising for deprecated association access (jeremyevans)
4+
35
* Clear all cached schema information when using Database#drop_schema on PostgreSQL (jeremyevans)
46

57
* Add Database#rename_schema on PostgreSQL (jeremyevans)
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
# frozen-string-literal: true
2+
3+
module Sequel
4+
module Plugins
5+
# The deprecated_associations plugin adds support for
6+
# deprecating associations. Attempts to use association
7+
# methods and access association metadata for deprecated
8+
# associations results in a warning.
9+
#
10+
# Album.plugin :deprecated_associations
11+
# Album.many_to_one :artist, deprecated: true
12+
# album = Album[1]
13+
#
14+
# Album.association_reflection(:artist) # warning
15+
# album.artist # warning
16+
# album.artist_dataset # warning
17+
# album.artist = Artist[2] # warning
18+
#
19+
# By default, the plugin issues a single warning per
20+
# association method or association reflection. See
21+
# DeprecatedAssociations.configure for options to make
22+
# deprecated association issue warnings for every access,
23+
# to include backtraces in warnings, or to raise an
24+
# exception instead of warning.
25+
#
26+
# Note that Model.association_reflections and
27+
# Model.all_association_reflections will include deprecated
28+
# associations, and accessing the metadata for deprecated
29+
# associations through these interfaces not issue warnings.
30+
#
31+
# Usage:
32+
#
33+
# # Make all models support deprecated associations
34+
# # (called before loading subclasses)
35+
# Sequel::Model.plugin :deprecated_associations
36+
#
37+
# # Make Album class support deprecated associations
38+
# Album.plugin :deprecated_associations
39+
module DeprecatedAssociations
40+
# Exception class used for deprecated association use when
41+
# raising exceptions instead of emitting warnings.
42+
class Access < Sequel::Error; end
43+
44+
# Configure the deprecated associations plugin. Options:
45+
#
46+
# backtrace: Print backtrace with warning
47+
# deduplicate: Set to false to emit warnings for every
48+
# deprecated association method call/access
49+
# (when not caching associations, this is always false)
50+
# raise: Raise Sequel::Plugin::DeprecatedAssociations::Access
51+
# instead of warning
52+
def self.configure(model, opts = OPTS)
53+
model.instance_exec do
54+
(@deprecated_associations_config ||= {}).merge!(opts)
55+
end
56+
end
57+
58+
module ClassMethods
59+
# Issue a deprecation warning if the association is deprecated.
60+
def association_reflection(assoc)
61+
ref = super
62+
if ref && ref[:deprecated]
63+
emit_deprecated_association_warning(ref, nil) do
64+
"Access of association reflection for deprecated association: class:#{name} association:#{assoc}"
65+
end
66+
end
67+
ref
68+
end
69+
70+
private
71+
72+
# Issue a deprecation warning when the defined method is called if the
73+
# association is deprecated and the method name does not start with the
74+
# underscore (to avoid not warning twice, once for the public association
75+
# method and once for the private association method).
76+
def association_module_def(name, opts=OPTS, &block)
77+
super
78+
if opts[:deprecated] && name[0] != "_"
79+
deprecated_associations_module.module_exec do
80+
define_method(name) do |*a, &b|
81+
self.class.send(:emit_deprecated_association_warning, opts, name) do
82+
"Calling deprecated association method: class:#{self.class.name} association:#{opts[:name]} method:#{name}"
83+
end
84+
super(*a, &b)
85+
end
86+
alias_method name, name
87+
end
88+
end
89+
nil
90+
end
91+
92+
# Issue a deprecation warning when the defined method is called if the
93+
# association is deprecated.
94+
def association_module_delegate_def(name, opts, &block)
95+
super
96+
if opts[:deprecated]
97+
deprecated_associations_module.module_exec do
98+
define_method(name) do |*a, &b|
99+
self.class.send(:emit_deprecated_association_warning, opts, name) do
100+
"Calling deprecated association method: class:#{self.class.name} association:#{opts[:name]} method:#{name}"
101+
end
102+
super(*a, &b)
103+
end
104+
# :nocov:
105+
ruby2_keywords(name) if respond_to?(:ruby2_keywords, true)
106+
# :nocov:
107+
alias_method(name, name)
108+
end
109+
end
110+
nil
111+
end
112+
113+
# A module to add deprecated association methods to. These methods
114+
# handle issuing the deprecation warnings, and call super to get the
115+
# default behavior.
116+
def deprecated_associations_module
117+
return @deprecated_associations_module if defined?(@deprecated_associations_module)
118+
@deprecated_associations_module = Module.new
119+
include(@deprecated_associations_module)
120+
@deprecated_associations_module
121+
end
122+
123+
# Emit a deprecation warning, or raise an exception if the :raise
124+
# plugin option was used.
125+
def emit_deprecated_association_warning(ref, method)
126+
config = @deprecated_associations_config
127+
128+
raise Access, yield if config[:raise]
129+
130+
unless config[:deduplicate] == false
131+
emit = false
132+
ref.send(:cached_fetch, [:deprecated_associations, method]) do
133+
emit = true
134+
end
135+
return unless emit
136+
end
137+
138+
if config[:backtrace]
139+
warn yield, caller(2)
140+
else
141+
warn yield, :uplevel => 2
142+
end
143+
end
144+
end
145+
end
146+
end
147+
end
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
require_relative "spec_helper"
2+
3+
describe "deprecated_associations plugin" do
4+
before do
5+
@db = Sequel.mock(:autoid=>1, :fetch=>[{:id=>1, :c_id=>2}], :numrows=>1)
6+
@c = Class.new(Sequel::Model(@db[:c]))
7+
def @c.name; :C end
8+
@c.columns :id, :c_id
9+
@c.plugin :deprecated_associations
10+
@warnings = warnings = []
11+
@c.define_singleton_method(:warn){|a,*| warnings << a}
12+
@o = @c.load(:id=>1, :c_id=>2)
13+
end
14+
15+
it "does not show warnings for non-deprecated singular associations" do
16+
@c.many_to_one :c, :class => @c
17+
@c.association_reflection(:c)
18+
@o.c
19+
@o.c_dataset
20+
@o.c = nil
21+
@warnings.must_equal []
22+
end
23+
24+
it "does not show warnings for non-deprecated plural associations" do
25+
@c.one_to_many :cs, :key => :c_id, :class => @c
26+
@c.association_reflection(:cs)
27+
@o.cs
28+
@o.cs_dataset
29+
o = @c.create
30+
@o.add_c(o)
31+
@o.remove_c(o)
32+
@o.remove_all_cs
33+
@warnings.must_equal []
34+
end
35+
36+
it "shows warnings for deprecated singular associations" do
37+
@c.many_to_one :c, :class => @c, :deprecated => true
38+
2.times do
39+
@c.association_reflection(:c)
40+
@c.eager(:c)
41+
@c.eager_graph(:c)
42+
@c.where(:c=>@o).sql
43+
@o.c
44+
@o.c_dataset
45+
@o.c = nil
46+
end
47+
@warnings.must_equal [
48+
"Access of association reflection for deprecated association: class:C association:c",
49+
"Calling deprecated association method: class:C association:c method:c",
50+
"Calling deprecated association method: class:C association:c method:c_dataset",
51+
"Calling deprecated association method: class:C association:c method:c="
52+
]
53+
end
54+
55+
it "shows warnings for deprecated plural associations" do
56+
@c.one_to_many :cs, :key => :c_id, :class => @c, :deprecated => true
57+
o = @c.create
58+
2.times do
59+
@c.association_reflection(:cs)
60+
@c.eager(:cs)
61+
@c.eager_graph(:cs)
62+
@c.where(:cs=>@o).sql
63+
@o.cs
64+
@o.cs_dataset
65+
@o.add_c(o)
66+
@o.remove_c(o)
67+
@o.remove_all_cs
68+
end
69+
@warnings.must_equal [
70+
"Access of association reflection for deprecated association: class:C association:cs",
71+
"Calling deprecated association method: class:C association:cs method:cs",
72+
"Calling deprecated association method: class:C association:cs method:cs_dataset",
73+
"Calling deprecated association method: class:C association:cs method:add_c",
74+
"Calling deprecated association method: class:C association:cs method:remove_c",
75+
"Calling deprecated association method: class:C association:cs method:remove_all_cs"
76+
]
77+
end
78+
79+
it "does not deduplicate warnings when not caching associations" do
80+
@c.cache_associations = false
81+
@c.many_to_one :c, :class => @c, :deprecated => true
82+
2.times do
83+
@c.association_reflection(:c)
84+
@c.eager(:c)
85+
@c.eager_graph(:c)
86+
@c.where(:c=>@o).sql
87+
@o.c
88+
@o.c_dataset
89+
@o.c = nil
90+
end
91+
@warnings.must_equal [
92+
"Access of association reflection for deprecated association: class:C association:c",
93+
"Access of association reflection for deprecated association: class:C association:c",
94+
"Access of association reflection for deprecated association: class:C association:c",
95+
"Access of association reflection for deprecated association: class:C association:c",
96+
"Calling deprecated association method: class:C association:c method:c",
97+
"Calling deprecated association method: class:C association:c method:c_dataset",
98+
"Calling deprecated association method: class:C association:c method:c="
99+
] * 2
100+
end
101+
102+
it "does not deduplicate warnings when using deduplicate: false plugin option" do
103+
@c.plugin :deprecated_associations, :deduplicate => false
104+
@c.one_to_many :cs, :key => :c_id, :class => @c, :deprecated => true
105+
o = @c.create
106+
2.times do
107+
@c.association_reflection(:cs)
108+
@c.eager(:cs)
109+
@c.eager_graph(:cs)
110+
@c.where(:cs=>@o).sql
111+
@o.cs
112+
@o.cs_dataset
113+
@o.add_c(o)
114+
@o.remove_c(o)
115+
@o.remove_all_cs
116+
end
117+
@warnings.must_equal [
118+
"Access of association reflection for deprecated association: class:C association:cs",
119+
"Access of association reflection for deprecated association: class:C association:cs",
120+
"Access of association reflection for deprecated association: class:C association:cs",
121+
"Access of association reflection for deprecated association: class:C association:cs",
122+
"Calling deprecated association method: class:C association:cs method:cs",
123+
"Calling deprecated association method: class:C association:cs method:cs_dataset",
124+
"Calling deprecated association method: class:C association:cs method:add_c",
125+
"Calling deprecated association method: class:C association:cs method:remove_c",
126+
"Calling deprecated association method: class:C association:cs method:cs_dataset",
127+
"Calling deprecated association method: class:C association:cs method:remove_all_cs"
128+
] * 2
129+
end
130+
131+
it "includes backtrace in warning when using :backtrace plugin option" do
132+
@c.plugin :deprecated_associations, :backtrace => true
133+
@c.many_to_one :c, :class => @c, :deprecated => true
134+
warnings = nil
135+
@c.singleton_class.send(:remove_method, :warn)
136+
@c.define_singleton_method(:warn){|*a| warnings = a}
137+
line = __LINE__ + 1
138+
@c.association_reflection(:c)
139+
warnings[0].must_equal "Access of association reflection for deprecated association: class:C association:c"
140+
warnings[1][0].must_include "#{__FILE__}:#{line}"
141+
end
142+
143+
it "raises for deprecated association when using :raise plugin option" do
144+
@c.plugin :deprecated_associations, :raise => true
145+
@c.many_to_one :c, :class => @c, :deprecated => true
146+
proc{@c.association_reflection(:c)}.must_raise Sequel::Plugins::DeprecatedAssociations::Access
147+
end
148+
end

www/pages/plugins.html.erb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,10 @@
8282
<span class="ul__span">Delay add_<i>association</i> calls on new objects until after after the object is saved. </span>
8383
</li>
8484
<li class="ul__li ul__li--grid">
85+
<a class="a" href="rdoc-plugins/classes/Sequel/Plugins/DeprecatedAssociations.html">deprecated_associations </a>
86+
<span class="ul__span">Supports deprecating associations and emitting warnings or raising when they are accessed.</span>
87+
</li>
88+
<li class="ul__li ul__li--grid">
8589
<a class="a" href="rdoc-plugins/classes/Sequel/Plugins/EagerEach.html">eager_each </a>
8690
<span class="ul__span">Makes each on an eagerly loaded dataset do eager loading.</span>
8791
</li>

0 commit comments

Comments
 (0)