activesupport 7.0.8.7 → 7.2.2.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 (198) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +143 -459
  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/inclusion.rb +13 -5
  57. data/lib/active_support/core_ext/object/instance_variables.rb +4 -2
  58. data/lib/active_support/core_ext/object/json.rb +17 -7
  59. data/lib/active_support/core_ext/object/with.rb +46 -0
  60. data/lib/active_support/core_ext/object/with_options.rb +4 -4
  61. data/lib/active_support/core_ext/object.rb +1 -0
  62. data/lib/active_support/core_ext/pathname/blank.rb +20 -0
  63. data/lib/active_support/core_ext/pathname/existence.rb +2 -0
  64. data/lib/active_support/core_ext/pathname.rb +1 -0
  65. data/lib/active_support/core_ext/range/conversions.rb +28 -7
  66. data/lib/active_support/core_ext/range/overlap.rb +40 -0
  67. data/lib/active_support/core_ext/range.rb +1 -2
  68. data/lib/active_support/core_ext/securerandom.rb +1 -5
  69. data/lib/active_support/core_ext/string/conversions.rb +1 -1
  70. data/lib/active_support/core_ext/string/filters.rb +21 -15
  71. data/lib/active_support/core_ext/string/indent.rb +1 -1
  72. data/lib/active_support/core_ext/string/inflections.rb +16 -5
  73. data/lib/active_support/core_ext/string/multibyte.rb +1 -1
  74. data/lib/active_support/core_ext/string/output_safety.rb +34 -177
  75. data/lib/active_support/core_ext/thread/backtrace/location.rb +12 -0
  76. data/lib/active_support/core_ext/time/calculations.rb +36 -30
  77. data/lib/active_support/core_ext/time/compatibility.rb +16 -0
  78. data/lib/active_support/core_ext/time/conversions.rb +1 -3
  79. data/lib/active_support/core_ext/time/zones.rb +4 -4
  80. data/lib/active_support/core_ext/time.rb +0 -1
  81. data/lib/active_support/core_ext.rb +0 -1
  82. data/lib/active_support/current_attributes.rb +53 -46
  83. data/lib/active_support/deep_mergeable.rb +53 -0
  84. data/lib/active_support/delegation.rb +202 -0
  85. data/lib/active_support/dependencies/autoload.rb +9 -16
  86. data/lib/active_support/deprecation/behaviors.rb +65 -42
  87. data/lib/active_support/deprecation/constant_accessor.rb +47 -25
  88. data/lib/active_support/deprecation/deprecators.rb +104 -0
  89. data/lib/active_support/deprecation/disallowed.rb +3 -5
  90. data/lib/active_support/deprecation/method_wrappers.rb +6 -23
  91. data/lib/active_support/deprecation/proxy_wrappers.rb +34 -22
  92. data/lib/active_support/deprecation/reporting.rb +49 -27
  93. data/lib/active_support/deprecation.rb +39 -9
  94. data/lib/active_support/deprecator.rb +7 -0
  95. data/lib/active_support/descendants_tracker.rb +66 -172
  96. data/lib/active_support/duration/iso8601_parser.rb +2 -2
  97. data/lib/active_support/duration/iso8601_serializer.rb +1 -4
  98. data/lib/active_support/duration.rb +13 -7
  99. data/lib/active_support/encrypted_configuration.rb +30 -9
  100. data/lib/active_support/encrypted_file.rb +9 -4
  101. data/lib/active_support/environment_inquirer.rb +22 -2
  102. data/lib/active_support/error_reporter/test_helper.rb +15 -0
  103. data/lib/active_support/error_reporter.rb +160 -36
  104. data/lib/active_support/evented_file_update_checker.rb +0 -1
  105. data/lib/active_support/execution_wrapper.rb +4 -5
  106. data/lib/active_support/file_update_checker.rb +5 -3
  107. data/lib/active_support/fork_tracker.rb +4 -32
  108. data/lib/active_support/gem_version.rb +4 -4
  109. data/lib/active_support/gzip.rb +2 -0
  110. data/lib/active_support/hash_with_indifferent_access.rb +41 -25
  111. data/lib/active_support/html_safe_translation.rb +19 -6
  112. data/lib/active_support/i18n.rb +1 -1
  113. data/lib/active_support/i18n_railtie.rb +20 -13
  114. data/lib/active_support/inflector/inflections.rb +2 -0
  115. data/lib/active_support/inflector/methods.rb +23 -11
  116. data/lib/active_support/inflector/transliterate.rb +3 -1
  117. data/lib/active_support/isolated_execution_state.rb +26 -22
  118. data/lib/active_support/json/decoding.rb +2 -1
  119. data/lib/active_support/json/encoding.rb +25 -43
  120. data/lib/active_support/key_generator.rb +9 -1
  121. data/lib/active_support/lazy_load_hooks.rb +6 -4
  122. data/lib/active_support/locale/en.yml +2 -0
  123. data/lib/active_support/log_subscriber.rb +74 -34
  124. data/lib/active_support/logger.rb +22 -60
  125. data/lib/active_support/logger_thread_safe_level.rb +10 -32
  126. data/lib/active_support/message_encryptor.rb +197 -53
  127. data/lib/active_support/message_encryptors.rb +141 -0
  128. data/lib/active_support/message_pack/cache_serializer.rb +23 -0
  129. data/lib/active_support/message_pack/extensions.rb +305 -0
  130. data/lib/active_support/message_pack/serializer.rb +63 -0
  131. data/lib/active_support/message_pack.rb +50 -0
  132. data/lib/active_support/message_verifier.rb +220 -89
  133. data/lib/active_support/message_verifiers.rb +135 -0
  134. data/lib/active_support/messages/codec.rb +65 -0
  135. data/lib/active_support/messages/metadata.rb +111 -45
  136. data/lib/active_support/messages/rotation_coordinator.rb +93 -0
  137. data/lib/active_support/messages/rotator.rb +34 -32
  138. data/lib/active_support/messages/serializer_with_fallback.rb +158 -0
  139. data/lib/active_support/multibyte/chars.rb +4 -2
  140. data/lib/active_support/multibyte/unicode.rb +9 -37
  141. data/lib/active_support/notifications/fanout.rb +248 -87
  142. data/lib/active_support/notifications/instrumenter.rb +93 -25
  143. data/lib/active_support/notifications.rb +29 -28
  144. data/lib/active_support/number_helper/number_converter.rb +16 -7
  145. data/lib/active_support/number_helper/number_to_currency_converter.rb +6 -6
  146. data/lib/active_support/number_helper/number_to_human_size_converter.rb +3 -3
  147. data/lib/active_support/number_helper/number_to_phone_converter.rb +1 -0
  148. data/lib/active_support/number_helper.rb +379 -318
  149. data/lib/active_support/option_merger.rb +2 -2
  150. data/lib/active_support/ordered_hash.rb +3 -3
  151. data/lib/active_support/ordered_options.rb +67 -15
  152. data/lib/active_support/parameter_filter.rb +84 -69
  153. data/lib/active_support/proxy_object.rb +8 -3
  154. data/lib/active_support/railtie.rb +25 -20
  155. data/lib/active_support/reloader.rb +12 -4
  156. data/lib/active_support/rescuable.rb +2 -0
  157. data/lib/active_support/secure_compare_rotator.rb +16 -9
  158. data/lib/active_support/string_inquirer.rb +4 -2
  159. data/lib/active_support/subscriber.rb +10 -27
  160. data/lib/active_support/syntax_error_proxy.rb +60 -0
  161. data/lib/active_support/tagged_logging.rb +64 -25
  162. data/lib/active_support/test_case.rb +156 -7
  163. data/lib/active_support/testing/assertions.rb +28 -12
  164. data/lib/active_support/testing/autorun.rb +0 -2
  165. data/lib/active_support/testing/constant_stubbing.rb +54 -0
  166. data/lib/active_support/testing/deprecation.rb +20 -27
  167. data/lib/active_support/testing/error_reporter_assertions.rb +107 -0
  168. data/lib/active_support/testing/isolation.rb +21 -9
  169. data/lib/active_support/testing/method_call_assertions.rb +7 -8
  170. data/lib/active_support/testing/parallelization/server.rb +3 -0
  171. data/lib/active_support/testing/parallelize_executor.rb +8 -3
  172. data/lib/active_support/testing/setup_and_teardown.rb +2 -0
  173. data/lib/active_support/testing/stream.rb +1 -1
  174. data/lib/active_support/testing/strict_warnings.rb +43 -0
  175. data/lib/active_support/testing/tests_without_assertions.rb +19 -0
  176. data/lib/active_support/testing/time_helpers.rb +38 -16
  177. data/lib/active_support/time_with_zone.rb +12 -18
  178. data/lib/active_support/values/time_zone.rb +25 -14
  179. data/lib/active_support/version.rb +1 -1
  180. data/lib/active_support/xml_mini/jdom.rb +3 -10
  181. data/lib/active_support/xml_mini/nokogiri.rb +1 -1
  182. data/lib/active_support/xml_mini/nokogirisax.rb +1 -1
  183. data/lib/active_support/xml_mini/rexml.rb +1 -1
  184. data/lib/active_support/xml_mini.rb +12 -3
  185. data/lib/active_support.rb +15 -3
  186. metadata +140 -19
  187. data/lib/active_support/core_ext/array/deprecated_conversions.rb +0 -25
  188. data/lib/active_support/core_ext/date/deprecated_conversions.rb +0 -40
  189. data/lib/active_support/core_ext/date_time/deprecated_conversions.rb +0 -36
  190. data/lib/active_support/core_ext/numeric/deprecated_conversions.rb +0 -60
  191. data/lib/active_support/core_ext/range/deprecated_conversions.rb +0 -36
  192. data/lib/active_support/core_ext/range/include_time_with_zone.rb +0 -5
  193. data/lib/active_support/core_ext/range/overlaps.rb +0 -10
  194. data/lib/active_support/core_ext/time/deprecated_conversions.rb +0 -73
  195. data/lib/active_support/core_ext/uri.rb +0 -5
  196. data/lib/active_support/deprecation/instance_delegator.rb +0 -38
  197. data/lib/active_support/per_thread_registry.rb +0 -65
  198. 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)