activesupport 6.0.6.1 → 6.1.7.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (133) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +441 -455
  3. data/MIT-LICENSE +1 -1
  4. data/lib/active_support/array_inquirer.rb +4 -2
  5. data/lib/active_support/backtrace_cleaner.rb +3 -3
  6. data/lib/active_support/benchmarkable.rb +1 -1
  7. data/lib/active_support/cache/file_store.rb +3 -3
  8. data/lib/active_support/cache/mem_cache_store.rb +28 -18
  9. data/lib/active_support/cache/memory_store.rb +46 -26
  10. data/lib/active_support/cache/redis_cache_store.rb +25 -25
  11. data/lib/active_support/cache/strategy/local_cache.rb +20 -5
  12. data/lib/active_support/cache.rb +87 -40
  13. data/lib/active_support/callbacks.rb +65 -56
  14. data/lib/active_support/concern.rb +46 -2
  15. data/lib/active_support/configurable.rb +3 -3
  16. data/lib/active_support/configuration_file.rb +51 -0
  17. data/lib/active_support/core_ext/benchmark.rb +2 -2
  18. data/lib/active_support/core_ext/class/attribute.rb +34 -44
  19. data/lib/active_support/core_ext/class/subclasses.rb +17 -38
  20. data/lib/active_support/core_ext/date/conversions.rb +2 -1
  21. data/lib/active_support/core_ext/date_and_time/calculations.rb +13 -0
  22. data/lib/active_support/core_ext/date_and_time/compatibility.rb +15 -0
  23. data/lib/active_support/core_ext/digest/uuid.rb +1 -0
  24. data/lib/active_support/core_ext/enumerable.rb +76 -4
  25. data/lib/active_support/core_ext/hash/conversions.rb +2 -2
  26. data/lib/active_support/core_ext/hash/deep_transform_values.rb +1 -1
  27. data/lib/active_support/core_ext/hash/keys.rb +1 -1
  28. data/lib/active_support/core_ext/hash/slice.rb +3 -2
  29. data/lib/active_support/core_ext/load_error.rb +1 -1
  30. data/lib/active_support/core_ext/marshal.rb +2 -0
  31. data/lib/active_support/core_ext/module/attr_internal.rb +2 -2
  32. data/lib/active_support/core_ext/module/attribute_accessors.rb +23 -29
  33. data/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb +8 -4
  34. data/lib/active_support/core_ext/module/concerning.rb +8 -2
  35. data/lib/active_support/core_ext/module/delegation.rb +38 -28
  36. data/lib/active_support/core_ext/module/introspection.rb +1 -25
  37. data/lib/active_support/core_ext/name_error.rb +29 -2
  38. data/lib/active_support/core_ext/numeric/conversions.rb +22 -18
  39. data/lib/active_support/core_ext/object/deep_dup.rb +1 -1
  40. data/lib/active_support/core_ext/object/json.rb +12 -1
  41. data/lib/active_support/core_ext/object/try.rb +2 -2
  42. data/lib/active_support/core_ext/range/compare_range.rb +9 -3
  43. data/lib/active_support/core_ext/range/include_time_with_zone.rb +8 -3
  44. data/lib/active_support/core_ext/regexp.rb +8 -1
  45. data/lib/active_support/core_ext/string/access.rb +5 -24
  46. data/lib/active_support/core_ext/string/conversions.rb +1 -0
  47. data/lib/active_support/core_ext/string/inflections.rb +38 -4
  48. data/lib/active_support/core_ext/string/inquiry.rb +1 -0
  49. data/lib/active_support/core_ext/string/multibyte.rb +2 -2
  50. data/lib/active_support/core_ext/string/output_safety.rb +7 -4
  51. data/lib/active_support/core_ext/string/starts_ends_with.rb +2 -2
  52. data/lib/active_support/core_ext/symbol/starts_ends_with.rb +14 -0
  53. data/lib/active_support/core_ext/symbol.rb +3 -0
  54. data/lib/active_support/core_ext/time/calculations.rb +19 -0
  55. data/lib/active_support/core_ext/time/conversions.rb +2 -0
  56. data/lib/active_support/core_ext/uri.rb +5 -1
  57. data/lib/active_support/core_ext.rb +1 -1
  58. data/lib/active_support/current_attributes/test_helper.rb +13 -0
  59. data/lib/active_support/current_attributes.rb +9 -2
  60. data/lib/active_support/dependencies/zeitwerk_integration.rb +4 -1
  61. data/lib/active_support/dependencies.rb +37 -18
  62. data/lib/active_support/deprecation/behaviors.rb +15 -2
  63. data/lib/active_support/deprecation/disallowed.rb +56 -0
  64. data/lib/active_support/deprecation/instance_delegator.rb +0 -1
  65. data/lib/active_support/deprecation/method_wrappers.rb +3 -2
  66. data/lib/active_support/deprecation/proxy_wrappers.rb +2 -2
  67. data/lib/active_support/deprecation/reporting.rb +50 -7
  68. data/lib/active_support/deprecation.rb +6 -1
  69. data/lib/active_support/descendants_tracker.rb +6 -2
  70. data/lib/active_support/digest.rb +2 -0
  71. data/lib/active_support/duration/iso8601_serializer.rb +15 -9
  72. data/lib/active_support/duration.rb +75 -25
  73. data/lib/active_support/encrypted_file.rb +27 -11
  74. data/lib/active_support/environment_inquirer.rb +20 -0
  75. data/lib/active_support/evented_file_update_checker.rb +69 -133
  76. data/lib/active_support/fork_tracker.rb +64 -0
  77. data/lib/active_support/gem_version.rb +3 -3
  78. data/lib/active_support/hash_with_indifferent_access.rb +48 -24
  79. data/lib/active_support/i18n_railtie.rb +14 -19
  80. data/lib/active_support/inflector/inflections.rb +1 -2
  81. data/lib/active_support/inflector/methods.rb +36 -33
  82. data/lib/active_support/inflector/transliterate.rb +4 -4
  83. data/lib/active_support/json/decoding.rb +4 -4
  84. data/lib/active_support/json/encoding.rb +5 -1
  85. data/lib/active_support/key_generator.rb +1 -1
  86. data/lib/active_support/locale/en.yml +7 -3
  87. data/lib/active_support/log_subscriber.rb +8 -0
  88. data/lib/active_support/logger.rb +1 -1
  89. data/lib/active_support/logger_silence.rb +2 -26
  90. data/lib/active_support/logger_thread_safe_level.rb +34 -12
  91. data/lib/active_support/message_encryptor.rb +4 -7
  92. data/lib/active_support/message_verifier.rb +5 -5
  93. data/lib/active_support/messages/rotation_configuration.rb +2 -1
  94. data/lib/active_support/messages/rotator.rb +6 -5
  95. data/lib/active_support/multibyte/chars.rb +4 -42
  96. data/lib/active_support/multibyte/unicode.rb +9 -83
  97. data/lib/active_support/notifications/fanout.rb +23 -8
  98. data/lib/active_support/notifications/instrumenter.rb +6 -15
  99. data/lib/active_support/notifications.rb +32 -5
  100. data/lib/active_support/number_helper/number_converter.rb +1 -1
  101. data/lib/active_support/number_helper/number_to_human_converter.rb +1 -1
  102. data/lib/active_support/number_helper/number_to_human_size_converter.rb +1 -1
  103. data/lib/active_support/number_helper/number_to_rounded_converter.rb +9 -5
  104. data/lib/active_support/number_helper/rounding_helper.rb +12 -28
  105. data/lib/active_support/number_helper.rb +29 -14
  106. data/lib/active_support/option_merger.rb +2 -1
  107. data/lib/active_support/ordered_options.rb +8 -2
  108. data/lib/active_support/parameter_filter.rb +16 -11
  109. data/lib/active_support/per_thread_registry.rb +2 -1
  110. data/lib/active_support/rails.rb +1 -4
  111. data/lib/active_support/railtie.rb +23 -1
  112. data/lib/active_support/rescuable.rb +4 -4
  113. data/lib/active_support/secure_compare_rotator.rb +51 -0
  114. data/lib/active_support/security_utils.rb +19 -12
  115. data/lib/active_support/string_inquirer.rb +4 -2
  116. data/lib/active_support/subscriber.rb +12 -7
  117. data/lib/active_support/tagged_logging.rb +30 -5
  118. data/lib/active_support/testing/assertions.rb +18 -11
  119. data/lib/active_support/testing/parallelization/server.rb +78 -0
  120. data/lib/active_support/testing/parallelization/worker.rb +100 -0
  121. data/lib/active_support/testing/parallelization.rb +12 -95
  122. data/lib/active_support/testing/time_helpers.rb +40 -3
  123. data/lib/active_support/time_with_zone.rb +67 -43
  124. data/lib/active_support/values/time_zone.rb +22 -10
  125. data/lib/active_support/xml_mini/rexml.rb +8 -1
  126. data/lib/active_support.rb +13 -1
  127. metadata +34 -36
  128. data/lib/active_support/core_ext/array/prepend_and_append.rb +0 -5
  129. data/lib/active_support/core_ext/hash/compact.rb +0 -5
  130. data/lib/active_support/core_ext/hash/transform_values.rb +0 -5
  131. data/lib/active_support/core_ext/module/reachable.rb +0 -6
  132. data/lib/active_support/core_ext/numeric/inquiry.rb +0 -5
  133. data/lib/active_support/core_ext/range/include_range.rb +0 -9
