activesupport 7.0.0.alpha2 → 7.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of activesupport might be problematic. Click here for more details.

Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +80 -0
  3. data/lib/active_support/cache/mem_cache_store.rb +9 -5
  4. data/lib/active_support/cache/memory_store.rb +2 -2
  5. data/lib/active_support/cache/redis_cache_store.rb +3 -8
  6. data/lib/active_support/cache/strategy/local_cache.rb +6 -12
  7. data/lib/active_support/callbacks.rb +145 -50
  8. data/lib/active_support/code_generator.rb +65 -0
  9. data/lib/active_support/core_ext/array/conversions.rb +3 -1
  10. data/lib/active_support/core_ext/array/deprecated_conversions.rb +25 -0
  11. data/lib/active_support/core_ext/array.rb +1 -0
  12. data/lib/active_support/core_ext/class/subclasses.rb +4 -2
  13. data/lib/active_support/core_ext/date/calculations.rb +2 -2
  14. data/lib/active_support/core_ext/date/conversions.rb +3 -3
  15. data/lib/active_support/core_ext/date/deprecated_conversions.rb +26 -0
  16. data/lib/active_support/core_ext/date.rb +1 -0
  17. data/lib/active_support/core_ext/date_and_time/compatibility.rb +1 -1
  18. data/lib/active_support/core_ext/date_time/conversions.rb +5 -5
  19. data/lib/active_support/core_ext/date_time/deprecated_conversions.rb +22 -0
  20. data/lib/active_support/core_ext/date_time.rb +1 -0
  21. data/lib/active_support/core_ext/digest/uuid.rb +26 -1
  22. data/lib/active_support/core_ext/module/attribute_accessors.rb +2 -0
  23. data/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb +19 -10
  24. data/lib/active_support/core_ext/numeric/conversions.rb +78 -75
  25. data/lib/active_support/core_ext/numeric/deprecated_conversions.rb +60 -0
  26. data/lib/active_support/core_ext/numeric.rb +1 -0
  27. data/lib/active_support/core_ext/object/with_options.rb +20 -1
  28. data/lib/active_support/core_ext/pathname/existence.rb +21 -0
  29. data/lib/active_support/core_ext/pathname.rb +3 -0
  30. data/lib/active_support/core_ext/range/conversions.rb +8 -8
  31. data/lib/active_support/core_ext/range/deprecated_conversions.rb +26 -0
  32. data/lib/active_support/core_ext/range/include_time_with_zone.rb +4 -25
  33. data/lib/active_support/core_ext/range.rb +1 -1
  34. data/lib/active_support/core_ext/time/calculations.rb +1 -1
  35. data/lib/active_support/core_ext/time/conversions.rb +4 -3
  36. data/lib/active_support/core_ext/time/deprecated_conversions.rb +22 -0
  37. data/lib/active_support/core_ext/time/zones.rb +2 -2
  38. data/lib/active_support/core_ext/time.rb +1 -0
  39. data/lib/active_support/core_ext/uri.rb +3 -13
  40. data/lib/active_support/core_ext.rb +1 -0
  41. data/lib/active_support/current_attributes.rb +26 -25
  42. data/lib/active_support/descendants_tracker.rb +175 -69
  43. data/lib/active_support/error_reporter.rb +117 -0
  44. data/lib/active_support/execution_context/test_helper.rb +13 -0
  45. data/lib/active_support/execution_context.rb +53 -0
  46. data/lib/active_support/execution_wrapper.rb +30 -4
  47. data/lib/active_support/executor/test_helper.rb +7 -0
  48. data/lib/active_support/fork_tracker.rb +18 -9
  49. data/lib/active_support/gem_version.rb +1 -1
  50. data/lib/active_support/html_safe_translation.rb +43 -0
  51. data/lib/active_support/i18n_railtie.rb +1 -1
  52. data/lib/active_support/inflector/inflections.rb +12 -3
  53. data/lib/active_support/inflector/methods.rb +2 -2
  54. data/lib/active_support/isolated_execution_state.rb +56 -0
  55. data/lib/active_support/logger_thread_safe_level.rb +2 -3
  56. data/lib/active_support/message_encryptor.rb +5 -0
  57. data/lib/active_support/multibyte/unicode.rb +0 -12
  58. data/lib/active_support/notifications/fanout.rb +61 -55
  59. data/lib/active_support/notifications/instrumenter.rb +15 -15
  60. data/lib/active_support/notifications.rb +5 -21
  61. data/lib/active_support/option_merger.rb +4 -0
  62. data/lib/active_support/per_thread_registry.rb +4 -0
  63. data/lib/active_support/railtie.rb +38 -11
  64. data/lib/active_support/ruby_features.rb +7 -0
  65. data/lib/active_support/subscriber.rb +2 -18
  66. data/lib/active_support/tagged_logging.rb +1 -1
  67. data/lib/active_support/testing/deprecation.rb +52 -1
  68. data/lib/active_support/testing/isolation.rb +1 -1
  69. data/lib/active_support/time_with_zone.rb +34 -6
  70. data/lib/active_support/values/time_zone.rb +5 -0
  71. data/lib/active_support/xml_mini.rb +3 -3
  72. data/lib/active_support.rb +7 -4
  73. metadata +23 -6
