activesupport 7.2.3.1 → 8.1.4.1

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 (135) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +584 -234
  3. data/lib/active_support/backtrace_cleaner.rb +72 -1
  4. data/lib/active_support/benchmark.rb +21 -0
  5. data/lib/active_support/benchmarkable.rb +3 -2
  6. data/lib/active_support/cache/file_store.rb +13 -2
  7. data/lib/active_support/cache/mem_cache_store.rb +17 -14
  8. data/lib/active_support/cache/memory_store.rb +9 -3
  9. data/lib/active_support/cache/redis_cache_store.rb +51 -34
  10. data/lib/active_support/cache/strategy/local_cache.rb +17 -8
  11. data/lib/active_support/cache/strategy/local_cache_middleware.rb +7 -7
  12. data/lib/active_support/cache.rb +86 -18
  13. data/lib/active_support/callbacks.rb +25 -11
  14. data/lib/active_support/class_attribute.rb +33 -0
  15. data/lib/active_support/code_generator.rb +9 -0
  16. data/lib/active_support/concurrency/load_interlock_aware_monitor.rb +8 -62
  17. data/lib/active_support/concurrency/share_lock.rb +0 -1
  18. data/lib/active_support/concurrency/thread_monitor.rb +55 -0
  19. data/lib/active_support/configurable.rb +34 -0
  20. data/lib/active_support/configuration_file.rb +15 -6
  21. data/lib/active_support/continuous_integration.rb +145 -0
  22. data/lib/active_support/core_ext/array/conversions.rb +3 -3
  23. data/lib/active_support/core_ext/array.rb +7 -7
  24. data/lib/active_support/core_ext/benchmark.rb +0 -16
  25. data/lib/active_support/core_ext/big_decimal.rb +1 -1
  26. data/lib/active_support/core_ext/class/attribute.rb +24 -18
  27. data/lib/active_support/core_ext/class.rb +2 -2
  28. data/lib/active_support/core_ext/date/conversions.rb +2 -0
  29. data/lib/active_support/core_ext/date.rb +5 -5
  30. data/lib/active_support/core_ext/date_and_time/compatibility.rb +0 -35
  31. data/lib/active_support/core_ext/date_time/calculations.rb +1 -0
  32. data/lib/active_support/core_ext/date_time/compatibility.rb +3 -5
  33. data/lib/active_support/core_ext/date_time.rb +5 -5
  34. data/lib/active_support/core_ext/digest.rb +1 -1
  35. data/lib/active_support/core_ext/enumerable.rb +8 -3
  36. data/lib/active_support/core_ext/erb/util.rb +3 -3
  37. data/lib/active_support/core_ext/file.rb +1 -1
  38. data/lib/active_support/core_ext/hash/deep_merge.rb +1 -0
  39. data/lib/active_support/core_ext/hash/except.rb +0 -12
  40. data/lib/active_support/core_ext/hash.rb +8 -8
  41. data/lib/active_support/core_ext/integer.rb +3 -3
  42. data/lib/active_support/core_ext/kernel.rb +3 -3
  43. data/lib/active_support/core_ext/module/attr_internal.rb +3 -4
  44. data/lib/active_support/core_ext/module.rb +11 -11
  45. data/lib/active_support/core_ext/numeric.rb +3 -3
  46. data/lib/active_support/core_ext/object/instance_variables.rb +1 -1
  47. data/lib/active_support/core_ext/object/json.rb +24 -11
  48. data/lib/active_support/core_ext/object/to_query.rb +7 -1
  49. data/lib/active_support/core_ext/object.rb +13 -13
  50. data/lib/active_support/core_ext/pathname.rb +2 -2
  51. data/lib/active_support/core_ext/range/compare_range.rb +2 -14
  52. data/lib/active_support/core_ext/range/overlap.rb +3 -3
  53. data/lib/active_support/core_ext/range/sole.rb +2 -0
  54. data/lib/active_support/core_ext/range.rb +4 -5
  55. data/lib/active_support/core_ext/string/filters.rb +2 -0
  56. data/lib/active_support/core_ext/string/inflections.rb +1 -1
  57. data/lib/active_support/core_ext/string/multibyte.rb +10 -1
  58. data/lib/active_support/core_ext/string/output_safety.rb +27 -13
  59. data/lib/active_support/core_ext/string.rb +13 -13
  60. data/lib/active_support/core_ext/symbol.rb +1 -1
  61. data/lib/active_support/core_ext/thread/backtrace/location.rb +2 -7
  62. data/lib/active_support/core_ext/time/calculations.rb +11 -2
  63. data/lib/active_support/core_ext/time/compatibility.rb +2 -27
  64. data/lib/active_support/core_ext/time/conversions.rb +2 -0
  65. data/lib/active_support/core_ext/time.rb +5 -5
  66. data/lib/active_support/current_attributes/test_helper.rb +2 -2
  67. data/lib/active_support/current_attributes.rb +13 -10
  68. data/lib/active_support/delegation.rb +25 -44
  69. data/lib/active_support/dependencies/interlock.rb +11 -5
  70. data/lib/active_support/dependencies.rb +6 -2
  71. data/lib/active_support/deprecation/reporting.rb +4 -21
  72. data/lib/active_support/deprecation.rb +1 -1
  73. data/lib/active_support/duration.rb +20 -16
  74. data/lib/active_support/editor.rb +70 -0
  75. data/lib/active_support/encrypted_configuration.rb +20 -2
  76. data/lib/active_support/error_reporter.rb +76 -2
  77. data/lib/active_support/event_reporter/test_helper.rb +32 -0
  78. data/lib/active_support/event_reporter.rb +592 -0
  79. data/lib/active_support/evented_file_update_checker.rb +5 -2
  80. data/lib/active_support/execution_context.rb +75 -7
  81. data/lib/active_support/file_update_checker.rb +7 -5
  82. data/lib/active_support/gem_version.rb +3 -3
  83. data/lib/active_support/gzip.rb +1 -0
  84. data/lib/active_support/hash_with_indifferent_access.rb +44 -24
  85. data/lib/active_support/i18n_railtie.rb +19 -11
  86. data/lib/active_support/inflector/inflections.rb +34 -16
  87. data/lib/active_support/inflector/methods.rb +6 -22
  88. data/lib/active_support/inflector/transliterate.rb +18 -10
  89. data/lib/active_support/isolated_execution_state.rb +17 -17
  90. data/lib/active_support/json/decoding.rb +9 -3
  91. data/lib/active_support/json/encoding.rb +149 -19
  92. data/lib/active_support/log_subscriber.rb +2 -6
  93. data/lib/active_support/logger_thread_safe_level.rb +6 -3
  94. data/lib/active_support/message_encryptors.rb +52 -0
  95. data/lib/active_support/message_pack/extensions.rb +23 -1
  96. data/lib/active_support/message_verifiers.rb +52 -0
  97. data/lib/active_support/messages/rotation_coordinator.rb +9 -0
  98. data/lib/active_support/messages/rotator.rb +5 -0
  99. data/lib/active_support/messages/serializer_with_fallback.rb +1 -1
  100. data/lib/active_support/multibyte/chars.rb +8 -1
  101. data/lib/active_support/multibyte.rb +4 -0
  102. data/lib/active_support/notifications/fanout.rb +64 -43
  103. data/lib/active_support/notifications/instrumenter.rb +1 -1
  104. data/lib/active_support/notifications.rb +2 -2
  105. data/lib/active_support/number_helper/number_converter.rb +4 -1
  106. data/lib/active_support/number_helper/number_to_currency_converter.rb +1 -1
  107. data/lib/active_support/number_helper/number_to_delimited_converter.rb +3 -4
  108. data/lib/active_support/number_helper/number_to_phone_converter.rb +3 -7
  109. data/lib/active_support/number_helper/rounding_helper.rb +1 -1
  110. data/lib/active_support/number_helper.rb +22 -0
  111. data/lib/active_support/ordered_options.rb +2 -2
  112. data/lib/active_support/railtie.rb +32 -9
  113. data/lib/active_support/security_utils.rb +4 -5
  114. data/lib/active_support/structured_event_subscriber.rb +99 -0
  115. data/lib/active_support/subscriber.rb +0 -5
  116. data/lib/active_support/syntax_error_proxy.rb +9 -1
  117. data/lib/active_support/tagged_logging.rb +5 -0
  118. data/lib/active_support/test_case.rb +67 -6
  119. data/lib/active_support/testing/assertions.rb +118 -27
  120. data/lib/active_support/testing/autorun.rb +5 -0
  121. data/lib/active_support/testing/error_reporter_assertions.rb +17 -0
  122. data/lib/active_support/testing/event_reporter_assertions.rb +227 -0
  123. data/lib/active_support/testing/isolation.rb +0 -2
  124. data/lib/active_support/testing/notification_assertions.rb +92 -0
  125. data/lib/active_support/testing/parallelization/worker.rb +7 -1
  126. data/lib/active_support/testing/parallelization.rb +17 -0
  127. data/lib/active_support/testing/tests_without_assertions.rb +1 -1
  128. data/lib/active_support/testing/time_helpers.rb +9 -4
  129. data/lib/active_support/time_with_zone.rb +36 -23
  130. data/lib/active_support/values/time_zone.rb +22 -13
  131. data/lib/active_support/xml_mini.rb +1 -2
  132. data/lib/active_support.rb +21 -9
  133. metadata +33 -17
  134. data/lib/active_support/core_ext/range/each.rb +0 -24
  135. data/lib/active_support/proxy_object.rb +0 -20
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "set"
4
-
5
3
  module ActiveSupport