@@ -13,6 +13,7 @@ module ActiveSupport
13
13
  descendants = @@direct_descendants[klass]
14
14
  descendants ? descendants.to_a : []
15
15
  end
16
+ alias_method :subclasses, :direct_descendants
16
17
 
17
18
  def descendants(klass)
18
19
  arr = []
@@ -59,6 +60,7 @@ module ActiveSupport
59
60
  def direct_descendants
60
61
  DescendantsTracker.direct_descendants(self)
61
62
  end
63
+ alias_method :subclasses, :direct_descendants
62
64
 
63
65
  def descendants
64
66
  DescendantsTracker.descendants(self)
@@ -77,15 +79,17 @@ module ActiveSupport
77
79
  end
78
80
 
79
81
  def <<(klass)
80
- cleanup!
81
82
  @refs << WeakRef.new(klass)
82
83
  end
83
84
 
84
85
  def each
85
- @refs.each do |ref|
86
+ @refs.reject! do |ref|
86
87
  yield ref.__getobj__
88
+ false
87
89
  rescue WeakRef::RefError
90
+ true
88
91
  end
92
+ self
89
93
  end
90
94
 
91
95
  def refs_size
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "digest"
4
+
3
5
  module ActiveSupport
4
6
  class Digest #:nodoc:
5
7
  class <<self