@@ -1,28 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_support/time_with_zone"
4
- require "active_support/deprecation"
5
-
6
- module ActiveSupport
7
- module IncludeTimeWithZone # :nodoc:
8
- # Extends the default Range#include? to support ActiveSupport::TimeWithZone.
9
- #
10
- # (1.hour.ago..1.hour.from_now).include?(Time.current) # => true
11
- #
12
- def include?(value)
13
- if self.begin.is_a?(TimeWithZone) || self.end.is_a?(TimeWithZone)
14
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
15
- Using `Range#include?` to check the inclusion of a value in
16
- a date time range is deprecated.
17
- It is recommended to use `Range#cover?` instead of `Range#include?` to
18
- check the inclusion of a value in a date time range.
19
- MSG
20
- cover?(value)
21
- else
22
- super
23
- end
24
- end
25
- end
26
- end
3
+ # frozen_string_literal: true
27
4
 
28
- Range.prepend(ActiveSupport::IncludeTimeWithZone)
5
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
6
+ `active_support/core_ext/range/include_time_with_zone` is deprecated and will be removed in Rails 7.1.
7
+ MSG
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "active_support/core_ext/range/conversions"
4
+ require "active_support/core_ext/range/deprecated_conversions" unless ENV["RAILS_DISABLE_DEPRECATED_TO_S_CONVERSION"]
4
5
  require "active_support/core_ext/range/compare_range"
5
- require "active_support/core_ext/range/include_time_with_zone"
6
6
  require "active_support/core_ext/range/overlaps"
7
7
  require "active_support/core_ext/range/each"
@@ -305,7 +305,7 @@ class Time
305
305
  other.is_a?(DateTime) ? to_f - other.to_f : minus_without_coercion(other)
306
306
  end
307
307
  alias_method :minus_without_coercion, :-
308
- alias_method :-, :minus_with_coercion
308
+ alias_method :-, :minus_with_coercion # rubocop:disable Lint/DuplicateMethods
309
309
 
310
310
  # Layers additional behavior on Time#<=> so that DateTime and ActiveSupport::TimeWithZone instances
311
311
  # can be chronologically compared with a Time
@@ -27,12 +27,12 @@ class Time
27
27
 
28
28
  # Converts to a formatted string. See DATE_FORMATS for built-in formats.
29
29
  #
30
- # This method is aliased to <tt>to_s</tt>.
30
+ # This method is aliased to <tt>to_fs</tt>.
31
31
  #
32
32
  # time = Time.now # => 2007-01-18 06:10:17 -06:00
33
33
  #
34
34
  # time.to_formatted_s(:time) # => "06:10"
35
- # time.to_s(:time) # => "06:10"
35
+ # time.to_formatted_s(:time) # => "06:10"
36
36
  #
37
37
  # time.to_formatted_s(:db) # => "2007-01-18 06:10:17"
38
38
  # time.to_formatted_s(:number) # => "20070118061017"
