activesupport 7.0.8 → 7.2.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (199) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +142 -428
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +4 -4
  5. data/lib/active_support/actionable_error.rb +3 -1
  6. data/lib/active_support/array_inquirer.rb +3 -1
  7. data/lib/active_support/backtrace_cleaner.rb +39 -7
  8. data/lib/active_support/benchmarkable.rb +1 -0
  9. data/lib/active_support/broadcast_logger.rb +251 -0
  10. data/lib/active_support/builder.rb +1 -1
  11. data/lib/active_support/cache/coder.rb +153 -0
  12. data/lib/active_support/cache/entry.rb +134 -0
  13. data/lib/active_support/cache/file_store.rb +49 -17
  14. data/lib/active_support/cache/mem_cache_store.rb +94 -128
  15. data/lib/active_support/cache/memory_store.rb +80 -25
  16. data/lib/active_support/cache/null_store.rb +6 -0
  17. data/lib/active_support/cache/redis_cache_store.rb +165 -152
  18. data/lib/active_support/cache/serializer_with_fallback.rb +152 -0
  19. data/lib/active_support/cache/strategy/local_cache.rb +29 -14
  20. data/lib/active_support/cache.rb +363 -291
  21. data/lib/active_support/callbacks.rb +118 -134
  22. data/lib/active_support/code_generator.rb +15 -10
  23. data/lib/active_support/concern.rb +4 -2
  24. data/lib/active_support/concurrency/load_interlock_aware_monitor.rb +42 -3
  25. data/lib/active_support/concurrency/null_lock.rb +13 -0
  26. data/lib/active_support/configurable.rb +10 -0
  27. data/lib/active_support/core_ext/array/conversions.rb +1 -2
  28. data/lib/active_support/core_ext/array.rb +0 -1
  29. data/lib/active_support/core_ext/class/subclasses.rb +17 -34
  30. data/lib/active_support/core_ext/date/blank.rb +4 -0
  31. data/lib/active_support/core_ext/date/conversions.rb +1 -2
  32. data/lib/active_support/core_ext/date.rb +0 -1
  33. data/lib/active_support/core_ext/date_and_time/calculations.rb +10 -0
  34. data/lib/active_support/core_ext/date_and_time/compatibility.rb +28 -1
  35. data/lib/active_support/core_ext/date_time/blank.rb +4 -0
  36. data/lib/active_support/core_ext/date_time/conversions.rb +2 -2
  37. data/lib/active_support/core_ext/date_time.rb +0 -1
  38. data/lib/active_support/core_ext/digest/uuid.rb +7 -10
  39. data/lib/active_support/core_ext/enumerable.rb +3 -75
  40. data/lib/active_support/core_ext/erb/util.rb +201 -0
  41. data/lib/active_support/core_ext/hash/conversions.rb +1 -1
  42. data/lib/active_support/core_ext/hash/deep_merge.rb +22 -14
  43. data/lib/active_support/core_ext/hash/keys.rb +4 -4
  44. data/lib/active_support/core_ext/module/attr_internal.rb +17 -6
  45. data/lib/active_support/core_ext/module/attribute_accessors.rb +6 -0
  46. data/lib/active_support/core_ext/module/attribute_accessors_per_thread.rb +34 -16
  47. data/lib/active_support/core_ext/module/concerning.rb +6 -6
  48. data/lib/active_support/core_ext/module/delegation.rb +20 -119
  49. data/lib/active_support/core_ext/module/deprecation.rb +12 -12
  50. data/lib/active_support/core_ext/module/introspection.rb +0 -1
  51. data/lib/active_support/core_ext/numeric/bytes.rb +9 -0
  52. data/lib/active_support/core_ext/numeric/conversions.rb +5 -3
  53. data/lib/active_support/core_ext/numeric.rb +0 -1
  54. data/lib/active_support/core_ext/object/blank.rb +45 -1
  55. data/lib/active_support/core_ext/object/deep_dup.rb +16 -0
  56. data/lib/active_support/core_ext/object/duplicable.rb +24 -15
  57. data/lib/active_support/core_ext/object/inclusion.rb +13 -5
  58. data/lib/active_support/core_ext/object/instance_variables.rb +4 -2
  59. data/lib/active_support/core_ext/object/json.rb +17 -7
  60. data/lib/active_support/core_ext/object/with.rb +46 -0
  61. data/lib/active_support/core_ext/object/with_options.rb +4 -4
  62. data/lib/active_support/core_ext/object.rb +1 -0
  63. data/lib/active_support/core_ext/pathname/blank.rb +20 -0
  64. data/lib/active_support/core_ext/pathname/existence.rb +2 -0
  65. data/lib/active_support/core_ext/pathname.rb +1 -0
  66. data/lib/active_support/core_ext/range/conversions.rb +28 -7
  67. data/lib/active_support/core_ext/range/overlap.rb +40 -0
  68. data/lib/active_support/core_ext/range.rb +1 -2
  69. data/lib/active_support/core_ext/securerandom.rb +1 -5
  70. data/lib/active_support/core_ext/string/conversions.rb +1 -1
  71. data/lib/active_support/core_ext/string/filters.rb +21 -15
  72. data/lib/active_support/core_ext/string/indent.rb +1 -1
  73. data/lib/active_support/core_ext/string/inflections.rb +16 -5
  74. data/lib/active_support/core_ext/string/multibyte.rb +1 -1
  75. data/lib/active_support/core_ext/string/output_safety.rb +34 -177
  76. data/lib/active_support/core_ext/thread/backtrace/location.rb +12 -0
  77. data/lib/active_support/core_ext/time/calculations.rb +36 -30
  78. data/lib/active_support/core_ext/time/compatibility.rb +16 -0
  79. data/lib/active_support/core_ext/time/conversions.rb +1 -3
  80. data/lib/active_support/core_ext/time/zones.rb +4 -4
  81. data/lib/active_support/core_ext/time.rb +0 -1
  82. data/lib/active_support/core_ext.rb +0 -1
  83. data/lib/active_support/current_attributes.rb +53 -46
  84. data/lib/active_support/deep_mergeable.rb +53 -0
  85. data/lib/active_support/delegation.rb +202 -0
  86. data/lib/active_support/dependencies/autoload.rb +9 -16
  87. data/lib/active_support/deprecation/behaviors.rb +65 -42
  88. data/lib/active_support/deprecation/constant_accessor.rb +47 -25
  89. data/lib/active_support/deprecation/deprecators.rb +104 -0
  90. data/lib/active_support/deprecation/disallowed.rb +3 -5
  91. data/lib/active_support/deprecation/method_wrappers.rb +6 -23
  92. data/lib/active_support/deprecation/proxy_wrappers.rb +34 -22
  93. data/lib/active_support/deprecation/reporting.rb +49 -27
  94. data/lib/active_support/deprecation.rb +39 -9
  95. data/lib/active_support/deprecator.rb +7 -0
  96. data/lib/active_support/descendants_tracker.rb +66 -172
  97. data/lib/active_support/duration/iso8601_parser.rb +2 -2
  98. data/lib/active_support/duration/iso8601_serializer.rb +1 -4
  99. data/lib/active_support/duration.rb +13 -7
  100. data/lib/active_support/encrypted_configuration.rb +30 -9
  101. data/lib/active_support/encrypted_file.rb +9 -4
  102. data/lib/active_support/environment_inquirer.rb +22 -2
  103. data/lib/active_support/error_reporter/test_helper.rb +15 -0
  104. data/lib/active_support/error_reporter.rb +160 -36
  105. data/lib/active_support/evented_file_update_checker.rb +0 -1
  106. data/lib/active_support/execution_wrapper.rb +4 -5
  107. data/lib/active_support/file_update_checker.rb +5 -3
  108. data/lib/active_support/fork_tracker.rb +4 -32
  109. data/lib/active_support/gem_version.rb +3 -3
  110. data/lib/active_support/gzip.rb +2 -0
  111. data/lib/active_support/hash_with_indifferent_access.rb +41 -25
  112. data/lib/active_support/html_safe_translation.rb +19 -6
  113. data/lib/active_support/i18n.rb +1 -1
  114. data/lib/active_support/i18n_railtie.rb +20 -13
  115. data/lib/active_support/inflector/inflections.rb +2 -0
  116. data/lib/active_support/inflector/methods.rb +23 -11
  117. data/lib/active_support/inflector/transliterate.rb +3 -1
  118. data/lib/active_support/isolated_execution_state.rb +26 -22
  119. data/lib/active_support/json/decoding.rb +2 -1
  120. data/lib/active_support/json/encoding.rb +25 -43
  121. data/lib/active_support/key_generator.rb +9 -1
  122. data/lib/active_support/lazy_load_hooks.rb +6 -4
  123. data/lib/active_support/locale/en.yml +2 -0
  124. data/lib/active_support/log_subscriber.rb +74 -34
  125. data/lib/active_support/logger.rb +22 -60
  126. data/lib/active_support/logger_thread_safe_level.rb +10 -32
  127. data/lib/active_support/message_encryptor.rb +197 -53
  128. data/lib/active_support/message_encryptors.rb +141 -0
  129. data/lib/active_support/message_pack/cache_serializer.rb +23 -0
  130. data/lib/active_support/message_pack/extensions.rb +305 -0
  131. data/lib/active_support/message_pack/serializer.rb +63 -0
  132. data/lib/active_support/message_pack.rb +50 -0
  133. data/lib/active_support/message_verifier.rb +220 -89
  134. data/lib/active_support/message_verifiers.rb +135 -0
  135. data/lib/active_support/messages/codec.rb +65 -0
  136. data/lib/active_support/messages/metadata.rb +111 -45
  137. data/lib/active_support/messages/rotation_coordinator.rb +93 -0
  138. data/lib/active_support/messages/rotator.rb +34 -32
  139. data/lib/active_support/messages/serializer_with_fallback.rb +158 -0
  140. data/lib/active_support/multibyte/chars.rb +4 -2
  141. data/lib/active_support/multibyte/unicode.rb +9 -37
  142. data/lib/active_support/notifications/fanout.rb +248 -87
  143. data/lib/active_support/notifications/instrumenter.rb +93 -25
  144. data/lib/active_support/notifications.rb +29 -28
  145. data/lib/active_support/number_helper/number_converter.rb +16 -7
  146. data/lib/active_support/number_helper/number_to_currency_converter.rb +6 -6
  147. data/lib/active_support/number_helper/number_to_human_size_converter.rb +3 -3
  148. data/lib/active_support/number_helper/number_to_phone_converter.rb +1 -0
  149. data/lib/active_support/number_helper.rb +379 -318
  150. data/lib/active_support/option_merger.rb +2 -2
  151. data/lib/active_support/ordered_hash.rb +3 -3
  152. data/lib/active_support/ordered_options.rb +67 -15
  153. data/lib/active_support/parameter_filter.rb +84 -69
  154. data/lib/active_support/proxy_object.rb +8 -3
  155. data/lib/active_support/railtie.rb +25 -20
  156. data/lib/active_support/reloader.rb +12 -4
  157. data/lib/active_support/rescuable.rb +2 -0
  158. data/lib/active_support/secure_compare_rotator.rb +16 -9
  159. data/lib/active_support/string_inquirer.rb +4 -2
  160. data/lib/active_support/subscriber.rb +10 -27
  161. data/lib/active_support/syntax_error_proxy.rb +60 -0
  162. data/lib/active_support/tagged_logging.rb +64 -25
  163. data/lib/active_support/test_case.rb +156 -7
  164. data/lib/active_support/testing/assertions.rb +28 -12
  165. data/lib/active_support/testing/autorun.rb +0 -2
  166. data/lib/active_support/testing/constant_stubbing.rb +54 -0
  167. data/lib/active_support/testing/deprecation.rb +20 -27
  168. data/lib/active_support/testing/error_reporter_assertions.rb +107 -0
  169. data/lib/active_support/testing/isolation.rb +21 -9
  170. data/lib/active_support/testing/method_call_assertions.rb +7 -8
  171. data/lib/active_support/testing/parallelization/server.rb +3 -0
  172. data/lib/active_support/testing/parallelize_executor.rb +8 -3
  173. data/lib/active_support/testing/setup_and_teardown.rb +2 -0
  174. data/lib/active_support/testing/stream.rb +1 -1
  175. data/lib/active_support/testing/strict_warnings.rb +43 -0
  176. data/lib/active_support/testing/tests_without_assertions.rb +19 -0
  177. data/lib/active_support/testing/time_helpers.rb +38 -16
  178. data/lib/active_support/time_with_zone.rb +12 -18
  179. data/lib/active_support/values/time_zone.rb +25 -14
  180. data/lib/active_support/version.rb +1 -1
  181. data/lib/active_support/xml_mini/jdom.rb +3 -10
  182. data/lib/active_support/xml_mini/nokogiri.rb +1 -1
  183. data/lib/active_support/xml_mini/nokogirisax.rb +1 -1
  184. data/lib/active_support/xml_mini/rexml.rb +1 -1
  185. data/lib/active_support/xml_mini.rb +12 -3
  186. data/lib/active_support.rb +15 -3
  187. metadata +145 -24
  188. data/lib/active_support/core_ext/array/deprecated_conversions.rb +0 -25
  189. data/lib/active_support/core_ext/date/deprecated_conversions.rb +0 -40
  190. data/lib/active_support/core_ext/date_time/deprecated_conversions.rb +0 -36
  191. data/lib/active_support/core_ext/numeric/deprecated_conversions.rb +0 -60
  192. data/lib/active_support/core_ext/range/deprecated_conversions.rb +0 -36
  193. data/lib/active_support/core_ext/range/include_time_with_zone.rb +0 -5
  194. data/lib/active_support/core_ext/range/overlaps.rb +0 -10
  195. data/lib/active_support/core_ext/time/deprecated_conversions.rb +0 -73
  196. data/lib/active_support/core_ext/uri.rb +0 -5
  197. data/lib/active_support/deprecation/instance_delegator.rb +0 -38
  198. data/lib/active_support/per_thread_registry.rb +0 -65
  199. data/lib/active_support/ruby_features.rb +0 -7
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_support/core_ext/object/blank"
4
-
5
3
  module ActiveSupport