@@ -6,6 +6,8 @@ module ActiveSupport
6
6
  class Duration
7
7
  # Serializes duration to string according to ISO 8601 Duration format.
8
8
  class ISO8601Serializer # :nodoc:
9
+ DATE_COMPONENTS = %i(years months days)
10
+
9
11
  def initialize(duration, precision: nil)
10
12
  @duration = duration
11
13
  @precision = precision
@@ -13,14 +15,14 @@ module ActiveSupport
13
15
 
14
16
  # Builds and returns output string.
15
17
  def serialize
16
- parts, sign = normalize
18
+ parts = normalize
17
19
  return "PT0S" if parts.empty?
18
20
 
19
21
  output = +"P"
20
22
  output << "#{parts[:years]}Y" if parts.key?(:years)
21
23
  output << "#{parts[:months]}M" if parts.key?(:months)
22
- output << "#{parts[:weeks]}W" if parts.key?(:weeks)
23
24
  output << "#{parts[:days]}D" if parts.key?(:days)
25
+ output << "#{parts[:weeks]}W" if parts.key?(:weeks)
24
26
  time = +""
25
27
  time << "#{parts[:hours]}H" if parts.key?(:hours)
26
28
  time << "#{parts[:minutes]}M" if parts.key?(:minutes)
@@ -28,7 +30,7 @@ module ActiveSupport
28
30
  time << "#{sprintf(@precision ? "%0.0#{@precision}f" : '%g', parts[:seconds])}S"
29
31
  end
30
32
  output << "T#{time}" unless time.empty?
31
- "#{sign}#{output}"
33
+ output
32
34
  end
33
35
 
34
36
  private
@@ -40,13 +42,17 @@ module ActiveSupport
40
42
  parts = @duration.parts.each_with_object(Hash.new(0)) do |(k, v), p|
41
43
  p[k] += v unless v.zero?
42
44
  end
43
- # If all parts are negative - let's make a negative duration
44
- sign = ""
45
- if parts.values.all? { |v| v < 0 }
46
- sign = "-"
47
- parts.transform_values!(&:-@)
45
+
46
+ # Convert weeks to days and remove weeks if mixed with date parts
47
+ if week_mixed_with_date?(parts)
48
+ parts[:days] += parts.delete(:weeks) * SECONDS_PER_WEEK / SECONDS_PER_DAY
48
49
  end
