Skip to content

Commit ea008e8

Browse files
samyronbyroot
andcommitted
Add a sort_keys option to the generator.
Fix: #976 Co-Authored-By: Jean Boussier <byroot@ruby-lang.org>
1 parent 94c1af2 commit ea008e8

8 files changed

Lines changed: 232 additions & 4 deletions

File tree

ext/json/ext/generator/generator.c

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,14 @@ typedef struct JSON_Generator_StateStruct {
3434
bool ascii_only;
3535
bool script_safe;
3636
bool strict;
37+
VALUE sort_keys;
3738
} JSON_Generator_State;
3839

39-
static VALUE mJSON, cState, cFragment, eGeneratorError, eNestingError, Encoding_UTF_8;
40+
static VALUE mJSON, cState, cFragment, eGeneratorError, eNestingError, Encoding_UTF_8, default_sort_keys_proc;
4041

4142
static ID i_to_s, i_to_json, i_new, i_encode;
4243
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,
43-
sym_ascii_only, sym_depth, sym_buffer_initial_length, sym_script_safe, sym_escape_slash, sym_strict, sym_as_json;
44+
sym_ascii_only, sym_depth, sym_buffer_initial_length, sym_script_safe, sym_escape_slash, sym_strict, sym_as_json, sym_sort_keys;
4445

4546

4647
#define GET_STATE_TO(self, state) \
@@ -709,6 +710,7 @@ static void State_mark(void *ptr)
709710
rb_gc_mark_movable(state->object_nl);
710711
rb_gc_mark_movable(state->array_nl);
711712
rb_gc_mark_movable(state->as_json);
713+
rb_gc_mark_movable(state->sort_keys);
712714
}
713715

714716
static void State_compact(void *ptr)
@@ -720,6 +722,7 @@ static void State_compact(void *ptr)
720722
state->object_nl = rb_gc_location(state->object_nl);
721723
state->array_nl = rb_gc_location(state->array_nl);
722724
state->as_json = rb_gc_location(state->as_json);
725+
state->sort_keys = rb_gc_location(state->sort_keys);
723726
}
724727

725728
static size_t State_memsize(const void *ptr)
@@ -769,6 +772,7 @@ static void vstate_spill(struct generate_json_data *data)
769772
RB_OBJ_WRITTEN(vstate, Qundef, state->object_nl);
770773
RB_OBJ_WRITTEN(vstate, Qundef, state->array_nl);
771774
RB_OBJ_WRITTEN(vstate, Qundef, state->as_json);
775+
RB_OBJ_WRITTEN(vstate, Qundef, state->sort_keys);
772776
}
773777

774778
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)
10501054

10511055
static void generate_json_object(FBuffer *buffer, struct generate_json_data *data, VALUE obj)
10521056
{
1057+
if (RB_UNLIKELY(data->state->sort_keys)) {
1058+
obj = rb_proc_call_with_block(data->state->sort_keys, 1, &obj, Qnil);
1059+
Check_Type(obj, T_HASH);
1060+
}
1061+
10531062
long depth = increase_depth(data);
10541063

10551064
if (RHASH_SIZE(obj) == 0) {
@@ -1376,6 +1385,7 @@ static VALUE cState_init_copy(VALUE obj, VALUE orig)
13761385
RB_OBJ_WRITTEN(obj, Qundef, objState->object_nl);
13771386
RB_OBJ_WRITTEN(obj, Qundef, objState->array_nl);
13781387
RB_OBJ_WRITTEN(obj, Qundef, objState->as_json);
1388+
RB_OBJ_WRITTEN(obj, Qundef, objState->sort_keys);
13791389

13801390
return obj;
13811391
}
@@ -1722,6 +1732,55 @@ static VALUE cState_ascii_only_set(VALUE self, VALUE enable)
17221732
return Qnil;
17231733
}
17241734

