activesupport 8.0.3 → 8.1.0.beta1

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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +237 -175
  3. data/lib/active_support/backtrace_cleaner.rb +71 -0
  4. data/lib/active_support/cache/mem_cache_store.rb +13 -13
  5. data/lib/active_support/cache/redis_cache_store.rb +36 -30
  6. data/lib/active_support/cache/strategy/local_cache.rb +16 -7
  7. data/lib/active_support/cache/strategy/local_cache_middleware.rb +7 -7
  8. data/lib/active_support/cache.rb +69 -6
  9. data/lib/active_support/configurable.rb +28 -0
  10. data/lib/active_support/continuous_integration.rb +145 -0
  11. data/lib/active_support/core_ext/benchmark.rb +0 -1
  12. data/lib/active_support/core_ext/date_and_time/compatibility.rb +1 -1
  13. data/lib/active_support/core_ext/erb/util.rb +3 -3
  14. data/lib/active_support/core_ext/object/json.rb +8 -1
  15. data/lib/active_support/core_ext/object/to_query.rb +5 -0
  16. data/lib/active_support/core_ext/range.rb +0 -1
  17. data/lib/active_support/core_ext/string/multibyte.rb +10 -1
  18. data/lib/active_support/core_ext/string/output_safety.rb +19 -12
  19. data/lib/active_support/current_attributes/test_helper.rb +2 -2
  20. data/lib/active_support/current_attributes.rb +13 -10
  21. data/lib/active_support/deprecation/reporting.rb +4 -2
  22. data/lib/active_support/deprecation.rb +1 -1
  23. data/lib/active_support/editor.rb +70 -0
  24. data/lib/active_support/error_reporter.rb +50 -6
  25. data/lib/active_support/event_reporter/test_helper.rb +32 -0
  26. data/lib/active_support/event_reporter.rb +570 -0
  27. data/lib/active_support/evented_file_update_checker.rb +5 -1
  28. data/lib/active_support/execution_context.rb +64 -7
  29. data/lib/active_support/file_update_checker.rb +8 -6
  30. data/lib/active_support/gem_version.rb +3 -3
  31. data/lib/active_support/gzip.rb +1 -0
  32. data/lib/active_support/hash_with_indifferent_access.rb +27 -7
  33. data/lib/active_support/i18n_railtie.rb +1 -2
  34. data/lib/active_support/inflector/inflections.rb +31 -15
  35. data/lib/active_support/inflector/transliterate.rb +6 -8
  36. data/lib/active_support/isolated_execution_state.rb +7 -13
  37. data/lib/active_support/json/decoding.rb +2 -2
  38. data/lib/active_support/json/encoding.rb +103 -14
  39. data/lib/active_support/log_subscriber.rb +2 -0
  40. data/lib/active_support/message_encryptors.rb +52 -0
  41. data/lib/active_support/message_pack/extensions.rb +5 -0
  42. data/lib/active_support/message_verifiers.rb +52 -0
  43. data/lib/active_support/messages/rotation_coordinator.rb +9 -0
  44. data/lib/active_support/messages/rotator.rb +5 -0
  45. data/lib/active_support/multibyte/chars.rb +8 -1
  46. data/lib/active_support/multibyte.rb +4 -0
  47. data/lib/active_support/railtie.rb +26 -12
  48. data/lib/active_support/syntax_error_proxy.rb +3 -0
  49. data/lib/active_support/test_case.rb +61 -6
  50. data/lib/active_support/testing/assertions.rb +34 -6
  51. data/lib/active_support/testing/error_reporter_assertions.rb +18 -1
  52. data/lib/active_support/testing/event_reporter_assertions.rb +217 -0
  53. data/lib/active_support/testing/notification_assertions.rb +92 -0
  54. data/lib/active_support/testing/parallelization/worker.rb +2 -0
  55. data/lib/active_support/testing/parallelization.rb +13 -0
  56. data/lib/active_support/testing/tests_without_assertions.rb +1 -1
  57. data/lib/active_support/testing/time_helpers.rb +7 -3
  58. data/lib/active_support/time_with_zone.rb +19 -5
  59. data/lib/active_support/values/time_zone.rb +8 -1
  60. data/lib/active_support/xml_mini.rb +1 -2
  61. data/lib/active_support.rb +11 -0
  62. metadata +10 -5
  63. data/lib/active_support/core_ext/range/each.rb +0 -24
@@ -240,9 +240,16 @@ class Pathname # :nodoc:
240
240
  end
241
241
 
242
242
  unless IPAddr.method_defined?(:as_json, false)
