activesupport 6.1.4.1 → 7.0.8.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (185) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +325 -395
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +2 -2
  5. data/lib/active_support/actionable_error.rb +1 -1
  6. data/lib/active_support/array_inquirer.rb +0 -2
  7. data/lib/active_support/backtrace_cleaner.rb +2 -2
  8. data/lib/active_support/benchmarkable.rb +2 -2
  9. data/lib/active_support/cache/file_store.rb +15 -9
  10. data/lib/active_support/cache/mem_cache_store.rb +148 -37
  11. data/lib/active_support/cache/memory_store.rb +24 -16
  12. data/lib/active_support/cache/null_store.rb +10 -2
  13. data/lib/active_support/cache/redis_cache_store.rb +68 -85
  14. data/lib/active_support/cache/strategy/local_cache.rb +38 -61
  15. data/lib/active_support/cache.rb +299 -147
  16. data/lib/active_support/callbacks.rb +184 -85
  17. data/lib/active_support/code_generator.rb +65 -0
  18. data/lib/active_support/concern.rb +5 -5
  19. data/lib/active_support/concurrency/load_interlock_aware_monitor.rb +2 -4
  20. data/lib/active_support/concurrency/share_lock.rb +2 -2
  21. data/lib/active_support/configurable.rb +8 -5
  22. data/lib/active_support/configuration_file.rb +1 -1
  23. data/lib/active_support/core_ext/array/access.rb +1 -5
  24. data/lib/active_support/core_ext/array/conversions.rb +13 -12
  25. data/lib/active_support/core_ext/array/deprecated_conversions.rb +25 -0
  26. data/lib/active_support/core_ext/array/grouping.rb +6 -6
  27. data/lib/active_support/core_ext/array/inquiry.rb +2 -2
  28. data/lib/active_support/core_ext/array.rb +1 -0
  29. data/lib/active_support/core_ext/big_decimal/conversions.rb +1 -1
  30. data/lib/active_support/core_ext/class/subclasses.rb +25 -17
  31. data/lib/active_support/core_ext/date/blank.rb +1 -1
  32. data/lib/active_support/core_ext/date/calculations.rb +24 -9
  33. data/lib/active_support/core_ext/date/conversions.rb +14 -14
  34. data/lib/active_support/core_ext/date/deprecated_conversions.rb +40 -0
  35. data/lib/active_support/core_ext/date.rb +1 -0
  36. data/lib/active_support/core_ext/date_and_time/calculations.rb +4 -4
  37. data/lib/active_support/core_ext/date_and_time/compatibility.rb +1 -1
  38. data/lib/active_support/core_ext/date_time/blank.rb +1 -1
  39. data/lib/active_support/core_ext/date_time/calculations.rb +4 -0
  40. data/lib/active_support/core_ext/date_time/conversions.rb +13 -13
  41. data/lib/active_support/core_ext/date_time/deprecated_conversions.rb +36 -0
  42. data/lib/active_support/core_ext/date_time.rb +1 -0
  43. data/lib/active_support/core_ext/digest/uuid.rb +39 -13
  44. data/lib/active_support/core_ext/enumerable.rb +112 -38
  45. data/lib/active_support/core_ext/file/atomic.rb +3 -1
  46. data/lib/active_support/core_ext/hash/conversions.rb +0 -1
  47. data/lib/active_support/core_ext/hash/deep_transform_values.rb +3 -3
  48. data/lib/active_support/core_ext/hash/indifferent_access.rb +3 -3
  49. data/lib/active_support/core_ext/hash/keys.rb +4 -4
  50. data/lib/active_support/core_ext/integer/inflections.rb +12 -12
  51. data/lib/active_support/core_ext/kernel/reporting.rb +4 -4
  52. data/lib/active_support/core_ext/kernel/singleton_class.rb +1 -1
  53. data/lib/active_support/core_ext/module/attribute_accessors.rb +2 -0
  54. data/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb +19 -10
  55. data/lib/active_support/core_ext/module/delegation.rb +2 -8
  56. data/lib/active_support/core_ext/name_error.rb +2 -8
  57. data/lib/active_support/core_ext/numeric/conversions.rb +80 -77
  58. data/lib/active_support/core_ext/numeric/deprecated_conversions.rb +60 -0
  59. data/lib/active_support/core_ext/numeric.rb +1 -0
  60. data/lib/active_support/core_ext/object/acts_like.rb +29 -5
  61. data/lib/active_support/core_ext/object/blank.rb +2 -2
  62. data/lib/active_support/core_ext/object/deep_dup.rb +1 -1
  63. data/lib/active_support/core_ext/object/duplicable.rb +15 -4
  64. data/lib/active_support/core_ext/object/json.rb +30 -25
  65. data/lib/active_support/core_ext/object/to_query.rb +2 -4
  66. data/lib/active_support/core_ext/object/try.rb +20 -20
  67. data/lib/active_support/core_ext/object/with_options.rb +21 -2
  68. data/lib/active_support/core_ext/pathname/existence.rb +21 -0
  69. data/lib/active_support/core_ext/pathname.rb +3 -0
  70. data/lib/active_support/core_ext/range/compare_range.rb +0 -25
  71. data/lib/active_support/core_ext/range/conversions.rb +8 -8
  72. data/lib/active_support/core_ext/range/deprecated_conversions.rb +36 -0
  73. data/lib/active_support/core_ext/range/each.rb +1 -1
  74. data/lib/active_support/core_ext/range/include_time_with_zone.rb +3 -26
  75. data/lib/active_support/core_ext/range/overlaps.rb +1 -1
  76. data/lib/active_support/core_ext/range.rb +1 -1
  77. data/lib/active_support/core_ext/securerandom.rb +1 -1
  78. data/lib/active_support/core_ext/string/conversions.rb +2 -2
  79. data/lib/active_support/core_ext/string/filters.rb +1 -1
  80. data/lib/active_support/core_ext/string/inflections.rb +1 -5
  81. data/lib/active_support/core_ext/string/inquiry.rb +1 -1
  82. data/lib/active_support/core_ext/string/output_safety.rb +94 -38
  83. data/lib/active_support/core_ext/symbol/starts_ends_with.rb +0 -8
  84. data/lib/active_support/core_ext/time/calculations.rb +13 -8
  85. data/lib/active_support/core_ext/time/conversions.rb +13 -12
  86. data/lib/active_support/core_ext/time/deprecated_conversions.rb +73 -0
  87. data/lib/active_support/core_ext/time/zones.rb +10 -26
  88. data/lib/active_support/core_ext/time.rb +1 -0
  89. data/lib/active_support/core_ext/uri.rb +3 -27
  90. data/lib/active_support/core_ext.rb +1 -0
  91. data/lib/active_support/current_attributes.rb +31 -14
  92. data/lib/active_support/dependencies/interlock.rb +10 -18
  93. data/lib/active_support/dependencies/require_dependency.rb +28 -0
  94. data/lib/active_support/dependencies.rb +58 -788
  95. data/lib/active_support/deprecation/behaviors.rb +8 -5
  96. data/lib/active_support/deprecation/disallowed.rb +3 -3
  97. data/lib/active_support/deprecation/method_wrappers.rb +3 -3
  98. data/lib/active_support/deprecation/proxy_wrappers.rb +2 -2
  99. data/lib/active_support/deprecation.rb +2 -2
  100. data/lib/active_support/descendants_tracker.rb +174 -68
  101. data/lib/active_support/digest.rb +5 -3
  102. data/lib/active_support/duration/iso8601_parser.rb +3 -3
  103. data/lib/active_support/duration/iso8601_serializer.rb +9 -1
  104. data/lib/active_support/duration.rb +81 -51
  105. data/lib/active_support/encrypted_configuration.rb +45 -3
  106. data/lib/active_support/encrypted_file.rb +21 -10
  107. data/lib/active_support/environment_inquirer.rb +1 -1
  108. data/lib/active_support/error_reporter.rb +117 -0
  109. data/lib/active_support/evented_file_update_checker.rb +20 -7
  110. data/lib/active_support/execution_context/test_helper.rb +13 -0
  111. data/lib/active_support/execution_context.rb +53 -0
  112. data/lib/active_support/execution_wrapper.rb +43 -21
  113. data/lib/active_support/executor/test_helper.rb +7 -0
  114. data/lib/active_support/fork_tracker.rb +19 -12
  115. data/lib/active_support/gem_version.rb +5 -5
  116. data/lib/active_support/hash_with_indifferent_access.rb +3 -1
  117. data/lib/active_support/html_safe_translation.rb +43 -0
  118. data/lib/active_support/i18n.rb +1 -0
  119. data/lib/active_support/i18n_railtie.rb +1 -1
  120. data/lib/active_support/inflector/inflections.rb +23 -7
  121. data/lib/active_support/inflector/methods.rb +29 -55
  122. data/lib/active_support/inflector/transliterate.rb +1 -1
  123. data/lib/active_support/isolated_execution_state.rb +72 -0
  124. data/lib/active_support/json/encoding.rb +3 -3
  125. data/lib/active_support/key_generator.rb +22 -5
  126. data/lib/active_support/lazy_load_hooks.rb +28 -4
  127. data/lib/active_support/locale/en.yml +1 -1
  128. data/lib/active_support/log_subscriber/test_helper.rb +2 -2
  129. data/lib/active_support/log_subscriber.rb +15 -5
  130. data/lib/active_support/logger_thread_safe_level.rb +4 -13
  131. data/lib/active_support/message_encryptor.rb +12 -6
  132. data/lib/active_support/message_verifier.rb +46 -14
  133. data/lib/active_support/messages/metadata.rb +2 -2
  134. data/lib/active_support/multibyte/chars.rb +10 -11
  135. data/lib/active_support/multibyte/unicode.rb +0 -12
  136. data/lib/active_support/multibyte.rb +1 -1
  137. data/lib/active_support/notifications/fanout.rb +91 -65
  138. data/lib/active_support/notifications/instrumenter.rb +32 -15
  139. data/lib/active_support/notifications.rb +23 -23
  140. data/lib/active_support/number_helper/number_converter.rb +1 -3
  141. data/lib/active_support/number_helper/number_to_currency_converter.rb +11 -6
  142. data/lib/active_support/number_helper/number_to_delimited_converter.rb +1 -1
  143. data/lib/active_support/number_helper/number_to_human_size_converter.rb +1 -1
  144. data/lib/active_support/number_helper/number_to_phone_converter.rb +1 -1
  145. data/lib/active_support/number_helper/rounding_helper.rb +1 -5
  146. data/lib/active_support/number_helper.rb +4 -5
  147. data/lib/active_support/option_merger.rb +10 -18
  148. data/lib/active_support/ordered_hash.rb +1 -1
  149. data/lib/active_support/ordered_options.rb +1 -1
  150. data/lib/active_support/parameter_filter.rb +20 -11
  151. data/lib/active_support/per_thread_registry.rb +5 -0
  152. data/lib/active_support/railtie.rb +69 -19
  153. data/lib/active_support/reloader.rb +1 -1
  154. data/lib/active_support/rescuable.rb +12 -12
  155. data/lib/active_support/ruby_features.rb +7 -0
  156. data/lib/active_support/secure_compare_rotator.rb +2 -2
  157. data/lib/active_support/string_inquirer.rb +0 -2
  158. data/lib/active_support/subscriber.rb +7 -18
  159. data/lib/active_support/tagged_logging.rb +2 -2
  160. data/lib/active_support/test_case.rb +13 -21
  161. data/lib/active_support/testing/assertions.rb +36 -6
  162. data/lib/active_support/testing/deprecation.rb +52 -1
  163. data/lib/active_support/testing/isolation.rb +30 -29
  164. data/lib/active_support/testing/method_call_assertions.rb +5 -5
  165. data/lib/active_support/testing/parallelization/server.rb +4 -0
  166. data/lib/active_support/testing/parallelization/worker.rb +3 -0
  167. data/lib/active_support/testing/parallelization.rb +4 -0
  168. data/lib/active_support/testing/parallelize_executor.rb +76 -0
  169. data/lib/active_support/testing/stream.rb +3 -5
  170. data/lib/active_support/testing/tagged_logging.rb +1 -1
  171. data/lib/active_support/testing/time_helpers.rb +13 -2
  172. data/lib/active_support/time_with_zone.rb +43 -22
  173. data/lib/active_support/values/time_zone.rb +35 -14
  174. data/lib/active_support/version.rb +1 -1
  175. data/lib/active_support/xml_mini/jdom.rb +1 -1
  176. data/lib/active_support/xml_mini/libxml.rb +5 -5
  177. data/lib/active_support/xml_mini/libxmlsax.rb +1 -1
  178. data/lib/active_support/xml_mini/nokogiri.rb +4 -4
  179. data/lib/active_support/xml_mini/nokogirisax.rb +1 -1
  180. data/lib/active_support/xml_mini/rexml.rb +1 -1
  181. data/lib/active_support/xml_mini.rb +5 -4
  182. data/lib/active_support.rb +17 -1
  183. metadata +26 -23
  184. data/lib/active_support/core_ext/marshal.rb +0 -26
  185. data/lib/active_support/dependencies/zeitwerk_integration.rb +0 -117
