Skip to content

Commit 3063847

Browse files
committed
🔧 Add config inheritance
Inheritance forms a singly-linked-list, so lookup will be O(n) on the number of ancestors. So arbitrarily deep ancestor lists may have poor performance, but normal cases should be fine. Without customization, ancestor trees will be three or four deep: client -> [versioned ->] global -> default
1 parent 198c3e6 commit 3063847

File tree

3 files changed

+166
-2
lines changed

3 files changed

+166
-2
lines changed

lib/net/imap/config.rb

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,27 @@
22
# :markup: markdown
33

44
require_relative "config/attr_accessors"
5+
require_relative "config/attr_inheritance"
56

67
module Net
78
class IMAP
89

910
# Net::IMAP::Config stores configuration options for Net::IMAP clients.
1011
#
12+
# ## Inheritance
13+
#
14+
# Configs have a parent[rdoc-ref:Config::AttrInheritance#parent] config, and
15+
# any attributes which have not been set locally will inherit the parent's
16+
# value.
17+
#
18+
# See the following methods, defined by Config::AttrInheritance:
19+
# - {#new}[rdoc-ref:Config::AttrInheritance#reset] -- create a new config
20+
# which inherits from the receiver.
21+
# - {#inherited?}[rdoc-ref:Config::AttrInheritance#inherited?] -- return
22+
# whether a particular attribute is inherited.
23+
# - {#reset}[rdoc-ref:Config::AttrInheritance#reset] -- reset attributes to
24+
# be inherited.
25+
#
1126
# ## Thread Safety
1227
#
1328
# *NOTE:* Updates to config objects are not synchronized for thread-safety.
@@ -17,6 +32,7 @@ class Config
1732
def self.default; @default end
1833

1934
include AttrAccessors
35+
include AttrInheritance
2036

2137
# The debug mode (boolean)
2238
#
@@ -46,9 +62,11 @@ def self.default; @default end
4662

4763
# Creates a new config object and initialize its attribute with +attrs+.
4864
#
65+
# If +parent+ is not given, the global config is used by default.
66+
#
4967
# If a block is given, the new config object is yielded to it.
50-
def initialize(**attrs)
51-
super()
68+
def initialize(parent = nil, **attrs)
69+
super(parent)
5270
attrs.each do send(:"#{_1}=", _2) end
5371
yield self if block_given?
5472
end
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# frozen_string_literal: true
2+
3+
module Net
4+
class IMAP
5+
class Config
6+
# Inheritance forms a singly-linked-list, so lookup will be O(n) on the
7+
# number of ancestors. Without customization, ancestor trees will only be
8+
# three or four deep:
9+
# client -> [versioned ->] global -> default
10+
module AttrInheritance
11+
INHERITED = Module.new.freeze
12+
private_constant :INHERITED
13+
14+
module Macros # :nodoc: internal API
15+
def attr_accessor(name) super; AttrInheritance.attr_accessor(name) end
16+
end
17+
private_constant :Macros
18+
19+
def self.included(mod)
20+
mod.extend Macros
21+
end
22+
private_class_method :included
23+
24+
def self.attr_accessor(name) # :nodoc: internal API
25+
module_eval <<~RUBY, __FILE__, __LINE__ + 1
26+
def #{name}; (val = super) == INHERITED ? parent&.#{name} : val end
27+
RUBY
28+
end
29+
30+
# The parent Config object
31+
attr_reader :parent
32+
33+
def initialize(parent = nil) # :notnew:
34+
super()
35+
@parent = parent
36+
reset
37+
end
38+
39+
# Creates a new config, which inherits from +self+.
40+
def new(**attrs) self.class.new(self, **attrs) end
41+
42+
# Returns +true+ if +attr+ is inherited from #parent and not overridden
43+
# by this config.
44+
def inherited?(attr) data[attr] == INHERITED end
45+
46+
# :call-seq:
47+
# reset -> self
48+
# reset(attr) -> attribute value
49+
#
50+
# Resets an +attr+ to inherit from the #parent config.
51+
#
52+
# When +attr+ is nil or not given, all attributes are reset.
53+
def reset(attr = nil)
54+
if attr.nil?
55+
data.members.each do |attr| data[attr] = INHERITED end
56+
self
57+
elsif inherited?(attr)
58+
nil
59+
else
60+
old, data[attr] = data[attr], INHERITED
61+
old
62+
end
63+
end
64+
65+
private
66+
67+
def initialize_copy(other)
68+
super
69+
@parent ||= other # only default has nil parent
70+
end
71+
72+
end
73+
end
74+
end
75+
end

test/net/imap/test_config.rb

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,75 @@ class ConfigTest < Test::Unit::TestCase
3232
refute default.debug?
3333
end
3434

35+
test ".new(parent, ...) and inheritance" do
36+
base = Config.new debug: false
37+
child = Config.new(base)
38+
assert_equal base, child.parent
39+
assert_equal false, child.debug
40+
assert_equal false, child.debug?
41+
base.debug = true
42+
assert_equal true, child.debug?
43+
child.debug = false
44+
assert_equal false, child.debug?
45+
child.reset(:debug)
46+
assert_equal true, child.debug?
47+
base.debug = false
48+
child.debug = true
49+
assert_equal true, child.debug?
50+
child = Config.new(base, debug: true)
51+
assert_equal true, child.debug?
52+
base.debug = true
53+
child = Config.new(base, debug: false)
54+
assert_equal false, child.debug?
55+
end
56+
57+
test "#new and inheritance" do
58+
base = Config.new debug: false
59+
child = base.new
60+
assert_equal base, child.parent
61+
assert_equal false, child.debug
62+
assert_equal false, child.debug?
63+
base.debug = true
64+
assert_equal true, child.debug?
65+
child.debug = false
66+
assert_equal false, child.debug?
67+
child.reset(:debug)
68+
assert_equal true, child.debug?
69+
base.debug = false
70+
child.debug = true
71+
assert_equal true, child.debug?
72+
child = base.new(debug: true)
73+
assert_equal true, child.debug?
74+
base.debug = true
75+
child = base.new(debug: false)
76+
assert_equal false, child.debug?
77+
end
78+
79+
test "#inherited? and #reset(attr)" do
80+
base = Config.new debug: false, open_timeout: 99, idle_response_timeout: 15
81+
child = base.new debug: true, open_timeout: 15, idle_response_timeout: 10
82+
refute child.inherited?(:idle_response_timeout)
83+
assert_equal 10, child.reset(:idle_response_timeout)
84+
assert child.inherited?(:idle_response_timeout)
85+
assert_equal 15, child.idle_response_timeout
86+
refute child.inherited?(:open_timeout)
87+
refute child.inherited?(:debug)
88+
child.debug = false
89+
refute child.inherited?(:debug)
90+
assert_equal false, child.reset(:debug)
91+
assert child.inherited?(:debug)
92+
assert_equal false, child.debug
93+
assert_equal nil, child.reset(:debug)
94+
end
95+
96+
test "#reset all attributes" do
97+
base = Config.new debug: false, open_timeout: 99, idle_response_timeout: 15
98+
child = base.new debug: true, open_timeout: 15, idle_response_timeout: 10
99+
result = child.reset
100+
assert_same child, result
101+
assert child.inherited?(:debug)
102+
assert child.inherited?(:open_timeout)
103+
assert child.inherited?(:idle_response_timeout)
104+
end
105+
35106
end

0 commit comments

Comments
 (0)