Tightened the source code and README documentations in the library.
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
internal import CPlaydate
|
||||
|
||||
extension JSON {
|
||||
/// A streaming JSON encoder writing into a string. Wraps `json_encoder`.
|
||||
/// A streaming JSON encoder into a string. Wraps `json_encoder`.
|
||||
/// Does not validate: the caller must emit well-formed JSON.
|
||||
public final class Encoder {
|
||||
/// A class so the write callback's userdata pointer stays stable.
|
||||
private final class Output {
|
||||
var bytes: [UInt8] = []
|
||||
}
|
||||
@@ -10,6 +12,7 @@ extension JSON {
|
||||
private var encoder = json_encoder()
|
||||
private let output = Output()
|
||||
|
||||
/// `pretty` adds human-readable formatting.
|
||||
public init(pretty: Bool = false) {
|
||||
jsonAPI.pointee.initEncoder.unsafelyUnwrapped(&encoder, { userdata, string, length in
|
||||
guard let userdata, let string else { return }
|
||||
@@ -21,7 +24,6 @@ extension JSON {
|
||||
/// The JSON produced so far.
|
||||
public var json: String { String(decoding: output.bytes, as: UTF8.self) }
|
||||
|
||||
/// Starts a JSON array.
|
||||
public func startArray() {
|
||||
withUnsafeMutablePointer(to: &encoder) { $0.pointee.startArray.unsafelyUnwrapped($0) }
|
||||
}
|
||||
@@ -31,7 +33,6 @@ extension JSON {
|
||||
withUnsafeMutablePointer(to: &encoder) { $0.pointee.addArrayMember.unsafelyUnwrapped($0) }
|
||||
}
|
||||
|
||||
/// Ends the current array.
|
||||
public func endArray() {
|
||||
withUnsafeMutablePointer(to: &encoder) { $0.pointee.endArray.unsafelyUnwrapped($0) }
|
||||
}
|
||||
@@ -41,7 +42,7 @@ extension JSON {
|
||||
withUnsafeMutablePointer(to: &encoder) { $0.pointee.startTable.unsafelyUnwrapped($0) }
|
||||
}
|
||||
|
||||
/// Call before writing each table value.
|
||||
/// Call before writing member `name`'s value.
|
||||
public func addTableMember(name: String) {
|
||||
name.withCString { cString in
|
||||
withUnsafeMutablePointer(to: &encoder) {
|
||||
@@ -51,34 +52,29 @@ extension JSON {
|
||||
}
|
||||
}
|
||||
|
||||
/// Ends the current object.
|
||||
public func endTable() {
|
||||
withUnsafeMutablePointer(to: &encoder) { $0.pointee.endTable.unsafelyUnwrapped($0) }
|
||||
}
|
||||
|
||||
/// Writes a `null` value.
|
||||
public func writeNull() {
|
||||
withUnsafeMutablePointer(to: &encoder) { $0.pointee.writeNull.unsafelyUnwrapped($0) }
|
||||
}
|
||||
|
||||
/// Writes a boolean value.
|
||||
public func writeBool(_ value: Bool) {
|
||||
withUnsafeMutablePointer(to: &encoder) {
|
||||
(value ? $0.pointee.writeTrue : $0.pointee.writeFalse).unsafelyUnwrapped($0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Writes an integer value.
|
||||
/// `value` must fit in `Int32`.
|
||||
public func writeInt(_ value: Int) {
|
||||
withUnsafeMutablePointer(to: &encoder) { $0.pointee.writeInt.unsafelyUnwrapped($0, Int32(value)) }
|
||||
}
|
||||
|
||||
/// Writes a floating-point value.
|
||||
public func writeDouble(_ value: Double) {
|
||||
withUnsafeMutablePointer(to: &encoder) { $0.pointee.writeDouble.unsafelyUnwrapped($0, value) }
|
||||
}
|
||||
|
||||
/// Writes a string value.
|
||||
public func writeString(_ value: String) {
|
||||
value.withCString { cString in
|
||||
withUnsafeMutablePointer(to: &encoder) {
|
||||
@@ -88,7 +84,7 @@ extension JSON {
|
||||
}
|
||||
}
|
||||
|
||||
/// Writes a complete `Value` tree.
|
||||
/// Writes a whole `Value` tree; `.float` as `Double`, keys in `Dictionary` order.
|
||||
public func write(_ value: Value) {
|
||||
switch value {
|
||||
case .null:
|
||||
|
||||
@@ -1,19 +1,15 @@
|
||||
extension JSON {
|
||||
/// A decoded JSON value.
|
||||
/// A JSON value tree, produced by `JSON.decode` and consumed by `JSON.encode(_:pretty:)`.
|
||||
public indirect enum Value {
|
||||
/// A JSON `null`.
|
||||
case null
|
||||
/// A JSON `true` or `false`.
|
||||
case bool(Bool)
|
||||
/// A JSON number without a fractional part.
|
||||
/// Encoded as 32-bit; must fit in `Int32`.
|
||||
case int(Int)
|
||||
/// A JSON number with a fractional part.
|
||||
/// A number with a fractional part.
|
||||
case float(Float)
|
||||
/// A JSON string.
|
||||
case string(String)
|
||||
/// A JSON array.
|
||||
case array([Value])
|
||||
/// A JSON object.
|
||||
/// A JSON object; key order is not preserved.
|
||||
case table([String: Value])
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,26 +1,22 @@
|
||||
internal import CPlaydate
|
||||
|
||||
/// The cached `playdate->json` C API table.
|
||||
/// Cached `playdate->json` table.
|
||||
var jsonAPI: UnsafePointer<playdate_json> { Playdate.jsonAPI.unsafelyUnwrapped }
|
||||
|
||||
/// The JSON API: decoding to and encoding from a `Value` tree.
|
||||
///
|
||||
/// The C decoder is callback-based; this wrapper drives it to build a
|
||||
/// complete `Value` tree. The encoder is exposed both as a streaming
|
||||
/// `Encoder` and as a one-shot `encode(_:)` of a `Value`.
|
||||
/// The JSON API: decodes to a complete `Value` tree; encodes by streaming (`Encoder`)
|
||||
/// or in one shot (`encode(_:pretty:)`).
|
||||
public enum JSON {}
|
||||
|
||||
extension JSON {
|
||||
// MARK: - Decoding
|
||||
|
||||
/// Boxes a finished container to pass through the C decoder as a `void*`.
|
||||
private final class ValueBox {
|
||||
var value: Value
|
||||
init(_ value: Value) { self.value = value }
|
||||
}
|
||||
|
||||
/// A container under construction. A class, so appends mutate uniquely
|
||||
/// referenced storage in place instead of copying the collection out of
|
||||
/// and back into an enum payload on every element.
|
||||
/// A container being built; a class so appends don't copy out of an enum payload.
|
||||
private final class Container {
|
||||
let isArray: Bool
|
||||
var items: [Value] = []
|
||||
@@ -32,7 +28,7 @@ extension JSON {
|
||||
}
|
||||
|
||||
private final class DecodeContext {
|
||||
/// Containers under construction, innermost last.
|
||||
/// Open containers, innermost last.
|
||||
var stack: [Container] = []
|
||||
var errorMessage: String?
|
||||
var errorLine: Int32 = 0
|
||||
@@ -90,14 +86,13 @@ extension JSON {
|
||||
guard let userdata = decoder?.pointee.userdata else { return nil }
|
||||
let context = Unmanaged<DecodeContext>.fromOpaque(userdata).takeUnretainedValue()
|
||||
guard let finished = context.stack.popLast() else { return nil }
|
||||
// Handed to the parent container (or the decode outval) as the
|
||||
// sublist's value; consumed by `convert`.
|
||||
// Goes to the parent's callback (or `outval` for the root); `convert` releases it.
|
||||
return Unmanaged.passRetained(ValueBox(finished.value)).toOpaque()
|
||||
}
|
||||
return decoder
|
||||
}
|
||||
|
||||
/// Decodes a JSON string into a `Value` tree.
|
||||
/// Decodes `jsonString`; throws the decoder's error message on failure.
|
||||
public static func decode(_ jsonString: String) throws(PlaydateError) -> Value {
|
||||
let context = DecodeContext()
|
||||
let unmanaged = Unmanaged.passUnretained(context)
|
||||
@@ -109,21 +104,20 @@ extension JSON {
|
||||
}
|
||||
}
|
||||
guard ok else {
|
||||
// A completed root container may already have been written to
|
||||
// outval before the failure; consume it so its box is not leaked.
|
||||
// Consume any root box already written to outval so it isn't leaked.
|
||||
_ = convert(outval)
|
||||
throw decodeError(context)
|
||||
}
|
||||
return convert(outval)
|
||||
}
|
||||
|
||||
/// Decodes JSON read from an open file into a `Value` tree.
|
||||
/// Decodes JSON from `file`'s current offset, leaving it open; throws the decoder's
|
||||
/// error message on failure.
|
||||
public static func decode(file: borrowing File.Handle) throws(PlaydateError) -> Value {
|
||||
let context = DecodeContext()
|
||||
var decoder = makeDecoder(context: Unmanaged.passUnretained(context))
|
||||
var reader = json_reader()
|
||||
// The handle is borrowed for the whole call, so its `SDFile` stays
|
||||
// open while the decoder reads through it.
|
||||
// Borrowing keeps the `SDFile` open for the whole decode.
|
||||
reader.userdata = file.pointer
|
||||
reader.read = { userdata, buffer, size in
|
||||
guard let userdata, let buffer else { return -1 }
|
||||
@@ -135,29 +129,27 @@ extension JSON {
|
||||
jsonAPI.pointee.decode.unsafelyUnwrapped(&decoder, reader, &outval) != 0
|
||||
}
|
||||
guard ok else {
|
||||
// A completed root container may already have been written to
|
||||
// outval before the failure; consume it so its box is not leaked.
|
||||
// Consume any root box already written to outval so it isn't leaked.
|
||||
_ = convert(outval)
|
||||
throw decodeError(context)
|
||||
}
|
||||
return convert(outval)
|
||||
}
|
||||
|
||||
/// Opens and decodes the JSON file at `path`.
|
||||
/// Decodes the file at `path` (Data directory first, then pdx), closing it on return.
|
||||
public static func decodeFile(path: String) throws(PlaydateError) -> Value {
|
||||
let file = try File.Handle(path: path, mode: [.read, .readData])
|
||||
return try decode(file: file)
|
||||
}
|
||||
|
||||
// Static message: interpolating the line number would pull integer
|
||||
// formatting machinery into every device binary that decodes JSON.
|
||||
// Static message: interpolating the line number pulls integer formatting into binaries.
|
||||
private static func decodeError(_ context: DecodeContext) -> PlaydateError {
|
||||
PlaydateError(message: context.errorMessage ?? "JSON decode failed")
|
||||
}
|
||||
|
||||
// MARK: - Encoding
|
||||
|
||||
/// Encodes a `Value` tree as a JSON string.
|
||||
/// Encodes `value`; `pretty` adds formatting. Table keys follow `Dictionary` order.
|
||||
public static func encode(_ value: Value, pretty: Bool = false) -> String {
|
||||
let encoder = Encoder(pretty: pretty)
|
||||
encoder.write(value)
|
||||
|
||||
Reference in New Issue
Block a user