6
4
  class Duration
7
5
  # Serializes duration to string according to ISO 8601 Duration format.
@@ -37,7 +35,6 @@ module ActiveSupport
37
35
  # Return pair of duration's parts and whole duration sign.
38
36
  # Parts are summarized (as they can become repetitive due to addition, etc).
39
37
  # Zero parts are removed as not significant.
40
- # If all parts are negative it will negate all of them and return minus as a sign.
41
38
  def normalize
42
39
  parts = @duration.parts.each_with_object(Hash.new(0)) do |(k, v), p|
43
40
  p[k] += v unless v.zero?
@@ -52,7 +49,7 @@ module ActiveSupport
52
49
  end
53
50
 
54
51
  def week_mixed_with_date?(parts)
55
- parts.key?(:weeks) && (parts.keys & DATE_COMPONENTS).any?
52
+ parts.key?(:weeks) && parts.keys.intersect?(DATE_COMPONENTS)
56
53
  end
57
54
 
58
55
  def format_seconds(seconds)
@@ -3,9 +3,10 @@
3
3
  require "active_support/core_ext/array/conversions"
4
4
  require "active_support/core_ext/module/delegation"
5
5
  require "active_support/core_ext/object/acts_like"
6
- require "active_support/core_ext/string/filters"
7
6
 
