diff --git a/README.md b/README.md index 8e432fb..b04e7a0 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # ReerJSON -## A faster version of JSONDecoder based on [yyjson](https://github.com/ibireme/yyjson) +## A faster version of JSONDecoder & JSONEncoder based on [yyjson](https://github.com/ibireme/yyjson) ![Coverage: 88%](https://img.shields.io/static/v1?label=coverage&message=88%&color=brightgreen) [![SwiftPM compatible](https://img.shields.io/badge/SwiftPM-compatible%20%28iOS%29-brightgreen)](https://swift.org/package-manager/) @@ -47,7 +47,7 @@ Depend on `ReerJSON` in your target. ``` # Usage -`ReerJSONDecoder` is API-compatible replacements for Foundation's JSONDecoder. +`ReerJSONDecoder` and `ReerJSONEncoder` are API-compatible replacements for Foundation's JSONDecoder and JSONEncoder. Simply swap the type and add the import, no other code changes required: ``` @@ -55,9 +55,11 @@ import ReerJSON // Before let decoder = JSONDecoder() +let encoder = JSONEncoder() // After let decoder = ReerJSONDecoder() +let encoder = ReerJSONEncoder() ``` All public interfaces, behaviors, error types, and coding strategies are identical to the Foundation counterparts. The ReerJSON test suite includes exhaustive test cases covering every feature, ensuring full compatibility. @@ -67,17 +69,26 @@ All public interfaces, behaviors, error types, and coding strategies are identic Except for the items listed below, ReerJSON behaves exactly the same as Foundation—every capability, every thrown error, and every edge case is covered by a comprehensive test suite. +## Decoder + | Decoder Diff | Foundation |ReerJSON | |---------------------------|------------|---------------------------| | JSON5 | ✅ | ✅ | | assumesTopLevelDictionary | ✅ | ❌ | | Infinity and NaN | ±Infinity, ±NaN | ±Infinity, ±NaN, ±Inf and case-insensitive. See [details](https://github.com/reers/ReerJSON/blob/main/Tests/ReerJSONTests/JSONEncoderTests.swift#L1975) | +## Encoder + +| Encoder Diff | Foundation | ReerJSON | +|-----------------------|-------------------------|---------------------------------------| +| Unicode escape casing | `\u001f` (lowercase) | `\u001F` (uppercase). Both are valid JSON per RFC 8259 | +| Pretty-print colon | `"key" : value` (space before and after colon) | `"key": value` (space after colon only) | + # TODO * [x] Add GitHub workflow for CI. * [x] Support `CodableWithConfiguration`. * [x] Support JSON5 decoding. -* [ ] Implement ReerJSONEncoder. +* [x] Implement ReerJSONEncoder. # License This project is licensed under the MIT License. diff --git a/Sources/ReerJSON/JSONEncoderImpl.swift b/Sources/ReerJSON/JSONEncoderImpl.swift new file mode 100644 index 0000000..87a3368 --- /dev/null +++ b/Sources/ReerJSON/JSONEncoderImpl.swift @@ -0,0 +1,545 @@ +// +// Copyright © 2024 swiftlang. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Copyright © 2025 reers. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation +import yyjson + +#if !os(Linux) +import JJLISO8601DateFormatter +#endif + +class JSONEncoderImpl: Encoder { + let doc: UnsafeMutablePointer + let options: ReerJSONEncoder.Options + var codingPath: [CodingKey] + + var userInfo: [CodingUserInfoKey: Any] { options.userInfo } + + var singleValue: UnsafeMutablePointer? + var array: UnsafeMutablePointer? + var object: UnsafeMutablePointer? + + init(doc: UnsafeMutablePointer, codingPath: [CodingKey], options: ReerJSONEncoder.Options) { + self.doc = doc + self.codingPath = codingPath + self.options = options + } + + @inline(__always) + func takeValue() -> UnsafeMutablePointer? { + if let object { return object } + if let array { return array } + return singleValue + } + + // MARK: - Encoder Methods + + func container(keyedBy type: Key.Type) -> KeyedEncodingContainer { + if let object { + return KeyedEncodingContainer(YYJSONKeyedEncodingContainer(impl: self, codingPath: codingPath, object: object)) + } + if let sv = singleValue, yyjson_mut_is_obj(sv) { + self.object = sv; self.singleValue = nil + return KeyedEncodingContainer(YYJSONKeyedEncodingContainer(impl: self, codingPath: codingPath, object: sv)) + } + guard singleValue == nil, array == nil else { + preconditionFailure("Attempt to push new keyed encoding container when already previously encoded at this path.") + } + let obj = yyjson_mut_obj(doc)! + self.object = obj + return KeyedEncodingContainer(YYJSONKeyedEncodingContainer(impl: self, codingPath: codingPath, object: obj)) + } + + func unkeyedContainer() -> UnkeyedEncodingContainer { + if let array { return YYJSONUnkeyedEncodingContainer(impl: self, codingPath: codingPath, array: array) } + if let sv = singleValue, yyjson_mut_is_arr(sv) { + self.array = sv; self.singleValue = nil + return YYJSONUnkeyedEncodingContainer(impl: self, codingPath: codingPath, array: sv) + } + guard singleValue == nil, object == nil else { + preconditionFailure("Attempt to push new unkeyed encoding container when already previously encoded at this path.") + } + let arr = yyjson_mut_arr(doc)! + self.array = arr + return YYJSONUnkeyedEncodingContainer(impl: self, codingPath: codingPath, array: arr) + } + + func singleValueContainer() -> SingleValueEncodingContainer { self } + + // MARK: - Primitive Value Helpers + + @inline(__always) func wrapInt(_ v: some FixedWidthInteger & SignedInteger) -> UnsafeMutablePointer { yyjson_mut_sint(doc, Int64(v)) } + @inline(__always) func wrapUInt(_ v: some FixedWidthInteger & UnsignedInteger) -> UnsafeMutablePointer { yyjson_mut_uint(doc, UInt64(v)) } + + #if compiler(>=6.0) + @available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) + @inline(__always) func wrapInt128(_ v: Int128) -> UnsafeMutablePointer { yyjson_mut_rawcpy(doc, String(v)) } + @available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) + @inline(__always) func wrapUInt128(_ v: UInt128) -> UnsafeMutablePointer { yyjson_mut_rawcpy(doc, String(v)) } + #endif + + @inline(__always) + func wrapFloat(_ float: T, for additionalKey: CodingKey?) throws -> UnsafeMutablePointer { + guard !float.isNaN, !float.isInfinite else { + return try wrapNonConformingFloat(float, for: additionalKey) + } + let d = Double(float) + if T.self == Double.self && d != d.rounded(.towardZero) { + return yyjson_mut_double(doc, d) + } + var s = float.description + if s.hasSuffix(".0") { s.removeLast(2) } + return yyjson_mut_rawcpy(doc, s) + } + + @inline(never) + private func wrapNonConformingFloat(_ float: T, for additionalKey: CodingKey?) throws -> UnsafeMutablePointer { + if case .convertToString(let posInf, let negInf, let nan) = options.nonConformingFloatEncodingStrategy { + switch float { + case T.infinity: return wrapString(posInf) + case -T.infinity: return wrapString(negInf) + default: return wrapString(nan) + } + } + var path = codingPath + if let additionalKey { path.append(additionalKey) } + throw EncodingError.invalidValue(float, .init(codingPath: path, debugDescription: "Unable to encode \(T.self).\(float) directly in JSON.")) + } + + @inline(__always) + func wrapString(_ string: String) -> UnsafeMutablePointer { + var s = string + return s.withUTF8 { buf in + yyjson_mut_strncpy(doc, buf.baseAddress, buf.count) + } + } + + // MARK: - Generic Encodable wrapping (T.self == for fast dispatch) + + func wrapGenericEncodable(_ value: T, for additionalKey: CodingKey?) throws -> UnsafeMutablePointer? { + // Fast-path: special Foundation types (most commonly encountered) + if T.self == Date.self { return try wrapDateValue(value as! Date, for: additionalKey) } + if T.self == Data.self { return try wrapDataValue(value as! Data, for: additionalKey) } + if T.self == URL.self { return wrapString((value as! URL).absoluteString) } + if T.self == Decimal.self { return yyjson_mut_rawcpy(doc, (value as! Decimal).description) } + + #if compiler(>=6.0) + if #available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) { + if T.self == Int128.self { return wrapInt128(value as! Int128) } + if T.self == UInt128.self { return wrapUInt128(value as! UInt128) } + } + #endif + + if T.self is _JSONStringDictionaryEncodableMarker.Type, let dict = value as? [String: Encodable] { + return try wrapStringKeyedDictValue(dict, for: additionalKey) + } + + if T.self == [Double].self { return wrapBulkDoubleArray(value as! [Double]) } + + if T.self == [String].self { return wrapBulkStringArray(value as! [String]) } + if T.self == [Bool].self { return wrapBulkBoolArray(value as! [Bool]) } + if T.self == [Int].self { return wrapBulkIntArray(value as! [Int]) } + if T.self == [Int8].self { return wrapBulkInt8Array(value as! [Int8]) } + if T.self == [Int16].self { return wrapBulkInt16Array(value as! [Int16]) } + if T.self == [Int32].self { return wrapBulkInt32Array(value as! [Int32]) } + if T.self == [Int64].self { return wrapBulkInt64Array(value as! [Int64]) } + if T.self == [UInt].self { return wrapBulkUIntArray(value as! [UInt]) } + if T.self == [UInt8].self { return wrapBulkUInt8Array(value as! [UInt8]) } + if T.self == [UInt16].self { return wrapBulkUInt16Array(value as! [UInt16]) } + if T.self == [UInt32].self { return wrapBulkUInt32Array(value as! [UInt32]) } + if T.self == [UInt64].self { return wrapBulkUInt64Array(value as! [UInt64]) } + if T.self == [Float].self { return try wrapFloatArray(value as! [Float]) } + + return try _encodeNestedValue(for: additionalKey) { try value.encode(to: self) } + } + + func wrapEncodable(_ value: Encodable, for additionalKey: CodingKey?) throws -> UnsafeMutablePointer? { + if let date = value as? Date { return try wrapDateValue(date, for: additionalKey) } + if let data = value as? Data { return try wrapDataValue(data, for: additionalKey) } + if let url = value as? URL { return wrapString(url.absoluteString) } + if let decimal = value as? Decimal { return yyjson_mut_rawcpy(doc, decimal.description) } + if value is _JSONStringDictionaryEncodableMarker, let dict = value as? [String: Encodable] { + return try wrapStringKeyedDictValue(dict, for: additionalKey) + } + #if compiler(>=6.0) + if #available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) { + if let i128 = value as? Int128 { return wrapInt128(i128) } + if let u128 = value as? UInt128 { return wrapUInt128(u128) } + } + #endif + + return try _encodeNestedValue(for: additionalKey) { try value.encode(to: self) } + } + + /// Reuse this encoder for a nested value.encode(to:) call, avoiding array copy + object allocation. + @inline(__always) + private func _encodeNestedValue(for additionalKey: CodingKey?, body: () throws -> Void) rethrows -> UnsafeMutablePointer? { + if let key = additionalKey { codingPath.append(key) } + let savedSV = singleValue; let savedArr = array; let savedObj = object + singleValue = nil; array = nil; object = nil + defer { + singleValue = savedSV; array = savedArr; object = savedObj + if additionalKey != nil { codingPath.removeLast() } + } + try body() + return takeValue() + } + + // MARK: - Bulk array fast paths (single C call, zero Swift loop) + + @inline(__always) func wrapBulkDoubleArray(_ arr: [Double]) -> UnsafeMutablePointer { + if options.nonConformingFloatEncodingStrategy.isThrow { + return arr.withUnsafeBufferPointer { yyjson_mut_arr_with_double(doc, $0.baseAddress, $0.count) } + } + let result = yyjson_mut_arr(doc)! + for d in arr { + if d != d.rounded(.towardZero) && !d.isNaN && !d.isInfinite { + yyjson_mut_arr_add_real(doc, result, d) + } else { + yyjson_mut_arr_append(result, (try? wrapFloat(d, for: nil)) ?? yyjson_mut_null(doc)) + } + } + return result + } + @inline(__always) func wrapBulkBoolArray(_ arr: [Bool]) -> UnsafeMutablePointer { + arr.withUnsafeBufferPointer { yyjson_mut_arr_with_bool(doc, $0.baseAddress, $0.count) } + } + @inline(__always) func wrapBulkInt8Array(_ arr: [Int8]) -> UnsafeMutablePointer { + arr.withUnsafeBufferPointer { yyjson_mut_arr_with_sint8(doc, $0.baseAddress, $0.count) } + } + @inline(__always) func wrapBulkInt16Array(_ arr: [Int16]) -> UnsafeMutablePointer { + arr.withUnsafeBufferPointer { yyjson_mut_arr_with_sint16(doc, $0.baseAddress, $0.count) } + } + @inline(__always) func wrapBulkInt32Array(_ arr: [Int32]) -> UnsafeMutablePointer { + arr.withUnsafeBufferPointer { yyjson_mut_arr_with_sint32(doc, $0.baseAddress, $0.count) } + } + @inline(__always) func wrapBulkInt64Array(_ arr: [Int64]) -> UnsafeMutablePointer { + arr.withUnsafeBufferPointer { yyjson_mut_arr_with_sint64(doc, $0.baseAddress, $0.count) } + } + @inline(__always) func wrapBulkIntArray(_ arr: [Int]) -> UnsafeMutablePointer { + let mapped = arr.map { Int64($0) } + return mapped.withUnsafeBufferPointer { yyjson_mut_arr_with_sint64(doc, $0.baseAddress, $0.count) } + } + @inline(__always) func wrapBulkUInt8Array(_ arr: [UInt8]) -> UnsafeMutablePointer { + arr.withUnsafeBufferPointer { yyjson_mut_arr_with_uint8(doc, $0.baseAddress, $0.count) } + } + @inline(__always) func wrapBulkUInt16Array(_ arr: [UInt16]) -> UnsafeMutablePointer { + arr.withUnsafeBufferPointer { yyjson_mut_arr_with_uint16(doc, $0.baseAddress, $0.count) } + } + @inline(__always) func wrapBulkUInt32Array(_ arr: [UInt32]) -> UnsafeMutablePointer { + arr.withUnsafeBufferPointer { yyjson_mut_arr_with_uint32(doc, $0.baseAddress, $0.count) } + } + @inline(__always) func wrapBulkUInt64Array(_ arr: [UInt64]) -> UnsafeMutablePointer { + arr.withUnsafeBufferPointer { yyjson_mut_arr_with_uint64(doc, $0.baseAddress, $0.count) } + } + @inline(__always) func wrapBulkUIntArray(_ arr: [UInt]) -> UnsafeMutablePointer { + let mapped = arr.map { UInt64($0) } + return mapped.withUnsafeBufferPointer { yyjson_mut_arr_with_uint64(doc, $0.baseAddress, $0.count) } + } + @inline(__always) func wrapBulkStringArray(_ arr: [String]) -> UnsafeMutablePointer { + let result = yyjson_mut_arr(doc)! + for s in arr { yyjson_mut_arr_append(result, wrapString(s)) } + return result + } + @inline(__always) func wrapFloatArray(_ arr: [Float]) throws -> UnsafeMutablePointer { + let result = yyjson_mut_arr(doc)! + for f in arr { yyjson_mut_arr_append(result, try wrapFloat(f, for: nil)) } + return result + } + + // MARK: - Date / Data / Dict wrapping + + func wrapDateValue(_ date: Date, for additionalKey: CodingKey?) throws -> UnsafeMutablePointer? { + switch options.dateEncodingStrategy { + case .deferredToDate: + return try _encodeNestedValue(for: additionalKey) { try date.encode(to: self) } + case .secondsSince1970: + return try wrapFloat(date.timeIntervalSince1970, for: additionalKey) + case .millisecondsSince1970: + return try wrapFloat(1000.0 * date.timeIntervalSince1970, for: additionalKey) + case .iso8601: + return wrapString(_iso8601Formatter.string(from: date)) + case .formatted(let formatter): + return wrapString(formatter.string(from: date)) + case .custom(let closure): + return try _encodeNestedValue(for: additionalKey) { try closure(date, self) } ?? yyjson_mut_obj(doc) + @unknown default: fatalError() + } + } + + func wrapDataValue(_ data: Data, for additionalKey: CodingKey?) throws -> UnsafeMutablePointer? { + switch options.dataEncodingStrategy { + case .deferredToData: + return try _encodeNestedValue(for: additionalKey) { try data.encode(to: self) } + case .base64: + return wrapString(data.base64EncodedString()) + case .custom(let closure): + return try _encodeNestedValue(for: additionalKey) { try closure(data, self) } ?? yyjson_mut_obj(doc) + @unknown default: fatalError() + } + } + + func wrapStringKeyedDictValue(_ dict: [String: Encodable], for additionalKey: CodingKey?) throws -> UnsafeMutablePointer? { + let obj = yyjson_mut_obj(doc)! + let savedPath = codingPath + if let additionalKey { codingPath.append(additionalKey) } + for (key, value) in dict { + let keyVal = wrapString(key) + let val = try wrapEncodable(value, for: _CodingKey(stringValue: key)!) ?? yyjson_mut_obj(doc)! + yyjson_mut_obj_add(obj, keyVal, val) + } + codingPath = savedPath + return obj + } + + // MARK: - Key encoding strategy + + @inline(__always) + func convertedKey(_ key: CodingKey) -> String { + switch options.keyEncodingStrategy { + case .useDefaultKeys: return key.stringValue + case .convertToSnakeCase: return Self._convertToSnakeCase(key.stringValue) + case .custom(let converter): return converter(codingPath + [key]).stringValue + @unknown default: return key.stringValue + } + } + + static func _convertToSnakeCase(_ stringKey: String) -> String { + guard !stringKey.isEmpty else { return stringKey } + var words: [Range] = [] + var wordStart = stringKey.startIndex + var searchRange = stringKey.index(after: wordStart)..=6.0) + @available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) + func encode(_ value: Int128) throws { singleValue = wrapInt128(value) } + @available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) + func encode(_ value: UInt128) throws { singleValue = wrapUInt128(value) } + #endif + func encode(_ value: T) throws { + singleValue = try wrapGenericEncodable(value, for: nil) ?? yyjson_mut_obj(doc) + } +} + +// MARK: - YYJSONKeyedEncodingContainer + +private struct YYJSONKeyedEncodingContainer: KeyedEncodingContainerProtocol { + typealias Key = K + let impl: JSONEncoderImpl + var codingPath: [CodingKey] + let object: UnsafeMutablePointer + let doc: UnsafeMutablePointer + let useDefaultKeys: Bool + + init(impl: JSONEncoderImpl, codingPath: [CodingKey], object: UnsafeMutablePointer) { + self.impl = impl + self.codingPath = codingPath + self.object = object + self.doc = impl.doc + if case .useDefaultKeys = impl.options.keyEncodingStrategy { + self.useDefaultKeys = true + } else { + self.useDefaultKeys = false + } + } + + @inline(__always) + private func _key(_ key: Key) -> String { + useDefaultKeys ? key.stringValue : impl.convertedKey(key) + } + + @inline(__always) + private func _strVal(_ s: String) -> UnsafeMutablePointer { + impl.wrapString(s) + } + + @inline(__always) + private func addToObject(key: String, value: UnsafeMutablePointer) { + yyjson_mut_obj_put(object, _strVal(key), value) + } + + mutating func encodeNil(forKey key: Key) throws { addToObject(key: _key(key), value: yyjson_mut_null(doc)) } + mutating func encode(_ value: Bool, forKey key: Key) throws { addToObject(key: _key(key), value: yyjson_mut_bool(doc, value)) } + mutating func encode(_ value: String, forKey key: Key) throws { addToObject(key: _key(key), value: _strVal(value)) } + mutating func encode(_ value: Double, forKey key: Key) throws { addToObject(key: _key(key), value: try impl.wrapFloat(value, for: key)) } + mutating func encode(_ value: Float, forKey key: Key) throws { addToObject(key: _key(key), value: try impl.wrapFloat(value, for: key)) } + mutating func encode(_ value: Int, forKey key: Key) throws { addToObject(key: _key(key), value: yyjson_mut_sint(doc, Int64(value))) } + mutating func encode(_ value: Int8, forKey key: Key) throws { addToObject(key: _key(key), value: yyjson_mut_sint(doc, Int64(value))) } + mutating func encode(_ value: Int16, forKey key: Key) throws { addToObject(key: _key(key), value: yyjson_mut_sint(doc, Int64(value))) } + mutating func encode(_ value: Int32, forKey key: Key) throws { addToObject(key: _key(key), value: yyjson_mut_sint(doc, Int64(value))) } + mutating func encode(_ value: Int64, forKey key: Key) throws { addToObject(key: _key(key), value: yyjson_mut_sint(doc, Int64(value))) } + mutating func encode(_ value: UInt, forKey key: Key) throws { addToObject(key: _key(key), value: yyjson_mut_uint(doc, UInt64(value))) } + mutating func encode(_ value: UInt8, forKey key: Key) throws { addToObject(key: _key(key), value: yyjson_mut_uint(doc, UInt64(value))) } + mutating func encode(_ value: UInt16, forKey key: Key) throws { addToObject(key: _key(key), value: yyjson_mut_uint(doc, UInt64(value))) } + mutating func encode(_ value: UInt32, forKey key: Key) throws { addToObject(key: _key(key), value: yyjson_mut_uint(doc, UInt64(value))) } + mutating func encode(_ value: UInt64, forKey key: Key) throws { addToObject(key: _key(key), value: yyjson_mut_uint(doc, UInt64(value))) } + #if compiler(>=6.0) + @available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) + mutating func encode(_ value: Int128, forKey key: Key) throws { addToObject(key: _key(key), value: impl.wrapInt128(value)) } + @available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) + mutating func encode(_ value: UInt128, forKey key: Key) throws { addToObject(key: _key(key), value: impl.wrapUInt128(value)) } + #endif + + mutating func encode(_ value: T, forKey key: Key) throws { + addToObject(key: _key(key), value: try impl.wrapGenericEncodable(value, for: key) ?? yyjson_mut_obj(doc)!) + } + + mutating func nestedContainer(keyedBy keyType: NestedKey.Type, forKey key: Key) -> KeyedEncodingContainer { + let k = impl.convertedKey(key) + if let existing = k.withCString({ yyjson_mut_obj_getn(object, $0, k.utf8.count) }), yyjson_mut_is_obj(existing) { + return KeyedEncodingContainer(YYJSONKeyedEncodingContainer(impl: impl, codingPath: codingPath + [key], object: existing)) + } + let obj = yyjson_mut_obj(doc)! + addToObject(key: k, value: obj) + return KeyedEncodingContainer(YYJSONKeyedEncodingContainer(impl: impl, codingPath: codingPath + [key], object: obj)) + } + + mutating func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer { + let arr = yyjson_mut_arr(doc)! + addToObject(key: impl.convertedKey(key), value: arr) + return YYJSONUnkeyedEncodingContainer(impl: impl, codingPath: codingPath + [key], array: arr) + } + + mutating func superEncoder() -> Encoder { + YYJSONReferencingEncoder(impl: impl, key: impl.convertedKey(_CodingKey.super), codingPath: codingPath + [_CodingKey.super], object: object) + } + mutating func superEncoder(forKey key: Key) -> Encoder { + YYJSONReferencingEncoder(impl: impl, key: impl.convertedKey(key), codingPath: codingPath + [key], object: object) + } +} + +// MARK: - YYJSONUnkeyedEncodingContainer + +private struct YYJSONUnkeyedEncodingContainer: UnkeyedEncodingContainer { + let impl: JSONEncoderImpl + var codingPath: [CodingKey] + let array: UnsafeMutablePointer + var doc: UnsafeMutablePointer { impl.doc } + var count: Int { Int(yyjson_mut_arr_size(array)) } + + mutating func encodeNil() throws { yyjson_mut_arr_add_null(doc, array) } + mutating func encode(_ value: Bool) throws { yyjson_mut_arr_add_bool(doc, array, value) } + mutating func encode(_ value: String) throws { yyjson_mut_arr_append(array, impl.wrapString(value)) } + mutating func encode(_ value: Double) throws { yyjson_mut_arr_append(array, try impl.wrapFloat(value, for: _CodingKey(index: count))) } + mutating func encode(_ value: Float) throws { yyjson_mut_arr_append(array, try impl.wrapFloat(value, for: _CodingKey(index: count))) } + mutating func encode(_ value: Int) throws { yyjson_mut_arr_add_sint(doc, array, Int64(value)) } + mutating func encode(_ value: Int8) throws { yyjson_mut_arr_add_sint(doc, array, Int64(value)) } + mutating func encode(_ value: Int16) throws { yyjson_mut_arr_add_sint(doc, array, Int64(value)) } + mutating func encode(_ value: Int32) throws { yyjson_mut_arr_add_sint(doc, array, Int64(value)) } + mutating func encode(_ value: Int64) throws { yyjson_mut_arr_add_sint(doc, array, Int64(value)) } + mutating func encode(_ value: UInt) throws { yyjson_mut_arr_add_uint(doc, array, UInt64(value)) } + mutating func encode(_ value: UInt8) throws { yyjson_mut_arr_add_uint(doc, array, UInt64(value)) } + mutating func encode(_ value: UInt16) throws { yyjson_mut_arr_add_uint(doc, array, UInt64(value)) } + mutating func encode(_ value: UInt32) throws { yyjson_mut_arr_add_uint(doc, array, UInt64(value)) } + mutating func encode(_ value: UInt64) throws { yyjson_mut_arr_add_uint(doc, array, UInt64(value)) } + #if compiler(>=6.0) + @available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) + mutating func encode(_ value: Int128) throws { yyjson_mut_arr_append(array, impl.wrapInt128(value)) } + @available(macOS 15.0, iOS 18.0, tvOS 18.0, watchOS 11.0, visionOS 2.0, *) + mutating func encode(_ value: UInt128) throws { yyjson_mut_arr_append(array, impl.wrapUInt128(value)) } + #endif + + mutating func encode(_ value: T) throws { + yyjson_mut_arr_append(array, try impl.wrapGenericEncodable(value, for: _CodingKey(index: count)) ?? yyjson_mut_obj(doc)!) + } + + mutating func nestedContainer(keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer { + let obj = yyjson_mut_obj(doc)!; yyjson_mut_arr_append(array, obj) + return KeyedEncodingContainer(YYJSONKeyedEncodingContainer(impl: impl, codingPath: codingPath + [_CodingKey(index: count - 1)], object: obj)) + } + mutating func nestedUnkeyedContainer() -> UnkeyedEncodingContainer { + let arr = yyjson_mut_arr(doc)!; yyjson_mut_arr_append(array, arr) + return YYJSONUnkeyedEncodingContainer(impl: impl, codingPath: codingPath + [_CodingKey(index: count - 1)], array: arr) + } + mutating func superEncoder() -> Encoder { + YYJSONReferencingArrayEncoder(impl: impl, codingPath: codingPath + [_CodingKey(index: count)], array: array, index: count) + } +} + +// MARK: - Referencing Encoders + +private class YYJSONReferencingEncoder: JSONEncoderImpl { + let key: String + let referencedObject: UnsafeMutablePointer + init(impl: JSONEncoderImpl, key: String, codingPath: [CodingKey], object: UnsafeMutablePointer) { + self.key = key; self.referencedObject = object + super.init(doc: impl.doc, codingPath: codingPath, options: impl.options) + } + deinit { + yyjson_mut_obj_put(referencedObject, wrapString(key), takeValue() ?? yyjson_mut_obj(doc)!) + } +} + +private class YYJSONReferencingArrayEncoder: JSONEncoderImpl { + let referencedArray: UnsafeMutablePointer + let insertIndex: Int + init(impl: JSONEncoderImpl, codingPath: [CodingKey], array: UnsafeMutablePointer, index: Int) { + self.referencedArray = array; self.insertIndex = index + super.init(doc: impl.doc, codingPath: codingPath, options: impl.options) + } + deinit { + yyjson_mut_arr_insert(referencedArray, takeValue() ?? yyjson_mut_obj(doc)!, insertIndex) + } +} diff --git a/Sources/ReerJSON/ReerJSONEncoder.swift b/Sources/ReerJSON/ReerJSONEncoder.swift new file mode 100644 index 0000000..a01d8a4 --- /dev/null +++ b/Sources/ReerJSON/ReerJSONEncoder.swift @@ -0,0 +1,270 @@ +// +// Copyright © 2024 swiftlang. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Copyright © 2025 reers. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +import Foundation +import yyjson + +open class ReerJSONEncoder { + + open var outputFormatting: JSONEncoder.OutputFormatting { + get { + optionsLock.lock() + defer { optionsLock.unlock() } + return options.outputFormatting + } + _modify { + optionsLock.lock() + var value = options.outputFormatting + defer { + options.outputFormatting = value + optionsLock.unlock() + } + yield &value + } + set { + optionsLock.lock() + defer { optionsLock.unlock() } + options.outputFormatting = newValue + } + } + + open var dateEncodingStrategy: JSONEncoder.DateEncodingStrategy { + get { + optionsLock.lock() + defer { optionsLock.unlock() } + return options.dateEncodingStrategy + } + _modify { + optionsLock.lock() + var value = options.dateEncodingStrategy + defer { + options.dateEncodingStrategy = value + optionsLock.unlock() + } + yield &value + } + set { + optionsLock.lock() + defer { optionsLock.unlock() } + options.dateEncodingStrategy = newValue + } + } + + open var dataEncodingStrategy: JSONEncoder.DataEncodingStrategy { + get { + optionsLock.lock() + defer { optionsLock.unlock() } + return options.dataEncodingStrategy + } + _modify { + optionsLock.lock() + var value = options.dataEncodingStrategy + defer { + options.dataEncodingStrategy = value + optionsLock.unlock() + } + yield &value + } + set { + optionsLock.lock() + defer { optionsLock.unlock() } + options.dataEncodingStrategy = newValue + } + } + + open var nonConformingFloatEncodingStrategy: JSONEncoder.NonConformingFloatEncodingStrategy { + get { + optionsLock.lock() + defer { optionsLock.unlock() } + return options.nonConformingFloatEncodingStrategy + } + _modify { + optionsLock.lock() + var value = options.nonConformingFloatEncodingStrategy + defer { + options.nonConformingFloatEncodingStrategy = value + optionsLock.unlock() + } + yield &value + } + set { + optionsLock.lock() + defer { optionsLock.unlock() } + options.nonConformingFloatEncodingStrategy = newValue + } + } + + open var keyEncodingStrategy: JSONEncoder.KeyEncodingStrategy { + get { + optionsLock.lock() + defer { optionsLock.unlock() } + return options.keyEncodingStrategy + } + _modify { + optionsLock.lock() + var value = options.keyEncodingStrategy + defer { + options.keyEncodingStrategy = value + optionsLock.unlock() + } + yield &value + } + set { + optionsLock.lock() + defer { optionsLock.unlock() } + options.keyEncodingStrategy = newValue + } + } + + @preconcurrency + open var userInfo: [CodingUserInfoKey: any Sendable] { + get { + optionsLock.lock() + defer { optionsLock.unlock() } + return options.userInfo + } + _modify { + optionsLock.lock() + var value = options.userInfo + defer { + options.userInfo = value + optionsLock.unlock() + } + yield &value + } + set { + optionsLock.lock() + defer { optionsLock.unlock() } + options.userInfo = newValue + } + } + + struct Options { + var outputFormatting: JSONEncoder.OutputFormatting = [] + var dateEncodingStrategy: JSONEncoder.DateEncodingStrategy = .deferredToDate + var dataEncodingStrategy: JSONEncoder.DataEncodingStrategy = .base64 + var nonConformingFloatEncodingStrategy: JSONEncoder.NonConformingFloatEncodingStrategy = .throw + var keyEncodingStrategy: JSONEncoder.KeyEncodingStrategy = .useDefaultKeys + var userInfo: [CodingUserInfoKey: any Sendable] = [:] + } + + fileprivate var options = Options() + fileprivate let optionsLock = LockedState() + + public init() {} + + open func encode(_ value: T) throws -> Data { + let doc = yyjson_mut_doc_new(nil)! + defer { yyjson_mut_doc_free(doc) } + + let encoder = JSONEncoderImpl(doc: doc, codingPath: [], options: options) + + if let date = value as? Date { + encoder.singleValue = try encoder.wrapDateValue(date, for: nil) + } else if let data = value as? Data { + encoder.singleValue = try encoder.wrapDataValue(data, for: nil) + } else if let url = value as? URL { + encoder.singleValue = encoder.wrapString(url.absoluteString) + } else if let decimal = value as? Decimal { + encoder.singleValue = yyjson_mut_rawcpy(doc, decimal.description) + } else if value is _JSONStringDictionaryEncodableMarker, let dict = value as? [String: Encodable] { + encoder.singleValue = try encoder.wrapStringKeyedDictValue(dict, for: nil) + } else { + try value.encode(to: encoder) + } + + guard let root = encoder.takeValue() else { + throw EncodingError.invalidValue(value, EncodingError.Context( + codingPath: [], + debugDescription: "Top-level \(T.self) did not encode any values." + )) + } + + if outputFormatting.contains(.sortedKeys) { + sortObjectKeys(root) + } + + yyjson_mut_doc_set_root(doc, root) + + var writeFlag: yyjson_write_flag = YYJSON_WRITE_NOFLAG + if outputFormatting.contains(.prettyPrinted) { + writeFlag |= YYJSON_WRITE_PRETTY_TWO_SPACES + } + if #available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) { + if !outputFormatting.contains(.withoutEscapingSlashes) { + writeFlag |= YYJSON_WRITE_ESCAPE_SLASHES + } + } else { + writeFlag |= YYJSON_WRITE_ESCAPE_SLASHES + } + + var len: Int = 0 + guard let cstr = yyjson_mut_write(doc, writeFlag, &len) else { + throw EncodingError.invalidValue(value, EncodingError.Context( + codingPath: [], + debugDescription: "Unable to encode the given top-level value to JSON." + )) + } + defer { free(cstr) } + return Data(bytes: cstr, count: len) + } + + // MARK: - In-place sort (no tree rebuild, no extra allocation) + + private func sortObjectKeys(_ val: UnsafeMutablePointer) { + typealias MutVal = UnsafeMutablePointer + + if yyjson_mut_is_obj(val) { + var pairs: [(keyVal: MutVal, val: MutVal, keyStr: UnsafePointer)] = [] + pairs.reserveCapacity(Int(yyjson_mut_obj_size(val))) + + var iter = yyjson_mut_obj_iter() + guard yyjson_mut_obj_iter_init(val, &iter) else { return } + + while let keyPtr = yyjson_mut_obj_iter_next(&iter) { + guard let valPtr = yyjson_mut_obj_iter_get_val(keyPtr), + let keyStr = yyjson_mut_get_str(keyPtr) else { continue } + pairs.append((keyPtr, valPtr, keyStr)) + } + + pairs.sort { strcmp($0.keyStr, $1.keyStr) < 0 } + + yyjson_mut_obj_clear(val) + + for pair in pairs { + sortObjectKeys(pair.val) + yyjson_mut_obj_add(val, pair.keyVal, pair.val) + } + } else if yyjson_mut_is_arr(val) { + var iter = yyjson_mut_arr_iter() + guard yyjson_mut_arr_iter_init(val, &iter) else { return } + while let elem = yyjson_mut_arr_iter_next(&iter) { + sortObjectKeys(elem) + } + } + } +} diff --git a/Sources/ReerJSON/Utilities.swift b/Sources/ReerJSON/Utilities.swift index 97ebc66..bb405d9 100644 --- a/Sources/ReerJSON/Utilities.swift +++ b/Sources/ReerJSON/Utilities.swift @@ -122,6 +122,10 @@ protocol StringDecodableDictionary { static var elementType: Decodable.Type { get } } +protocol _JSONStringDictionaryEncodableMarker {} + +extension Dictionary: _JSONStringDictionaryEncodableMarker where Key == String, Value: Encodable {} + extension Dictionary: StringDecodableDictionary where Key == String, Value: Decodable { static var elementType: Decodable.Type { return Value.self } } @@ -138,6 +142,14 @@ extension JSONDecoder.KeyDecodingStrategy { } } +extension JSONEncoder.NonConformingFloatEncodingStrategy { + @inline(__always) + var isThrow: Bool { + if case .throw = self { return true } + return false + } +} + // This is a workaround for the lack of a "set value only if absent" function for Dictionary. extension Optional { mutating func _setIfNil(to value: Wrapped) { diff --git a/Tests/ReerJSONTests/JSONEncoderTests.swift b/Tests/ReerJSONTests/JSONEncoderTests.swift index 8e480d1..919632d 100644 --- a/Tests/ReerJSONTests/JSONEncoderTests.swift +++ b/Tests/ReerJSONTests/JSONEncoderTests.swift @@ -96,18 +96,18 @@ private struct JSONEncoderTests { @Test func encodingTopLevelArrayOfInt() throws { let a = [1,2,3] - let result1 = String(data: try JSONEncoder().encode(a), encoding: .utf8) + let result1 = String(data: try ReerJSONEncoder().encode(a), encoding: .utf8) #expect(result1 == "[1,2,3]") let b : [Int] = [] - let result2 = String(data: try JSONEncoder().encode(b), encoding: .utf8) + let result2 = String(data: try ReerJSONEncoder().encode(b), encoding: .utf8) #expect(result2 == "[]") } // @Test func encodingTopLevelWithConfiguration() throws { // // CodableTypeWithConfiguration is a struct that conforms to CodableWithConfiguration // let value = CodableTypeWithConfiguration.testValue -// let encoder = JSONEncoder() +// let encoder = ReerJSONEncoder() // let decoder = JSONDecoder() // // var decoded = try decoder.decode( @@ -155,7 +155,7 @@ private struct JSONEncoderTests { await #expect(processExitsWith: .failure) { let model = Model.testValue // This following test would fail as it attempts to re-encode into already encoded container is invalid. This will always fail - _ = try JSONEncoder().encode(model) + _ = try ReerJSONEncoder().encode(model) } } #endif @@ -438,7 +438,7 @@ private struct JSONEncoderTests { let expected = "{\"QQQhello\":\"test\"}" let encoded = EncodeMe(keyName: "hello") - let encoder = JSONEncoder() + let encoder = ReerJSONEncoder() let customKeyConversion = { @Sendable (_ path : [CodingKey]) -> CodingKey in let key = _TestKey(stringValue: "QQQ" + path.last!.stringValue)! return key @@ -472,7 +472,7 @@ private struct JSONEncoderTests { let expected = "{\"QQQouterValue\":{\"QQQnestedValue\":{\"QQQhelloWorld\":\"test\"}}}" let encoded = EncodeNestedNested(outerValue: EncodeNested(nestedValue: EncodeMe(keyName: "helloWorld"))) - let encoder = JSONEncoder() + let encoder = ReerJSONEncoder() // We only will mutate this from one thread as we call the encoder synchronously nonisolated(unsafe) var callCount = 0 @@ -619,7 +619,7 @@ private struct JSONEncoderTests { // Encoding let encoded = DecodeMe5() - let encoder = JSONEncoder() + let encoder = ReerJSONEncoder() encoder.keyEncodingStrategy = .custom(customKeyConversion) let decodingResultData = try encoder.encode(encoded) let decodingResultString = String(bytes: decodingResultData, encoding: .utf8) @@ -630,14 +630,14 @@ private struct JSONEncoderTests { // MARK: - Encoder Features @Test func nestedContainerCodingPaths() { - let encoder = JSONEncoder() + let encoder = ReerJSONEncoder() #expect(throws: Never.self) { try encoder.encode(NestedContainersTestType()) } } @Test func superEncoderCodingPaths() { - let encoder = JSONEncoder() + let encoder = ReerJSONEncoder() #expect(throws: Never.self) { try encoder.encode(NestedContainersTestType(testSuperEncoder: true)) } @@ -683,7 +683,7 @@ private struct JSONEncoderTests { } @Test func decodingConcreteTypeParameter() throws { - let encoder = JSONEncoder() + let encoder = ReerJSONEncoder() let json = try encoder.encode(Employee.testValue) let decoder = JSONDecoder() @@ -720,7 +720,7 @@ private struct JSONEncoderTests { // // The issue at hand reproduces when you have a referencing encoder (superEncoder() creates one) that has a container on the stack (unkeyedContainer() adds one) that encodes a value going through box_() (Array does that) that encodes something which throws (Float.infinity does that). // When reproducing, this will cause a test failure via fatalError(). - _ = try? JSONEncoder().encode(ReferencingEncoderWrapper([Float.infinity])) + _ = try? ReerJSONEncoder().encode(ReferencingEncoderWrapper([Float.infinity])) } @Test func encoderStateThrowOnEncodeCustomDate() { @@ -737,7 +737,7 @@ private struct JSONEncoderTests { } // The closure needs to push a container before throwing an error to trigger. - let encoder = JSONEncoder() + let encoder = ReerJSONEncoder() encoder.dateEncodingStrategy = .custom({ _, encoder in let _ = encoder.unkeyedContainer() enum CustomError : Error { case foo } @@ -761,7 +761,7 @@ private struct JSONEncoderTests { } // The closure needs to push a container before throwing an error to trigger. - let encoder = JSONEncoder() + let encoder = ReerJSONEncoder() encoder.dataEncodingStrategy = .custom({ _, encoder in let _ = encoder.unkeyedContainer() enum CustomError : Error { case foo } @@ -801,7 +801,7 @@ private struct JSONEncoderTests { } let value = Level1.init(level2: .init(name: "level2")) - let data = try JSONEncoder().encode(value) + let data = try ReerJSONEncoder().encode(value) let decodedValue = try JSONDecoder().decode(Level1.self, from: data) #expect(value == decodedValue) @@ -985,7 +985,7 @@ private struct JSONEncoderTests { } let before = DelayedDecodable_ContainerVersion(42) - let data = try JSONEncoder().encode(before) + let data = try ReerJSONEncoder().encode(before) let decoded = try JSONDecoder().decode(DelayedDecodable_ContainerVersion.self, from: data) #expect(throws: Never.self) { @@ -1033,7 +1033,7 @@ private struct JSONEncoderTests { private func _testEncodeFailure(of value: T, sourceLocation: SourceLocation = #_sourceLocation) { #expect(throws: (any Error).self, "Encode of top-level \(T.self) was expected to fail.", sourceLocation: sourceLocation) { - try JSONEncoder().encode(value) + try ReerJSONEncoder().encode(value) } } @@ -1057,7 +1057,7 @@ private struct JSONEncoderTests { sourceLocation: SourceLocation = #_sourceLocation) where T : Codable, T : Equatable { var payload: Data do { - let encoder = JSONEncoder() + let encoder = ReerJSONEncoder() encoder.outputFormatting = outputFormatting encoder.dateEncodingStrategy = dateEncodingStrategy encoder.dataEncodingStrategy = dataEncodingStrategy @@ -1090,7 +1090,7 @@ private struct JSONEncoderTests { private func _testRoundTripTypeCoercionFailure(of value: T, as type: U.Type, sourceLocation: SourceLocation = #_sourceLocation) where T : Codable, U : Codable { #expect(throws: (any Error).self, "Coercion from \(T.self) to \(U.self) was expected to fail.", sourceLocation: sourceLocation) { - let data = try JSONEncoder().encode(value) + let data = try ReerJSONEncoder().encode(value) let _ = try JSONDecoder().decode(U.self, from: data) } } @@ -1153,7 +1153,7 @@ private struct JSONEncoderTests { @Test func encodingJSONHexUnicodeEscapes() throws { let testCases = [ "\u{0001}\u{0002}\u{0003}": "\"\\u0001\\u0002\\u0003\"", - "\u{0010}\u{0018}\u{001f}": "\"\\u0010\\u0018\\u001f\"", + "\u{0010}\u{0018}\u{001f}": "\"\\u0010\\u0018\\u001F\"", ] for (string, json) in testCases { _testRoundTrip(of: string, expectedJSON: Data(json.utf8)) @@ -1172,7 +1172,7 @@ private struct JSONEncoderTests { @Test func nullByte() throws { let string = "abc\u{0000}def" - let encoder = JSONEncoder() + let encoder = ReerJSONEncoder() let decoder = JSONDecoder() let data = try encoder.encode([string]) @@ -1511,7 +1511,7 @@ private struct JSONEncoderTests { } @Test func decodeLargeDoubleAsInteger() throws { - let data = try JSONEncoder().encode(Double.greatestFiniteMagnitude) + let data = try ReerJSONEncoder().encode(Double.greatestFiniteMagnitude) #expect(throws: (any Error).self) { try JSONDecoder().decode(UInt64.self, from: data) } @@ -1533,7 +1533,7 @@ private struct JSONEncoderTests { let orig = ["decimalValue" : 1.1] setlocale(LC_ALL, "fr_FR") - let data = try JSONEncoder().encode(orig) + let data = try ReerJSONEncoder().encode(orig) #if os(Windows) setlocale(LC_ALL, "en_US") @@ -1668,7 +1668,7 @@ private struct JSONEncoderTests { @Test func infiniteDate() { let date = Date(timeIntervalSince1970: .infinity) - let encoder = JSONEncoder() + let encoder = ReerJSONEncoder() encoder.dateEncodingStrategy = .deferredToDate #expect(throws: (any Error).self) { @@ -1692,7 +1692,7 @@ private struct JSONEncoderTests { // Intentionally nothing. } } - let enc = JSONEncoder() + let enc = ReerJSONEncoder() #expect(throws: (any Error).self) { try enc.encode(EncodesNothing()) @@ -1741,7 +1741,7 @@ private struct JSONEncoderTests { // NOTE!!! At present, the order in which the values in the unkeyed container's superEncoders above get inserted into the resulting array depends on the order in which the superEncoders are deinit'd!! This can result in some very unexpected results, and this pattern is not recommended. This test exists just to verify compatibility. } } - let data = try JSONEncoder().encode(SuperEncoding()) + let data = try ReerJSONEncoder().encode(SuperEncoding()) let string = String(data: data, encoding: .utf8)! #expect(string.contains("\"firstSuper\":\"First\"")) @@ -1783,22 +1783,22 @@ private struct JSONEncoderTests { } } } - var data = try JSONEncoder().encode(RedundantEncoding(replacedType: .value, useSuperEncoder: false)) + var data = try ReerJSONEncoder().encode(RedundantEncoding(replacedType: .value, useSuperEncoder: false)) #expect(String(data: data, encoding: .utf8) == ("{\"key\":42}")) - data = try JSONEncoder().encode(RedundantEncoding(replacedType: .value, useSuperEncoder: true)) + data = try ReerJSONEncoder().encode(RedundantEncoding(replacedType: .value, useSuperEncoder: true)) #expect(String(data: data, encoding: .utf8) == ("{\"key\":42}")) - data = try JSONEncoder().encode(RedundantEncoding(replacedType: .keyedContainer, useSuperEncoder: false)) + data = try ReerJSONEncoder().encode(RedundantEncoding(replacedType: .keyedContainer, useSuperEncoder: false)) #expect(String(data: data, encoding: .utf8) == ("{\"key\":42}")) - data = try JSONEncoder().encode(RedundantEncoding(replacedType: .keyedContainer, useSuperEncoder: true)) + data = try ReerJSONEncoder().encode(RedundantEncoding(replacedType: .keyedContainer, useSuperEncoder: true)) #expect(String(data: data, encoding: .utf8) == ("{\"key\":42}")) - data = try JSONEncoder().encode(RedundantEncoding(replacedType: .unkeyedContainer, useSuperEncoder: false)) + data = try ReerJSONEncoder().encode(RedundantEncoding(replacedType: .unkeyedContainer, useSuperEncoder: false)) #expect(String(data: data, encoding: .utf8) == ("{\"key\":42}")) - data = try JSONEncoder().encode(RedundantEncoding(replacedType: .unkeyedContainer, useSuperEncoder: true)) + data = try ReerJSONEncoder().encode(RedundantEncoding(replacedType: .unkeyedContainer, useSuperEncoder: true)) #expect(String(data: data, encoding: .utf8) == ("{\"key\":42}")) } @@ -1830,7 +1830,7 @@ private struct JSONEncoderTests { } let toEncode = Something(dict: [:]) - let data = try JSONEncoder().encode(toEncode) + let data = try ReerJSONEncoder().encode(toEncode) let result = try JSONDecoder().decode(Something.self, from: data) #expect(result.dict.count == 0) } @@ -1871,42 +1871,42 @@ private struct JSONEncoderTests { } } await #expect(processExitsWith: .failure) { - let _ = try JSONEncoder().encode(RedundantEncoding(subcase: .replaceValueWithKeyedContainer)) + let _ = try ReerJSONEncoder().encode(RedundantEncoding(subcase: .replaceValueWithKeyedContainer)) } await #expect(processExitsWith: .failure) { - let _ = try JSONEncoder().encode(RedundantEncoding(subcase: .replaceValueWithUnkeyedContainer)) + let _ = try ReerJSONEncoder().encode(RedundantEncoding(subcase: .replaceValueWithUnkeyedContainer)) } await #expect(processExitsWith: .failure) { - let _ = try JSONEncoder().encode(RedundantEncoding(subcase: .replaceKeyedContainerWithUnkeyed)) + let _ = try ReerJSONEncoder().encode(RedundantEncoding(subcase: .replaceKeyedContainerWithUnkeyed)) } await #expect(processExitsWith: .failure) { - let _ = try JSONEncoder().encode(RedundantEncoding(subcase: .replaceUnkeyedContainerWithKeyed)) + let _ = try ReerJSONEncoder().encode(RedundantEncoding(subcase: .replaceUnkeyedContainerWithKeyed)) } } #endif @Test func decodeIfPresent() throws { - let emptyDictJSON = try JSONEncoder().encode(DecodeIfPresentAllTypes.allNils) + let emptyDictJSON = try ReerJSONEncoder().encode(DecodeIfPresentAllTypes.allNils) let testEmptyDict = try JSONDecoder().decode(DecodeIfPresentAllTypes.self, from: emptyDictJSON) #expect(testEmptyDict == .allNils) - let allNullDictJSON = try JSONEncoder().encode(DecodeIfPresentAllTypes.allNils) + let allNullDictJSON = try ReerJSONEncoder().encode(DecodeIfPresentAllTypes.allNils) let testAllNullDict = try JSONDecoder().decode(DecodeIfPresentAllTypes.self, from: allNullDictJSON) #expect(testAllNullDict == .allNils) - let allOnesDictJSON = try JSONEncoder().encode(DecodeIfPresentAllTypes.allOnes) + let allOnesDictJSON = try ReerJSONEncoder().encode(DecodeIfPresentAllTypes.allOnes) let testAllOnesDict = try JSONDecoder().decode(DecodeIfPresentAllTypes.self, from: allOnesDictJSON) #expect(testAllOnesDict == .allOnes) - let emptyArrayJSON = try JSONEncoder().encode(DecodeIfPresentAllTypes.allNils) + let emptyArrayJSON = try ReerJSONEncoder().encode(DecodeIfPresentAllTypes.allNils) let testEmptyArray = try JSONDecoder().decode(DecodeIfPresentAllTypes.self, from: emptyArrayJSON) #expect(testEmptyArray == .allNils) - let allNullArrayJSON = try JSONEncoder().encode(DecodeIfPresentAllTypes.allNils) + let allNullArrayJSON = try ReerJSONEncoder().encode(DecodeIfPresentAllTypes.allNils) let testAllNullArray = try JSONDecoder().decode(DecodeIfPresentAllTypes.self, from: allNullArrayJSON) #expect(testAllNullArray == .allNils) - let allOnesArrayJSON = try JSONEncoder().encode(DecodeIfPresentAllTypes.allOnes) + let allOnesArrayJSON = try ReerJSONEncoder().encode(DecodeIfPresentAllTypes.allOnes) let testAllOnesArray = try JSONDecoder().decode(DecodeIfPresentAllTypes.self, from: allOnesArrayJSON) #expect(testAllOnesArray == .allOnes) } @@ -2376,7 +2376,7 @@ extension JSONEncoderTests { let expected = "{\"leaveMeAlone\":\"test\"}" let toEncode: [String: String] = ["leaveMeAlone": "test"] - let encoder = JSONEncoder() + let encoder = ReerJSONEncoder() encoder.keyEncodingStrategy = .convertToSnakeCase let resultData = try encoder.encode(toEncode) let resultString = String(bytes: resultData, encoding: .utf8) @@ -2406,7 +2406,7 @@ extension JSONEncoderTests { // Encoding let encoded = DecodeMe4(thisIsCamelCase: "test", thisIsCamelCaseToo: "test2") - let encoder = JSONEncoder() + let encoder = ReerJSONEncoder() encoder.keyEncodingStrategy = .convertToSnakeCase let encodingResultData = try encoder.encode(encoded) let encodingResultString = try #require(String(bytes: encodingResultData, encoding: .utf8)) @@ -2433,7 +2433,7 @@ extension JSONEncoderTests { @Test func decodingKeyStrategyCamelGenerated() throws { let encoded = DecodeMe3(thisIsCamelCase: "test") - let encoder = JSONEncoder() + let encoder = ReerJSONEncoder() encoder.keyEncodingStrategy = .convertToSnakeCase let resultData = try encoder.encode(encoded) let resultString = String(bytes: resultData, encoding: .utf8) @@ -2466,7 +2466,7 @@ extension JSONEncoderTests { @Test func encodingDictionaryFailureKeyPath() { let toEncode: [String: EncodeFailure] = ["key": EncodeFailure(someValue: Double.nan)] - let encoder = JSONEncoder() + let encoder = ReerJSONEncoder() encoder.keyEncodingStrategy = .convertToSnakeCase do { _ = try encoder.encode(toEncode) @@ -2482,7 +2482,7 @@ extension JSONEncoderTests { @Test func encodingDictionaryFailureKeyPathNested() { let toEncode: [String: [String: EncodeFailureNested]] = ["key": ["sub_key": EncodeFailureNested(nestedValue: EncodeFailure(someValue: Double.nan))]] - let encoder = JSONEncoder() + let encoder = ReerJSONEncoder() encoder.keyEncodingStrategy = .convertToSnakeCase do { _ = try encoder.encode(toEncode) @@ -2534,7 +2534,7 @@ extension JSONEncoderTests { let expected = "{\"\(test.1)\":\"test\"}" let encoded = EncodeMe(keyName: test.0) - let encoder = JSONEncoder() + let encoder = ReerJSONEncoder() encoder.keyEncodingStrategy = .convertToSnakeCase let resultData = try encoder.encode(encoded) let resultString = String(bytes: resultData, encoding: .utf8) @@ -2563,10 +2563,10 @@ extension JSONEncoderTests { return } - let prettyPrintEncoder = JSONEncoder() + let prettyPrintEncoder = ReerJSONEncoder() prettyPrintEncoder.outputFormatting = .prettyPrinted - for encoder in [JSONEncoder(), prettyPrintEncoder] { + for encoder in [ReerJSONEncoder(), prettyPrintEncoder] { #expect(throws: Never.self, sourceLocation: sourceLocation) { let reencodedData = try encoder.encode(decoded) let redecodedObjects = try decoder.decode(T.self, from: reencodedData) @@ -2979,7 +2979,7 @@ extension JSONEncoderTests { } @Test func encodingOutputFormattingPrettyPrintedSortedKeys() { - let expectedJSON = "{\n \"email\" : \"appleseed@apple.com\",\n \"name\" : \"Johnny Appleseed\"\n}".data(using: .utf8)! + let expectedJSON = "{\n \"email\": \"appleseed@apple.com\",\n \"name\": \"Johnny Appleseed\"\n}".data(using: .utf8)! let person = Person.testValue _testRoundTrip(of: person, expectedJSON: expectedJSON, outputFormatting: [.prettyPrinted, .sortedKeys]) } @@ -3117,7 +3117,7 @@ extension JSONEncoderTests { } } - let encoder = JSONEncoder() + let encoder = ReerJSONEncoder() encoder.outputFormatting = .sortedKeys let data = try encoder.encode(EncodesTwice()) let string = String(data: data, encoding: .utf8)! @@ -3139,7 +3139,7 @@ extension JSONEncoderTests { try keyed.encode("c", forKey: .a) } } - let encoder = JSONEncoder() + let encoder = ReerJSONEncoder() encoder.outputFormatting = .sortedKeys let data = try encoder.encode(Test()) let string = String(data: data, encoding: .utf8)!