From 266f1f246e2947d9f201bd79d2696fdd2c3ce4a2 Mon Sep 17 00:00:00 2001 From: Burdette Lamar Date: Thu, 2 Jul 2026 01:52:05 -0500 Subject: [PATCH 1/9] [DOC] Update Set#compare_by_identity{,?} documentation Co-authored-by: Jeremy Evans --- set.c | 45 ++++++++++++++++++++++++++++++++++++--------- 1 file changed, 36 insertions(+), 9 deletions(-) diff --git a/set.c b/set.c index 75b7708043b425..ca1380710a920c 100644 --- a/set.c +++ b/set.c @@ -1312,7 +1312,27 @@ set_reset_table_with_type(VALUE set, const struct st_hash_type *type) * call-seq: * compare_by_identity -> self * - * Makes the set compare its elements by their identity and returns self. + * Sets +self+ to compare by object identity + * (rather than by object content, which is the initial setting); + * returns +self+: + * + * set = Set.new + * set.compare_by_identity + * str = +"foo" + * set.add(str) + * # => Set["foo"] + * set.include?(str) + * # => true + * set.add(str) + * # => Set["foo"]) + * set.include?(+"foo") + * # => false + * set.add(+"foo") + * # => Set["foo", "foo"]) + * + * Once set, the compare-by-identity property may not be unset. + * + * Related: #compare_by_identity?. */ static VALUE set_i_compare_by_identity(VALUE set) @@ -1330,8 +1350,16 @@ set_i_compare_by_identity(VALUE set) * call-seq: * compare_by_identity? -> true or false * - * Returns true if the set will compare its elements by their - * identity. Also see Set#compare_by_identity. + * Returns whether +self+ compares elements by object identity + * (rather than by content): + * + * set = Set[] + * set.compare_by_identity? # => false + * set.compare_by_identity + * set.compare_by_identity? # => true + * + * Related: #compare_by_identity; + * see also {Methods for Querying}[rdoc-ref:Set@Methods+for+Querying]. */ static VALUE set_i_compare_by_identity_p(VALUE set) @@ -2352,12 +2380,12 @@ rb_set_size(VALUE set) * or greater than a given object. * - #==: Returns whether +self+ and a given enumerable are equal, * as determined by Object#eql?. - * - #compare_by_identity?: - * Returns whether the set considers only identity - * when comparing elements. * * === Methods for Querying * + * - #compare_by_identity?: + * Returns whether the set considers only identity + * when comparing elements. * - #length (aliased as #size): * Returns the count of elements. * - #empty?: @@ -2378,9 +2406,6 @@ rb_set_size(VALUE set) * - #intersect?: * Returns +true+ if the set and a given enumerable: * have any common elements, +false+ otherwise. - * - #compare_by_identity?: - * Returns whether the set considers only identity - * when comparing elements. * * === Methods for Assigning * @@ -2447,6 +2472,8 @@ rb_set_size(VALUE set) * * === Other Methods * + * - #compare_by_identity: + * Sets +self+ to compare by object identity (rather than by object content). * - #reset: * Resets the internal state; useful if an element * has been modified while an element in the set. From 426899ea649aba7f0ca75dc68936217dc0070cd2 Mon Sep 17 00:00:00 2001 From: Burdette Lamar Date: Thu, 2 Jul 2026 01:52:49 -0500 Subject: [PATCH 2/9] [DOC] Update Set#select! documentation --- set.c | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/set.c b/set.c index ca1380710a920c..af68200d93142b 100644 --- a/set.c +++ b/set.c @@ -1639,11 +1639,19 @@ set_i_keep_if(VALUE set) /* * call-seq: - * select! { |o| ... } -> self + * select! {|element| ... } -> self or nil * select! -> enumerator * - * Equivalent to Set#keep_if, but returns nil if no changes were made. - * Returns an enumerator if no block is given. + * With a block given, like #keep_if, but returns +nil+ if no changes were made: + * + * set = Set[*0..9] # => Set[0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + * set.select! {|i| i.even? } # => Set[0, 2, 4, 6, 8] + * set.select! {|i| i.even? } # => nil + * set.select! {|i| i.odd? } # => Set[] + * + * With no block given, returns an Enumerator. + * + * Related: see {Methods for Deleting}[rdoc-ref:Set@Methods+for+Deleting]. */ static VALUE set_i_select(VALUE set) From e4a403ebd7013a22e4fc57e83e088be4db57c478 Mon Sep 17 00:00:00 2001 From: Burdette Lamar Date: Thu, 2 Jul 2026 01:55:14 -0500 Subject: [PATCH 3/9] [DOC] Update Set#subset? documentation --- set.c | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/set.c b/set.c index af68200d93142b..7dbbdeb26bf938 100644 --- a/set.c +++ b/set.c @@ -1883,9 +1883,17 @@ set_i_proper_subset(VALUE set, VALUE other) /* * call-seq: - * subset?(set) -> true or false + * subset?(other_set) -> true or false * - * Returns true if the set is a subset of the given set. + * Returns whether +self+ is a {subset}[https://en.wikipedia.org/wiki/Subset] + * of the given +other_set+: + * + * set = Set[*'b'..'e'] + * set.subset?(set) # => true + * set.subset?(Set[*'a'..'f']) # => true + * set.subset?(Set[*'c'..'e']) # => false + * + * Related: {Methods for Querying}[rdoc-ref:Set@Methods+for+Querying]. */ static VALUE set_i_subset(VALUE set, VALUE other) From 722e44f68e9d35c7b44a88ac71f9d88682e8d61d Mon Sep 17 00:00:00 2001 From: Burdette Lamar Date: Thu, 2 Jul 2026 01:56:20 -0500 Subject: [PATCH 4/9] [DOC] Update Set#subtract documentation --- set.c | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/set.c b/set.c index 7dbbdeb26bf938..dbe9dbcfaf549c 100644 --- a/set.c +++ b/set.c @@ -1499,10 +1499,16 @@ set_remove_enum_from(VALUE set, VALUE arg) /* * call-seq: - * subtract(enum) -> self + * subtract(enumerable) -> self * - * Deletes every element that appears in the given enumerable object - * and returns self. + * Deletes from +self+ every element found in the given +enumerable+; + * returns +self+: + * + * set = Set[*0..9] # => Set[0, 1, 2, 3, 4, 5, 6, 7, 8, 9] + * set.subtract(5..14) # => Set[0, 1, 2, 3, 4] + * set.subtract(Set[6, 2]) # => Set[0, 1, 3, 4] + * + * Related: see {Methods for Deleting}[rdoc-ref:Set@Methods+for+Deleting]. */ static VALUE set_i_subtract(VALUE set, VALUE other) From 22f006cd8901184509d958d76c8518231d6e0ece Mon Sep 17 00:00:00 2001 From: Burdette Lamar Date: Thu, 2 Jul 2026 01:56:57 -0500 Subject: [PATCH 5/9] [DOC] Update Set#superset? documentation --- set.c | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/set.c b/set.c index dbe9dbcfaf549c..312138bda0d091 100644 --- a/set.c +++ b/set.c @@ -1933,9 +1933,17 @@ set_i_proper_superset(VALUE set, VALUE other) /* * call-seq: - * superset?(set) -> true or false + * superset?(other_set) -> true or false * - * Returns true if the set is a superset of the given set. + * Returns whether +self+ is a {superset}[https://en.wikipedia.org/wiki/Subset] + * of the given +other_set+: + * + * set = Set[*'a'..'f'] # => Set["a", "b", "c", "d", "e", "f"] + * set.superset?(set) # => true + * set.superset?(Set[*'b'..'e']) # => true + * set.superset?(Set[*'b'..'x']) # => false + * + * Related: {Methods for Querying}[rdoc-ref:Set@Methods+for+Querying]. */ static VALUE set_i_superset(VALUE set, VALUE other) From 961ca26e6f5d4fcee3864daf6f5c847e02aec65c Mon Sep 17 00:00:00 2001 From: Burdette Lamar Date: Thu, 2 Jul 2026 01:57:33 -0500 Subject: [PATCH 6/9] [DOC] Update Set#to_a documentation --- set.c | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/set.c b/set.c index 312138bda0d091..e8dab7b95334a6 100644 --- a/set.c +++ b/set.c @@ -647,10 +647,12 @@ set_to_a_i(st_data_t key, st_data_t arg) * call-seq: * to_a -> array * - * Returns an array containing all elements in the set. + * Returns an array containing the elements of +self+: * - * Set[1, 2].to_a #=> [1, 2] - * Set[1, 'c', :s].to_a #=> [1, "c", :s] + * Set[1, 2].to_a # => [1, 2] + * Set[1, 'c', :s].to_a # => [1, "c", :s] + * + * Related: {Methods for Converting}[rdoc-ref:Set@Methods+for+Converting]. */ static VALUE set_i_to_a(VALUE set) From 03d8b94e8f83338f1feea3b05de2399e8e75d93a Mon Sep 17 00:00:00 2001 From: Koichi Sasada Date: Tue, 16 Jun 2026 16:27:35 +0000 Subject: [PATCH 7/9] Fix flaky TCPSocket#local_address implicit-hostname spec The "using an implicit hostname" example connects with TCPSocket.new(nil, port) to a server bound to the IPv4 loopback, then asserts that local_address.ip_address equals @host ("127.0.0.1"). This is flaky. A nil hostname resolves to the loopback of both families, and Happy Eyeballs prefers IPv6, so the connection is not guaranteed to reach the IPv4 test server. IPv4 and IPv6 can share the same ephemeral port number (v6only), so under parallel runs (-j20) an unrelated listener on ::1: can be reached instead. local_address then comes back as "::1" and the spec fails. The invariant the example actually wants to check is that local_address reflects the connection's family, which always matches the peer. Compare against remote_address.ip_address instead of @host: deterministic in a clean environment and robust to the cross-family ephemeral-port collision. Verified: the spec passes normally, and with a forced ::1 listener on the same port the old `== @host` assertion fails while `== remote` holds. (Imported from ruby/spec; should be upstreamed there.) Error: TCPSocket#local_address using IPv4 using an implicit hostname the returned Addrinfo uses the correct IP address FAILED Expected "::1" == "127.0.0.1" to be truthy but was false spec/ruby/library/socket/tcpsocket/local_address_spec.rb:68 CI: https://ci.rvm.jp/results/trunk@ruby-sp3/6380323 Log: https://ci.rvm.jp/logfiles/brlog.trunk.20260616-071309 Co-Authored-By: Claude Opus 4.8 (1M context) --- spec/ruby/library/socket/tcpsocket/local_address_spec.rb | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/spec/ruby/library/socket/tcpsocket/local_address_spec.rb b/spec/ruby/library/socket/tcpsocket/local_address_spec.rb index 5dcf741f2904b6..bfa97231a6b7c2 100644 --- a/spec/ruby/library/socket/tcpsocket/local_address_spec.rb +++ b/spec/ruby/library/socket/tcpsocket/local_address_spec.rb @@ -65,7 +65,13 @@ describe 'the returned Addrinfo' do it 'uses the correct IP address' do - @sock.local_address.ip_address.should == @host + # An implicit (nil) hostname resolves to the loopback of either family + # and Happy Eyeballs prefers IPv6, so the connection is not guaranteed + # to land on @host: under parallel runs an unrelated listener on + # ::1: can be reached instead, making @host a + # flaky expectation (local_address would be ::1). The local end always + # matches the family actually connected, so compare against the peer. + @sock.local_address.ip_address.should == @sock.remote_address.ip_address end end end From 3e0e54244dddeca6c08e2fff40434b6909b97921 Mon Sep 17 00:00:00 2001 From: Hiroshi SHIBATA Date: Thu, 2 Jul 2026 17:40:21 +0900 Subject: [PATCH 8/9] Keep skipping test_port_receive_dnt_with_port_send on Windows (#17625) * Increase timeout of test_port_receive_dnt_with_port_send Still times out with 90 seconds on Windows CI. https://github.com/ruby/ruby/actions/runs/28566080915/job/84693945202 Co-Authored-By: Claude Fable 5 * Keep skipping test_port_receive_dnt_with_port_send on MinGW The raised timeout is enough for mswin, but the test still times out on the MinGW runner. Restore the MinGW omit guard while leaving it enabled on mswin. Co-Authored-By: Claude Opus 4.8 * Keep skipping test_port_receive_dnt_with_port_send on Windows Even with a 120 second timeout the test still times out on the mswin runner, so stop running it on Windows entirely. Restore the original Windows omit guard and the 90 second timeout. Co-Authored-By: Claude Opus 4.8 --------- Co-authored-by: Claude Fable 5 --- test/ruby/test_ractor.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/ruby/test_ractor.rb b/test/ruby/test_ractor.rb index 8b1545d0a0b17f..e7eb0cd4b34fe7 100644 --- a/test/ruby/test_ractor.rb +++ b/test/ruby/test_ractor.rb @@ -276,7 +276,7 @@ def test_require_non_string # [Bug #21398] def test_port_receive_dnt_with_port_send - omit 'unstable on macos-14' if RUBY_PLATFORM =~ /darwin/ + omit 'unstable on windows and macos-14' if RUBY_PLATFORM =~ /mswin|mingw|darwin/ assert_ractor(<<~'RUBY', timeout: 90) THREADS = 10 JOBS_PER_THREAD = 50 From 7c69f93d5344dc39411b426f37a52b067dc11222 Mon Sep 17 00:00:00 2001 From: Scott Myron Date: Wed, 24 Jun 2026 21:29:24 -0500 Subject: [PATCH 9/9] [ruby/json] Add a `sort_keys` option to the generator. Fix: https://github.com/ruby/json/issues/976 https://github.com/ruby/json/commit/ea008e82fe Co-Authored-By: Jean Boussier --- ext/json/generator/generator.c | 73 +++++++++++++++++++++++- ext/json/lib/json.rb | 6 +- ext/json/lib/json/common.rb | 9 +++ ext/json/lib/json/ext/generator/state.rb | 1 + test/json/json_generator_test.rb | 55 ++++++++++++++++++ 5 files changed, 141 insertions(+), 3 deletions(-) diff --git a/ext/json/generator/generator.c b/ext/json/generator/generator.c index d4164051f78b3f..6f473325efa0e3 100644 --- a/ext/json/generator/generator.c +++ b/ext/json/generator/generator.c @@ -34,13 +34,14 @@ typedef struct JSON_Generator_StateStruct { bool ascii_only; bool script_safe; bool strict; + VALUE sort_keys; } JSON_Generator_State; -static VALUE mJSON, cState, cFragment, eGeneratorError, eNestingError, Encoding_UTF_8; +static VALUE mJSON, cState, cFragment, eGeneratorError, eNestingError, Encoding_UTF_8, default_sort_keys_proc; static ID i_to_s, i_to_json, i_new, i_encode; static VALUE sym_indent, sym_space, sym_space_before, sym_object_nl, sym_array_nl, sym_max_nesting, sym_allow_nan, sym_allow_duplicate_key, - sym_ascii_only, sym_depth, sym_buffer_initial_length, sym_script_safe, sym_escape_slash, sym_strict, sym_as_json; + sym_ascii_only, sym_depth, sym_buffer_initial_length, sym_script_safe, sym_escape_slash, sym_strict, sym_as_json, sym_sort_keys; #define GET_STATE_TO(self, state) \ @@ -709,6 +710,7 @@ static void State_mark(void *ptr) rb_gc_mark_movable(state->object_nl); rb_gc_mark_movable(state->array_nl); rb_gc_mark_movable(state->as_json); + rb_gc_mark_movable(state->sort_keys); } static void State_compact(void *ptr) @@ -720,6 +722,7 @@ static void State_compact(void *ptr) state->object_nl = rb_gc_location(state->object_nl); state->array_nl = rb_gc_location(state->array_nl); state->as_json = rb_gc_location(state->as_json); + state->sort_keys = rb_gc_location(state->sort_keys); } static size_t State_memsize(const void *ptr) @@ -769,6 +772,7 @@ static void vstate_spill(struct generate_json_data *data) RB_OBJ_WRITTEN(vstate, Qundef, state->object_nl); RB_OBJ_WRITTEN(vstate, Qundef, state->array_nl); RB_OBJ_WRITTEN(vstate, Qundef, state->as_json); + RB_OBJ_WRITTEN(vstate, Qundef, state->sort_keys); } static inline VALUE json_call_to_json(struct generate_json_data *data, VALUE obj) @@ -1050,6 +1054,11 @@ static inline long increase_depth(struct generate_json_data *data) static void generate_json_object(FBuffer *buffer, struct generate_json_data *data, VALUE obj) { + if (RB_UNLIKELY(data->state->sort_keys)) { + obj = rb_proc_call_with_block(data->state->sort_keys, 1, &obj, Qnil); + Check_Type(obj, T_HASH); + } + long depth = increase_depth(data); if (RHASH_SIZE(obj) == 0) { @@ -1376,6 +1385,7 @@ static VALUE cState_init_copy(VALUE obj, VALUE orig) RB_OBJ_WRITTEN(obj, Qundef, objState->object_nl); RB_OBJ_WRITTEN(obj, Qundef, objState->array_nl); RB_OBJ_WRITTEN(obj, Qundef, objState->as_json); + RB_OBJ_WRITTEN(obj, Qundef, objState->sort_keys); return obj; } @@ -1722,6 +1732,55 @@ static VALUE cState_ascii_only_set(VALUE self, VALUE enable) return Qnil; } +static VALUE cState_set_default_sort_keys_proc(VALUE self, VALUE proc) +{ + if (!rb_obj_is_proc(proc)) { + rb_raise(rb_eTypeError, "sort_key_proc must be a Proc"); + } + return default_sort_keys_proc = proc; +} + +static VALUE normalize_sort_keys(VALUE value) +{ + if (rb_obj_is_proc(value)) { + return value; + } else if (value == Qtrue) { + return default_sort_keys_proc; + } else if (RTEST(value)) { + rb_raise(rb_eTypeError, "The `sort_keys` argument must be a boolean or a Proc"); + } else { + return Qfalse; + } +} + +/* + * call-seq: sort_keys + * + * Get the value of sort_keys. + */ +static VALUE cState_sort_keys_p(VALUE self) +{ + GET_STATE(self); + return state->sort_keys; +} + +/* + * call-seq: sort_keys=(value) + * + * value is a boolean or a proc. If the value is the boolean true, object keys + * will be sorted lexicographically in ascending order. + * + * If the value is a proc, it receives the entire Hash and must return a Hash + * with its pairs in the desired order, allowing for arbitrary sorting. + */ +static VALUE cState_sort_keys_set(VALUE self, VALUE value) +{ + rb_check_frozen(self); + GET_STATE(self); + RB_OBJ_WRITE(self, &state->sort_keys, normalize_sort_keys(value)); + return Qnil; +} + static VALUE cState_allow_duplicate_key_p(VALUE self) { GET_STATE(self); @@ -1832,6 +1891,9 @@ static int configure_state_i(VALUE key, VALUE val, VALUE _arg) state->as_json_single_arg = proc && rb_proc_arity(proc) == 1; state_write_value(data, &state->as_json, proc); } + else if (key == sym_sort_keys) { + state_write_value(data, &state->sort_keys, normalize_sort_keys(val)); + } return ST_CONTINUE; } @@ -1909,6 +1971,8 @@ void Init_generator(void) VALUE mExt = rb_define_module_under(mJSON, "Ext"); VALUE mGenerator = rb_define_module_under(mExt, "Generator"); + rb_global_variable(&default_sort_keys_proc); + rb_global_variable(&eGeneratorError); eGeneratorError = rb_path2class("JSON::GeneratorError"); @@ -1918,6 +1982,8 @@ void Init_generator(void) cState = rb_define_class_under(mGenerator, "State", rb_cObject); rb_define_alloc_func(cState, cState_s_allocate); rb_define_singleton_method(cState, "from_state", cState_from_state_s, 1); + rb_define_singleton_method(cState, "default_sort_keys_proc=", cState_set_default_sort_keys_proc, 1); + rb_define_method(cState, "initialize", cState_initialize, -1); rb_define_alias(cState, "initialize", "initialize"); // avoid method redefinition warnings rb_define_private_method(cState, "_configure", cState_configure, 1); @@ -1957,6 +2023,8 @@ void Init_generator(void) rb_define_method(cState, "buffer_initial_length=", cState_buffer_initial_length_set, 1); rb_define_method(cState, "generate", cState_generate, -1); rb_define_method(cState, "_generate_no_fallback", cState_generate_no_fallback, -1); + rb_define_method(cState, "sort_keys", cState_sort_keys_p, 0); + rb_define_method(cState, "sort_keys=", cState_sort_keys_set, 1); rb_define_private_method(cState, "allow_duplicate_key?", cState_allow_duplicate_key_p, 0); @@ -1986,6 +2054,7 @@ void Init_generator(void) sym_strict = ID2SYM(rb_intern("strict")); sym_as_json = ID2SYM(rb_intern("as_json")); sym_allow_duplicate_key = ID2SYM(rb_intern("allow_duplicate_key")); + sym_sort_keys = ID2SYM(rb_intern("sort_keys")); usascii_encindex = rb_usascii_encindex(); utf8_encindex = rb_utf8_encindex(); diff --git a/ext/json/lib/json.rb b/ext/json/lib/json.rb index f8dc4ccc9ed0dd..d1e94147dc1788 100644 --- a/ext/json/lib/json.rb +++ b/ext/json/lib/json.rb @@ -408,7 +408,6 @@ # to be inserted after each \JSON object; defaults to the empty \String, ''. # - Option +indent+ (\String) specifies the string (usually spaces) to be # used for indentation; defaults to the empty \String, ''; -# defaults to the empty \String, ''; # has no effect unless options +array_nl+ or +object_nl+ specify newlines. # - Option +space+ (\String) specifies a string (usually a space) to be # inserted after the colon in each \JSON object's pair; @@ -416,6 +415,11 @@ # - Option +space_before+ (\String) specifies a string (usually a space) to be # inserted before the colon in each \JSON object's pair; # defaults to the empty \String, ''. +# - Option +sort_keys+ (boolean or \Proc) controls whether and how the keys of a +# hash are sorted when generating the output; defaults to false. +# When +true+, keys are sorted lexicographically. When a \Proc, it receives +# the entire \Hash and must return a \Hash with its pairs in the desired +# order, allowing for arbitrary sort orders. # # In this example, +obj+ is used first to generate the shortest # \JSON data (no whitespace), then again with all formatting options diff --git a/ext/json/lib/json/common.rb b/ext/json/lib/json/common.rb index 230bf08012e111..fdcb860db29adc 100644 --- a/ext/json/lib/json/common.rb +++ b/ext/json/lib/json/common.rb @@ -155,6 +155,15 @@ def parser=(parser) # :nodoc: # Set the module _generator_ to be used by JSON. def generator=(generator) # :nodoc: old, $VERBOSE = $VERBOSE, nil + + # 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 } + if defined?(::Ractor) && Ractor.respond_to?(:shareable_lambda) + sort_keys_proc = Ractor.shareable_lambda(&sort_keys_proc) + end + generator::State.default_sort_keys_proc = sort_keys_proc + @generator = generator if generator.const_defined?(:GeneratorMethods) generator_methods = generator::GeneratorMethods diff --git a/ext/json/lib/json/ext/generator/state.rb b/ext/json/lib/json/ext/generator/state.rb index e4f425af6a3a6c..3c1d2fb94009c8 100644 --- a/ext/json/lib/json/ext/generator/state.rb +++ b/ext/json/lib/json/ext/generator/state.rb @@ -54,6 +54,7 @@ def to_h strict: strict?, depth: depth, buffer_initial_length: buffer_initial_length, + sort_keys: sort_keys } allow_duplicate_key = allow_duplicate_key? diff --git a/test/json/json_generator_test.rb b/test/json/json_generator_test.rb index 753ee0fbdf199f..ab19704b146fb0 100755 --- a/test/json/json_generator_test.rb +++ b/test/json/json_generator_test.rb @@ -190,6 +190,59 @@ def test_generate_pretty_custom JSON end + def test_generate_sort_keys + json = generate({2=>"a", 1=>"b", 3=>"c"}, sort_keys: true) + assert_equal('{"1":"b","2":"a","3":"c"}', json) + + json = generate({2=>"a", 1=>"b", 3=>"c"}, sort_keys: false) + assert_equal('{"2":"a","1":"b","3":"c"}', json) + + json = pretty_generate({2=>"a", 1=>"b", 3=>"c"}, sort_keys: true) + assert_equal(<<~'JSON'.chomp, json) + { + "1": "b", + "2": "a", + "3": "c" + } + JSON + + json = pretty_generate({2=>"a", 1=>"b", 3=>"c"}, sort_keys: false) + assert_equal(<<~'JSON'.chomp, json) + { + "2": "a", + "1": "b", + "3": "c" + } + JSON + + json = pretty_generate({2=>"a", 1=>"b", 3=>"c"}) + assert_equal(<<~'JSON'.chomp, json) + { + "2": "a", + "1": "b", + "3": "c" + } + JSON + end + + def test_generate_sort_keys_with_proc + reverse = ->(hash) { hash.sort.reverse.to_h } + json = generate({2=>"a", 1=>"b", 3=>"c"}, sort_keys: reverse) + assert_equal('{"3":"c","2":"a","1":"b"}', json) + + by_value = ->(hash) { hash.sort_by { |_k, v| v }.to_h } + json = generate({2=>"c", 1=>"a", 3=>"b"}, sort_keys: by_value) + assert_equal('{"1":"a","3":"b","2":"c"}', json) + + state = State.new(sort_keys: reverse) + assert_same reverse, state.to_h[:sort_keys] + assert_equal('{"3":"c","2":"a","1":"b"}', state.generate({2=>"a", 1=>"b", 3=>"c"})) + + # A truthy sort_keys is normalized to the default sorting proc. + state = State.new(sort_keys: true) + assert_instance_of Proc, state.sort_keys + 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) @@ -289,6 +342,7 @@ def test_state_defaults :object_nl => "", :space => "", :space_before => "", + :sort_keys => false, }.sort_by { |n,| n.to_s }, state.to_h.sort_by { |n,| n.to_s }) state = JSON::State.new(allow_duplicate_key: true) @@ -307,6 +361,7 @@ def test_state_defaults :object_nl => "", :space => "", :space_before => "", + :sort_keys => false, }.sort_by { |n,| n.to_s }, state.to_h.sort_by { |n,| n.to_s }) end