8
7
  module ActiveSupport
8
+ # = Active Support \Duration
9
+ #
9
10
  # Provides accurate date and time measurements using Date#advance and
10
11
  # Time#advance, respectively. It mainly supports the methods on Numeric.
11
12
  #
@@ -13,7 +14,7 @@ module ActiveSupport
13
14
  class Duration
14
15
  class Scalar < Numeric # :nodoc:
15
16
  attr_reader :value
16
- delegate :to_i, :to_f, :to_s, to: :value
17
+ delegate :to_i, :to_f, :to_s, to: :@value
17
18
 
18
19
  def initialize(value)
19
20
  @value = value
@@ -220,6 +221,8 @@ module ActiveSupport
220
221
  end
221
222
  end
222
223
 
224
+ Delegation.generate(self, [:to_f, :positive?, :negative?, :zero?, :abs], to: :@value, as: Integer, nilable: false)
225
+
223
226
  def initialize(value, parts, variable = nil) # :nodoc:
224
227
  @value, @parts = value, parts
225
228
  @parts.reject! { |k, v| v.zero? } unless value == 0
@@ -231,7 +234,10 @@ module ActiveSupport
231
234
  end
232
235
  end
233
236
 
234
- # 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}
235
241
  def parts
236
242
  @parts.dup
