[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
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
# :call-seq:
# JSON[object] -> new_array or new_string
@ -236,9 +342,16 @@ module JSON
# JSON.parse('')
#
def parse(source, opts = nil)
opts = ParserOptions.prepare(opts) unless opts.nil?
Parser.parse(source, opts)
end
PARSE_L_OPTIONS = {
max_nesting: false,
allow_nan: true,
}.freeze
private_constant :PARSE_L_OPTIONS
# :call-seq:
# JSON.parse!(source, opts) -> object
#
@ -251,12 +364,11 @@ module JSON
# which disables checking for nesting depth.
# - Option +allow_nan+, if not provided, defaults to +true+.
def parse!(source, opts = nil)
options = {
:max_nesting => false,
:allow_nan => true
}
options.merge!(opts) if opts
Parser.new(source, options).parse
if opts.nil?
parse(source, PARSE_L_OPTIONS)
else
parse(source, PARSE_L_OPTIONS.merge(opts))
end
end
# :call-seq:
@ -859,10 +971,9 @@ module JSON
options[:strict] = true
end
options[:as_json] = as_json if as_json
options[:create_additions] = false unless options.key?(:create_additions)
@state = State.new(options).freeze
@parser_config = Ext::Parser::Config.new(options)
@parser_config = Ext::Parser::Config.new(ParserOptions.prepare(options))
end
# 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_hash_new_capa", "ruby.h") # RUBY_VERSION >= 3.2
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
append_cflags("-std=c99")

View File

@ -31,28 +31,15 @@ typedef unsigned char _Bool;
static VALUE mJSON, eNestingError, Encoding_UTF_8;
static VALUE CNaN, CInfinity, CMinusInfinity;
static ID i_json_creatable_p, i_json_create, i_create_id,
i_chr, i_deep_const_get, i_match, i_aset, i_aref,
static ID i_chr, i_aset, i_aref,
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,
sym_create_additions, sym_create_id, sym_object_class, sym_array_class,
sym_decimal_class, sym_match_string, sym_on_load;
sym_decimal_class, sym_on_load;
static int binary_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
// For TruffleRuby
void
@ -445,20 +432,14 @@ static int convert_UTF32_to_UTF8(char *buf, uint32_t ch)
typedef struct JSON_ParserStruct {
VALUE on_load_proc;
VALUE create_id;
VALUE object_class;
VALUE array_class;
VALUE decimal_class;
ID decimal_method_id;
VALUE match_string;
int max_nesting;
bool allow_nan;
bool allow_trailing_comma;
bool parsing_name;
bool symbolize_names;
bool freeze;
bool create_additions;
bool deprecated_create_additions;
} JSON_ParserConfig;
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)
{
VALUE array;
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));
}
VALUE array = rb_ary_new_from_values(count, rvalue_stack_peek(state->stack, count));
rvalue_stack_pop(state->stack, count);
if (config->freeze) {
@ -791,52 +761,13 @@ static inline VALUE json_decode_array(JSON_ParserState *state, JSON_ParserConfig
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)
{
VALUE object;
if (RB_UNLIKELY(config->object_class)) {
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);
}
VALUE object = rb_hash_new_capa(count);
rb_hash_bulk_insert(count, rvalue_stack_peek(state->stack, count), object);
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) {
RB_OBJ_FREEZE(object);
}
@ -844,17 +775,6 @@ static inline VALUE json_decode_object(JSON_ParserState *state, JSON_ParserConfi
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)
{
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);
}
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;
}
@ -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_freeze) { config->freeze = RTEST(val); }
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) {
if (RTEST(val)) {
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;
}
@ -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
// the provided keys than to check all possible keys.
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
* also the default. It's not possible to use this option in
* 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
* (Float) when parsing decimal numbers. This class must accept a single
* string argument in its constructor.
@ -1338,11 +1215,7 @@ static VALUE cParserConfig_initialize(VALUE self, VALUE 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->match_string);
return self;
}
@ -1406,11 +1279,7 @@ static void JSON_ParserConfig_mark(void *ptr)
{
JSON_ParserConfig *config = ptr;
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->match_string);
}
static void JSON_ParserConfig_free(void *ptr)
@ -1479,19 +1348,9 @@ void Init_parser(void)
sym_symbolize_names = ID2SYM(rb_intern("symbolize_names"));
sym_freeze = ID2SYM(rb_intern("freeze"));
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_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_match = rb_intern("match");
i_deep_const_get = rb_intern("deep_const_get");
i_aset = rb_intern("[]=");
i_aref = rb_intern("[]");
i_leftshift = rb_intern("<<");

View File

@ -151,7 +151,12 @@ class JSONAdditionTest < Test::Unit::TestCase
end
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))
end
end