@@ -11,7 +11,7 @@ module ActiveSupport
11
11
  #
12
12
  # 1.month.ago # equivalent to Time.now.advance(months: -1)
13
13
  class Duration
14
- class Scalar < Numeric #:nodoc:
14
+ class Scalar < Numeric # :nodoc:
15
15
  attr_reader :value
16
16
  delegate :to_i, :to_f, :to_s, to: :value
17
17
 
@@ -39,11 +39,11 @@ module ActiveSupport
39
39
 
40
40
  def +(other)
41
41
  if Duration === other
42
- seconds = value + other.parts.fetch(:seconds, 0)
43
- new_parts = other.parts.merge(seconds: seconds)
42
+ seconds = value + other._parts.fetch(:seconds, 0)
43
+ new_parts = other._parts.merge(seconds: seconds)
44
44
  new_value = value + other.value
45
45
 
46
- Duration.new(new_value, new_parts)
46
+ Duration.new(new_value, new_parts, other.variable?)
47
47
  else
48
48
  calculate(:+, other)
49
49
  end
@@ -51,12 +51,12 @@ module ActiveSupport
51
51
 
52
52
  def -(other)
53
53
  if Duration === other
54
- seconds = value - other.parts.fetch(:seconds, 0)
55
- new_parts = other.parts.transform_values(&:-@)
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
 
