activesupport 7.2.2 → 8.0.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 (55) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +76 -198
  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/memory_store.rb +6 -2
  8. data/lib/active_support/cache/redis_cache_store.rb +5 -2
  9. data/lib/active_support/cache.rb +16 -11
  10. data/lib/active_support/callbacks.rb +5 -3
  11. data/lib/active_support/class_attribute.rb +33 -0
  12. data/lib/active_support/code_generator.rb +9 -0
  13. data/lib/active_support/concurrency/share_lock.rb +0 -1
  14. data/lib/active_support/configuration_file.rb +15 -6
  15. data/lib/active_support/core_ext/array/conversions.rb +3 -3
  16. data/lib/active_support/core_ext/benchmark.rb +6 -9
  17. data/lib/active_support/core_ext/class/attribute.rb +24 -20
  18. data/lib/active_support/core_ext/date/conversions.rb +2 -0
  19. data/lib/active_support/core_ext/date_and_time/compatibility.rb +2 -2
  20. data/lib/active_support/core_ext/enumerable.rb +8 -3
  21. data/lib/active_support/core_ext/erb/util.rb +2 -2
  22. data/lib/active_support/core_ext/hash/except.rb +0 -12
  23. data/lib/active_support/core_ext/module/attr_internal.rb +3 -4
  24. data/lib/active_support/core_ext/object/json.rb +15 -9
  25. data/lib/active_support/core_ext/securerandom.rb +24 -8
  26. data/lib/active_support/core_ext/thread/backtrace/location.rb +2 -7
  27. data/lib/active_support/core_ext/time/calculations.rb +14 -2
  28. data/lib/active_support/core_ext/time/compatibility.rb +9 -1
  29. data/lib/active_support/core_ext/time/conversions.rb +2 -0
  30. data/lib/active_support/delegation.rb +0 -2
  31. data/lib/active_support/dependencies.rb +0 -1
  32. data/lib/active_support/deprecation/reporting.rb +0 -19
  33. data/lib/active_support/deprecation.rb +1 -1
  34. data/lib/active_support/duration.rb +14 -10
  35. data/lib/active_support/encrypted_configuration.rb +20 -2
  36. data/lib/active_support/error_reporter.rb +25 -1
  37. data/lib/active_support/evented_file_update_checker.rb +0 -1
  38. data/lib/active_support/gem_version.rb +3 -3
  39. data/lib/active_support/hash_with_indifferent_access.rb +16 -16
  40. data/lib/active_support/i18n_railtie.rb +19 -11
  41. data/lib/active_support/isolated_execution_state.rb +0 -2
  42. data/lib/active_support/json/encoding.rb +2 -2
  43. data/lib/active_support/notifications/fanout.rb +0 -1
  44. data/lib/active_support/number_helper.rb +22 -0
  45. data/lib/active_support/railtie.rb +4 -0
  46. data/lib/active_support/tagged_logging.rb +5 -0
  47. data/lib/active_support/testing/assertions.rb +79 -21
  48. data/lib/active_support/testing/isolation.rb +0 -2
  49. data/lib/active_support/testing/strict_warnings.rb +1 -1
  50. data/lib/active_support/testing/time_helpers.rb +2 -1
  51. data/lib/active_support/time_with_zone.rb +22 -13
  52. data/lib/active_support/values/time_zone.rb +11 -9
  53. data/lib/active_support.rb +10 -3
  54. metadata +23 -8
  55. data/lib/active_support/proxy_object.rb +0 -20
@@ -19,11 +19,20 @@ module ActiveSupport
19
19
  end
20
20
 
21
21
  def parse(context: nil, **options)
22
- source = render(context)
23
- if YAML.respond_to?(:unsafe_load)
24
- YAML.unsafe_load(source, **options) || {}
22
+ source = @content.include?("<%") ? render(context) : @content
23
+
24
+ if source == @content
25
+ if YAML.respond_to?(:unsafe_load)
26
+ YAML.unsafe_load_file(@content_path, **options) || {}
27
+ else
28
+ YAML.load_file(@content_path, **options) || {}
29
+ end
25
30
  else