6
4
  # Error generated by +delegate+ when a method is called on +nil+ and +allow_nil+
7
5
  # option is not used.
@@ -136,11 +134,11 @@ module ActiveSupport
136
134
  "def #{method_name}(#{definition})" <<
137
135
  " _ = #{receiver}" <<
138
136
  " _.#{method}(#{definition})" <<
139
- "rescue NoMethodError => e" <<
137
+ "rescue ::NoMethodError => e" <<
140
138
  " if _.nil? && e.name == :#{method}" <<
141
- " raise ::ActiveSupport::DelegationError.nil_target(:#{method_name}, :'#{receiver}')" <<
139
+ " ::Kernel.raise ::ActiveSupport::DelegationError.nil_target(:#{method_name}, :'#{receiver}')" <<
142
140
  " else" <<
143
- " raise" <<
141
+ " ::Kernel.raise" <<
144
142
  " end" <<
145
143
  "end"
146
144
  end
@@ -153,49 +151,32 @@ module ActiveSupport
153
151
  target = target.to_s
154
152
  target = "self.#{target}" if RESERVED_METHOD_NAMES.include?(target) || target == "__target"
155
153
 
156
- if allow_nil
157
- owner.module_eval <<~RUBY, __FILE__, __LINE__ + 1
158
- def respond_to_missing?(name, include_private = false)
159
- # It may look like an oversight, but we deliberately do not pass
160
- # +include_private+, because they do not get delegated.
161
-
162
- return false if name == :marshal_dump || name == :_dump
163
- #{target}.respond_to?(name) || super
164
- end
165
-
166
- def method_missing(method, ...)
167
- __target = #{target}
168
- if __target.nil? && !nil.respond_to?(method)
169
- nil
170
- elsif __target.respond_to?(method)
171
- __target.public_send(method, ...)
172
- else
173
- super
174
- end
175
- end
176
- RUBY
154
+ nil_behavior = if allow_nil
155
+ "nil"
177
156
  else
