diff --git a/CHANGES.md b/CHANGES.md index 456a6887..927365a2 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,9 +1,9 @@ # Changes +* `strict: true` now accept symbols as values. Previously they'd only be accepted as hash keys. * The C extension Parser has been entirely reimplemented from scratch. * Introduced `JSON::Coder` as a new API allowing to customize how non native types are serialized in a non-global way. - ### 2024-12-18 (2.9.1) * Fix support for Solaris 10. diff --git a/ext/json/ext/generator/generator.c b/ext/json/ext/generator/generator.c index 119b1dfd..bb390d1b 100644 --- a/ext/json/ext/generator/generator.c +++ b/ext/json/ext/generator/generator.c @@ -991,6 +991,29 @@ static void generate_json_string(FBuffer *buffer, struct generate_json_data *dat fbuffer_append_char(buffer, '"'); } +static void generate_json_fallback(FBuffer *buffer, struct generate_json_data *data, JSON_Generator_State *state, VALUE obj) +{ + VALUE tmp; + if (rb_respond_to(obj, i_to_json)) { + tmp = rb_funcall(obj, i_to_json, 1, vstate_get(data)); + Check_Type(tmp, T_STRING); + fbuffer_append_str(buffer, tmp); + } else { + tmp = rb_funcall(obj, i_to_s, 0); + Check_Type(tmp, T_STRING); + generate_json_string(buffer, data, state, tmp); + } +} + +static inline void generate_json_symbol(FBuffer *buffer, struct generate_json_data *data, JSON_Generator_State *state, VALUE obj) +{ + if (state->strict) { + generate_json_string(buffer, data, state, rb_sym2str(obj)); + } else { + generate_json_fallback(buffer, data, state, obj); + } +} + static void generate_json_null(FBuffer *buffer, struct generate_json_data *data, JSON_Generator_State *state, VALUE obj) { fbuffer_append(buffer, "null", 4); @@ -1057,7 +1080,6 @@ static void generate_json_fragment(FBuffer *buffer, struct generate_json_data *d static void generate_json(FBuffer *buffer, struct generate_json_data *data, JSON_Generator_State *state, VALUE obj) { - VALUE tmp; bool as_json_called = false; start: if (obj == Qnil) { @@ -1071,6 +1093,8 @@ static void generate_json(FBuffer *buffer, struct generate_json_data *data, JSON generate_json_fixnum(buffer, data, state, obj); } else if (RB_FLONUM_P(obj)) { generate_json_float(buffer, data, state, obj); + } else if (RB_STATIC_SYM_P(obj)) { + generate_json_symbol(buffer, data, state, obj); } else { goto general; } @@ -1092,6 +1116,9 @@ static void generate_json(FBuffer *buffer, struct generate_json_data *data, JSON if (klass != rb_cString) goto general; generate_json_string(buffer, data, state, obj); break; + case T_SYMBOL: + generate_json_symbol(buffer, data, state, obj); + break; case T_FLOAT: if (klass != rb_cFloat) goto general; generate_json_float(buffer, data, state, obj); @@ -1110,14 +1137,8 @@ static void generate_json(FBuffer *buffer, struct generate_json_data *data, JSON } else { raise_generator_error(obj, "%"PRIsVALUE" not allowed in JSON", CLASS_OF(obj)); } - } else if (rb_respond_to(obj, i_to_json)) { - tmp = rb_funcall(obj, i_to_json, 1, vstate_get(data)); - Check_Type(tmp, T_STRING); - fbuffer_append_str(buffer, tmp); } else { - tmp = rb_funcall(obj, i_to_s, 0); - Check_Type(tmp, T_STRING); - generate_json_string(buffer, data, state, tmp); + generate_json_fallback(buffer, data, state, obj); } } } diff --git a/java/src/json/ext/Generator.java b/java/src/json/ext/Generator.java index f5b1beb7..36914b73 100644 --- a/java/src/json/ext/Generator.java +++ b/java/src/json/ext/Generator.java @@ -108,6 +108,8 @@ private static Handler getHandlerFor(Ruby run case FLOAT : return (Handler) FLOAT_HANDLER; case FIXNUM : return (Handler) FIXNUM_HANDLER; case BIGNUM : return (Handler) BIGNUM_HANDLER; + case SYMBOL : + return (Handler) SYMBOL_HANDLER; case STRING : if (Helpers.metaclass(object) != runtime.getString()) break; return (Handler) STRING_HANDLER; @@ -468,6 +470,29 @@ void generate(ThreadContext context, Session session, RubyString object, OutputS } }; + static final Handler SYMBOL_HANDLER = + new Handler() { + @Override + int guessSize(ThreadContext context, Session session, RubySymbol object) { + GeneratorState state = session.getState(context); + if (state.strict()) { + return STRING_HANDLER.guessSize(context, session, object.asString()); + } else { + return GENERIC_HANDLER.guessSize(context, session, object); + } + } + + @Override + void generate(ThreadContext context, Session session, RubySymbol object, OutputStream buffer) throws IOException { + GeneratorState state = session.getState(context); + if (state.strict()) { + STRING_HANDLER.generate(context, session, object.asString(), buffer); + } else { + GENERIC_HANDLER.generate(context, session, object, buffer); + } + } + }; + static RubyString ensureValidEncoding(ThreadContext context, RubyString str) { Encoding encoding = str.getEncoding(); RubyString utf8String; diff --git a/lib/json/add/symbol.rb b/lib/json/add/symbol.rb index 82e6a885..20dd5948 100644 --- a/lib/json/add/symbol.rb +++ b/lib/json/add/symbol.rb @@ -36,8 +36,13 @@ def as_json(*) # # # {"json_class":"Symbol","s":"foo"} # - def to_json(*a) - as_json.to_json(*a) + def to_json(state = nil, *a) + state = ::JSON::State.from_state(state) + if state.strict? + super + else + as_json.to_json(state, *a) + end end # See #as_json. diff --git a/lib/json/truffle_ruby/generator.rb b/lib/json/truffle_ruby/generator.rb index ec4fb09b..37a980fe 100644 --- a/lib/json/truffle_ruby/generator.rb +++ b/lib/json/truffle_ruby/generator.rb @@ -303,7 +303,7 @@ def to_h # GeneratorError exception. def generate(obj, anIO = nil) if @indent.empty? and @space.empty? and @space_before.empty? and @object_nl.empty? and @array_nl.empty? and - !@ascii_only and !@script_safe and @max_nesting == 0 and !@strict + !@ascii_only and !@script_safe and @max_nesting == 0 and (!@strict || Symbol === obj) result = generate_json(obj, ''.dup) else result = obj.to_json(self) @@ -364,6 +364,12 @@ def generate_new(obj, anIO = nil) # :nodoc: end when Integer buf << obj.to_s + when Symbol + if @strict + fast_serialize_string(obj.name, buf) + else + buf << obj.to_json(self) + end else # Note: Float is handled this way since Float#to_s is slow anyway buf << obj.to_json(self) @@ -539,10 +545,10 @@ def json_transform(state) each { |value| result << delim unless first result << state.indent * depth if indent - if state.strict? && !(false == value || true == value || nil == value || String === value || Array === value || Hash === value || Integer === value || Float === value || Fragment === value) + if state.strict? && !(false == value || true == value || nil == value || String === value || Array === value || Hash === value || Integer === value || Float === value || Fragment === value || Symbol == value) if state.as_json value = state.as_json.call(value) - unless false == value || true == value || nil == value || String === value || Array === value || Hash === value || Integer === value || Float === value || Fragment === value + unless false == value || true == value || nil == value || String === value || Array === value || Hash === value || Integer === value || Float === value || Fragment === value || Symbol === value raise GeneratorError.new("#{value.class} returned by #{state.as_json} not allowed in JSON", value) end result << value.to_json(state) @@ -596,6 +602,17 @@ def to_json(state = nil, *args) end end + module Symbol + def to_json(state = nil, *args) + state = State.from_state(state) + if state.strict? + name.to_json(state, *args) + else + super + end + end + end + module String # This string should be encoded with UTF-8 A call to this method # returns a JSON string encoded with UTF16 big endian characters as diff --git a/test/json/json_generator_test.rb b/test/json/json_generator_test.rb index d97f0505..942802d6 100755 --- a/test/json/json_generator_test.rb +++ b/test/json/json_generator_test.rb @@ -86,6 +86,10 @@ def test_dump_strict assert_equal '42', dump(42, strict: true) assert_equal 'true', dump(true, strict: true) + + assert_equal '"hello"', dump(:hello, strict: true) + assert_equal '"hello"', :hello.to_json(strict: true) + assert_equal '"World"', "World".to_json(strict: true) end def test_generate_pretty