@@ -54,11 +54,12 @@ class Time
54
54
  if formatter = DATE_FORMATS[format]
55
55
  formatter.respond_to?(:call) ? formatter.call(self).to_s : strftime(formatter)
56
56
  else
57
+ # Change to `to_s` when deprecation is gone. Also deprecate `to_default_s`.
57
58
  to_default_s
58
59
  end
59
60
  end
61
+ alias_method :to_fs, :to_formatted_s
60
62
  alias_method :to_default_s, :to_s
61
- alias_method :to_s, :to_formatted_s
62
63
 
63
64
  # Returns a formatted string of the offset from UTC, or an alternative
64
65
  # string if the time zone is already UTC.
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "time"
4
+
5
+ class Time
6
+ NOT_SET = Object.new # :nodoc:
7
+ def to_s(format = NOT_SET) # :nodoc:
8
+ if formatter = DATE_FORMATS[format]
9
+ ActiveSupport::Deprecation.warn(
10
+ "Time#to_s(#{format.inspect}) is deprecated. Please use Time#to_formatted_s(#{format.inspect}) instead."
11
+ )
12
+ formatter.respond_to?(:call) ? formatter.call(self).to_s : strftime(formatter)
13
+ elsif format == NOT_SET
14
+ to_default_s
15
+ else
16
+ ActiveSupport::Deprecation.warn(
17
+ "Time#to_s(#{format.inspect}) is deprecated. Please use Time#to_formatted_s(#{format.inspect}) instead."
18
+ )
19
+ to_default_s
20
+ end
21
+ end
22
+ end
@@ -12,7 +12,7 @@ class Time
12
12
  # Returns the TimeZone for the current request, if this has been set (via Time.zone=).
13
13
  # If <tt>Time.zone</tt> has not been set for the current request, returns the TimeZone specified in <tt>config.time_zone</tt>.
14
14
  def zone
15
- Thread.current[:time_zone] || zone_default
15
+ ::ActiveSupport::IsolatedExecutionState[:time_zone] || zone_default
16
16
  end
17
17
 
18
18
  # Sets <tt>Time.zone</tt> to a TimeZone object for the current request/thread.
@@ -39,7 +39,7 @@ class Time
39
39
  # end
40
40
  # end
41
41
  def zone=(time_zone)
42
- Thread.current[:time_zone] = find_zone!(time_zone)
42
+ ::ActiveSupport::IsolatedExecutionState[:time_zone] = find_zone!(time_zone)
43
43
  end
44
44
 
45
45
  # Allows override of <tt>Time.zone</tt> locally inside supplied block;
@@ -4,4 +4,5 @@ require "active_support/core_ext/time/acts_like"
4
4
  require "active_support/core_ext/time/calculations"
5
5
  require "active_support/core_ext/time/compatibility"
6
6
  require "active_support/core_ext/time/conversions"
7
+ require "active_support/core_ext/time/deprecated_conversions" unless ENV["RAILS_DISABLE_DEPRECATED_TO_S_CONVERSION"]
7
8
  require "active_support/core_ext/time/zones"
@@ -1,15 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "uri"
4
-
5
- module URI
6
- class << self
7
- def parser
8
- ActiveSupport::Deprecation.warn(<<-MSG.squish)
9
- URI.parser is deprecated and will be removed in Rails 7.0.
10
- Use `URI::DEFAULT_PARSER` instead.
11
- MSG
12
- URI::DEFAULT_PARSER
13
- end
14
- end
15
- end
3
+ ActiveSupport::Deprecation.warn(<<-MSG.squish)
4
+ `active_support/core_ext/uri` is deprecated and will be removed in Rails 7.1.
5
+ MSG
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  Dir.glob(File.expand_path("core_ext/*.rb", __dir__)).sort.each do |path|
4
+ next if path.end_with?("core_ext/uri.rb")
4
5
  require path
5
6
  end
@@ -98,25 +98,37 @@ module ActiveSupport
98
98
 
99
99
  # Declares one or more attributes that will be given both class and instance accessor methods.
100
100
  def attribute(*names)
