json 2.20.0 → 2.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGES.md +8 -0
- data/README.md +5 -5
- data/ext/json/ext/generator/generator.c +72 -3
- data/ext/json/ext/json.h +4 -0
- data/ext/json/ext/parser/parser.c +129 -39
- data/lib/json/common.rb +9 -0
- data/lib/json/ext/generator/state.rb +1 -0
- data/lib/json/ext.rb +26 -0
- data/lib/json/truffle_ruby/generator.rb +36 -1
- data/lib/json/version.rb +1 -1
- data/lib/json.rb +5 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 778c5ab5faa1a727c1c453ef34351537fd77dbb9a2f176abc15f6861391baab9
|
|
4
|
+
data.tar.gz: 37f10916f3608596b4665564f61ee872bd647617048c0d593a4d2c917acc87df
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 36af0bdf70e46b374bb7d0ad209e946fffa367f2316a279249295aff233c4b187d69fa65753db29ffb20639e828896fd18e33e6060ecb0908f3a85b170c53443
|
|
7
|
+
data.tar.gz: d148e9ef5ea0b428a562d422acb9357c8a061dada945eacb37b2c97e10c9c050253624881b16a4de547a7705011493db83a442afe230b828f9935a522c86c08d
|
data/CHANGES.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
### Unreleased
|
|
4
4
|
|
|
5
|
+
### 2026-07-12 (2.21.0)
|
|
6
|
+
|
|
7
|
+
* `JSON.generate` now accept a `sort_keys` option, which takes either a boolean or a block.
|
|
8
|
+
* Added `#empty?` and `#partial_value?` methods on `JSON::ResumableParser`.
|
|
9
|
+
* Numerous correctness and performance fixes for `JSON::ResumableParser`.
|
|
10
|
+
* Avoid triggering Ruby's `float out of range` warning when parsing out of range numbers.
|
|
11
|
+
* Declare C types with Ruby 4.1 `RUBY_TYPED_THREAD_SAFE_FREE`.
|
|
12
|
+
|
|
5
13
|
### 2026-06-23 (2.20.0)
|
|
6
14
|
|
|
7
15
|
* Both C and Java parsers are no longer recursive, so parsing very deep documents with `max_nesting: false` will no longer
|
data/README.md
CHANGED
|
@@ -85,7 +85,7 @@ Both of these behavior can be disabled using the `strict: true` option:
|
|
|
85
85
|
|
|
86
86
|
```ruby
|
|
87
87
|
JSON.generate(Object.new, strict: true) # => Object not allowed in JSON (JSON::GeneratorError)
|
|
88
|
-
JSON.generate(Position.new(1, 2)) # => Position not allowed in JSON (JSON::GeneratorError)
|
|
88
|
+
JSON.generate(Position.new(1, 2), strict: true) # => Position not allowed in JSON (JSON::GeneratorError)
|
|
89
89
|
```
|
|
90
90
|
|
|
91
91
|
## JSON::Coder
|
|
@@ -117,13 +117,13 @@ It is also called for objects that do have a JSON equivalent, but are used as Ha
|
|
|
117
117
|
as well as for strings that aren't valid UTF-8:
|
|
118
118
|
|
|
119
119
|
```ruby
|
|
120
|
-
coder = JSON::
|
|
120
|
+
coder = JSON::Coder.new do |object, is_object_key|
|
|
121
121
|
case object
|
|
122
122
|
when String
|
|
123
|
-
if !
|
|
124
|
-
Base64.encode64(
|
|
123
|
+
if !object.valid_encoding? || object.encoding != Encoding::UTF_8
|
|
124
|
+
Base64.encode64(object)
|
|
125
125
|
else
|
|
126
|
-
|
|
126
|
+
object
|
|
127
127
|
end
|
|
128
128
|
else
|
|
129
129
|
object
|
|
@@ -34,13 +34,14 @@ typedef struct JSON_Generator_StateStruct {
|
|
|
34
34
|
bool ascii_only;
|
|
35
35
|
bool script_safe;
|
|
36
36
|
bool strict;
|
|
37
|
+
VALUE sort_keys;
|
|
37
38
|
} JSON_Generator_State;
|
|
38
39
|
|
|
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;
|
|
40
41
|
|
|
41
42
|
static ID i_to_s, i_to_json, i_new, i_encode;
|
|
42
43
|
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;
|
|
44
45
|
|
|
45
46
|
|
|
46
47
|
#define GET_STATE_TO(self, state) \
|
|
@@ -709,6 +710,7 @@ static void State_mark(void *ptr)
|
|
|
709
710
|
rb_gc_mark_movable(state->object_nl);
|
|
710
711
|
rb_gc_mark_movable(state->array_nl);
|
|
711
712
|
rb_gc_mark_movable(state->as_json);
|
|
713
|
+
rb_gc_mark_movable(state->sort_keys);
|
|
712
714
|
}
|
|
713
715
|
|
|
714
716
|
static void State_compact(void *ptr)
|
|
@@ -720,6 +722,7 @@ static void State_compact(void *ptr)
|
|
|
720
722
|
state->object_nl = rb_gc_location(state->object_nl);
|
|
721
723
|
state->array_nl = rb_gc_location(state->array_nl);
|
|
722
724
|
state->as_json = rb_gc_location(state->as_json);
|
|
725
|
+
state->sort_keys = rb_gc_location(state->sort_keys);
|
|
723
726
|
}
|
|
724
727
|
|
|
725
728
|
static size_t State_memsize(const void *ptr)
|
|
@@ -739,7 +742,7 @@ static const rb_data_type_t JSON_Generator_State_type = {
|
|
|
739
742
|
.dsize = State_memsize,
|
|
740
743
|
.dcompact = State_compact,
|
|
741
744
|
},
|
|
742
|
-
.flags = RUBY_TYPED_WB_PROTECTED |
|
|
745
|
+
.flags = RUBY_TYPED_WB_PROTECTED | RUBY_TYPED_THREAD_SAFE_FREE | RUBY_TYPED_FROZEN_SHAREABLE | RUBY_TYPED_EMBEDDABLE,
|
|
743
746
|
};
|
|
744
747
|
|
|
745
748
|
static void state_init(JSON_Generator_State *state)
|
|
@@ -769,6 +772,7 @@ static void vstate_spill(struct generate_json_data *data)
|
|
|
769
772
|
RB_OBJ_WRITTEN(vstate, Qundef, state->object_nl);
|
|
770
773
|
RB_OBJ_WRITTEN(vstate, Qundef, state->array_nl);
|
|
771
774
|
RB_OBJ_WRITTEN(vstate, Qundef, state->as_json);
|
|
775
|
+
RB_OBJ_WRITTEN(vstate, Qundef, state->sort_keys);
|
|
772
776
|
}
|
|
773
777
|
|
|
774
778
|
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)
|
|
|
1050
1054
|
|
|
1051
1055
|
static void generate_json_object(FBuffer *buffer, struct generate_json_data *data, VALUE obj)
|
|
1052
1056
|
{
|
|
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
|
+
|
|
1053
1062
|
long depth = increase_depth(data);
|
|
1054
1063
|
|
|
1055
1064
|
if (RHASH_SIZE(obj) == 0) {
|
|
@@ -1376,6 +1385,7 @@ static VALUE cState_init_copy(VALUE obj, VALUE orig)
|
|
|
1376
1385
|
RB_OBJ_WRITTEN(obj, Qundef, objState->object_nl);
|
|
1377
1386
|
RB_OBJ_WRITTEN(obj, Qundef, objState->array_nl);
|
|
1378
1387
|
RB_OBJ_WRITTEN(obj, Qundef, objState->as_json);
|
|
1388
|
+
RB_OBJ_WRITTEN(obj, Qundef, objState->sort_keys);
|
|
1379
1389
|
|
|
1380
1390
|
return obj;
|
|
1381
1391
|
}
|
|
@@ -1722,6 +1732,55 @@ static VALUE cState_ascii_only_set(VALUE self, VALUE enable)
|
|
|
1722
1732
|
return Qnil;
|
|
1723
1733
|
}
|
|
1724
1734
|
|
|
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
|
+
|
|
1725
1784
|
static VALUE cState_allow_duplicate_key_p(VALUE self)
|
|
1726
1785
|
{
|
|
1727
1786
|
GET_STATE(self);
|
|
@@ -1832,6 +1891,9 @@ static int configure_state_i(VALUE key, VALUE val, VALUE _arg)
|
|
|
1832
1891
|
state->as_json_single_arg = proc && rb_proc_arity(proc) == 1;
|
|
1833
1892
|
state_write_value(data, &state->as_json, proc);
|
|
1834
1893
|
}
|
|
1894
|
+
else if (key == sym_sort_keys) {
|
|
1895
|
+
state_write_value(data, &state->sort_keys, normalize_sort_keys(val));
|
|
1896
|
+
}
|
|
1835
1897
|
return ST_CONTINUE;
|
|
1836
1898
|
}
|
|
1837
1899
|
|
|
@@ -1909,6 +1971,8 @@ void Init_generator(void)
|
|
|
1909
1971
|
VALUE mExt = rb_define_module_under(mJSON, "Ext");
|
|
1910
1972
|
VALUE mGenerator = rb_define_module_under(mExt, "Generator");
|
|
1911
1973
|
|
|
1974
|
+
rb_global_variable(&default_sort_keys_proc);
|
|
1975
|
+
|
|
1912
1976
|
rb_global_variable(&eGeneratorError);
|
|
1913
1977
|
eGeneratorError = rb_path2class("JSON::GeneratorError");
|
|
1914
1978
|
|
|
@@ -1918,6 +1982,8 @@ void Init_generator(void)
|
|
|
1918
1982
|
cState = rb_define_class_under(mGenerator, "State", rb_cObject);
|
|
1919
1983
|
rb_define_alloc_func(cState, cState_s_allocate);
|
|
1920
1984
|
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
|
+
|
|
1921
1987
|
rb_define_method(cState, "initialize", cState_initialize, -1);
|
|
1922
1988
|
rb_define_alias(cState, "initialize", "initialize"); // avoid method redefinition warnings
|
|
1923
1989
|
rb_define_private_method(cState, "_configure", cState_configure, 1);
|
|
@@ -1957,6 +2023,8 @@ void Init_generator(void)
|
|
|
1957
2023
|
rb_define_method(cState, "buffer_initial_length=", cState_buffer_initial_length_set, 1);
|
|
1958
2024
|
rb_define_method(cState, "generate", cState_generate, -1);
|
|
1959
2025
|
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);
|
|
1960
2028
|
|
|
1961
2029
|
rb_define_private_method(cState, "allow_duplicate_key?", cState_allow_duplicate_key_p, 0);
|
|
1962
2030
|
|
|
@@ -1986,6 +2054,7 @@ void Init_generator(void)
|
|
|
1986
2054
|
sym_strict = ID2SYM(rb_intern("strict"));
|
|
1987
2055
|
sym_as_json = ID2SYM(rb_intern("as_json"));
|
|
1988
2056
|
sym_allow_duplicate_key = ID2SYM(rb_intern("allow_duplicate_key"));
|
|
2057
|
+
sym_sort_keys = ID2SYM(rb_intern("sort_keys"));
|
|
1989
2058
|
|
|
1990
2059
|
usascii_encindex = rb_usascii_encindex();
|
|
1991
2060
|
utf8_encindex = rb_utf8_encindex();
|
data/ext/json/ext/json.h
CHANGED
|
@@ -5,7 +5,10 @@
|
|
|
5
5
|
static VALUE mJSON, eNestingError, eParserError, Encoding_UTF_8;
|
|
6
6
|
static VALUE CNaN, CInfinity, CMinusInfinity, JSON_empty_string;
|
|
7
7
|
|
|
8
|
-
static ID i_new, i_try_convert,
|
|
8
|
+
static ID i_new, i_try_convert, i_encode, i_at_line, i_at_column;
|
|
9
|
+
#ifndef HAVE_RB_STR_TO_INTERNED_STR
|
|
10
|
+
static ID i_uminus;
|
|
11
|
+
#endif
|
|
9
12
|
|
|
10
13
|
static VALUE sym_max_nesting, sym_allow_nan, sym_allow_trailing_comma, sym_allow_comments,
|
|
11
14
|
sym_allow_control_characters, sym_allow_invalid_escape, sym_symbolize_names,
|
|
@@ -314,7 +317,7 @@ static const rb_data_type_t JSON_Parser_rvalue_stack_type = {
|
|
|
314
317
|
},
|
|
315
318
|
// We deliberately don't declare rvalue_stack as RUBY_TYPED_WB_PROTECTED
|
|
316
319
|
// because it churns a lot of values so trigering write barriers every time is very costly.
|
|
317
|
-
.flags =
|
|
320
|
+
.flags = RUBY_TYPED_THREAD_SAFE_FREE | RUBY_TYPED_EMBEDDABLE,
|
|
318
321
|
};
|
|
319
322
|
|
|
320
323
|
static rvalue_stack *rvalue_stack_spill(rvalue_stack *old_stack, VALUE *handle, rvalue_stack **stack_ref)
|
|
@@ -511,7 +514,7 @@ static const rb_data_type_t JSON_Parser_frame_stack_type = {
|
|
|
511
514
|
.dfree = json_frame_stack_free,
|
|
512
515
|
.dsize = json_frame_stack_memsize,
|
|
513
516
|
},
|
|
514
|
-
.flags =
|
|
517
|
+
.flags = RUBY_TYPED_THREAD_SAFE_FREE | RUBY_TYPED_WB_PROTECTED | RUBY_TYPED_EMBEDDABLE,
|
|
515
518
|
};
|
|
516
519
|
|
|
517
520
|
static json_frame_stack *json_frame_stack_spill(json_frame_stack *old_stack, VALUE *handle, json_frame_stack **stack_ref)
|
|
@@ -765,22 +768,34 @@ static const rb_data_type_t JSON_ParserConfig_type;
|
|
|
765
768
|
|
|
766
769
|
const char *COMMENT_DEPRECATION_MESSAGE = "Encountered comment in JSON. This will raise an error in json 3.0 unless enabled via `allow_comments: true`";
|
|
767
770
|
NOINLINE(static) void
|
|
768
|
-
json_eat_comments(JSON_ParserState *state, JSON_ParserConfig *config)
|
|
771
|
+
json_eat_comments(JSON_ParserState *state, JSON_ParserConfig *config, const char *resume_pos)
|
|
769
772
|
{
|
|
770
773
|
if (config->on_comment == JSON_RAISE) {
|
|
771
774
|
raise_syntax_error("unexpected token %s", state);
|
|
772
775
|
}
|
|
773
776
|
|
|
774
777
|
const char *start = state->cursor;
|
|
778
|
+
// An incomplete comment suspends a resumable parse by rewinding the cursor
|
|
779
|
+
// and throwing. Callers that already consumed a token not yet committed to
|
|
780
|
+
// the frame stack pass resume_pos so the rewind re-reads that token too.
|
|
781
|
+
// Non-resumable error positions keep pointing at the comment either way.
|
|
782
|
+
const char *rewind_pos = (state->parser && resume_pos) ? resume_pos : start;
|
|
775
783
|
state->cursor++;
|
|
776
784
|
|
|
777
785
|
switch (peek(state)) {
|
|
778
786
|
case '/': {
|
|
779
|
-
|
|
780
|
-
if (!
|
|
787
|
+
const char *newline = memchr(state->cursor, '\n', state->end - state->cursor);
|
|
788
|
+
if (!newline) {
|
|
789
|
+
// state->parser marks resumable mode, where the buffer end is only a
|
|
790
|
+
// chunk boundary: the terminating newline may still arrive, so leave
|
|
791
|
+
// the comment unterminated instead of consuming to end as a one-shot
|
|
792
|
+
// parse would.
|
|
793
|
+
if (state->parser) {
|
|
794
|
+
raise_eos_error_at("unterminated comment, expected end of line", state, rewind_pos);
|
|
795
|
+
}
|
|
781
796
|
state->cursor = state->end;
|
|
782
797
|
} else {
|
|
783
|
-
state->cursor
|
|
798
|
+
state->cursor = newline + 1;
|
|
784
799
|
}
|
|
785
800
|
break;
|
|
786
801
|
}
|
|
@@ -790,7 +805,7 @@ json_eat_comments(JSON_ParserState *state, JSON_ParserConfig *config)
|
|
|
790
805
|
while (true) {
|
|
791
806
|
const char *next_match = memchr(state->cursor, '*', state->end - state->cursor);
|
|
792
807
|
if (!next_match) {
|
|
793
|
-
raise_eos_error_at("unterminated comment, expected closing '*/'", state,
|
|
808
|
+
raise_eos_error_at("unterminated comment, expected closing '*/'", state, rewind_pos);
|
|
794
809
|
}
|
|
795
810
|
|
|
796
811
|
state->cursor = next_match + 1;
|
|
@@ -802,7 +817,7 @@ json_eat_comments(JSON_ParserState *state, JSON_ParserConfig *config)
|
|
|
802
817
|
break;
|
|
803
818
|
}
|
|
804
819
|
default:
|
|
805
|
-
raise_parse_error_at("unexpected token %s", state, start, eos(state));
|
|
820
|
+
raise_parse_error_at("unexpected token %s", state, eos(state) ? rewind_pos : start, eos(state));
|
|
806
821
|
break;
|
|
807
822
|
}
|
|
808
823
|
|
|
@@ -813,7 +828,7 @@ json_eat_comments(JSON_ParserState *state, JSON_ParserConfig *config)
|
|
|
813
828
|
}
|
|
814
829
|
|
|
815
830
|
ALWAYS_INLINE(static) void
|
|
816
|
-
|
|
831
|
+
json_eat_whitespace_resume_at(JSON_ParserState *state, JSON_ParserConfig *config, bool include_comments, const char *resume_pos)
|
|
817
832
|
{
|
|
818
833
|
while (true) {
|
|
819
834
|
switch (peek(state)) {
|
|
@@ -848,7 +863,7 @@ json_eat_whitespace(JSON_ParserState *state, JSON_ParserConfig *config, bool inc
|
|
|
848
863
|
return;
|
|
849
864
|
}
|
|
850
865
|
|
|
851
|
-
json_eat_comments(state, config);
|
|
866
|
+
json_eat_comments(state, config, resume_pos);
|
|
852
867
|
break;
|
|
853
868
|
|
|
854
869
|
default:
|
|
@@ -857,6 +872,12 @@ json_eat_whitespace(JSON_ParserState *state, JSON_ParserConfig *config, bool inc
|
|
|
857
872
|
}
|
|
858
873
|
}
|
|
859
874
|
|
|
875
|
+
ALWAYS_INLINE(static) void
|
|
876
|
+
json_eat_whitespace(JSON_ParserState *state, JSON_ParserConfig *config, bool include_comments)
|
|
877
|
+
{
|
|
878
|
+
json_eat_whitespace_resume_at(state, config, include_comments, NULL);
|
|
879
|
+
}
|
|
880
|
+
|
|
860
881
|
static inline VALUE build_string(const char *start, const char *end, bool intern, bool symbolize)
|
|
861
882
|
{
|
|
862
883
|
if (symbolize) {
|
|
@@ -1130,6 +1151,13 @@ static inline VALUE json_decode_float(JSON_ParserConfig *config, uint64_t mantis
|
|
|
1130
1151
|
}
|
|
1131
1152
|
|
|
1132
1153
|
if (RB_UNLIKELY(mantissa_digits > 18 || mantissa_digits + exponent < -307)) {
|
|
1154
|
+
// If the value is so small that it definitely underflows to 0.0, return early
|
|
1155
|
+
// to avoid triggering a "Float out of range" warning from rb_cstr_to_dbl.
|
|
1156
|
+
// When mantissa_digits + exponent < -324, value < 10^(-324) < DBL_TRUE_MIN/2,
|
|
1157
|
+
// so it rounds to 0 in IEEE 754 round-to-nearest.
|
|
1158
|
+
if (RB_UNLIKELY(mantissa_digits + exponent < -324)) {
|
|
1159
|
+
return rb_float_new(negative ? -0.0 : 0.0);
|
|
1160
|
+
}
|
|
1133
1161
|
return json_decode_large_float(start, end - start);
|
|
1134
1162
|
}
|
|
1135
1163
|
|
|
@@ -1429,7 +1457,7 @@ static inline int json_parse_digits(JSON_ParserState *state, uint64_t *accumulat
|
|
|
1429
1457
|
return (int)(state->cursor - start);
|
|
1430
1458
|
}
|
|
1431
1459
|
|
|
1432
|
-
static inline VALUE json_parse_number(JSON_ParserState *state, JSON_ParserConfig *config, bool negative, const char *start)
|
|
1460
|
+
static inline VALUE json_parse_number(JSON_ParserState *state, JSON_ParserConfig *config, bool negative, const char *start, bool resumable)
|
|
1433
1461
|
{
|
|
1434
1462
|
bool integer = true;
|
|
1435
1463
|
const char first_digit = *state->cursor;
|
|
@@ -1486,6 +1514,16 @@ static inline VALUE json_parse_number(JSON_ParserState *state, JSON_ParserConfig
|
|
|
1486
1514
|
}
|
|
1487
1515
|
}
|
|
1488
1516
|
|
|
1517
|
+
// A number touching the end of the buffer may still grow in a later chunk,
|
|
1518
|
+
// so the caller will rewind and wait. Decoding it now would build a value
|
|
1519
|
+
// -- for a long run of digits, an expensive bignum -- only to discard it,
|
|
1520
|
+
// and repeating that on every resumed chunk is quadratic in the number's
|
|
1521
|
+
// length. The digit scan above already advanced the cursor, which is all
|
|
1522
|
+
// the caller needs to detect the incomplete number.
|
|
1523
|
+
if (RB_UNLIKELY(resumable && eos(state))) {
|
|
1524
|
+
return Qundef;
|
|
1525
|
+
}
|
|
1526
|
+
|
|
1489
1527
|
if (integer) {
|
|
1490
1528
|
return json_decode_integer(mantissa, mantissa_digits, negative, start, state->cursor);
|
|
1491
1529
|
}
|
|
@@ -1563,6 +1601,13 @@ ALWAYS_INLINE(static) bool json_parse_any(JSON_ParserState *state, JSON_ParserCo
|
|
|
1563
1601
|
JSON_PHASE_VALUE: {
|
|
1564
1602
|
json_eat_whitespace(state, config, true);
|
|
1565
1603
|
|
|
1604
|
+
// A trailing comma lands us here expecting an element but finding the
|
|
1605
|
+
// closing bracket; hand off to ARRAY_COMMA to close. An empty array
|
|
1606
|
+
// closes inline at '[', so this position is only reached after a ','.
|
|
1607
|
+
if (config->allow_trailing_comma && frame->type == JSON_FRAME_ARRAY && peek(state) == ']') {
|
|
1608
|
+
goto JSON_PHASE_ARRAY_COMMA;
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1566
1611
|
VALUE value;
|
|
1567
1612
|
const char *value_start = state->cursor;
|
|
1568
1613
|
|
|
@@ -1603,7 +1648,7 @@ ALWAYS_INLINE(static) bool json_parse_any(JSON_ParserState *state, JSON_ParserCo
|
|
|
1603
1648
|
case '-': {
|
|
1604
1649
|
state->cursor++;
|
|
1605
1650
|
|
|
1606
|
-
value = json_parse_number(state, config, true, value_start);
|
|
1651
|
+
value = json_parse_number(state, config, true, value_start, resumable);
|
|
1607
1652
|
|
|
1608
1653
|
if (RB_UNLIKELY(UNDEF_P(value) && config->allow_nan && peek(state) == 'I')) {
|
|
1609
1654
|
state->cursor = value_start;
|
|
@@ -1626,7 +1671,7 @@ ALWAYS_INLINE(static) bool json_parse_any(JSON_ParserState *state, JSON_ParserCo
|
|
|
1626
1671
|
}
|
|
1627
1672
|
|
|
1628
1673
|
case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': {
|
|
1629
|
-
value = json_parse_number(state, config, false, value_start);
|
|
1674
|
+
value = json_parse_number(state, config, false, value_start, resumable);
|
|
1630
1675
|
|
|
1631
1676
|
// Top level numbers are ambiguous when parsing streams, we can't
|
|
1632
1677
|
// know if we parsed all the digits if we hit EOS.
|
|
@@ -1658,7 +1703,9 @@ ALWAYS_INLINE(static) bool json_parse_any(JSON_ParserState *state, JSON_ParserCo
|
|
|
1658
1703
|
|
|
1659
1704
|
case '[': {
|
|
1660
1705
|
state->cursor++;
|
|
1661
|
-
|
|
1706
|
+
// The '[' is consumed but its frame is only pushed below, so a
|
|
1707
|
+
// comment suspending here must resume from the bracket.
|
|
1708
|
+
json_eat_whitespace_resume_at(state, config, true, value_start);
|
|
1662
1709
|
|
|
1663
1710
|
const char next = peek(state);
|
|
1664
1711
|
if (next == ']') {
|
|
@@ -1687,7 +1734,8 @@ ALWAYS_INLINE(static) bool json_parse_any(JSON_ParserState *state, JSON_ParserCo
|
|
|
1687
1734
|
|
|
1688
1735
|
case '{': {
|
|
1689
1736
|
state->cursor++;
|
|
1690
|
-
|
|
1737
|
+
// Same as '[': the frame is only pushed below.
|
|
1738
|
+
json_eat_whitespace_resume_at(state, config, true, value_start);
|
|
1691
1739
|
|
|
1692
1740
|
if (peek(state) == '}') {
|
|
1693
1741
|
state->cursor++;
|
|
@@ -1745,6 +1793,13 @@ ALWAYS_INLINE(static) bool json_parse_any(JSON_ParserState *state, JSON_ParserCo
|
|
|
1745
1793
|
|
|
1746
1794
|
json_eat_whitespace(state, config, true);
|
|
1747
1795
|
|
|
1796
|
+
// A trailing comma lands us here expecting a key but finding the closing
|
|
1797
|
+
// brace; hand off to OBJECT_COMMA to close. An empty object closes inline
|
|
1798
|
+
// at '{', so this position is only reached after a ','.
|
|
1799
|
+
if (config->allow_trailing_comma && peek(state) == '}') {
|
|
1800
|
+
goto JSON_PHASE_OBJECT_COMMA;
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1748
1803
|
const char *start = state->cursor;
|
|
1749
1804
|
|
|
1750
1805
|
if (RB_LIKELY(peek(state) == '"')) {
|
|
@@ -1807,13 +1862,10 @@ ALWAYS_INLINE(static) bool json_parse_any(JSON_ParserState *state, JSON_ParserCo
|
|
|
1807
1862
|
|
|
1808
1863
|
if (RB_LIKELY(next_char == ',')) {
|
|
1809
1864
|
state->cursor++;
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
goto JSON_PHASE_ARRAY_COMMA;
|
|
1815
|
-
}
|
|
1816
|
-
}
|
|
1865
|
+
// Commit the phase before eating the whitespace that follows: an
|
|
1866
|
+
// incomplete comment there would suspend the parse, and a phase not
|
|
1867
|
+
// yet advanced past the ',' would drop it on resume. A trailing comma
|
|
1868
|
+
// is recognized in JSON_PHASE_VALUE once the ']' is in the buffer.
|
|
1817
1869
|
frame->phase = JSON_PHASE_VALUE;
|
|
1818
1870
|
goto JSON_PHASE_VALUE;
|
|
1819
1871
|
} else if (next_char == ']') {
|
|
@@ -1852,15 +1904,10 @@ ALWAYS_INLINE(static) bool json_parse_any(JSON_ParserState *state, JSON_ParserCo
|
|
|
1852
1904
|
|
|
1853
1905
|
if (RB_LIKELY(next_char == ',')) {
|
|
1854
1906
|
state->cursor++;
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
// Trailing comma: stay in COMMA to close on the next iteration.
|
|
1860
|
-
goto JSON_PHASE_OBJECT_COMMA;
|
|
1861
|
-
}
|
|
1862
|
-
}
|
|
1863
|
-
|
|
1907
|
+
// Commit the phase before eating the whitespace that follows: an
|
|
1908
|
+
// incomplete comment there would suspend the parse, and a phase not
|
|
1909
|
+
// yet advanced past the ',' would drop it on resume. A trailing comma
|
|
1910
|
+
// is recognized in JSON_PHASE_OBJECT_KEY once the '}' is in the buffer.
|
|
1864
1911
|
frame->phase = JSON_PHASE_OBJECT_KEY;
|
|
1865
1912
|
goto JSON_PHASE_OBJECT_KEY;
|
|
1866
1913
|
} else if (next_char == '}') {
|
|
@@ -2180,7 +2227,7 @@ static const rb_data_type_t JSON_ParserConfig_type = {
|
|
|
2180
2227
|
.dsize = JSON_ParserConfig_memsize,
|
|
2181
2228
|
.dcompact = JSON_ParserConfig_compact,
|
|
2182
2229
|
},
|
|
2183
|
-
.flags =
|
|
2230
|
+
.flags = RUBY_TYPED_THREAD_SAFE_FREE | RUBY_TYPED_WB_PROTECTED | RUBY_TYPED_FROZEN_SHAREABLE | RUBY_TYPED_EMBEDDABLE,
|
|
2184
2231
|
};
|
|
2185
2232
|
|
|
2186
2233
|
static VALUE cJSON_parser_s_allocate(VALUE klass)
|
|
@@ -2265,7 +2312,7 @@ static const rb_data_type_t JSON_ResumableParser_type = {
|
|
|
2265
2312
|
// RUBY_TYPED_WB_PROTECTED is deliberately not declared because
|
|
2266
2313
|
// this is a superset of JSON_Parser_rvalue_stack_type, so we'd need
|
|
2267
2314
|
// to trigger a lot of write barriers.
|
|
2268
|
-
.flags =
|
|
2315
|
+
.flags = RUBY_TYPED_THREAD_SAFE_FREE | RUBY_TYPED_EMBEDDABLE,
|
|
2269
2316
|
};
|
|
2270
2317
|
|
|
2271
2318
|
static VALUE cResumableParser_allocate(VALUE klass)
|
|
@@ -2615,11 +2662,16 @@ static VALUE cResumableParser_partial_value_body(VALUE self)
|
|
|
2615
2662
|
missing_object_value = 1;
|
|
2616
2663
|
}
|
|
2617
2664
|
|
|
2618
|
-
// Copy the value stack as we need to mutate it.
|
|
2665
|
+
// Copy the value stack as we need to mutate it. The collapse loop folds each
|
|
2666
|
+
// open container by popping its entries and pushing the single result, so a
|
|
2667
|
+
// parent always reclaims its child's slot; head exceeds its live size by at
|
|
2668
|
+
// most one, either for the missing-value placeholder pushed below or for the
|
|
2669
|
+
// result of folding an empty innermost container. That one spare slot keeps
|
|
2670
|
+
// rvalue_stack_push from growing (reallocating) this ALLOCV buffer.
|
|
2619
2671
|
long capa = parser.value_stack.head;
|
|
2620
|
-
parser.value_stack.capa =
|
|
2621
|
-
VALUE tmpbuf, *value_stack_buffer = ALLOCV_N(VALUE, tmpbuf, capa
|
|
2622
|
-
MEMCPY(value_stack_buffer, parser.value_stack.ptr, VALUE,
|
|
2672
|
+
parser.value_stack.capa = capa + 1;
|
|
2673
|
+
VALUE tmpbuf, *value_stack_buffer = ALLOCV_N(VALUE, tmpbuf, parser.value_stack.capa);
|
|
2674
|
+
MEMCPY(value_stack_buffer, parser.value_stack.ptr, VALUE, capa);
|
|
2623
2675
|
parser.value_stack.ptr = value_stack_buffer;
|
|
2624
2676
|
|
|
2625
2677
|
JSON_ParserState *state = &parser.state;
|
|
@@ -2712,7 +2764,7 @@ static VALUE cResumableParser_rest(VALUE self)
|
|
|
2712
2764
|
}
|
|
2713
2765
|
|
|
2714
2766
|
/*
|
|
2715
|
-
* call-seq:
|
|
2767
|
+
* call-seq: eos? -> true or false
|
|
2716
2768
|
*
|
|
2717
2769
|
* Returns whether the internal buffer has been entirely consumed.
|
|
2718
2770
|
*/
|
|
@@ -2722,6 +2774,41 @@ static VALUE cResumableParser_eos_p(VALUE self)
|
|
|
2722
2774
|
return eos(&parser->state) ? Qtrue : Qfalse;
|
|
2723
2775
|
}
|
|
2724
2776
|
|
|
2777
|
+
/*
|
|
2778
|
+
* call-seq: partial_value? -> true or false
|
|
2779
|
+
*
|
|
2780
|
+
* Returns whether a document is currently under construction: an unclosed
|
|
2781
|
+
* container, a key awaiting its value, etc.
|
|
2782
|
+
*
|
|
2783
|
+
* It answers the same question as <tt>!partial_value.nil?</tt>, but as a
|
|
2784
|
+
* cheap predicate on the parser's internal state, without materializing the
|
|
2785
|
+
* partially parsed Ruby objects:
|
|
2786
|
+
* parser << '{"a":1,'
|
|
2787
|
+
* parser.parse # => false
|
|
2788
|
+
* parser.partial_value? # => true
|
|
2789
|
+
*
|
|
2790
|
+
* A fully parsed document whose value hasn't been retrieved yet is not under
|
|
2791
|
+
* construction: #value? returns true and #partial_value? returns false.
|
|
2792
|
+
*/
|
|
2793
|
+
static VALUE cResumableParser_partial_value_p(VALUE self)
|
|
2794
|
+
{
|
|
2795
|
+
JSON_ResumableParser *parser = cResumableParser_get(self);
|
|
2796
|
+
|
|
2797
|
+
// Mirror of #value?: values on the stack while the document isn't DONE
|
|
2798
|
+
// belong to a partially built document. A container whose first key or
|
|
2799
|
+
// element hasn't been parsed yet has no frame nor value registered (the
|
|
2800
|
+
// tokenizer rewinds to the container start on EOS), so that state is
|
|
2801
|
+
// observable through the buffer (#eos?/#rest) instead, keeping this
|
|
2802
|
+
// predicate consistent with #partial_value returning nil.
|
|
2803
|
+
if (parser->value_stack.head > 0) {
|
|
2804
|
+
json_frame *frame = json_frame_stack_peek(&parser->frames);
|
|
2805
|
+
if (frame->phase != JSON_PHASE_DONE) {
|
|
2806
|
+
return Qtrue;
|
|
2807
|
+
}
|
|
2808
|
+
}
|
|
2809
|
+
return Qfalse;
|
|
2810
|
+
}
|
|
2811
|
+
|
|
2725
2812
|
/*
|
|
2726
2813
|
* call-seq: parsed_bytes -> integer
|
|
2727
2814
|
*
|
|
@@ -2778,6 +2865,7 @@ void Init_parser(void)
|
|
|
2778
2865
|
rb_define_method(cResumableParser, "value", cResumableParser_value, 0);
|
|
2779
2866
|
rb_define_method(cResumableParser, "value?", cResumableParser_value_p, 0);
|
|
2780
2867
|
rb_define_method(cResumableParser, "partial_value", cResumableParser_partial_value, 0);
|
|
2868
|
+
rb_define_method(cResumableParser, "partial_value?", cResumableParser_partial_value_p, 0);
|
|
2781
2869
|
rb_define_method(cResumableParser, "clear", cResumableParser_clear, 0);
|
|
2782
2870
|
rb_define_method(cResumableParser, "rest", cResumableParser_rest, 0);
|
|
2783
2871
|
rb_define_method(cResumableParser, "eos?", cResumableParser_eos_p, 0);
|
|
@@ -2812,7 +2900,9 @@ void Init_parser(void)
|
|
|
2812
2900
|
|
|
2813
2901
|
i_new = rb_intern("new");
|
|
2814
2902
|
i_try_convert = rb_intern("try_convert");
|
|
2903
|
+
#ifndef HAVE_RB_STR_TO_INTERNED_STR
|
|
2815
2904
|
i_uminus = rb_intern("-@");
|
|
2905
|
+
#endif
|
|
2816
2906
|
i_encode = rb_intern("encode");
|
|
2817
2907
|
i_at_line = rb_intern("@line");
|
|
2818
2908
|
i_at_column = rb_intern("@column");
|
data/lib/json/common.rb
CHANGED
|
@@ -155,6 +155,15 @@ module JSON
|
|
|
155
155
|
# Set the module _generator_ to be used by JSON.
|
|
156
156
|
def generator=(generator) # :nodoc:
|
|
157
157
|
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
|
+
|
|
158
167
|
@generator = generator
|
|
159
168
|
if generator.const_defined?(:GeneratorMethods)
|
|
160
169
|
generator_methods = generator::GeneratorMethods
|
data/lib/json/ext.rb
CHANGED
|
@@ -41,5 +41,31 @@ module JSON
|
|
|
41
41
|
end
|
|
42
42
|
end
|
|
43
43
|
|
|
44
|
+
if defined?(ResumableParser) # Not yet available on JRuby
|
|
45
|
+
class ResumableParser
|
|
46
|
+
# Returns whether the parser is entirely done: no unconsumed bytes in
|
|
47
|
+
# the buffer, no document under construction and no parsed value
|
|
48
|
+
# awaiting retrieval.
|
|
49
|
+
#
|
|
50
|
+
# The main use case is detecting a truncated stream once the input is
|
|
51
|
+
# exhausted:
|
|
52
|
+
#
|
|
53
|
+
# loop do
|
|
54
|
+
# begin
|
|
55
|
+
# parser << socket.readpartial(4096)
|
|
56
|
+
# rescue EOFError
|
|
57
|
+
# break
|
|
58
|
+
# end
|
|
59
|
+
# while parser.parse
|
|
60
|
+
# process(parser.value)
|
|
61
|
+
# end
|
|
62
|
+
# end
|
|
63
|
+
# warn "stream was truncated" unless parser.empty?
|
|
64
|
+
def empty?
|
|
65
|
+
eos? && !partial_value? && !value?
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
44
70
|
JSON_LOADED = true unless defined?(JSON::JSON_LOADED)
|
|
45
71
|
end
|
|
@@ -111,6 +111,8 @@ module JSON
|
|
|
111
111
|
# This class is used to create State instances, that are use to hold data
|
|
112
112
|
# while generating a JSON text from a Ruby data structure.
|
|
113
113
|
class State
|
|
114
|
+
singleton_class.attr_accessor :default_sort_keys_proc # :nodoc:
|
|
115
|
+
|
|
114
116
|
def self.generate(obj, opts = nil, io = nil)
|
|
115
117
|
new(opts).generate(obj, io)
|
|
116
118
|
end
|
|
@@ -164,6 +166,7 @@ module JSON
|
|
|
164
166
|
@script_safe = false
|
|
165
167
|
@strict = false
|
|
166
168
|
@max_nesting = 100
|
|
169
|
+
@sort_keys = false
|
|
167
170
|
configure(opts) if opts
|
|
168
171
|
end
|
|
169
172
|
|
|
@@ -199,6 +202,33 @@ module JSON
|
|
|
199
202
|
# supported by the JSON spec will raise a JSON::GeneratorError
|
|
200
203
|
attr_accessor :strict
|
|
201
204
|
|
|
205
|
+
# Controls key sorting in the generated JSON. If set to +true+, object
|
|
206
|
+
# keys are sorted by key lexicographically. If set to a Proc, it
|
|
207
|
+
# receives the entire Hash and must return a Hash with its pairs in the
|
|
208
|
+
# desired order.
|
|
209
|
+
attr_reader :sort_keys
|
|
210
|
+
|
|
211
|
+
def sort_keys=(value) # :nodoc:
|
|
212
|
+
type_error = false
|
|
213
|
+
@sort_keys = case value
|
|
214
|
+
when Proc
|
|
215
|
+
value
|
|
216
|
+
when true
|
|
217
|
+
State.default_sort_keys_proc
|
|
218
|
+
when nil, false
|
|
219
|
+
false
|
|
220
|
+
else
|
|
221
|
+
type_error = true
|
|
222
|
+
false
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
if type_error
|
|
226
|
+
raise TypeError, "The `sort_keys` argument must be a boolean or a Proc"
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
@sort_keys
|
|
230
|
+
end
|
|
231
|
+
|
|
202
232
|
# :stopdoc:
|
|
203
233
|
attr_reader :buffer_initial_length
|
|
204
234
|
|
|
@@ -285,6 +315,7 @@ module JSON
|
|
|
285
315
|
@allow_nan = !!opts[:allow_nan] if opts.key?(:allow_nan)
|
|
286
316
|
@as_json = opts[:as_json].to_proc if opts[:as_json]
|
|
287
317
|
@ascii_only = opts[:ascii_only] if opts.key?(:ascii_only)
|
|
318
|
+
self.sort_keys = opts[:sort_keys] if opts.key?(:sort_keys)
|
|
288
319
|
@depth = opts[:depth] || 0
|
|
289
320
|
@buffer_initial_length ||= opts[:buffer_initial_length]
|
|
290
321
|
|
|
@@ -349,9 +380,13 @@ module JSON
|
|
|
349
380
|
|
|
350
381
|
depth = @depth
|
|
351
382
|
if @indent.empty? and @space.empty? and @space_before.empty? and @object_nl.empty? and @array_nl.empty? and
|
|
352
|
-
!@ascii_only and !@script_safe and @max_nesting == 0 and (!@strict || Symbol === obj)
|
|
383
|
+
!@ascii_only and !@script_safe and @max_nesting == 0 and (!@strict || Symbol === obj) and !@sort_keys
|
|
353
384
|
result = generate_json(obj, ''.dup)
|
|
354
385
|
else
|
|
386
|
+
if @sort_keys
|
|
387
|
+
obj = @sort_keys.call(obj)
|
|
388
|
+
end
|
|
389
|
+
|
|
355
390
|
result = obj.to_json(self)
|
|
356
391
|
end
|
|
357
392
|
JSON::TruffleRuby::Generator.valid_utf8?(result) or raise GeneratorError.new(
|
data/lib/json/version.rb
CHANGED
data/lib/json.rb
CHANGED
|
@@ -408,7 +408,6 @@ require 'json/common'
|
|
|
408
408
|
# to be inserted after each \JSON object; defaults to the empty \String, <tt>''</tt>.
|
|
409
409
|
# - Option +indent+ (\String) specifies the string (usually spaces) to be
|
|
410
410
|
# used for indentation; defaults to the empty \String, <tt>''</tt>;
|
|
411
|
-
# defaults to the empty \String, <tt>''</tt>;
|
|
412
411
|
# has no effect unless options +array_nl+ or +object_nl+ specify newlines.
|
|
413
412
|
# - Option +space+ (\String) specifies a string (usually a space) to be
|
|
414
413
|
# inserted after the colon in each \JSON object's pair;
|
|
@@ -416,6 +415,11 @@ require 'json/common'
|
|
|
416
415
|
# - Option +space_before+ (\String) specifies a string (usually a space) to be
|
|
417
416
|
# inserted before the colon in each \JSON object's pair;
|
|
418
417
|
# 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.
|
|
419
423
|
#
|
|
420
424
|
# In this example, +obj+ is used first to generate the shortest
|
|
421
425
|
# \JSON data (no whitespace), then again with all formatting options
|