59
- Duration.new(new_value, new_parts)
59
+ Duration.new(new_value, new_parts, other.variable?)
60
60
  else
61
61
  calculate(:-, other)
62
62
  end
@@ -64,10 +64,10 @@ module ActiveSupport
64
64
 
65
65
  def *(other)
66
66
  if Duration === other
67
- new_parts = other.parts.transform_values { |other_value| value * other_value }
67
+ new_parts = other._parts.transform_values { |other_value| value * other_value }
68
68
  new_value = value * other.value
69
69
 
70
- Duration.new(new_value, new_parts)
70
+ Duration.new(new_value, new_parts, other.variable?)
71
71
  else
72
72
  calculate(:*, other)
73
73
  end
@@ -89,6 +89,10 @@ module ActiveSupport
89
89
  end
90
90
  end
91
91
 
92
+ def variable? # :nodoc:
93
+ false
94
+ end
95
+
92
96
  private
93
97
  def calculate(op, other)
94
98
  if Scalar === other
@@ -123,8 +127,9 @@ module ActiveSupport
123
127
  }.freeze
124
128
 
125
129
  PARTS = [:years, :months, :weeks, :days, :hours, :minutes, :seconds].freeze
130
+ VARIABLE_PARTS = [:years, :months, :weeks, :days].freeze
126
131
 
