activesupport 7.1.3.2 → 8.0.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 (117) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +74 -1126
  3. data/lib/active_support/array_inquirer.rb +1 -1
  4. data/lib/active_support/backtrace_cleaner.rb +15 -3
  5. data/lib/active_support/benchmark.rb +21 -0
  6. data/lib/active_support/benchmarkable.rb +3 -2
  7. data/lib/active_support/broadcast_logger.rb +19 -18
  8. data/lib/active_support/cache/file_store.rb +27 -12
  9. data/lib/active_support/cache/mem_cache_store.rb +16 -74
  10. data/lib/active_support/cache/memory_store.rb +8 -3
  11. data/lib/active_support/cache/redis_cache_store.rb +21 -15
  12. data/lib/active_support/cache/serializer_with_fallback.rb +0 -23
  13. data/lib/active_support/cache.rb +76 -78
  14. data/lib/active_support/callbacks.rb +79 -116
  15. data/lib/active_support/class_attribute.rb +33 -0
  16. data/lib/active_support/code_generator.rb +24 -10
  17. data/lib/active_support/concurrency/share_lock.rb +0 -1
  18. data/lib/active_support/configuration_file.rb +15 -6
  19. data/lib/active_support/core_ext/array/conversions.rb +3 -5
  20. data/lib/active_support/core_ext/benchmark.rb +6 -9
  21. data/lib/active_support/core_ext/class/attribute.rb +24 -20
  22. data/lib/active_support/core_ext/class/subclasses.rb +15 -35
  23. data/lib/active_support/core_ext/date/blank.rb +4 -0
  24. data/lib/active_support/core_ext/date/conversions.rb +2 -2
  25. data/lib/active_support/core_ext/date_and_time/compatibility.rb +28 -1
  26. data/lib/active_support/core_ext/date_time/blank.rb +4 -0
  27. data/lib/active_support/core_ext/date_time/conversions.rb +0 -4
  28. data/lib/active_support/core_ext/digest/uuid.rb +6 -0
  29. data/lib/active_support/core_ext/enumerable.rb +8 -3
  30. data/lib/active_support/core_ext/erb/util.rb +7 -2
  31. data/lib/active_support/core_ext/hash/except.rb +0 -12
  32. data/lib/active_support/core_ext/hash/keys.rb +4 -4
  33. data/lib/active_support/core_ext/module/attr_internal.rb +16 -6
  34. data/lib/active_support/core_ext/module/delegation.rb +20 -148
  35. data/lib/active_support/core_ext/module/deprecation.rb +1 -4
  36. data/lib/active_support/core_ext/numeric/conversions.rb +3 -3
  37. data/lib/active_support/core_ext/object/blank.rb +45 -1
  38. data/lib/active_support/core_ext/object/duplicable.rb +24 -15
  39. data/lib/active_support/core_ext/object/instance_variables.rb +11 -19
  40. data/lib/active_support/core_ext/object/json.rb +21 -13
  41. data/lib/active_support/core_ext/object/with.rb +5 -3
  42. data/lib/active_support/core_ext/pathname/blank.rb +4 -0
  43. data/lib/active_support/core_ext/range/overlap.rb +1 -1
  44. data/lib/active_support/core_ext/securerandom.rb +4 -4
  45. data/lib/active_support/core_ext/string/conversions.rb +1 -1
  46. data/lib/active_support/core_ext/string/filters.rb +1 -1
  47. data/lib/active_support/core_ext/string/multibyte.rb +1 -1
  48. data/lib/active_support/core_ext/string/output_safety.rb +0 -7
  49. data/lib/active_support/core_ext/thread/backtrace/location.rb +2 -7
  50. data/lib/active_support/core_ext/time/calculations.rb +32 -30
  51. data/lib/active_support/core_ext/time/compatibility.rb +24 -0
  52. data/lib/active_support/core_ext/time/conversions.rb +2 -2
  53. data/lib/active_support/core_ext/time/zones.rb +1 -1
  54. data/lib/active_support/core_ext.rb +0 -1
  55. data/lib/active_support/current_attributes.rb +38 -40
  56. data/lib/active_support/delegation.rb +200 -0
  57. data/lib/active_support/dependencies/autoload.rb +0 -12
  58. data/lib/active_support/dependencies.rb +0 -1
  59. data/lib/active_support/deprecation/constant_accessor.rb +47 -26
  60. data/lib/active_support/deprecation/proxy_wrappers.rb +9 -12
  61. data/lib/active_support/deprecation/reporting.rb +3 -17
  62. data/lib/active_support/deprecation.rb +8 -5
  63. data/lib/active_support/descendants_tracker.rb +9 -87
  64. data/lib/active_support/duration/iso8601_parser.rb +2 -2
  65. data/lib/active_support/duration/iso8601_serializer.rb +1 -2
  66. data/lib/active_support/duration.rb +25 -16
  67. data/lib/active_support/encrypted_configuration.rb +20 -2
  68. data/lib/active_support/encrypted_file.rb +1 -1
  69. data/lib/active_support/error_reporter.rb +65 -3
  70. data/lib/active_support/evented_file_update_checker.rb +0 -2
  71. data/lib/active_support/execution_wrapper.rb +0 -1
  72. data/lib/active_support/file_update_checker.rb +1 -1
  73. data/lib/active_support/fork_tracker.rb +2 -38
  74. data/lib/active_support/gem_version.rb +4 -4
  75. data/lib/active_support/hash_with_indifferent_access.rb +21 -23
  76. data/lib/active_support/html_safe_translation.rb +7 -4
  77. data/lib/active_support/i18n_railtie.rb +19 -11
  78. data/lib/active_support/isolated_execution_state.rb +0 -2
  79. data/lib/active_support/json/encoding.rb +3 -3
  80. data/lib/active_support/log_subscriber.rb +1 -12
  81. data/lib/active_support/logger.rb +15 -2
  82. data/lib/active_support/logger_thread_safe_level.rb +0 -8
  83. data/lib/active_support/message_pack/extensions.rb +15 -2
  84. data/lib/active_support/message_verifier.rb +12 -0
  85. data/lib/active_support/messages/codec.rb +1 -1
  86. data/lib/active_support/multibyte/chars.rb +2 -2
  87. data/lib/active_support/notifications/fanout.rb +4 -8
  88. data/lib/active_support/notifications/instrumenter.rb +32 -21
  89. data/lib/active_support/notifications.rb +28 -27
  90. data/lib/active_support/number_helper/number_converter.rb +2 -2
  91. data/lib/active_support/number_helper.rb +22 -0
  92. data/lib/active_support/option_merger.rb +2 -2
  93. data/lib/active_support/ordered_options.rb +53 -15
  94. data/lib/active_support/railtie.rb +8 -11
  95. data/lib/active_support/string_inquirer.rb +1 -1
  96. data/lib/active_support/subscriber.rb +1 -0
  97. data/lib/active_support/syntax_error_proxy.rb +1 -11
  98. data/lib/active_support/tagged_logging.rb +9 -1
  99. data/lib/active_support/test_case.rb +3 -1
  100. data/lib/active_support/testing/assertions.rb +79 -21
  101. data/lib/active_support/testing/constant_stubbing.rb +30 -8
  102. data/lib/active_support/testing/deprecation.rb +5 -12
  103. data/lib/active_support/testing/isolation.rb +19 -9
  104. data/lib/active_support/testing/method_call_assertions.rb +2 -16
  105. data/lib/active_support/testing/parallelization/server.rb +3 -0
  106. data/lib/active_support/testing/setup_and_teardown.rb +2 -0
  107. data/lib/active_support/testing/strict_warnings.rb +8 -4
  108. data/lib/active_support/testing/tests_without_assertions.rb +19 -0
  109. data/lib/active_support/testing/time_helpers.rb +4 -3
  110. data/lib/active_support/time_with_zone.rb +30 -17
  111. data/lib/active_support/values/time_zone.rb +25 -14
  112. data/lib/active_support/xml_mini.rb +11 -2
  113. data/lib/active_support.rb +12 -4
  114. metadata +68 -19
  115. data/lib/active_support/deprecation/instance_delegator.rb +0 -65
  116. data/lib/active_support/proxy_object.rb +0 -17
  117. data/lib/active_support/ruby_features.rb +0 -7
