[ruby/json] Move create_addtions logic in Ruby.

By leveraging the `on_load` callback we can move all this logic
out of the parser. Which mean we no longer have to duplicate
that logic in both parser and that we'll later be able to extract
it entirely from the gem.

https://github.com/ruby/json/commit/f411ddf1ce
This commit is contained in:
Jean Boussier 2025-03-27 12:25:08 +01:00 committed by Hiroshi SHIBATA
parent e8c46f4ca5
commit ec171b4075
Notes: git 2025-03-28 03:45:12 +00:00
4 changed files with 130 additions and 156 deletions

View File

@ -5,6 +5,112 @@ require 'json/version'
module JSON module JSON
autoload :GenericObject, 'json/generic_object' autoload :GenericObject, 'json/generic_object'
module ParserOptions # :nodoc:
class << self
def prepare(opts)
if opts[:object_class] || opts[:array_class]
opts = opts.dup
on_load = opts[:on_load]
on_load = object_class_proc(opts[:object_class], on_load) if opts[:object_class]
on_load = array_class_proc(opts[:array_class], on_load) if opts[:array_class]
opts[:on_load] = on_load
end
if opts.fetch(:create_additions, false) != false
opts = create_additions_proc(opts)
end
opts
end
private
def object_class_proc(object_class, on_load)
->(obj) do
if Hash === obj
object = object_class.new
obj.each { |k, v| object[k] = v }
obj = object
end
on_load.nil? ? obj : on_load.call(obj)
end
end
def array_class_proc(array_class, on_load)
->(obj) do
if Array === obj
array = array_class.new
obj.each { |v| array << v }
obj = array
end
on_load.nil? ? obj : on_load.call(obj)
end
end
# TODO: exact :create_additions support to another gem for version 3.0
def create_additions_proc(opts)
if opts[:symbolize_names]
raise ArgumentError, "options :symbolize_names and :create_additions cannot be used in conjunction"
end
opts = opts.dup
create_additions = opts.fetch(:create_additions, false)
on_load = opts[:on_load]
object_class = opts[:object_class] || Hash
opts[:on_load] = ->(object) do
case object
when String
opts[:match_string]&.each do |pattern, klass|
if match = pattern.match(object)
create_additions_warning if create_additions.nil?
object = klass.json_create(object)
break
end
end
when object_class
if opts[:create_additions] != false
if class_name = object[JSON.create_id]
klass = JSON.deep_const_get(class_name)
if (klass.respond_to?(:json_creatable?) && klass.json_creatable?) || klass.respond_to?(:json_create)
create_additions_warning if create_additions.nil?
object = klass.json_create(object)
end
end
end
end
on_load.nil? ? object : on_load.call(object)
end
opts
end
GEM_ROOT = File.expand_path("../../../", __FILE__) + "/"
def create_additions_warning
message = "JSON.load implicit support for `create_additions: true` is deprecated " \
"and will be removed in 3.0, use JSON.unsafe_load or explicitly " \
"pass `create_additions: true`"
uplevel = 4
caller_locations(uplevel, 10).each do |frame|
if frame.path.nil? || frame.path.start_with?(GEM_ROOT) || frame.path.end_with?("/truffle/cext_ruby.rb", ".c")
uplevel += 1
else
break
end
end
if RUBY_VERSION >= "3.0"
warn(message, uplevel: uplevel - 1, category: :deprecated)
else
warn(message, uplevel: uplevel - 1)
end
end
end
end
class << self class << self
# :call-seq: # :call-seq:
# JSON[object] -> new_array or new_string # JSON[object] -> new_array or new_string
@ -236,9 +342,16 @@ module JSON
# JSON.parse('') # JSON.parse('')
# #
def parse(source, opts = nil) def parse(source, opts = nil)
opts = ParserOptions.prepare(opts) unless opts.nil?
Parser.parse(source, opts) Parser.parse(source, opts)
end end
PARSE_L_OPTIONS = {
max_nesting: false,
allow_nan: true,
}.freeze
private_constant :PARSE_L_OPTIONS
# :call-seq: # :call-seq:
# JSON.parse!(source, opts) -> object # JSON.parse!(source, opts) -> object
# #
@ -251,12 +364,11 @@ module JSON
# which disables checking for nesting depth. # which disables checking for nesting depth.
# - Option +allow_nan+, if not provided, defaults to +true+. # - Option +allow_nan+, if not provided, defaults to +true+.
def parse!(source, opts = nil) def parse!(source, opts = nil)
options = { if opts.nil?
:max_nesting => false, parse(source, PARSE_L_OPTIONS)
:allow_nan => true else
} parse(source, PARSE_L_OPTIONS.merge(opts))
options.merge!(opts) if opts end
Parser.new(source, options).parse
end end
# :call-seq: # :call-seq:
@ -859,10 +971,9 @@ module JSON
options[:strict] = true options[:strict] = true
end end
options[:as_json] = as_json if as_json options[:as_json] = as_json if as_json
options[:create_additions] = false unless options.key?(:create_additions)
@state = State.new(options).freeze @state = State.new(options).freeze
@parser_config = Ext::Parser::Config.new(options) @parser_config = Ext::Parser::Config.new(ParserOptions.prepare(options))
end end
# call-seq: # call-seq:

