diff options
author | Jean Boussier <[email protected]> | 2025-03-27 12:25:08 +0100 |
---|---|---|
committer | Hiroshi SHIBATA <[email protected]> | 2025-03-28 12:44:53 +0900 |
commit | ec171b4075407d02698a445e169f57fd68a9dcfc (patch) | |
tree | 699ff6c756f7d364cc7896798ba5a1ff04076125 | |
parent | e8c46f4ca5e6ba2638fbfc81fdb9d141cd88e99a (diff) |
[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
Notes
Notes:
Merged: https://github.com/ruby/ruby/pull/13004
-rw-r--r-- | ext/json/lib/json/common.rb | 127 | ||||
-rw-r--r-- | ext/json/parser/extconf.rb | 1 | ||||
-rw-r--r-- | ext/json/parser/parser.c | 151 | ||||
-rw-r--r-- | test/json/json_addition_test.rb | 7 |
4 files changed, 130 insertions, 156 deletions
diff --git a/ext/json/lib/json/common.rb b/ext/json/lib/json/common.rb index 17c7e3e249..a542bdb361 100644 --- a/ext/json/lib/json/common.rb +++ b/ext/json/lib/json/common.rb @@ -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: diff --git a/ext/json/parser/extconf.rb b/ext/json/parser/extconf.rb index a8e21aed4b..09c9637788 100644 --- a/ext/json/parser/extconf.rb +++ b/ext/json/parser/extconf.rb @@ -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") diff --git a/ext/json/parser/parser.c b/ext/json/parser/parser.c index b08fadd7df..f20769a365 100644 --- a/ext/json/parser/parser.c +++ b/ext/json/parser/parser.c @@ -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("<<"); diff --git a/test/json/json_addition_test.rb b/test/json/json_addition_test.rb index 4d8d186873..a98e06df8c 100644 --- a/test/json/json_addition_test.rb +++ b/test/json/json_addition_test.rb @@ -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 |