/*
* Copyright (c) Huawei Technologies Co., Ltd. 2025. All rights reserved.
* This source file is part of the Cangjie project, licensed under Apache-2.0
* with Runtime Library Exception.
*
* See https://cangjie-lang.cn/pages/LICENSE for license information.
*/
package stdx.encoding.json.stream
import std.collection.ArrayList
import std.io.OutputStream
import std.time.*
enum JsonWriterState {
| EmptyArray
| NoEmptyArray
| EmptyObject
| NoEmptyObject
| EmptyText
| NoEmptyText
}
public class JsonWriter {
static const DEFAULT_CAPACITY: Int64 = 4096
static const FLUSH_THRESHOLD: Int64 = 3500
var out: OutputStream
var stack: ArrayList<JsonWriterState> = ArrayList<JsonWriterState>()
var haveName = false
var buffer = Array<UInt8>(DEFAULT_CAPACITY, repeat: 0)
var curPos = 0
public var writeConfig: WriteConfig = WriteConfig.compact
public init(out: OutputStream) {
this.out = out
stack.add(EmptyText)
}
public func flush(): Unit {
out.write(buffer[0..curPos])
curPos = 0
out.flush()
}
@OverflowWrapping
public func jsonValue(value: String): JsonWriter {
beforeValue()
if (value.size > JsonWriter.FLUSH_THRESHOLD) {
flushOutBuf()
unsafe { out.write(value.rawData()) }
} else {
if (curPos + value.size > JsonWriter.DEFAULT_CAPACITY - 1) {
flushOutBuf()
}
unsafe { value.rawData().copyTo(buffer, 0, curPos, value.size) }
curPos += value.size
}
this
}
func flushOutBuf() {
out.write(buffer[0..curPos])
curPos = 0
0
}
public func writeValue<T>(v: T): JsonWriter where T <: JsonSerializable {
v.toJson(this)
haveName = false
this
}
@OverflowWrapping
public func writeNullValue(): JsonWriter {
beforeValue()
buffer[curPos] = b'n'
buffer[curPos + 1] = b'u'
buffer[curPos + 2] = b'l'
buffer[curPos + 3] = b'l'
curPos += 4
this
}
protected func writeWrap(): JsonWriter {
if (curPos + 1 > JsonWriter.DEFAULT_CAPACITY - 1) {
flushOutBuf()
}
buffer[curPos] = b'\n'
curPos += 1
this
}
@OverflowWrapping
public func writeName(name: String): JsonWriter {
beforeName()
haveName = true
name.toJson(this)
buffer[curPos] = b':'
curPos++
haveName = true // reuse String.toJson, need to keep name state
useSpaceAfterSeparators()
this
}
@OverflowWrapping
public func startArray(): Unit {
beforeValue()
buffer[curPos] = b'['
curPos++
put(EmptyArray)
}
// end array need to check if the state is after an array
@OverflowWrapping
public func endArray(): Unit {
if (curPos > FLUSH_THRESHOLD) {
flushOutBuf()
}
match (top()) {
case EmptyArray | NoEmptyArray =>
newlineWithIndent(stack.size - 2)
buffer[curPos] = b']'
curPos++
case _ => throw IllegalStateException("End array must be directly after Start array")
}
pop()
}
@OverflowWrapping
public func startObject(): Unit {
beforeValue()
buffer[curPos] = b'{'
curPos++
put(EmptyObject)
}
@OverflowWrapping
public func endObject(): Unit {
if (curPos > FLUSH_THRESHOLD) {
flushOutBuf()
}
if (haveName) {
throw IllegalStateException("End object cannot directly after a name.")
}
match (top()) {
case EmptyObject | NoEmptyObject =>
newlineWithIndent(stack.size - 2)
buffer[curPos] = b'}'
curPos++
case _ => throw IllegalStateException("End object must be directly after Start object.")
}
pop()
}
@OverflowWrapping
func beforeName() {
if (curPos > FLUSH_THRESHOLD) {
flushOutBuf()
}
if (haveName) {
throw IllegalStateException("Already wrote a name, expecting a value.")
}
match (top()) {
case EmptyObject => stack[stack.size - 1] = NoEmptyObject
newlineWithIndent(stack.size - 1)
case NoEmptyObject =>
buffer[curPos] = b','
curPos++
newlineWithIndent(stack.size - 1)
case _ => throw IllegalStateException("The name must be within an object.")
}
}
@OverflowWrapping
func beforeValue() {
if (curPos > FLUSH_THRESHOLD) {
flushOutBuf()
}
match (top()) {
case NoEmptyArray =>
buffer[curPos] = b','
curPos++
if (writeConfig.newline.isEmpty()) {
useSpaceAfterSeparators()
}
newlineWithIndent(stack.size - 1)
case EmptyArray => stack[stack.size - 1] = NoEmptyArray
newlineWithIndent(stack.size - 1)
case NoEmptyText => throw IllegalStateException("JSON text must have only one top-level value.")
case EmptyText => stack[stack.size - 1] = NoEmptyText
case EmptyObject => stack[stack.size - 1] = NoEmptyObject
case NoEmptyObject => // write value in object must write name first
if (!haveName) {
throw IllegalStateException("Value in Json object must after a name.")
}
haveName = false
}
}
func top(): JsonWriterState {
return stack[stack.size - 1]
}
func put(scope: JsonWriterState) {
if (stack.size >= MAX_JSON_STREAM_DEPTH) {
throw IllegalStateException("Json nested depth exceeds ${MAX_JSON_STREAM_DEPTH}.")
}
stack.add(scope)
}
func pop() {
stack.remove(at: stack.size - 1)
}
func newlineWithIndent(indentLevel: Int64): Unit {
// new line
if (writeConfig.newline.isEmpty()) {
return
}
for (b in writeConfig.newline) {
buffer[curPos] = b
curPos++
if (curPos >= buffer.size) {
flushOutBuf()
}
}
// indent
if (writeConfig.indent.isEmpty()) {
return
}
for (_ in 0..indentLevel) {
for (b in writeConfig.indent) {
buffer[curPos] = b
curPos++
if (curPos >= buffer.size) {
flushOutBuf()
}
}
}
}
func useSpaceAfterSeparators(): Unit {
if (writeConfig.useSpaceAfterSeparators) {
buffer[curPos] = b' '
curPos++
}
}
}
public struct WriteConfig {
private WriteConfig(
var _newline!: String,
var _indent!: String,
var _usesSpaceAfterSeparators!: Bool,
var _htmlSafe!: Bool,
var _timeFormat!: String
) {}
public mut prop newline: String {
get() {
_newline
}
set(v) {
for (b in v where (b != b'\r' && b != b'\n')) {
throw IllegalArgumentException("Invalid newline token: ${b}")
}
_newline = v
}
}
public mut prop indent: String {
get() {
_indent
}
set(v) {
for (b in v where (b != b' ' && b != b'\t')) {
throw IllegalArgumentException("Invalid indent token: ${b}")
}
_indent = v
}
}
public mut prop useSpaceAfterSeparators: Bool {
get() {
_usesSpaceAfterSeparators
}
set(v) {
_usesSpaceAfterSeparators = v
}
}
public mut prop htmlSafe: Bool {
get() {
_htmlSafe
}
set(v) {
_htmlSafe = v
}
}
public mut prop dateTimeFormat: String {
get() {
_timeFormat
}
set(v) {
_timeFormat = v
}
}
public static let compact: WriteConfig = WriteConfig(
_newline: "",
_indent: "",
_usesSpaceAfterSeparators: false,
_htmlSafe: false,
_timeFormat: DateTimeFormat.RFC3339
)
public static let pretty: WriteConfig = WriteConfig(
_newline: "\n",
_indent: " ",
_usesSpaceAfterSeparators: true,
_htmlSafe: false,
_timeFormat: DateTimeFormat.RFC3339
)
}