@@ -3,7 +3,7 @@
3
3
  module ActiveSupport
4
4
  class Deprecation
5
5
  class DeprecationProxy # :nodoc:
6
- def self.new(*args, &block)
6
+ def self.new(*args, **kwargs, &block)
7
7
  object = args.first
8
8
 
9
9
  return object unless object
@@ -36,11 +36,10 @@ module ActiveSupport
36
36
  # (Backtrace)
37
37
  # # => "#<Object:0x007fb9b34c34b0>"
38
38
  class DeprecatedObjectProxy < DeprecationProxy
39
- def initialize(object, message, deprecator = nil)
39
+ def initialize(object, message, deprecator)
40
40
  @object = object
41
41
  @message = message
42
- ActiveSupport.deprecator.warn("DeprecatedObjectProxy without a deprecator is deprecated") unless deprecator
43
- @deprecator = deprecator || ActiveSupport::Deprecation._instance
42
+ @deprecator = deprecator
44
43
  end
45
44
 
46
45
  private
@@ -86,12 +85,11 @@ module ActiveSupport
86
85
  # example.request.to_s
87
86
  # # => "special_request"
88
87
  class DeprecatedInstanceVariableProxy < DeprecationProxy
89
- def initialize(instance, method, var = "@#{method}", deprecator = nil)
88
+ def initialize(instance, method, var = "@#{method}", deprecator:)
90
89
  @instance = instance
