Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optimize Symbol generation in strict mode #745

Merged
merged 1 commit into from
Feb 5, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion CHANGES.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
37 changes: 29 additions & 8 deletions ext/json/ext/generator/generator.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -1049,7 +1072,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) {
Expand All @@ -1063,6 +1085,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;
}
Expand All @@ -1084,6 +1108,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);
Expand All @@ -1102,14 +1129,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);
}
}
}
Expand Down
25 changes: 25 additions & 0 deletions java/src/json/ext/Generator.java
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ private static <T extends IRubyObject> Handler<? super T> getHandlerFor(Ruby run
case FLOAT : return (Handler<T>) FLOAT_HANDLER;
case FIXNUM : return (Handler<T>) FIXNUM_HANDLER;
case BIGNUM : return (Handler<T>) BIGNUM_HANDLER;
case SYMBOL :
return (Handler<T>) SYMBOL_HANDLER;
case STRING :
if (Helpers.metaclass(object) != runtime.getString()) break;
return (Handler<T>) STRING_HANDLER;
Expand Down Expand Up @@ -458,6 +460,29 @@ void generate(ThreadContext context, Session session, RubyString object, OutputS
}
};

static final Handler<RubySymbol> SYMBOL_HANDLER =
new Handler<RubySymbol>() {
@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;
Expand Down
9 changes: 7 additions & 2 deletions lib/json/add/symbol.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
23 changes: 20 additions & 3 deletions lib/json/truffle_ruby/generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -591,6 +597,17 @@ def to_json(state = nil, *)
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
Expand Down
4 changes: 4 additions & 0 deletions test/json/json_generator_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down