243
+ # Use `IPAddr#as_json` from the IPAddr gem if the version is 1.2.7 or higher.
243
244
  class IPAddr # :nodoc:
244
245
  def as_json(options = nil)
245
- to_s
246
+ if ipv4? && prefix == 32
247
+ to_s
248
+ elsif ipv6? && prefix == 128
249
+ to_s
250
+ else
251
+ "#{self}/#{prefix}"
252
+ end
246
253
  end
247
254
  end
248
255
  end
@@ -17,6 +17,11 @@ class Object
17
17
  end
18
18
 
19
19
  class NilClass
20
+ # Returns a CGI-escaped +key+.
21
+ def to_query(key)
22
+ CGI.escape(key.to_param)
23
+ end
24
+
20
25
  # Returns +self+.
21
26
  def to_param
22
27
  self
@@ -3,5 +3,4 @@
3
3
  require "active_support/core_ext/range/conversions"
4
4
  require "active_support/core_ext/range/compare_range"
5
5
  require "active_support/core_ext/range/overlap"
6
- require "active_support/core_ext/range/each"
7
6
  require "active_support/core_ext/range/sole"
@@ -35,7 +35,16 @@ class String
35
35
  # For more information about the methods defined on the Chars proxy see ActiveSupport::Multibyte::Chars. For
36
36
  # information about how to change the default Multibyte behavior see ActiveSupport::Multibyte.
37
37
  def mb_chars
38
- ActiveSupport::Multibyte.proxy_class.new(self)
38
+ ActiveSupport.deprecator.warn(
39
+ "String#mb_chars is deprecated and will be removed in Rails 8.2. " \
40
+ "Use normal string methods instead."
41
+ )
42
+
43
+ if ActiveSupport::Multibyte.proxy_class == ActiveSupport::Multibyte::Chars
44
+ ActiveSupport::Multibyte::Chars.new(self, deprecation: false)
45
+ else
46
+ ActiveSupport::Multibyte.proxy_class.new(self)
47
+ end
39
48
  end
40
49
 
41
50
  # Returns +true+ if string has utf_8 encoding.
@@ -67,14 +67,13 @@ module ActiveSupport # :nodoc:
67
67
  original_concat(value)
68
68
  end
69
69
 
70
- def initialize(str = "")
71
- @html_safe = true
70
+ def initialize(_str = "")
72
71
  super
73
72
  end
74
73
 
75
74
  def initialize_copy(other)
76
75
  super
77
- @html_safe = other.html_safe?
76
+ @html_unsafe = true unless other.html_safe?
78
77
  end
79
78
 
80
79
  def concat(value)
@@ -116,7 +115,9 @@ module ActiveSupport # :nodoc:
116
115
  def *(_)
117
116
  new_string = super
118
117
  new_safe_buffer = new_string.is_a?(SafeBuffer) ? new_string : SafeBuffer.new(new_string)
119
- new_safe_buffer.instance_variable_set(:@html_safe, @html_safe)
118
+ if @html_unsafe
119
+ new_safe_buffer.instance_variable_set(:@html_unsafe, true)
120
+ end
120
121
  new_safe_buffer
121
122
  end
122
123
 
@@ -131,14 +132,18 @@ module ActiveSupport # :nodoc:
131
132
  self.class.new(super(escaped_args))
132
133
  end
133
134
 
134
- attr_reader :html_safe
135
- alias_method :html_safe?, :html_safe
136
- remove_method :html_safe
135
+ def html_safe?
136
+ @html_unsafe.nil?
137
+ end
137
138
 
138
139
  def to_s
139
140
  self
140
141
  end
141
142
 
143
+ def as_json(*)
144
+ to_str
145
+ end
146
+
142
147
  def to_param
143
148
  to_str
144
149
  end
@@ -155,7 +160,7 @@ module ActiveSupport # :nodoc:
155
160
  end # end
156
161
 
157
162
  def #{unsafe_method}!(*args) # def capitalize!(*args)
158
- @html_safe = false # @html_safe = false
163
+ @html_unsafe = true # @html_unsafe = true
159
164
  super # super
160
165
  end # end
161
166
  EOT
@@ -176,7 +181,7 @@ module ActiveSupport # :nodoc:
176
181
  end # end
177
182
 
178
183
  def #{unsafe_method}!(*args, &block) # def gsub!(*args, &block)
179
- @html_safe = false # @html_safe = false
184
+ @html_unsafe = true # @html_unsafe = true
180
185
  if block # if block