91
90
  @method = method
92
91
  @var = var
93
- ActiveSupport.deprecator.warn("DeprecatedInstanceVariableProxy without a deprecator is deprecated") unless deprecator
94
- @deprecator = deprecator || ActiveSupport::Deprecation._instance
92
+ @deprecator = deprecator
95
93
  end
96
94
 
97
95
  private
@@ -127,13 +125,12 @@ module ActiveSupport
127
125
  super
128
126
  end
129
127
 
130
- def initialize(old_const, new_const, deprecator = nil, message: "#{old_const} is deprecated! Use #{new_const} instead.")
128
+ def initialize(old_const, new_const, deprecator, message: "#{old_const} is deprecated! Use #{new_const} instead.")
131
129
  Kernel.require "active_support/inflector/methods"
132
130
 
133
131
  @old_const = old_const
134
132
  @new_const = new_const
135
- ActiveSupport.deprecator.warn("DeprecatedConstantProxy without a deprecator is deprecated") unless deprecator
136
- @deprecator = deprecator || ActiveSupport::Deprecation._instance
133
+ @deprecator = deprecator
137
134
  @message = message
138
135
  end
139
136
 
@@ -183,9 +180,9 @@ module ActiveSupport
183
180
  target.const_get(name)
184
181
  end
185
182
 
186
- def method_missing(called, *args, &block)
183
+ def method_missing(...)
187
184
  @deprecator.warn(@message, caller_locations)
188
- target.__send__(called, *args, &block)
185
+ target.__send__(...)
189
186
  end
190
187
  end
191
188
  end
@@ -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,24 +149,11 @@ module ActiveSupport
150
149
  [offending_line.path, offending_line.lineno, offending_line.label]
151
150
  end
152
151
 