237
243
  end
@@ -365,8 +371,8 @@ module ActiveSupport
365
371
  # 1.year.to_i # => 31556952
366
372
  #
367
373
  # In such cases, Ruby's core
368
- # Date[https://ruby-doc.org/stdlib/libdoc/date/rdoc/Date.html] and
369
- # 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
370
376
  # date and time arithmetic.
371
377
  def to_i
372
378
  @value.to_i
@@ -503,8 +509,8 @@ module ActiveSupport
503
509
  value.respond_to?(method)
504
510
  end
505
511
 
506
- def method_missing(method, *args, &block)
507
- value.public_send(method, *args, &block)
512
+ def method_missing(...)
513
+ value.public_send(...)
508
514
  end
509
515
 
510
516
  def raise_type_error(other)
@@ -4,9 +4,12 @@ require "yaml"
4
4
  require "active_support/encrypted_file"
5
5
  require "active_support/ordered_options"
6
6
  require "active_support/core_ext/object/inclusion"
7
+ require "active_support/core_ext/hash/keys"
7
8
  require "active_support/core_ext/module/delegation"
8
9
 
9
10
  module ActiveSupport
11
+ # = Encrypted Configuration
12
+ #
10
13
  # Provides convenience methods on top of EncryptedFile to access values stored
11
14
  # as encrypted YAML.
12
15
  #
@@ -30,12 +33,23 @@ module ActiveSupport
30
33
  # # => KeyError
31
34
  #
32
35
  class EncryptedConfiguration < EncryptedFile
33
- delegate :[], :fetch, to: :config
36
+ class InvalidContentError < RuntimeError
37
+ def initialize(content_path)
38
+ super "Invalid YAML in '#{content_path}'."
39
+ end
40
+
41
+ def message
42
+ cause.is_a?(Psych::SyntaxError) ? "#{super}\n\n #{cause.message}" : super
43
+ end
44
+ end
45
+
34
46
  delegate_missing_to :options
35
47
 
36
48
  def initialize(config_path:, key_path:, env_key:, raise_if_missing_key:)
37
49
  super content_path: config_path, key_path: key_path,
38
50
  env_key: env_key, raise_if_missing_key: raise_if_missing_key
