Skip to content

Commit

Permalink
Optimize Symbol generation in strict mode
Browse files Browse the repository at this point in the history
Co-authored-by: Jean Boussier <[email protected]>
  • Loading branch information
etiennebarrie and byroot committed Feb 5, 2025
1 parent c472d72 commit d37638e
Show file tree
Hide file tree
Showing 6 changed files with 86 additions and 14 deletions.
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

0 comments on commit d37638e

Please sign in to comment.