178
- owner.module_eval <<~RUBY, __FILE__, __LINE__ + 1
179
- def respond_to_missing?(name, include_private = false)
180
- # It may look like an oversight, but we deliberately do not pass
181
- # +include_private+, because they do not get delegated.
157
+ "::Kernel.raise ::ActiveSupport::DelegationError.nil_target(method, :'#{target}')"
158
+ end
182
159
 
183
- return false if name == :marshal_dump || name == :_dump
184
- #{target}.respond_to?(name) || super
185
- end
160
+ owner.module_eval <<~RUBY, __FILE__, __LINE__ + 1
161
+ def respond_to_missing?(name, include_private = false)
162
+ # It may look like an oversight, but we deliberately do not pass
163
+ # +include_private+, because they do not get delegated.
186
164
 
187
- def method_missing(method, ...)
188
- __target = #{target}
189
- if __target.nil? && !nil.respond_to?(method)
190
- raise ::ActiveSupport::DelegationError.nil_target(method, :'#{target}')
191
- elsif __target.respond_to?(method)
192
- __target.public_send(method, ...)
193
- else
194
- super
195
- end
165
+ return false if name == :marshal_dump || name == :_dump
166
+ #{target}.respond_to?(name) || super
167
+ end
168
+
169
+ def method_missing(method, ...)
170
+ __target = #{target}
171
+ if __target.nil? && !nil.respond_to?(method)
172
+ #{nil_behavior}
173
+ elsif __target.respond_to?(method)
174
+ __target.public_send(method, ...)
175
+ else
176
+ super
196
177
  end