49
- [parts, sign]
50
+
51
+ parts
52
+ end
53
+
54
+ def week_mixed_with_date?(parts)
55
+ parts.key?(:weeks) && (parts.keys & DATE_COMPONENTS).any?
50
56
  end
51
57
  end
52
58
  end
@@ -39,7 +39,7 @@ module ActiveSupport
39
39
 
40
40
  def +(other)
41
41
  if Duration === other
42
- seconds = value + other.parts[:seconds]
42
+ seconds = value + other.parts.fetch(:seconds, 0)
43
43
  new_parts = other.parts.merge(seconds: seconds)
44
44
  new_value = value + other.value
45
45
 
@@ -51,8 +51,8 @@ module ActiveSupport
51
51
 
52
52
  def -(other)
53
53
  if Duration === other
54
- seconds = value - other.parts[:seconds]
55
- new_parts = other.parts.map { |part, other_value| [part, -other_value] }.to_h
54
+ seconds = value - other.parts.fetch(:seconds, 0)
55
+ new_parts = other.parts.transform_values(&:-@)
56
56
  new_parts = new_parts.merge(seconds: seconds)
57
57
  new_value = value - other.value
58
58
 
@@ -64,7 +64,7 @@ module ActiveSupport
64
64
 
65
65
  def *(other)
66
66
  if Duration === other
67
- new_parts = other.parts.map { |part, other_value| [part, value * other_value] }.to_h
67
+ new_parts = other.parts.transform_values { |other_value| value * other_value }
68
68
  new_value = value * other.value
69
69
 
70
70
  Duration.new(new_value, new_parts)
@@ -147,31 +147,31 @@ module ActiveSupport
147
147
  end
148
148
 
149
149
  def seconds(value) #:nodoc:
150
- new(value, [[:seconds, value]])
150
+ new(value, seconds: value)
151
151
  end
152
152
 
153
153
  def minutes(value) #:nodoc:
154
- new(value * SECONDS_PER_MINUTE, [[:minutes, value]])
154
+ new(value * SECONDS_PER_MINUTE, minutes: value)
155
155
  end
156
156
 
157
157
  def hours(value) #:nodoc:
158
- new(value * SECONDS_PER_HOUR, [[:hours, value]])
158
+ new(value * SECONDS_PER_HOUR, hours: value)
159
159
  end
160
160
 
161
161
  def days(value) #:nodoc:
162
- new(value * SECONDS_PER_DAY, [[:days, value]])
162
+ new(value * SECONDS_PER_DAY, days: value)
163
163
  end
164
164
 
165
165
  def weeks(value) #:nodoc:
166
- new(value * SECONDS_PER_WEEK, [[:weeks, value]])
166
+ new(value * SECONDS_PER_WEEK, weeks: value)
167
167
  end
168
168
 
169
169
  def months(value) #:nodoc:
170
- new(value * SECONDS_PER_MONTH, [[:months, value]])
170
+ new(value * SECONDS_PER_MONTH, months: value)
171
171
  end
172
172
 
173
173
  def years(value) #:nodoc:
174
- new(value * SECONDS_PER_YEAR, [[:years, value]])
174
+ new(value * SECONDS_PER_YEAR, years: value)
175
175
  end
176
176
 
177
177
  # Creates a new Duration from a seconds value that is converted
@@ -181,18 +181,23 @@ module ActiveSupport
181
181
  # ActiveSupport::Duration.build(2716146).parts # => {:months=>1, :days=>1}
182
182
  #
183
183
  def build(value)
184
+ unless value.is_a?(::Numeric)
185
+ raise TypeError, "can't build an #{self.name} from a #{value.class.name}"
186
+ end
187
+
184
188
  parts = {}
185
- remainder = value.round(9)
189
+ remainder_sign = value <=> 0
190
+ remainder = value.round(9).abs
186
191
 
187
192
  PARTS.each do |part|
188
193
  unless part == :seconds
189
194
  part_in_seconds = PARTS_IN_SECONDS[part]
190
- parts[part] = remainder.div(part_in_seconds)
195
+ parts[part] = remainder.div(part_in_seconds) * remainder_sign
191
196
  remainder %= part_in_seconds
192
197
  end
193
198
  end unless value == 0
194
199
 
195
- parts[:seconds] = remainder
200
+ parts[:seconds] = remainder * remainder_sign
196
201
 
197
202
  new(value, parts)
198
203
  end
