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/set.c b/set.c index 75b7708043b425..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) @@ -1312,7 +1314,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 +1352,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) @@ -1471,10 +1501,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) @@ -1611,11 +1647,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) @@ -1847,9 +1891,17 @@ set_i_proper_subset(VALUE set, VALUE other) /* * call-seq: - * subset?(set) -> true or false + * subset?(other_set) -> true or false + * + * Returns whether +self+ is a {subset}[https://en.wikipedia.org/wiki/Subset] + * of the given +other_set+: * - * Returns true if the set is a subset of the given 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) @@ -1883,9 +1935,17 @@ set_i_proper_superset(VALUE set, VALUE other) /* * call-seq: - * superset?(set) -> true or false + * superset?(other_set) -> true or false + * + * 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 * - * Returns true if the set is a superset of the given set. + * Related: {Methods for Querying}[rdoc-ref:Set@Methods+for+Querying]. */ static VALUE set_i_superset(VALUE set, VALUE other) @@ -2352,12 +2412,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 +2438,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 +2504,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. 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 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 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