activesupport 8.0.2.1 → 8.1.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 (105) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +334 -129
  3. data/README.rdoc +1 -1
  4. data/lib/active_support/backtrace_cleaner.rb +71 -0
  5. data/lib/active_support/broadcast_logger.rb +46 -59
  6. data/lib/active_support/cache/mem_cache_store.rb +25 -27
  7. data/lib/active_support/cache/redis_cache_store.rb +36 -30
  8. data/lib/active_support/cache/strategy/local_cache.rb +16 -7
  9. data/lib/active_support/cache/strategy/local_cache_middleware.rb +7 -7
  10. data/lib/active_support/cache.rb +70 -6
  11. data/lib/active_support/callbacks.rb +20 -8
  12. data/lib/active_support/concurrency/load_interlock_aware_monitor.rb +8 -62
  13. data/lib/active_support/concurrency/thread_monitor.rb +55 -0
  14. data/lib/active_support/configurable.rb +34 -0
  15. data/lib/active_support/continuous_integration.rb +145 -0
  16. data/lib/active_support/core_ext/array.rb +7 -7
  17. data/lib/active_support/core_ext/benchmark.rb +4 -11
  18. data/lib/active_support/core_ext/big_decimal.rb +1 -1
  19. data/lib/active_support/core_ext/class/attribute.rb +8 -6
  20. data/lib/active_support/core_ext/class.rb +2 -2
  21. data/lib/active_support/core_ext/date.rb +5 -5
  22. data/lib/active_support/core_ext/date_and_time/compatibility.rb +0 -35
  23. data/lib/active_support/core_ext/date_time/compatibility.rb +3 -5
  24. data/lib/active_support/core_ext/date_time/conversions.rb +4 -2
  25. data/lib/active_support/core_ext/date_time.rb +5 -5
  26. data/lib/active_support/core_ext/digest.rb +1 -1
  27. data/lib/active_support/core_ext/enumerable.rb +16 -4
  28. data/lib/active_support/core_ext/erb/util.rb +3 -3
  29. data/lib/active_support/core_ext/file.rb +1 -1
  30. data/lib/active_support/core_ext/hash.rb +8 -8
  31. data/lib/active_support/core_ext/integer.rb +3 -3
  32. data/lib/active_support/core_ext/kernel.rb +3 -3
  33. data/lib/active_support/core_ext/module.rb +11 -11
  34. data/lib/active_support/core_ext/numeric.rb +3 -3
  35. data/lib/active_support/core_ext/object/json.rb +8 -1
  36. data/lib/active_support/core_ext/object/to_query.rb +7 -1
  37. data/lib/active_support/core_ext/object/try.rb +2 -2
  38. data/lib/active_support/core_ext/object.rb +13 -13
  39. data/lib/active_support/core_ext/pathname.rb +2 -2
  40. data/lib/active_support/core_ext/range/overlap.rb +3 -3
  41. data/lib/active_support/core_ext/range/sole.rb +17 -0
  42. data/lib/active_support/core_ext/range.rb +4 -4
  43. data/lib/active_support/core_ext/string/filters.rb +3 -3
  44. data/lib/active_support/core_ext/string/multibyte.rb +12 -3
  45. data/lib/active_support/core_ext/string/output_safety.rb +19 -12
  46. data/lib/active_support/core_ext/string.rb +13 -13
  47. data/lib/active_support/core_ext/symbol.rb +1 -1
  48. data/lib/active_support/core_ext/time/calculations.rb +0 -7
  49. data/lib/active_support/core_ext/time/compatibility.rb +2 -27
  50. data/lib/active_support/core_ext/time.rb +5 -5
  51. data/lib/active_support/core_ext.rb +1 -1
  52. data/lib/active_support/current_attributes/test_helper.rb +2 -2
  53. data/lib/active_support/current_attributes.rb +26 -16
  54. data/lib/active_support/dependencies/interlock.rb +11 -5
  55. data/lib/active_support/dependencies.rb +6 -1
  56. data/lib/active_support/deprecation/reporting.rb +4 -2
  57. data/lib/active_support/deprecation.rb +1 -1
  58. data/lib/active_support/editor.rb +70 -0
  59. data/lib/active_support/error_reporter.rb +50 -6
  60. data/lib/active_support/event_reporter/test_helper.rb +32 -0
  61. data/lib/active_support/event_reporter.rb +592 -0
  62. data/lib/active_support/evented_file_update_checker.rb +5 -1
  63. data/lib/active_support/execution_context.rb +64 -7
  64. data/lib/active_support/file_update_checker.rb +8 -6
  65. data/lib/active_support/gem_version.rb +3 -3
  66. data/lib/active_support/gzip.rb +1 -0
  67. data/lib/active_support/hash_with_indifferent_access.rb +47 -24
  68. data/lib/active_support/i18n_railtie.rb +2 -2
  69. data/lib/active_support/inflector/inflections.rb +31 -15
  70. data/lib/active_support/inflector/transliterate.rb +6 -8
  71. data/lib/active_support/isolated_execution_state.rb +12 -15
  72. data/lib/active_support/json/decoding.rb +6 -4
  73. data/lib/active_support/json/encoding.rb +135 -17
  74. data/lib/active_support/lazy_load_hooks.rb +1 -1
  75. data/lib/active_support/log_subscriber.rb +2 -6
  76. data/lib/active_support/logger_thread_safe_level.rb +6 -3
  77. data/lib/active_support/message_encryptors.rb +52 -0
  78. data/lib/active_support/message_pack/extensions.rb +5 -0
  79. data/lib/active_support/message_verifiers.rb +52 -0
  80. data/lib/active_support/messages/rotation_coordinator.rb +9 -0
  81. data/lib/active_support/messages/rotator.rb +5 -0
  82. data/lib/active_support/multibyte/chars.rb +8 -1
  83. data/lib/active_support/multibyte.rb +4 -0
  84. data/lib/active_support/notifications/fanout.rb +64 -42
  85. data/lib/active_support/notifications/instrumenter.rb +1 -1
  86. data/lib/active_support/railtie.rb +32 -15
  87. data/lib/active_support/structured_event_subscriber.rb +99 -0
  88. data/lib/active_support/subscriber.rb +0 -5
  89. data/lib/active_support/syntax_error_proxy.rb +3 -0
  90. data/lib/active_support/test_case.rb +61 -6
  91. data/lib/active_support/testing/assertions.rb +34 -6
  92. data/lib/active_support/testing/error_reporter_assertions.rb +18 -1
  93. data/lib/active_support/testing/event_reporter_assertions.rb +227 -0
  94. data/lib/active_support/testing/notification_assertions.rb +92 -0
  95. data/lib/active_support/testing/parallelization/server.rb +15 -2
  96. data/lib/active_support/testing/parallelization/worker.rb +4 -2
  97. data/lib/active_support/testing/parallelization.rb +25 -1
  98. data/lib/active_support/testing/tests_without_assertions.rb +1 -1
  99. data/lib/active_support/testing/time_helpers.rb +7 -3
  100. data/lib/active_support/time_with_zone.rb +22 -22
  101. data/lib/active_support/values/time_zone.rb +8 -1
  102. data/lib/active_support/xml_mini.rb +3 -2
  103. data/lib/active_support.rb +20 -15
  104. metadata +25 -17
  105. data/lib/active_support/core_ext/range/each.rb +0 -24