51
+ @config = nil
52
+ @options = nil
39
53
  end
40
54
 
41
55
  # Reads the file and returns the decrypted content. See EncryptedFile#read.
@@ -46,10 +60,8 @@ module ActiveSupport
46
60
  ""
47
61
  end
48
62
 
49
- def write(contents)
50
- deserialize(contents)
51
-
52
- super
63
+ def validate! # :nodoc:
64
+ deserialize(read)
53
65
  end
54
66
 
55
67
  # Returns the decrypted content as a Hash with symbolized keys.
@@ -64,11 +76,15 @@ module ActiveSupport
64
76
  @config ||= deserialize(read).deep_symbolize_keys
65
77
  end
66
78
 
79
+ def inspect # :nodoc:
80
+ "#<#{self.class.name}:#{'%#016x' % (object_id << 1)}>"
81
+ end
82
+
67
83
  private
68
84
  def deep_transform(hash)
69
85
  return hash unless hash.is_a?(Hash)
70
86
 
71
- h = ActiveSupport::InheritableOptions.new
87
+ h = ActiveSupport::OrderedOptions.new
72
88
  hash.each do |k, v|
73
89
  h[k] = deep_transform(v)
74
90
  end
@@ -79,9 +95,14 @@ module ActiveSupport
79
95
  @options ||= deep_transform(config)
80
96
  end
81
97
 
82
- def deserialize(config)
83
- doc = YAML.respond_to?(:unsafe_load) ? YAML.unsafe_load(config) : YAML.load(config)
84
- doc.presence || {}
98
+ def deserialize(content)
99
+ config = YAML.respond_to?(:unsafe_load) ?
100
+ YAML.unsafe_load(content, filename: content_path) :
101
+ YAML.load(content, filename: content_path)
102
+
103
+ config.presence || {}
104
+ rescue Psych::SyntaxError
105
+ raise InvalidContentError.new(content_path)
85
106
  end
86
107
  end
87
108
  end
@@ -53,6 +53,12 @@ module ActiveSupport
53
53
  read_env_key || read_key_file || handle_missing_key
54
54
  end
55
55
 
56
+ # Returns truthy if #key is truthy. Returns falsy otherwise. Unlike #key,
57
+ # does not raise MissingKeyError when +raise_if_missing_key+ is true.
58
+ def key?
59
+ read_env_key || read_key_file
60
+ end
61
+
56
62
  # Reads the file and returns the decrypted content.
57
63
  #
58
64
  # Raises:
@@ -63,7 +69,7 @@ module ActiveSupport
63
69
  # decrypted or verified.
64
70
  def read
65
71
  if !key.nil? && content_path.exist?
66
- decrypt content_path.binread
72
+ decrypt content_path.binread.strip
67
73
  else
68
74
  raise MissingContentError, content_path
69
75
  end
@@ -104,7 +110,7 @@ module ActiveSupport
104
110
  end
105
111
 
106
112
  def encryptor
107
- @encryptor ||= ActiveSupport::MessageEncryptor.new([ key ].pack("H*"), cipher: CIPHER)
113
+ @encryptor ||= ActiveSupport::MessageEncryptor.new([ key ].pack("H*"), cipher: CIPHER, serializer: Marshal)
108
114
  end
109
115
 
110
116
 
@@ -113,8 +119,7 @@ module ActiveSupport
113
119
  end
114
120
 
115
121
  def read_key_file
116
- return @key_file_contents if defined?(@key_file_contents)
117
- @key_file_contents = (key_path.binread.strip if key_path.exist?)
122
+ @key_file_contents ||= (key_path.binread.strip if key_path.exist?)
118
123
  end
119
124
 
120
125
  def handle_missing_key
@@ -1,20 +1,40 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "active_support/string_inquirer"
4
+ require "active_support/core_ext/object/inclusion"
4
5
 
5
6
  module ActiveSupport
6
7
  class EnvironmentInquirer < StringInquirer # :nodoc:
7
- DEFAULT_ENVIRONMENTS = ["development", "test", "production"]
8
+ # Optimization for the three default environments, so this inquirer doesn't need to rely on
9
+ # the slower delegation through method_missing that StringInquirer would normally entail.
10
+ DEFAULT_ENVIRONMENTS = %w[ development test production ]
11
+
12
+ # Environments that'll respond true for #local?
13
+ LOCAL_ENVIRONMENTS = %w[ development test ]
14
+
8
15
  def initialize(env)
16
+ raise(ArgumentError, "'local' is a reserved environment name") if env == "local"
17
+
9
18
  super(env)
10
19
 
11
20
  DEFAULT_ENVIRONMENTS.each do |default|
12
21
  instance_variable_set :"@#{default}", env == default
13
22
  end
23
+
24
+ @local = in? LOCAL_ENVIRONMENTS
14
25
  end