101
- generated_attribute_methods.module_eval do
101
+ ActiveSupport::CodeGenerator.batch(generated_attribute_methods, __FILE__, __LINE__) do |owner|
102
102
  names.each do |name|
103
- define_method(name) do
104
- attributes[name.to_sym]
103
+ owner.define_cached_method(name, namespace: :current_attributes) do |batch|
104
+ batch <<
105
+ "def #{name}" <<
106
+ "attributes[:#{name}]" <<
107
+ "end"
105
108
  end
106
-
107
- define_method("#{name}=") do |attribute|
108
- attributes[name.to_sym] = attribute
109
+ owner.define_cached_method("#{name}=", namespace: :current_attributes) do |batch|
110
+ batch <<
111
+ "def #{name}=(value)" <<
112
+ "attributes[:#{name}] = value" <<
113
+ "end"
109
114
  end
110
115
  end
111
116
  end
112
117
 
113
- names.each do |name|
114
- define_singleton_method(name) do
115
- instance.public_send(name)
116
- end
117
-
118
- define_singleton_method("#{name}=") do |attribute|
119
- instance.public_send("#{name}=", attribute)
118
+ ActiveSupport::CodeGenerator.batch(singleton_class, __FILE__, __LINE__) do |owner|
119
+ names.each do |name|
120
+ owner.define_cached_method(name, namespace: :current_attributes_delegation) do |batch|
121
+ batch <<
122
+ "def #{name}" <<
123
+ "instance.#{name}" <<
124
+ "end"
125
+ end
126
+ owner.define_cached_method("#{name}=", namespace: :current_attributes_delegation) do |batch|
127
+ batch <<
128
+ "def #{name}=(value)" <<
129
+ "instance.#{name} = value" <<
130
+ "end"
131
+ end
120
132
  end
121
133
  end
122
134
  end
@@ -143,24 +155,13 @@ module ActiveSupport
143
155
  current_instances.clear
144
156
  end
145
157
 
146
- def _use_thread_variables=(value) # :nodoc:
147
- clear_all
148
- @@use_thread_variables = value
149
- end
150
- @@use_thread_variables = false
151
-
152
158
  private
153
159
  def generated_attribute_methods
154
160
  @generated_attribute_methods ||= Module.new.tap { |mod| include mod }
155
161
  end
156
162
 
157
163
  def current_instances
158
- if @@use_thread_variables
159
- Thread.current.thread_variable_get(:current_attributes_instances) ||
160
- Thread.current.thread_variable_set(:current_attributes_instances, {})
161
- else
162
- Thread.current[:current_attributes_instances] ||= {}
163
- end
164
+ IsolatedExecutionState[:current_attributes_instances] ||= {}
164
165
  end
165
166
 
166
167
  def current_instances_key
@@ -1,113 +1,219 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "weakref"
4
+ require "active_support/ruby_features"
4
5
 
5
6
  module ActiveSupport
6
7
  # This module provides an internal implementation to track descendants
7
8
  # which is faster than iterating through ObjectSpace.
8
9
  module DescendantsTracker
9
- @@direct_descendants = {}
10
-
11
10
  class << self
12
11
  def direct_descendants(klass)
13
- descendants = @@direct_descendants[klass]
14
- descendants ? descendants.to_a : []
12
+ ActiveSupport::Deprecation.warn(<<~MSG)
13
+ ActiveSupport::DescendantsTracker.direct_descendants is deprecated and will be removed in Rails 7.1.
14
+ Use ActiveSupport::DescendantsTracker.subclasses instead.
15
+ MSG
16
+ subclasses(klass)
15
17
  end
16
- alias_method :subclasses, :direct_descendants
18
+ end
19
+
20
+ @clear_disabled = false
21
+
22
+ if RubyFeatures::CLASS_SUBCLASSES
23
+ @@excluded_descendants = if RUBY_ENGINE == "ruby"
24
+ # On MRI `ObjectSpace::WeakMap` keys are weak references.
25
+ # So we can simply use WeakMap as a `Set`.
26
+ ObjectSpace::WeakMap.new
27
+ else
28
+ # On TruffleRuby `ObjectSpace::WeakMap` keys are strong references.
29
+ # So we use `object_id` as a key and the actual object as a value.
30
+ #
31
+ # JRuby for now doesn't have Class#descendant, but when it will, it will likely
32
+ # have the same WeakMap semantic than Truffle so we future proof this as much as possible.
33
+ class WeakSet # :nodoc:
34
+ def initialize
35
+ @map = ObjectSpace::WeakMap.new
36
+ end
17
37
 