@@ -46,8 +46,11 @@ module ActiveSupport
46
46
  raise ArgumentError, "A block is required to initialize a FileUpdateChecker"
47
47
  end
48
48
 
49
- @files = files.freeze
50
- @glob = compile_glob(dirs)
49
+ gem_paths = Gem.path
50
+ @files = files.reject { |file| File.expand_path(file).start_with?(*gem_paths) }.freeze
51
+
52
+ @globs = compile_glob(dirs)&.reject { |dir| dir.start_with?(*gem_paths) }
53
+
51
54
  @block = block
52
55
 
53
56
  @watched = nil
@@ -103,7 +106,7 @@ module ActiveSupport
103
106
  def watched
104
107
  @watched || begin
105
108
  all = @files.select { |f| File.exist?(f) }
106
- all.concat(Dir[@glob]) if @glob
109
+ all.concat(Dir[*@globs]) if @globs
107
110
  all.tap(&:uniq!)
108
111
  end
109
112
  end
@@ -120,7 +123,7 @@ module ActiveSupport
120
123
  # healthy to consider this edge case because with mtimes in the future
121
124
  # reloading is not triggered.
122
125
  def max_mtime(paths)
123
- time_now = Time.now
126
+ time_now = Time.at(0, Process.clock_gettime(Process::CLOCK_REALTIME, :nanosecond), :nanosecond)
124
127
  max_mtime = nil
125
128
 
126
129
  # Time comparisons are performed with #compare_without_coercion because
