net-imap 0.4.22 → 0.6.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +12 -2
  3. data/README.md +10 -4
  4. data/docs/styles.css +75 -14
  5. data/lib/net/imap/authenticators.rb +2 -2
  6. data/lib/net/imap/command_data.rb +40 -95
  7. data/lib/net/imap/config/attr_accessors.rb +8 -9
  8. data/lib/net/imap/config/attr_inheritance.rb +64 -1
  9. data/lib/net/imap/config/attr_type_coercion.rb +22 -10
  10. data/lib/net/imap/config/attr_version_defaults.rb +90 -0
  11. data/lib/net/imap/config.rb +241 -125
  12. data/lib/net/imap/connection_state.rb +48 -0
  13. data/lib/net/imap/data_encoding.rb +80 -31
  14. data/lib/net/imap/deprecated_client_options.rb +6 -3
  15. data/lib/net/imap/errors.rb +158 -0
  16. data/lib/net/imap/esearch_result.rb +225 -0
  17. data/lib/net/imap/fetch_data.rb +126 -47
  18. data/lib/net/imap/flags.rb +1 -1
  19. data/lib/net/imap/response_data.rb +123 -187
  20. data/lib/net/imap/response_parser/parser_utils.rb +19 -23
  21. data/lib/net/imap/response_parser.rb +182 -38
  22. data/lib/net/imap/response_reader.rb +10 -12
  23. data/lib/net/imap/sasl/anonymous_authenticator.rb +3 -3
  24. data/lib/net/imap/sasl/authentication_exchange.rb +52 -20
  25. data/lib/net/imap/sasl/authenticators.rb +8 -4
  26. data/lib/net/imap/sasl/client_adapter.rb +77 -26
  27. data/lib/net/imap/sasl/cram_md5_authenticator.rb +4 -4
  28. data/lib/net/imap/sasl/digest_md5_authenticator.rb +218 -56
  29. data/lib/net/imap/sasl/external_authenticator.rb +2 -2
  30. data/lib/net/imap/sasl/gs2_header.rb +7 -7
  31. data/lib/net/imap/sasl/login_authenticator.rb +4 -3
  32. data/lib/net/imap/sasl/oauthbearer_authenticator.rb +6 -6
  33. data/lib/net/imap/sasl/plain_authenticator.rb +7 -7
  34. data/lib/net/imap/sasl/protocol_adapters.rb +60 -4
  35. data/lib/net/imap/sasl/scram_authenticator.rb +8 -8
  36. data/lib/net/imap/sasl.rb +7 -4
  37. data/lib/net/imap/sasl_adapter.rb +0 -1
  38. data/lib/net/imap/search_result.rb +10 -5
  39. data/lib/net/imap/sequence_set.rb +1104 -421
  40. data/lib/net/imap/stringprep/nameprep.rb +1 -1
  41. data/lib/net/imap/stringprep/trace.rb +4 -4
  42. data/lib/net/imap/uidplus_data.rb +4 -147
  43. data/lib/net/imap/vanished_data.rb +65 -0
  44. data/lib/net/imap.rb +1002 -313
  45. data/net-imap.gemspec +1 -1
  46. data/rakelib/rfcs.rake +2 -0
  47. data/rakelib/string_prep_tables_generator.rb +6 -2
  48. metadata +7 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bf6dcd9dec4a915da25b27d419470818ac35833ff30f75c73f7e5a49946cf20c
4
- data.tar.gz: d9a3fc1b21be8180434c4700cfbbeeeb4b302e9380e3cad13d2168cb6f9eb5f7
3
+ metadata.gz: 4249dc5d175bd3ae3a3b3b79e0f368e674ec96045510a8b504e829feefb54f3d
4
+ data.tar.gz: 6adbee15b0303b36ec1c574991d4bdbf518bcd5621c6b668f05df0bc201ba50a
5
5
  SHA512:
6
- metadata.gz: bb6bf40015deeb1bd40c6473f3373b51775f0f2b90db7cdb19e221e4cf17ab0f4ff1c1c796028fcec48c38c2d8cb65d42f47afce0d99add965ceb588ca2bdec6
7
- data.tar.gz: 36dab024f741adf40fee30f8d688ef9a17aac95e9cf47a1f48560cf35314b6240a65ad807c4389815af65e034051ce94b99f61c387c0aaa39a6c87753e6f7a3a
6
+ metadata.gz: a20c230ac3977d9acc09bc964badc60acbedaca84246d1ce67787c78443cdea350ade6907e58b61a5c9f09bf3b12e5e57cd319ef82a096c3780f97dc7017ed31
7
+ data.tar.gz: 68ab8b48664998bbf05d5367fea4e5b06e3ba3b2d4e48ba8ae026069060b23f7dafaf7ecf2681fc5aa4ff0f134e55d9399ca51456ff3a7ec718ba829629866b7
data/Gemfile CHANGED
@@ -4,13 +4,23 @@ source "https://rubygems.org"
4
4
 
5
5
  gemspec
6
6
 
7
- gem "digest"
7
+ # gem "digest" # not included as a workaround for #576
8
8
  gem "strscan"
9
9
  gem "base64"
10
+ gem "psych", ">= 5.3.0" # 5.2.5 for Data serialization, 5.3.0 for TruffleRuby
10
11
 
12
+ gem "irb"
11
13
  gem "rake"
12
14
  gem "rdoc"
13
15
  gem "test-unit"
14
16
  gem "test-unit-ruby-core", git: "https://github.com/ruby/test-unit-ruby-core"
15
17
 
16
- gem "benchmark-driver"
18
+ gem "benchmark", require: false
19
+ gem "benchmark-driver", require: false
20
+ gem "vernier", require: false, platform: :mri
21
+
22
+ group :test do
23
+ gem "simplecov", require: false, platforms: %i[mri windows]
24
+ gem "simplecov-html", require: false, platforms: %i[mri windows]
25
+ gem "simplecov-json", require: false, platforms: %i[mri windows]
26
+ end
data/README.md CHANGED
@@ -1,7 +1,9 @@
1
1
  # Net::IMAP
2
2
 
3
3
  Net::IMAP implements Internet Message Access Protocol (IMAP) client
