net-imap 0.4.24 → 0.5.14

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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +10 -1
  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 +73 -78
  7. data/lib/net/imap/config/attr_type_coercion.rb +22 -10
  8. data/lib/net/imap/config/attr_version_defaults.rb +93 -0
  9. data/lib/net/imap/config.rb +70 -94
  10. data/lib/net/imap/connection_state.rb +48 -0
  11. data/lib/net/imap/data_encoding.rb +3 -3
  12. data/lib/net/imap/data_lite.rb +226 -0
  13. data/lib/net/imap/deprecated_client_options.rb +6 -3
  14. data/lib/net/imap/errors.rb +6 -0
  15. data/lib/net/imap/esearch_result.rb +219 -0
  16. data/lib/net/imap/fetch_data.rb +126 -47
  17. data/lib/net/imap/flags.rb +1 -1
  18. data/lib/net/imap/response_data.rb +120 -186
  19. data/lib/net/imap/response_parser/parser_utils.rb +5 -0
  20. data/lib/net/imap/response_parser.rb +155 -21
  21. data/lib/net/imap/response_reader.rb +9 -12
  22. data/lib/net/imap/sasl/anonymous_authenticator.rb +3 -3
  23. data/lib/net/imap/sasl/authentication_exchange.rb +52 -20
  24. data/lib/net/imap/sasl/authenticators.rb +8 -4
  25. data/lib/net/imap/sasl/client_adapter.rb +77 -26
  26. data/lib/net/imap/sasl/cram_md5_authenticator.rb +4 -4
  27. data/lib/net/imap/sasl/digest_md5_authenticator.rb +218 -56
  28. data/lib/net/imap/sasl/external_authenticator.rb +2 -2
  29. data/lib/net/imap/sasl/gs2_header.rb +7 -7
  30. data/lib/net/imap/sasl/login_authenticator.rb +4 -3
  31. data/lib/net/imap/sasl/oauthbearer_authenticator.rb +6 -6
  32. data/lib/net/imap/sasl/plain_authenticator.rb +7 -7
  33. data/lib/net/imap/sasl/protocol_adapters.rb +60 -4
  34. data/lib/net/imap/sasl/scram_authenticator.rb +10 -10
  35. data/lib/net/imap/sasl.rb +7 -4
  36. data/lib/net/imap/sasl_adapter.rb +0 -1
  37. data/lib/net/imap/search_result.rb +4 -5
  38. data/lib/net/imap/sequence_set.rb +529 -154
  39. data/lib/net/imap/stringprep/nameprep.rb +1 -1
  40. data/lib/net/imap/stringprep/trace.rb +4 -4
  41. data/lib/net/imap/uidplus_data.rb +2 -84
  42. data/lib/net/imap/vanished_data.rb +65 -0
  43. data/lib/net/imap.rb +996 -305
  44. data/net-imap.gemspec +1 -1
  45. data/rakelib/rfcs.rake +2 -0
  46. data/rakelib/string_prep_tables_generator.rb +6 -2
  47. metadata +7 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9419630b908b12c7f89846682dae953e50376d89ebb3b7391b3426bb34480991
4
- data.tar.gz: 4558a77d38a4def28af960c201ca9359fcc828d5a316f76f95cf3c0e575702b6
3
+ metadata.gz: 33bccbb75eba778cb42fc5340afc2f10a899ca671123e8b538a39acbdf16bd1b
4
+ data.tar.gz: c4252164f38a0f36b827fb32247500e95293880e2f8026f4b7ed04926614df41
5
5
  SHA512:
6
- metadata.gz: 5f4baf570c8ed5732493ba801121ae092177fca3ab5f9162ef66f0db2a23e91c09fe740d8ddba52977e65737906a6735a8405bb94e923111970621e79b813141
7
- data.tar.gz: 624e1b8a76630bffa007391b303eab5729b20665d9a5242f58749e0127340ddd1a842eb94ca46da085ccfa91cc093367348586cde705b51fe3eff25882f8f096
6
+ metadata.gz: e682041f5c1f0e071578c0910f3eace9438064741cce16b148870c73137fd38926154269907f4948dec365e83b9115facff5ec21ac23072270ef39bab64cea10
7
+ data.tar.gz: 8a04cac2ad54cd0bc4b2e2f5855bac16f571c3ab6aa0183caa1427ad7c420f8c3920e74b85871c22454dbd827c7772b24cfe96bcaf262222664c05f7483f5dbd
data/Gemfile CHANGED
@@ -8,9 +8,18 @@ gemspec
8
8
  gem "strscan"