18
- def descendants(klass)
19
- arr = []
20
- accumulate_descendants(klass, arr)
21
- arr
38
+ def [](object)
39
+ @map.key?(object.object_id)
40
+ end
41
+
42
+ def []=(object, _present)
43
+ @map[object_id] = object
44
+ end
45
+ end
46
+ WeakSet.new
22
47
  end
23
48
 
24
- def clear(only: nil)
25
- if only.nil?
26
- @@direct_descendants.clear
27
- return
49
+ class << self
50
+ def disable_clear! # :nodoc:
51
+ unless @clear_disabled
52
+ @clear_disabled = true
53
+ remove_method(:subclasses)
54
+ remove_method(:descendants)
55
+ @@excluded_descendants = nil
56
+ end
57
+ end
58
+
59
+ def subclasses(klass)
60
+ klass.subclasses
28
61
  end
29
62
 
30
- @@direct_descendants.each do |klass, direct_descendants_of_klass|
31
- if only.member?(klass)
32
- @@direct_descendants.delete(klass)
33
- else
34
- direct_descendants_of_klass.reject! do |direct_descendant_of_class|
35
- only.member?(direct_descendant_of_class)
63
+ def descendants(klass)
64
+ klass.descendants
65
+ end
66
+
67
+ def clear(classes) # :nodoc:
68
+ raise "DescendantsTracker.clear was disabled because config.cache_classes = true" if @clear_disabled
69
+
70
+ classes.each do |klass|
71
+ @@excluded_descendants[klass] = true
72
+ klass.descendants.each do |descendant|
73
+ @@excluded_descendants[descendant] = true
36
74
  end
37
75
  end
38
76
  end
77
+
78
+ def native? # :nodoc:
79
+ true
80
+ end
81
+ end
82
+
83
+ def subclasses
84
+ subclasses = super
85
+ subclasses.reject! { |d| @@excluded_descendants[d] }
86
+ subclasses
87
+ end
88
+
89
+ def descendants
90
+ descendants = super
91
+ descendants.reject! { |d| @@excluded_descendants[d] }
92
+ descendants
39
93
  end
40
94
 
41
- # This is the only method that is not thread safe, but is only ever called
42
- # during the eager loading phase.
43
- def store_inherited(klass, descendant)
44
- (@@direct_descendants[klass] ||= DescendantsArray.new) << descendant
95
+ def direct_descendants
96
+ ActiveSupport::Deprecation.warn(<<~MSG)
97
+ ActiveSupport::DescendantsTracker#direct_descendants is deprecated and will be removed in Rails 7.1.
98
+ Use #subclasses instead.
99
+ MSG
100
+ subclasses
45
101
  end
102
+ else
103
+ @@direct_descendants = {}
104
+
105
+ class << self
106
+ def disable_clear! # :nodoc:
107
+ @clear_disabled = true
108
+ end
109
+
110
+ def subclasses(klass)
111
+ descendants = @@direct_descendants[klass]
112
+ descendants ? descendants.to_a : []
113
+ end
114
+
115
+ def descendants(klass)
116
+ arr = []
117
+ accumulate_descendants(klass, arr)
118
+ arr
119
+ end
120
+
121
+ def clear(classes) # :nodoc:
122
+ raise "DescendantsTracker.clear was disabled because config.cache_classes = true" if @clear_disabled
46
123
 
47
- private
48
- def accumulate_descendants(klass, acc)
49
- if direct_descendants = @@direct_descendants[klass]
50
- direct_descendants.each do |direct_descendant|
51
- acc << direct_descendant
52
- accumulate_descendants(direct_descendant, acc)
124
+ @@direct_descendants.each do |klass, direct_descendants_of_klass|
125
+ if classes.member?(klass)
126
+ @@direct_descendants.delete(klass)
127
+ else
128
+ direct_descendants_of_klass.reject! do |direct_descendant_of_class|
129
+ classes.member?(direct_descendant_of_class)
130
+ end
53
131
  end