127
- attr_accessor :value, :parts
132
+ attr_reader :value
128
133
 
129
134
  autoload :ISO8601Parser, "active_support/duration/iso8601_parser"
130
135
  autoload :ISO8601Serializer, "active_support/duration/iso8601_serializer"
@@ -140,38 +145,38 @@ module ActiveSupport
140
145
  new(calculate_total_seconds(parts), parts)
141
146
  end
142
147
 
143
- def ===(other) #:nodoc:
148
+ def ===(other) # :nodoc:
144
149
  other.is_a?(Duration)
145
150
  rescue ::NoMethodError
146
151
  false
147
152
  end
148
153
 
149
- def seconds(value) #:nodoc:
150
- new(value, seconds: value)
154
+ def seconds(value) # :nodoc:
155
+ new(value, { seconds: value }, false)
151
156
  end
152
157
 
153
- def minutes(value) #:nodoc:
154
- new(value * SECONDS_PER_MINUTE, minutes: value)
158
+ def minutes(value) # :nodoc:
159
+ new(value * SECONDS_PER_MINUTE, { minutes: value }, false)
155
160
  end
156
161
 
157
- def hours(value) #:nodoc:
158
- new(value * SECONDS_PER_HOUR, hours: value)
162
+ def hours(value) # :nodoc:
163
+ new(value * SECONDS_PER_HOUR, { hours: value }, false)
159
164
  end
160
165
 
161
- def days(value) #:nodoc:
162
- new(value * SECONDS_PER_DAY, days: value)
166
+ def days(value) # :nodoc:
167
+ new(value * SECONDS_PER_DAY, { days: value }, true)
163
168
  end
164
169
 
165
- def weeks(value) #:nodoc:
166
- new(value * SECONDS_PER_WEEK, weeks: value)
170
+ def weeks(value) # :nodoc:
171
+ new(value * SECONDS_PER_WEEK, { weeks: value }, true)
167
172
  end
168
173
 
169
- def months(value) #:nodoc:
170
- new(value * SECONDS_PER_MONTH, months: value)
174
+ def months(value) # :nodoc:
175
+ new(value * SECONDS_PER_MONTH, { months: value }, true)
171
176
  end
172
177
 
173
- def years(value) #:nodoc:
174
- new(value * SECONDS_PER_YEAR, years: value)
178
+ def years(value) # :nodoc:
179
+ new(value * SECONDS_PER_YEAR, { years: value }, true)
175
180
  end
176
181
 
177
182
  # Creates a new Duration from a seconds value that is converted
@@ -186,19 +191,25 @@ module ActiveSupport
186
191
  end
187
192
 
188
193
  parts = {}
189
- remainder = value.round(9)
194
+ remainder_sign = value <=> 0
195
+ remainder = value.round(9).abs
196
+ variable = false
190
197
 
191
198
  PARTS.each do |part|
192
199
  unless part == :seconds
193
200
  part_in_seconds = PARTS_IN_SECONDS[part]
194
- parts[part] = remainder.div(part_in_seconds)
201
+ parts[part] = remainder.div(part_in_seconds) * remainder_sign
195
202
  remainder %= part_in_seconds
203
+
204
+ unless parts[part].zero?
205
+ variable ||= VARIABLE_PARTS.include?(part)
206
+ end
196
207
  end
197
208
  end unless value == 0
198
209
 
199
- parts[:seconds] = remainder
210
+ parts[:seconds] = remainder * remainder_sign
200
211
 
201
- new(value, parts)
212
+ new(value, parts, variable)
202
213
  end
203
214
 
204
215
  private
@@ -209,12 +220,23 @@ module ActiveSupport
209
220
  end
210
221
  end
211
222
 
212
- def initialize(value, parts) #:nodoc:
223
+ def initialize(value, parts, variable = nil) # :nodoc:
213
224
  @value, @parts = value, parts
214
225
  @parts.reject! { |k, v| v.zero? } unless value == 0
