EncodeD JSON output as UTF-8 bytes for the JSON encoder in the Playdate bindings target.
This commit is contained in:
@@ -4,7 +4,7 @@ extension JSON {
|
|||||||
/// A streaming JSON encoder writing into a string. Wraps `json_encoder`.
|
/// A streaming JSON encoder writing into a string. Wraps `json_encoder`.
|
||||||
public final class Encoder {
|
public final class Encoder {
|
||||||
private final class Output {
|
private final class Output {
|
||||||
var text = ""
|
var bytes: [UInt8] = []
|
||||||
}
|
}
|
||||||
|
|
||||||
private var encoder = json_encoder()
|
private var encoder = json_encoder()
|
||||||
@@ -14,13 +14,12 @@ extension JSON {
|
|||||||
jsonAPI.pointee.initEncoder.unsafelyUnwrapped(&encoder, { userdata, string, length in
|
jsonAPI.pointee.initEncoder.unsafelyUnwrapped(&encoder, { userdata, string, length in
|
||||||
guard let userdata, let string else { return }
|
guard let userdata, let string else { return }
|
||||||
let output = Unmanaged<Output>.fromOpaque(userdata).takeUnretainedValue()
|
let output = Unmanaged<Output>.fromOpaque(userdata).takeUnretainedValue()
|
||||||
let bytes = UnsafeRawBufferPointer(start: string, count: Int(length))
|
output.bytes.append(contentsOf: UnsafeRawBufferPointer(start: string, count: Int(length)))
|
||||||
output.text += String(decoding: bytes, as: UTF8.self)
|
|
||||||
}, Unmanaged.passUnretained(output).toOpaque(), pretty ? 1 : 0)
|
}, Unmanaged.passUnretained(output).toOpaque(), pretty ? 1 : 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The JSON produced so far.
|
/// The JSON produced so far.
|
||||||
public var json: String { output.text }
|
public var json: String { String(decoding: output.bytes, as: UTF8.self) }
|
||||||
|
|
||||||
/// Starts a JSON array.
|
/// Starts a JSON array.
|
||||||
public func startArray() {
|
public func startArray() {
|
||||||
@@ -44,9 +43,10 @@ extension JSON {
|
|||||||
|
|
||||||
/// Call before writing each table value.
|
/// Call before writing each table value.
|
||||||
public func addTableMember(name: String) {
|
public func addTableMember(name: String) {
|
||||||
name.withPlaydateCString { cName in
|
name.withPlaydateUTF8 { bytes, count in
|
||||||
withUnsafeMutablePointer(to: &encoder) {
|
withUnsafeMutablePointer(to: &encoder) {
|
||||||
$0.pointee.addTableMember.unsafelyUnwrapped($0, cName, Int32(name.utf8.count))
|
$0.pointee.addTableMember.unsafelyUnwrapped(
|
||||||
|
$0, bytes.assumingMemoryBound(to: CChar.self), Int32(count))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -80,9 +80,10 @@ extension JSON {
|
|||||||
|
|
||||||
/// Writes a string value.
|
/// Writes a string value.
|
||||||
public func writeString(_ value: String) {
|
public func writeString(_ value: String) {
|
||||||
value.withPlaydateCString { cString in
|
value.withPlaydateUTF8 { bytes, count in
|
||||||
withUnsafeMutablePointer(to: &encoder) {
|
withUnsafeMutablePointer(to: &encoder) {
|
||||||
$0.pointee.writeString.unsafelyUnwrapped($0, cString, Int32(value.utf8.count))
|
$0.pointee.writeString.unsafelyUnwrapped(
|
||||||
|
$0, bytes.assumingMemoryBound(to: CChar.self), Int32(count))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -348,5 +348,35 @@ enum Mock {
|
|||||||
data: .init(tableval: root))
|
data: .init(tableval: root))
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A simplified encoder: each call emits its JSON fragment verbatim,
|
||||||
|
// without the real OS's separator and pretty-printing logic.
|
||||||
|
jsonAPI.pointee.initEncoder = { encoder, write, userdata, _ in
|
||||||
|
Mock.record("initEncoder")
|
||||||
|
guard let encoder else { return }
|
||||||
|
encoder.pointee.writeStringFunc = write
|
||||||
|
encoder.pointee.userdata = userdata
|
||||||
|
encoder.pointee.startTable = { Mock.emitJSON($0, "{") }
|
||||||
|
encoder.pointee.endTable = { Mock.emitJSON($0, "}") }
|
||||||
|
encoder.pointee.startArray = { Mock.emitJSON($0, "[") }
|
||||||
|
encoder.pointee.endArray = { Mock.emitJSON($0, "]") }
|
||||||
|
encoder.pointee.addTableMember = { encoder, name, length in
|
||||||
|
guard let name else { return }
|
||||||
|
let bytes = UnsafeRawBufferPointer(start: name, count: Int(length))
|
||||||
|
Mock.emitJSON(encoder, "\"\(String(decoding: bytes, as: UTF8.self))\":")
|
||||||
|
}
|
||||||
|
encoder.pointee.addArrayMember = { Mock.emitJSON($0, ",") }
|
||||||
|
encoder.pointee.writeInt = { Mock.emitJSON($0, "\($1)") }
|
||||||
|
encoder.pointee.writeString = { encoder, string, length in
|
||||||
|
guard let string else { return }
|
||||||
|
let bytes = UnsafeRawBufferPointer(start: string, count: Int(length))
|
||||||
|
Mock.emitJSON(encoder, "\"\(String(decoding: bytes, as: UTF8.self))\"")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static func emitJSON(_ encoder: UnsafeMutablePointer<json_encoder>?, _ text: String) {
|
||||||
|
guard let encoder, let write = encoder.pointee.writeStringFunc else { return }
|
||||||
|
text.withCString { write(encoder.pointee.userdata, $0, Int32(text.utf8.count)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -224,4 +224,18 @@ struct WrapperTests {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test func jsonEncoderAccumulatesOutput() {
|
||||||
|
let encoder = JSON.Encoder()
|
||||||
|
encoder.startTable()
|
||||||
|
encoder.addTableMember(name: "level")
|
||||||
|
encoder.writeInt(3)
|
||||||
|
encoder.addTableMember(name: "name")
|
||||||
|
encoder.writeString("Röck")
|
||||||
|
encoder.endTable()
|
||||||
|
|
||||||
|
// The mock emits fragments verbatim, with no separators between
|
||||||
|
// members; "Röck" exercises multi-byte UTF-8 through the byte buffer.
|
||||||
|
#expect(encoder.json == "{\"level\":3\"name\":\"Röck\"}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user