activesupport 7.2.3 → 8.0.5

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 (68) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +191 -213
  3. data/lib/active_support/backtrace_cleaner.rb +1 -1
  4. data/lib/active_support/benchmark.rb +21 -0
  5. data/lib/active_support/benchmarkable.rb +3 -2
  6. data/lib/active_support/cache/file_store.rb +12 -2
  7. data/lib/active_support/cache/mem_cache_store.rb +4 -1
  8. data/lib/active_support/cache/memory_store.rb +6 -2
  9. data/lib/active_support/cache/redis_cache_store.rb +6 -3
  10. data/lib/active_support/cache.rb +16 -11
  11. data/lib/active_support/callbacks.rb +5 -3
  12. data/lib/active_support/class_attribute.rb +33 -0
  13. data/lib/active_support/code_generator.rb +9 -0
  14. data/lib/active_support/concurrency/share_lock.rb +0 -1
  15. data/lib/active_support/configuration_file.rb +15 -6
  16. data/lib/active_support/core_ext/array/conversions.rb +3 -3
  17. data/lib/active_support/core_ext/benchmark.rb +6 -9
  18. data/lib/active_support/core_ext/class/attribute.rb +24 -18
  19. data/lib/active_support/core_ext/date/conversions.rb +2 -0
  20. data/lib/active_support/core_ext/date_and_time/compatibility.rb +2 -2
  21. data/lib/active_support/core_ext/enumerable.rb +8 -3
  22. data/lib/active_support/core_ext/hash/deep_merge.rb +1 -0
  23. data/lib/active_support/core_ext/hash/except.rb +0 -12
  24. data/lib/active_support/core_ext/module/attr_internal.rb +3 -4
  25. data/lib/active_support/core_ext/object/json.rb +16 -10
  26. data/lib/active_support/core_ext/object/to_query.rb +2 -1
  27. data/lib/active_support/core_ext/range/overlap.rb +3 -3
  28. data/lib/active_support/core_ext/string/inflections.rb +1 -1
  29. data/lib/active_support/core_ext/string/output_safety.rb +3 -1
  30. data/lib/active_support/core_ext/thread/backtrace/location.rb +2 -7
  31. data/lib/active_support/core_ext/time/calculations.rb +14 -2
  32. data/lib/active_support/core_ext/time/conversions.rb +2 -0
  33. data/lib/active_support/delegation.rb +25 -44
  34. data/lib/active_support/dependencies.rb +0 -1
  35. data/lib/active_support/deprecation/reporting.rb +0 -19
  36. data/lib/active_support/deprecation.rb +1 -1
  37. data/lib/active_support/duration.rb +14 -10
  38. data/lib/active_support/encrypted_configuration.rb +20 -2
  39. data/lib/active_support/error_reporter.rb +31 -1
  40. data/lib/active_support/evented_file_update_checker.rb +0 -1
  41. data/lib/active_support/gem_version.rb +3 -3
  42. data/lib/active_support/hash_with_indifferent_access.rb +15 -16
  43. data/lib/active_support/i18n_railtie.rb +19 -11
  44. data/lib/active_support/inflector/inflections.rb +2 -1
  45. data/lib/active_support/inflector/methods.rb +3 -3
  46. data/lib/active_support/isolated_execution_state.rb +4 -4
  47. data/lib/active_support/json/decoding.rb +3 -1
  48. data/lib/active_support/json/encoding.rb +2 -2
  49. data/lib/active_support/logger_thread_safe_level.rb +6 -3
  50. data/lib/active_support/message_pack/extensions.rb +1 -1
  51. data/lib/active_support/notifications/fanout.rb +0 -1
  52. data/lib/active_support/notifications/instrumenter.rb +1 -1
  53. data/lib/active_support/number_helper/number_converter.rb +1 -1
  54. data/lib/active_support/number_helper/number_to_delimited_converter.rb +17 -2
  55. data/lib/active_support/number_helper.rb +22 -0
  56. data/lib/active_support/railtie.rb +6 -0
  57. data/lib/active_support/tagged_logging.rb +5 -0
  58. data/lib/active_support/test_case.rb +6 -0
  59. data/lib/active_support/testing/assertions.rb +84 -21
  60. data/lib/active_support/testing/autorun.rb +5 -0
  61. data/lib/active_support/testing/isolation.rb +0 -2
  62. data/lib/active_support/testing/parallelization/worker.rb +5 -1
  63. data/lib/active_support/testing/time_helpers.rb +2 -1
  64. data/lib/active_support/time_with_zone.rb +22 -13
  65. data/lib/active_support/values/time_zone.rb +11 -9
  66. data/lib/active_support.rb +10 -3
  67. metadata +22 -7
  68. data/lib/active_support/proxy_object.rb +0 -20
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "set"
4
-
5
3
  module ActiveSupport