4
- functionality. The protocol is described in [IMAP](https://tools.ietf.org/html/rfc3501).
4
+ functionality. The protocol is described in
5
+ [RFC3501](https://www.rfc-editor.org/rfc/rfc3501),
6
+ [RFC9051](https://www.rfc-editor.org/rfc/rfc9051) and various extensions.
5
7
 
6
8
  ## Installation
7
9
 
@@ -50,12 +52,16 @@ end
50
52
 
51
53
  ```ruby
52
54
  imap.select('Mail/sent-mail')
53
- if not imap.list('Mail/', 'sent-apr03')
55
+ if imap.list('Mail/', 'sent-apr03').empty?
54
56
  imap.create('Mail/sent-apr03')
55
57
  end
56
58
  imap.search(["BEFORE", "30-Apr-2003", "SINCE", "1-Apr-2003"]).each do |message_id|
57
- imap.copy(message_id, "Mail/sent-apr03")
58
- imap.store(message_id, "+FLAGS", [:Deleted])
59
+ if imap.capable?(:move) || imap.capable?(:IMAP4rev2)
60
+ imap.move(message_id, "Mail/sent-apr03")
61
+ else
62
+ imap.copy(message_id, "Mail/sent-apr03")
63
+ imap.store(message_id, "+FLAGS", [:Deleted])
64
+ end
59
65
  end
60
66
  imap.expunge
61
67
  ```
data/docs/styles.css CHANGED
@@ -1,24 +1,85 @@
1
1
  /* this is a work in progress. :) */
2
2
 
3
- main .method-header {
4
- background: rgba(27,31,35,0.05);
5
- border: 1px solid #6C8C22;
3
+ /***********************************************
4
+ * Method descriptions
5
+ ***********************************************/
6
+
7
+ main .method-detail {
8
+ display: grid;
9
+ grid-template-columns: 1fr auto;
10
+ justify-content: space-between;
11
+ }
12
+
13
+ main .method-header,
14
+ main .method-controls,
15
+ .attribute-method-heading {
6
16
  padding: 0.5em;
7
- border-radius: 4px;
8
- /* padding: 0 0.5em; */
9
- /* border-width: 0 1px; */
10
- /* border-color: #6C8C22; */
11
- /* border-style: solid; */
17
+ /* border: 1px solid var(--highlight-color); */
18
+ background: var(--table-header-background-color);
19
+ line-height: 1.6;
20
+ }
21
+
22
+ .attribute-method-heading .attribute-access-type {
23
+ float: right;
24
+ }
25
+
26
+ main .method-header {
27
+ border-right: none;
28
+ border-radius: 4px 0 0 4px;
29
+ }
30
+
31
+ main .method-heading :any-link {
32
+ text-decoration: none;
33
+ }
34
+
35
+ main .method-controls {
36
+ border-left: none;
37
+ border-radius: 0 4px 4px 0;
12
38
  }
13
39
 
14
40
  main .method-description, main .aliases {
41
+ grid-column: 1 / span 2;
15
42
  padding-left: 1em;
16
43
  }
17
44
 
18
- body {
19
- /*
20
- * The default (300) can be too low contrast. Also, many fonts don't
21
- * distinguish between 300->400, so <em>...</em> had no effect.
22
- */
23
- font-weight: 400;
45
+ @media (max-width: 700px) {
46
+ main .method-header, main .method-controls, main .method-description {
47
+ grid-column: 1 / span 2;
48
+ margin: 0;
49
+ }
50
+ main .method-controls {
51
+ background: none;
52
+ }
53
+ }
54
+
55
+ /***********************************************
56
+ * Description lists
57
+ ***********************************************/
58
+
59
+ main dt {
60
+ margin-bottom: 0; /* override rdoc 6.8 */
61
+ float: unset; /* override rdoc 6.8 */
62
+ line-height: 1.5; /* matches `main p` */
63
+ }
64
+
65
+ main dl.note-list dt {
66
+ margin-right: 1em;
67
+ float: left;
68
+ }
69
+
70
+ main dl.note-list dt:has(+ dt) {
71
+ margin-right: 0.25em;
72
+ }
73
+
74
+ main dl.note-list dt:has(+ dt)::after {
75
+ content: ', ';
76
+ font-weight: normal;
77
+ }
78
+
79
+ main dd {
80
+ margin: 0 0 1em 1em;
81
+ }
82
+
83
+ main dd p:first-child {
84
+ margin-top: 0;
24
85
  }
@@ -9,7 +9,7 @@ module Net::IMAP::Authenticators
9
9
  "%s.%s is deprecated. Use %s.%s instead." % [
10
10
  Net::IMAP, __method__, Net::IMAP::SASL, __method__
11
11
  ],
12
- uplevel: 1
12
+ uplevel: 1, category: :deprecated
13
13
  )
14
14
  Net::IMAP::SASL.add_authenticator(...)
15
15
  end
@@ -20,7 +20,7 @@ module Net::IMAP::Authenticators
20
20
  "%s.%s is deprecated. Use %s.%s instead." % [
21
21
  Net::IMAP, __method__, Net::IMAP::SASL, __method__
22
22
  ],
23
- uplevel: 1
23
+ uplevel: 1, category: :deprecated
24
24
  )
25
25
  Net::IMAP::SASL.authenticator(...)
26
26
  end
@@ -119,138 +119,83 @@ module Net
119
119
  put_string("\\" + symbol.to_s)
120
120
  end
121
121
 
122
- class RawData # :nodoc:
122
+ CommandData = Data.define(:data) do # :nodoc:
123
123
  def send_data(imap, tag)
124
- imap.__send__(:put_string, @data)
124
+ raise NoMethodError, "#{self.class} must implement #{__method__}"
125
125
  end
126
126
 
127
127
  def validate
128
128
  end
129
-
130
- private
131
-
132
- def initialize(data)
133
- @data = data
134
- end
135
129
  end
136
130
 
137
- class Atom # :nodoc:
131
+ class RawData < CommandData # :nodoc:
138
132
  def send_data(imap, tag)
139
- imap.__send__(:put_string, @data)
140
- end
141
-
142
- def validate
143
- end
144
-
145
- private
146
-
147
- def initialize(data)
148
- @data = data
133
+ imap.__send__(:put_string, data)
149
134
  end
150
135
  end
151
136
 
152
- class QuotedString # :nodoc:
137
+ class Atom < CommandData # :nodoc:
153
138
  def send_data(imap, tag)
154
- imap.__send__(:send_quoted_string, @data)
155
- end
156
-
157
- def validate
158
- end
159
-
160
- private
161
-
162
- def initialize(data)
163
- @data = data
139
+ imap.__send__(:put_string, data)
164
140
  end
165
141
  end
166
142
 
167
- class Literal # :nodoc:
143
+ class QuotedString < CommandData # :nodoc:
168
144
  def send_data(imap, tag)
169
- imap.__send__(:send_literal, @data, tag)
170
- end
171
-
172
- def validate
173
- end
174
-
175
- private
176
-
177
- def initialize(data)
178
- @data = data
145
+ imap.__send__(:send_quoted_string, data)
179
146
  end
180
147
  end
181
148
 
182
- class MessageSet # :nodoc:
149
+ class Literal < CommandData # :nodoc:
183
150
  def send_data(imap, tag)
184
- imap.__send__(:put_string, format_internal(@data))
185
- end
186
-
187
- def validate
188
- validate_internal(@data)
189
- end
190
-
191
- private
192
-
193
- def initialize(data)
194
- @data = data
151
+ imap.__send__(:send_literal, data, tag)
195
152
  end
153
+ end
196
154
 
197
- def format_internal(data)
198
- case data
199
- when "*"
200
- return data
201
- when Integer
202
- if data == -1
203
- return "*"
204
- else
205
- return data.to_s
206
- end
207
- when Range
208
- return format_internal(data.first) +
209
- ":" + format_internal(data.last)
210
- when Array
211
- return data.collect {|i| format_internal(i)}.join(",")
212
- when ThreadMember
213
- return data.seqno.to_s +
214
- ":" + data.children.collect {|i| format_internal(i).join(",")}
155
+ class PartialRange < CommandData # :nodoc:
156
+ uint32_max = 2**32 - 1
157
+ POS_RANGE = 1..uint32_max
158
+ NEG_RANGE = -uint32_max..-1
159
+ Positive = ->{ (_1 in Range) and POS_RANGE.cover?(_1) }
160
+ Negative = ->{ (_1 in Range) and NEG_RANGE.cover?(_1) }
161
+
162
+ def initialize(data:)
163
+ min, max = case data
164
+ in Range
165
+ data.minmax.map { Integer _1 }
166
+ in ResponseParser::Patterns::PARTIAL_RANGE
167
+ data.split(":").map { Integer _1 }.minmax
168
+ else
169
+ raise ArgumentError, "invalid partial range input: %p" % [data]
170
+ end
171
+ data = min..max
172
+ unless data in Positive | Negative
173
+ raise ArgumentError, "invalid partial-range: %p" % [data]
215
174
  end
175
+ super
176
+ rescue TypeError, RangeError
177
+ raise ArgumentError, "expected range min/max to be Integers"
216
178
  end
217
179
 
218
- def validate_internal(data)
219
- case data
220
- when "*"
221
- when Integer
222
- NumValidator.ensure_nz_number(data)
223
- when Range
224
- when Array
225
- data.each do |i|
226
- validate_internal(i)
227
- end
228
- when ThreadMember
229
- data.children.each do |i|
230
- validate_internal(i)
231
- end
232
- else
233
- raise DataFormatError, data.inspect
234
- end
180
+ def formatted = "%d:%d" % data.minmax
181
+
182
+ def send_data(imap, tag)
183
+ imap.__send__(:put_string, formatted)
235
184
  end
236
185
  end
237
186
 
238
- class ClientID # :nodoc:
187
+ class ClientID < CommandData # :nodoc:
239
188
 
240
189
  def send_data(imap, tag)
241
- imap.__send__(:send_data, format_internal(@data), tag)
190
+ imap.__send__(:send_data, format_internal(data), tag)
242
191
  end
243
192
 
244
193
  def validate
245
- validate_internal(@data)
194
+ validate_internal(data)
246
195
  end
247
196
 
248
197
  private
249
198
 
250
- def initialize(data)
251
- @data = data
252
- end
253
-
254
199
  def validate_internal(client_id)
255
200
  client_id.to_h.each do |k,v|
256
201
  unless StringFormatter.valid_string?(k)
@@ -27,24 +27,23 @@ module Net
27
27
 
28
28
  def self.attr_accessor(name) # :nodoc: internal API
29
29
  name = name.to_sym
30
+ raise ArgumentError, "already defined #{name}" if attributes.include?(name)
31
+ attributes << name
30
32
  def_delegators :data, name, :"#{name}="
31
33
  end
32
34
 
33
- def self.attributes
34
- instance_methods.grep(/=\z/).map { _1.to_s.delete_suffix("=").to_sym }
35
- end
36
- private_class_method :attributes
35
+ # An array of Config attribute names
36
+ singleton_class.attr_reader :attributes
37
+ @attributes = []
37
38
 
38
39
  def self.struct # :nodoc: internal API
39
- unless defined?(self::Struct)
40
- const_set :Struct, Struct.new(*attributes)
41
- end
42
- self::Struct
40
+ attributes.freeze
41
+ Struct.new(*attributes)
43
42
  end
44
43
 
45
44
  def initialize # :notnew:
46
45
  super()
47
- @data = AttrAccessors.struct.new
46
+ @data = Config::Struct.new
48
47
  end
49
48
 
50
49
  # Freezes the internal attributes struct, in addition to +self+.
@@ -54,9 +54,72 @@ module Net
54
54
  # Creates a new config, which inherits from +self+.
55
55
  def new(**attrs) self.class.new(self, **attrs) end
56
56
 
57
+ # :call-seq:
58
+ # inherited?(attr) -> true or false
59
+ # inherited?(*attrs) -> true or false
60
+ # inherited? -> true or false
61
+ #
57
62
  # Returns +true+ if +attr+ is inherited from #parent and not overridden
58
63
  # by this config.
59
- def inherited?(attr) data[attr] == INHERITED end
64
+ #
65
+ # When multiple +attrs+ are given, returns +true+ if *all* of them are
66
+ # inherited, or +false+ if any of them are overriden. When no +attrs+
67
+ # are given, returns +true+ if *all* attributes are inherited, or
68
+ # +false+ if any attribute is overriden.
69
+ #
70
+ # Related: #overrides?
71
+ def inherited?(*attrs)
72
+ attrs = data.members if attrs.empty?
73
+ attrs.all? { data[_1] == INHERITED }
74
+ end
75
+
76
+ # :call-seq:
77
+ # inherits_defaults?(*attrs) -> true | Rational | nil | false
78
+ #
79
+ # Returns whether all +attrs+ are inherited from a default config.
80
+ # When no +attrs+ are given, returns whether *all* attributes are
81
+ # inherited from a default config.
82
+ #
83
+ # Returns +true+ when all attributes inherit from Config.default, the
84
+ # version number (as a Rational) when all attributes inherit from a
85
+ # versioned default (see Config@Versioned+defaults), +nil+ if any
86
+ # attributes inherit from Config.global overrides (but not from
87
+ # non-global ancestors), or +false+ when any attributes have been
88
+ # overridden by +self+ or an ancestor (besides global or default
89
+ # configs),
90
+ #
91
+ # Related: #overrides?
92
+ def inherits_defaults?(*attrs)
93
+ if equal?(Config.default)
94
+ true
95
+ elsif equal?(Config.global)
96
+ true if inherited?(*attrs)
97
+ elsif (v = AttrVersionDefaults::VERSIONS.find { equal? Config[_1] })
98
+ attrs = DEFAULT_TO_INHERIT if attrs.empty?
99
+ attrs &= DEFAULT_TO_INHERIT
100
+ (attrs.empty? || parent.inherits_defaults?(*attrs)) && v
101
+ else
102
+ inherited?(*attrs) && parent.inherits_defaults?(*attrs)
103
+ end
104
+ end
105
+
106
+ # :call-seq:
107
+ # overrides?(attr) -> true or false
108
+ # overrides?(*attrs) -> true or false
109
+ # overrides? -> true or false
110
+ #
111
+ # Returns +true+ if +attr+ is defined on this config and not inherited
112
+ # from #parent.
113
+ #
114
+ # When multiple +attrs+ are given, returns +true+ if
115
+ # *any* of them are defined on +self+. When no +attrs+ are given,
116
+ # returns +true+ if *any* attribute is overriden.
117
+ #
118
+ # Related: #inherited?
119
+ def overrides?(*attrs)
120
+ attrs = data.members if attrs.empty?
121
+ attrs.any? { data[_1] != INHERITED }
122
+ end
60
123
 
61
124
  # :call-seq:
62
125
  # reset -> self
@@ -19,7 +19,7 @@ module Net
19
19
  AttrTypeCoercion.attr_accessor(attr, type: type)
20
20
  end
21
21
 
22
- module_function def Integer?; NilOrInteger end
22
+ module_function def Integer? = NilOrInteger
23
23
  end
24
24
  private_constant :Macros
25
25
 
@@ -28,14 +28,26 @@ module Net
28
28
  end
29
29
  private_class_method :included
30
30
 
31
- # Used in v0.5.8+ for Ractor sharability.
32
- def self.safe(...) nil.instance_eval(...).freeze end
31
+ if defined?(Ractor.shareable_proc)
32
+ def self.safe(&b)
33
+ case obj = b.call
34
+ when Proc
35
+ Ractor.shareable_proc(&obj)
36
+ else
37
+ Ractor.make_shareable obj
38
+ end
39
+ end
40
+ elsif defined?(Ractor.make_shareable)
41
+ def self.safe(&b)
42
+ obj = nil.instance_eval(&b).freeze
43
+ Ractor.make_shareable obj
44
+ end
45
+ else
46
+ def self.safe(&b) nil.instance_eval(&b).freeze end
47
+ end
33
48
  private_class_method :safe
34
49
 
35
- Types = Hash.new do |h, type|
36
- type.nil? || Proc === type or raise TypeError, "type not nil or Proc"
37
- safe{type}
38
- end
50
+ Types = Hash.new do |h, type| type => Proc | nil; safe{type} end
39
51
  Types[:boolean] = Boolean = safe{-> {!!_1}}
40
52
  Types[Integer] = safe{->{Integer(_1)}}
41
53
 
@@ -48,10 +60,10 @@ module Net
48
60
  NilOrInteger = safe{->val { Integer val unless val.nil? }}
49
61
 
50
62
  Enum = ->(*enum) {
51
- enum = safe{enum}
52
- expected = -"one of #{enum.map(&:inspect).join(", ")}"
63
+ safe_enum = safe{enum}
64
+ expected = -"one of #{safe_enum.map(&:inspect).join(", ")}"
53
65
  safe{->val {
54
- return val if enum.include?(val)
66
+ return val if safe_enum.include?(val)
55
67
  raise ArgumentError, "expected %s, got %p" % [expected, val]
56
68
  }}
57
69
  }
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+
5
+ module Net
6
+ class IMAP
7
+ class Config
8
+ # >>>
9
+ # *NOTE:* This module is an internal implementation detail, with no
10
+ # guarantee of backward compatibility.
11
+ #
12
+ # Adds a +defaults+ parameter to +attr_accessor+, which is used to compile
13
+ # Config.version_defaults.
14
+ module AttrVersionDefaults
15
+ # The <tt>x.y</tt> part of Net::IMAP::VERSION, as a Rational number.
16
+ CURRENT_VERSION = VERSION.to_r
17
+
18
+ # The config version used for <tt>Config[:next]</tt>.
19
+ NEXT_VERSION = CURRENT_VERSION + 0.1r
20
+
21
+ # The config version used for <tt>Config[:future]</tt>.
22
+ FUTURE_VERSION = 1.0r
23
+
24
+ VERSIONS = ((0.0r..FUTURE_VERSION) % 0.1r).to_a.freeze
25
+
26
+ # See Config.version_defaults.
27
+ singleton_class.attr_reader :version_defaults
28
+
29
+ @version_defaults = Hash.new {|h, k|
30
+ # NOTE: String responds to both so the order is significant.
31
+ # And ignore non-numeric conversion to zero, because: "wat!?".to_r == 0
32
+ (h.fetch(k.to_r, nil) || h.fetch(k.to_f, nil) if k.is_a?(Numeric)) ||
33
+ (h.fetch(k.to_sym, nil) if k.respond_to?(:to_sym)) ||
34
+ (h.fetch(k.to_r, nil) if k.respond_to?(:to_r) && k.to_r != 0r) ||
35
+ (h.fetch(k.to_f, nil) if k.respond_to?(:to_f) && k.to_f != 0.0)
36
+ }
37
+
38
+ # :stopdoc: internal APIs only
39
+
40
+ def attr_accessor(name, defaults: nil, default: (unset = true), **kw)
41
+ unless unset
42
+ version = DEFAULT_TO_INHERIT.include?(name) ? nil : 0.0r
43
+ defaults = { version => default }
44
+ end
45
+ defaults&.each_pair do |version, default|
46
+ AttrVersionDefaults.version_defaults[version] ||= {}
47
+ AttrVersionDefaults.version_defaults[version][name] = default
48
+ end
49
+ super(name, **kw)
50
+ end
51
+
52
+ def self.compile_default!
53
+ raise "Config.default already compiled" if Config.default
54
+ default = VERSIONS.select { _1 <= CURRENT_VERSION }
55
+ .filter_map { version_defaults[_1] }
56
+ .prepend(version_defaults.delete(nil))
57
+ .inject(&:merge)
58
+ Config.new(**default).freeze
59
+ end
60
+
61
+ def self.compile_version_defaults!
62
+ version_defaults[0.0r] = Config[version_defaults.fetch(0.0r)]
63
+
64
+ VERSIONS.each_cons(2) do |prior, version|
65
+ updates = version_defaults[version]
66
+ version_defaults[version] = version_defaults[prior]
67
+ .then { updates ? _1.dup.update(**updates).freeze : _1 }
68
+ end
69
+
70
+ # Safe conversions one way only:
71
+ # 0.6r.to_f == 0.6 # => true
72
+ # 0.6 .to_r == 0.6r # => false
73
+ version_defaults.to_a.each do |k, v|
74
+ next unless k in Rational
75
+ version_defaults[k.to_f] = v
76
+ end
77
+
78
+ version_defaults[:original] = Config[0.0r]
79
+ version_defaults[:current] = Config[CURRENT_VERSION]
80
+ version_defaults[:default] = Config[CURRENT_VERSION]
81
+ version_defaults[:next] = Config[NEXT_VERSION]
82
+ version_defaults[:future] = Config[FUTURE_VERSION]
83
+
84
+ version_defaults.freeze
85
+ end
86
+
87
+ end
88
+ end
89
+ end
90
+ end