Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions lib/json/common.rb
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,24 @@ def parser=(parser) # :nodoc:
def generator=(generator) # :nodoc:
old, $VERBOSE = $VERBOSE, nil

unless generator::State.respond_to?(:default_sort_keys_proc_unchecked=, true)
generator::State.singleton_class.class_eval do
alias_method :default_sort_keys_proc_unchecked=, :default_sort_keys_proc=
private :default_sort_keys_proc_unchecked=

def default_sort_keys_proc=(proc)
unless ::Proc === proc
raise ::TypeError, "sort_key_proc must be a Proc"
end
if defined?(::Ractor) && !::Ractor.shareable?(proc) && !::Ractor.current.equal?(::Ractor.main)
raise ::Ractor::IsolationError,
"can not set a non-shareable Proc as the default sort_keys proc from a non-main Ractor"
end
self.default_sort_keys_proc_unchecked = proc
end
end
end

# The default proc used when the +sort_keys+ generation option is +true+.
# It returns a new hash with the entries sorted by their keys.
sort_keys_proc = ->(hash) { hash.sort.to_h }
Expand Down
12 changes: 12 additions & 0 deletions test/json/json_generator_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,18 @@ def test_generate_sort_keys_with_proc
assert_instance_of Proc, state.sort_keys
end

def test_default_sort_keys_proc_setter_survives_generator_reassignment
omit "fork not supported" unless Process.respond_to?(:fork)
pid = fork do
JSON.generator = JSON::Ext::Generator
JSON.generator = JSON::Ext::Generator
JSON::State.default_sort_keys_proc = ->(hash) { hash.sort.to_h }
exit!(JSON.generate({b: 1, a: 2}, sort_keys: true) == '{"a":2,"b":1}' ? 0 : 1)
end
_, status = Process.wait2(pid)
assert_predicate status, :success?
end

def test_generate_custom
state = State.new(space_before: " ", space: " ", indent: "<i>", object_nl: "\n", array_nl: "<a_nl>")
json = generate({1=>{2=>3,4=>[5,6]}}, state)
Expand Down
29 changes: 29 additions & 0 deletions test/json/ractor_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -123,4 +123,33 @@ def test_coder_proc
_, status = Process.waitpid2(pid)
assert_predicate status, :success?
end if Ractor.respond_to?(:shareable_proc)

def test_default_sort_keys_proc_ractor_safety
pid = fork do
Warning[:experimental] = false
results = Ractor.new do
outcomes = []

begin
JSON::State.default_sort_keys_proc = ->(hash) { hash.sort.to_h }
outcomes << :accepted
rescue Ractor::IsolationError
outcomes << :rejected
end

# A shareable Proc is allowed from any Ractor.
JSON::State.default_sort_keys_proc = Ractor.shareable_lambda { |hash| hash.sort.reverse.to_h }
outcomes << JSON.generate({b: 1, a: 2}, sort_keys: true)
outcomes
end.value

# The main Ractor owns the slot, so it may install any Proc.
JSON::State.default_sort_keys_proc = ->(hash) { hash.sort.to_h }
main_result = JSON.generate({b: 1, a: 2}, sort_keys: true)

exit(results == [:rejected, '{"b":1,"a":2}'] && main_result == '{"a":2,"b":1}' ? 0 : 1)
end
_, status = Process.waitpid2(pid)
assert_predicate status, :success?
end if Ractor.respond_to?(:shareable_lambda)
end if defined?(Ractor) && Process.respond_to?(:fork)
Loading