15
26
 
16
27
  DEFAULT_ENVIRONMENTS.each do |env|
17
- class_eval "def #{env}?; @#{env}; end"
28
+ class_eval <<~RUBY, __FILE__, __LINE__ + 1
29
+ def #{env}?
30
+ @#{env}
31
+ end
32
+ RUBY
33
+ end
34
+
35
+ # Returns true if we're in the development or test environment.
36
+ def local?
37
+ @local
18
38
  end
19
39
  end
20
40
  end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveSupport::ErrorReporter::TestHelper # :nodoc:
4
+ class ErrorSubscriber
5
+ attr_reader :events
6
+
7
+ def initialize
8
+ @events = []
9
+ end
10
+
11
+ def report(error, handled:, severity:, source:, context:)
12
+ @events << [error, handled, severity, source, context]
13
+ end
14
+ end
15
+ end
@@ -1,9 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveSupport
4
+ # = Active Support \Error Reporter
5
+ #
4
6
  # +ActiveSupport::ErrorReporter+ is a common interface for error reporting services.
5
7
  #
6
- # To rescue and report any unhandled error, you can use the +handle+ method:
8
+ # To rescue and report any unhandled error, you can use the #handle method:
7
9
  #
8
10
  # Rails.error.handle do
9
11
  # do_something!
@@ -11,68 +13,151 @@ module ActiveSupport
11
13
  #
12
14
  # If an error is raised, it will be reported and swallowed.
13
15
  #
14
- # Alternatively if you want to report the error but not swallow it, you can use +record+
16
+ # Alternatively, if you want to report the error but not swallow it, you can use #record:
15
17
  #
16
18
  # Rails.error.record do
17
19
  # do_something!
18
20
  # end
19
21
  #
20
- # Both methods can be restricted to only handle a specific exception class
22
+ # Both methods can be restricted to handle only a specific error class:
21
23
  #
22
24
  # maybe_tags = Rails.error.handle(Redis::BaseError) { redis.get("tags") }
23
25
  #
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
26
  class ErrorReporter
42
27
  SEVERITIES = %i(error warning info)
28
+ DEFAULT_SOURCE = "application"
29
+ DEFAULT_RESCUE = [StandardError].freeze
43
30
 
44
- attr_accessor :logger
31
+ attr_accessor :logger, :debug_mode
32
+
33
+ UnexpectedError = Class.new(Exception)
45
34
 
46
35
  def initialize(*subscribers, logger: nil)
47
36
  @subscribers = subscribers.flatten
48
37
  @logger = logger
38
+ @debug_mode = false
49
39
  end
50
40
 
51
- # Report any unhandled exception, and swallow it.
41
+ # Evaluates the given block, reporting and swallowing any unhandled error.
42
+ # If no error is raised, returns the return value of the block. Otherwise,
43
+ # returns the result of +fallback.call+, or +nil+ if +fallback+ is not
44
+ # specified.
52
45
  #
46
+ # # Will report a TypeError to all subscribers and return nil.
53
47
  # Rails.error.handle do
54
48
  # 1 + '1'
55
49
  # end
56
50
  #
57
- def handle(error_class = StandardError, severity: :warning, context: {}, fallback: nil)
51
+ # Can be restricted to handle only specific error classes:
52
+ #
53
+ # maybe_tags = Rails.error.handle(Redis::BaseError) { redis.get("tags") }
54
+ #
55
+ # ==== Options
56
+ #
57
+ # * +:severity+ - This value is passed along to subscribers to indicate how
58
+ # important the error report is. Can be +:error+, +:warning+, or +:info+.
59
+ # Defaults to +:warning+.
60
+ #
61
+ # * +:context+ - Extra information that is passed along to subscribers. For
62
+ # example:
63
+ #
64
+ # Rails.error.handle(context: { section: "admin" }) do
65
+ # # ...
66
+ # end
67
+ #
68
+ # * +:fallback+ - A callable that provides +handle+'s return value when an
69
+ # unhandled error is raised. For example:
70
+ #
71
+ # user = Rails.error.handle(fallback: -> { User.anonymous }) do
72
+ # User.find_by(params)
73
+ # end
74
+ #
75
+ # * +:source+ - This value is passed along to subscribers to indicate the
76
+ # source of the error. Subscribers can use this value to ignore certain
77
+ # errors. Defaults to <tt>"application"</tt>.
78
+ def handle(*error_classes, severity: :warning, context: {}, fallback: nil, source: DEFAULT_SOURCE)
79
+ error_classes = DEFAULT_RESCUE if error_classes.empty?
58
80
  yield
59
- rescue error_class => error
60
- report(error, handled: true, severity: severity, context: context)
81
+ rescue *error_classes => error
82
+ report(error, handled: true, severity: severity, context: context, source: source)
61
83
  fallback.call if fallback