9
9
  gem "base64"
10
10
 
11
+ gem "irb"
11
12
  gem "rake"
12
13
  gem "rdoc"
13
14
  gem "test-unit"
14
15
  gem "test-unit-ruby-core", git: "https://github.com/ruby/test-unit-ruby-core"
15
16
 
16
- gem "benchmark-driver"
17
+ gem "benchmark", require: false
18
+ gem "benchmark-driver", require: false
19
+ gem "vernier", require: false, platform: :mri
20
+
21
+ group :test do
22
+ gem "simplecov", require: false, platforms: %i[mri windows]
23
+ gem "simplecov-html", require: false, platforms: %i[mri windows]
24
+ gem "simplecov-json", require: false, platforms: %i[mri windows]
25
+ 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
@@ -3,6 +3,9 @@
3
3
  require "date"
4
4
 
5
5
  require_relative "errors"
6
+ require_relative "data_lite"
7
+
8
+ # :enddoc:
6
9
 
7
10
  module Net
8
11
  class IMAP < Protocol
@@ -78,7 +81,7 @@ module Net
78
81
  put_string('"' + str.gsub(/["\\]/, "\\\\\\&") + '"')
79
82
  end
80
83
 
81
- def send_binary_literal(*a, **kw) send_literal(*a, **kw, binary: true) end
84
+ def send_binary_literal(*a, **kw); send_literal(*a, **kw, binary: true) end
82
85
 
83
86
  # `non_sync` is an optional tri-state flag:
84
87
  # * `true` -> Force non-synchronizing `LITERAL+`/`LITERAL-` behavior.
@@ -130,27 +133,7 @@ module Net
130
133
  def send_date_data(date) put_string Net::IMAP.encode_date(date) end
131
134
  def send_time_data(time) put_string Net::IMAP.encode_time(time) end
132
135
 
133
- # simplistic emulation of CommandData = Data.define(:data)
134
- class CommandData # :nodoc:
135
- class << self
136
- def new(arg = nil, data: arg) super(data: data) end
137
- alias :[] :new
138
- end
139
-
140
- def initialize(data:)
141
- @data = data
142
- freeze
143
- end
144
-
145
- attr_reader :data
146
-
147
- def to_h(&block) block ? to_h.to_h(&block) : { data: data } end
148
- def ==(other) self.class === other && to_h == other.to_h end
149
- def eql?(other) self.class === other && to_h.eql?(other.to_h) end
150
-
151
- # following class definition goes beyond the basic Data.define(:data)
152
- ##
153
-
136
+ CommandData = Data.define(:data) do # :nodoc:
154
137
  def self.validate(...)
155
138
  data = new(...)
156
139
  data.validate
@@ -179,7 +162,7 @@ module Net
179
162
  class RawText < CommandData # :nodoc:
180
163
  def initialize(data:)
181
164
  data = String(data.to_str)
182
- data = if [Encoding::ASCII, Encoding::UTF_8].include?(data.encoding)
165
+ data = if data.encoding in Encoding::ASCII | Encoding::UTF_8
183
166
  -data
184
167
  elsif data.ascii_only?
185
168
  -(data.dup.force_encoding("ASCII"))
@@ -200,9 +183,9 @@ module Net
200
183
  end
201
184
  end
202
185
 
203
- def ascii_only?; data.ascii_only? end
186
+ def ascii_only? = data.ascii_only?
204
187
 
205
- def send_data(imap, tag) imap.__send__(:put_string, data) end
188
+ def send_data(imap, tag) = imap.__send__(:put_string, data)
206
189
  end
207
190
 
208
191
  class RawData < CommandData # :nodoc:
@@ -212,11 +195,10 @@ module Net
212
195
  validate
213
196
  end
214
197
 
215
- def send_data(imap, tag) data.each do _1.send_data(imap, tag) end end
198
+ def send_data(imap, tag) = data.each do _1.send_data(imap, tag) end
216
199
 
217
200
  def validate
218
- return unless RawText === data.last
219
- text = data.last.data
201
+ return unless data.last in RawText(data: text)
220
202
  if text.rindex(/~?\{[1-9]\d*\+?\}\z/n)
221
203
  raise DataFormatError, "RawData cannot end with literal continuation"
222
204
  end
@@ -231,10 +213,7 @@ module Net
231
213
  text, binary, bytesize, non_sync, data = $`, !!$1, $2, !!$3, $'
232
214
  bytesize = Integer bytesize, 10
233
215
  parts << RawText[text] unless text.empty?
234
- parts << extract_literal(data,
235
- binary: binary,
236
- bytesize: bytesize,
237
- non_sync: non_sync)
216
+ parts << extract_literal(data, binary:, bytesize:, non_sync:)
238
217
  data[0, bytesize] = ""
239
218
  end
240
219
  parts << RawText[data] unless data.empty?
@@ -247,7 +226,7 @@ module Net
247
226
  "expected: %s, remaining: %s" % [bytesize, data.bytesize]
248
227
  end
249
228
  literal = data.byteslice(0, bytesize)
250
- (binary ? Literal8 : Literal).new(data: literal, non_sync: non_sync)
229
+ (binary ? Literal8 : Literal).new(data: literal, non_sync:)
251
230
  end
252
231
  end
253
232
 
@@ -275,47 +254,24 @@ module Net
275
254
  end
276
255
  end
277
256
 
278
- class QuotedString # :nodoc:
257
+ class QuotedString < CommandData # :nodoc:
279
258
  def send_data(imap, tag)
280
- imap.__send__(:send_quoted_string, @data)
281
- end
282
-
283
- def validate
284
- end
285
-
286
- private
287
-
288
- def initialize(data)
289
- @data = data
259
+ imap.__send__(:send_quoted_string, data)
290
260
  end
291
261
  end
292
262
 
293
- class Literal # :nodoc:
294
- class << self
295
- def new(_data = nil, _non_sync = nil, data: _data, non_sync: _non_sync)
296
- super(data: data, non_sync: non_sync)
297
- end
298
- alias :[] :new
263
+ class Literal < Data.define(:data, :non_sync) # :nodoc:
264
+ def self.validate(...)
265
+ data = new(...)
266
+ data.validate
267
+ data
299
268
  end
300
269
 
301
- attr_reader :data, :non_sync
302
-
303
- def to_h(&block) block ? to_h.to_h(&block) : { data: data, non_sync: non_sync } end
304
- def ==(other) self.class === other && to_h == other.to_h end
305
- def eql?(other) self.class === other && to_h.eql?(other.to_h) end
306
-
307
270
  def initialize(data:, non_sync: nil)
308
271
  data = -String(data.to_str).b or
309
272
  raise DataFormatError, "#{self.class} expects string input"
310
- @data, @non_sync = data, non_sync
273
+ super
311
274
  validate
312
- freeze
313
- end
314
-
315
- def self.validate(...)
316
- data = new(...)
317
- data.validate
318
- data
319
275
  end
320
276
 
321
277
  def bytesize; data.bytesize end
@@ -328,7 +284,7 @@ module Net
328
284
  end
329
285
 
330
286
  def send_data(imap, tag)
331
- imap.__send__(:send_literal, data, tag, non_sync: non_sync)
287
+ imap.__send__(:send_literal, data, tag, non_sync:)
332
288
  end
333
289
  end
334
290
 
@@ -336,23 +292,66 @@ module Net
336
292
  def validate; nil end # all bytes are okay
337
293
 
338
294
  def send_data(imap, tag)
339
- imap.__send__(:send_binary_literal, data, tag, non_sync: non_sync)
295
+ imap.__send__(:send_binary_literal, data, tag, non_sync:)
296
+ end
297
+ end
298
+
299
+ class PartialRange < CommandData # :nodoc:
300
+ uint32_max = 2**32 - 1
301
+ POS_RANGE = 1..uint32_max
302
+ NEG_RANGE = -uint32_max..-1
303
+ Positive = ->{ (_1 in Range) and POS_RANGE.cover?(_1) }
304
+ Negative = ->{ (_1 in Range) and NEG_RANGE.cover?(_1) }
305
+
306
+ def initialize(data:)
307
+ min, max = case data
308
+ in Range
309
+ data.minmax.map { Integer _1 }
310
+ in ResponseParser::Patterns::PARTIAL_RANGE
311
+ data.split(":").map { Integer _1 }.minmax
312
+ else
313
+ raise ArgumentError, "invalid partial range input: %p" % [data]
314
+ end
315
+ data = min..max
316
+ unless data in Positive | Negative
317
+ raise ArgumentError, "invalid partial-range: %p" % [data]
318
+ end
319
+ super
320
+ rescue TypeError, RangeError
321
+ raise ArgumentError, "expected range min/max to be Integers"
322
+ end
323
+
324
+ def formatted = "%d:%d" % data.minmax
325
+
326
+ def send_data(imap, tag)
327
+ imap.__send__(:put_string, formatted)
340
328
  end
341
329
  end
342
330
 
343
- class MessageSet # :nodoc:
331
+ # *DEPRECATED*. Replaced by SequenceSet.
332
+ class MessageSet < CommandData # :nodoc:
344
333
  def send_data(imap, tag)
345
- imap.__send__(:put_string, format_internal(@data))
334
+ imap.__send__(:put_string, format_internal(data))
346
335
  end
347
336
 
348
337
  def validate
349
- validate_internal(@data)
338
+ validate_internal(data)
350
339
  end
351
340
 
352
341
  private
353
342
 
354
- def initialize(data)
355
- @data = data
343
+ def initialize(data:)
344
+ super
345
+ warn("DEPRECATED: #{MessageSet} should be replaced with #{SequenceSet}.",
346
+ uplevel: 1, category: :deprecated)
347
+ begin
348
+ # to ensure the input works with SequenceSet, too
349
+ SequenceSet.new(data)
350
+ rescue
351
+ warn "MessageSet input is incompatible with SequenceSet: [%s] %s" % [
352
+ $!.class, $!.message
353
+ ]
354
+ end
356
355
  end
357
356
 
358
357
  def format_internal(data)
@@ -396,22 +395,18 @@ module Net
396
395
  end
397
396
  end
398
397
 
399
- class ClientID # :nodoc:
398
+ class ClientID < CommandData # :nodoc:
400
399
 
401
400
  def send_data(imap, tag)
402
- imap.__send__(:send_data, format_internal(@data), tag)
401
+ imap.__send__(:send_data, format_internal(data), tag)
403
402
  end
404
403
 
405
404
  def validate
406
- validate_internal(@data)
405
+ validate_internal(data)
407
406
  end
408
407
 
409
408
  private
410
409
 
411
- def initialize(data)
412
- @data = data
413
- end
414
-
415
410
  def validate_internal(client_id)
416
411
  client_id.to_h.each do |k,v|
417
412
  unless StringFormatter.valid_string?(k)
@@ -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,93 @@
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
+ # Temporarily assign Config.default, enabling #load_defaults(:default)
63
+ version_defaults[:default] = Config.default
64
+ # Use #load_defaults so some attributes are inherited from global.
65
+ version_defaults[:default] = Config.new.load_defaults(:default).freeze
66
+ version_defaults[0.0r] = Config[version_defaults.fetch(0.0r)]
67
+
68
+ VERSIONS.each_cons(2) do |prior, version|
69
+ updates = version_defaults[version]
70
+ version_defaults[version] = version_defaults[prior]
71
+ .then { updates ? _1.dup.update(**updates).freeze : _1 }
72
+ end
73
+
74
+ # Safe conversions one way only:
75
+ # 0.6r.to_f == 0.6 # => true
76
+ # 0.6 .to_r == 0.6r # => false
77
+ version_defaults.to_a.each do |k, v|
78
+ next unless k in Rational
79
+ version_defaults[k.to_f] = v
80
+ end
81
+
82
+ version_defaults[:original] = Config[0.0r]
83
+ version_defaults[:current] = Config[CURRENT_VERSION]
84
+ version_defaults[:next] = Config[NEXT_VERSION]
85
+ version_defaults[:future] = Config[FUTURE_VERSION]
86
+
87
+ version_defaults.freeze
88
+ end
89
+
90
+ end
91
+ end
92
+ end
93
+ end