153
- def _extract_callstack(callstack)
154
- warn "Please pass `caller_locations` to the deprecation API" if $VERBOSE
155
- offending_line = callstack.find { |line| !ignored_callstack?(line) } || callstack.first
156
-
157
- if offending_line
158
- if md = offending_line.match(/^(.+?):(\d+)(?::in `(.*?)')?/)
159
- md.captures
160
- else
161
- offending_line
162
- end
163
- end
164
- end
165
-
166
- RAILS_GEM_ROOT = File.expand_path("../../../..", __dir__) + "/"
167
- LIB_DIR = RbConfig::CONFIG["libdir"]
152
+ RAILS_GEM_ROOT = File.expand_path("../../../..", __dir__) + "/" # :nodoc:
153
+ LIB_DIR = RbConfig::CONFIG["libdir"] # :nodoc:
168
154
 
169
155
  def ignored_callstack?(path)
170
- path.start_with?(RAILS_GEM_ROOT, LIB_DIR)
156
+ path.start_with?(RAILS_GEM_ROOT, LIB_DIR) || path.include?("<internal:")
171
157
  end
172
158
  end
173
159
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "singleton"
4
-
5
3
  module ActiveSupport
6
4
  # = Active Support \Deprecation
7
5
  #
@@ -41,7 +39,6 @@ module ActiveSupport
41
39
  # a circular require warning for active_support/deprecation.rb.
42
40
  #
43
41
  # So, we define the constant first, and load dependencies later.
44
- require "active_support/deprecation/instance_delegator"
45
42
  require "active_support/deprecation/behaviors"
46
43
  require "active_support/deprecation/reporting"
47
44
  require "active_support/deprecation/disallowed"
@@ -52,12 +49,18 @@ module ActiveSupport
52
49
  require "active_support/core_ext/module/deprecation"
53
50
  require "concurrent/atomic/thread_local_var"
54
51
 
55
- include InstanceDelegator
56
52
  include Behavior
57
53
  include Reporting
58
54
  include Disallowed
59
55
  include MethodWrapper
60
56
 
57
+ MUTEX = Mutex.new # :nodoc:
58
+ private_constant :MUTEX
59
+
60
+ def self._instance # :nodoc:
61
+ @_instance ||= MUTEX.synchronize { @_instance ||= new }
62
+ end
63
+
61
64
  # The version number in which the deprecated behavior will be removed, by default.
62
65
  attr_accessor :deprecation_horizon
63
66
 
@@ -65,7 +68,7 @@ module ActiveSupport
65
68
  # and the second is a library name.
66
69
  #
67
70
  # ActiveSupport::Deprecation.new('2.0', 'MyLibrary')
68
- def initialize(deprecation_horizon = "7.2", gem_name = "Rails")
71
+ def initialize(deprecation_horizon = "8.1", gem_name = "Rails")
69
72
  self.gem_name = gem_name
70
73
  self.deprecation_horizon = deprecation_horizon
71
74
  # By default, warnings are not silenced and debugging is off.
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "weakref"
4
- require "active_support/ruby_features"
5
4
 
6
5
  module ActiveSupport
7
6
  # = Active Support Descendants Tracker
@@ -95,96 +94,19 @@ module ActiveSupport
95
94
  end
96
95
  end
97
96
 
98
- if RubyFeatures::CLASS_SUBCLASSES
99
- class << self
100
- def subclasses(klass)
101
- klass.subclasses
102
- end
103
-
104
- def descendants(klass)
105
- klass.descendants
106
- end
107
- end
108
-
109
- def descendants
110
- subclasses = DescendantsTracker.reject!(self.subclasses)
111
- subclasses.concat(subclasses.flat_map(&:descendants))
112
- end
113
- else
114
- # DescendantsArray is an array that contains weak references to classes.
115
- # Note: DescendantsArray is redundant with WeakSet, however WeakSet when used
116
- # on Ruby 2.7 or 3.0 can trigger a Ruby crash: https://bugs.ruby-lang.org/issues/18928
117
- class DescendantsArray # :nodoc:
118
- include Enumerable
119
-
120
- def initialize
121
- @refs = []
122
- end
123
-
124
- def <<(klass)
125
- @refs << WeakRef.new(klass)
126
- end
127
-
128
- def each
129
- @refs.reject! do |ref|
130
- yield ref.__getobj__
131
- false
132
- rescue WeakRef::RefError
133
- true
134
- end
135
- self
136
- end
137
-
138
- def refs_size
139
- @refs.size
140
- end
141
-
142
- def cleanup!
143
- @refs.delete_if { |ref| !ref.weakref_alive? }
144
- end
145
-
146
- def reject!
147
- @refs.reject! do |ref|
148
- yield ref.__getobj__
149
- rescue WeakRef::RefError
150
- true
151
- end
152
- end
153
- end
154
-
155
- @direct_descendants = {}
156
-
157
- class << self
158
- def subclasses(klass)
159
- descendants = @direct_descendants[klass]
160
- descendants ? DescendantsTracker.reject!(descendants.to_a) : []
161
- end
162
-
163
- def descendants(klass)
164
- subclasses = self.subclasses(klass)
165
- subclasses.concat(subclasses.flat_map { |k| descendants(k) })
166
- end
167
-
168
- # This is the only method that is not thread safe, but is only ever called
169
- # during the eager loading phase.
170
- def store_inherited(klass, descendant) # :nodoc:
171
- (@direct_descendants[klass] ||= DescendantsArray.new) << descendant
172
- end
173
- end
174
-
175
- def subclasses
176
- DescendantsTracker.subclasses(self)
97
+ class << self
98
+ def subclasses(klass)
99
+ klass.subclasses
177
100
  end
178
101
 
179
- def descendants
180
- DescendantsTracker.descendants(self)
102
+ def descendants(klass)
103
+ klass.descendants
181
104
  end
105
+ end
182
106
 
183
- private
184
- def inherited(base) # :nodoc:
185
- DescendantsTracker.store_inherited(self, base)
186
- super
187
- end
107
+ def descendants
108
+ subclasses = DescendantsTracker.reject!(self.subclasses)
109
+ subclasses.concat(subclasses.flat_map(&:descendants))
188
110
  end
189
111
  end
190
112
  end
@@ -102,12 +102,12 @@ module ActiveSupport
102
102
  raise_parsing_error("is empty duration") if parts.empty?
103
103
 
104
104
  # Mixing any of Y, M, D with W is invalid.
105
- if parts.key?(:weeks) && (parts.keys & DATE_COMPONENTS).any?
105
+ if parts.key?(:weeks) && parts.keys.intersect?(DATE_COMPONENTS)
106
106
  raise_parsing_error("mixing weeks with other date parts not allowed")
107
107
  end
108
108
 
109
109
  # Specifying an empty T part is invalid.
110
- if mode == :time && (parts.keys & TIME_COMPONENTS).empty?
110
+ if mode == :time && !parts.keys.intersect?(TIME_COMPONENTS)
111
111
  raise_parsing_error("time part marker is present but time part is empty")
112
112
  end
113
113
 
@@ -35,7 +35,6 @@ module ActiveSupport
35
35
  # Return pair of duration's parts and whole duration sign.
36
36
  # Parts are summarized (as they can become repetitive due to addition, etc).
37
37
  # Zero parts are removed as not significant.
38
- # If all parts are negative it will negate all of them and return minus as a sign.
39
38
  def normalize
40
39
  parts = @duration.parts.each_with_object(Hash.new(0)) do |(k, v), p|
41
40
  p[k] += v unless v.zero?
@@ -50,7 +49,7 @@ module ActiveSupport
50
49
  end
51
50
 
52
51
  def week_mixed_with_date?(parts)
53
- parts.key?(:weeks) && (parts.keys & DATE_COMPONENTS).any?
52
+ parts.key?(:weeks) && parts.keys.intersect?(DATE_COMPONENTS)
54
53
  end
55
54
 
56
55
  def format_seconds(seconds)
@@ -14,7 +14,7 @@ module ActiveSupport
14
14
  class Duration
15
15
  class Scalar < Numeric # :nodoc:
16
16
  attr_reader :value
17
- delegate :to_i, :to_f, :to_s, to: :value
17
+ delegate :to_i, :to_f, :to_s, to: :@value
18
18
 
19
19
  def initialize(value)
20
20
  @value = value
@@ -221,6 +221,8 @@ module ActiveSupport
221
221
  end
222
222
  end
223
223
 
224
+ Delegation.generate(self, [:to_f, :positive?, :negative?, :zero?, :abs], to: :@value, as: Integer, nilable: false)
225
+
224
226
  def initialize(value, parts, variable = nil) # :nodoc:
225
227
  @value, @parts = value, parts
226
228
  @parts.reject! { |k, v| v.zero? } unless value == 0
@@ -232,7 +234,10 @@ module ActiveSupport
232
234
  end
233
235
  end
234
236
 
235
- # Returns a copy of the parts hash that defines the duration
237
+ # Returns a copy of the parts hash that defines the duration.
238
+ #
239
+ # 5.minutes.parts # => {:minutes=>5}
240
+ # 3.years.parts # => {:years=>3}
236
241
  def parts
237
242
  @parts.dup
238
243
  end
@@ -366,8 +371,8 @@ module ActiveSupport
366
371
  # 1.year.to_i # => 31556952
367
372
  #
368
373
  # In such cases, Ruby's core
369
- # Date[https://ruby-doc.org/stdlib/libdoc/date/rdoc/Date.html] and
370
- # Time[https://ruby-doc.org/stdlib/libdoc/time/rdoc/Time.html] should be used for precision
374
+ # Date[https://docs.ruby-lang.org/en/master/Date.html] and
375
+ # Time[https://docs.ruby-lang.org/en/master/Time.html] should be used for precision
371
376
  # date and time arithmetic.
372
377
  def to_i
373
378
  @value.to_i
@@ -486,17 +491,21 @@ module ActiveSupport
486
491
  if @parts.empty?
487
492
  time.since(sign * value)
488
493
  else
489
- @parts.inject(time) do |t, (type, number)|
490
- if type == :seconds
491
- t.since(sign * number)
492
- elsif type == :minutes
493
- t.since(sign * number * 60)
494
- elsif type == :hours
495
- t.since(sign * number * 3600)
496
- else
497
- t.advance(type => sign * number)
498
- 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
499
506
  end
507
+
508
+ time
500
509
  end
501
510
  end
502
511
 
@@ -504,8 +513,8 @@ module ActiveSupport
504
513
  value.respond_to?(method)
505
514
  end
506
515
 
507
- def method_missing(method, *args, &block)
508
- value.public_send(method, *args, &block)
516
+ def method_missing(...)
517
+ value.public_send(...)
509
518
  end
510
519
 
511
520
  def raise_type_error(other)
@@ -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
 
@@ -69,7 +69,7 @@ module ActiveSupport
69
69
  # decrypted or verified.
70
70
  def read
71
71
  if !key.nil? && content_path.exist?
72
- decrypt content_path.binread
72
+ decrypt content_path.binread.strip
73
73
  else
74
74
  raise MissingContentError, content_path
75
75
  end
@@ -26,12 +26,16 @@ module ActiveSupport
26
26
  class ErrorReporter
27
27
  SEVERITIES = %i(error warning info)
28
28
  DEFAULT_SOURCE = "application"
29
+ DEFAULT_RESCUE = [StandardError].freeze
29
30
 
30
- attr_accessor :logger
31
+ attr_accessor :logger, :debug_mode
32
+
33
+ UnexpectedError = Class.new(Exception)
31
34
 
32
35
  def initialize(*subscribers, logger: nil)
33
36
  @subscribers = subscribers.flatten
34
37
  @logger = logger
38
+ @debug_mode = false
35
39
  end
36
40
 
37
41
  # Evaluates the given block, reporting and swallowing any unhandled error.
@@ -72,7 +76,7 @@ module ActiveSupport
72
76
  # source of the error. Subscribers can use this value to ignore certain
73
77
  # errors. Defaults to <tt>"application"</tt>.
74
78
  def handle(*error_classes, severity: :warning, context: {}, fallback: nil, source: DEFAULT_SOURCE)
75
- error_classes = [StandardError] if error_classes.blank?
79
+ error_classes = DEFAULT_RESCUE if error_classes.empty?
76
80
  yield
77
81
  rescue *error_classes => error
78
82
  report(error, handled: true, severity: severity, context: context, source: source)
@@ -108,13 +112,47 @@ module ActiveSupport
108
112
  # source of the error. Subscribers can use this value to ignore certain
109
113
  # errors. Defaults to <tt>"application"</tt>.
110
114
  def record(*error_classes, severity: :error, context: {}, source: DEFAULT_SOURCE)
111
- error_classes = [StandardError] if error_classes.blank?
115
+ error_classes = DEFAULT_RESCUE if error_classes.empty?
112
116
  yield
113
117
  rescue *error_classes => error
114
118
  report(error, handled: false, severity: severity, context: context, source: source)
115
119
  raise
116
120
  end
117
121
 
122
+ # Either report the given error when in production, or raise it when in development or test.
123
+ #
124
+ # When called in production, after the error is reported, this method will return
125
+ # nil and execution will continue.
126
+ #
127
+ # When called in development, the original error is wrapped in a different error class to ensure
128
+ # it's not being rescued higher in the stack and will be surfaced to the developer.
129
+ #
130
+ # This method is intended for reporting violated assertions about preconditions, or similar
131
+ # cases that can and should be gracefully handled in production, but that aren't supposed to happen.
132
+ #
133
+ # The error can be either an exception instance or a String.
134
+ #
135
+ # example:
136
+ #
137
+ # def edit
138
+ # if published?
139
+ # Rails.error.unexpected("[BUG] Attempting to edit a published article, that shouldn't be possible")
140
+ # return false
141
+ # end
142
+ # # ...
143
+ # end
144
+ #
145
+ def unexpected(error, severity: :warning, context: {}, source: DEFAULT_SOURCE)
146
+ error = RuntimeError.new(error) if error.is_a?(String)
147
+
148
+ if @debug_mode
149
+ ensure_backtrace(error)
150
+ raise UnexpectedError, "#{error.class.name}: #{error.message}", error.backtrace, cause: error
151
+ else
152
+ report(error, handled: true, severity: severity, context: context, source: source)
153
+ end
154
+ end
155
+
118
156
  # Register a new error subscriber. The subscriber must respond to
119
157
  #
120
158
  # report(Exception, handled: Boolean, severity: (:error OR :warning OR :info), context: Hash, source: String)
@@ -171,6 +209,7 @@ module ActiveSupport
171
209
  #
172
210
  def report(error, handled: true, severity: handled ? :warning : :error, context: {}, source: DEFAULT_SOURCE)
173
211
  return if error.instance_variable_defined?(:@__rails_error_reported)
212
+ ensure_backtrace(error)
174
213
 
175
214
  unless SEVERITIES.include?(severity)
176
215
  raise ArgumentError, "severity must be one of #{SEVERITIES.map(&:inspect).join(", ")}, got: #{severity.inspect}"
@@ -199,5 +238,28 @@ module ActiveSupport
199
238
 
200
239
  nil
201
240
  end
241
+
242
+ private
243
+ def ensure_backtrace(error)
244
+ return if error.frozen? # re-raising won't add a backtrace
245
+ return unless error.backtrace.nil?
246
+
247
+ begin
248
+ # We could use Exception#set_backtrace, but until Ruby 3.4
249
+ # it only support setting `Exception#backtrace` and not
250
+ # `Exception#backtrace_locations`. So raising the exception
251
+ # is a good way to build a real backtrace.
252
+ raise error
253
+ rescue error.class => error
254
+ end
255
+
256
+ count = 0
257
+ while error.backtrace_locations.first&.path == __FILE__
258
+ count += 1
259
+ error.backtrace_locations.shift
260
+ end
261
+
262
+ error.backtrace.shift(count)
263
+ end
202
264
  end
203
265
  end
@@ -3,10 +3,8 @@
3
3
  gem "listen", "~> 3.5"
4
4
  require "listen"
5
5
 
6
- require "set"
7
6
  require "pathname"
8
7
  require "concurrent/atomic/atomic_boolean"
9
- require "active_support/fork_tracker"
10
8
 
11
9
  module ActiveSupport
12
10
  # Allows you to "listen" to changes in a file system.
@@ -2,7 +2,6 @@
2
2
 
3
3
  require "active_support/error_reporter"
4
4
  require "active_support/callbacks"
5
- require "concurrent/hash"
6
5
 
7
6
  module ActiveSupport
8
7
  class ExecutionWrapper
@@ -104,7 +104,7 @@ module ActiveSupport
104
104
  @watched || begin
105
105
  all = @files.select { |f| File.exist?(f) }
106
106
  all.concat(Dir[@glob]) if @glob
107
- all
107
+ all.tap(&:uniq!)
108
108
  end
109
109
  end
110
110
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  module ActiveSupport
4
4
  module ForkTracker # :nodoc:
5
- module ModernCoreExt
5
+ module CoreExt
6
6
  def _fork
7
7
  pid = super
8
8
  if pid == 0
@@ -12,27 +12,6 @@ module ActiveSupport
12
12
  end
13
13
  end
14
14
 
15
- module CoreExt
16
- def fork(...)
17
- if block_given?
18
- super do
19
- ForkTracker.check!
20
- yield
21
- end
22
- else
23
- unless pid = super
24
- ForkTracker.check!
25
- end
26
- pid
27
- end
28
- end
29
- end
30
-
31
- module CoreExtPrivate
32
- include CoreExt
33
- private :fork
34
- end
35
-
36
15
  @pid = Process.pid
37
16
  @callbacks = []
38
17
 
@@ -45,23 +24,8 @@ module ActiveSupport
45
24
  end
46
25
  end
47
26
 
48
- if Process.respond_to?(:_fork) # Ruby 3.1+
49
- def check!
50
- # We trust the `_fork` callback
51
- end
52
- else
53
- alias_method :check!, :after_fork_callback
54
- end
55
-
56
27
  def hook!
57
- if Process.respond_to?(:_fork) # Ruby 3.1+
58
- ::Process.singleton_class.prepend(ModernCoreExt)
59
- elsif Process.respond_to?(:fork)
60
- ::Object.prepend(CoreExtPrivate) if RUBY_VERSION < "3.0"
61
- ::Kernel.prepend(CoreExtPrivate)
62
- ::Kernel.singleton_class.prepend(CoreExt)
63
- ::Process.singleton_class.prepend(CoreExt)
64
- end
28
+ ::Process.singleton_class.prepend(CoreExt)
65
29
  end
66
30
 
67
31
  def after_fork(&block)
@@ -7,10 +7,10 @@ module ActiveSupport
7
7
  end
8
8
 
9
9
  module VERSION
10
- MAJOR = 7
11
- MINOR = 1
12
- TINY = 3
13
- PRE = "2"
10
+ MAJOR = 8
11
+ MINOR = 0
12
+ TINY = 1
13
+ PRE = nil
14
14
 
15
15
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
16
16
  end