62
84
  end
63
85
 
64
- def record(error_class = StandardError, severity: :error, context: {})
86
+ # Evaluates the given block, reporting and re-raising any unhandled error.
87
+ # If no error is raised, returns the return value of the block.
88
+ #
89
+ # # Will report a TypeError to all subscribers and re-raise it.
90
+ # Rails.error.record do
91
+ # 1 + '1'
92
+ # end
93
+ #
94
+ # Can be restricted to handle only specific error classes:
95
+ #
96
+ # tags = Rails.error.record(Redis::BaseError) { redis.get("tags") }
97
+ #
98
+ # ==== Options
99
+ #
100
+ # * +:severity+ - This value is passed along to subscribers to indicate how
101
+ # important the error report is. Can be +:error+, +:warning+, or +:info+.
102
+ # Defaults to +:error+.
103
+ #
104
+ # * +:context+ - Extra information that is passed along to subscribers. For
105
+ # example:
106
+ #
107
+ # Rails.error.record(context: { section: "admin" }) do
108
+ # # ...
109
+ # end
110
+ #
111
+ # * +:source+ - This value is passed along to subscribers to indicate the
112
+ # source of the error. Subscribers can use this value to ignore certain
113
+ # errors. Defaults to <tt>"application"</tt>.
114
+ def record(*error_classes, severity: :error, context: {}, source: DEFAULT_SOURCE)
115
+ error_classes = DEFAULT_RESCUE if error_classes.empty?
65
116
  yield
66
- rescue error_class => error
67
- report(error, handled: false, severity: severity, context: context)
117
+ rescue *error_classes => error
118
+ report(error, handled: false, severity: severity, context: context, source: source)
68
119
  raise
69
120
  end
70
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
+ error.set_backtrace(caller(1)) if error.backtrace.nil?
148
+
149
+ if @debug_mode
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
+
71
156
  # Register a new error subscriber. The subscriber must respond to
72
157
  #
73
- # report(Exception, handled: Boolean, context: Hash)
158
+ # report(Exception, handled: Boolean, severity: (:error OR :warning OR :info), context: Hash, source: String)
74
159
  #
75
- # The +report+ method +should+ never raise an error.
160
+ # The +report+ method <b>should never</b> raise an error.
76
161
  def subscribe(subscriber)
77
162
  unless subscriber.respond_to?(:report)
78
163
  raise ArgumentError, "Error subscribers must respond to #report"
@@ -80,26 +165,61 @@ module ActiveSupport
80
165
  @subscribers << subscriber
81
166
  end
82
167
 
83
- # Update the execution context that is accessible to error subscribers
168
+ # Unregister an error subscriber. Accepts either a subscriber or a class.
169
+ #
170
+ # subscriber = MyErrorSubscriber.new
171
+ # Rails.error.subscribe(subscriber)
172
+ #
173
+ # Rails.error.unsubscribe(subscriber)
174
+ # # or
175
+ # Rails.error.unsubscribe(MyErrorSubscriber)
176
+ def unsubscribe(subscriber)
177
+ @subscribers.delete_if { |s| subscriber === s }
178
+ end
179
+
180
+ # Prevent a subscriber from being notified of errors for the
181
+ # duration of the block. You may pass in the subscriber itself, or its class.
182
+ #
183
+ # This can be helpful for error reporting service integrations, when they wish
184
+ # to handle any errors higher in the stack.
185
+ def disable(subscriber)
186
+ disabled_subscribers = (ActiveSupport::IsolatedExecutionState[self] ||= [])
187
+ disabled_subscribers << subscriber
188
+ begin
189
+ yield
190
+ ensure
191
+ disabled_subscribers.delete(subscriber)
192
+ end
193
+ end
194
+
195
+ # Update the execution context that is accessible to error subscribers. Any
196
+ # context passed to #handle, #record, or #report will be merged with the
197
+ # context set here.
84
198
  #
85
199
  # Rails.error.set_context(section: "checkout", user_id: @user.id)
86
200
  #
87
- # See +ActiveSupport::ExecutionContext.set+
88
201
  def set_context(...)
89
202
  ActiveSupport::ExecutionContext.set(...)
90
203
  end
91
204
 
92
- # When the block based +handle+ and +record+ methods are not suitable, you can directly use +report+
205
+ # Report an error directly to subscribers. You can use this method when the
206
+ # block-based #handle and #record methods are not suitable.
207
+ #
208
+ # Rails.error.report(error)
93
209
  #
94
- # Rails.error.report(error, handled: true)
95
- def report(error, handled:, severity: handled ? :warning : :error, context: {})
210
+ def report(error, handled: true, severity: handled ? :warning : :error, context: {}, source: DEFAULT_SOURCE)
211
+ return if error.instance_variable_defined?(:@__rails_error_reported)
212
+
96
213
  unless SEVERITIES.include?(severity)