View File

@ -4,7 +4,6 @@ require 'mkmf'
have_func("rb_enc_interned_str", "ruby.h") # RUBY_VERSION >= 3.0 have_func("rb_enc_interned_str", "ruby.h") # RUBY_VERSION >= 3.0
have_func("rb_hash_new_capa", "ruby.h") # RUBY_VERSION >= 3.2 have_func("rb_hash_new_capa", "ruby.h") # RUBY_VERSION >= 3.2
have_func("rb_hash_bulk_insert", "ruby.h") # Missing on TruffleRuby have_func("rb_hash_bulk_insert", "ruby.h") # Missing on TruffleRuby
have_func("rb_category_warn", "ruby.h") # Missing on TruffleRuby
have_func("strnlen", "string.h") # Missing on Solaris 10 have_func("strnlen", "string.h") # Missing on Solaris 10
append_cflags("-std=c99") append_cflags("-std=c99")

View File

@ -31,28 +31,15 @@ typedef unsigned char _Bool;
static VALUE mJSON, eNestingError, Encoding_UTF_8; static VALUE mJSON, eNestingError, Encoding_UTF_8;
static VALUE CNaN, CInfinity, CMinusInfinity; static VALUE CNaN, CInfinity, CMinusInfinity;
static ID i_json_creatable_p, i_json_create, i_create_id, static ID i_chr, i_aset, i_aref,
i_chr, i_deep_const_get, i_match, i_aset, i_aref,
i_leftshift, i_new, i_try_convert, i_uminus, i_encode; i_leftshift, i_new, i_try_convert, i_uminus, i_encode;
static VALUE sym_max_nesting, sym_allow_nan, sym_allow_trailing_comma, sym_symbolize_names, sym_freeze, static VALUE sym_max_nesting, sym_allow_nan, sym_allow_trailing_comma, sym_symbolize_names, sym_freeze,
sym_create_additions, sym_create_id, sym_object_class, sym_array_class, sym_decimal_class, sym_on_load;
sym_decimal_class, sym_match_string, sym_on_load;
static int binary_encindex; static int binary_encindex;
static int utf8_encindex; static int utf8_encindex;
#ifdef HAVE_RB_CATEGORY_WARN
# define json_deprecated(message) rb_category_warn(RB_WARN_CATEGORY_DEPRECATED, message)
#else
# define json_deprecated(message) rb_warn(message)
#endif
static const char deprecated_create_additions_warning[] =
"JSON.load implicit support for `create_additions: true` is deprecated "
"and will be removed in 3.0, use JSON.unsafe_load or explicitly "
"pass `create_additions: true`";
#ifndef HAVE_RB_HASH_BULK_INSERT #ifndef HAVE_RB_HASH_BULK_INSERT
// For TruffleRuby // For TruffleRuby
void void
@ -445,20 +432,14 @@ static int convert_UTF32_to_UTF8(char *buf, uint32_t ch)
typedef struct JSON_ParserStruct { typedef struct JSON_ParserStruct {
VALUE on_load_proc; VALUE on_load_proc;
VALUE create_id;
VALUE object_class;
VALUE array_class;
VALUE decimal_class; VALUE decimal_class;
ID decimal_method_id; ID decimal_method_id;
VALUE match_string;
int max_nesting; int max_nesting;
bool allow_nan; bool allow_nan;
bool allow_trailing_comma; bool allow_trailing_comma;
bool parsing_name; bool parsing_name;
bool symbolize_names; bool symbolize_names;
bool freeze; bool freeze;
bool create_additions;
bool deprecated_create_additions;
} JSON_ParserConfig; } JSON_ParserConfig;
typedef struct JSON_ParserStateStruct { typedef struct JSON_ParserStateStruct {
@ -770,18 +751,7 @@ static VALUE json_decode_float(JSON_ParserConfig *config, const char *start, con
static inline VALUE json_decode_array(JSON_ParserState *state, JSON_ParserConfig *config, long count) static inline VALUE json_decode_array(JSON_ParserState *state, JSON_ParserConfig *config, long count)
{ {
VALUE array; VALUE array = rb_ary_new_from_values(count, rvalue_stack_peek(state->stack, count));
if (RB_UNLIKELY(config->array_class)) {
array = rb_class_new_instance(0, 0, config->array_class);
VALUE *items = rvalue_stack_peek(state->stack, count);
long index;
for (index = 0; index < count; index++) {
rb_funcall(array, i_leftshift, 1, items[index]);
}
} else {
array = rb_ary_new_from_values(count, rvalue_stack_peek(state->stack, count));
}
rvalue_stack_pop(state->stack, count); rvalue_stack_pop(state->stack, count);
if (config->freeze) { if (config->freeze) {
@ -791,52 +761,13 @@ static inline VALUE json_decode_array(JSON_ParserState *state, JSON_ParserConfig
return array; return array;
} }
static bool json_obj_creatable_p(VALUE klass)
{
if (rb_respond_to(klass, i_json_creatable_p)) {
return RTEST(rb_funcall(klass, i_json_creatable_p, 0));
} else {
return rb_respond_to(klass, i_json_create);
}
}
static inline VALUE json_decode_object(JSON_ParserState *state, JSON_ParserConfig *config, long count) static inline VALUE json_decode_object(JSON_ParserState *state, JSON_ParserConfig *config, long count)
{ {
VALUE object; VALUE object = rb_hash_new_capa(count);
if (RB_UNLIKELY(config->object_class)) { rb_hash_bulk_insert(count, rvalue_stack_peek(state->stack, count), object);
object = rb_class_new_instance(0, 0, config->object_class);
long index = 0;
VALUE *items = rvalue_stack_peek(state->stack, count);
while (index < count) {
VALUE name = items[index++];
VALUE value = items[index++];
rb_funcall(object, i_aset, 2, name, value);
}
} else {
object = rb_hash_new_capa(count);
rb_hash_bulk_insert(count, rvalue_stack_peek(state->stack, count), object);
}
rvalue_stack_pop(state->stack, count); rvalue_stack_pop(state->stack, count);
if (RB_UNLIKELY(config->create_additions)) {
VALUE klassname;
if (config->object_class) {
klassname = rb_funcall(object, i_aref, 1, config->create_id);
} else {
klassname = rb_hash_aref(object, config->create_id);
}
if (!NIL_P(klassname)) {
VALUE klass = rb_funcall(mJSON, i_deep_const_get, 1, klassname);
if (json_obj_creatable_p(klass)) {
if (config->deprecated_create_additions) {
json_deprecated(deprecated_create_additions_warning);
}
object = rb_funcall(klass, i_json_create, 1, object);
}
}
}
if (config->freeze) { if (config->freeze) {
RB_OBJ_FREEZE(object); RB_OBJ_FREEZE(object);
} }
@ -844,17 +775,6 @@ static inline VALUE json_decode_object(JSON_ParserState *state, JSON_ParserConfi
return object; return object;
} }
static int match_i(VALUE regexp, VALUE klass, VALUE memo)
{
if (regexp == Qundef) return ST_STOP;
if (json_obj_creatable_p(klass) &&
RTEST(rb_funcall(regexp, i_match, 1, rb_ary_entry(memo, 0)))) {
rb_ary_push(memo, klass);
return ST_STOP;
}
return ST_CONTINUE;
}
static inline VALUE json_decode_string(JSON_ParserState *state, JSON_ParserConfig *config, const char *start, const char *end, bool escaped, bool is_name) static inline VALUE json_decode_string(JSON_ParserState *state, JSON_ParserConfig *config, const char *start, const char *end, bool escaped, bool is_name)
{ {
VALUE string; VALUE string;
@ -866,17 +786,6 @@ static inline VALUE json_decode_string(JSON_ParserState *state, JSON_ParserConfi
string = json_string_fastpath(state, start, end, is_name, intern, symbolize); string = json_string_fastpath(state, start, end, is_name, intern, symbolize);
} }
if (RB_UNLIKELY(config->create_additions && RTEST(config->match_string))) {
VALUE klass;
VALUE memo = rb_ary_new2(2);
rb_ary_push(memo, string);
rb_hash_foreach(config->match_string, match_i, memo);
klass = rb_ary_entry(memo, 1);
if (RTEST(klass)) {
string = rb_funcall(klass, i_json_create, 1, string);
}
}
return string; return string;
} }
@ -1229,10 +1138,6 @@ static int parser_config_init_i(VALUE key, VALUE val, VALUE data)
else if (key == sym_symbolize_names) { config->symbolize_names = RTEST(val); } else if (key == sym_symbolize_names) { config->symbolize_names = RTEST(val); }
else if (key == sym_freeze) { config->freeze = RTEST(val); } else if (key == sym_freeze) { config->freeze = RTEST(val); }
else if (key == sym_on_load) { config->on_load_proc = RTEST(val) ? val : Qfalse; } else if (key == sym_on_load) { config->on_load_proc = RTEST(val) ? val : Qfalse; }
else if (key == sym_create_id) { config->create_id = RTEST(val) ? val : Qfalse; }
else if (key == sym_object_class) { config->object_class = RTEST(val) ? val : Qfalse; }
else if (key == sym_array_class) { config->array_class = RTEST(val) ? val : Qfalse; }
else if (key == sym_match_string) { config->match_string = RTEST(val) ? val : Qfalse; }
else if (key == sym_decimal_class) { else if (key == sym_decimal_class) {
if (RTEST(val)) { if (RTEST(val)) {
if (rb_respond_to(val, i_try_convert)) { if (rb_respond_to(val, i_try_convert)) {
@ -1262,15 +1167,6 @@ static int parser_config_init_i(VALUE key, VALUE val, VALUE data)
} }
} }
} }
else if (key == sym_create_additions) {
if (NIL_P(val)) {
config->create_additions = true;
config->deprecated_create_additions = true;
} else {
config->create_additions = RTEST(val);
config->deprecated_create_additions = false;
}
}
return ST_CONTINUE; return ST_CONTINUE;
} }
@ -1285,16 +1181,6 @@ static void parser_config_init(JSON_ParserConfig *config, VALUE opts)
// We assume in most cases few keys are set so it's faster to go over // We assume in most cases few keys are set so it's faster to go over
// the provided keys than to check all possible keys. // the provided keys than to check all possible keys.
rb_hash_foreach(opts, parser_config_init_i, (VALUE)config); rb_hash_foreach(opts, parser_config_init_i, (VALUE)config);
if (config->symbolize_names && config->create_additions) {
rb_raise(rb_eArgError,
"options :symbolize_names and :create_additions cannot be "
" used in conjunction");
}
if (config->create_additions && !config->create_id) {
config->create_id = rb_funcall(mJSON, i_create_id, 0);
}
} }
} }
@ -1319,15 +1205,6 @@ static void parser_config_init(JSON_ParserConfig *config, VALUE opts)
* (keys) in a JSON object. Otherwise strings are returned, which is * (keys) in a JSON object. Otherwise strings are returned, which is
* also the default. It's not possible to use this option in * also the default. It's not possible to use this option in
* conjunction with the *create_additions* option. * conjunction with the *create_additions* option.
* * *create_additions*: If set to false, the Parser doesn't create
* additions even if a matching class and create_id was found. This option
* defaults to false.
* * *object_class*: Defaults to Hash. If another type is provided, it will be used
* instead of Hash to represent JSON objects. The type must respond to
* +new+ without arguments, and return an object that respond to +[]=+.
* * *array_class*: Defaults to Array If another type is provided, it will be used
* instead of Hash to represent JSON arrays. The type must respond to
* +new+ without arguments, and return an object that respond to +<<+.
* * *decimal_class*: Specifies which class to use instead of the default * * *decimal_class*: Specifies which class to use instead of the default
* (Float) when parsing decimal numbers. This class must accept a single * (Float) when parsing decimal numbers. This class must accept a single
* string argument in its constructor. * string argument in its constructor.
@ -1338,11 +1215,7 @@ static VALUE cParserConfig_initialize(VALUE self, VALUE opts)
parser_config_init(config, opts); parser_config_init(config, opts);
RB_OBJ_WRITTEN(self, Qundef, config->create_id);
RB_OBJ_WRITTEN(self, Qundef, config->object_class);
RB_OBJ_WRITTEN(self, Qundef, config->array_class);
RB_OBJ_WRITTEN(self, Qundef, config->decimal_class); RB_OBJ_WRITTEN(self, Qundef, config->decimal_class);
RB_OBJ_WRITTEN(self, Qundef, config->match_string);
return self; return self;
} }
@ -1406,11 +1279,7 @@ static void JSON_ParserConfig_mark(void *ptr)
{ {
JSON_ParserConfig *config = ptr; JSON_ParserConfig *config = ptr;
rb_gc_mark(config->on_load_proc); rb_gc_mark(config->on_load_proc);
rb_gc_mark(config->create_id);
rb_gc_mark(config->object_class);
rb_gc_mark(config->array_class);
rb_gc_mark(config->decimal_class); rb_gc_mark(config->decimal_class);
rb_gc_mark(config->match_string);
} }
static void JSON_ParserConfig_free(void *ptr) static void JSON_ParserConfig_free(void *ptr)
@ -1479,19 +1348,9 @@ void Init_parser(void)
sym_symbolize_names = ID2SYM(rb_intern("symbolize_names")); sym_symbolize_names = ID2SYM(rb_intern("symbolize_names"));
sym_freeze = ID2SYM(rb_intern("freeze")); sym_freeze = ID2SYM(rb_intern("freeze"));
sym_on_load = ID2SYM(rb_intern("on_load")); sym_on_load = ID2SYM(rb_intern("on_load"));
sym_create_additions = ID2SYM(rb_intern("create_additions"));
sym_create_id = ID2SYM(rb_intern("create_id"));
sym_object_class = ID2SYM(rb_intern("object_class"));
sym_array_class = ID2SYM(rb_intern("array_class"));
sym_decimal_class = ID2SYM(rb_intern("decimal_class")); sym_decimal_class = ID2SYM(rb_intern("decimal_class"));
sym_match_string = ID2SYM(rb_intern("match_string"));
i_create_id = rb_intern("create_id");
i_json_creatable_p = rb_intern("json_creatable?");
i_json_create = rb_intern("json_create");
i_chr = rb_intern("chr"); i_chr = rb_intern("chr");
i_match = rb_intern("match");
i_deep_const_get = rb_intern("deep_const_get");
i_aset = rb_intern("[]="); i_aset = rb_intern("[]=");
i_aref = rb_intern("[]"); i_aref = rb_intern("[]");
i_leftshift = rb_intern("<<"); i_leftshift = rb_intern("<<");

View File

@ -151,7 +151,12 @@ class JSONAdditionTest < Test::Unit::TestCase
end end
def test_deprecated_load_create_additions def test_deprecated_load_create_additions
assert_deprecated_warning(/use JSON\.unsafe_load/) do pattern = /json_addition_test\.rb.*use JSON\.unsafe_load/
if RUBY_ENGINE == 'truffleruby'
pattern = /use JSON\.unsafe_load/
end
assert_deprecated_warning(pattern) do
JSON.load(JSON.dump(Time.now)) JSON.load(JSON.dump(Time.now))
end end
end end