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