1735+
static VALUE cState_set_default_sort_keys_proc(VALUE self, VALUE proc)
1736+
{
1737+
if (!rb_obj_is_proc(proc)) {
1738+
rb_raise(rb_eTypeError, "sort_key_proc must be a Proc");
1739+
}
1740+
return default_sort_keys_proc = proc;
1741+
}
1742+
1743+
static VALUE normalize_sort_keys(VALUE value)
1744+
{
1745+
if (rb_obj_is_proc(value)) {
1746+
return value;
1747+
} else if (value == Qtrue) {
1748+
return default_sort_keys_proc;
1749+
} else if (RTEST(value)) {
1750+
rb_raise(rb_eTypeError, "The `sort_keys` argument must be a boolean or a Proc");
1751+
} else {
1752+
return Qfalse;
1753+
}
1754+
}
1755+
1756+
/*
1757+
* call-seq: sort_keys
1758+
*
1759+
* Get the value of sort_keys.
1760+
*/
1761+
static VALUE cState_sort_keys_p(VALUE self)
1762+
{
1763+
GET_STATE(self);
1764+
return state->sort_keys;
1765+
}
1766+
1767+
/*
1768+
* call-seq: sort_keys=(value)
1769+
*
1770+
* value is a boolean or a proc. If the value is the boolean true, object keys
1771+
* will be sorted lexicographically in ascending order.
1772+
*
1773+
* If the value is a proc, it receives the entire Hash and must return a Hash
1774+
* with its pairs in the desired order, allowing for arbitrary sorting.
1775+
*/
1776+
static VALUE cState_sort_keys_set(VALUE self, VALUE value)
1777+
{
1778+
rb_check_frozen(self);
1779+
GET_STATE(self);
1780+
RB_OBJ_WRITE(self, &state->sort_keys, normalize_sort_keys(value));
1781+
return Qnil;
1782+
}
1783+
17251784
static VALUE cState_allow_duplicate_key_p(VALUE self)
17261785
{
17271786
GET_STATE(self);
@@ -1832,6 +1891,9 @@ static int configure_state_i(VALUE key, VALUE val, VALUE _arg)
18321891
state->as_json_single_arg = proc && rb_proc_arity(proc) == 1;
18331892
state_write_value(data, &state->as_json, proc);
18341893
}
1894+
else if (key == sym_sort_keys) {
1895+
state_write_value(data, &state->sort_keys, normalize_sort_keys(val));
1896+
}
18351897
return ST_CONTINUE;
18361898
}
18371899

@@ -1909,6 +1971,8 @@ void Init_generator(void)
19091971
VALUE mExt = rb_define_module_under(mJSON, "Ext");
19101972
VALUE mGenerator = rb_define_module_under(mExt, "Generator");
19111973

1974+
rb_global_variable(&default_sort_keys_proc);
1975+
19121976
rb_global_variable(&eGeneratorError);
19131977
eGeneratorError = rb_path2class("JSON::GeneratorError");
19141978

@@ -1918,6 +1982,8 @@ void Init_generator(void)
19181982
cState = rb_define_class_under(mGenerator, "State", rb_cObject);
19191983
rb_define_alloc_func(cState, cState_s_allocate);
19201984
rb_define_singleton_method(cState, "from_state", cState_from_state_s, 1);
1985+
rb_define_singleton_method(cState, "default_sort_keys_proc=", cState_set_default_sort_keys_proc, 1);
1986+
19211987
rb_define_method(cState, "initialize", cState_initialize, -1);
19221988
rb_define_alias(cState, "initialize", "initialize"); // avoid method redefinition warnings
19231989
rb_define_private_method(cState, "_configure", cState_configure, 1);
@@ -1957,6 +2023,8 @@ void Init_generator(void)
19572023
rb_define_method(cState, "buffer_initial_length=", cState_buffer_initial_length_set, 1);
19582024
rb_define_method(cState, "generate", cState_generate, -1);
19592025
rb_define_method(cState, "_generate_no_fallback", cState_generate_no_fallback, -1);
2026+
rb_define_method(cState, "sort_keys", cState_sort_keys_p, 0);
2027+
rb_define_method(cState, "sort_keys=", cState_sort_keys_set, 1);
19602028

19612029
rb_define_private_method(cState, "allow_duplicate_key?", cState_allow_duplicate_key_p, 0);
19622030

@@ -1986,6 +2054,7 @@ void Init_generator(void)
19862054
sym_strict = ID2SYM(rb_intern("strict"));
19872055
sym_as_json = ID2SYM(rb_intern("as_json"));
19882056
sym_allow_duplicate_key = ID2SYM(rb_intern("allow_duplicate_key"));
2057+
sym_sort_keys = ID2SYM(rb_intern("sort_keys"));
19892058