226
+ @parts.freeze
227
+ @variable = variable
228
+
229
+ if @variable.nil?
230
+ @variable = @parts.any? { |part, _| VARIABLE_PARTS.include?(part) }
231
+ end
215
232
  end
216
233
 
217
- def coerce(other) #:nodoc:
234
+ # Returns a copy of the parts hash that defines the duration
235
+ def parts
236
+ @parts.dup
237
+ end
238
+
239
+ def coerce(other) # :nodoc:
218
240
  case other
219
241
  when Scalar
220
242
  [other, self]
@@ -239,13 +261,13 @@ module ActiveSupport
239
261
  # are treated as seconds.
240
262
  def +(other)
241
263
  if Duration === other
242
- parts = @parts.merge(other.parts) do |_key, value, other_value|
264
+ parts = @parts.merge(other._parts) do |_key, value, other_value|
243
265
  value + other_value
244
266
  end
245
- Duration.new(value + other.value, parts)
267
+ Duration.new(value + other.value, parts, @variable || other.variable?)
246
268
  else
247
269
  seconds = @parts.fetch(:seconds, 0) + other
248
- Duration.new(value + other, @parts.merge(seconds: seconds))
270
+ Duration.new(value + other, @parts.merge(seconds: seconds), @variable)
249
271
  end
250
272
  end
251
273
 
@@ -258,9 +280,9 @@ module ActiveSupport
258
280
  # Multiplies this Duration by a Numeric and returns a new Duration.
259
281
  def *(other)
260
282
  if Scalar === other || Duration === other
261
- Duration.new(value * other.value, parts.transform_values { |number| number * other.value })
283
+ Duration.new(value * other.value, @parts.transform_values { |number| number * other.value }, @variable || other.variable?)
262
284
  elsif Numeric === other
263
- Duration.new(value * other, parts.transform_values { |number| number * other })
285
+ Duration.new(value * other, @parts.transform_values { |number| number * other }, @variable)
264
286
  else
265
287
  raise_type_error(other)
266
288
  end
@@ -269,11 +291,11 @@ module ActiveSupport
269
291
  # Divides this Duration by a Numeric and returns a new Duration.
270
292
  def /(other)
271
293
  if Scalar === other
272
- Duration.new(value / other.value, parts.transform_values { |number| number / other.value })
294
+ Duration.new(value / other.value, @parts.transform_values { |number| number / other.value }, @variable)
273
295
  elsif Duration === other
274
296
  value / other.value
275
297
  elsif Numeric === other
276
- Duration.new(value / other, parts.transform_values { |number| number / other })
298
+ Duration.new(value / other, @parts.transform_values { |number| number / other }, @variable)
277
299
  else
278
300
  raise_type_error(other)
279
301
  end
@@ -291,15 +313,15 @@ module ActiveSupport
291
313
  end
292
314
  end
293
315
 
294
- def -@ #:nodoc:
295
- Duration.new(-value, parts.transform_values(&:-@))
316
+ def -@ # :nodoc:
317
+ Duration.new(-value, @parts.transform_values(&:-@), @variable)
296
318
  end
297
319
 
298
- def +@ #:nodoc:
320
+ def +@ # :nodoc:
299
321
  self
300
322
  end
301
323
 
302
- def is_a?(klass) #:nodoc:
324
+ def is_a?(klass) # :nodoc:
303
325
  Duration == klass || value.is_a?(klass)
304
326
  end
305
327
  alias :kind_of? :is_a?
@@ -419,24 +441,24 @@ module ActiveSupport
419
441
  alias :until :ago
420
442
  alias :before :ago
421
443
 
422
- def inspect #:nodoc:
423
- return "#{value} seconds" if parts.empty?
444
+ def inspect # :nodoc:
445
+ return "#{value} seconds" if @parts.empty?
424
446
 
425
- parts.
447
+ @parts.
426
448
  sort_by { |unit, _ | PARTS.index(unit) }.
427
449
  map { |unit, val| "#{val} #{val == 1 ? unit.to_s.chop : unit.to_s}" }.
428
- to_sentence(locale: ::I18n.default_locale)
450
+ to_sentence(locale: false)
429
451
  end
430
452
 
431
- def as_json(options = nil) #:nodoc:
453
+ def as_json(options = nil) # :nodoc:
432
454
  to_i
433
455
  end
434
456
 
435
- def init_with(coder) #:nodoc:
457
+ def init_with(coder) # :nodoc:
436
458
  initialize(coder["value"], coder["parts"])
437
459
  end
438
460
 
439
- def encode_with(coder) #:nodoc:
461
+ def encode_with(coder) # :nodoc:
440
462
  coder.map = { "value" => @value, "parts" => @parts }
441
463
  end
442
464
 
@@ -446,16 +468,24 @@ module ActiveSupport
446
468
  ISO8601Serializer.new(self, precision: precision).serialize
447
469
  end
448
470
 
471
+ def variable? # :nodoc:
472
+ @variable
473
+ end
474
+
475
+ def _parts # :nodoc:
476
+ @parts
477
+ end
478
+
449
479
  private
