diff --git a/lib/json/common.rb b/lib/json/common.rb index 82d334061..1c5d4c22e 100644 --- a/lib/json/common.rb +++ b/lib/json/common.rb @@ -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 } diff --git a/test/json/json_generator_test.rb b/test/json/json_generator_test.rb index 1b2197735..e86fe6a2e 100755 --- a/test/json/json_generator_test.rb +++ b/test/json/json_generator_test.rb @@ -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: "", object_nl: "\n", array_nl: "") json = generate({1=>{2=>3,4=>[5,6]}}, state) diff --git a/test/json/ractor_test.rb b/test/json/ractor_test.rb index 9e28f13c1..a501344fa 100644 --- a/test/json/ractor_test.rb +++ b/test/json/ractor_test.rb @@ -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)