@@ -145,10 +148,9 @@ module ActiveSupport
145
148
  hash.freeze # Freeze so changes aren't accidentally pushed
146
149
  return if hash.empty?
147
150
 
148
- globs = hash.map do |key, value|
151
+ hash.map do |key, value|
149
152
  "#{escape(key)}/**/*#{compile_ext(value)}"
150
153
  end
151
- "{#{globs.join(",")}}"
152
154
  end
153
155
 
154
156
  def escape(key)
@@ -8,9 +8,9 @@ module ActiveSupport
8
8
 
9
9
  module VERSION
10
10
  MAJOR = 8
11
- MINOR = 0
12
- TINY = 2
13
- PRE = "1"
11
+ MINOR = 1
12
+ TINY = 1
13
+ PRE = nil
14
14
 
15
15
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
16
16
  end
@@ -32,6 +32,7 @@ module ActiveSupport
32
32
  def self.compress(source, level = Zlib::DEFAULT_COMPRESSION, strategy = Zlib::DEFAULT_STRATEGY)
33
33
  output = Stream.new
34
34
  gz = Zlib::GzipWriter.new(output, level, strategy)
35
+ gz.mtime = 0
35
36
  gz.write(source)
36
37
  gz.close
37
38
  output.string
@@ -68,15 +68,15 @@ module ActiveSupport
68
68
  end
69
69
 
70
70
  def initialize(constructor = nil)
71
- if constructor.respond_to?(:to_hash)
71
+ if constructor.nil?
72
+ super()
73
+ elsif constructor.respond_to?(:to_hash)
72
74
  super()
73
75
  update(constructor)
74
76
 
75
77
  hash = constructor.is_a?(Hash) ? constructor : constructor.to_hash
76
78
  self.default = hash.default if hash.default
77
79
  self.default_proc = hash.default_proc if hash.default_proc
78
- elsif constructor.nil?
79
- super()
80
80
  else
81
81
  super(constructor)
82
82
  end
@@ -95,11 +95,27 @@ module ActiveSupport
95
95
  # hash[:key] = 'value'
96
96
  #
97
97
  # This value can be later fetched using either +:key+ or <tt>'key'</tt>.
98
+ #
99
+ # If the value is a Hash or contains one or multiple Hashes, they will be
100
+ # converted to +HashWithIndifferentAccess+.
98
101
  def []=(key, value)
99
102
  regular_writer(convert_key(key), convert_value(value, conversion: :assignment))
100
103
  end
101
104
 
102
- alias_method :store, :[]=
105
+ # Assigns a new value to the hash:
106
+ #
107
+ # hash = ActiveSupport::HashWithIndifferentAccess.new
108
+ # hash[:key] = 'value'
109
+ #
110
+ # This value can be later fetched using either +:key+ or <tt>'key'</tt>.
111
+ #
112
+ # If the value is a Hash or contains one or multiple Hashes, they will be
113
+ # converted to +HashWithIndifferentAccess+. unless `convert_value: false`
114
+ # is set.
115
+ def store(key, value, convert_value: true)
116
+ value = convert_value(value, conversion: :assignment) if convert_value
117
+ regular_writer(convert_key(key), value)
118
+ end
103
119
 
104
120
  # Updates the receiver in-place, merging in the hashes passed as arguments:
105
121
  #
@@ -262,9 +278,7 @@ module ActiveSupport
262
278
  # hash[:a][:c] # => "c"
263
279
  # dup[:a][:c] # => "c"
264
280
  def dup
265
- self.class.new(self).tap do |new_hash|
266
- set_defaults(new_hash)
267
- end
281
+ copy_defaults(self.class.new(self))
268
282
  end
269
283
 
270
284
  # This method has the same semantics of +update+, except it does not
@@ -281,13 +295,13 @@ module ActiveSupport
281
295
  # hash['a'] = nil
282
296
  # hash.reverse_merge(a: 0, b: 1) # => {"a"=>nil, "b"=>1}
283
297
  def reverse_merge(other_hash)
284
- super(self.class.new(other_hash))
298
+ super(cast(other_hash))
285
299
  end
286
300
  alias_method :with_defaults, :reverse_merge
287
301
 
288
302
  # Same semantics as +reverse_merge+ but modifies the receiver in-place.
289
303
  def reverse_merge!(other_hash)
290
- super(self.class.new(other_hash))
304
+ super(cast(other_hash))
291
305
  end
292
306
  alias_method :with_defaults!, :reverse_merge!
293
307
 
