Coverage Summary for Class: GhostJsonFlatWriter (com.ghost.serialization.writer)
| Class |
Class, %
|
Method, %
|
Branch, %
|
Line, %
|
Instruction, %
|
| GhostJsonFlatWriter |
100%
(1/1)
|
89.6%
(43/48)
|
86.3%
(157/182)
|
90.6%
(298/329)
|
89.3%
(1238/1387)
|
@file:OptIn(InternalGhostApi::class)
package com.ghost.serialization.writer
import com.ghost.serialization.InternalGhostApi
import com.ghost.serialization.acquireScratchBuffer
import com.ghost.serialization.exception.GhostJsonException
import com.ghost.serialization.parser.GhostJsonConstants.ASCII_LIMIT
import com.ghost.serialization.parser.GhostJsonConstants.BACKSLASH
import com.ghost.serialization.parser.GhostJsonConstants.BACKSLASH_INT
import com.ghost.serialization.parser.GhostJsonConstants.BITMASK_INDEX_MASK
import com.ghost.serialization.parser.GhostJsonConstants.BITMASK_SHIFT
import com.ghost.serialization.parser.GhostJsonConstants.BITMASK_UNIT
import com.ghost.serialization.parser.GhostJsonConstants.CLOSE_ARR_INT
import com.ghost.serialization.parser.GhostJsonConstants.CLOSE_OBJ_INT
import com.ghost.serialization.parser.GhostJsonConstants.COLON_QUOTE_BS
import com.ghost.serialization.parser.GhostJsonConstants.COMMA_INT
import com.ghost.serialization.parser.GhostJsonConstants.DOUBLE_DIGIT_LUT
import com.ghost.serialization.parser.GhostJsonConstants.ERR_DEPTH_EXCEEDED
import com.ghost.serialization.parser.GhostJsonConstants.ERR_NON_FINITE
import com.ghost.serialization.parser.GhostJsonConstants.ESCAPE_MASKS
import com.ghost.serialization.parser.GhostJsonConstants.ESCAPE_REPLACEMENTS
import com.ghost.serialization.parser.GhostJsonConstants.HEX_CHARS
import com.ghost.serialization.parser.GhostJsonConstants.HEX_MASK
import com.ghost.serialization.parser.GhostJsonConstants.HUNDRED_LONG
import com.ghost.serialization.parser.GhostJsonConstants.LONG_SCRATCH_SIZE
import com.ghost.serialization.parser.GhostJsonConstants.MAX_DEPTH
import com.ghost.serialization.parser.GhostJsonConstants.MAX_SAFE_INTEGER_DOUBLE
import com.ghost.serialization.parser.GhostJsonConstants.MINUS
import com.ghost.serialization.parser.GhostJsonConstants.MINUS_INT
import com.ghost.serialization.parser.GhostJsonConstants.MIN_INT_BS
import com.ghost.serialization.parser.GhostJsonConstants.MIN_LONG_BS
import com.ghost.serialization.parser.GhostJsonConstants.MIN_SAFE_INTEGER_DOUBLE
import com.ghost.serialization.parser.GhostJsonConstants.OPEN_ARR_INT
import com.ghost.serialization.parser.GhostJsonConstants.OPEN_OBJ_INT
import com.ghost.serialization.parser.GhostJsonConstants.PLAIN_ASCII_FAST_PATH_LIMIT
import com.ghost.serialization.parser.GhostJsonConstants.QUOTE_BYTE
import com.ghost.serialization.parser.GhostJsonConstants.QUOTE_INT
import com.ghost.serialization.parser.GhostJsonConstants.SHIFT_12
import com.ghost.serialization.parser.GhostJsonConstants.SHIFT_4
import com.ghost.serialization.parser.GhostJsonConstants.SHIFT_8
import com.ghost.serialization.parser.GhostJsonConstants.SPACE_INT
import com.ghost.serialization.parser.GhostJsonConstants.STRING_QUOTE_PAIR_BYTES
import com.ghost.serialization.parser.GhostJsonConstants.TEN_LONG
import com.ghost.serialization.parser.GhostJsonConstants.UNICODE_PREFIX_U
import com.ghost.serialization.parser.GhostJsonConstants.WHOLE_NUMBER_CHECK
import com.ghost.serialization.parser.GhostJsonConstants.WRITER_SCRATCH_SIZE
import com.ghost.serialization.parser.GhostJsonConstants.ZERO_DOUBLE
import com.ghost.serialization.parser.GhostJsonConstants.ZERO_INT
import com.ghost.serialization.releaseScratchBuffer
import okio.ByteString
/**
* In-memory specialization of [GhostJsonWriter] backed by a contiguous
* [FlatByteArrayWriter] instead of the segmented [okio.Buffer] used by the
* streaming path.
*/
@Suppress("CascadeIf")
class GhostJsonFlatWriter @InternalGhostApi constructor(
@InternalGhostApi val buffer: FlatByteArrayWriter
) {
@PublishedApi
internal var needsComma: Boolean = false
private var depth: Int = 0
internal var scratch: ByteArray? = null
/**
* Acquires the temporary scratch buffer for numeric/string conversions.
*/
internal fun acquireScratch(): ByteArray {
val currentScratch = scratch
if (currentScratch != null) {
return currentScratch
}
val newScratch = acquireScratchBuffer(WRITER_SCRATCH_SIZE)
scratch = newScratch
return newScratch
}
/**
* Returns the scratch buffer to its pool. Must be called once at the end
* of the root encode so subsequent encodes (potentially on other threads)
* can reuse the buffer.
*/
@Suppress("unused")
@InternalGhostApi
fun release() {
val currentScratch = scratch
if (currentScratch != null) {
releaseScratchBuffer(currentScratch)
scratch = null
}
needsComma = false
depth = 0
}
/**
* Resets writer state for reuse from a pool while keeping the scratch
* buffer warm. Pair with a [FlatByteArrayWriter.reset] on the underlying
* [buffer] to start a fresh encode without re-allocating either.
*/
@InternalGhostApi
fun reset() {
needsComma = false
depth = 0
}
/**
* No-op for the flat-array path.
*/
@InternalGhostApi
@Suppress("EmptyFunctionBlock")
fun flush() {
/* No Ops */
}
// ── Structural ────────────────────────────────────────────────────────────
/**
* Starts a new JSON object.
* Automatically handles comma insertion and depth tracking.
*/
fun beginObject(): GhostJsonFlatWriter {
val d = depth
if (d >= MAX_DEPTH) {
throwDepthError()
}
appendSeparator()
buffer.writeByte(OPEN_OBJ_INT)
needsComma = false
depth = d + 1
return this
}
/** Ends the current JSON object. */
fun endObject(): GhostJsonFlatWriter {
buffer.writeByte(CLOSE_OBJ_INT)
needsComma = true
depth--
return this
}
/**
* Starts a new JSON array.
* Automatically handles comma insertion and depth tracking.
*/
fun beginArray(): GhostJsonFlatWriter {
val d = depth
if (d >= MAX_DEPTH) {
throwDepthError()
}
appendSeparator()
buffer.writeByte(OPEN_ARR_INT)
needsComma = false
depth = d + 1
return this
}
/**
* Ends the current JSON array.
*/
fun endArray(): GhostJsonFlatWriter {
buffer.writeByte(CLOSE_ARR_INT)
needsComma = true
depth--
return this
}
/**
* Writes a field name as a string.
* Escapes the key and appends the colon separator.
*/
fun name(key: String): GhostJsonFlatWriter {
appendSeparator()
buffer.writeByte(QUOTE_INT)
writeEscaped(key)
buffer.write(COLON_QUOTE_BS)
needsComma = false
return this
}
/**
* Writes a pre-encoded field name [ByteString].
* This is the fastest way to write field names as it avoids runtime escaping.
*/
fun name(key: ByteString): GhostJsonFlatWriter {
appendSeparator()
buffer.write(key)
needsComma = false
return this
}
/**
* Writes a field name raw [ByteString] without validating or escaping.
*/
@InternalGhostApi
fun writeNameRaw(header: ByteString): GhostJsonFlatWriter {
return name(header)
}
/**
* Fused name + value with automatic comma handling.
* Used by KSP-generated serializers for subsequent object fields.
*/
@InternalGhostApi
fun writeField(header: ByteString, value: Int): GhostJsonFlatWriter {
appendSeparator()
buffer.write(header)
writeIntValueRaw(value)
needsComma = true
return this
}
/**
* Fused name + value with automatic comma handling.
* Used by KSP-generated serializers for subsequent object fields.
*/
@InternalGhostApi
fun writeField(header: ByteString, value: Long): GhostJsonFlatWriter {
appendSeparator()
buffer.write(header)
writeLongValueRaw(value)
needsComma = true
return this
}
/**
* Fused name + value with automatic comma handling.
* Used by KSP-generated serializers for subsequent object fields.
*/
@InternalGhostApi
fun writeField(header: ByteString, value: String): GhostJsonFlatWriter {
appendSeparator()
buffer.write(header)
writeStringValueRaw(value)
needsComma = true
return this
}
/**
* Fused name + value with automatic comma handling.
* Used by KSP-generated serializers for subsequent object fields.
*/
@InternalGhostApi
fun writeField(header: ByteString, value: Boolean): GhostJsonFlatWriter {
appendSeparator()
buffer.write(header)
writeBooleanValueRaw(value)
needsComma = true
return this
}
/**
* Fused name + value with automatic comma handling.
* Used by KSP-generated serializers for subsequent object fields.
*/
@InternalGhostApi
fun writeField(header: ByteString, value: Double): GhostJsonFlatWriter {
appendSeparator()
buffer.write(header)
writeDoubleValueRaw(value)
needsComma = true
return this
}
/**
* Fused name + value with automatic comma handling.
* Used by KSP-generated serializers for subsequent object fields.
*/
@InternalGhostApi
fun writeField(header: ByteString, value: Float): GhostJsonFlatWriter {
appendSeparator()
buffer.write(header)
writeFloatValueRaw(value)
needsComma = true
return this
}
// ── value() public API ────────────────────────────────────────────────────
/**
* Writes a string value into the JSON stream.
*/
fun value(text: String): GhostJsonFlatWriter {
appendSeparator()
writeStringValueRaw(text)
needsComma = true
return this
}
/**
* Writes an integer value into the JSON stream.
*/
fun value(number: Int): GhostJsonFlatWriter {
appendSeparator()
writeIntValueRaw(number)
needsComma = true
return this
}
/**
* Writes a long value into the JSON stream.
*/
fun value(number: Long): GhostJsonFlatWriter {
appendSeparator()
writeLongValueRaw(number)
needsComma = true
return this
}
/**
* Writes a double value into the JSON stream.
*/
fun value(number: Double): GhostJsonFlatWriter {
appendSeparator()
writeDoubleValueRaw(number)
needsComma = true
return this
}
/**
* Writes a float value into the JSON stream.
*/
fun value(number: Float): GhostJsonFlatWriter {
appendSeparator()
writeFloatValueRaw(number)
needsComma = true
return this
}
/**
* Writes a boolean value into the JSON stream.
*/
fun value(value: Boolean): GhostJsonFlatWriter {
appendSeparator()
if (value) { buffer.writeTrue() } else { buffer.writeFalse() }
needsComma = true
return this
}
/**
* Writes a single [Char] as a JSON string without allocating an intermediate [String].
*/
fun value(char: Char): GhostJsonFlatWriter {
appendSeparator()
buffer.writeQuotedBmpCodeUnit(char.code)
needsComma = true
return this
}
/**
* Writes a null value into the JSON stream.
*/
fun nullValue(): GhostJsonFlatWriter {
appendSeparator()
buffer.writeNull()
needsComma = true
return this
}
/**
* Writes raw JSON bytes directly into the stream without quoting or escaping.
* Use this to emit a pre-serialized JSON fragment (object, array, or primitive)
* captured via [com.ghost.serialization.parser.captureRawJsonBytes].
*/
fun rawValue(bytes: ByteArray): GhostJsonFlatWriter {
appendSeparator()
buffer.write(bytes)
needsComma = true
return this
}
/**
* Writes a slice of raw JSON bytes without quoting or escaping.
*/
fun rawValue(bytes: ByteArray, offset: Int, length: Int): GhostJsonFlatWriter {
appendSeparator()
buffer.write(bytes, offset, length)
needsComma = true
return this
}
/**
* Writes [raw] without copying when it aliases the parse input buffer.
*/
fun rawValue(raw: com.ghost.serialization.types.RawJson): GhostJsonFlatWriter =
rawValue(raw.storage, raw.storageOffset, raw.storageLength)
/**
* Writes a boolean value without a field name or separator.
*/
@InternalGhostApi
fun writeBooleanValueRaw(value: Boolean) {
if (value) { buffer.writeTrue() } else { buffer.writeFalse() }
}
/**
* Writes an integer value without a field name or separator.
*/
@InternalGhostApi
fun writeIntValueRaw(value: Int) {
// Fast-path: single digit positive (most common: IDs, counts, status codes)
if (value in 0..9) {
buffer.writeByte(ZERO_INT + value)
return
}
if (value in -9..-1) {
// Single-digit negative: two bytes, one bounds-check
buffer.write2Bytes(MINUS_INT, ZERO_INT - value)
return
}
if (value == Int.MIN_VALUE) {
buffer.write(MIN_INT_BS)
return
}
writeLongValueRawInternal(value.toLong())
}
/**
* Writes a long value without a field name or separator.
*/
@InternalGhostApi
fun writeLongValueRaw(value: Long) {
// Fast-path: single digit positive
if (value in 0L..9L) {
val intVal = value.toInt()
buffer.writeByte(ZERO_INT + intVal)
return
}
if (value in -9L..-1L) {
// Single-digit negative: two bytes, one bounds-check
val intVal = value.toInt()
buffer.write2Bytes(MINUS_INT, ZERO_INT - intVal)
return
}
if (value == Int.MIN_VALUE.toLong()) {
buffer.write(MIN_INT_BS)
return
}
if (value == Long.MIN_VALUE) {
buffer.write(MIN_LONG_BS)
return
}
writeLongValueRawInternal(value)
}
/**
* Internal implementation for writing Long values into the scratch buffer.
*/
private fun writeLongValueRawInternal(value: Long) {
val scratchBuf = scratch ?: acquireScratch()
val scratchEnd = LONG_SCRATCH_SIZE
var pos = scratchEnd
var localValue = value
val isNegative = localValue < 0
if (isNegative) {
if (localValue == Long.MIN_VALUE) {
buffer.write(MIN_LONG_BS)
return
}
localValue = -localValue
}
while (localValue >= HUNDRED_LONG) {
val rem = (localValue % HUNDRED_LONG).toInt() * 2
scratchBuf[--pos] = DOUBLE_DIGIT_LUT[rem + 1] // ones
scratchBuf[--pos] = DOUBLE_DIGIT_LUT[rem] // tens
localValue /= HUNDRED_LONG
}
if (localValue < TEN_LONG) {
scratchBuf[--pos] = (ZERO_INT + localValue.toInt()).toByte()
} else {
val rem = localValue.toInt() * 2
scratchBuf[--pos] = DOUBLE_DIGIT_LUT[rem + 1] // ones
scratchBuf[--pos] = DOUBLE_DIGIT_LUT[rem] // tens
}
if (isNegative) {
scratchBuf[--pos] = MINUS
}
buffer.write(scratchBuf, pos, scratchEnd - pos)
}
/**
* Writes a double value without a field name or separator.
*/
@InternalGhostApi
fun writeDoubleValueRaw(number: Double) {
if (number in MIN_SAFE_INTEGER_DOUBLE..MAX_SAFE_INTEGER_DOUBLE &&
number % WHOLE_NUMBER_CHECK == ZERO_DOUBLE &&
!(number == 0.0 && number.toRawBits() < 0)
) {
writeLongValueRawInternal(number.toLong())
buffer.writeDotZero()
return
}
val scratchBuf = acquireScratch()
val bytesWrittenLength = GhostDoubleFormatter.writeDoubleDirect(
value = number,
scratch = scratchBuf,
offset = 0,
fallback = { fallbackNum ->
if (!fallbackNum.isFinite()) {
throw GhostJsonException(ERR_NON_FINITE, 0, 0)
}
buffer.writeUtf8(fallbackNum.toString())
-1
}
)
if (bytesWrittenLength > 0) {
buffer.write(scratchBuf, 0, bytesWrittenLength)
}
}
@InternalGhostApi
fun writeFloatValueRaw(number: Float) {
val doubleVal = number.toDouble()
if (doubleVal in MIN_SAFE_INTEGER_DOUBLE..MAX_SAFE_INTEGER_DOUBLE &&
doubleVal % WHOLE_NUMBER_CHECK == ZERO_DOUBLE &&
!(number == 0.0f && number.toRawBits() < 0)
) {
writeLongValueRawInternal(doubleVal.toLong())
buffer.writeDotZero()
return
}
val scratchBuf = acquireScratch()
val bytesWrittenLength = GhostDoubleFormatter.writeFloatDirect(
value = number,
scratch = scratchBuf,
offset = 0,
fallback = { fallbackNum ->
if (!fallbackNum.isFinite()) {
throw GhostJsonException(ERR_NON_FINITE, 0, 0)
}
buffer.writeUtf8(fallbackNum.toString())
-1
}
)
if (bytesWrittenLength > 0) {
buffer.write(scratchBuf, 0, bytesWrittenLength)
}
}
/**
* Writes the null literal.
*/
@Suppress("unused")
@InternalGhostApi
fun writeNullValueRaw() {
buffer.writeNull()
}
/**
* Appends a separator comma if a comma is needed before the next entry.
*/
@PublishedApi
@Suppress("NOTHING_TO_INLINE")
internal inline fun appendSeparator() {
if (needsComma) {
buffer.writeByte(COMMA_INT)
needsComma = false
}
}
/**
* Writes a string value with quotes and proper escaping.
*/
@InternalGhostApi
fun writeStringValueRaw(value: String) {
val length = value.length
if (length == 0) {
// Two quotes, one bounds-check
buffer.write2Bytes(QUOTE_INT, QUOTE_INT)
return
}
// Fast path: plain ASCII with no characters needing escaping.
// A single scan (cheaper than the ESCAPE_MASKS bitmask lookup) to decide.
// If all chars qualify, writeQuotedAscii writes directly into the backing
// array with a single ensureCapacity and an unrolled loop — no scratch buffer.
if (length <= PLAIN_ASCII_FAST_PATH_LIMIT) {
var allPlain = true
var index = 0
while (index < length) {
val code = value[index].code
if (code !in SPACE_INT..<ASCII_LIMIT || code == QUOTE_INT || code == BACKSLASH_INT) {
allPlain = false
break
}
index++
}
if (allPlain) {
buffer.writeQuotedAscii(value, length)
return
}
}
writeStringValueRawSlow(value, length)
}
private fun writeStringValueRawSlow(value: String, length: Int) {
val scratchBuf = acquireScratch()
if (length + STRING_QUOTE_PAIR_BYTES > scratchBuf.size) {
buffer.writeByte(QUOTE_INT)
writeEscaped(value)
buffer.writeByte(QUOTE_INT)
return
}
scratchBuf[0] = QUOTE_BYTE
writeEscapedIntoScratch(value, length, scratchBuf)
}
/**
* Utility method to write escaped strings when size exceeds the quick buffer.
*/
private fun writeEscaped(text: String, start: Int = 0) {
val scratchBuf = acquireScratch()
val length = text.length
val remaining = length - start
if (remaining <= 0) {
return
}
val replacements = ESCAPE_REPLACEMENTS
val escapeMasks = ESCAPE_MASKS
val scratchSize = scratchBuf.size
if (remaining <= scratchSize) {
var scratchPos = 0
var index = start
while (index < length) {
val charCode = text[index].code
// Unrolled fast path for plain ASCII
if (charCode < ASCII_LIMIT) {
val maskIdx = charCode shr BITMASK_SHIFT
val bitIdx = charCode and BITMASK_INDEX_MASK
if ((escapeMasks[maskIdx] shr bitIdx) and BITMASK_UNIT == 0L) {
scratchBuf[scratchPos++] = charCode.toByte()
index++
continue
}
}
if (scratchPos > 0) {
buffer.write(scratchBuf, 0, scratchPos)
scratchPos = 0
}
if (charCode < ASCII_LIMIT) {
val replacement = replacements[charCode]
if (replacement != null) {
buffer.write(replacement)
} else {
writeUnicodeEscape(charCode, scratchBuf)
}
} else {
val c = text[index]
if (c.isHighSurrogate() && index + 1 < length && text[index + 1].isLowSurrogate()) {
buffer.writeUtf8(text, index, index + 2)
index++
} else {
buffer.writeUtf8(text, index, index + 1)
}
}
index++
}
if (scratchPos > 0) {
buffer.write(scratchBuf, 0, scratchPos)
}
return
}
var scratchPos = 0
var index = start
while (index < length) {
val charCode = text[index].code
if (
charCode < ASCII_LIMIT &&
(escapeMasks[charCode shr BITMASK_SHIFT] shr
(charCode and BITMASK_INDEX_MASK)) and BITMASK_UNIT == 0L
) {
scratchBuf[scratchPos++] = charCode.toByte()
if (scratchPos == scratchSize) {
buffer.write(scratchBuf, 0, scratchPos)
scratchPos = 0
}
index++
continue
}
if (scratchPos > 0) {
buffer.write(scratchBuf, 0, scratchPos)
scratchPos = 0
}
if (charCode < ASCII_LIMIT) {
val replacement = replacements[charCode]
if (replacement != null) {
buffer.write(replacement)
} else {
writeUnicodeEscape(charCode, scratchBuf)
}
} else {
val char = text[index]
if (char.isHighSurrogate() && index + 1 < length && text[index + 1].isLowSurrogate()) {
buffer.writeUtf8(text, index, index + 2)
index++
} else {
buffer.writeUtf8(text, index, index + 1)
}
}
index++
}
if (scratchPos > 0) {
buffer.write(scratchBuf, 0, scratchPos)
}
}
/**
* Escape strings directly into the scratch buffer.
*/
private fun writeEscapedIntoScratch(text: String, length: Int, scratchBuf: ByteArray) {
val escapeMasks = ESCAPE_MASKS
val escapeReplacements = ESCAPE_REPLACEMENTS
var scratchPos = 1 // Start after the opening quote already written at index 0.
var index = 0
while (index < length) {
val charCode = text[index].code
if (
charCode < ASCII_LIMIT &&
(escapeMasks[charCode shr BITMASK_SHIFT] shr
(charCode and BITMASK_INDEX_MASK)) and BITMASK_UNIT == 0L
) {
scratchBuf[scratchPos++] = charCode.toByte()
index++
continue
}
// Flush what we have so far
if (scratchPos > 0) {
buffer.write(scratchBuf, 0, scratchPos)
scratchPos = 0
}
// Handle the escape
if (charCode < ASCII_LIMIT) {
val replacement = escapeReplacements[charCode]
if (replacement != null) {
buffer.write(replacement)
} else {
writeUnicodeEscape(charCode, scratchBuf)
}
} else {
val c = text[index]
if (c.isHighSurrogate() && index + 1 < length && text[index + 1].isLowSurrogate()) {
buffer.writeUtf8(text, index, index + 2)
index++
} else {
buffer.writeUtf8(text, index, index + 1)
}
}
index++
}
// Add the closing quote and final flush
if (scratchPos + 1 > scratchBuf.size) {
if (scratchPos > 0) {
buffer.write(scratchBuf, 0, scratchPos)
}
buffer.writeByte(QUOTE_INT)
} else {
scratchBuf[scratchPos++] = QUOTE_BYTE
buffer.write(scratchBuf, 0, scratchPos)
}
}
/**
* Throws an exception when max depth limits are exceeded.
*/
private fun throwDepthError() {
throw GhostJsonException("$ERR_DEPTH_EXCEEDED (${MAX_DEPTH})", 0, 0)
}
/**
* Formats unicode characters to standard JSON unicode escape string values.
*/
private fun writeUnicodeEscape(code: Int, scratchBuf: ByteArray) {
val hexChars = HEX_CHARS
scratchBuf[0] = BACKSLASH
scratchBuf[1] = UNICODE_PREFIX_U
scratchBuf[2] = hexChars[(code shr SHIFT_12) and HEX_MASK]
scratchBuf[3] = hexChars[(code shr SHIFT_8) and HEX_MASK]
scratchBuf[4] = hexChars[(code shr SHIFT_4) and HEX_MASK]
scratchBuf[5] = hexChars[code and HEX_MASK]
buffer.write(scratchBuf, 0, 6)
}
}