19902059
usascii_encindex = rb_usascii_encindex();
19912060
utf8_encindex = rb_utf8_encindex();

java/src/json/ext/Generator.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import org.jruby.RubyFloat;
1818
import org.jruby.RubyHash;
1919
import org.jruby.RubyIO;
20+
import org.jruby.RubyProc;
2021
import org.jruby.RubyString;
2122
import org.jruby.RubySymbol;
2223
import org.jruby.runtime.Helpers;
@@ -573,6 +574,11 @@ static void generateHash(ThreadContext context, Session session, RubyHash object
573574
return;
574575
}
575576

577+
RubyProc sortKeysProc = state.getSortKeysProc();
578+
if (sortKeysProc != null) {
579+
object = (RubyHash) Helpers.invoke(context, sortKeysProc, "call", object);
580+
}
581+
576582
final ByteList objectNl = state.getObjectNl();
577583
byte[] objectNLBytes = objectNl.unsafeBytes();
578584
final byte[] indent = Utils.repeat(state.getIndent(), depth);

java/src/json/ext/GeneratorState.java

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ public class GeneratorState extends RubyObject {
3737
private boolean allowDuplicateKey = false;
3838
private boolean deprecateDuplicateKey = true;
3939

40+
private static IRubyObject defaultSortKeyProc;
41+
4042
/**
4143
* The indenting unit string. Will be repeated several times for larger
4244
* indenting levels.
@@ -104,6 +106,13 @@ public class GeneratorState extends RubyObject {
104106
private int bufferInitialLength = DEFAULT_BUFFER_INITIAL_LENGTH;
105107
static final int DEFAULT_BUFFER_INITIAL_LENGTH = 1024;
106108

109+
/**
110+
* Controls key sorting when generating JSON. <code>null</code> means keys
111+
* are emitted in insertion order; a true value sorts keys lexicographically;
112+
* a {@link RubyProc} is used as a comparator receiving two [key, value] pairs.
113+
*/
114+
private IRubyObject sortKeys;
115+
107116
/**
108117
* The current depth (inside a #to_json call)
109118
*/
@@ -158,6 +167,12 @@ static GeneratorState fromState(ThreadContext context, RuntimeInfo info,
158167
return (GeneratorState)klass.newInstance(context, context.nil);
159168
}
160169

170+
@JRubyMethod(meta=true, name="default_sort_keys_proc=")
171+
public static IRubyObject setDefaultSortKeyProc(IRubyObject klass, IRubyObject proc) {
172+
defaultSortKeyProc = proc;
173+
return proc;
174+
}
175+
161176
/**
162177
* <code>State#initialize(opts = {})</code>
163178
* <p>
@@ -222,6 +237,7 @@ public IRubyObject initialize_copy(ThreadContext context, IRubyObject vOrig) {
222237

223238
this.allowDuplicateKey = orig.allowDuplicateKey;
224239
this.deprecateDuplicateKey = orig.deprecateDuplicateKey;
240+
this.sortKeys = orig.sortKeys;
225241

226242
return this;
227243
}
@@ -431,6 +447,23 @@ public boolean strict() {
431447
return strict;
432448
}
433449

450+
/**
451+
* Returns the proc used to sort the keys of an object, or
452+
* <code>null</code> if keys should not be sorted. The proc receives the
453+
* entire Hash and returns a Hash with its pairs in the desired order.
454+
*/
455+
public RubyProc getSortKeysProc() {
456+
return sortKeys instanceof RubyProc ? (RubyProc) sortKeys : null;
457+
}
458+
459+
private static IRubyObject normalizeSortKeys(ThreadContext context, IRubyObject value) {
460+
if (value instanceof RubyProc) return value;
461+
if (value != null && value.isTrue()) {
462+
return defaultSortKeyProc;
463+
}
464+
return null;
465+
}
466+
434467
@JRubyMethod(name={"strict","strict?"})
435468
public RubyBoolean strict_get(ThreadContext context) {
436469
return RubyBoolean.newBoolean(context, strict);
@@ -474,6 +507,18 @@ public IRubyObject buffer_initial_length_set(IRubyObject buffer_initial_length)
474507
return buffer_initial_length;
475508
}
476509

510+
@JRubyMethod(name="sort_keys")
511+
public IRubyObject sort_keys_get(ThreadContext context) {
512+
return sortKeys == null ? context.getRuntime().getFalse() : sortKeys;
513+
}
514+
515+
@JRubyMethod(name="sort_keys=")
516+
public IRubyObject sort_keys_set(ThreadContext context, IRubyObject sortKeys) {
517+
checkFrozen();
518+
this.sortKeys = normalizeSortKeys(context, sortKeys);
519+
return sortKeys;
520+
}
521+
477522
public int getDepth() {
478523
return depth;
479524
}
@@ -568,6 +613,9 @@ public IRubyObject _configure(ThreadContext context, IRubyObject vOpts) {
568613
this.allowDuplicateKey = opts.getBool("allow_duplicate_key", false);
569614
this.deprecateDuplicateKey = false;
570615
}
616+
617+
sortKeys = normalizeSortKeys(context, opts.get("sort_keys"));
618+
571619
return this;
572620
}
573621