450
480
  def sum(sign, time = ::Time.current)
451
481
  unless time.acts_like?(:time) || time.acts_like?(:date)
452
482
  raise ::ArgumentError, "expected a time or date, got #{time.inspect}"
453
483
  end
454
484
 
455
- if parts.empty?
485
+ if @parts.empty?
456
486
  time.since(sign * value)
457
487
  else
458
- parts.inject(time) do |t, (type, number)|
488
+ @parts.inject(time) do |t, (type, number)|
459
489
  if type == :seconds
460
490
  t.since(sign * number)
461
491
  elsif type == :minutes
@@ -7,6 +7,28 @@ require "active_support/core_ext/object/inclusion"
7
7
  require "active_support/core_ext/module/delegation"
8
8
 
9
9
  module ActiveSupport
10
+ # Provides convenience methods on top of EncryptedFile to access values stored
11
+ # as encrypted YAML.
12
+ #
13
+ # Values can be accessed via +Hash+ methods, such as +fetch+ and +dig+, or via
14
+ # dynamic accessor methods, similar to OrderedOptions.
15
+ #
16
+ # my_config = ActiveSupport::EncryptedConfiguration.new(...)
17
+ # my_config.read # => "some_secret: 123\nsome_namespace:\n another_secret: 456"
18
+ #
19
+ # my_config[:some_secret]
20
+ # # => 123
21
+ # my_config.some_secret
22
+ # # => 123
23
+ # my_config.dig(:some_namespace, :another_secret)
24
+ # # => 456
25
+ # my_config.some_namespace.another_secret
26
+ # # => 456
27
+ # my_config.fetch(:foo)
28
+ # # => KeyError
29
+ # my_config.foo!
30
+ # # => KeyError
31
+ #
10
32
  class EncryptedConfiguration < EncryptedFile
11
33
  delegate :[], :fetch, to: :config
12
34
  delegate_missing_to :options
@@ -16,10 +38,11 @@ module ActiveSupport
16
38
  env_key: env_key, raise_if_missing_key: raise_if_missing_key
17
39
  end
18
40
 
19
- # Allow a config to be started without a file present
41
+ # Reads the file and returns the decrypted content. See EncryptedFile#read.
20
42
  def read
21
43
  super
22
44
  rescue ActiveSupport::EncryptedFile::MissingContentError
45
+ # Allow a config to be started without a file present
23
46
  ""
24
47
  end
25
48
 
@@ -29,17 +52,36 @@ module ActiveSupport
29
52
  super
30
53
  end
31
54
 
55
+ # Returns the decrypted content as a Hash with symbolized keys.
56
+ #
57
+ # my_config = ActiveSupport::EncryptedConfiguration.new(...)
58
+ # my_config.read # => "some_secret: 123\nsome_namespace:\n another_secret: 456"
59
+ #
60
+ # my_config.config
61
+ # # => { some_secret: 123, some_namespace: { another_secret: 789 } }
62
+ #
32
63
  def config
33
64
  @config ||= deserialize(read).deep_symbolize_keys
34
65
  end
35
66
 
36
67
  private
68
+ def deep_transform(hash)
69
+ return hash unless hash.is_a?(Hash)
70
+
71
+ h = ActiveSupport::InheritableOptions.new
72
+ hash.each do |k, v|
73
+ h[k] = deep_transform(v)
74
+ end
75
+ h
76
+ end
77
+
37
78
  def options
38
- @options ||= ActiveSupport::InheritableOptions.new(config)
79
+ @options ||= deep_transform(config)
39
80
  end
40
81
 
41
82
  def deserialize(config)
42
- YAML.load(config).presence || {}
83
+ doc = YAML.respond_to?(:unsafe_load) ? YAML.unsafe_load(config) : YAML.load(config)
84
+ doc.presence || {}
43
85
  end
44
86
  end
45
87
  end
@@ -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
@@ -45,10 +45,22 @@ module ActiveSupport
45
45
  @env_key, @raise_if_missing_key = env_key, raise_if_missing_key
46
46
  end
47
47
 
48
+ # Returns the encryption key, first trying the environment variable
49
+ # specified by +env_key+, then trying the key file specified by +key_path+.
50
+ # If +raise_if_missing_key+ is true, raises MissingKeyError if the
51
+ # environment variable is not set and the key file does not exist.
48
52
  def key
49
53
  read_env_key || read_key_file || handle_missing_key
50
54
  end
51
55
 
56
+ # Reads the file and returns the decrypted content.
57
+ #
58
+ # Raises:
59
+ # - MissingKeyError if the key is missing and +raise_if_missing_key+ is true.
60
+ # - MissingContentError if the encrypted file does not exist or otherwise
61
+ # if the key is missing.
62
+ # - ActiveSupport::MessageEncryptor::InvalidMessage if the content cannot be
63
+ # decrypted or verified.
52
64
  def read
53
65
  if !key.nil? && content_path.exist?
54
66
  decrypt content_path.binread
@@ -69,17 +81,16 @@ module ActiveSupport
69
81
 
