From 7576f5c762c78c49e8c00033b3dd03d1d9dfe763 Mon Sep 17 00:00:00 2001 From: Charles Oliver Nutter Date: Mon, 14 Aug 2023 19:25:04 -0500 Subject: [PATCH] When dumping to IO, dump directly Json.dump allows you to pass an IO to which the dump output will be sent, but it still buffers the entire output in memory before sending it to the given IO. This leads to issues on JRuby like jruby/jruby#6265 when it tries to create a byte[] that exceeds the maximum size of a signed int (JVM's array size limit). This commit plumbs the IO all the way through the generation logic so that it can be written to directly without filling a temporary memory buffer first. This allow JRuby to dump object graphs that would normally produce more content than the JVM can hold in a single array, providing a workaround for jruby/jruby#6265. It is unfortunately a bit slow to dump directly to IO due to the many small writes that all acquire locks and participate in the IO encoding subsystem. A more direct path that can skip some of these pieces could be more competitive with the in-memory version, but functionally it expands the size of graphs that cana be dumped when using JRuby. See flori/json#524 --- java/src/json/ext/ByteListTranscoder.java | 21 +-- java/src/json/ext/Generator.java | 148 +++++++++++++++------- java/src/json/ext/GeneratorState.java | 18 +++ java/src/json/ext/Parser.java | 14 +- java/src/json/ext/StringDecoder.java | 19 +-- java/src/json/ext/StringEncoder.java | 11 +- lib/json/common.rb | 14 +- lib/json/pure/generator.rb | 111 +++++++++------- 8 files changed, 228 insertions(+), 128 deletions(-) diff --git a/java/src/json/ext/ByteListTranscoder.java b/java/src/json/ext/ByteListTranscoder.java index 6f6ab66c..30075067 100644 --- a/java/src/json/ext/ByteListTranscoder.java +++ b/java/src/json/ext/ByteListTranscoder.java @@ -9,6 +9,9 @@ import org.jruby.runtime.ThreadContext; import org.jruby.util.ByteList; +import java.io.IOException; +import java.io.OutputStream; + /** * A class specialized in transcoding a certain String format into another, * using UTF-8 ByteLists as both input and output. @@ -23,7 +26,7 @@ abstract class ByteListTranscoder { /** Position of the next character to read */ protected int pos; - private ByteList out; + private OutputStream out; /** * When a character that can be copied straight into the output is found, * its index is stored on this variable, and copying is delayed until @@ -37,11 +40,11 @@ protected ByteListTranscoder(ThreadContext context) { this.context = context; } - protected void init(ByteList src, ByteList out) { + protected void init(ByteList src, OutputStream out) { this.init(src, 0, src.length(), out); } - protected void init(ByteList src, int start, int end, ByteList out) { + protected void init(ByteList src, int start, int end, OutputStream out) { this.src = src; this.pos = start; this.charStart = start; @@ -142,19 +145,19 @@ protected void quoteStart() { * recently read character, or {@link #charStart} to quote * until the character before it. */ - protected void quoteStop(int endPos) { + protected void quoteStop(int endPos) throws IOException { if (quoteStart != -1) { - out.append(src, quoteStart, endPos - quoteStart); + out.write(src.unsafeBytes(), src.begin() + quoteStart, src.begin() + endPos - quoteStart); quoteStart = -1; } } - protected void append(int b) { - out.append(b); + protected void append(int b) throws IOException { + out.write(b); } - protected void append(byte[] origin, int start, int length) { - out.append(origin, start, length); + protected void append(byte[] origin, int start, int length) throws IOException { + out.write(origin, start, length); } diff --git a/java/src/json/ext/Generator.java b/java/src/json/ext/Generator.java index 6a996868..bc2e0c57 100644 --- a/java/src/json/ext/Generator.java +++ b/java/src/json/ext/Generator.java @@ -5,6 +5,8 @@ */ package json.ext; +import org.jcodings.specific.USASCIIEncoding; +import org.jcodings.specific.UTF8Encoding; import org.jruby.Ruby; import org.jruby.RubyArray; import org.jruby.RubyBasicObject; @@ -13,10 +15,17 @@ import org.jruby.RubyFixnum; import org.jruby.RubyFloat; import org.jruby.RubyHash; +import org.jruby.RubyIO; import org.jruby.RubyString; +import org.jruby.runtime.Helpers; import org.jruby.runtime.ThreadContext; import org.jruby.runtime.builtin.IRubyObject; import org.jruby.util.ByteList; +import org.jruby.util.IOOutputStream; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; public final class Generator { private Generator() { @@ -55,6 +64,18 @@ private Generator() { return handler.generateNew(session, object); } + /** + * Encodes the given object as a JSON string, as in other forms, but + * outputs directly to the given stream + */ + public static void + generateJson(ThreadContext context, T object, + GeneratorState config, OutputStream out) { + Session session = new Session(context, config); + Handler handler = getHandlerFor(context.runtime, object); + handler.generateNew(session, object, out); + } + /** * Returns the best serialization handler for the given object. */ @@ -159,6 +180,16 @@ public T infect(T object) { /* Handler base classes */ + static class ByteListOutputStream extends ByteArrayOutputStream { + public ByteListOutputStream(int size) { + super(size); + } + + public ByteList toByteListDirect() { + return new ByteList(buf, 0, count); + } + } + private static abstract class Handler { /** * Returns an estimative of how much space the serialization of the @@ -171,16 +202,33 @@ int guessSize(Session session, T object) { RubyString generateNew(Session session, T object) { RubyString result; - ByteList buffer = new ByteList(guessSize(session, object)); - generate(session, object, buffer); - result = RubyString.newString(session.getRuntime(), buffer); + ByteListOutputStream blos = new ByteListOutputStream(guessSize(session, object)); + generateNew(session, object, blos); + result = RubyString.newString(session.getRuntime(), blos.toByteListDirect()); ThreadContext context = session.getContext(); RuntimeInfo info = session.getInfo(); result.force_encoding(context, info.utf8.get()); return result; } - abstract void generate(Session session, T object, ByteList buffer); + void generateNew(Session session, T object, RubyIO buffer) { + buffer.setEnc2(UTF8Encoding.INSTANCE); + generateNew(session, object, buffer); + } + + void generateNew(Session session, T object, OutputStream buffer) { + try { + generate(session, object, buffer); + } catch (IOException ioe) { + throw Helpers.newIOErrorFromException(session.getRuntime(), ioe); + } + } + + abstract void generate(Session session, T object, OutputStream os) throws IOException; + + protected void writeByteList(OutputStream os, ByteList byteList) throws IOException { + os.write(byteList.unsafeBytes(), byteList.begin(), byteList.realSize()); + } } /** @@ -205,8 +253,8 @@ RubyString generateNew(Session session, T object) { } @Override - void generate(Session session, T object, ByteList buffer) { - buffer.append(keyword); + void generate(Session session, T object, OutputStream buffer) throws IOException { + writeByteList(buffer, keyword); } } @@ -216,26 +264,27 @@ void generate(Session session, T object, ByteList buffer) { static final Handler BIGNUM_HANDLER = new Handler() { @Override - void generate(Session session, RubyBignum object, ByteList buffer) { + void generate(Session session, RubyBignum object, OutputStream buffer) throws IOException { // JRUBY-4751: RubyBignum.to_s() returns generic object // representation (fixed in 1.5, but we maintain backwards // compatibility; call to_s(IRubyObject[]) then - buffer.append(((RubyString)object.to_s(IRubyObject.NULL_ARRAY)).getByteList()); + byte[] bigIntStr = object.getBigIntegerValue().toString().getBytes(); + buffer.write(bigIntStr, 0, bigIntStr.length); } }; static final Handler FIXNUM_HANDLER = new Handler() { @Override - void generate(Session session, RubyFixnum object, ByteList buffer) { - buffer.append(object.to_s().getByteList()); + void generate(Session session, RubyFixnum object, OutputStream buffer) throws IOException { + writeByteList(buffer, object.to_s().getByteList()); } }; static final Handler FLOAT_HANDLER = new Handler() { @Override - void generate(Session session, RubyFloat object, ByteList buffer) { + void generate(Session session, RubyFloat object, OutputStream buffer) throws IOException { double value = RubyFloat.num2dbl(object); if (Double.isInfinite(value) || Double.isNaN(value)) { @@ -245,7 +294,7 @@ void generate(Session session, RubyFloat object, ByteList buffer) { object + " not allowed in JSON"); } } - buffer.append(((RubyString)object.to_s()).getByteList()); + writeByteList(buffer, ((RubyString)object.to_s()).getByteList()); } }; @@ -263,7 +312,7 @@ int guessSize(Session session, RubyArray object) { } @Override - void generate(Session session, RubyArray object, ByteList buffer) { + void generate(Session session, RubyArray object, OutputStream buffer) throws IOException { ThreadContext context = session.getContext(); Ruby runtime = context.getRuntime(); GeneratorState state = session.getState(); @@ -280,8 +329,8 @@ void generate(Session session, RubyArray object, ByteList buffer) { session.infectBy(object); - buffer.append((byte)'['); - buffer.append(arrayNl); + buffer.write((byte)'['); + buffer.write(arrayNl.unsafeBytes()); boolean firstItem = true; for (int i = 0, t = object.getLength(); i < t; i++) { IRubyObject element = object.eltInternal(i); @@ -289,20 +338,20 @@ void generate(Session session, RubyArray object, ByteList buffer) { if (firstItem) { firstItem = false; } else { - buffer.append(delim); + buffer.write(delim); } - buffer.append(shift); + buffer.write(shift); Handler handler = (Handler) getHandlerFor(runtime, element); handler.generate(session, element, buffer); } state.decreaseDepth(); if (arrayNl.length() != 0) { - buffer.append(arrayNl); - buffer.append(shift, 0, state.getDepth() * indentUnit.length()); + buffer.write(arrayNl.unsafeBytes()); + buffer.write(shift, 0, state.getDepth() * indentUnit.length()); } - buffer.append((byte)']'); + buffer.write((byte)']'); } }; @@ -321,7 +370,7 @@ int guessSize(Session session, RubyHash object) { @Override void generate(final Session session, RubyHash object, - final ByteList buffer) { + final OutputStream buffer) throws IOException { ThreadContext context = session.getContext(); final Ruby runtime = context.getRuntime(); final GeneratorState state = session.getState(); @@ -332,39 +381,43 @@ void generate(final Session session, RubyHash object, final ByteList spaceBefore = state.getSpaceBefore(); final ByteList space = state.getSpace(); - buffer.append((byte)'{'); - buffer.append(objectNl); + buffer.write((byte)'{'); + buffer.write(objectNl.unsafeBytes()); final boolean[] firstPair = new boolean[]{true}; object.visitAll(new RubyHash.Visitor() { @Override public void visit(IRubyObject key, IRubyObject value) { - if (firstPair[0]) { - firstPair[0] = false; - } else { - buffer.append((byte)','); - buffer.append(objectNl); + try { + if (firstPair[0]) { + firstPair[0] = false; + } else { + buffer.write((byte) ','); + buffer.write(objectNl.unsafeBytes()); + } + if (objectNl.length() != 0) buffer.write(indent); + + STRING_HANDLER.generate(session, key.asString(), buffer); + session.infectBy(key); + + buffer.write(spaceBefore.unsafeBytes()); + buffer.write((byte) ':'); + buffer.write(space.unsafeBytes()); + + Handler valueHandler = (Handler) getHandlerFor(runtime, value); + valueHandler.generate(session, value, buffer); + session.infectBy(value); + } catch (IOException ioe) { + throw Helpers.newIOErrorFromException(session.getRuntime(), ioe); } - if (objectNl.length() != 0) buffer.append(indent); - - STRING_HANDLER.generate(session, key.asString(), buffer); - session.infectBy(key); - - buffer.append(spaceBefore); - buffer.append((byte)':'); - buffer.append(space); - - Handler valueHandler = (Handler) getHandlerFor(runtime, value); - valueHandler.generate(session, value, buffer); - session.infectBy(value); } }); state.decreaseDepth(); if (!firstPair[0] && objectNl.length() != 0) { - buffer.append(objectNl); + buffer.write(objectNl.unsafeBytes()); } - buffer.append(Utils.repeat(state.getIndent(), state.getDepth())); - buffer.append((byte)'}'); + buffer.write(Utils.repeat(state.getIndent(), state.getDepth())); + buffer.write((byte)'}'); } }; @@ -379,7 +432,7 @@ int guessSize(Session session, RubyString object) { } @Override - void generate(Session session, RubyString object, ByteList buffer) { + void generate(Session session, RubyString object, OutputStream buffer) throws IOException { RuntimeInfo info = session.getInfo(); RubyString src; @@ -414,7 +467,7 @@ RubyString generateNew(Session session, IRubyObject object) { } @Override - void generate(Session session, IRubyObject object, ByteList buffer) { + void generate(Session session, IRubyObject object, OutputStream buffer) throws IOException { RubyString str = object.asString(); STRING_HANDLER.generate(session, str, buffer); } @@ -439,9 +492,8 @@ RubyString generateNew(Session session, IRubyObject object) { } @Override - void generate(Session session, IRubyObject object, ByteList buffer) { - RubyString result = generateNew(session, object); - buffer.append(result.getByteList()); + void generate(Session session, IRubyObject object, OutputStream buffer) throws IOException { + generateNew(session, object, buffer); } }; } diff --git a/java/src/json/ext/GeneratorState.java b/java/src/json/ext/GeneratorState.java index a0541d67..a8c285b9 100644 --- a/java/src/json/ext/GeneratorState.java +++ b/java/src/json/ext/GeneratorState.java @@ -9,17 +9,23 @@ import org.jruby.RubyBoolean; import org.jruby.RubyClass; import org.jruby.RubyHash; +import org.jruby.RubyIO; import org.jruby.RubyInteger; import org.jruby.RubyNumeric; import org.jruby.RubyObject; import org.jruby.RubyString; import org.jruby.anno.JRubyMethod; +import org.jruby.java.addons.IOJavaAddons; import org.jruby.runtime.Block; import org.jruby.runtime.ObjectAllocator; import org.jruby.runtime.ThreadContext; import org.jruby.runtime.Visibility; import org.jruby.runtime.builtin.IRubyObject; import org.jruby.util.ByteList; +import org.jruby.util.IOOutputStream; +import org.jruby.util.TypeConverter; + +import java.io.OutputStream; /** * The JSON::Ext::Generator::State class. @@ -222,6 +228,18 @@ public IRubyObject generate(ThreadContext context, IRubyObject obj) { return result; } + @JRubyMethod + public IRubyObject generate(ThreadContext context, IRubyObject obj, IRubyObject output) { + OutputStream outStream; + if (output instanceof RubyIO) { + outStream = ((RubyIO) output).getOutStream(); + } else { + outStream = new IOOutputStream(output); + } + Generator.generateJson(context, obj, this, outStream); + return output; + } + private static boolean matchClosingBrace(ByteList bl, int pos, int len, int brace) { for (int endPos = len - 1; endPos > pos; endPos--) { diff --git a/java/src/json/ext/Parser.java b/java/src/json/ext/Parser.java index ef283932..fbacf904 100644 --- a/java/src/json/ext/Parser.java +++ b/java/src/json/ext/Parser.java @@ -22,12 +22,16 @@ import org.jruby.exceptions.JumpException; import org.jruby.exceptions.RaiseException; import org.jruby.runtime.Block; +import org.jruby.runtime.Helpers; import org.jruby.runtime.ObjectAllocator; import org.jruby.runtime.ThreadContext; import org.jruby.runtime.Visibility; import org.jruby.runtime.builtin.IRubyObject; import org.jruby.util.ByteList; import org.jruby.util.ConvertBytes; + +import java.io.IOException; + import static org.jruby.util.ConvertDouble.DoubleConverter; /** @@ -1401,9 +1405,13 @@ else if ( data[p] > _JSON_string_trans_keys[_mid+1] ) // line 579 "Parser.rl" { int offset = byteList.begin(); - ByteList decoded = decoder.decode(byteList, memo + 1 - offset, - p - offset); - result = getRuntime().newString(decoded); + try { + ByteList decoded = decoder.decode(byteList, memo + 1 - offset, + p - offset); + result = getRuntime().newString(decoded); + } catch (IOException ioe) { + throw Helpers.newIOErrorFromException(getRuntime(), ioe); + } if (result == null) { p--; { p += 1; _goto_targ = 5; if (true) continue _goto;} diff --git a/java/src/json/ext/StringDecoder.java b/java/src/json/ext/StringDecoder.java index 76cf1837..c281f891 100644 --- a/java/src/json/ext/StringDecoder.java +++ b/java/src/json/ext/StringDecoder.java @@ -9,6 +9,8 @@ import org.jruby.runtime.ThreadContext; import org.jruby.util.ByteList; +import java.io.IOException; + /** * A decoder that reads a JSON-encoded string from the given sources and * returns its decoded form on a new ByteList. Escaped Unicode characters @@ -28,18 +30,19 @@ final class StringDecoder extends ByteListTranscoder { super(context); } - ByteList decode(ByteList src, int start, int end) { - ByteList out = new ByteList(end - start); - out.setEncoding(src.getEncoding()); + ByteList decode(ByteList src, int start, int end) throws IOException { + Generator.ByteListOutputStream out = new Generator.ByteListOutputStream(end - start); init(src, start, end, out); while (hasNext()) { handleChar(readUtf8Char()); } quoteStop(pos); - return out; + ByteList bl = out.toByteListDirect(); + bl.setEncoding(src.getEncoding()); + return bl; } - private void handleChar(int c) { + private void handleChar(int c) throws IOException { if (c == '\\') { quoteStop(charStart); handleEscapeSequence(); @@ -48,7 +51,7 @@ private void handleChar(int c) { } } - private void handleEscapeSequence() { + private void handleEscapeSequence() throws IOException { ensureMin(1); switch (readUtf8Char()) { case 'b': @@ -83,7 +86,7 @@ private void handleEscapeSequence() { } } - private void handleLowSurrogate(char highSurrogate) { + private void handleLowSurrogate(char highSurrogate) throws IOException { surrogatePairStart = charStart; ensureMin(1); int lowSurrogate = readUtf8Char(); @@ -103,7 +106,7 @@ private void handleLowSurrogate(char highSurrogate) { } } - private void writeUtf8Char(int codePoint) { + private void writeUtf8Char(int codePoint) throws IOException { if (codePoint < 0x80) { append(codePoint); } else if (codePoint < 0x800) { diff --git a/java/src/json/ext/StringEncoder.java b/java/src/json/ext/StringEncoder.java index 26678ede..26f774ae 100644 --- a/java/src/json/ext/StringEncoder.java +++ b/java/src/json/ext/StringEncoder.java @@ -9,6 +9,9 @@ import org.jruby.runtime.ThreadContext; import org.jruby.util.ByteList; +import java.io.IOException; +import java.io.OutputStream; + /** * An encoder that reads from the given source and outputs its representation * to another ByteList. The source string is fully checked for UTF-8 validity, @@ -43,7 +46,7 @@ final class StringEncoder extends ByteListTranscoder { this.escapeSlash = escapeSlash; } - void encode(ByteList src, ByteList out) { + void encode(ByteList src, OutputStream out) throws IOException { init(src, out); append('"'); while (hasNext()) { @@ -53,7 +56,7 @@ void encode(ByteList src, ByteList out) { append('"'); } - private void handleChar(int c) { + private void handleChar(int c) throws IOException { switch (c) { case '"': case '\\': @@ -90,13 +93,13 @@ private void handleChar(int c) { } } - private void escapeChar(char c) { + private void escapeChar(char c) throws IOException { quoteStop(charStart); aux[ESCAPE_CHAR_OFFSET + 1] = (byte)c; append(aux, ESCAPE_CHAR_OFFSET, 2); } - private void escapeUtf8Char(int codePoint) { + private void escapeUtf8Char(int codePoint) throws IOException { int numChars = Character.toChars(codePoint, utf16, 0); escapeCodeUnit(utf16[0], ESCAPE_UNI1_OFFSET + 2); if (numChars > 1) escapeCodeUnit(utf16[1], ESCAPE_UNI2_OFFSET + 2); diff --git a/lib/json/common.rb b/lib/json/common.rb index ea46896f..5514089c 100644 --- a/lib/json/common.rb +++ b/lib/json/common.rb @@ -293,7 +293,7 @@ def load_file!(filespec, opts = {}) # # Raises JSON::NestingError (nesting of 100 is too deep): # JSON.generate(a) # - def generate(obj, opts = nil) + def generate(obj, opts = nil, result: nil) if State === opts state, opts = opts, nil else @@ -309,7 +309,7 @@ def generate(obj, opts = nil) end state = state.configure(opts) end - state.generate(obj) + state.generate(obj, result) end # :stopdoc: @@ -631,20 +631,14 @@ class << self def dump(obj, anIO = nil, limit = nil) if anIO and limit.nil? anIO = anIO.to_io if anIO.respond_to?(:to_io) - unless anIO.respond_to?(:write) + unless anIO.respond_to?(:<<) limit = anIO anIO = nil end end opts = JSON.dump_default_options opts = opts.merge(:max_nesting => limit) if limit - result = generate(obj, opts) - if anIO - anIO.write result - anIO - else - result - end + generate(obj, opts, result: anIO) rescue JSON::NestingError raise ArgumentError, "exceed depth limit" end diff --git a/lib/json/pure/generator.rb b/lib/json/pure/generator.rb index 2257ee34..2c97eca7 100644 --- a/lib/json/pure/generator.rb +++ b/lib/json/pure/generator.rb @@ -1,4 +1,4 @@ -#frozen_string_literal: false +#frozen_string_literal: true module JSON MAP = { "\x0" => '\u0000', @@ -256,10 +256,10 @@ def to_h # returns the result. If no valid JSON document can be # created this method raises a # GeneratorError exception. - def generate(obj) - result = obj.to_json(self) - JSON.valid_utf8?(result) or raise GeneratorError, - "source sequence #{result.inspect} is illegal/malformed utf-8" + def generate(obj, result: nil) + result = obj.to_json(self, result:) + # JSON.valid_utf8?(result) or raise GeneratorError, + # "source sequence #{result.inspect} is illegal/malformed utf-8" result end @@ -287,7 +287,9 @@ module Object # Converts this object to a string (calling #to_s), converts # it to a JSON string, and returns the result. This is a fallback, if no # special method #to_json was defined for some object. - def to_json(*) to_s.to_json end + def to_json(*, result: nil) + to_s.to_json(result:) + end end module Hash @@ -296,10 +298,10 @@ module Hash # _state_ is a JSON::State object, that can also be used to configure the # produced JSON string output further. # _depth_ is used to find out nesting depth, to indent accordingly. - def to_json(state = nil, *) + def to_json(state = nil, *, result: nil) state = State.from_state(state) state.check_max_nesting - json_transform(state) + json_transform(state, result) end private @@ -309,10 +311,10 @@ def json_shift(state) state.indent * state.depth end - def json_transform(state) - delim = ',' + def json_transform(state, result) + delim = ','.dup delim << state.object_nl - result = '{' + result ? result << '{' : result = '{'.dup result << state.object_nl depth = state.depth += 1 first = true @@ -320,12 +322,12 @@ def json_transform(state) each { |key,value| result << delim unless first result << state.indent * depth if indent - result << key.to_s.to_json(state) + key.to_s.to_json(state, result:) result << state.space_before result << ':' result << state.space if value.respond_to?(:to_json) - result << value.to_json(state) + value.to_json(state, result:) else result << %{"#{String(value)}"} end @@ -346,18 +348,18 @@ module Array # this Array instance. # _state_ is a JSON::State object, that can also be used to configure the # produced JSON string output further. - def to_json(state = nil, *) + def to_json(state = nil, *, result: nil) state = State.from_state(state) state.check_max_nesting - json_transform(state) + json_transform(state, result) end private - def json_transform(state) - delim = ',' + def json_transform(state, result) + delim = ','.dup delim << state.array_nl - result = '[' + result ? result << '[' : result = '['.dup result << state.array_nl depth = state.depth += 1 first = true @@ -366,7 +368,7 @@ def json_transform(state) result << delim unless first result << state.indent * depth if indent if value.respond_to?(:to_json) - result << value.to_json(state) + value.to_json(state, result:) else result << %{"#{String(value)}"} end @@ -381,29 +383,35 @@ def json_transform(state) module Integer # Returns a JSON string representation for this Integer number. - def to_json(*) to_s end + def to_json(*, result: nil) + str = to_s + result ? result << str : result = str + result + end end module Float # Returns a JSON string representation for this Float number. - def to_json(state = nil, *) + def to_json(state = nil, *, result: nil) state = State.from_state(state) - case - when infinite? - if state.allow_nan? - to_s - else - raise GeneratorError, "#{self} not allowed in JSON" - end - when nan? - if state.allow_nan? - to_s - else - raise GeneratorError, "#{self} not allowed in JSON" - end - else - to_s - end + str = case + when infinite? + if state.allow_nan? + to_s + else + raise GeneratorError, "#{self} not allowed in JSON" + end + when nan? + if state.allow_nan? + to_s + else + raise GeneratorError, "#{self} not allowed in JSON" + end + else + to_s + end + result ? result << str : result = str + result end end @@ -411,18 +419,20 @@ 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 # \u????. - def to_json(state = nil, *args) + def to_json(state = nil, *args, result: nil) state = State.from_state(state) if encoding == ::Encoding::UTF_8 string = self else string = encode(::Encoding::UTF_8) end - if state.ascii_only? - '"' << JSON.utf8_to_json_ascii(string, state.escape_slash) << '"' - else - '"' << JSON.utf8_to_json(string, state.escape_slash) << '"' - end + string = if state.ascii_only? + "\"#{JSON.utf8_to_json_ascii(string, state.escape_slash)}\"" + else + "\"#{JSON.utf8_to_json(string, state.escape_slash)}\"" + end + result ? result << string : result = string + result end # Module that holds the extending methods if, the String module is @@ -461,17 +471,26 @@ def to_json_raw(*args) module TrueClass # Returns a JSON string for true: 'true'. - def to_json(*) 'true' end + def to_json(*, result: nil) + result ? result << 'true' : result = 'true' + result + end end module FalseClass # Returns a JSON string for false: 'false'. - def to_json(*) 'false' end + def to_json(*, result: nil) + result ? result << 'false' : result = 'false' + result + end end module NilClass # Returns a JSON string for nil: 'null'. - def to_json(*) 'null' end + def to_json(*, result: nil) + result ? result << 'null' : result = nil + result + end end end end