@@ -596,6 +644,7 @@ public RubyHash to_h(ThreadContext context) {
596644
result.op_aset(context, runtime.newSymbol("strict"), strict_get(context));
597645
result.op_aset(context, runtime.newSymbol("depth"), depth_get(context));
598646
result.op_aset(context, runtime.newSymbol("buffer_initial_length"), buffer_initial_length_get(context));
647+
result.op_aset(context, runtime.newSymbol("sort_keys"), sort_keys_get(context));
599648

600649
if (this.allowDuplicateKey) {
601650
if (!this.deprecateDuplicateKey) {

lib/json.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -408,14 +408,18 @@
408408
# to be inserted after each \JSON object; defaults to the empty \String, <tt>''</tt>.
409409
# - Option +indent+ (\String) specifies the string (usually spaces) to be
410410
# used for indentation; defaults to the empty \String, <tt>''</tt>;
411-
# defaults to the empty \String, <tt>''</tt>;
412411
# has no effect unless options +array_nl+ or +object_nl+ specify newlines.
413412
# - Option +space+ (\String) specifies a string (usually a space) to be
414413
# inserted after the colon in each \JSON object's pair;
415414
# defaults to the empty \String, <tt>''</tt>.
416415
# - Option +space_before+ (\String) specifies a string (usually a space) to be
417416
# inserted before the colon in each \JSON object's pair;
418417
# defaults to the empty \String, <tt>''</tt>.
418+
# - Option +sort_keys+ (boolean or \Proc) controls whether and how the keys of a
419+
# hash are sorted when generating the output; defaults to <tt>false</tt>.
420+
# When +true+, keys are sorted lexicographically. When a \Proc, it receives
421+
# the entire \Hash and must return a \Hash with its pairs in the desired
422+
# order, allowing for arbitrary sort orders.
419423
#
420424
# In this example, +obj+ is used first to generate the shortest
421425
# \JSON data (no whitespace), then again with all formatting options

lib/json/common.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,15 @@ def parser=(parser) # :nodoc:
155155
# Set the module _generator_ to be used by JSON.
156156
def generator=(generator) # :nodoc:
157157
old, $VERBOSE = $VERBOSE, nil
158+
159+
# The default proc used when the +sort_keys+ generation option is +true+.
160+
# It returns a new hash with the entries sorted by their keys.
161+
sort_keys_proc = ->(hash) { hash.sort.to_h }
162+
if defined?(::Ractor) && Ractor.respond_to?(:shareable_lambda)
163+
sort_keys_proc = Ractor.shareable_lambda(&sort_keys_proc)
164+
end
165+
generator::State.default_sort_keys_proc = sort_keys_proc
166+
158167
@generator = generator
159168
if generator.const_defined?(:GeneratorMethods)
160169
generator_methods = generator::GeneratorMethods

lib/json/ext/generator/state.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ def to_h
5454
strict: strict?,
5555
depth: depth,
5656
buffer_initial_length: buffer_initial_length,
57+
sort_keys: sort_keys
5758
}
5859

5960
allow_duplicate_key = allow_duplicate_key?

0 commit comments

Comments
 (0)