70
82
  private
71
83
  def writing(contents)
72
- tmp_file = "#{Process.pid}.#{content_path.basename.to_s.chomp('.enc')}"
73
- tmp_path = Pathname.new File.join(Dir.tmpdir, tmp_file)
74
- tmp_path.binwrite contents
84
+ Tempfile.create(["", "-" + content_path.basename.to_s.chomp(".enc")]) do |tmp_file|
85
+ tmp_path = Pathname.new(tmp_file)
86
+ tmp_path.binwrite contents
75
87
 
76
- yield tmp_path
88
+ yield tmp_path
77
89
 
78
- updated_contents = tmp_path.binread
90
+ updated_contents = tmp_path.binread
79
91
 
80
- write(updated_contents) if updated_contents != contents
81
- ensure
82
- FileUtils.rm(tmp_path) if tmp_path&.exist?
92
+ write(updated_contents) if updated_contents != contents
93
+ end
83
94
  end
84
95
 
85
96
 
@@ -98,7 +109,7 @@ module ActiveSupport
98
109
 
99
110
 
100
111
  def read_env_key
101
- ENV[env_key]
112
+ ENV[env_key].presence
102
113
  end
103
114
 
104
115
  def read_key_file
@@ -3,7 +3,7 @@
3
3
  require "active_support/string_inquirer"
4
4
 
5
5
  module ActiveSupport
6
- class EnvironmentInquirer < StringInquirer #:nodoc:
6
+ class EnvironmentInquirer < StringInquirer # :nodoc:
7
7
  DEFAULT_ENVIRONMENTS = ["development", "test", "production"]
8
8
  def initialize(env)
9
9
  super(env)
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveSupport
4
+ # +ActiveSupport::ErrorReporter+ is a common interface for error reporting services.
5
+ #
6
+ # To rescue and report any unhandled error, you can use the +handle+ method:
7
+ #
8
+ # Rails.error.handle do
9
+ # do_something!
10
+ # end
11
+ #
12
+ # If an error is raised, it will be reported and swallowed.
13
+ #
14
+ # Alternatively if you want to report the error but not swallow it, you can use +record+
15
+ #
16
+ # Rails.error.record do
17
+ # do_something!
18
+ # end
19
+ #
20
+ # Both methods can be restricted to only handle a specific exception class
21
+ #
22
+ # maybe_tags = Rails.error.handle(Redis::BaseError) { redis.get("tags") }
23
+ #
24
+ # You can also pass some extra context information that may be used by the error subscribers:
25
+ #
26
+ # Rails.error.handle(context: { section: "admin" }) do
27
+ # # ...
28
+ # end
29
+ #
30
+ # Additionally a +severity+ can be passed along to communicate how important the error report is.
31
+ # +severity+ can be one of +:error+, +:warning+, or +:info+. Handled errors default to the +:warning+
32
+ # severity, and unhandled ones to +:error+.
33
+ #
34
+ # Both +handle+ and +record+ pass through the return value from the block. In the case of +handle+
35
+ # rescuing an error, a fallback can be provided. The fallback must be a callable whose result will
36
+ # be returned when the block raises and is handled:
37
+ #
38
+ # user = Rails.error.handle(fallback: -> { User.anonymous }) do
39
+ # User.find_by(params)
40
+ # end
41
+ class ErrorReporter
42
+ SEVERITIES = %i(error warning info)
43
+
44
+ attr_accessor :logger
45
+
46
+ def initialize(*subscribers, logger: nil)
47
+ @subscribers = subscribers.flatten
48
+ @logger = logger
49
+ end
50
+
51
+ # Report any unhandled exception, and swallow it.
52
+ #
53
+ # Rails.error.handle do
54
+ # 1 + '1'
55
+ # end
56
+ #
57
+ def handle(error_class = StandardError, severity: :warning, context: {}, fallback: nil)
58
+ yield
59
+ rescue error_class => error
60
+ report(error, handled: true, severity: severity, context: context)
61
+ fallback.call if fallback
62
+ end
63
+
64
+ def record(error_class = StandardError, severity: :error, context: {})
65
+ yield
66
+ rescue error_class => error
67
+ report(error, handled: false, severity: severity, context: context)
68
+ raise
69
+ end
70
+
71
+ # Register a new error subscriber. The subscriber must respond to
72
+ #
73
+ # report(Exception, handled: Boolean, context: Hash)
74
+ #
75
+ # The +report+ method +should+ never raise an error.
76
+ def subscribe(subscriber)
77
+ unless subscriber.respond_to?(:report)
78
+ raise ArgumentError, "Error subscribers must respond to #report"
79
+ end
80
+ @subscribers << subscriber
81
+ end
82
+
83
+ # Update the execution context that is accessible to error subscribers
84
+ #
85
+ # Rails.error.set_context(section: "checkout", user_id: @user.id)
86
+ #
87
+ # See +ActiveSupport::ExecutionContext.set+
88
+ def set_context(...)
89
+ ActiveSupport::ExecutionContext.set(...)
90
+ end
91
+
92
+ # When the block based +handle+ and +record+ methods are not suitable, you can directly use +report+
93
+ #
94
+ # Rails.error.report(error, handled: true)
95
+ def report(error, handled:, severity: handled ? :warning : :error, context: {})
96
+ unless SEVERITIES.include?(severity)
97
+ raise ArgumentError, "severity must be one of #{SEVERITIES.map(&:inspect).join(", ")}, got: #{severity.inspect}"
98
+ end
99
+
100
+ full_context = ActiveSupport::ExecutionContext.to_h.merge(context)
101
+ @subscribers.each do |subscriber|
102
+ subscriber.report(error, handled: handled, severity: severity, context: full_context)
103
+ rescue => subscriber_error
104
+ if logger
105
+ logger.fatal(
106
+ "Error subscriber raised an error: #{subscriber_error.message} (#{subscriber_error.class})\n" +
107
+ subscriber_error.backtrace.join("\n")
108
+ )
109
+ else
110
+ raise
111
+ end
112
+ end
113
+
114
+ nil
115
+ end
116
+ end
117
+ end
@@ -1,15 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ gem "listen", "~> 3.5"
4
+ require "listen"
5
+
3
6
  require "set"