54
132
  end
55
133
  end
56
- end
57
134
 
58
- def inherited(base)
59
- DescendantsTracker.store_inherited(self, base)
60
- super
61
- end
135
+ def native? # :nodoc:
136
+ false
137
+ end
62
138
 
63
- def direct_descendants
64
- DescendantsTracker.direct_descendants(self)
65
- end
66
- alias_method :subclasses, :direct_descendants
139
+ # This is the only method that is not thread safe, but is only ever called
140
+ # during the eager loading phase.
141
+ def store_inherited(klass, descendant)
142
+ (@@direct_descendants[klass] ||= DescendantsArray.new) << descendant
143
+ end
67
144
 
68
- def descendants
69
- DescendantsTracker.descendants(self)
70
- end
145
+ private
146
+ def accumulate_descendants(klass, acc)
147
+ if direct_descendants = @@direct_descendants[klass]
148
+ direct_descendants.each do |direct_descendant|
149
+ acc << direct_descendant
150
+ accumulate_descendants(direct_descendant, acc)
151
+ end
152
+ end
153
+ end
154
+ end
71
155
 
72
- # DescendantsArray is an array that contains weak references to classes.
73
- class DescendantsArray # :nodoc:
74
- include Enumerable
156
+ def inherited(base)
157
+ DescendantsTracker.store_inherited(self, base)
158
+ super
159
+ end
75
160
 
76
- def initialize
77
- @refs = []
161
+ def direct_descendants
162
+ ActiveSupport::Deprecation.warn(<<~MSG)
163
+ ActiveSupport::DescendantsTracker#direct_descendants is deprecated and will be removed in Rails 7.1.
164
+ Use #subclasses instead.
165
+ MSG
166
+ DescendantsTracker.subclasses(self)
78
167
  end
79
168
 
80
- def initialize_copy(orig)
81
- @refs = @refs.dup
169
+ def subclasses
170
+ DescendantsTracker.subclasses(self)
82
171
  end
83
172
 
84
- def <<(klass)
85
- @refs << WeakRef.new(klass)
173
+ def descendants
174
+ DescendantsTracker.descendants(self)
86
175
  end
87
176
 
88
- def each
89
- @refs.reject! do |ref|
90
- yield ref.__getobj__
91
- false
92
- rescue WeakRef::RefError
93
- true
177
+ # DescendantsArray is an array that contains weak references to classes.
178
+ class DescendantsArray # :nodoc:
179
+ include Enumerable
180
+
181
+ def initialize
182
+ @refs = []
94
183
  end
95
- self
96
- end
97
184
 
98
- def refs_size
99
- @refs.size
100
- end
185
+ def initialize_copy(orig)
186
+ @refs = @refs.dup
187
+ end
101
188
 
102
- def cleanup!
103
- @refs.delete_if { |ref| !ref.weakref_alive? }
104
- end
189
+ def <<(klass)
190
+ @refs << WeakRef.new(klass)
191
+ end
105
192
 
106
- def reject!
107
- @refs.reject! do |ref|
108
- yield ref.__getobj__
109
- rescue WeakRef::RefError
110
- true
193
+ def each
194
+ @refs.reject! do |ref|
195
+ yield ref.__getobj__
196
+ false
197
+ rescue WeakRef::RefError
198
+ true
199
+ end
200
+ self
201
+ end
202
+
203
+ def refs_size
204
+ @refs.size
205
+ end
206
+
207
+ def cleanup!
208
+ @refs.delete_if { |ref| !ref.weakref_alive? }
209
+ end
210
+
211
+ def reject!
212
+ @refs.reject! do |ref|
213
+ yield ref.__getobj__
214
+ rescue WeakRef::RefError
215
+ true
216
+ end
111
217
  end
112
218
  end
113
219
  end
@@ -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
@@ -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