181
186
  super(*args) { |*params| # super(*args) { |*params|
182
187
  set_block_back_references(block, $~) # set_block_back_references(block, $~)
@@ -191,14 +196,14 @@ module ActiveSupport # :nodoc:
191
196
 
192
197
  private
193
198
  def explicit_html_escape_interpolated_argument(arg)
194
- (!html_safe? || arg.html_safe?) ? arg : CGI.escapeHTML(arg.to_s)
199
+ (!html_safe? || arg.html_safe?) ? arg : ERB::Util.unwrapped_html_escape(arg)
195
200
  end
196
201
 
197
202
  def implicit_html_escape_interpolated_argument(arg)
198
203
  if !html_safe? || arg.html_safe?
199
204
  arg
200
205
  else
201
- CGI.escapeHTML(arg.to_str)
206
+ ERB::Util.unwrapped_html_escape(arg.to_str)
202
207
  end
203
208
  end
204
209
 
@@ -210,7 +215,9 @@ module ActiveSupport # :nodoc:
210
215
 
211
216
  def string_into_safe_buffer(new_string, is_html_safe)
212
217
  new_safe_buffer = new_string.is_a?(SafeBuffer) ? new_string : SafeBuffer.new(new_string)
213
- new_safe_buffer.instance_variable_set :@html_safe, is_html_safe
218
+ unless is_html_safe
219
+ new_safe_buffer.instance_variable_set :@html_unsafe, true
220
+ end
214
221
  new_safe_buffer
215
222
  end
216
223
  end
@@ -2,12 +2,12 @@
2
2
 
3
3
  module ActiveSupport::CurrentAttributes::TestHelper # :nodoc:
4
4
  def before_setup
5
- ActiveSupport::CurrentAttributes.reset_all
5
+ ActiveSupport::CurrentAttributes.clear_all
6
6
  super
7
7
  end
8
8
 
9
9
  def after_teardown
10
10
  super
11
- ActiveSupport::CurrentAttributes.reset_all
11
+ ActiveSupport::CurrentAttributes.clear_all
12
12
  end
13
13
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "active_support/callbacks"
4
+ require "active_support/execution_context"
4
5
  require "active_support/core_ext/object/with"
5
6
  require "active_support/core_ext/enumerable"
6
7
  require "active_support/core_ext/module/delegation"
@@ -125,13 +126,13 @@ module ActiveSupport
125
126
  owner.define_cached_method(name, namespace: :current_attributes) do |batch|
126
127
  batch <<
127
128
  "def #{name}" <<
128
- "attributes[:#{name}]" <<
129
+ "@attributes[:#{name}]" <<
129
130
  "end"
130
131
  end
131
132
  owner.define_cached_method("#{name}=", namespace: :current_attributes) do |batch|
132
133
  batch <<
133
134
  "def #{name}=(value)" <<
134
- "attributes[:#{name}] = value" <<
135
+ "@attributes[:#{name}] = value" <<
135
136
  "end"
136
137
  end
137
138
  end
@@ -153,13 +154,11 @@ module ActiveSupport
153
154
 
154
155
  delegate :set, :reset, to: :instance
155
156
 
156
- def reset_all # :nodoc:
157
- current_instances.each_value(&:reset)
158
- end
159
-
160
157
  def clear_all # :nodoc:
161
- reset_all
162
- current_instances.clear
158
+ if instances = current_instances
159
+ instances.values.each(&:reset)
160
+ instances.clear
161
+ end
163
162
  end
164
163
 
165
164
  private
@@ -168,7 +167,7 @@ module ActiveSupport
168
167
  end
169
168
 
170
169
  def current_instances
171
- IsolatedExecutionState[:current_attributes_instances] ||= {}
170
+ ExecutionContext.current_attributes_instances
172
171
  end
173
172
 
174
173
  def current_instances_key
@@ -201,12 +200,16 @@ module ActiveSupport
201
200
 
202
201
  class_attribute :defaults, instance_writer: false, default: {}.freeze
203
202
 
204
- attr_accessor :attributes
203
+ attr_writer :attributes
205
204
 
206
205
  def initialize
207
206
  @attributes = resolve_defaults
208
207
  end
209
208
 
209
+ def attributes
210
+ @attributes.dup
211
+ end
212
+
210
213
  # Expose one or more attributes within a block. Old values are returned after the block concludes.
211
214
  # Example demonstrating the common use of needing to set Current attributes outside the request-cycle:
212
215
  #
@@ -149,8 +149,10 @@ module ActiveSupport
149
149
  [offending_line.path, offending_line.lineno, offending_line.label]
150
150
  end
151
151
 
152
- RAILS_GEM_ROOT = File.expand_path("../../../..", __dir__) + "/" # :nodoc:
153
- LIB_DIR = RbConfig::CONFIG["libdir"] # :nodoc:
152
+ RAILS_GEM_ROOT = File.expand_path("../../../..", __dir__) + "/"
153
+ private_constant :RAILS_GEM_ROOT
154
+ LIB_DIR = RbConfig::CONFIG["libdir"]
155
+ private_constant :LIB_DIR
154
156
 
155
157
  def ignored_callstack?(path)
156
158
  path.start_with?(RAILS_GEM_ROOT, LIB_DIR) || path.include?("<internal:")
@@ -68,7 +68,7 @@ module ActiveSupport
68
68
  # and the second is a library name.
69
69
  #
70
70
  # ActiveSupport::Deprecation.new('2.0', 'MyLibrary')
71
- def initialize(deprecation_horizon = "8.1", gem_name = "Rails")
71
+ def initialize(deprecation_horizon = "8.2", gem_name = "Rails")
72
72
  self.gem_name = gem_name
73
73
  self.deprecation_horizon = deprecation_horizon
74
74
  # By default, warnings are not silenced and debugging is off.
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :markup: markdown
4
+
5
+ module ActiveSupport
6
+ class Editor
7
+ @editors = {}
8
+ @current = false
9
+
10
+ class << self
11
+ # Registers a URL pattern for opening file in a given editor.
12
+ # This allows Rails to generate clickable links to control known editors.
13
+ #
14
+ # Example:
15
+ #
16
+ # ActiveSupport::Editor.register("myeditor", "myeditor://%s:%d")
17
+ def register(name, url_pattern, aliases: [])
18
+ editor = new(url_pattern)
19
+ @editors[name] = editor
20
+ aliases.each do |a|
21
+ @editors[a] = editor
22
+ end
23
+ end
24
+
25
+ # Returns the current editor pattern if it is known.
26
+ # First check for the `RAILS_EDITOR` environment variable, and if it's
27
+ # missing, check for the `EDITOR` environment variable.
28
+ def current
29
+ if @current == false
30
+ @current = if editor_name = ENV["RAILS_EDITOR"] || ENV["EDITOR"]
31
+ @editors[editor_name]
32
+ end
33
+ end
34
+ @current
35
+ end
36
+
37
+ # :nodoc:
38
+
39
+ def find(name)
40
+ @editors[name]
41
+ end
42
+
43
+ def reset
44
+ @current = false
45
+ end
46
+ end
47
+
48
+ def initialize(url_pattern)
49
+ @url_pattern = url_pattern
50
+ end
51
+
52
+ def url_for(path, line)
53
+ sprintf(@url_pattern, path, line)
54
+ end
55
+
56
+ register "atom", "atom://core/open/file?filename=%s&line=%d"
57
+ register "cursor", "cursor://file/%s:%f"
58
+ register "emacs", "emacs://open?url=file://%s&line=%d", aliases: ["emacsclient"]
59
+ register "idea", "idea://open?file=%s&line=%d"
60
+ register "macvim", "mvim://open?url=file://%s&line=%d", aliases: ["mvim"]
61
+ register "nova", "nova://open?path=%s&line=%d"
62
+ register "rubymine", "x-mine://open?file=%s&line=%d"
63
+ register "sublime", "subl://open?url=file://%s&line=%d", aliases: ["subl"]
64
+ register "textmate", "txmt://open?url=file://%s&line=%d", aliases: ["mate"]
65
+ register "vscode", "vscode://file/%s:%d", aliases: ["code"]
66
+ register "vscodium", "vscodium://file/%s:%d", aliases: ["codium"]
67
+ register "windsurf", "windsurf://file/%s:%d"
68
+ register "zed", "zed://file/%s:%d"
69
+ end
70
+ end
@@ -36,6 +36,7 @@ module ActiveSupport
36
36
  @subscribers = subscribers.flatten
37
37
  @logger = logger
38
38
  @debug_mode = false
39
+ @context_middlewares = ErrorContextMiddlewareStack.new
39
40
  end
40
41
 
41
42
  # Evaluates the given block, reporting and swallowing any unhandled error.
@@ -202,6 +203,22 @@ module ActiveSupport
202
203
  ActiveSupport::ExecutionContext.set(...)
203
204
  end
204
205
 
206
+ # Add a middleware to modify the error context before it is sent to subscribers.
207
+ #
208
+ # Middleware is added to a stack of callables run on an error's execution context
209
+ # before passing to subscribers. Allows creation of entries in error context that
210
+ # are shared by all subscribers.
211
+ #
212
+ # A context middleware receives the same parameters as #report.
213
+ # It must return a hash - the middleware stack returns the hash after it has
214
+ # run through all middlewares. A middleware can mutate or replace the hash.
215
+ #
216
+ # Rails.error.add_middleware(-> (error, context) { context.merge({ foo: :bar }) })
217
+ #
218
+ def add_middleware(middleware)
219
+ @context_middlewares.use(middleware)
220
+ end
221
+
205
222
  # Report an error directly to subscribers. You can use this method when the
206
223
  # block-based #handle and #record methods are not suitable.
207
224
  #
@@ -215,13 +232,22 @@ module ActiveSupport
215
232
  # string argument.
216
233
  def report(error, handled: true, severity: handled ? :warning : :error, context: {}, source: DEFAULT_SOURCE)
217
234
  return if error.instance_variable_defined?(:@__rails_error_reported)
235
+ raise ArgumentError, "Reported error must be an Exception, got: #{error.inspect}" unless error.is_a?(Exception)
236
+
218
237
  ensure_backtrace(error)
219
238
 
220
239
  unless SEVERITIES.include?(severity)
221
240
  raise ArgumentError, "severity must be one of #{SEVERITIES.map(&:inspect).join(", ")}, got: #{severity.inspect}"
222
241
  end
223
242
 
224
- full_context = ActiveSupport::ExecutionContext.to_h.merge(context)
243
+ full_context = @context_middlewares.execute(
244
+ error,
245
+ context: ActiveSupport::ExecutionContext.to_h.merge(context || {}),
246
+ handled:,
247
+ severity:,
248
+ source:
249
+ )
250
+
225
251
  disabled_subscribers = ActiveSupport::IsolatedExecutionState[self]
226
252
  @subscribers.each do |subscriber|
227
253
  unless disabled_subscribers&.any? { |s| s === subscriber }
@@ -250,14 +276,12 @@ module ActiveSupport
250
276
 
251
277
  private
252
278
  def ensure_backtrace(error)
253
- return if error.frozen? # re-raising won't add a backtrace
279
+ return if error.frozen? # re-raising won't add a backtrace or set the cause
254
280
  return unless error.backtrace.nil?
255
281
 
256
282
  begin
257
- # We could use Exception#set_backtrace, but until Ruby 3.4
258
- # it only support setting `Exception#backtrace` and not
259
- # `Exception#backtrace_locations`. So raising the exception
260
- # is a good way to build a real backtrace.
283
+ # As of Ruby 3.4, we could use Exception#set_backtrace to set the backtrace,
284
+ # but there's nothing like Exception#set_cause. Raising+rescuing is the only way to set the cause.
261
285
  raise error
262
286
  rescue error.class => error
263
287
  end
@@ -270,5 +294,25 @@ module ActiveSupport
270
294
 
271
295
  error.backtrace.shift(count)
272
296
  end
297
+
298
+ class ErrorContextMiddlewareStack # :nodoc:
299
+ def initialize
300
+ @stack = []
301
+ end
302
+
303
+ # Add a middleware to the error context stack.
304
+ def use(middleware)
305
+ unless middleware.respond_to?(:call)
306
+ raise ArgumentError, "Error context middleware must respond to #call"
307
+ end
308
+
309
+ @stack << middleware
310
+ end
311
+
312
+ # Run all middlewares in the stack
313
+ def execute(error, handled:, severity:, context:, source:)
314
+ @stack.inject(context) { |c, middleware| middleware.call(error, context: c, handled:, severity:, source:) }
315
+ end
316
+ end
273
317
  end
274
318
  end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveSupport::EventReporter::TestHelper # :nodoc:
4
+ class EventSubscriber # :nodoc:
5
+ attr_reader :events
6
+
7
+ def initialize
8
+ @events = []
9
+ end
10
+
11
+ def emit(event)
12
+ @events << event
13
+ end
14
+ end
15
+
16
+ def event_matcher(name:, payload: nil, tags: {}, context: {}, source_location: nil)
17
+ ->(event) {
18
+ return false unless event[:name] == name
19
+ return false unless event[:payload] == payload
20
+ return false unless event[:tags] == tags
21
+ return false unless event[:context] == context
22
+
23
+ [:filepath, :lineno, :label].each do |key|
24
+ if source_location && source_location[key]
25
+ return false unless event[:source_location][key] == source_location[key]
26
+ end
27
+ end
28
+
29
+ true
30
+ }
31
+ end
32
+ end