197
- RUBY
198
- end
178
+ end
179
+ RUBY
199
180
  end
200
181
  end
201
182
  end
@@ -10,19 +10,24 @@ module ActiveSupport # :nodoc:
10
10
  end
11
11
 
12
12
  def loading(&block)
13
- @lock.exclusive(purpose: :load, compatible: [:load], after_compatible: [:load], &block)
13
+ ActiveSupport.deprecator.warn(
14
+ "ActiveSupport::Dependencies::Interlock#loading is deprecated and " \
15
+ "will be removed in Rails 9.0. The loading interlock is no longer " \
16
+ "used since Rails switched to Zeitwerk for autoloading."
17
+ )
18
+ yield if block
14
19
  end
15
20
 
16
21
  def unloading(&block)
17
- @lock.exclusive(purpose: :unload, compatible: [:load, :unload], after_compatible: [:load, :unload], &block)
22
+ @lock.exclusive(purpose: :unload, compatible: [:unload], after_compatible: [:unload], &block)
18
23
  end
19
24
 
20
25
  def start_unloading
21
- @lock.start_exclusive(purpose: :unload, compatible: [:load, :unload])
26
+ @lock.start_exclusive(purpose: :unload, compatible: [:unload])
22
27
  end
23
28
 
24
29
  def done_unloading
25
- @lock.stop_exclusive(compatible: [:load, :unload])
30
+ @lock.stop_exclusive(compatible: [:unload])
26
31
  end
27
32
 
28
33
  def start_running
@@ -38,7 +43,8 @@ module ActiveSupport # :nodoc:
38
43
  end
39
44
 
40
45
  def permit_concurrent_loads(&block)
41
- @lock.yield_shares(compatible: [:load], &block)
46
+ # Soft deprecated: no deprecation warning for now, but this is a no-op.
47
+ yield if block
42
48
  end
43
49
 
44
50
  def raw_state(&block) # :nodoc:
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "set"
4
3
  require "active_support/dependencies/interlock"
5
4
 
6
5
  module ActiveSupport # :nodoc:
@@ -22,7 +21,12 @@ module ActiveSupport # :nodoc:
22
21
  # preventing any other thread from being inside a #run_interlock
23
22
  # block at the same time.
24
23
  def self.load_interlock(&block)
25
- interlock.loading(&block)
24
+ ActiveSupport.deprecator.warn(
25
+ "ActiveSupport::Dependencies.load_interlock is deprecated and " \
26
+ "will be removed in Rails 9.0. The loading interlock is no longer " \
27
+ "used since Rails switched to Zeitwerk for autoloading."
28
+ )
29
+ yield if block
26
30
  end
27
31
 