@@ -206,8 +211,7 @@ module ActiveSupport
206
211
  end
207
212
 
208
213
  def initialize(value, parts) #:nodoc:
209
- @value, @parts = value, parts.to_h
210
- @parts.default = 0
214
+ @value, @parts = value, parts
211
215
  @parts.reject! { |k, v| v.zero? } unless value == 0
212
216
  end
213
217
 
@@ -236,13 +240,12 @@ module ActiveSupport
236
240
  # are treated as seconds.
237
241
  def +(other)
238
242
  if Duration === other
239
- parts = @parts.dup
240
- other.parts.each do |(key, value)|
241
- parts[key] += value
243
+ parts = @parts.merge(other.parts) do |_key, value, other_value|
244
+ value + other_value
242
245
  end
243
246
  Duration.new(value + other.value, parts)
244
247
  else
245
- seconds = @parts[:seconds] + other
248
+ seconds = @parts.fetch(:seconds, 0) + other
246
249
  Duration.new(value + other, @parts.merge(seconds: seconds))
247
250
  end
248
251
  end
@@ -256,9 +259,9 @@ module ActiveSupport
256
259
  # Multiplies this Duration by a Numeric and returns a new Duration.
257
260
  def *(other)
258
261
  if Scalar === other || Duration === other
259
- Duration.new(value * other.value, parts.map { |type, number| [type, number * other.value] })
262
+ Duration.new(value * other.value, parts.transform_values { |number| number * other.value })
260
263
  elsif Numeric === other
261
- Duration.new(value * other, parts.map { |type, number| [type, number * other] })
264
+ Duration.new(value * other, parts.transform_values { |number| number * other })
262
265
  else
263
266
  raise_type_error(other)
264
267
  end
@@ -267,11 +270,11 @@ module ActiveSupport
267
270
  # Divides this Duration by a Numeric and returns a new Duration.
268
271
  def /(other)
269
272
  if Scalar === other
270
- Duration.new(value / other.value, parts.map { |type, number| [type, number / other.value] })
273
+ Duration.new(value / other.value, parts.transform_values { |number| number / other.value })
271
274
  elsif Duration === other
272
275
  value / other.value
273
276
  elsif Numeric === other
274
- Duration.new(value / other, parts.map { |type, number| [type, number / other] })
277
+ Duration.new(value / other, parts.transform_values { |number| number / other })
275
278
  else
276
279
  raise_type_error(other)
277
280
  end
@@ -290,7 +293,11 @@ module ActiveSupport
290
293
  end
291
294
 
292
295
  def -@ #:nodoc:
293
- Duration.new(-value, parts.map { |type, number| [type, -number] })
296
+ Duration.new(-value, parts.transform_values(&:-@))
297
+ end
298
+
299
+ def +@ #:nodoc:
300
+ self
294
301
  end
295
302
 
296
303
  def is_a?(klass) #:nodoc:
@@ -343,6 +350,49 @@ module ActiveSupport
343
350
  def to_i
344
351
  @value.to_i
345
352
  end
353
+ alias :in_seconds :to_i
354
+
355
+ # Returns the amount of minutes a duration covers as a float
356
+ #
357
+ # 1.day.in_minutes # => 1440.0
358
+ def in_minutes
359
+ in_seconds / SECONDS_PER_MINUTE.to_f
360
+ end
361
+
362
+ # Returns the amount of hours a duration covers as a float
363
+ #
364
+ # 1.day.in_hours # => 24.0
365
+ def in_hours
366
+ in_seconds / SECONDS_PER_HOUR.to_f
367
+ end
368
+
369
+ # Returns the amount of days a duration covers as a float
370
+ #
371
+ # 12.hours.in_days # => 0.5
372
+ def in_days
373
+ in_seconds / SECONDS_PER_DAY.to_f
374
+ end
375
+
376
+ # Returns the amount of weeks a duration covers as a float
377
+ #
378
+ # 2.months.in_weeks # => 8.696
379
+ def in_weeks
380
+ in_seconds / SECONDS_PER_WEEK.to_f
381
+ end
382
+
383
+ # Returns the amount of months a duration covers as a float
384
+ #
385
+ # 9.weeks.in_months # => 2.07
386
+ def in_months
387
+ in_seconds / SECONDS_PER_MONTH.to_f
388
+ end
389
+
390
+ # Returns the amount of years a duration covers as a float
391
+ #
392
+ # 30.days.in_years # => 0.082
393
+ def in_years
394
+ in_seconds / SECONDS_PER_YEAR.to_f
395
+ end
346
396
 
