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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +237 -175
- data/lib/active_support/backtrace_cleaner.rb +71 -0
- data/lib/active_support/cache/mem_cache_store.rb +13 -13
- data/lib/active_support/cache/redis_cache_store.rb +36 -30
- data/lib/active_support/cache/strategy/local_cache.rb +16 -7
- data/lib/active_support/cache/strategy/local_cache_middleware.rb +7 -7
- data/lib/active_support/cache.rb +69 -6
- data/lib/active_support/configurable.rb +28 -0
- data/lib/active_support/continuous_integration.rb +145 -0
- data/lib/active_support/core_ext/benchmark.rb +0 -1
- data/lib/active_support/core_ext/date_and_time/compatibility.rb +1 -1
- data/lib/active_support/core_ext/erb/util.rb +3 -3
- data/lib/active_support/core_ext/object/json.rb +8 -1
- data/lib/active_support/core_ext/object/to_query.rb +5 -0
- data/lib/active_support/core_ext/range.rb +0 -1
- data/lib/active_support/core_ext/string/multibyte.rb +10 -1
- data/lib/active_support/core_ext/string/output_safety.rb +19 -12
- data/lib/active_support/current_attributes/test_helper.rb +2 -2
- data/lib/active_support/current_attributes.rb +13 -10
- data/lib/active_support/deprecation/reporting.rb +4 -2
- data/lib/active_support/deprecation.rb +1 -1
- data/lib/active_support/editor.rb +70 -0
- data/lib/active_support/error_reporter.rb +50 -6
- data/lib/active_support/event_reporter/test_helper.rb +32 -0
- data/lib/active_support/event_reporter.rb +570 -0
- data/lib/active_support/evented_file_update_checker.rb +5 -1
- data/lib/active_support/execution_context.rb +64 -7
- data/lib/active_support/file_update_checker.rb +8 -6
- data/lib/active_support/gem_version.rb +3 -3
- data/lib/active_support/gzip.rb +1 -0
- data/lib/active_support/hash_with_indifferent_access.rb +27 -7
- data/lib/active_support/i18n_railtie.rb +1 -2
- data/lib/active_support/inflector/inflections.rb +31 -15
- data/lib/active_support/inflector/transliterate.rb +6 -8
- data/lib/active_support/isolated_execution_state.rb +7 -13
- data/lib/active_support/json/decoding.rb +2 -2
- data/lib/active_support/json/encoding.rb +103 -14
- data/lib/active_support/log_subscriber.rb +2 -0
- data/lib/active_support/message_encryptors.rb +52 -0
- data/lib/active_support/message_pack/extensions.rb +5 -0
- data/lib/active_support/message_verifiers.rb +52 -0
- data/lib/active_support/messages/rotation_coordinator.rb +9 -0
- data/lib/active_support/messages/rotator.rb +5 -0
- data/lib/active_support/multibyte/chars.rb +8 -1
- data/lib/active_support/multibyte.rb +4 -0
- data/lib/active_support/railtie.rb +26 -12
- data/lib/active_support/syntax_error_proxy.rb +3 -0
- data/lib/active_support/test_case.rb +61 -6
- data/lib/active_support/testing/assertions.rb +34 -6
- data/lib/active_support/testing/error_reporter_assertions.rb +18 -1
- data/lib/active_support/testing/event_reporter_assertions.rb +217 -0
- data/lib/active_support/testing/notification_assertions.rb +92 -0
- data/lib/active_support/testing/parallelization/worker.rb +2 -0
- data/lib/active_support/testing/parallelization.rb +13 -0
- data/lib/active_support/testing/tests_without_assertions.rb +1 -1
- data/lib/active_support/testing/time_helpers.rb +7 -3
- data/lib/active_support/time_with_zone.rb +19 -5
- data/lib/active_support/values/time_zone.rb +8 -1
- data/lib/active_support/xml_mini.rb +1 -2
- data/lib/active_support.rb +11 -0
- metadata +10 -5
- data/lib/active_support/core_ext/range/each.rb +0 -24
@@ -41,7 +41,6 @@ module ActiveSupport
|
|
41
41
|
|
42
42
|
prepend Strategy::LocalCache
|
43
43
|
|
44
|
-
KEY_MAX_SIZE = 250
|
45
44
|
ESCAPE_KEY_CHARS = /[\x00-\x20%\x7F-\xFF]/n
|
46
45
|
|
47
46
|
# Creates a new Dalli::Client instance with specified addresses and options.
|
@@ -80,6 +79,7 @@ module ActiveSupport
|
|
80
79
|
if options.key?(:cache_nils)
|
81
80
|
options[:skip_nil] = !options.delete(:cache_nils)
|
82
81
|
end
|
82
|
+
options[:max_key_size] ||= MAX_KEY_SIZE
|
83
83
|
super(options)
|
84
84
|
|
85
85
|
unless [String, Dalli::Client, NilClass].include?(addresses.first.class)
|
@@ -110,9 +110,6 @@ module ActiveSupport
|
|
110
110
|
# * <tt>raw: true</tt> - Sends the value directly to the server as raw
|
111
111
|
# bytes. The value must be a string or number. You can use memcached
|
112
112
|
# direct operations like +increment+ and +decrement+ only on raw values.
|
113
|
-
#
|
114
|
-
# * <tt>unless_exist: true</tt> - Prevents overwriting an existing cache
|
115
|
-
# entry.
|
116
113
|
|
117
114
|
# Increment a cached integer value using the memcached incr atomic operator.
|
118
115
|
# Returns the updated value.
|
@@ -129,6 +126,11 @@ module ActiveSupport
|
|
129
126
|
#
|
130
127
|
# Incrementing a non-numeric value, or a value written without
|
131
128
|
# <tt>raw: true</tt>, will fail and return +nil+.
|
129
|
+
#
|
130
|
+
# To read the value later, call #read_counter:
|
131
|
+
#
|
132
|
+
# cache.increment("baz") # => 7
|
133
|
+
# cache.read_counter("baz") # 7
|
132
134
|
def increment(name, amount = 1, options = nil)
|
133
135
|
options = merged_options(options)
|
134
136
|
key = normalize_key(name, options)
|
@@ -155,6 +157,11 @@ module ActiveSupport
|
|
155
157
|
#
|
156
158
|
# Decrementing a non-numeric value, or a value written without
|
157
159
|
# <tt>raw: true</tt>, will fail and return +nil+.
|
160
|
+
#
|
161
|
+
# To read the value later, call #read_counter:
|
162
|
+
#
|
163
|
+
# cache.decrement("baz") # => 3
|
164
|
+
# cache.read_counter("baz") # 3
|
158
165
|
def decrement(name, amount = 1, options = nil)
|
159
166
|
options = merged_options(options)
|
160
167
|
key = normalize_key(name, options)
|
@@ -249,19 +256,12 @@ module ActiveSupport
|
|
249
256
|
# before applying the regular expression to ensure we are escaping all
|
250
257
|
# characters properly.
|
251
258
|
def normalize_key(key, options)
|
252
|
-
key =
|
259
|
+
key = expand_and_namespace_key(key, options)
|
253
260
|
if key
|
254
261
|
key = key.dup.force_encoding(Encoding::ASCII_8BIT)
|
255
262
|
key = key.gsub(ESCAPE_KEY_CHARS) { |match| "%#{match.getbyte(0).to_s(16).upcase}" }
|
256
|
-
|
257
|
-
if key.size > KEY_MAX_SIZE
|
258
|
-
key_separator = ":hash:"
|
259
|
-
key_hash = ActiveSupport::Digest.hexdigest(key)
|
260
|
-
key_trim_size = KEY_MAX_SIZE - key_separator.size - key_hash.size
|
261
|
-
key = "#{key[0, key_trim_size]}#{key_separator}#{key_hash}"
|
262
|
-
end
|
263
263
|
end
|
264
|
-
key
|
264
|
+
truncate_key(key)
|
265
265
|
end
|
266
266
|
|
267
267
|
def deserialize_entry(payload, raw: false, **)
|
@@ -35,9 +35,6 @@ module ActiveSupport
|
|
35
35
|
# +Redis::Distributed+ 4.0.1+ for distributed mget support.
|
36
36
|
# * +delete_matched+ support for Redis KEYS globs.
|
37
37
|
class RedisCacheStore < Store
|
38
|
-
# Keys are truncated with the Active Support digest if they exceed 1kB
|
39
|
-
MAX_KEY_BYTESIZE = 1024
|
40
|
-
|
41
38
|
DEFAULT_REDIS_OPTIONS = {
|
42
39
|
connect_timeout: 1,
|
43
40
|
read_timeout: 1,
|
@@ -106,20 +103,29 @@ module ActiveSupport
|
|
106
103
|
end
|
107
104
|
end
|
108
105
|
|
109
|
-
attr_reader :max_key_bytesize
|
110
106
|
attr_reader :redis
|
111
107
|
|
112
108
|
# Creates a new Redis cache store.
|
113
109
|
#
|
114
|
-
# There are
|
115
|
-
#
|
116
|
-
#
|
117
|
-
#
|
118
|
-
# instance.
|
110
|
+
# There are a few ways to provide the Redis client used by the cache:
|
111
|
+
#
|
112
|
+
# 1. The +:redis+ param can be:
|
113
|
+
# - A Redis instance.
|
114
|
+
# - A +ConnectionPool+ instance wrapping a Redis instance.
|
115
|
+
# - A block that returns a Redis instance.
|
116
|
+
#
|
117
|
+
# 2. The +:url+ param can be:
|
118
|
+
# - A string used to create a Redis instance.
|
119
|
+
# - An array of strings used to create a +Redis::Distributed+ instance.
|
120
|
+
#
|
121
|
+
# If the final Redis instance is not already a +ConnectionPool+, it will
|
122
|
+
# be wrapped in one using +ActiveSupport::Cache::Store::DEFAULT_POOL_OPTIONS+.
|
123
|
+
# These options can be overridden with the +:pool+ param, or the pool can be
|
124
|
+
# disabled with +:pool: false+.
|
119
125
|
#
|
120
126
|
# Option Class Result
|
121
|
-
# :redis Proc -> options[:redis].call
|
122
127
|
# :redis Object -> options[:redis]
|
128
|
+
# :redis Proc -> options[:redis].call
|
123
129
|
# :url String -> Redis.new(url: …)
|
124
130
|
# :url Array -> Redis::Distributed.new([{ url: … }, { url: … }, …])
|
125
131
|
#
|
@@ -148,14 +154,17 @@ module ActiveSupport
|
|
148
154
|
# cache.exist?('bar') # => false
|
149
155
|
def initialize(error_handler: DEFAULT_ERROR_HANDLER, **redis_options)
|
150
156
|
universal_options = redis_options.extract!(*UNIVERSAL_OPTIONS)
|
157
|
+
redis = redis_options[:redis]
|
158
|
+
|
159
|
+
already_pool = redis.instance_of?(::ConnectionPool) ||
|
160
|
+
(redis.respond_to?(:wrapped_pool) && redis.wrapped_pool.instance_of?(::ConnectionPool))
|
151
161
|
|
152
|
-
if pool_options = self.class.send(:retrieve_pool_options, redis_options)
|
162
|
+
if !already_pool && pool_options = self.class.send(:retrieve_pool_options, redis_options)
|
153
163
|
@redis = ::ConnectionPool.new(pool_options) { self.class.build_redis(**redis_options) }
|
154
164
|
else
|
155
165
|
@redis = self.class.build_redis(**redis_options)
|
156
166
|
end
|
157
167
|
|
158
|
-
@max_key_bytesize = MAX_KEY_BYTESIZE
|
159
168
|
@error_handler = error_handler
|
160
169
|
|
161
170
|
super(universal_options)
|
@@ -213,7 +222,7 @@ module ActiveSupport
|
|
213
222
|
nodes.each do |node|
|
214
223
|
begin
|
215
224
|
cursor, keys = node.scan(cursor, match: pattern, count: SCAN_BATCH_SIZE)
|
216
|
-
node.
|
225
|
+
node.unlink(*keys) unless keys.empty?
|
217
226
|
end until cursor == "0"
|
218
227
|
end
|
219
228
|
end
|
@@ -236,6 +245,11 @@ module ActiveSupport
|
|
236
245
|
# Incrementing a non-numeric value, or a value written without
|
237
246
|
# <tt>raw: true</tt>, will fail and return +nil+.
|
238
247
|
#
|
248
|
+
# To read the value later, call #read_counter:
|
249
|
+
#
|
250
|
+
# cache.increment("baz") # => 7
|
251
|
+
# cache.read_counter("baz") # 7
|
252
|
+
#
|
239
253
|
# Failsafe: Raises errors.
|
240
254
|
def increment(name, amount = 1, options = nil)
|
241
255
|
options = merged_options(options)
|
@@ -263,6 +277,11 @@ module ActiveSupport
|
|
263
277
|
# Decrementing a non-numeric value, or a value written without
|
264
278
|
# <tt>raw: true</tt>, will fail and return +nil+.
|
265
279
|
#
|
280
|
+
# To read the value later, call #read_counter:
|
281
|
+
#
|
282
|
+
# cache.decrement("baz") # => 3
|
283
|
+
# cache.read_counter("baz") # 3
|
284
|
+
#
|
266
285
|
# Failsafe: Raises errors.
|
267
286
|
def decrement(name, amount = 1, options = nil)
|
268
287
|
options = merged_options(options)
|
@@ -384,14 +403,16 @@ module ActiveSupport
|
|
384
403
|
# Delete an entry from the cache.
|
385
404
|
def delete_entry(key, **options)
|
386
405
|
failsafe :delete_entry, returning: false do
|
387
|
-
redis.then { |c| c.
|
406
|
+
redis.then { |c| c.unlink(key) == 1 }
|
388
407
|
end
|
389
408
|
end
|
390
409
|
|
391
410
|
# Deletes multiple entries in the cache. Returns the number of entries deleted.
|
392
411
|
def delete_multi_entries(entries, **_options)
|
412
|
+
return 0 if entries.empty?
|
413
|
+
|
393
414
|
failsafe :delete_multi_entries, returning: 0 do
|
394
|
-
redis.then { |c| c.
|
415
|
+
redis.then { |c| c.unlink(*entries) }
|
395
416
|
end
|
396
417
|
end
|
397
418
|
|
@@ -410,21 +431,6 @@ module ActiveSupport
|
|
410
431
|
end
|
411
432
|
end
|
412
433
|
|
413
|
-
# Truncate keys that exceed 1kB.
|
414
|
-
def normalize_key(key, options)
|
415
|
-
truncate_key super&.b
|
416
|
-
end
|
417
|
-
|
418
|
-
def truncate_key(key)
|
419
|
-
if key && key.bytesize > max_key_bytesize
|
420
|
-
suffix = ":hash:#{ActiveSupport::Digest.hexdigest(key)}"
|
421
|
-
truncate_at = max_key_bytesize - suffix.bytesize
|
422
|
-
"#{key.byteslice(0, truncate_at)}#{suffix}"
|
423
|
-
else
|
424
|
-
key
|
425
|
-
end
|
426
|
-
end
|
427
|
-
|
428
434
|
def deserialize_entry(payload, raw: false, **)
|
429
435
|
if raw && !payload.nil?
|
430
436
|
Entry.new(payload)
|
@@ -68,12 +68,25 @@ module ActiveSupport
|
|
68
68
|
use_temporary_local_cache(LocalStore.new, &block)
|
69
69
|
end
|
70
70
|
|
71
|
+
# Set a new local cache.
|
72
|
+
def new_local_cache
|
73
|
+
LocalCacheRegistry.set_cache_for(local_cache_key, LocalStore.new)
|
74
|
+
end
|
75
|
+
|
76
|
+
# Unset the current local cache.
|
77
|
+
def unset_local_cache
|
78
|
+
LocalCacheRegistry.set_cache_for(local_cache_key, nil)
|
79
|
+
end
|
80
|
+
|
81
|
+
# The current local cache.
|
82
|
+
def local_cache
|
83
|
+
LocalCacheRegistry.cache_for(local_cache_key)
|
84
|
+
end
|
85
|
+
|
71
86
|
# Middleware class can be inserted as a Rack handler to be local cache for the
|
72
87
|
# duration of request.
|
73
88
|
def middleware
|
74
|
-
@middleware ||= Middleware.new(
|
75
|
-
"ActiveSupport::Cache::Strategy::LocalCache",
|
76
|
-
local_cache_key)
|
89
|
+
@middleware ||= Middleware.new("ActiveSupport::Cache::Strategy::LocalCache", self)
|
77
90
|
end
|
78
91
|
|
79
92
|
def clear(options = nil) # :nodoc:
|
@@ -214,10 +227,6 @@ module ActiveSupport
|
|
214
227
|
@local_cache_key ||= "#{self.class.name.underscore}_local_cache_#{object_id}".gsub(/[\/-]/, "_").to_sym
|
215
228
|
end
|
216
229
|
|
217
|
-
def local_cache
|
218
|
-
LocalCacheRegistry.cache_for(local_cache_key)
|
219
|
-
end
|
220
|
-
|
221
230
|
def bypass_local_cache(&block)
|
222
231
|
use_temporary_local_cache(nil, &block)
|
223
232
|
end
|
@@ -11,11 +11,12 @@ module ActiveSupport
|
|
11
11
|
# This class wraps up local storage for middlewares. Only the middleware method should
|
12
12
|
# construct them.
|
13
13
|
class Middleware # :nodoc:
|
14
|
-
attr_reader :name
|
14
|
+
attr_reader :name
|
15
|
+
attr_accessor :cache
|
15
16
|
|
16
|
-
def initialize(name,
|
17
|
+
def initialize(name, cache)
|
17
18
|
@name = name
|
18
|
-
@
|
19
|
+
@cache = cache
|
19
20
|
@app = nil
|
20
21
|
end
|
21
22
|
|
@@ -25,18 +26,17 @@ module ActiveSupport
|
|
25
26
|
end
|
26
27
|
|
27
28
|
def call(env)
|
28
|
-
|
29
|
+
cache.new_local_cache
|
29
30
|
response = @app.call(env)
|
30
31
|
response[2] = ::Rack::BodyProxy.new(response[2]) do
|
31
|
-
|
32
|
+
cache.unset_local_cache
|
32
33
|
end
|
33
34
|
cleanup_on_body_close = true
|
34
35
|
response
|
35
36
|
rescue Rack::Utils::InvalidParameterError
|
36
37
|
[400, {}, []]
|
37
38
|
ensure
|
38
|
-
|
39
|
-
cleanup_on_body_close
|
39
|
+
cache.unset_local_cache unless cleanup_on_body_close
|
40
40
|
end
|
41
41
|
end
|
42
42
|
end
|
data/lib/active_support/cache.rb
CHANGED
@@ -36,6 +36,7 @@ module ActiveSupport
|
|
36
36
|
:serializer,
|
37
37
|
:skip_nil,
|
38
38
|
:raw,
|
39
|
+
:max_key_size,
|
39
40
|
]
|
40
41
|
|
41
42
|
# Mapping of canonical option names to aliases that a store will recognize.
|
@@ -187,6 +188,12 @@ module ActiveSupport
|
|
187
188
|
# @last_mod_time = Time.now # Invalidate the entire cache by changing namespace
|
188
189
|
#
|
189
190
|
class Store
|
191
|
+
# Default +ConnectionPool+ options
|
192
|
+
DEFAULT_POOL_OPTIONS = { size: 5, timeout: 5 }.freeze
|
193
|
+
|
194
|
+
# Keys are truncated with the Active Support digest if they exceed the limit.
|
195
|
+
MAX_KEY_SIZE = 250
|
196
|
+
|
190
197
|
cattr_accessor :logger, instance_writer: true
|
191
198
|
cattr_accessor :raise_on_invalid_cache_expiration_time, default: false
|
192
199
|
|
@@ -195,9 +202,6 @@ module ActiveSupport
|
|
195
202
|
|
196
203
|
class << self
|
197
204
|
private
|
198
|
-
DEFAULT_POOL_OPTIONS = { size: 5, timeout: 5 }.freeze
|
199
|
-
private_constant :DEFAULT_POOL_OPTIONS
|
200
|
-
|
201
205
|
def retrieve_pool_options(options)
|
202
206
|
if options.key?(:pool)
|
203
207
|
pool_options = options.delete(:pool)
|
@@ -299,6 +303,9 @@ module ActiveSupport
|
|
299
303
|
@options[:compress] = true unless @options.key?(:compress)
|
300
304
|
@options[:compress_threshold] ||= DEFAULT_COMPRESS_LIMIT
|
301
305
|
|
306
|
+
@max_key_size = @options.delete(:max_key_size)
|
307
|
+
@max_key_size = MAX_KEY_SIZE if @max_key_size.nil? # allow 'false' as a value
|
308
|
+
|
302
309
|
@coder = @options.delete(:coder) do
|
303
310
|
legacy_serializer = Cache.format_version < 7.1 && !@options[:serializer]
|
304
311
|
serializer = @options.delete(:serializer) || default_serializer
|
@@ -557,7 +564,7 @@ module ActiveSupport
|
|
557
564
|
|
558
565
|
instrument_multi :write_multi, normalized_hash, options do |payload|
|
559
566
|
entries = hash.each_with_object({}) do |(name, value), memo|
|
560
|
-
memo[normalize_key(name, options)] = Entry.new(value, **options
|
567
|
+
memo[normalize_key(name, options)] = Entry.new(value, **options, version: normalize_version(name, options))
|
561
568
|
end
|
562
569
|
|
563
570
|
write_multi_entries entries, **options
|
@@ -659,13 +666,15 @@ module ActiveSupport
|
|
659
666
|
# version, the read will be treated as a cache miss. This feature is
|
660
667
|
# used to support recyclable cache keys.
|
661
668
|
#
|
669
|
+
# * +:unless_exist+ - Prevents overwriting an existing cache entry.
|
670
|
+
#
|
662
671
|
# Other options will be handled by the specific cache store implementation.
|
663
672
|
def write(name, value, options = nil)
|
664
673
|
options = merged_options(options)
|
665
674
|
key = normalize_key(name, options)
|
666
675
|
|
667
676
|
instrument(:write, key, options) do
|
668
|
-
entry = Entry.new(value, **options
|
677
|
+
entry = Entry.new(value, **options, version: normalize_version(name, options))
|
669
678
|
write_entry(key, entry, **options)
|
670
679
|
end
|
671
680
|
end
|
@@ -742,6 +751,32 @@ module ActiveSupport
|
|
742
751
|
raise NotImplementedError.new("#{self.class.name} does not support decrement")
|
743
752
|
end
|
744
753
|
|
754
|
+
# Reads a counter that was set by #increment / #decrement.
|
755
|
+
#
|
756
|
+
# cache.write_counter("foo", 1)
|
757
|
+
# cache.read_counter("foo") # => 1
|
758
|
+
# cache.increment("foo")
|
759
|
+
# cache.read_counter("foo") # => 2
|
760
|
+
#
|
761
|
+
# Options are passed to the underlying cache implementation.
|
762
|
+
def read_counter(name, **options)
|
763
|
+
options = merged_options(options).merge(raw: true)
|
764
|
+
read(name, **options)&.to_i
|
765
|
+
end
|
766
|
+
|
767
|
+
# Writes a counter that can then be modified by #increment / #decrement.
|
768
|
+
#
|
769
|
+
# cache.write_counter("foo", 1)
|
770
|
+
# cache.read_counter("foo") # => 1
|
771
|
+
# cache.increment("foo")
|
772
|
+
# cache.read_counter("foo") # => 2
|
773
|
+
#
|
774
|
+
# Options are passed to the underlying cache implementation.
|
775
|
+
def write_counter(name, value, **options)
|
776
|
+
options = merged_options(options).merge(raw: true)
|
777
|
+
write(name, value.to_i, **options)
|
778
|
+
end
|
779
|
+
|
745
780
|
# Cleans up the cache by removing expired entries.
|
746
781
|
#
|
747
782
|
# Options are passed to the underlying cache implementation.
|
@@ -761,6 +796,17 @@ module ActiveSupport
|
|
761
796
|
raise NotImplementedError.new("#{self.class.name} does not support clear")
|
762
797
|
end
|
763
798
|
|
799
|
+
# Get the current namespace
|
800
|
+
def namespace
|
801
|
+
@options[:namespace]
|
802
|
+
end
|
803
|
+
|
804
|
+
# Set the current namespace. Note, this will be ignored if custom
|
805
|
+
# options are passed to cache wills with a namespace key.
|
806
|
+
def namespace=(namespace)
|
807
|
+
@options[:namespace] = namespace
|
808
|
+
end
|
809
|
+
|
764
810
|
private
|
765
811
|
def default_serializer
|
766
812
|
case Cache.format_version
|
@@ -927,16 +973,33 @@ module ActiveSupport
|
|
927
973
|
options
|
928
974
|
end
|
929
975
|
|
930
|
-
# Expands and
|
976
|
+
# Expands, namespaces and truncates the cache key.
|
931
977
|
# Raises an exception when the key is +nil+ or an empty string.
|
932
978
|
# May be overridden by cache stores to do additional normalization.
|
933
979
|
def normalize_key(key, options = nil)
|
980
|
+
key = expand_and_namespace_key(key, options)
|
981
|
+
truncate_key(key)
|
982
|
+
end
|
983
|
+
|
984
|
+
def expand_and_namespace_key(key, options = nil)
|
934
985
|
str_key = expanded_key(key)
|
935
986
|
raise(ArgumentError, "key cannot be blank") if !str_key || str_key.empty?
|
936
987
|
|
937
988
|
namespace_key str_key, options
|
938
989
|
end
|
939
990
|
|
991
|
+
def truncate_key(key)
|
992
|
+
if key && @max_key_size && key.bytesize > @max_key_size
|
993
|
+
suffix = ":hash:#{ActiveSupport::Digest.hexdigest(key)}"
|
994
|
+
truncate_at = @max_key_size - suffix.bytesize
|
995
|
+
key = key.byteslice(0, truncate_at)
|
996
|
+
key.scrub!("")
|
997
|
+
"#{key}#{suffix}"
|
998
|
+
else
|
999
|
+
key
|
1000
|
+
end
|
1001
|
+
end
|
1002
|
+
|
940
1003
|
# Prefix the key with a namespace string:
|
941
1004
|
#
|
942
1005
|
# namespace_key 'foo', namespace: 'cache'
|
@@ -27,6 +27,19 @@ module ActiveSupport
|
|
27
27
|
end
|
28
28
|
|
29
29
|
module ClassMethods
|
30
|
+
# Reads and writes attributes from a configuration OrderedOptions.
|
31
|
+
#
|
32
|
+
# require "active_support/configurable"
|
33
|
+
#
|
34
|
+
# class User
|
35
|
+
# include ActiveSupport::Configurable
|
36
|
+
# end
|
37
|
+
#
|
38
|
+
# User.config.allowed_access = true
|
39
|
+
# User.config.level = 1
|
40
|
+
#
|
41
|
+
# User.config.allowed_access # => true
|
42
|
+
# User.config.level # => 1
|
30
43
|
def config
|
31
44
|
@_config ||= if respond_to?(:superclass) && superclass.respond_to?(:config)
|
32
45
|
superclass.config.inheritable_copy
|
@@ -36,6 +49,21 @@ module ActiveSupport
|
|
36
49
|
end
|
37
50
|
end
|
38
51
|
|
52
|
+
# Configure values from within the passed block.
|
53
|
+
#
|
54
|
+
# require "active_support/configurable"
|
55
|
+
#
|
56
|
+
# class User
|
57
|
+
# include ActiveSupport::Configurable
|
58
|
+
# end
|
59
|
+
#
|
60
|
+
# User.allowed_access # => nil
|
61
|
+
#
|
62
|
+
# User.configure do |config|
|
63
|
+
# config.allowed_access = true
|
64
|
+
# end
|
65
|
+
#
|
66
|
+
# User.allowed_access # => true
|
39
67
|
def configure
|
40
68
|
yield config
|
41
69
|
end
|
@@ -0,0 +1,145 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveSupport
|
4
|
+
# Provides a DSL for declaring a continuous integration workflow that can be run either locally or in the cloud.
|
5
|
+
# Each step is timed, reports success/error, and is aggregated into a collective report that reports total runtime,
|
6
|
+
# as well as whether the entire run was successful or not.
|
7
|
+
#
|
8
|
+
# Example:
|
9
|
+
#
|
10
|
+
# ActiveSupport::ContinuousIntegration.run do
|
11
|
+
# step "Setup", "bin/setup --skip-server"
|
12
|
+
# step "Style: Ruby", "bin/rubocop"
|
13
|
+
# step "Security: Gem audit", "bin/bundler-audit"
|
14
|
+
# step "Tests: Rails", "bin/rails test test:system"
|
15
|
+
#
|
16
|
+
# if success?
|
17
|
+
# step "Signoff: Ready for merge and deploy", "gh signoff"
|
18
|
+
# else
|
19
|
+
# failure "Skipping signoff; CI failed.", "Fix the issues and try again."
|
20
|
+
# end
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# Starting with Rails 8.1, a default `bin/ci` and `config/ci.rb` file are created to provide out-of-the-box CI.
|
24
|
+
class ContinuousIntegration
|
25
|
+
COLORS = {
|
26
|
+
banner: "\033[1;32m", # Green
|
27
|
+
title: "\033[1;35m", # Purple
|
28
|
+
subtitle: "\033[1;90m", # Medium Gray
|
29
|
+
error: "\033[1;31m", # Red
|
30
|
+
success: "\033[1;32m" # Green
|
31
|
+
}
|
32
|
+
|
33
|
+
attr_reader :results
|
34
|
+
|
35
|
+
# Perform a CI run. Execute each step, show their results and runtime, and exit with a non-zero status if there are any failures.
|
36
|
+
#
|
37
|
+
# Pass an optional title, subtitle, and a block that declares the steps to be executed.
|
38
|
+
#
|
39
|
+
# Sets the CI environment variable to "true" to allow for conditional behavior in the app, like enabling eager loading and disabling logging.
|
40
|
+
#
|
41
|
+
# Example:
|
42
|
+
#
|
43
|
+
# ActiveSupport::ContinuousIntegration.run do
|
44
|
+
# step "Setup", "bin/setup --skip-server"
|
45
|
+
# step "Style: Ruby", "bin/rubocop"
|
46
|
+
# step "Security: Gem audit", "bin/bundler-audit"
|
47
|
+
# step "Tests: Rails", "bin/rails test test:system"
|
48
|
+
#
|
49
|
+
# if success?
|
50
|
+
# step "Signoff: Ready for merge and deploy", "gh signoff"
|
51
|
+
# else
|
52
|
+
# failure "Skipping signoff; CI failed.", "Fix the issues and try again."
|
53
|
+
# end
|
54
|
+
# end
|
55
|
+
def self.run(title = "Continuous Integration", subtitle = "Running tests, style checks, and security audits", &block)
|
56
|
+
new.tap do |ci|
|
57
|
+
ENV["CI"] = "true"
|
58
|
+
ci.heading title, subtitle, padding: false
|
59
|
+
ci.report(title, &block)
|
60
|
+
abort unless ci.success?
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def initialize
|
65
|
+
@results = []
|
66
|
+
end
|
67
|
+
|
68
|
+
# Declare a step with a title and a command. The command can either be given as a single string or as multiple
|
69
|
+
# strings that will be passed to `system` as individual arguments (and therefore correctly escaped for paths etc).
|
70
|
+
#
|
71
|
+
# Examples:
|
72
|
+
#
|
73
|
+
# step "Setup", "bin/setup"
|
74
|
+
# step "Single test", "bin/rails", "test", "--name", "test_that_is_one"
|
75
|
+
def step(title, *command)
|
76
|
+
heading title, command.join(" "), type: :title
|
77
|
+
report(title) { results << system(*command) }
|
78
|
+
end
|
79
|
+
|
80
|
+
# Returns true if all steps were successful.
|
81
|
+
def success?
|
82
|
+
results.all?
|
83
|
+
end
|
84
|
+
|
85
|
+
# Display an error heading with the title and optional subtitle to reflect that the run failed.
|
86
|
+
def failure(title, subtitle = nil)
|
87
|
+
heading title, subtitle, type: :error
|
88
|
+
end
|
89
|
+
|
90
|
+
# Display a colorized heading followed by an optional subtitle.
|
91
|
+
#
|
92
|
+
# Examples:
|
93
|
+
#
|
94
|
+
# heading "Smoke Testing", "End-to-end tests verifying key functionality", padding: false
|
95
|
+
# heading "Skipping video encoding tests", "Install FFmpeg to run these tests", type: :error
|
96
|
+
#
|
97
|
+
# See ActiveSupport::ContinuousIntegration::COLORS for a complete list of options.
|
98
|
+
def heading(heading, subtitle = nil, type: :banner, padding: true)
|
99
|
+
echo "#{padding ? "\n\n" : ""}#{heading}", type: type
|
100
|
+
echo "#{subtitle}#{padding ? "\n" : ""}", type: :subtitle if subtitle
|
101
|
+
end
|
102
|
+
|
103
|
+
# Echo text to the terminal in the color corresponding to the type of the text.
|
104
|
+
#
|
105
|
+
# Examples:
|
106
|
+
#
|
107
|
+
# echo "This is going to be green!", type: :success
|
108
|
+
# echo "This is going to be red!", type: :error
|
109
|
+
#
|
110
|
+
# See ActiveSupport::ContinuousIntegration::COLORS for a complete list of options.
|
111
|
+
def echo(text, type:)
|
112
|
+
puts colorize(text, type)
|
113
|
+
end
|
114
|
+
|
115
|
+
# :nodoc:
|
116
|
+
def report(title, &block)
|
117
|
+
Signal.trap("INT") { abort colorize(:error, "\n❌ #{title} interrupted") }
|
118
|
+
|
119
|
+
ci = self.class.new
|
120
|
+
elapsed = timing { ci.instance_eval(&block) }
|
121
|
+
|
122
|
+
if ci.success?
|
123
|
+
echo "\n✅ #{title} passed in #{elapsed}", type: :success
|
124
|
+
else
|
125
|
+
echo "\n❌ #{title} failed in #{elapsed}", type: :error
|
126
|
+
end
|
127
|
+
|
128
|
+
results.concat ci.results
|
129
|
+
ensure
|
130
|
+
Signal.trap("INT", "-")
|
131
|
+
end
|
132
|
+
|
133
|
+
private
|
134
|
+
def timing
|
135
|
+
started_at = Time.now.to_f
|
136
|
+
yield
|
137
|
+
min, sec = (Time.now.to_f - started_at).divmod(60)
|
138
|
+
"#{"#{min}m" if min > 0}%.2fs" % sec
|
139
|
+
end
|
140
|
+
|
141
|
+
def colorize(text, type)
|
142
|
+
"#{COLORS.fetch(type)}#{text}\033[0m"
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
@@ -17,7 +17,7 @@ module DateAndTime
|
|
17
17
|
singleton_class.silence_redefinition_of_method :preserve_timezone
|
18
18
|
|
19
19
|
#--
|
20
|
-
# This re-implements the
|
20
|
+
# This re-implements the behavior of the mattr_reader, instead
|
21
21
|
# of prepending on to it, to avoid overcomplicating a module that
|
22
22
|
# is in turn included in several places. This will all go away in
|
23
23
|
# Rails 8.0 anyway.
|
@@ -12,7 +12,7 @@ module ActiveSupport
|
|
12
12
|
if s.html_safe?
|
13
13
|
s
|
14
14
|
else
|
15
|
-
super(
|
15
|
+
super(s)
|
16
16
|
end
|
17
17
|
end
|
18
18
|
alias :unwrapped_html_escape :html_escape # :nodoc:
|
@@ -61,7 +61,7 @@ class ERB
|
|
61
61
|
# html_escape_once('<< Accept & Checkout')
|
62
62
|
# # => "<< Accept & Checkout"
|
63
63
|
def html_escape_once(s)
|
64
|
-
|
64
|
+
s.to_s.gsub(HTML_ESCAPE_ONCE_REGEXP, HTML_ESCAPE).html_safe
|
65
65
|
end
|
66
66
|
|
67
67
|
module_function :html_escape_once
|
@@ -169,7 +169,7 @@ class ERB
|
|
169
169
|
while !source.eos?
|
170
170
|
pos = source.pos
|
171
171
|
source.scan_until(/(?:#{start_re}|#{finish_re})/)
|
172
|
-
|
172
|
+
return [[:PLAIN, source.string]] unless source.matched?
|
173
173
|
len = source.pos - source.matched.bytesize - pos
|
174
174
|
|
175
175
|
case source.matched
|