@@ -296,7 +310,7 @@ module ActiveSupport
296
310
  # h = { "a" => 100, "b" => 200 }
297
311
  # h.replace({ "c" => 300, "d" => 400 }) # => {"c"=>300, "d"=>400}
298
312
  def replace(other_hash)
299
- super(self.class.new(other_hash))
313
+ super(cast(other_hash))
300
314
  end
301
315
 
302
316
  # Removes the specified key from the hash.
@@ -338,21 +352,26 @@ module ActiveSupport
338
352
  NOT_GIVEN = Object.new # :nodoc:
339
353
 
340
354
  def transform_keys(hash = NOT_GIVEN, &block)
341
- return to_enum(:transform_keys) if NOT_GIVEN.equal?(hash) && !block_given?
342
- dup.tap { |h| h.transform_keys!(hash, &block) }
355
+ if NOT_GIVEN.equal?(hash)
356
+ if block_given?
357
+ self.class.new(super(&block))
358
+ else
359
+ to_enum(:transform_keys)
360
+ end
361
+ else
362
+ self.class.new(super)
363
+ end
343
364
  end
344
365
 
345
366
  def transform_keys!(hash = NOT_GIVEN, &block)
346
- return to_enum(:transform_keys!) if NOT_GIVEN.equal?(hash) && !block_given?
347
-
348
- if hash.nil?
349
- super
350
- elsif NOT_GIVEN.equal?(hash)
351
- keys.each { |key| self[yield(key)] = delete(key) }
352
- elsif block_given?
353
- keys.each { |key| self[hash[key] || yield(key)] = delete(key) }
367
+ if NOT_GIVEN.equal?(hash)
368
+ if block_given?
369
+ replace(copy_defaults(transform_keys(&block)))
370
+ else
371
+ return to_enum(:transform_keys!)
372
+ end
354
373
  else
355
- keys.each { |key| self[hash[key] || key] = delete(key) }
374
+ replace(copy_defaults(transform_keys(hash, &block)))
356
375
  end
357
376
 
358
377
  self
@@ -376,8 +395,7 @@ module ActiveSupport
376
395
  def to_hash
377
396
  copy = Hash[self]
378
397
  copy.transform_values! { |v| convert_value_to_hash(v) }
379
- set_defaults(copy)
380
- copy
398
+ copy_defaults(copy)
381
399
  end
382
400
 
383
401
  def to_proc
@@ -385,6 +403,10 @@ module ActiveSupport
385
403
  end
386
404
 
387
405
  private
406
+ def cast(other)
407
+ self.class === other ? other : self.class.new(other)
408
+ end
409
+
388
410
  def convert_key(key)
389
411
  Symbol === key ? key.name : key
390
412
  end
@@ -413,12 +435,13 @@ module ActiveSupport
413
435
  end
414
436
 
415
437
 
416
- def set_defaults(target)
438
+ def copy_defaults(target)
417
439
  if default_proc
418
440
  target.default_proc = default_proc.dup
419
441
  else
420
442
  target.default = default
421
443
  end
444
+ target
422
445
  end
423
446
 
424
447
  def update_with_single_argument(other_hash, block)
@@ -2,6 +2,7 @@
2
2
 
3
3
  require "active_support"
4
4
  require "active_support/core_ext/array/wrap"
5
+ require "rails/railtie"
5
6
 
6
7
  # :enddoc:
7
8
 
@@ -66,8 +67,7 @@ module I18n
66
67
 
67
68
  if app.config.reloading_enabled?
68
69
  directories = watched_dirs_with_extensions(reloadable_paths)
69
- root_load_paths = I18n.load_path.select { |path| path.to_s.start_with?(Rails.root.to_s) }
70
- reloader = app.config.file_watcher.new(root_load_paths, directories) do
70
+ reloader = app.config.file_watcher.new(I18n.load_path, directories) do
71
71
  I18n.load_path.delete_if { |path| path.to_s.start_with?(Rails.root.to_s) && !File.exist?(path) }
72
72
  I18n.load_path |= reloadable_paths.flat_map(&:existent)
73
73
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "concurrent/map"
4
+ require "active_support/core_ext/module/delegation"
4
5
  require "active_support/i18n"
5
6
 
6
7
  module ActiveSupport
@@ -29,44 +30,59 @@ module ActiveSupport
29
30
  # before any of the rules that may already have been loaded.
30
31
  class Inflections
31
32
  @__instance__ = Concurrent::Map.new