347
397
  # Returns +true+ if +other+ is also a Duration instance, which has the
348
398
  # same parts as this one.
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "pathname"
4
- require "tmpdir"
4
+ require "tempfile"
5
5
  require "active_support/message_encryptor"
6
6
 
7
7
  module ActiveSupport
@@ -20,17 +20,28 @@ module ActiveSupport
20
20
  end
21
21
  end
22
22
 
23
+ class InvalidKeyLengthError < RuntimeError
24
+ def initialize
25
+ super "Encryption key must be exactly #{EncryptedFile.expected_key_length} characters."
26
+ end
27
+ end
28
+
23
29
  CIPHER = "aes-128-gcm"
24
30
 
25
31
  def self.generate_key
26
32
  SecureRandom.hex(ActiveSupport::MessageEncryptor.key_len(CIPHER))
27
33
  end
28
34
 
35
+ def self.expected_key_length # :nodoc:
36
+ @expected_key_length ||= generate_key.length
37
+ end
38
+
29
39
 
30
40
  attr_reader :content_path, :key_path, :env_key, :raise_if_missing_key
31
41
 
32
42
  def initialize(content_path:, key_path:, env_key:, raise_if_missing_key:)
33
- @content_path, @key_path = Pathname.new(content_path), Pathname.new(key_path)
43
+ @content_path = Pathname.new(content_path).yield_self { |path| path.symlink? ? path.realpath : path }
44
+ @key_path = Pathname.new(key_path)
34
45
  @env_key, @raise_if_missing_key = env_key, raise_if_missing_key
35
46
  end
36
47
 
@@ -58,21 +69,21 @@ module ActiveSupport
58
69
 
59
70
  private
60
71
  def writing(contents)
61
- tmp_file = "#{Process.pid}.#{content_path.basename.to_s.chomp('.enc')}"
62
- tmp_path = Pathname.new File.join(Dir.tmpdir, tmp_file)
63
- tmp_path.binwrite contents
72
+ Tempfile.create(["", "-" + content_path.basename.to_s.chomp(".enc")]) do |tmp_file|
73
+ tmp_path = Pathname.new(tmp_file)
74
+ tmp_path.binwrite contents
64
75
 
65
- yield tmp_path
76
+ yield tmp_path
66
77
 
67
- updated_contents = tmp_path.binread
78
+ updated_contents = tmp_path.binread
68
79
 
69
- write(updated_contents) if updated_contents != contents
70
- ensure
71
- FileUtils.rm(tmp_path) if tmp_path&.exist?
80
+ write(updated_contents) if updated_contents != contents
81
+ end
72
82
  end
73
83
 
74
84
 
75
85
  def encrypt(contents)
86
+ check_key_length
76
87
  encryptor.encrypt_and_sign contents
77
88
  end
78
89
 
@@ -90,11 +101,16 @@ module ActiveSupport
90
101
  end
91
102
 
92
103
  def read_key_file
93
- key_path.binread.strip if key_path.exist?
104
+ return @key_file_contents if defined?(@key_file_contents)
105
+ @key_file_contents = (key_path.binread.strip if key_path.exist?)
94
106
  end
95
107
 
96
108
  def handle_missing_key
97
109
  raise MissingKeyError.new(key_path: key_path, env_key: env_key) if raise_if_missing_key
98
110
  end
111
+
112
+ def check_key_length
113
+ raise InvalidKeyLengthError if key&.length != self.class.expected_key_length
114
+ end
99
115
  end
100
116
  end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/string_inquirer"
4
+
5
+ module ActiveSupport
6
+ class EnvironmentInquirer < StringInquirer #:nodoc:
7
+ DEFAULT_ENVIRONMENTS = ["development", "test", "production"]
8
+ def initialize(env)
9
+ super(env)
10
+
11
+ DEFAULT_ENVIRONMENTS.each do |default|
12
+ instance_variable_set :"@#{default}", env == default
13
+ end
14
+ end
15
+
16
+ DEFAULT_ENVIRONMENTS.each do |env|
17
+ class_eval "def #{env}?; @#{env}; end"
18
+ end
19
+ end
20
+ end
@@ -3,6 +3,8 @@
3
3
  require "set"
4
4
  require "pathname"
5
5
  require "concurrent/atomic/atomic_boolean"