28
32
  # Execute the supplied block while holding an exclusive lock,
@@ -139,7 +139,6 @@ module ActiveSupport
139
139
 
140
140
  def extract_callstack(callstack)
141
141
  return [] if callstack.empty?
142
- return _extract_callstack(callstack) if callstack.first.is_a? String
143
142
 
144
143
  offending_line = callstack.find { |frame|
145
144
  # Code generated with `eval` doesn't have an `absolute_path`, e.g. templates.
@@ -150,26 +149,10 @@ module ActiveSupport
150
149
  [offending_line.path, offending_line.lineno, offending_line.label]
151
150
  end
152
151
 
153
- def _extract_callstack(callstack)
154
- ActiveSupport.deprecator.warn(<<~MESSAGE)
155
- Passing the result of `caller` to ActiveSupport::Deprecation#warn is deprecated and will be removed in Rails 8.0.
156
-
157
- Please pass the result of `caller_locations` instead.
158
- MESSAGE
159
-
160
- offending_line = callstack.find { |line| !ignored_callstack?(line) } || callstack.first
161
-
162
- if offending_line
163
- if md = offending_line.match(/^(.+?):(\d+)(?::in `(.*?)')?/)
164
- md.captures
165
- else
166
- offending_line
167
- end
168
- end
169
- end
170
-
171
- RAILS_GEM_ROOT = File.expand_path("../../../..", __dir__) + "/" # :nodoc:
172
- LIB_DIR = RbConfig::CONFIG["libdir"] # :nodoc:
152
+ RAILS_GEM_ROOT = File.expand_path("../../../..", __dir__) + "/"
153
+ private_constant :RAILS_GEM_ROOT
154
+ LIB_DIR = RbConfig::CONFIG["libdir"]
155
+ private_constant :LIB_DIR
173
156
 
174
157
  def ignored_callstack?(path)
175
158
  path.start_with?(RAILS_GEM_ROOT, LIB_DIR) || path.include?("<internal:")
@@ -68,7 +68,7 @@ module ActiveSupport
68
68
  # and the second is a library name.
69
69
  #
70
70
  # ActiveSupport::Deprecation.new('2.0', 'MyLibrary')
71
- def initialize(deprecation_horizon = "8.0", gem_name = "Rails")
71
+ def initialize(deprecation_horizon = "8.2", gem_name = "Rails")
72
72
  self.gem_name = gem_name
73
73
  self.deprecation_horizon = deprecation_horizon
74
74
  # By default, warnings are not silenced and debugging is off.
@@ -383,42 +383,42 @@ module ActiveSupport
383
383
  #
384
384
  # 1.day.in_minutes # => 1440.0
385
385
  def in_minutes
386
- in_seconds / SECONDS_PER_MINUTE.to_f
386
+ value / SECONDS_PER_MINUTE.to_f
387
387
  end
388
388
 
389
389
  # Returns the amount of hours a duration covers as a float
390
390
  #
391
391
  # 1.day.in_hours # => 24.0
392
392
  def in_hours
393
- in_seconds / SECONDS_PER_HOUR.to_f
393
+ value / SECONDS_PER_HOUR.to_f
394
394
  end
395
395
 
396
396
  # Returns the amount of days a duration covers as a float
397
397
  #
398
398
  # 12.hours.in_days # => 0.5
399
399
  def in_days
400
- in_seconds / SECONDS_PER_DAY.to_f
400
+ value / SECONDS_PER_DAY.to_f
401
401
  end
402
402
 
403
403
  # Returns the amount of weeks a duration covers as a float
404
404
  #
405
405
  # 2.months.in_weeks # => 8.696
406
406
  def in_weeks
407
- in_seconds / SECONDS_PER_WEEK.to_f
407
+ value / SECONDS_PER_WEEK.to_f
408
408
  end
409
409
 
410
410
  # Returns the amount of months a duration covers as a float
411
411
  #
412
412
  # 9.weeks.in_months # => 2.07
413
413
  def in_months
414
- in_seconds / SECONDS_PER_MONTH.to_f
414
+ value / SECONDS_PER_MONTH.to_f
415
415
  end
416
416
 
417
417
  # Returns the amount of years a duration covers as a float
418
418
  #
419
419
  # 30.days.in_years # => 0.082
420
420
  def in_years
421
- in_seconds / SECONDS_PER_YEAR.to_f
421
+ value / SECONDS_PER_YEAR.to_f
422
422
  end
423
423
 
424
424
  # Returns +true+ if +other+ is also a Duration instance, which has the
@@ -491,17 +491,21 @@ module ActiveSupport
491
491
  if @parts.empty?
492
492
  time.since(sign * value)
493
493
  else
494
- @parts.inject(time) do |t, (type, number)|
495
- if type == :seconds
496
- t.since(sign * number)
497
- elsif type == :minutes
498
- t.since(sign * number * 60)
499
- elsif type == :hours
500
- t.since(sign * number * 3600)
501
- else
502
- t.advance(type => sign * number)
503
- end
494
+ @parts.each do |type, number|
495
+ t = time
496
+ time =
497
+ if type == :seconds
498
+ t.since(sign * number)
499
+ elsif type == :minutes
500
+ t.since(sign * number * 60)
501
+ elsif type == :hours
502
+ t.since(sign * number * 3600)
503
+ else
504
+ t.advance(type => sign * number)
505
+ end
504
506
  end
507
+
508
+ time
505
509
  end
506
510
  end
507
511
 
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :markup: markdown
4
+
5
+ module ActiveSupport
6
+ class Editor
7
+ @editors = {}
8
+ @current = false
9
+
10
+ class << self
11
+ # Registers a URL pattern for opening file in a given editor.
12
+ # This allows Rails to generate clickable links to control known editors.
13
+ #
14
+ # Example:
15
+ #
16
+ # ActiveSupport::Editor.register("myeditor", "myeditor://%s:%d")
17
+ def register(name, url_pattern, aliases: [])
18
+ editor = new(url_pattern)
19
+ @editors[name] = editor
20
+ aliases.each do |a|
21
+ @editors[a] = editor
22
+ end
23
+ end
24
+
25
+ # Returns the current editor pattern if it is known.
26
+ # First check for the `RAILS_EDITOR` environment variable, and if it's
27
+ # missing, check for the `EDITOR` environment variable.
28
+ def current
29
+ if @current == false
30
+ @current = if editor_name = ENV["RAILS_EDITOR"] || ENV["EDITOR"]
31
+ @editors[editor_name]
32
+ end
33
+ end
34
+ @current
35
+ end
36
+
37
+ # :nodoc:
38
+
39
+ def find(name)
40
+ @editors[name]
41
+ end
42
+
43
+ def reset
44
+ @current = false
45
+ end
46
+ end
47
+
48
+ def initialize(url_pattern)
49
+ @url_pattern = url_pattern
50
+ end
51
+
52
+ def url_for(path, line)
53
+ sprintf(@url_pattern, path, line)
54
+ end
55
+
56
+ register "atom", "atom://core/open/file?filename=%s&line=%d"
57
+ register "cursor", "cursor://file/%s:%f"
58
+ register "emacs", "emacs://open?url=file://%s&line=%d", aliases: ["emacsclient"]
59
+ register "idea", "idea://open?file=%s&line=%d"
60
+ register "macvim", "mvim://open?url=file://%s&line=%d", aliases: ["mvim"]
61
+ register "nova", "nova://open?path=%s&line=%d"
62
+ register "rubymine", "x-mine://open?file=%s&line=%d"
63
+ register "sublime", "subl://open?url=file://%s&line=%d", aliases: ["subl"]
64
+ register "textmate", "txmt://open?url=file://%s&line=%d", aliases: ["mate"]
65
+ register "vscode", "vscode://file/%s:%d", aliases: ["code"]
66
+ register "vscodium", "vscodium://file/%s:%d", aliases: ["codium"]
67
+ register "windsurf", "windsurf://file/%s:%d"
68
+ register "zed", "zed://file/%s:%d"
69
+ end
70
+ end
@@ -43,6 +43,12 @@ module ActiveSupport
43
43
  end
44
44
  end
45
45
 
46
+ class InvalidKeyError < RuntimeError
47
+ def initialize(content_path, key)
48
+ super "Key '#{key}' is invalid, it must respond to '#to_sym' from configuration in '#{content_path}'."
49
+ end
50
+ end
51
+
46
52
  delegate_missing_to :options
47
53
 
48
54
  def initialize(config_path:, key_path:, env_key:, raise_if_missing_key:)
@@ -61,7 +67,11 @@ module ActiveSupport
61
67
  end
62
68
 
63
69
  def validate! # :nodoc:
64
- deserialize(read)
70
+ deserialize(read).each_key do |key|
71
+ key.to_sym
72
+ rescue NoMethodError
73
+ raise InvalidKeyError.new(content_path, key)
74
+ end
65
75
  end
66
76
 
67
77
  # Returns the decrypted content as a Hash with symbolized keys.
@@ -73,7 +83,7 @@ module ActiveSupport
73
83
  # # => { some_secret: 123, some_namespace: { another_secret: 789 } }
74
84
  #
75
85
  def config
76
- @config ||= deserialize(read).deep_symbolize_keys
86
+ @config ||= deep_symbolize_keys(deserialize(read))
77
87
  end
78
88
 
79
89
  def inspect # :nodoc:
@@ -81,6 +91,14 @@ module ActiveSupport
81
91
  end
82
92
 
83
93
  private
94
+ def deep_symbolize_keys(hash)
95
+ hash.deep_transform_keys do |key|
96
+ key.to_sym
97
+ rescue NoMethodError
98
+ raise InvalidKeyError.new(content_path, key)
99
+ end
100
+ end
101
+
84
102
  def deep_transform(hash)
85
103
  return hash unless hash.is_a?(Hash)
86
104
 
@@ -36,6 +36,7 @@ module ActiveSupport
36
36
  @subscribers = subscribers.flatten
37
37
  @logger = logger
38
38
  @debug_mode = false
39
+ @context_middlewares = ErrorContextMiddlewareStack.new
39
40
  end
40
41
 
41
42
  # Evaluates the given block, reporting and swallowing any unhandled error.
@@ -144,9 +145,9 @@ module ActiveSupport
144
145
  #
145
146
  def unexpected(error, severity: :warning, context: {}, source: DEFAULT_SOURCE)
146
147
  error = RuntimeError.new(error) if error.is_a?(String)
147
- error.set_backtrace(caller(1)) if error.backtrace.nil?
148
148
 
149
149
  if @debug_mode
150
+ ensure_backtrace(error)
150
151
  raise UnexpectedError, "#{error.class.name}: #{error.message}", error.backtrace, cause: error
151
152
  else
152
153
  report(error, handled: true, severity: severity, context: context, source: source)
@@ -202,19 +203,51 @@ module ActiveSupport
202
203
  ActiveSupport::ExecutionContext.set(...)
203
204
  end
204
205
 
206
+ # Add a middleware to modify the error context before it is sent to subscribers.
207
+ #
208
+ # Middleware is added to a stack of callables run on an error's execution context
209
+ # before passing to subscribers. Allows creation of entries in error context that
210
+ # are shared by all subscribers.
211
+ #
212
+ # A context middleware receives the same parameters as #report.
213
+ # It must return a hash - the middleware stack returns the hash after it has
214
+ # run through all middlewares. A middleware can mutate or replace the hash.
215
+ #
216
+ # Rails.error.add_middleware(-> (error, context) { context.merge({ foo: :bar }) })
217
+ #
218
+ def add_middleware(middleware)
219
+ @context_middlewares.use(middleware)
220
+ end
221
+
205
222
  # Report an error directly to subscribers. You can use this method when the
206
223
  # block-based #handle and #record methods are not suitable.
207
224
  #
208
225
  # Rails.error.report(error)
209
226
  #
227
+ # The +error+ argument must be an instance of Exception.
228
+ #
229
+ # Rails.error.report(Exception.new("Something went wrong"))
230
+ #
231
+ # Otherwise you can use #unexpected to report an error which does accept a
232
+ # string argument.
210
233
  def report(error, handled: true, severity: handled ? :warning : :error, context: {}, source: DEFAULT_SOURCE)
211
234
  return if error.instance_variable_defined?(:@__rails_error_reported)
235
+ raise ArgumentError, "Reported error must be an Exception, got: #{error.inspect}" unless error.is_a?(Exception)
236
+
237
+ ensure_backtrace(error)
212
238
 
213
239
  unless SEVERITIES.include?(severity)
214
240
  raise ArgumentError, "severity must be one of #{SEVERITIES.map(&:inspect).join(", ")}, got: #{severity.inspect}"
215
241
  end
216
242
 
217
- full_context = ActiveSupport::ExecutionContext.to_h.merge(context)
243
+ full_context = @context_middlewares.execute(
244
+ error,
245
+ context: ActiveSupport::ExecutionContext.to_h.merge(context || {}),
246
+ handled:,
247
+ severity:,
248
+ source:
249
+ )
250
+
218
251
  disabled_subscribers = ActiveSupport::IsolatedExecutionState[self]
219
252
  @subscribers.each do |subscriber|
220
253
  unless disabled_subscribers&.any? { |s| s === subscriber }
@@ -240,5 +273,46 @@ module ActiveSupport
240
273
 
241
274
  nil
242
275
  end
276
+
277
+ private
278
+ def ensure_backtrace(error)
279
+ return if error.frozen? # re-raising won't add a backtrace or set the cause
280
+ return unless error.backtrace.nil?
281
+
282
+ begin
283
+ # As of Ruby 3.4, we could use Exception#set_backtrace to set the backtrace,
284
+ # but there's nothing like Exception#set_cause. Raising+rescuing is the only way to set the cause.
285
+ raise error
286
+ rescue error.class => error
287
+ end
288
+
289
+ count = 0
290
+ while error.backtrace_locations.first&.path == __FILE__
291
+ count += 1
292
+ error.backtrace_locations.shift
293
+ end
294
+
295
+ error.backtrace.shift(count)
296
+ end
297
+
298
+ class ErrorContextMiddlewareStack # :nodoc:
299
+ def initialize
300
+ @stack = []
301
+ end
302
+
303
+ # Add a middleware to the error context stack.
304
+ def use(middleware)
305
+ unless middleware.respond_to?(:call)
306
+ raise ArgumentError, "Error context middleware must respond to #call"
307
+ end
308
+
309
+ @stack << middleware
310
+ end
311
+
312
+ # Run all middlewares in the stack
313
+ def execute(error, handled:, severity:, context:, source:)
314
+ @stack.inject(context) { |c, middleware| middleware.call(error, context: c, handled:, severity:, source:) }
315
+ end
316
+ end
243
317
  end
244
318
  end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveSupport::EventReporter::TestHelper # :nodoc:
4
+ class EventSubscriber # :nodoc:
5
+ attr_reader :events
6
+
7
+ def initialize
8
+ @events = []
9
+ end
10
+
11
+ def emit(event)
12
+ @events << event
13
+ end
14
+ end
15
+
16
+ def event_matcher(name:, payload: nil, tags: {}, context: {}, source_location: nil)
17
+ ->(event) {
18
+ return false unless event[:name] == name
19
+ return false unless event[:payload] == payload
20
+ return false unless event[:tags] == tags
21
+ return false unless event[:context] == context
22
+
23
+ [:filepath, :lineno, :label].each do |key|
24
+ if source_location && source_location[key]
25
+ return false unless event[:source_location][key] == source_location[key]
26
+ end
27
+ end
28
+
29
+ true
30
+ }
31
+ end
32
+ end