4
7
  require "pathname"
5
8
  require "concurrent/atomic/atomic_boolean"
6
- require "listen"
7
9
  require "active_support/fork_tracker"
8
10
 
9
11
  module ActiveSupport
10
12
  # Allows you to "listen" to changes in a file system.
11
- # The evented file updater does not hit disk when checking for updates
12
- # instead it uses platform specific file system events to trigger a change
13
+ # The evented file updater does not hit disk when checking for updates.
14
+ # Instead, it uses platform-specific file system events to trigger a change
13
15
  # in state.
14
16
  #
15
17
  # The file checker takes an array of files to watch or a hash specifying directories
@@ -17,8 +19,6 @@ module ActiveSupport
17
19
  # EventedFileUpdateChecker#execute is run or when EventedFileUpdateChecker#execute_if_updated
18
20
  # is run and there have been changes to the file system.
19
21
  #
20
- # Note: Forking will cause the first call to `updated?` to return `true`.
21
- #
22
22
  # Example:
23
23
  #
24
24
  # checker = ActiveSupport::EventedFileUpdateChecker.new(["/tmp/foo"]) { puts "changed" }
@@ -34,7 +34,7 @@ module ActiveSupport
34
34
  # checker.execute_if_updated
35
35
  # # => "changed"
36
36
  #
37
- class EventedFileUpdateChecker #:nodoc: all
37
+ class EventedFileUpdateChecker # :nodoc: all
38
38
  def initialize(files, dirs = {}, &block)
39
39
  unless block
40
40
  raise ArgumentError, "A block is required to initialize an EventedFileUpdateChecker"
@@ -45,6 +45,10 @@ module ActiveSupport
45
45
  ObjectSpace.define_finalizer(self, @core.finalizer)
46
46
  end
47
47
 
48
+ def inspect
49
+ "#<ActiveSupport::EventedFileUpdateChecker:#{object_id} @files=#{@core.files.to_a.inspect}"
50
+ end
51
+
48
52
  def updated?
49
53
  if @core.restart?
50
54
  @core.thread_safely(&:restart)
@@ -68,7 +72,7 @@ module ActiveSupport
68
72
  end
69
73
 
70
74
  class Core
71
- attr_reader :updated
75
+ attr_reader :updated, :files
72
76
 
73
77
  def initialize(files, dirs)
74
78
  @files = files.map { |file| Pathname(file).expand_path }.to_set
@@ -86,6 +90,10 @@ module ActiveSupport
86
90
  @mutex = Mutex.new
87
91
 
88
92
  start
93
+ # inotify / FSEvents file descriptors are inherited on fork, so
94
+ # we need to reopen them otherwise only the parent or the child
95
+ # will be notified.
96
+ # FIXME: this callback is keeping a reference on the instance
89
97
  @after_fork = ActiveSupport::ForkTracker.after_fork { start }
90
98
  end
91
99
 
@@ -107,6 +115,11 @@ module ActiveSupport
107
115
  @dtw, @missing = [*@dtw, *@missing].partition(&:exist?)
108
116
  @listener = @dtw.any? ? Listen.to(*@dtw, &method(:changed)) : nil
109
117
  @listener&.start
118
+
119
+ # Wait for the listener to be ready to avoid race conditions
120
+ # Unfortunately this isn't quite enough on macOS because the Darwin backend
121
+ # has an extra private thread we can't wait on.
122
+ @listener&.wait_for_state(:processing_events)
110
123
  end
111
124
 
112
125
  def stop
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveSupport::ExecutionContext::TestHelper # :nodoc:
4
+ def before_setup
5
+ ActiveSupport::ExecutionContext.clear
6
+ super
7
+ end
8
+
9
+ def after_teardown
10
+ super
11
+ ActiveSupport::ExecutionContext.clear
12
+ end
13
+ end