97
214
  raise ArgumentError, "severity must be one of #{SEVERITIES.map(&:inspect).join(", ")}, got: #{severity.inspect}"
98
215
  end
99
216
 
100
217
  full_context = ActiveSupport::ExecutionContext.to_h.merge(context)
218
+ disabled_subscribers = ActiveSupport::IsolatedExecutionState[self]
101
219
  @subscribers.each do |subscriber|
102
- subscriber.report(error, handled: handled, severity: severity, context: full_context)
220
+ unless disabled_subscribers&.any? { |s| s === subscriber }
221
+ subscriber.report(error, handled: handled, severity: severity, context: full_context, source: source)
222
+ end
103
223
  rescue => subscriber_error
104
224
  if logger
105
225
  logger.fatal(
@@ -111,6 +231,10 @@ module ActiveSupport
111
231
  end
112
232
  end
113
233
 
234
+ unless error.frozen?
235
+ error.instance_variable_set(:@__rails_error_reported, true)
236
+ end
237
+
114
238
  nil
115
239
  end
116
240
  end
@@ -6,7 +6,6 @@ require "listen"
6
6
  require "set"
7
7
  require "pathname"
8
8
  require "concurrent/atomic/atomic_boolean"
9
- require "active_support/fork_tracker"
10
9
 
11
10
  module ActiveSupport
12
11
  # 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
@@ -84,14 +83,14 @@ module ActiveSupport
84
83
  end
85
84
 
86
85
  # Perform the work in the supplied block as an execution.
87
- def self.wrap
86
+ def self.wrap(source: "application.active_support")
88
87
  return yield if active?
89
88
 
90
89
  instance = run!
91
90
  begin
92
91
  yield
93
92
  rescue => error
94
- error_reporter.report(error, handled: false)
93
+ error_reporter&.report(error, handled: false, source: source)
95
94
  raise
96
95
  ensure
97
96
  instance.complete!
@@ -108,8 +107,8 @@ module ActiveSupport
108
107
  end
109
108
  end
110
109
 
111
- def self.error_reporter
112
- @error_reporter ||= ActiveSupport::ErrorReporter.new
110
+ def self.error_reporter # :nodoc:
111
+ ActiveSupport.error_reporter
113
112
  end
114
113
 
115
114
  def self.active_key # :nodoc:
@@ -3,7 +3,9 @@
3
3
  require "active_support/core_ext/time/calculations"
4
4
 
5
5
  module ActiveSupport
6
- # FileUpdateChecker specifies the API used by Rails to watch files
6
+ # = \File Update Checker
7
+ #
8
+ # FileUpdateChecker specifies the API used by \Rails to watch files
7
9
  # and control reloading. The API depends on four methods:
8
10
  #
9
11
  # * +initialize+ which expects two parameters and one block as
@@ -20,7 +22,7 @@ module ActiveSupport
20
22
  # After initialization, a call to +execute_if_updated+ must execute
21
23
  # the block only if there was really a change in the filesystem.
22
24
  #
23
- # This class is used by Rails to reload the I18n framework whenever
25
+ # This class is used by \Rails to reload the I18n framework whenever
24
26
  # they are changed upon a new request.
25
27
  #
26
28
  # i18n_reloader = ActiveSupport::FileUpdateChecker.new(paths) do
@@ -102,7 +104,7 @@ module ActiveSupport
102
104
  @watched || begin
103
105
  all = @files.select { |f| File.exist?(f) }
104
106
  all.concat(Dir[@glob]) if @glob
105
- all
107
+ all.tap(&:uniq!)
106
108
  end
107
109
  end
108
110
 
@@ -2,42 +2,21 @@
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
9
- ForkTracker.check!
9
+ ForkTracker.after_fork_callback
10
10
  end
11
11
  pid
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
 
39
18
  class << self
40
- def check!
19
+ def after_fork_callback
41
20
  new_pid = Process.pid
42
21
  if @pid != new_pid
43
22
  @callbacks.each(&:call)
@@ -46,14 +25,7 @@ module ActiveSupport
46
25
  end
47
26
 
48
27
  def hook!
49
- if Process.respond_to?(:_fork) # Ruby 3.1+
50
- ::Process.singleton_class.prepend(ModernCoreExt)
51
- elsif Process.respond_to?(:fork)
52
- ::Object.prepend(CoreExtPrivate) if RUBY_VERSION < "3.0"
53
- ::Kernel.prepend(CoreExtPrivate)
54
- ::Kernel.singleton_class.prepend(CoreExt)
55
- ::Process.singleton_class.prepend(CoreExt)
56
- end
28
+ ::Process.singleton_class.prepend(CoreExt)
57
29
  end
58
30
 
59
31
  def after_fork(&block)