6
4
  # Error generated by +delegate+ when a method is called on +nil+ and +allow_nil+
7
5
  # option is not used.
@@ -136,11 +134,11 @@ module ActiveSupport
136
134
  "def #{method_name}(#{definition})" <<
137
135
  " _ = #{receiver}" <<
138
136
  " _.#{method}(#{definition})" <<
139
- "rescue NoMethodError => e" <<
137
+ "rescue ::NoMethodError => e" <<
140
138
  " if _.nil? && e.name == :#{method}" <<
141
- " raise ::ActiveSupport::DelegationError.nil_target(:#{method_name}, :'#{receiver}')" <<
139
+ " ::Kernel.raise ::ActiveSupport::DelegationError.nil_target(:#{method_name}, :'#{receiver}')" <<
142
140
  " else" <<
143
- " raise" <<
141
+ " ::Kernel.raise" <<
144
142
  " end" <<
145
143
  "end"
146
144
  end
@@ -153,49 +151,32 @@ module ActiveSupport
153
151
  target = target.to_s
154
152
  target = "self.#{target}" if RESERVED_METHOD_NAMES.include?(target) || target == "__target"
155
153
 
156
- if allow_nil
157
- owner.module_eval <<~RUBY, __FILE__, __LINE__ + 1
158
- def respond_to_missing?(name, include_private = false)
159
- # It may look like an oversight, but we deliberately do not pass
160
- # +include_private+, because they do not get delegated.
161
-
162
- return false if name == :marshal_dump || name == :_dump
163
- #{target}.respond_to?(name) || super
164
- end
165
-
166
- def method_missing(method, ...)
167
- __target = #{target}
168
- if __target.nil? && !nil.respond_to?(method)
169
- nil
170
- elsif __target.respond_to?(method)
171
- __target.public_send(method, ...)
172
- else
173
- super
174
- end
175
- end
176
- RUBY
154
+ nil_behavior = if allow_nil
155
+ "nil"
177
156
  else
178
- owner.module_eval <<~RUBY, __FILE__, __LINE__ + 1
179
- def respond_to_missing?(name, include_private = false)
180
- # It may look like an oversight, but we deliberately do not pass
181
- # +include_private+, because they do not get delegated.
157
+ "::Kernel.raise ::ActiveSupport::DelegationError.nil_target(method, :'#{target}')"
158
+ end
182
159
 
183
- return false if name == :marshal_dump || name == :_dump
184
- #{target}.respond_to?(name) || super
185
- end
160
+ owner.module_eval <<~RUBY, __FILE__, __LINE__ + 1
161
+ def respond_to_missing?(name, include_private = false)
162
+ # It may look like an oversight, but we deliberately do not pass
163
+ # +include_private+, because they do not get delegated.
186
164
 
187
- def method_missing(method, ...)
188
- __target = #{target}
189
- if __target.nil? && !nil.respond_to?(method)
190
- raise ::ActiveSupport::DelegationError.nil_target(method, :'#{target}')
191
- elsif __target.respond_to?(method)
192
- __target.public_send(method, ...)
193
- else
194
- super
195
- end
165
+ return false if name == :marshal_dump || name == :_dump
166
+ #{target}.respond_to?(name) || super
167
+ end
168
+
169
+ def method_missing(method, ...)
170
+ __target = #{target}
171
+ if __target.nil? && !nil.respond_to?(method)
172
+ #{nil_behavior}
173
+ elsif __target.respond_to?(method)
174
+ __target.public_send(method, ...)
175
+ else
176
+ super
196
177
  end
197
- RUBY
198
- end
178
+ end
179
+ RUBY
199
180
  end