6
+ require "listen"
7
+ require "active_support/fork_tracker"
6
8
 
7
9
  module ActiveSupport
8
10
  # Allows you to "listen" to changes in a file system.
@@ -38,62 +40,22 @@ module ActiveSupport
38
40
  raise ArgumentError, "A block is required to initialize an EventedFileUpdateChecker"
39
41
  end
40
42
 
41
- @ph = PathHelper.new
42
- @files = files.map { |f| @ph.xpath(f) }.to_set
43
-
44
- @dirs = {}
45
- dirs.each do |dir, exts|
46
- @dirs[@ph.xpath(dir)] = Array(exts).map { |ext| @ph.normalize_extension(ext) }
47
- end
48
-
49
- @block = block
50
- @updated = Concurrent::AtomicBoolean.new(false)
51
- @lcsp = @ph.longest_common_subpath(@dirs.keys)
52
- @pid = Process.pid
53
- @boot_mutex = Mutex.new
54
-
55
- dtw = directories_to_watch
56
- @dtw, @missing = dtw.partition(&:exist?)
57
-
58
- if @dtw.any?
59
- # Loading listen triggers warnings. These are originated by a legit
60
- # usage of attr_* macros for private attributes, but adds a lot of noise
61
- # to our test suite. Thus, we lazy load it and disable warnings locally.
62
- silence_warnings do
63
- require "listen"
64
- rescue LoadError => e
65
- raise LoadError, "Could not load the 'listen' gem. Add `gem 'listen'` to the development group of your Gemfile", e.backtrace
66
- end
67
- end
68
- boot!
43
+ @block = block
44
+ @core = Core.new(files, dirs)
45
+ ObjectSpace.define_finalizer(self, @core.finalizer)
69
46
  end
70
47
 
71
48
  def updated?
72
- @boot_mutex.synchronize do
73
- if @pid != Process.pid
74
- boot!
75
- @pid = Process.pid
76
- @updated.make_true
77
- end
78
- end
79
-
80
- if @missing.any?(&:exist?)
81
- @boot_mutex.synchronize do
82
- appeared, @missing = @missing.partition(&:exist?)
83
- shutdown!
84
-
85
- @dtw += appeared
86
- boot!
87
-
88
- @updated.make_true
89
- end
49
+ if @core.restart?
50
+ @core.thread_safely(&:restart)
51
+ @core.updated.make_true
90
52
  end
91
53
 
92
- @updated.true?
54
+ @core.updated.true?
93
55
  end
94
56
 
95
57
  def execute
96
- @updated.make_false
58
+ @core.updated.make_false
97
59
  @block.call
98
60
  end
99
61
 
@@ -105,17 +67,59 @@ module ActiveSupport
105
67
  end
106
68
  end
107
69
 
108
- private
109
- def boot!
110
- normalize_dirs!
70
+ class Core
71
+ attr_reader :updated
72
+
73
+ def initialize(files, dirs)
74
+ @files = files.map { |file| Pathname(file).expand_path }.to_set
75
+
76
+ @dirs = dirs.each_with_object({}) do |(dir, exts), hash|
77
+ hash[Pathname(dir).expand_path] = Array(exts).map { |ext| ext.to_s.sub(/\A\.?/, ".") }.to_set
78
+ end
79
+
80
+ @common_path = common_path(@dirs.keys)
81
+
82
+ @dtw = directories_to_watch
83
+ @missing = []
84
+
85
+ @updated = Concurrent::AtomicBoolean.new(false)
86
+ @mutex = Mutex.new
87
+
88
+ start
89
+ @after_fork = ActiveSupport::ForkTracker.after_fork { start }
90
+ end
91
+
92
+ def finalizer
93
+ proc do
94
+ stop
95
+ ActiveSupport::ForkTracker.unregister(@after_fork)
96
+ end
97
+ end
111
98
 
112
- unless @dtw.empty?
113
- Listen.to(*@dtw, &method(:changed)).start
99
+ def thread_safely
100
+ @mutex.synchronize do
101
+ yield self
114
102
  end
115
103
  end
116
104
 
117
- def shutdown!
118
- Listen.stop
105
+ def start
106
+ normalize_dirs!
107
+ @dtw, @missing = [*@dtw, *@missing].partition(&:exist?)
108
+ @listener = @dtw.any? ? Listen.to(*@dtw, &method(:changed)) : nil
109
+ @listener&.start
110
+ end
111
+
112
+ def stop
113
+ @listener&.stop
114
+ end
115
+
116
+ def restart
117
+ stop
118
+ start
119
+ end
120
+
121
+ def restart?
122
+ @missing.any?(&:exist?)
119
123
  end
