Skip to content
73 changes: 71 additions & 2 deletions ext/json/generator/generator.c
Original file line number Diff line number Diff line change
Expand Up @@ -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) \
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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");

Expand All @@ -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);
Expand Down Expand Up @@ -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);

Expand Down Expand Up @@ -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();
Expand Down
6 changes: 5 additions & 1 deletion ext/json/lib/json.rb
Original file line number Diff line number Diff line change
Expand Up @@ -408,14 +408,18 @@
# to be inserted after each \JSON object; defaults to the empty \String, <tt>''</tt>.
# - Option +indent+ (\String) specifies the string (usually spaces) to be
# used for indentation; defaults to the empty \String, <tt>''</tt>;
# defaults to the empty \String, <tt>''</tt>;
# 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;
# defaults to the empty \String, <tt>''</tt>.
# - 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, <tt>''</tt>.
# - Option +sort_keys+ (boolean or \Proc) controls whether and how the keys of a
# hash are sorted when generating the output; defaults to <tt>false</tt>.
# 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
Expand Down
9 changes: 9 additions & 0 deletions ext/json/lib/json/common.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions ext/json/lib/json/ext/generator/state.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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?
Expand Down
103 changes: 81 additions & 22 deletions set.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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?:
Expand All @@ -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
*
Expand Down Expand Up @@ -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.
Expand Down
Loading