activesupport 8.0.4 → 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 +233 -198
- 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/class/attribute.rb +6 -8
- data/lib/active_support/core_ext/date_and_time/compatibility.rb +1 -1
- data/lib/active_support/core_ext/enumerable.rb +2 -2
- 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/notifications/instrumenter.rb +1 -1
- 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/server.rb +2 -15
- data/lib/active_support/testing/parallelization/worker.rb +4 -2
- data/lib/active_support/testing/parallelization.rb +14 -12
- 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 -4
- data/lib/active_support.rb +11 -0
- metadata +10 -5
- data/lib/active_support/core_ext/range/each.rb +0 -24
|
@@ -2,8 +2,41 @@
|
|
|
2
2
|
|
|
3
3
|
module ActiveSupport
|
|
4
4
|
module ExecutionContext # :nodoc:
|
|
5
|
+
class Record
|
|
6
|
+
attr_reader :store, :current_attributes_instances
|
|
7
|
+
|
|
8
|
+
def initialize
|
|
9
|
+
@store = {}
|
|
10
|
+
@current_attributes_instances = {}
|
|
11
|
+
@stack = []
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def push
|
|
15
|
+
@stack << @store << @current_attributes_instances
|
|
16
|
+
@store = {}
|
|
17
|
+
@current_attributes_instances = {}
|
|
18
|
+
self
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def pop
|
|
22
|
+
@current_attributes_instances = @stack.pop
|
|
23
|
+
@store = @stack.pop
|
|
24
|
+
self
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
5
28
|
@after_change_callbacks = []
|
|
29
|
+
|
|
30
|
+
# Execution context nesting should only legitimately happen during test
|
|
31
|
+
# because the test case itself is wrapped in an executor, and it might call
|
|
32
|
+
# into a controller or job which should be executed with their own fresh context.
|
|
33
|
+
# However in production this should never happen, and for extra safety we make sure to
|
|
34
|
+
# fully clear the state at the end of the request or job cycle.
|
|
35
|
+
@nestable = false
|
|
36
|
+
|
|
6
37
|
class << self
|
|
38
|
+
attr_accessor :nestable
|
|
39
|
+
|
|
7
40
|
def after_change(&block)
|
|
8
41
|
@after_change_callbacks << block
|
|
9
42
|
end
|
|
@@ -14,9 +47,11 @@ module ActiveSupport
|
|
|
14
47
|
options.symbolize_keys!
|
|
15
48
|
keys = options.keys
|
|
16
49
|
|
|
17
|
-
store =
|
|
50
|
+
store = record.store
|
|
18
51
|
|
|
19
|
-
previous_context =
|
|
52
|
+
previous_context = if block_given?
|
|
53
|
+
keys.zip(store.values_at(*keys)).to_h
|
|
54
|
+
end
|
|
20
55
|
|
|
21
56
|
store.merge!(options)
|
|
22
57
|
@after_change_callbacks.each(&:call)
|
|
@@ -32,21 +67,43 @@ module ActiveSupport
|
|
|
32
67
|
end
|
|
33
68
|
|
|
34
69
|
def []=(key, value)
|
|
35
|
-
store[key.to_sym] = value
|
|
70
|
+
record.store[key.to_sym] = value
|
|
36
71
|
@after_change_callbacks.each(&:call)
|
|
37
72
|
end
|
|
38
73
|
|
|
39
74
|
def to_h
|
|
40
|
-
store.dup
|
|
75
|
+
record.store.dup
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def push
|
|
79
|
+
if @nestable
|
|
80
|
+
record.push
|
|
81
|
+
else
|
|
82
|
+
clear
|
|
83
|
+
end
|
|
84
|
+
self
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def pop
|
|
88
|
+
if @nestable
|
|
89
|
+
record.pop
|
|
90
|
+
else
|
|
91
|
+
clear
|
|
92
|
+
end
|
|
93
|
+
self
|
|
41
94
|
end
|
|
42
95
|
|
|
43
96
|
def clear
|
|
44
|
-
|
|
97
|
+
IsolatedExecutionState[:active_support_execution_context] = nil
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def current_attributes_instances
|
|
101
|
+
record.current_attributes_instances
|
|
45
102
|
end
|
|
46
103
|
|
|
47
104
|
private
|
|
48
|
-
def
|
|
49
|
-
IsolatedExecutionState[:active_support_execution_context] ||=
|
|
105
|
+
def record
|
|
106
|
+
IsolatedExecutionState[:active_support_execution_context] ||= Record.new
|
|
50
107
|
end
|
|
51
108
|
end
|
|
52
109
|
end
|
|
@@ -46,8 +46,11 @@ module ActiveSupport
|
|
|
46
46
|
raise ArgumentError, "A block is required to initialize a FileUpdateChecker"
|
|
47
47
|
end
|
|
48
48
|
|
|
49
|
-
|
|
50
|
-
@
|
|
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[
|
|
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.
|
|
126
|
+
time_now = Time.now
|
|
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
|
-
|
|
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)
|
data/lib/active_support/gzip.rb
CHANGED
|
@@ -68,15 +68,15 @@ module ActiveSupport
|
|
|
68
68
|
end
|
|
69
69
|
|
|
70
70
|
def initialize(constructor = nil)
|
|
71
|
-
if constructor.
|
|
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
|
-
|
|
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
|
#
|
|
@@ -279,13 +295,13 @@ module ActiveSupport
|
|
|
279
295
|
# hash['a'] = nil
|
|
280
296
|
# hash.reverse_merge(a: 0, b: 1) # => {"a"=>nil, "b"=>1}
|
|
281
297
|
def reverse_merge(other_hash)
|
|
282
|
-
super(
|
|
298
|
+
super(cast(other_hash))
|
|
283
299
|
end
|
|
284
300
|
alias_method :with_defaults, :reverse_merge
|
|
285
301
|
|
|
286
302
|
# Same semantics as +reverse_merge+ but modifies the receiver in-place.
|
|
287
303
|
def reverse_merge!(other_hash)
|
|
288
|
-
super(
|
|
304
|
+
super(cast(other_hash))
|
|
289
305
|
end
|
|
290
306
|
alias_method :with_defaults!, :reverse_merge!
|
|
291
307
|
|
|
@@ -294,7 +310,7 @@ module ActiveSupport
|
|
|
294
310
|
# h = { "a" => 100, "b" => 200 }
|
|
295
311
|
# h.replace({ "c" => 300, "d" => 400 }) # => {"c"=>300, "d"=>400}
|
|
296
312
|
def replace(other_hash)
|
|
297
|
-
super(
|
|
313
|
+
super(cast(other_hash))
|
|
298
314
|
end
|
|
299
315
|
|
|
300
316
|
# Removes the specified key from the hash.
|
|
@@ -387,6 +403,10 @@ module ActiveSupport
|
|
|
387
403
|
end
|
|
388
404
|
|
|
389
405
|
private
|
|
406
|
+
def cast(other)
|
|
407
|
+
self.class === other ? other : self.class.new(other)
|
|
408
|
+
end
|
|
409
|
+
|
|
390
410
|
def convert_key(key)
|
|
391
411
|
Symbol === key ? key.name : key
|
|
392
412
|
end
|
|
@@ -66,8 +66,7 @@ module I18n
|
|
|
66
66
|
|
|
67
67
|
if app.config.reloading_enabled?
|
|
68
68
|
directories = watched_dirs_with_extensions(reloadable_paths)
|
|
69
|
-
|
|
70
|
-
reloader = app.config.file_watcher.new(root_load_paths, directories) do
|
|
69
|
+
reloader = app.config.file_watcher.new(I18n.load_path, directories) do
|
|
71
70
|
I18n.load_path.delete_if { |path| path.to_s.start_with?(Rails.root.to_s) && !File.exist?(path) }
|
|
72
71
|
I18n.load_path |= reloadable_paths.flat_map(&:existent)
|
|
73
72
|
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
|
-
@
|
|
36
|
-
|
|
41
|
+
@members = []
|
|
42
|
+
@pattern = nil
|
|
37
43
|
end
|
|
38
44
|
|
|
39
45
|
def delete(entry)
|
|
40
|
-
|
|
41
|
-
@
|
|
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
|
|
45
|
-
|
|
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
|
-
@
|
|
63
|
+
@members.concat(words)
|
|
64
|
+
@pattern = nil
|
|
52
65
|
self
|
|
53
66
|
end
|
|
54
67
|
|
|
55
68
|
def uncountable?(str)
|
|
56
|
-
@
|
|
57
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
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.
|
|
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,28 +28,27 @@ 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
|
|
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
|
-
|
|
43
|
+
@scope.current.active_support_execution_state&.key?(key)
|
|
45
44
|
end
|
|
46
45
|
|
|
47
46
|
def delete(key)
|
|
48
|
-
|
|
47
|
+
@scope.current.active_support_execution_state&.delete(key)
|
|
49
48
|
end
|
|
50
49
|
|
|
51
50
|
def clear
|
|
52
|
-
|
|
51
|
+
@scope.current.active_support_execution_state&.clear
|
|
53
52
|
end
|
|
54
53
|
|
|
55
54
|
def context
|
|
@@ -62,11 +61,6 @@ module ActiveSupport
|
|
|
62
61
|
# and streaming should be rethought.
|
|
63
62
|
context.active_support_execution_state = other.active_support_execution_state.dup
|
|
64
63
|
end
|
|
65
|
-
|
|
66
|
-
private
|
|
67
|
-
def state
|
|
68
|
-
context.active_support_execution_state ||= {}
|
|
69
|
-
end
|
|
70
64
|
end
|
|
71
65
|
|
|
72
66
|
self.isolation_level = :thread
|
|
@@ -21,8 +21,8 @@ module ActiveSupport
|
|
|
21
21
|
# # => {"team" => "rails", "players" => "36"}
|
|
22
22
|
# ActiveSupport::JSON.decode("2.39")
|
|
23
23
|
# # => 2.39
|
|
24
|
-
def decode(json)
|
|
25
|
-
data = ::JSON.parse(json,
|
|
24
|
+
def decode(json, options = {})
|
|
25
|
+
data = ::JSON.parse(json, options)
|
|
26
26
|
|
|
27
27
|
if ActiveSupport.parse_json_times
|
|
28
28
|
convert_dates_from(data)
|
|
@@ -20,8 +20,8 @@ module ActiveSupport
|
|
|
20
20
|
# ActiveSupport::JSON.encode({ team: 'rails', players: '36' })
|
|
21
21
|
# # => "{\"team\":\"rails\",\"players\":\"36\"}"
|
|
22
22
|
#
|
|
23
|
-
#
|
|
24
|
-
# U+2028 (Line Separator) and U+2029 (Paragraph Separator):
|
|
23
|
+
# By default, it generates JSON that is safe to include in JavaScript, as
|
|
24
|
+
# it escapes U+2028 (Line Separator) and U+2029 (Paragraph Separator):
|
|
25
25
|
#
|
|
26
26
|
# ActiveSupport::JSON.encode({ key: "\u2028" })
|
|
27
27
|
# # => "{\"key\":\"\\u2028\"}"
|
|
@@ -32,18 +32,42 @@ module ActiveSupport
|
|
|
32
32
|
# ActiveSupport::JSON.encode({ key: "<>&" })
|
|
33
33
|
# # => "{\"key\":\"\\u003c\\u003e\\u0026\"}"
|
|
34
34
|
#
|
|
35
|
-
# This can be changed with the +escape_html_entities+ option, or the
|
|
35
|
+
# This behavior can be changed with the +escape_html_entities+ option, or the
|
|
36
36
|
# global escape_html_entities_in_json configuration option.
|
|
37
37
|
#
|
|
38
38
|
# ActiveSupport::JSON.encode({ key: "<>&" }, escape_html_entities: false)
|
|
39
39
|
# # => "{\"key\":\"<>&\"}"
|
|
40
|
+
#
|
|
41
|
+
# For performance reasons, you can set the +escape+ option to false,
|
|
42
|
+
# which will skip all escaping:
|
|
43
|
+
#
|
|
44
|
+
# ActiveSupport::JSON.encode({ key: "\u2028<>&" }, escape: false)
|
|
45
|
+
# # => "{\"key\":\"\u2028<>&\"}"
|
|
40
46
|
def encode(value, options = nil)
|
|
41
|
-
|
|
47
|
+
if options.nil? || options.empty?
|
|
48
|
+
Encoding.encode_without_options(value)
|
|
49
|
+
else
|
|
50
|
+
Encoding.json_encoder.new(options).encode(value)
|
|
51
|
+
end
|
|
42
52
|
end
|
|
43
53
|
alias_method :dump, :encode
|
|
44
54
|
end
|
|
45
55
|
|
|
46
56
|
module Encoding # :nodoc:
|
|
57
|
+
U2028 = -"\u2028".b
|
|
58
|
+
U2029 = -"\u2029".b
|
|
59
|
+
|
|
60
|
+
ESCAPED_CHARS = {
|
|
61
|
+
U2028 => '\u2028'.b,
|
|
62
|
+
U2029 => '\u2029'.b,
|
|
63
|
+
">".b => '\u003e'.b,
|
|
64
|
+
"<".b => '\u003c'.b,
|
|
65
|
+
"&".b => '\u0026'.b,
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
ESCAPE_REGEX_WITH_HTML_ENTITIES = Regexp.union(*ESCAPED_CHARS.keys)
|
|
69
|
+
ESCAPE_REGEX_WITHOUT_HTML_ENTITIES = Regexp.union(U2028, U2029)
|
|
70
|
+
|
|
47
71
|
class JSONGemEncoder # :nodoc:
|
|
48
72
|
attr_reader :options
|
|
49
73
|
|
|
@@ -58,17 +82,18 @@ module ActiveSupport
|
|
|
58
82
|
end
|
|
59
83
|
json = stringify(jsonify(value))
|
|
60
84
|
|
|
85
|
+
return json unless @options.fetch(:escape, true)
|
|
86
|
+
|
|
61
87
|
# Rails does more escaping than the JSON gem natively does (we
|
|
62
88
|
# escape \u2028 and \u2029 and optionally >, <, & to work around
|
|
63
89
|
# certain browser problems).
|
|
90
|
+
json.force_encoding(::Encoding::BINARY)
|
|
64
91
|
if @options.fetch(:escape_html_entities, Encoding.escape_html_entities_in_json)
|
|
65
|
-
json.gsub!(
|
|
66
|
-
|
|
67
|
-
json.gsub!(
|
|
92
|
+
json.gsub!(ESCAPE_REGEX_WITH_HTML_ENTITIES, ESCAPED_CHARS)
|
|
93
|
+
else
|
|
94
|
+
json.gsub!(ESCAPE_REGEX_WITHOUT_HTML_ENTITIES, ESCAPED_CHARS)
|
|
68
95
|
end
|
|
69
|
-
json.
|
|
70
|
-
json.gsub!("\u2029", '\u2029')
|
|
71
|
-
json
|
|
96
|
+
json.force_encoding(::Encoding::UTF_8)
|
|
72
97
|
end
|
|
73
98
|
|
|
74
99
|
private
|
|
@@ -101,16 +126,66 @@ module ActiveSupport
|
|
|
101
126
|
when Array
|
|
102
127
|
value.map { |v| jsonify(v) }
|
|
103
128
|
else
|
|
104
|
-
|
|
129
|
+
if defined?(::JSON::Fragment) && ::JSON::Fragment === value
|
|
130
|
+
value
|
|
131
|
+
else
|
|
132
|
+
jsonify value.as_json
|
|
133
|
+
end
|
|
105
134
|
end
|
|
106
135
|
end
|
|
107
136
|
|
|
108
137
|
# Encode a "jsonified" Ruby data structure using the JSON gem
|
|
109
138
|
def stringify(jsonified)
|
|
110
|
-
::JSON.generate(jsonified
|
|
139
|
+
::JSON.generate(jsonified)
|
|
111
140
|
end
|
|
112
141
|
end
|
|
113
142
|
|
|
143
|
+
if defined?(::JSON::Coder)
|
|
144
|
+
class JSONGemCoderEncoder # :nodoc:
|
|
145
|
+
JSON_NATIVE_TYPES = [Hash, Array, Float, String, Symbol, Integer, NilClass, TrueClass, FalseClass, ::JSON::Fragment].freeze
|
|
146
|
+
CODER = ::JSON::Coder.new do |value|
|
|
147
|
+
json_value = value.as_json
|
|
148
|
+
# Handle objects returning self from as_json
|
|
149
|
+
if json_value.equal?(value)
|
|
150
|
+
next ::JSON::Fragment.new(::JSON.generate(json_value))
|
|
151
|
+
end
|
|
152
|
+
# Handle objects not returning JSON-native types from as_json
|
|
153
|
+
count = 5
|
|
154
|
+
until JSON_NATIVE_TYPES.include?(json_value.class)
|
|
155
|
+
raise SystemStackError if count == 0
|
|
156
|
+
json_value = json_value.as_json
|
|
157
|
+
count -= 1
|
|
158
|
+
end
|
|
159
|
+
json_value
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def initialize(options = nil)
|
|
164
|
+
@options = options ? options.dup.freeze : {}.freeze
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Encode the given object into a JSON string
|
|
168
|
+
def encode(value)
|
|
169
|
+
value = value.as_json(@options) unless @options.empty?
|
|
170
|
+
|
|
171
|
+
json = CODER.dump(value)
|
|
172
|
+
|
|
173
|
+
return json unless @options.fetch(:escape, true)
|
|
174
|
+
|
|
175
|
+
# Rails does more escaping than the JSON gem natively does (we
|
|
176
|
+
# escape \u2028 and \u2029 and optionally >, <, & to work around
|
|
177
|
+
# certain browser problems).
|
|
178
|
+
json.force_encoding(::Encoding::BINARY)
|
|
179
|
+
if @options.fetch(:escape_html_entities, Encoding.escape_html_entities_in_json)
|
|
180
|
+
json.gsub!(ESCAPE_REGEX_WITH_HTML_ENTITIES, ESCAPED_CHARS)
|
|
181
|
+
else
|
|
182
|
+
json.gsub!(ESCAPE_REGEX_WITHOUT_HTML_ENTITIES, ESCAPED_CHARS)
|
|
183
|
+
end
|
|
184
|
+
json.force_encoding(::Encoding::UTF_8)
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
114
189
|
class << self
|
|
115
190
|
# If true, use ISO 8601 format for dates and times. Otherwise, fall back
|
|
116
191
|
# to the Active Support legacy format.
|
|
@@ -126,12 +201,26 @@ module ActiveSupport
|
|
|
126
201
|
|
|
127
202
|
# Sets the encoder used by \Rails to encode Ruby objects into JSON strings
|
|
128
203
|
# in +Object#to_json+ and +ActiveSupport::JSON.encode+.
|
|
129
|
-
|
|
204
|
+
attr_reader :json_encoder
|
|
205
|
+
|
|
206
|
+
def json_encoder=(encoder)
|
|
207
|
+
@json_encoder = encoder
|
|
208
|
+
@encoder_without_options = encoder.new
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def encode_without_options(value) # :nodoc:
|
|
212
|
+
@encoder_without_options.encode(value)
|
|
213
|
+
end
|
|
130
214
|
end
|
|
131
215
|
|
|
132
216
|
self.use_standard_json_time_format = true
|
|
133
217
|
self.escape_html_entities_in_json = true
|
|
134
|
-
self.json_encoder =
|
|
218
|
+
self.json_encoder =
|
|
219
|
+
if defined?(::JSON::Coder)
|
|
220
|
+
JSONGemCoderEncoder
|
|
221
|
+
else
|
|
222
|
+
JSONGemEncoder
|
|
223
|
+
end
|
|
135
224
|
self.time_precision = 3
|
|
136
225
|
end
|
|
137
226
|
end
|