120
124
 
121
125
  def normalize_dirs!
@@ -125,27 +129,27 @@ module ActiveSupport
125
129
  end
126
130
 
127
131
  def changed(modified, added, removed)
128
- unless updated?
132
+ unless @updated.true?
129
133
  @updated.make_true if (modified + added + removed).any? { |f| watching?(f) }
130
134
  end
131
135
  end
132
136
 
133
137
  def watching?(file)
134
- file = @ph.xpath(file)
138
+ file = Pathname(file)
135
139
 
136
140
  if @files.member?(file)
137
141
  true
138
142
  elsif file.directory?
139
143
  false
140
144
  else
141
- ext = @ph.normalize_extension(file.extname)
145
+ ext = file.extname
142
146
 
143
147
  file.dirname.ascend do |dir|
144
148
  matching = @dirs[dir]
145
149
 
146
150
  if matching && (matching.empty? || matching.include?(ext))
147
151
  break true
148
- elsif dir == @lcsp || dir.root?
152
+ elsif dir == @common_path || dir.root?
149
153
  break false
150
154
  end
151
155
  end
@@ -153,82 +157,14 @@ module ActiveSupport
153
157
  end
154
158
 
155
159
  def directories_to_watch
156
- dtw = @files.map(&:dirname) + @dirs.keys
157
- dtw.compact!
158
- dtw.uniq!
159
-
160
- normalized_gem_paths = Gem.path.map { |path| File.join path, "" }
161
- dtw = dtw.reject do |path|
162
- normalized_gem_paths.any? { |gem_path| path.to_s.start_with?(gem_path) }
163
- end
164
-
165
- @ph.filter_out_descendants(dtw)
160
+ dtw = @dirs.keys | @files.map(&:dirname)
161
+ accounted_for = dtw.to_set + Gem.path.map { |path| Pathname(path) }
162
+ dtw.reject { |dir| dir.ascend.drop(1).any? { |parent| accounted_for.include?(parent) } }
166
163
  end
167
164
 
168
- class PathHelper
169
- def xpath(path)
170
- Pathname.new(path).expand_path
171
- end
172
-
173
- def normalize_extension(ext)
174
- ext.to_s.sub(/\A\./, "")
175
- end
176
-
177
- # Given a collection of Pathname objects returns the longest subpath
178
- # common to all of them, or +nil+ if there is none.
179
- def longest_common_subpath(paths)
180
- return if paths.empty?
181
-
182
- lcsp = Pathname.new(paths[0])
183
-
184
- paths[1..-1].each do |path|
185
- until ascendant_of?(lcsp, path)
186
- if lcsp.root?
187
- # If we get here a root directory is not an ascendant of path.
188
- # This may happen if there are paths in different drives on
189
- # Windows.
190
- return
191
- else
192
- lcsp = lcsp.parent
193
- end
194
- end
195
- end
196
-
197
- lcsp
198
- end
199
-
200
- # Returns the deepest existing ascendant, which could be the argument itself.
201
- def existing_parent(dir)
202
- dir.ascend do |ascendant|
203
- break ascendant if ascendant.directory?
204
- end
205
- end
206
-
207
- # Filters out directories which are descendants of others in the collection (stable).
208
- def filter_out_descendants(dirs)
209
- return dirs if dirs.length < 2
210
-
211
- dirs_sorted_by_nparts = dirs.sort_by { |dir| dir.each_filename.to_a.length }
212
- descendants = []
213
-
214
- until dirs_sorted_by_nparts.empty?
215
- dir = dirs_sorted_by_nparts.shift
216
-
217
- dirs_sorted_by_nparts.reject! do |possible_descendant|
218
- ascendant_of?(dir, possible_descendant) && descendants << possible_descendant
219
- end
220
- end
221
-
222
- # Array#- preserves order.
223
- dirs - descendants
224
- end
225
-
226
- private
227
- def ascendant_of?(base, other)
228
- base != other && other.ascend do |ascendant|
229
- break true if base == ascendant
230
- end
231
- end
165
+ def common_path(paths)
166
+ paths.map { |path| path.ascend.to_a }.reduce(&:&)&.first
232
167
  end
168
+ end
233
169
  end
234
170
  end