33
+ @__en_instance__ = nil
34
+
35
+ class Uncountables # :nodoc:
36
+ include Enumerable
37
+
38
+ delegate :each, :pop, :empty?, :to_s, :==, :to_a, :to_ary, to: :@members
32
39
 
33
- class Uncountables < Array
34
40
  def initialize
35
- @regex_array = []
36
- super
41
+ @members = []
42
+ @pattern = nil
37
43
  end
38
44
 
39
45
  def delete(entry)
40
- super entry
41
- @regex_array.delete(to_regex(entry))
46
+ @members.delete(entry)
47
+ @pattern = nil
48
+ end
49
+
50
+ def <<(word)
51
+ word = word.downcase
52
+ @members << word
53
+ @pattern = nil
54
+ self
42
55
  end
43
56
 
44
- def <<(*word)
45
- add(word)
57
+ def flatten
58
+ @members.dup
46
59
  end
47
60
 
48
61
  def add(words)
49
62
  words = words.flatten.map(&:downcase)
50
- concat(words)
51
- @regex_array += words.map { |word| to_regex(word) }
63
+ @members.concat(words)
64
+ @pattern = nil
52
65
  self
53
66
  end
54
67
 
55
68
  def uncountable?(str)
56
- @regex_array.any? { |regex| regex.match? str }
57
- end
58
-
59
- private
60
- def to_regex(string)
61
- /\b#{::Regexp.escape(string)}\Z/i
69
+ if @pattern.nil?
70
+ members_pattern = Regexp.union(@members.map { |w| /#{Regexp.escape(w)}/i })
71
+ @pattern = /\b#{members_pattern}\Z/i
62
72
  end
73
+ @pattern.match?(str)
74
+ end
63
75
  end
64
76
 
65
77
  def self.instance(locale = :en)
78
+ return @__en_instance__ ||= new if locale == :en
79
+
66
80
  @__instance__[locale] ||= new
67
81
  end
68
82
 
69
83
  def self.instance_or_fallback(locale)
84
+ return @__en_instance__ ||= new if locale == :en
85
+
70
86
  I18n.fallbacks[locale].each do |k|
71
87
  return @__instance__[k] if @__instance__.key?(k)
72
88
  end
@@ -128,18 +128,16 @@ module ActiveSupport
128
128
  parameterized_string.gsub!(/[^a-z0-9\-_]+/i, separator)
129
129
 
130
130
  unless separator.nil? || separator.empty?
131
- if separator == "-"
132
- re_duplicate_separator = /-{2,}/
133
- re_leading_trailing_separator = /^-|-$/i
131
+ # No more than one of the separator in a row.
132
+ if separator.length == 1
133
+ parameterized_string.squeeze!(separator)
134
134
  else
135
135
  re_sep = Regexp.escape(separator)
136
- re_duplicate_separator = /#{re_sep}{2,}/
137
- re_leading_trailing_separator = /^#{re_sep}|#{re_sep}$/i
136
+ parameterized_string.gsub!(/#{re_sep}{2,}/, separator)
138
137
  end
139
- # No more than one of the separator in a row.
140
- parameterized_string.gsub!(re_duplicate_separator, separator)
141
138
  # Remove leading/trailing separator.
142
- parameterized_string.gsub!(re_leading_trailing_separator, "")
139
+ parameterized_string.delete_prefix!(separator)
140
+ parameterized_string.delete_suffix!(separator)
143
141
  end
144
142
 
145
143
  parameterized_string.downcase! unless preserve_case
@@ -28,45 +28,42 @@ module ActiveSupport
28
28
  @isolation_level = level
29
29
  end
30
30
 
31
- def unique_id
32
- self[:__id__] ||= Object.new
33
- end
34
-
35
31
  def [](key)
36
- state[key]
32
+ if state = @scope.current.active_support_execution_state
33
+ state[key]
34
+ end
37
35
  end
38
36
 
39
37
  def []=(key, value)
38
+ state = (@scope.current.active_support_execution_state ||= {})
40
39
  state[key] = value
41
40
  end
42
41
 
43
42
  def key?(key)
44
- state.key?(key)
43
+ @scope.current.active_support_execution_state&.key?(key)
45
44
  end
46
45
 
47
46
  def delete(key)
48
- state.delete(key)
47
+ @scope.current.active_support_execution_state&.delete(key)
49
48
  end
50
49
 
51
50
  def clear
52
- state.clear
51
+ @scope.current.active_support_execution_state&.clear
53
52
  end
54
53
 
55
54
  def context
56
55
  scope.current
57
56
  end
58
57
 
59
- def share_with(other)
58
+ def share_with(other, &block)
60
59
  # Action Controller streaming spawns a new thread and copy thread locals.
61
60
  # We do the same here for backward compatibility, but this is very much a hack
62
61
  # and streaming should be rethought.
63
- context.active_support_execution_state = other.active_support_execution_state.dup
62
+ old_state, context.active_support_execution_state = context.active_support_execution_state, other.active_support_execution_state.dup
63
+ block.call
64
+ ensure
65
+ context.active_support_execution_state = old_state
64
66
  end
65
-
66
- private
67
- def state
68
- context.active_support_execution_state ||= {}
69
- end
70
67
  end
71
68
 
72
69
  self.isolation_level = :thread
@@ -14,13 +14,15 @@ module ActiveSupport
14
14
  DATETIME_REGEX = /\A(?:\d{4}-\d{2}-\d{2}|\d{4}-\d{1,2}-\d{1,2}[T \t]+\d{1,2}:\d{2}:\d{2}(\.[0-9]*)?(([ \t]*)Z|[-+]\d{2}?(:\d{2})?)?)\z/
15
15
 
16
16
  class << self
17
- # Parses a JSON string (JavaScript Object Notation) into a hash.
17
+ # Parses a JSON string (JavaScript Object Notation) into a Ruby object.
18
18
  # See http://www.json.org for more info.
19
19
  #
20
20
  # ActiveSupport::JSON.decode("{\"team\":\"rails\",\"players\":\"36\"}")
21
- # => {"team" => "rails", "players" => "36"}
22
- def decode(json)
23
- data = ::JSON.parse(json, quirks_mode: true)
21
+ # # => {"team" => "rails", "players" => "36"}
22
+ # ActiveSupport::JSON.decode("2.39")
23
+ # # => 2.39
24
+ def decode(json, options = {})
25
+ data = ::JSON.parse(json, options)
24
26
 
25
27
  if ActiveSupport.parse_json_times
26
28
  convert_dates_from(data)
@@ -8,6 +8,7 @@ module ActiveSupport
8
8
  delegate :use_standard_json_time_format, :use_standard_json_time_format=,
9
9
  :time_precision, :time_precision=,
10
10
  :escape_html_entities_in_json, :escape_html_entities_in_json=,
11
+ :escape_js_separators_in_json, :escape_js_separators_in_json=,
11
12
  :json_encoder, :json_encoder=,
12
13
  to: :'ActiveSupport::JSON::Encoding'
13
14
  end
@@ -20,8 +21,8 @@ module ActiveSupport
20
21
  # ActiveSupport::JSON.encode({ team: 'rails', players: '36' })
21
22
  # # => "{\"team\":\"rails\",\"players\":\"36\"}"
22
23
  #
23
- # Generates JSON that is safe to include in JavaScript as it escapes
24
- # U+2028 (Line Separator) and U+2029 (Paragraph Separator):
24
+ # By default, it generates JSON that is safe to include in JavaScript, as
25
+ # it escapes U+2028 (Line Separator) and U+2029 (Paragraph Separator):
25
26
  #
26
27
  # ActiveSupport::JSON.encode({ key: "\u2028" })
27
28
  # # => "{\"key\":\"\\u2028\"}"
@@ -32,18 +33,45 @@ module ActiveSupport
32
33
  # ActiveSupport::JSON.encode({ key: "<>&" })
33
34
  # # => "{\"key\":\"\\u003c\\u003e\\u0026\"}"
34
35
  #
35
- # This can be changed with the +escape_html_entities+ option, or the
36
+ # This behavior can be changed with the +escape_html_entities+ option, or the
36
37
  # global escape_html_entities_in_json configuration option.
37
38
  #
38
39
  # ActiveSupport::JSON.encode({ key: "<>&" }, escape_html_entities: false)
39
40
  # # => "{\"key\":\"<>&\"}"
41
+ #
42
+ # For performance reasons, you can set the +escape+ option to false,
43
+ # which will skip all escaping:
44
+ #
45
+ # ActiveSupport::JSON.encode({ key: "\u2028<>&" }, escape: false)
46
+ # # => "{\"key\":\"\u2028<>&\"}"
40
47
  def encode(value, options = nil)
41
- Encoding.json_encoder.new(options).encode(value)
48
+ if options.nil? || options.empty?
49
+ Encoding.encode_without_options(value)
50
+ elsif options == { escape: false }.freeze
51
+ Encoding.encode_without_escape(value)
52
+ else
53
+ Encoding.json_encoder.new(options).encode(value)
54
+ end
42
55
  end
43
56
  alias_method :dump, :encode
44
57
  end
45
58
 
46
59
  module Encoding # :nodoc:
60
+ U2028 = -"\u2028".b
61
+ U2029 = -"\u2029".b
62
+
63
+ ESCAPED_CHARS = {
64
+ U2028 => '\u2028'.b,
65
+ U2029 => '\u2029'.b,
66
+ ">".b => '\u003e'.b,
67
+ "<".b => '\u003c'.b,
68
+ "&".b => '\u0026'.b,
69
+ }
70
+
71
+ HTML_ENTITIES_REGEX = Regexp.union(*(ESCAPED_CHARS.keys - [U2028, U2029]))
72
+ FULL_ESCAPE_REGEX = Regexp.union(*ESCAPED_CHARS.keys)
73
+ JS_SEPARATORS_REGEX = Regexp.union(U2028, U2029)
74
+
47
75
  class JSONGemEncoder # :nodoc:
48
76
  attr_reader :options
49
77
 
@@ -58,17 +86,19 @@ module ActiveSupport
58
86
  end
59
87
  json = stringify(jsonify(value))
60
88
 
61
- # Rails does more escaping than the JSON gem natively does (we
62
- # escape \u2028 and \u2029 and optionally >, <, & to work around
63
- # certain browser problems).
89
+ return json unless @options.fetch(:escape, true)
90
+
91
+ json.force_encoding(::Encoding::BINARY)
64
92
  if @options.fetch(:escape_html_entities, Encoding.escape_html_entities_in_json)
65
- json.gsub!(">", '\u003e')
66
- json.gsub!("<", '\u003c')
67
- json.gsub!("&", '\u0026')
93
+ if Encoding.escape_js_separators_in_json
94
+ json.gsub!(FULL_ESCAPE_REGEX, ESCAPED_CHARS)
95
+ else
96
+ json.gsub!(HTML_ENTITIES_REGEX, ESCAPED_CHARS)
97
+ end
98
+ elsif Encoding.escape_js_separators_in_json
99
+ json.gsub!(JS_SEPARATORS_REGEX, ESCAPED_CHARS)
68
100
  end
69
- json.gsub!("\u2028", '\u2028')
70
- json.gsub!("\u2029", '\u2029')
71
- json
101
+ json.force_encoding(::Encoding::UTF_8)
72
102
  end
73
103
 
74
104
  private
@@ -101,14 +131,75 @@ module ActiveSupport
101
131
  when Array
102
132
  value.map { |v| jsonify(v) }
103
133
  else
104
- jsonify value.as_json
134
+ if defined?(::JSON::Fragment) && ::JSON::Fragment === value
135
+ value
136
+ else
137
+ jsonify value.as_json
138
+ end
105
139
  end
106
140
  end
107
141
 
108
142
  # Encode a "jsonified" Ruby data structure using the JSON gem
109
143
  def stringify(jsonified)
110
- ::JSON.generate(jsonified, quirks_mode: true, max_nesting: false)
144
+ ::JSON.generate(jsonified)
145
+ end
146
+ end
147
+
148
+ # ruby/json 2.14.x yields non-String keys but doesn't let us know it's a key
149
+ if defined?(::JSON::Coder) && Gem::Version.new(::JSON::VERSION) >= Gem::Version.new("2.15.2")
150
+ class JSONGemCoderEncoder # :nodoc:
151
+ JSON_NATIVE_TYPES = [Hash, Array, Float, String, Symbol, Integer, NilClass, TrueClass, FalseClass, ::JSON::Fragment].freeze
152
+ CODER = ::JSON::Coder.new do |value, is_key|
153
+ json_value = value.as_json
154
+ # Keep compatibility by calling to_s on non-String keys
155
+ next json_value.to_s if is_key
156
+ # Handle objects returning self from as_json
157
+ if json_value.equal?(value)
158
+ next ::JSON::Fragment.new(::JSON.generate(json_value))
159
+ end
160
+ # Handle objects not returning JSON-native types from as_json
161
+ count = 5
162
+ until JSON_NATIVE_TYPES.include?(json_value.class)
163
+ raise SystemStackError if count == 0
164
+ json_value = json_value.as_json
165
+ count -= 1
166
+ end
167
+ json_value
168
+ end
169
+
170
+
171
+ def initialize(options = nil)
172
+ if options
173
+ options = options.dup
174
+ @escape = options.delete(:escape) { true }
175
+ @options = options.freeze
176
+ else
177
+ @escape = true
178
+ @options = {}.freeze
179
+ end
180
+ end
181
+
182
+ # Encode the given object into a JSON string
183
+ def encode(value)
184
+ value = value.as_json(@options) unless @options.empty?
185
+
186
+ json = CODER.dump(value)
187
+
188
+ return json unless @escape
189
+
190
+ json.force_encoding(::Encoding::BINARY)
191
+ if @options.fetch(:escape_html_entities, Encoding.escape_html_entities_in_json)
192
+ if Encoding.escape_js_separators_in_json
193
+ json.gsub!(FULL_ESCAPE_REGEX, ESCAPED_CHARS)
194
+ else
195
+ json.gsub!(HTML_ENTITIES_REGEX, ESCAPED_CHARS)
196
+ end
197
+ elsif Encoding.escape_js_separators_in_json
198
+ json.gsub!(JS_SEPARATORS_REGEX, ESCAPED_CHARS)
199
+ end
200
+ json.force_encoding(::Encoding::UTF_8)
111
201
  end
202
+ end
112
203
  end
113
204
 
114
205
  class << self
@@ -120,18 +211,45 @@ module ActiveSupport
120
211
  # as a safety measure.
121
212
  attr_accessor :escape_html_entities_in_json
122
213
 
214
+ # If true, encode LINE SEPARATOR (U+2028) and PARAGRAPH SEPARATOR (U+2029)
215
+ # as escaped unicode sequences ('\u2028' and '\u2029').
216
+ # Historically these characters were not valid inside JavaScript strings
217
+ # but that changed in ECMAScript 2019. As such it's no longer a concern in
218
+ # modern browsers: https://caniuse.com/mdn-javascript_builtins_json_json_superset.
219
+ attr_accessor :escape_js_separators_in_json
220
+
123
221
  # Sets the precision of encoded time values.
124
222
  # Defaults to 3 (equivalent to millisecond precision)
125
223
  attr_accessor :time_precision
126
224
 
127
225
  # Sets the encoder used by \Rails to encode Ruby objects into JSON strings
128
226
  # in +Object#to_json+ and +ActiveSupport::JSON.encode+.
129
- attr_accessor :json_encoder
227
+ attr_reader :json_encoder
228
+
229
+ def json_encoder=(encoder)
230
+ @json_encoder = encoder
231
+ @encoder_without_options = encoder.new
232
+ @encoder_without_escape = encoder.new(escape: false)
233
+ end
234
+
235
+ def encode_without_options(value) # :nodoc:
236
+ @encoder_without_options.encode(value)
237
+ end
238
+
239
+ def encode_without_escape(value) # :nodoc:
240
+ @encoder_without_escape.encode(value)
241
+ end
130
242
  end
131
243
 
132
244
  self.use_standard_json_time_format = true
133
245
  self.escape_html_entities_in_json = true
134
- self.json_encoder = JSONGemEncoder
246
+ self.escape_js_separators_in_json = true
247
+ self.json_encoder =
248
+ if defined?(JSONGemCoderEncoder)
249
+ JSONGemCoderEncoder
250
+ else
251
+ JSONGemEncoder
252
+ end
135
253
  self.time_precision = 3
136
254
  end
137
255
  end
@@ -53,7 +53,7 @@ module ActiveSupport
53
53
  # loaded. If the component has already loaded, the block is executed
54
54
  # immediately.
55
55
  #
56
- # Options:
56
+ # ==== Options
57
57
  #
58
58
  # * <tt>:yield</tt> - Yields the object that run_load_hooks to +block+.
59
59
  # * <tt>:run_once</tt> - Given +block+ will run only once.
@@ -149,12 +149,6 @@ module ActiveSupport
149
149
  log_exception(event.name, e)
150
150
  end
151
151
 
152
- def publish_event(event)
153
- super if logger
154
- rescue => e
155
- log_exception(event.name, e)
156
- end
157
-
158
152
  attr_writer :event_levels # :nodoc:
159
153
 
160
154
  private
@@ -184,6 +178,8 @@ module ActiveSupport
184
178
  end
185
179
 
186
180
  def log_exception(name, e)
181
+ ActiveSupport.error_reporter.report(e, source: name)
182
+
187
183
  if logger
188
184
  logger.error "Could not log #{name.inspect} event. #{e.class}: #{e.message} #{e.backtrace}"
189
185
  end