200
181
  end
201
182
  end
@@ -1,6 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "set"
4
3
  require "active_support/dependencies/interlock"
5
4
 
6
5
  module ActiveSupport # :nodoc:
@@ -139,7 +139,6 @@ module ActiveSupport
139
139
 
140
140
  def extract_callstack(callstack)
141
141
  return [] if callstack.empty?
142
- return _extract_callstack(callstack) if callstack.first.is_a? String
143
142
 
144
143
  offending_line = callstack.find { |frame|
145
144
  # Code generated with `eval` doesn't have an `absolute_path`, e.g. templates.
@@ -150,24 +149,6 @@ module ActiveSupport
150
149
  [offending_line.path, offending_line.lineno, offending_line.label]
151
150
  end
152
151
 
153
- def _extract_callstack(callstack)
154
- ActiveSupport.deprecator.warn(<<~MESSAGE)
155
- Passing the result of `caller` to ActiveSupport::Deprecation#warn is deprecated and will be removed in Rails 8.0.
156
-
157
- Please pass the result of `caller_locations` instead.
158
- MESSAGE
159
-
160
- offending_line = callstack.find { |line| !ignored_callstack?(line) } || callstack.first
161
-
162
- if offending_line
163
- if md = offending_line.match(/^(.+?):(\d+)(?::in `(.*?)')?/)
164
- md.captures
165
- else
166
- offending_line
167
- end
168
- end
169
- end
170
-
171
152
  RAILS_GEM_ROOT = File.expand_path("../../../..", __dir__) + "/" # :nodoc:
172
153
  LIB_DIR = RbConfig::CONFIG["libdir"] # :nodoc:
173
154
 
@@ -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.0", gem_name = "Rails")
71
+ def initialize(deprecation_horizon = "8.1", 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.
@@ -491,17 +491,21 @@ module ActiveSupport
491
491
  if @parts.empty?
492
492
  time.since(sign * value)
493
493
  else
494
- @parts.inject(time) do |t, (type, number)|
495
- if type == :seconds
496
- t.since(sign * number)
497
- elsif type == :minutes
498
- t.since(sign * number * 60)
499
- elsif type == :hours
500
- t.since(sign * number * 3600)
501
- else
502
- t.advance(type => sign * number)
503
- end
494
+ @parts.each do |type, number|
495
+ t = time
496
+ time =
497
+ if type == :seconds
498
+ t.since(sign * number)
499
+ elsif type == :minutes
500
+ t.since(sign * number * 60)
501
+ elsif type == :hours
502
+ t.since(sign * number * 3600)
503
+ else
504
+ t.advance(type => sign * number)
505
+ end
504
506
  end
507
+
508
+ time
505
509
  end
506
510
  end
507
511
 
@@ -43,6 +43,12 @@ module ActiveSupport
43
43
  end
44
44
  end
45
45
 
46
+ class InvalidKeyError < RuntimeError
47
+ def initialize(content_path, key)
48
+ super "Key '#{key}' is invalid, it must respond to '#to_sym' from configuration in '#{content_path}'."
49
+ end
50
+ end
51
+
46
52
  delegate_missing_to :options
47
53
 
48
54
  def initialize(config_path:, key_path:, env_key:, raise_if_missing_key:)
@@ -61,7 +67,11 @@ module ActiveSupport
61
67
  end
62
68
 
63
69
  def validate! # :nodoc:
64
- deserialize(read)
70
+ deserialize(read).each_key do |key|
71
+ key.to_sym
72
+ rescue NoMethodError
73
+ raise InvalidKeyError.new(content_path, key)
74
+ end
65
75
  end
66
76
 
67
77
  # Returns the decrypted content as a Hash with symbolized keys.
@@ -73,7 +83,7 @@ module ActiveSupport
73
83
  # # => { some_secret: 123, some_namespace: { another_secret: 789 } }
74
84
  #
75
85
  def config
76
- @config ||= deserialize(read).deep_symbolize_keys
86
+ @config ||= deep_symbolize_keys(deserialize(read))
77
87
  end
78
88
 
79
89
  def inspect # :nodoc:
@@ -81,6 +91,14 @@ module ActiveSupport
81
91
  end
82
92
 
83
93
  private
94
+ def deep_symbolize_keys(hash)
95
+ hash.deep_transform_keys do |key|
96
+ key.to_sym
97
+ rescue NoMethodError
98
+ raise InvalidKeyError.new(content_path, key)
99
+ end
100
+ end
101
+
84
102
  def deep_transform(hash)
85
103
  return hash unless hash.is_a?(Hash)
86
104
 
@@ -144,9 +144,9 @@ module ActiveSupport
144
144
  #
145
145
  def unexpected(error, severity: :warning, context: {}, source: DEFAULT_SOURCE)
146
146
  error = RuntimeError.new(error) if error.is_a?(String)
147
- error.set_backtrace(caller(1)) if error.backtrace.nil?
148
147
 
149
148
  if @debug_mode
149
+ ensure_backtrace(error)
150
150
  raise UnexpectedError, "#{error.class.name}: #{error.message}", error.backtrace, cause: error
151
151
  else
152
152
  report(error, handled: true, severity: severity, context: context, source: source)
@@ -207,8 +207,15 @@ module ActiveSupport
207
207
  #
208
208
  # Rails.error.report(error)
209
209
  #
210
+ # The +error+ argument must be an instance of Exception.
211
+ #
212
+ # Rails.error.report(Exception.new("Something went wrong"))
213
+ #
214
+ # Otherwise you can use #unexpected to report an error which does accept a
215
+ # string argument.
210
216
  def report(error, handled: true, severity: handled ? :warning : :error, context: {}, source: DEFAULT_SOURCE)
211
217
  return if error.instance_variable_defined?(:@__rails_error_reported)
218
+ ensure_backtrace(error)
212
219
 
213
220
  unless SEVERITIES.include?(severity)
214
221
  raise ArgumentError, "severity must be one of #{SEVERITIES.map(&:inspect).join(", ")}, got: #{severity.inspect}"
@@ -240,5 +247,28 @@ module ActiveSupport
240
247
 
241
248
  nil
242
249
  end
250
+
251
+ private
252
+ def ensure_backtrace(error)
253
+ return if error.frozen? # re-raising won't add a backtrace
254
+ return unless error.backtrace.nil?
255
+
256
+ 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.
261
+ raise error
262
+ rescue error.class => error
263
+ end
264
+
265
+ count = 0
266
+ while error.backtrace_locations.first&.path == __FILE__
267
+ count += 1
268
+ error.backtrace_locations.shift
269
+ end
270
+
271
+ error.backtrace.shift(count)
272
+ end
243
273
  end
244
274
  end
@@ -3,7 +3,6 @@
3
3
  gem "listen", "~> 3.5"
4
4
  require "listen"
5
5
 
6
- require "set"
7
6
  require "pathname"
8
7
  require "concurrent/atomic/atomic_boolean"
9
8
 
@@ -7,9 +7,9 @@ module ActiveSupport
7
7
  end
8
8
 
9
9
  module VERSION
10
- MAJOR = 7
11
- MINOR = 2
12
- TINY = 3
10
+ MAJOR = 8
11
+ MINOR = 0
12
+ TINY = 5
13
13
  PRE = nil
14
14
 
15
15
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
@@ -311,10 +311,6 @@ module ActiveSupport
311
311
  end
312
312
  alias_method :without, :except
313
313
 
314
- def stringify_keys!; self end
315
- def deep_stringify_keys!; self end
316
- def stringify_keys; dup end
317
- def deep_stringify_keys; dup end
318
314
  undef :symbolize_keys!
319
315
  undef :deep_symbolize_keys!
320
316
  def symbolize_keys; to_hash.symbolize_keys! end
@@ -381,13 +377,9 @@ module ActiveSupport
381
377
 
382
378
  # Convert to a regular hash with string keys.
383
379
  def to_hash
384
- _new_hash = Hash.new
385
- copy_defaults(_new_hash)
386
-
387
- each do |key, value|
388
- _new_hash[key] = convert_value(value, conversion: :to_hash)
389
- end
390
- _new_hash
380
+ copy = Hash[self]
381
+ copy.transform_values! { |v| convert_value_to_hash(v) }
382
+ copy_defaults(copy)
391
383
  end
392
384
 
393
385
  def to_proc
@@ -401,11 +393,7 @@ module ActiveSupport
401
393
 
402
394
  def convert_value(value, conversion: nil)
403
395
  if value.is_a? Hash
404
- if conversion == :to_hash
405
- value.to_hash
406
- else
407
- value.nested_under_indifferent_access
408
- end
396
+ value.nested_under_indifferent_access
409
397
  elsif value.is_a?(Array)
410
398
  if conversion != :assignment || value.frozen?
411
399
  value = value.dup
@@ -416,6 +404,17 @@ module ActiveSupport
416
404
  end
417
405
  end
418
406
 
407
+ def convert_value_to_hash(value)
408
+ if value.is_a? Hash
409
+ value.to_hash
410
+ elsif value.is_a?(Array)
411
+ value.map { |e| convert_value_to_hash(e) }
412
+ else
413
+ value
414
+ end
415
+ end
416
+
417
+
419
418
  def copy_defaults(target)
420
419
  if default_proc
421
420
  target.default_proc = default_proc.dup
@@ -14,15 +14,18 @@ module I18n
14
14
 
15
15
  config.eager_load_namespaces << I18n
16
16
 
17
- # Set the i18n configuration after initialization since a lot of
18
- # configuration is still usually done in application initializers.
19
- config.after_initialize do |app|
17
+ # Make sure i18n is ready before eager loading, in case any eager loaded
18
+ # code needs it.
19
+ config.before_eager_load do |app|
20
20
  I18n::Railtie.initialize_i18n(app)
21
21
  end
22
22
 
23
- # Trigger i18n config before any eager loading has happened
24
- # so it's ready if any classes require it when eager loaded.
25
- config.before_eager_load do |app|
23
+ # i18n initialization needs to run after application initialization, since
24
+ # initializers may configure i18n.
25
+ #
26
+ # If the application eager loaded, this was done on before_eager_load. The
27
+ # hook is still OK, though, because initialize_i18n is idempotent.
28
+ config.after_initialize do |app|
26
29
  I18n::Railtie.initialize_i18n(app)
27
30
  end
28
31
 
@@ -49,7 +52,8 @@ module I18n
49
52
  when :load_path
50
53
  I18n.load_path += value
51
54
  when :raise_on_missing_translations
52
- setup_raise_on_missing_translations_config(app)
55
+ strict = value == :strict
56
+ setup_raise_on_missing_translations_config(app, strict)
53
57
  else
54
58
  I18n.public_send("#{setting}=", value)
55
59
  end
@@ -62,8 +66,9 @@ module I18n
62
66
 
63
67
  if app.config.reloading_enabled?
64
68
  directories = watched_dirs_with_extensions(reloadable_paths)
65
- reloader = app.config.file_watcher.new(I18n.load_path.dup, directories) do
66
- I18n.load_path.keep_if { |p| File.exist?(p) }
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
71
+ I18n.load_path.delete_if { |path| path.to_s.start_with?(Rails.root.to_s) && !File.exist?(path) }
67
72
  I18n.load_path |= reloadable_paths.flat_map(&:existent)
68
73
  end
69
74
 
@@ -71,17 +76,20 @@ module I18n
71
76
  app.reloader.to_run do
72
77
  reloader.execute_if_updated { require_unload_lock! }
73
78
  end
74
- reloader.execute
75
79
  end
76
80
 
77
81
  @i18n_inited = true
78
82
  end
79
83
 
80
- def self.setup_raise_on_missing_translations_config(app)
84
+ def self.setup_raise_on_missing_translations_config(app, strict)
81
85
  ActiveSupport.on_load(:action_view) do
82
86
  ActionView::Helpers::TranslationHelper.raise_on_missing_translations = app.config.i18n.raise_on_missing_translations
83
87
  end
84
88
 
89
+ ActiveSupport.on_load(:active_model_translation) do
90
+ ActiveModel::Translation.raise_on_missing_translations = app.config.i18n.raise_on_missing_translations if strict
91
+ end
92
+
85
93
  if app.config.i18n.raise_on_missing_translations &&
86
94
  I18n.exception_handler.is_a?(I18n::ExceptionHandler) # Only override the i18n gem's default exception handler.
87
95
 
@@ -248,7 +248,8 @@ module ActiveSupport
248
248
 
249
249
  private
250
250
  def define_acronym_regex_patterns
251
- @acronym_regex = @acronyms.empty? ? /(?=a)b/ : /#{@acronyms.values.join("|")}/
251
+ sorted_acronyms = @acronyms.empty? ? [] : @acronyms.values.sort_by { |a| -a.length }
252
+ @acronym_regex = sorted_acronyms.empty? ? /(?=a)b/ : /#{sorted_acronyms.join("|")}/
252
253
  @acronyms_camelize_regex = /^(?:#{@acronym_regex}(?=\b|[A-Z_])|\w)/
253
254
  @acronyms_underscore_regex = /(?:(?<=([A-Za-z\d]))|\b)(#{@acronym_regex})(?=\b|[^a-z])/
254
255
  end
@@ -119,7 +119,7 @@ module ActiveSupport
119
119
  # The capitalization of the first word can be turned off by setting the
120
120
  # +:capitalize+ option to false (default is true).
121
121
  #
122
- # The trailing '_id' can be kept and capitalized by setting the
122
+ # The trailing '_id' can be kept by setting the
123
123
  # optional parameter +keep_id_suffix+ to true (default is false).
124
124
  #
125
125
  # humanize('employee_salary') # => "Employee salary"
@@ -143,13 +143,13 @@ module ActiveSupport
143
143
  result.delete_suffix!(" id")
144
144
  end
145
145
 
146
- result.gsub!(/([a-z\d]+)/i) do |match|
146
+ result.gsub!(/([[[:alpha:]]\d]+)/i) do |match|
147
147
  match.downcase!
148
148
  inflections.acronyms[match] || match
149
149
  end
150
150
 
151
151
  if capitalize
152
- result.sub!(/\A\w/) do |match|
152
+ result.sub!(/\A[[:alpha:]]/) do |match|
153
153
  match.upcase!
154
154
  match
155
155
  end
@@ -1,7 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "fiber"
4
-
5
3
  module ActiveSupport
6
4
  module IsolatedExecutionState # :nodoc:
7
5
  @isolation_level = nil
@@ -58,11 +56,13 @@ module ActiveSupport
58
56
  scope.current
59
57
  end
60
58
 
61
- def share_with(other)
59
+ def share_with(other, except: [])
62
60
  # Action Controller streaming spawns a new thread and copy thread locals.
63
61
  # We do the same here for backward compatibility, but this is very much a hack
64
62
  # and streaming should be rethought.
65
- context.active_support_execution_state = other.active_support_execution_state.dup
63
+ state = other.active_support_execution_state.dup
64
+ Array(except).each { |key| state.delete(key) }
65
+ context.active_support_execution_state = state
66
66
  end
67
67
 
68
68
  private
@@ -14,11 +14,13 @@ 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
21
  # # => {"team" => "rails", "players" => "36"}
22
+ # ActiveSupport::JSON.decode("2.39")
23
+ # # => 2.39
22
24
  def decode(json)
23
25
  data = ::JSON.parse(json, quirks_mode: true)
24
26
 
@@ -54,14 +54,14 @@ module ActiveSupport
54
54
  # Encode the given object into a JSON string
55
55
  def encode(value)
56
56
  unless options.empty?
57
- value = value.as_json(options.dup)
57
+ value = value.as_json(options.dup.freeze)
58
58
  end
59
59
  json = stringify(jsonify(value))
60
60
 
61
61
  # Rails does more escaping than the JSON gem natively does (we
62
62
  # escape \u2028 and \u2029 and optionally >, <, & to work around
63
63
  # certain browser problems).
64
- if Encoding.escape_html_entities_in_json
64
+ if @options.fetch(:escape_html_entities, Encoding.escape_html_entities_in_json)
65
65
  json.gsub!(">", '\u003e')
66
66
  json.gsub!("<", '\u003c')
67
67
  json.gsub!("&", '\u0026')
@@ -7,6 +7,11 @@ module ActiveSupport
7
7
  module LoggerThreadSafeLevel # :nodoc:
8
8
  extend ActiveSupport::Concern
9
9
 
10
+ def initialize(...)
11
+ super
12
+ @local_level_key = :"logger_thread_safe_level_#{object_id}"
13
+ end
14
+
10
15
  def local_level
11
16
  IsolatedExecutionState[local_level_key]
12
17
  end
@@ -40,8 +45,6 @@ module ActiveSupport
40
45
  end
41
46
 
42
47
  private
43
- def local_level_key
44
- @local_level_key ||= :"logger_thread_safe_level_#{object_id}"
45
- end
48
+ attr_reader :local_level_key
46
49
  end
47
50
  end
@@ -4,7 +4,7 @@ require "bigdecimal"
4
4
  require "date"
5
5
  require "ipaddr"
6
6
  require "pathname"
7
- require "uri/generic"
7
+ require "uri"
8
8
  require "msgpack/bigint"
9
9
  require "active_support/hash_with_indifferent_access"
10
10
  require "active_support/time"
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "concurrent/map"
4
- require "set"
5
4
  require "active_support/core_ext/object/try"
6
5
 
7
6
  module ActiveSupport
@@ -164,7 +164,7 @@ module ActiveSupport
164
164
  @cpu_time_finish - @cpu_time_start
165
165
  end
166
166
 
167
- # Returns the idle time time (in milliseconds) passed between the call to
167
+ # Returns the idle time (in milliseconds) passed between the call to
168
168
  # #start! and the call to #finish!.
169
169
  def idle_time
170
170
  diff = duration - cpu_time
@@ -180,7 +180,7 @@ module ActiveSupport
180
180
  when Float, Rational
181
181
  number.to_d(0)
182
182
  when String
183
- BigDecimal(number, exception: false)
183
+ BigDecimal(number, exception: false) unless number.to_s.match?(/[de]/i)
184
184
  else
185
185
  number.to_d rescue nil
186
186
  end
@@ -16,9 +16,24 @@ module ActiveSupport
16
16
  private
17
17
  def parts
18
18
  left, right = number.to_s.split(".")
19
- left.gsub!(delimiter_pattern) do |digit_to_delimit|
20
- "#{digit_to_delimit}#{options[:delimiter]}"
19
+ if delimiter_pattern
20
+ left.gsub!(delimiter_pattern) do |digit_to_delimit|
21
+ "#{digit_to_delimit}#{options[:delimiter]}"
22
+ end
23
+ else
24
+ left_parts = []
25
+ offset = left.size % 3
26
+ if offset > 0
27
+ left_parts << left[0, offset]
28
+ end
29
+
30
+ (left.size / 3).times do |i|
31
+ left_parts << left[offset + (i * 3), 3]
32
+ end
33
+
34
+ left = left_parts.join(options[:delimiter])
21
35
  end
36
+
22
37
  [left, right].compact
23
38
  end
24
39
 
@@ -1,6 +1,28 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveSupport
4
+ # = Number Helper
5
+ #
6
+ # Provides methods for formatting numbers into currencies, percentages,
7
+ # phone numbers, and more.
8
+ #
9
+ # Example usage in a class:
10
+ # class Topic
11
+ # include ActiveSupport::NumberHelper
12
+ #
13
+ # def price
14
+ # number_to_currency(@price)
15
+ # end
16
+ # end
17
+ #
18
+ # Example usage in a module:
19
+ # require "active_support/number_helper"
20
+ #
21
+ # module NumberFormatting
22
+ # def format_price(price)
23
+ # ActiveSupport::NumberHelper.number_to_currency(price)
24
+ # end
25
+ # end
4
26
  module NumberHelper
5
27
  extend ActiveSupport::Autoload
6
28
 
@@ -96,6 +96,12 @@ module ActiveSupport
96
96
  config.eager_load_namespaces << TZInfo
97
97
  end
98
98
 
99
+ initializer "active_support.to_time_preserves_timezone" do |app|
100
+ config.after_initialize do
101
+ ActiveSupport.to_time_preserves_timezone = app.config.active_support.to_time_preserves_timezone
102
+ end
103
+ end
104
+
99
105
  # Sets the default week start
100
106
  # If assigned value is not a valid day symbol (e.g. :sunday, :monday, ...), an exception will be raised.
101
107
  initializer "active_support.initialize_beginning_of_week" do |app|