26
- YAML.load(source, **options) || {}
31
+ if YAML.respond_to?(:unsafe_load)
32
+ YAML.unsafe_load(source, **options) || {}
33
+ else
34
+ YAML.load(source, **options) || {}
35
+ end
27
36
  end
28
37
  rescue Psych::SyntaxError => error
29
38
  raise "YAML syntax error occurred while parsing #{@content_path}. " \
@@ -33,8 +42,7 @@ module ActiveSupport
33
42
 
34
43
  private
35
44
  def read(content_path)
36
- require "yaml"
37
- require "erb"
45
+ require "yaml" unless defined?(YAML)
38
46
 
39
47
  File.read(content_path).tap do |content|
40
48
  if content.include?("\u00A0")
@@ -44,6 +52,7 @@ module ActiveSupport
44
52
  end
45
53
 
46
54
  def render(context)
55
+ require "erb" unless defined?(ERB)
47
56
  erb = ERB.new(@content).tap { |e| e.filename = @content_path }
48
57
  context ? erb.result(context) : erb.result
49
58
  end
@@ -16,11 +16,11 @@ class Array
16
16
  # ==== Options
17
17
  #
18
18
  # * <tt>:words_connector</tt> - The sign or word used to join all but the last
19
- # element in arrays with three or more elements (default: ", ").
19
+ # element in arrays with three or more elements (default: <tt>", "</tt>).
20
20
  # * <tt>:last_word_connector</tt> - The sign or word used to join the last element
21
- # in arrays with three or more elements (default: ", and ").
21
+ # in arrays with three or more elements (default: <tt>", and "</tt>).
22
22
  # * <tt>:two_words_connector</tt> - The sign or word used to join the elements
23
- # in arrays with two elements (default: " and ").
23
+ # in arrays with two elements (default: <tt>" and "</tt>).
24
24
  # * <tt>:locale</tt> - If +i18n+ is available, you can set a locale and use
25
25
  # the connector options defined on the 'support.array' namespace in the
26
26
  # corresponding dictionary file.
@@ -3,14 +3,11 @@
3
3
  require "benchmark"
4
4
 
5
5
  class << Benchmark
6
- # Benchmark realtime in milliseconds.
7
- #
8
- # Benchmark.realtime { User.all }
9
- # # => 8.0e-05
10
- #
11
- # Benchmark.ms { User.all }
12
- # # => 0.074
13
- def ms(&block)
14
- 1000 * realtime(&block)
6
+ def ms(&block) # :nodoc
7
+ # NOTE: Please also remove the Active Support `benchmark` dependency when removing this
8
+ ActiveSupport.deprecator.warn <<~TEXT
9
+ `Benchmark.ms` is deprecated and will be removed in Rails 8.1 without replacement.
10
+ TEXT
11
+ ActiveSupport::Benchmark.realtime(:float_millisecond, &block)
15
12
  end
16
13
  end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "active_support/core_ext/module/redefine_method"
4
+ require "active_support/class_attribute"
4
5
 
5
6
  class Class
6
7
  # Declare a class-level attribute whose value is inheritable by subclasses.
@@ -83,32 +84,37 @@ class Class
83
84
  #
84
85
  # class_attribute :settings, default: {}
85
86
  def class_attribute(*attrs, instance_accessor: true,
86
- instance_reader: instance_accessor, instance_writer: instance_accessor, instance_predicate: true, default: nil)
87
-
87
+ instance_reader: instance_accessor, instance_writer: instance_accessor, instance_predicate: true, default: nil
88
+ )
88
89
  class_methods, methods = [], []
89
90
  attrs.each do |name|
90
91
  unless name.is_a?(Symbol) || name.is_a?(String)
91
92
  raise TypeError, "#{name.inspect} is not a symbol nor a string"
92
93
  end
93
94
 
94
- class_methods << <<~RUBY # In case the method exists and is not public
95
- silence_redefinition_of_method def #{name}
96
- end
97
- RUBY
95
+ name = name.to_sym
96
+ namespaced_name = :"__class_attr_#{name}"
97
+ ::ActiveSupport::ClassAttribute.redefine(self, name, namespaced_name, default)
98
98
 
99
- methods << <<~RUBY if instance_reader
100
- silence_redefinition_of_method def #{name}
101
- defined?(@#{name}) ? @#{name} : self.class.#{name}
102
- end
103
- RUBY
99
+ delegators = [
100
+ "def #{name}; #{namespaced_name}; end",
101
+ "def #{name}=(value); self.#{namespaced_name} = value; end",
102
+ ]
104
103
 
105
- class_methods << <<~RUBY
106
- silence_redefinition_of_method def #{name}=(value)
107
- redefine_method(:#{name}) { value } if singleton_class?
108
- redefine_singleton_method(:#{name}) { value }
109
- value
110
- end
111
- RUBY
104
+ class_methods.concat(delegators)
105
+ if singleton_class?
106
+ methods.concat(delegators)
107
+ else
108
+ methods << <<~RUBY if instance_reader
109
+ silence_redefinition_of_method def #{name}
110
+ if defined?(@#{name})
111
+ @#{name}
112
+ else
113
+ self.class.#{name}
114
+ end
115
+ end
116
+ RUBY
117
+ end
112
118
 
113
119
  methods << <<~RUBY if instance_writer
114
120
  silence_redefinition_of_method(:#{name}=)
@@ -125,7 +131,5 @@ class Class
125
131
 
126
132
  location = caller_locations(1, 1).first
127
133
  class_eval(["class << self", *class_methods, "end", *methods].join(";").tr("\n", ";"), location.path, location.lineno)
128
-
129
- attrs.each { |name| public_send("#{name}=", default) }
130
134
  end
131
135
  end
@@ -17,6 +17,7 @@ class Date
17
17
  date.strftime("%B #{day_format}, %Y") # => "April 25th, 2007"
18
18
  },
19
19
  rfc822: "%d %b %Y",
20
+ rfc2822: "%d %b %Y",
20
21
  iso8601: lambda { |date| date.iso8601 }
21
22
  }
22
23
 
@@ -34,6 +35,7 @@ class Date
34
35
  # date.to_fs(:long) # => "November 10, 2007"
35
36
  # date.to_fs(:long_ordinal) # => "November 10th, 2007"
36
37
  # date.to_fs(:rfc822) # => "10 Nov 2007"
38
+ # date.to_fs(:rfc2822) # => "10 Nov 2007"
37
39
  # date.to_fs(:iso8601) # => "2007-11-10"
38
40
  #
39
41
  # == Adding your own date formats to to_fs
@@ -26,8 +26,8 @@ module DateAndTime
26
26
  # Only warn once, the first time the value is used (which should
27
27
  # be the first time #to_time is called).
28
28
  ActiveSupport.deprecator.warn(
29
- "to_time will always preserve the timezone offset of the receiver in Rails 8.0. " \
30
- "To opt in to the new behavior, set `ActiveSupport.to_time_preserves_timezone = true`."
29
+ "`to_time` will always preserve the receiver timezone rather than system local time in Rails 8.1." \
30
+ "To opt in to the new behavior, set `config.active_support.to_time_preserves_timezone = :zone`."
31
31
  )
32
32
 
33
33
  @@preserve_timezone = false
@@ -192,9 +192,14 @@ module Enumerable
192
192
  # # => [ Person.find(1), Person.find(5), Person.find(3) ]
193
193
  #
194
194
  # If the +series+ include keys that have no corresponding element in the Enumerable, these are ignored.
195
- # If the Enumerable has additional elements that aren't named in the +series+, these are not included in the result.
196
- def in_order_of(key, series)
197
- group_by(&key).values_at(*series).flatten(1).compact
195
+ # If the Enumerable has additional elements that aren't named in the +series+, these are not included in the result, unless
196
+ # the +filter+ option is set to +false+.
197
+ def in_order_of(key, series, filter: true)
198
+ if filter
199
+ group_by(&key).values_at(*series).flatten(1).compact
200
+ else
201
+ sort_by { |v| series.index(v.public_send(key)) || series.size }.compact
202
+ end
198
203
  end
199
204
 
200
205
  # Returns the sole item in the enumerable. If there are no items, or more
@@ -174,7 +174,7 @@ class ERB
174
174
 
175
175
  case source.matched
176
176
  when start_re
177
- tokens << [:TEXT, source.string[pos, len]] if len > 0
177
+ tokens << [:TEXT, source.string.byteslice(pos, len)] if len > 0
178
178
  tokens << [:OPEN, source.matched]
179
179
  if source.scan(/(.*?)(?=#{finish_re}|\z)/m)
180
180
  tokens << [:CODE, source.matched] unless source.matched.empty?
@@ -183,7 +183,7 @@ class ERB
183
183
  raise NotImplementedError
184
184
  end
185
185
  when finish_re
186
- tokens << [:CODE, source.string[pos, len]] if len > 0
186
+ tokens << [:CODE, source.string.byteslice(pos, len)] if len > 0
187
187
  tokens << [:CLOSE, source.matched]
188
188
  else
189
189
  raise NotImplementedError, source.matched
@@ -1,18 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Hash
4
- # Returns a hash that includes everything except given keys.
5
- # hash = { a: true, b: false, c: nil }
6
- # hash.except(:c) # => { a: true, b: false }
7
- # hash.except(:a, :b) # => { c: nil }
8
- # hash # => { a: true, b: false, c: nil }
9
- #
10
- # This is useful for limiting a set of parameters to everything but a few known toggles:
11
- # @person.update(params[:person].except(:admin))
12
- def except(*keys)
13
- slice(*self.keys - keys)
14
- end unless method_defined?(:except)
15
-
16
4
  # Removes the given keys from hash and returns it.
17
5
  # hash = { a: true, b: false, c: nil }
18
6
  # hash.except!(:c) # => { a: true, b: false }
@@ -24,14 +24,13 @@ class Module
24
24
 
25
25
  def attr_internal_naming_format=(format)
26
26
  if format.start_with?("@")
27
- ActiveSupport.deprecator.warn <<~MESSAGE
28
- Setting `attr_internal_naming_format` with a `@` prefix is deprecated and will be removed in Rails 8.0.
27
+ raise ArgumentError, <<~MESSAGE.squish
28
+ Setting `attr_internal_naming_format` with a `@` prefix is not supported.
29
29
 
30
30
  You can simply replace #{format.inspect} by #{format.delete_prefix("@").inspect}.
31
31
  MESSAGE
32
-
33
- format = format.delete_prefix("@")
34
32
  end
33
+
35
34
  @attr_internal_naming_format = format
36
35
  end
37
36
  end
@@ -65,11 +65,9 @@ class Object
65
65
  end
66
66
  end
67
67
 
68
- if RUBY_VERSION >= "3.2"
69
- class Data # :nodoc:
70
- def as_json(options = nil)
71
- to_h.as_json(options)
72
- end
68
+ class Data # :nodoc:
69
+ def as_json(options = nil)
70
+ to_h.as_json(options)
73
71
  end
74
72
  end
75
73
 
@@ -105,7 +103,7 @@ end
105
103
 
106
104
  class Symbol
107
105
  def as_json(options = nil) # :nodoc:
108
- to_s
106
+ name
109
107
  end
110
108
  end
111
109
 
@@ -164,7 +162,12 @@ end
164
162
 
165
163
  class Array
166
164
  def as_json(options = nil) # :nodoc:
167
- map { |v| options ? v.as_json(options.dup) : v.as_json }
165
+ if options
166
+ options = options.dup.freeze unless options.frozen?
167
+ map { |v| v.as_json(options) }
168
+ else
169
+ map { |v| v.as_json }
170
+ end
168
171
  end
169
172
  end
170
173
 
@@ -184,8 +187,11 @@ class Hash
184
187
  end
185
188
 
186
189
  result = {}
187
- subset.each do |k, v|
188
- result[k.to_s] = options ? v.as_json(options.dup) : v.as_json
190
+ if options
191
+ options = options.dup.freeze unless options.frozen?
192
+ subset.each { |k, v| result[k.to_s] = v.as_json(options) }
193
+ else
194
+ subset.each { |k, v| result[k.to_s] = v.as_json }
189
195
  end
190
196
  result
191
197
  end
@@ -16,8 +16,18 @@ module SecureRandom
16
16
  #
17
17
  # p SecureRandom.base58 # => "4kUgL2pdQMSCQtjE"
18
18
  # p SecureRandom.base58(24) # => "77TMHrHJFvFDwodq8w7Ev2m7"
19
- def self.base58(n = 16)
20
- SecureRandom.alphanumeric(n, chars: BASE58_ALPHABET)
19
+ if SecureRandom.method(:alphanumeric).parameters.size == 2 # Remove check when Ruby 3.3 is the minimum supported version
20
+ def self.base58(n = 16)
21
+ alphanumeric(n, chars: BASE58_ALPHABET)
22
+ end
23
+ else
24
+ def self.base58(n = 16)
25
+ SecureRandom.random_bytes(n).unpack("C*").map do |byte|
26
+ idx = byte % 64
27
+ idx = SecureRandom.random_number(58) if idx >= 58
28
+ BASE58_ALPHABET[idx]
29
+ end.join
30
+ end
21
31
  end
22
32
 
23
33
  # SecureRandom.base36 generates a random base36 string in lowercase.
@@ -31,11 +41,17 @@ module SecureRandom
31
41
  #
32
42
  # p SecureRandom.base36 # => "4kugl2pdqmscqtje"
33
43
  # p SecureRandom.base36(24) # => "77tmhrhjfvfdwodq8w7ev2m7"
34
- def self.base36(n = 16)
35
- SecureRandom.random_bytes(n).unpack("C*").map do |byte|
36
- idx = byte % 64
37
- idx = SecureRandom.random_number(36) if idx >= 36
38
- BASE36_ALPHABET[idx]
39
- end.join
44
+ if SecureRandom.method(:alphanumeric).parameters.size == 2 # Remove check when Ruby 3.3 is the minimum supported version
45
+ def self.base36(n = 16)
46
+ alphanumeric(n, chars: BASE36_ALPHABET)
47
+ end
48
+ else
49
+ def self.base36(n = 16)
50
+ SecureRandom.random_bytes(n).unpack("C*").map do |byte|
51
+ idx = byte % 64
52
+ idx = SecureRandom.random_number(36) if idx >= 36
53
+ BASE36_ALPHABET[idx]
54
+ end.join
55
+ end
40
56
  end
41
57
  end
@@ -1,12 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Thread::Backtrace::Location # :nodoc:
4
- if defined?(ErrorHighlight) && Gem::Version.new(ErrorHighlight::VERSION) >= Gem::Version.new("0.4.0")
5
- def spot(ex)
6
- ErrorHighlight.spot(ex, backtrace_location: self)
7
- end
8
- else
9
- def spot(ex)
10
- end
4
+ def spot(ex)
5
+ ErrorHighlight.spot(ex, backtrace_location: self)
11
6
  end
12
7
  end
@@ -147,6 +147,13 @@ class Time
147
147
  elsif zone.respond_to?(:utc_to_local)
148
148
  new_time = ::Time.new(new_year, new_month, new_day, new_hour, new_min, new_sec, zone)
149
149
 
150
+ # Some versions of Ruby have a bug where Time.new with a zone object and
151
+ # fractional seconds will end up with a broken utc_offset.
152
+ # This is fixed in Ruby 3.3.1 and 3.2.4
153
+ unless new_time.utc_offset.integer?
154
+ new_time += 0
155
+ end
156
+
150
157
  # When there are two occurrences of a nominal time due to DST ending,
151
158
  # `Time.new` chooses the first chronological occurrence (the one with a
152
159
  # larger UTC offset). However, for `change`, we want to choose the
@@ -217,8 +224,13 @@ class Time
217
224
  # Returns a new Time representing the time a number of seconds since the instance time
218
225
  def since(seconds)
219
226
  self + seconds
220
- rescue
221
- to_datetime.since(seconds)
227
+ rescue TypeError
228
+ result = to_datetime.since(seconds)
229
+ ActiveSupport.deprecator.warn(
230
+ "Passing an instance of #{seconds.class} to #{self.class}#since is deprecated. This behavior will raise " \
231
+ "a `TypeError` in Rails 8.1."
232
+ )
233
+ result
222
234
  end
223
235
  alias :in :since
224
236
 
@@ -15,10 +15,18 @@ class Time
15
15
  end
16
16
 
17
17
  def preserve_timezone # :nodoc:
18
- active_support_local_zone == zone || super
18
+ system_local_time? || super
19
19
  end
20
20
 
21
21
  private
22
+ def system_local_time?
23
+ if ::Time.equal?(self.class)
24
+ zone = self.zone
25
+ String === zone &&
26
+ (zone != "UTC" || active_support_local_zone == "UTC")
27
+ end
28
+ end
29
+
22
30
  @@active_support_local_tz = nil
23
31
 
24
32
  def active_support_local_zone
@@ -22,6 +22,7 @@ class Time
22
22
  offset_format = time.formatted_offset(false)
23
23
  time.strftime("%a, %d %b %Y %H:%M:%S #{offset_format}")
24
24
  },
25
+ rfc2822: lambda { |time| time.rfc2822 },
25
26
  iso8601: lambda { |time| time.iso8601 }
26
27
  }
27
28
 
@@ -40,6 +41,7 @@ class Time
40
41
  # time.to_fs(:long) # => "January 18, 2007 06:10"
41
42
  # time.to_fs(:long_ordinal) # => "January 18th, 2007 06:10"
42
43
  # time.to_fs(:rfc822) # => "Thu, 18 Jan 2007 06:10:17 -0600"
44
+ # time.to_fs(:rfc2822) # => "Thu, 18 Jan 2007 06:10:17 -0600"
43
45
  # time.to_fs(:iso8601) # => "2007-01-18T06:10:17-06:00"
44
46
  #
45
47
  # == Adding your own time formats to +to_fs+
@@ -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.
@@ -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)
@@ -209,6 +209,7 @@ module ActiveSupport
209
209
  #
210
210
  def report(error, handled: true, severity: handled ? :warning : :error, context: {}, source: DEFAULT_SOURCE)
211
211
  return if error.instance_variable_defined?(:@__rails_error_reported)
212
+ ensure_backtrace(error)
212
213
 
213
214
  unless SEVERITIES.include?(severity)
214
215
  raise ArgumentError, "severity must be one of #{SEVERITIES.map(&:inspect).join(", ")}, got: #{severity.inspect}"
@@ -237,5 +238,28 @@ module ActiveSupport
237
238
 
238
239
  nil
239
240
  end
241
+
242
+ private
243
+ def ensure_backtrace(error)
244
+ return if error.frozen? # re-raising won't add a backtrace
245
+ return unless error.backtrace.nil?
246
+
247
+ begin
248
+ # We could use Exception#set_backtrace, but until Ruby 3.4
249
+ # it only support setting `Exception#backtrace` and not
250
+ # `Exception#backtrace_locations`. So raising the exception
251
+ # is a good way to build a real backtrace.
252
+ raise error
253
+ rescue error.class => error
254
+ end
255
+
256
+ count = 0
257
+ while error.backtrace_locations.first&.path == __FILE__
258
+ count += 1
259
+ error.backtrace_locations.shift
260
+ end
261
+
262
+ error.backtrace.shift(count)
263
+ end
240
264
  end
241
265
  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 = 2
10
+ MAJOR = 8
11
+ MINOR = 0
12
+ TINY = 1
13
13
  PRE = nil
14
14
 
15
15
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")