From 70fe996c6ff52f1d378661d2577206a211f36fc4 Mon Sep 17 00:00:00 2001 From: Gwynne Raskind Date: Sat, 13 Aug 2022 12:50:14 -0500 Subject: [PATCH 1/6] Rewrite LeafEncoder completely to behave much better, conform better to Codable requirements, and actually support superEncoder(). Add a bunch of missing test coverage. --- Sources/Leaf/LeafEncoder.swift | 357 ++++++++----------- Sources/Leaf/LeafRenderer+ViewRenderer.swift | 2 +- Tests/LeafTests/LeafEncoderTests.swift | 139 ++++++++ 3 files changed, 295 insertions(+), 203 deletions(-) create mode 100644 Tests/LeafTests/LeafEncoderTests.swift diff --git a/Sources/Leaf/LeafEncoder.swift b/Sources/Leaf/LeafEncoder.swift index 8fb5834..4c31e11 100644 --- a/Sources/Leaf/LeafEncoder.swift +++ b/Sources/Leaf/LeafEncoder.swift @@ -1,231 +1,184 @@ import LeafKit -internal final class LeafEncoder { - func encode(_ encodable: E) throws -> [String: LeafData] - where E: Encodable - { - let encoder = _Encoder(codingPath: []) +internal struct LeafEncoder { + /// Use `Codable` to convert an (almost) arbitrary encodable type to a dictionary of key/``LeafData`` pairs + /// for use as a rendering context. The type's encoded form must have a dictionary (keyed container) at its + /// top level; it may not be an array or scalar value. + static func encode(_ encodable: E) throws -> [String: LeafData] where E: Encodable { + let encoder = EncoderImpl(codingPath: []) try encodable.encode(to: encoder) - let data = encoder.container!.data!.resolve() + + // If the context encoded nothing at all, yield an empty dictionary. + let data = encoder.storage?.resolvedData ?? .dictionary([:]) + + // Unfortunately we have to delay this check until this point thanks to `Encoder` ever so helpfully not + // declaring most of its methods as throwing. guard let dictionary = data.dictionary else { - throw LeafError(.unsupportedFeature("You must use a top level dictionary or type for the context. Arrays are not allowed")) + throw LeafError(.illegalAccess("Leaf contexts must be dictionaries or structure types; arrays and scalar values are not permitted.")) } + return dictionary } } -/// MARK: Private +// MARK: - Private -protocol _Container { - var data: _Data? { get } +/// One of these is always necessary when implementing an unkeyed container, and needed quite often for most +/// other things in Codable. Sure would be nice if the stdlib had one instead of there being 1000-odd versions +/// floating around various dependencies. +fileprivate struct GenericCodingKey: CodingKey, Hashable { + let stringValue: String, intValue: Int? + init(stringValue: String) { (self.stringValue, self.intValue) = (stringValue, Int(stringValue)) } + init(intValue: Int) { (self.stringValue, self.intValue) = ("\(intValue)", intValue) } + var description: String { "GenericCodingKey(\"\(self.stringValue)\"\(self.intValue.map { ", int: \($0)" } ?? ""))" } } -enum _Data { - case container(_Container) - case data(LeafData) - - func resolve() -> LeafData { - switch self { - case .container(let container): - return container.data!.resolve() - case .data(let data): - return data - } - } +/// Helper protocol allowing a single existential representation for all of the possible nested storage patterns +/// that show up during encoding. +fileprivate protocol LeafEncodingResolvable { + var resolvedData: LeafData? { get } } -/// Private `Encoder`. -private final class _Encoder: Encoder { - var userInfo: [CodingUserInfoKey: Any] { [:] } - let codingPath: [CodingKey] - var container: _Container? - - /// Creates a new form url-encoded encoder - init(codingPath: [CodingKey]) { - self.codingPath = codingPath - self.container = nil - } - - /// See `Encoder` - func container(keyedBy type: Key.Type) -> KeyedEncodingContainer - where Key: CodingKey - { - let container = KeyedContainer(codingPath: codingPath) - self.container = container - return .init(container) - } - - /// See `Encoder` - func unkeyedContainer() -> UnkeyedEncodingContainer { - let container = UnkeyedContainer(codingPath: codingPath) - self.container = container - return container - } - - /// See `Encoder` - func singleValueContainer() -> SingleValueEncodingContainer { - let container = SingleValueContainer(codingPath: codingPath) - self.container = container - return container - } +/// A ``LeafData`` value always resolves to itself. +extension LeafData: LeafEncodingResolvable { + var resolvedData: LeafData? { self } } -/// Private `SingleValueEncodingContainer`. -private final class SingleValueContainer: SingleValueEncodingContainer, _Container { - /// See `SingleValueEncodingContainer` - var codingPath: [CodingKey] - - /// The data being encoded - var data: _Data? - - /// Creates a new single value encoder - init(codingPath: [CodingKey]) { - self.codingPath = codingPath - } - - /// See `SingleValueEncodingContainer` - func encodeNil() throws { - // skip - } - - /// See `SingleValueEncodingContainer` - func encode(_ value: T) throws where T: Encodable { - if let leafRepresentable = value as? LeafDataRepresentable { - self.data = .data(leafRepresentable.leafData) - } else { - let encoder = _Encoder(codingPath: self.codingPath) - try value.encode(to: encoder) - self.data = encoder.container!.data +extension LeafEncoder { + /// The ``Encoder`` conformer. + private final class EncoderImpl: Encoder, LeafEncodingResolvable { + var userInfo: [CodingUserInfoKey: Any] + let codingPath: [CodingKey] + var storage: LeafEncodingResolvable? + + /// An encoder can be resolved to the resolved value of its storage. This ability is used to support the + /// the use of `superEncoder()` and `superEncoder(forKey:)`. + var resolvedData: LeafData? { self.storage?.resolvedData } + + init(userInfo: [CodingUserInfoKey: Any] = [:], codingPath: [CodingKey]) { + self.userInfo = userInfo + self.codingPath = codingPath } - } -} - - -/// Private `KeyedEncodingContainerProtocol`. -private final class KeyedContainer: KeyedEncodingContainerProtocol, _Container - where Key: CodingKey -{ - var codingPath: [CodingKey] - - var data: _Data? { - return .data(.dictionary(self.dictionary.mapValues { $0.resolve() })) - } - - var dictionary: [String: _Data] - - init(codingPath: [CodingKey]) { - self.codingPath = codingPath - self.dictionary = [:] - } - - /// See `KeyedEncodingContainerProtocol` - func encodeNil(forKey key: Key) throws { - // skip - } - - /// See `KeyedEncodingContainerProtocol` - func encode(_ value: T, forKey key: Key) throws - where T : Encodable - { - if let leafRepresentable = value as? LeafDataRepresentable { - self.dictionary[key.stringValue] = .data(leafRepresentable.leafData) - } else { - let encoder = _Encoder(codingPath: codingPath + [key]) - try value.encode(to: encoder) - self.dictionary[key.stringValue] = encoder.container!.data + + convenience init(subdecoding encoder: EncoderImpl, withKey key: CodingKey?) { + self.init(userInfo: encoder.userInfo, codingPath: encoder.codingPath + [key].compacted()) + } + + /// Need to expose the ability to access unwrapped keyed container to enable use of nested + /// keyed containers (see the keyed and unkeyed containers). + func rawContainer(keyedBy type: Key.Type) -> EncoderKeyedContainerImpl { + guard self.storage == nil else { fatalError("Can't encode to multiple containers at the same encoding level") } + self.storage = EncoderKeyedContainerImpl(encoder: self) + return self.storage as! EncoderKeyedContainerImpl } - } - - /// See `KeyedEncodingContainerProtocol` - func nestedContainer(keyedBy keyType: NestedKey.Type, forKey key: Key) -> KeyedEncodingContainer - where NestedKey: CodingKey - { - let container = KeyedContainer(codingPath: self.codingPath + [key]) - self.dictionary[key.stringValue] = .container(container) - return .init(container) - } - - /// See `KeyedEncodingContainerProtocol` - func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer { - let container = UnkeyedContainer(codingPath: self.codingPath + [key]) - self.dictionary[key.stringValue] = .container(container) - return container - } - - /// See `KeyedEncodingContainerProtocol` - func superEncoder() -> Encoder { - fatalError() - } - - /// See `KeyedEncodingContainerProtocol` - func superEncoder(forKey key: Key) -> Encoder { - fatalError() - } -} - -/// Private `UnkeyedEncodingContainer`. -private final class UnkeyedContainer: UnkeyedEncodingContainer, _Container { - var codingPath: [CodingKey] - var count: Int - var data: _Data? { - return .data(.array(self.array.map { $0.resolve() })) - } - var array: [_Data] - init(codingPath: [CodingKey]) { - self.codingPath = codingPath - self.count = 0 - self.array = [] - } + func container(keyedBy type: Key.Type) -> KeyedEncodingContainer { + .init(self.rawContainer(keyedBy: type)) + } - func encodeNil() throws { - // skip - } + func unkeyedContainer() -> UnkeyedEncodingContainer { + guard self.storage == nil else { fatalError("Can't encode to multiple containers at the same encoding level") } + self.storage = EncoderUnkeyedContainerImpl(encoder: self) + return self.storage as! EncoderUnkeyedContainerImpl + } - func encode(_ value: T) throws where T: Encodable { - defer { self.count += 1 } - if let leafRepresentable = value as? LeafDataRepresentable { - self.array.append(.data(leafRepresentable.leafData)) - } else { - let encoder = _Encoder(codingPath: codingPath) - try value.encode(to: encoder) - if let safeData = encoder.container!.data { - self.array.append(safeData) + func singleValueContainer() -> SingleValueEncodingContainer { + guard self.storage == nil else { fatalError("Can't encode to multiple containers at the same encoding level") } + self.storage = EncoderValueContainerImpl(encoder: self) + return self.storage as! EncoderValueContainerImpl + } + + /// Encode an arbitrary encodable input, optionally deepening the current coding path with a + /// given key during encoding, and return it as a resolvable item. + func encode(_ value: T, forKey key: CodingKey?) throws -> LeafEncodingResolvable? where T: Encodable { + if let leafRepresentable = value as? LeafDataRepresentable { + /// Shortcut through ``LeafDataRepresentable`` if `T` conforms to it. + return leafRepresentable.leafData + } else { + /// Otherwise, route encoding through a new subdecoder based on self, with an appropriate + /// coding path. This is the central recursion point of the entire Codable setup. + let subencoder = Self.init(subdecoding: self, withKey: key) + try value.encode(to: subencoder) + return subencoder.storage?.resolvedData } } } - /// See UnkeyedEncodingContainer.nestedContainer - func nestedContainer(keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer - where NestedKey: CodingKey - { - defer { self.count += 1 } - let container = KeyedContainer(codingPath: self.codingPath) - self.array.append(.container(container)) - return .init(container) - } - - /// See UnkeyedEncodingContainer.nestedUnkeyedContainer - func nestedUnkeyedContainer() -> UnkeyedEncodingContainer { - defer { self.count += 1 } - let container = UnkeyedContainer(codingPath: self.codingPath) - self.array.append(.container(container)) - return container - } - - /// See UnkeyedEncodingContainer.superEncoder - func superEncoder() -> Encoder { - fatalError() + private final class EncoderValueContainerImpl: SingleValueEncodingContainer, LeafEncodingResolvable { + let encoder: EncoderImpl + var codingPath: [CodingKey] { self.encoder.codingPath } + var resolvedData: LeafData? + + init(encoder: EncoderImpl) { self.encoder = encoder } + func encodeNil() throws {} + func encode(_ value: T) throws where T: Encodable { + self.resolvedData = try self.encoder.encode(value, forKey: nil)?.resolvedData + } } -} -private extension EncodingError { - static func invalidValue(_ value: Any, at path: [CodingKey]) -> EncodingError { - let pathString = path.map { $0.stringValue }.joined(separator: ".") - let context = EncodingError.Context( - codingPath: path, - debugDescription: "Invalid value at '\(pathString)': \(value)" - ) - return Swift.EncodingError.invalidValue(value, context) + private final class EncoderKeyedContainerImpl: KeyedEncodingContainerProtocol, LeafEncodingResolvable where Key: CodingKey { + let encoder: EncoderImpl + var codingPath: [CodingKey] { self.encoder.codingPath } + var data: [String: LeafEncodingResolvable] = [:] + var resolvedData: LeafData? { let compact = self.data.compactMapValues { $0.resolvedData }; return compact.isEmpty ? nil : .dictionary(compact) } + + init(encoder: EncoderImpl) { self.encoder = encoder } + func insert(_ value: T?, forKey key: CodingKey) -> T? { + guard let value = value else { return nil } + self.data[key.stringValue] = value + return value + } + func encodeNil(forKey key: Key) throws {} + func encode(_ value: T, forKey key: Key) throws where T : Encodable { + func _go(_ data: T) -> T? { self.insert(data, forKey: key) } + guard let r = try self.encoder.encode(value, forKey: key) else { return } + _ = _openExistential(r, do: _go(_:)) + } + func nestedContainer(keyedBy keyType: NK.Type, forKey key: Key) -> KeyedEncodingContainer where NK: CodingKey { + /// Use a subencoder to create a nested container so the coding paths are correctly maintained. + /// Save the subcontainer in our data so it can be resolved later before returning it. + .init(self.insert(EncoderImpl(subdecoding: self.encoder, withKey: key).rawContainer(keyedBy: NK.self), forKey: key)!) + } + func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer { + self.insert((EncoderImpl(subdecoding: self.encoder, withKey: key).unkeyedContainer() as! EncoderUnkeyedContainerImpl), forKey: key)! + } + /// A super encoder is, in fact, just a subdecoder with delusions of grandeur and some rather haughty + /// pretensions. (It's mostly Codable's fault anyway.) + func superEncoder() -> Encoder { + self.insert(EncoderImpl(subdecoding: self.encoder, withKey: GenericCodingKey(stringValue: "super")), forKey: GenericCodingKey(stringValue: "super"))! + } + func superEncoder(forKey key: Key) -> Encoder { self.insert(EncoderImpl(subdecoding: self.encoder, withKey: key), forKey: key)! } + } + + private final class EncoderUnkeyedContainerImpl: UnkeyedEncodingContainer, LeafEncodingResolvable { + let encoder: EncoderImpl + var codingPath: [CodingKey] { self.encoder.codingPath } + var count: Int = 0 + var data: [LeafEncodingResolvable] = [] + var nextCodingKey: CodingKey { GenericCodingKey(intValue: self.count) } + var resolvedData: LeafData? { let compact = data.compactMap(\.resolvedData); return compact.isEmpty ? nil : .array(compact) } + + init(encoder: EncoderImpl) { self.encoder = encoder } + func add(_ value: T) throws -> T { + /// Don't increment count until after the append; we don't want to do so if it throws. + self.data.append(value) + self.count += 1 + return value + } + func encodeNil() throws {} + func encode(_ value: T) throws where T: Encodable { + func _go(_ value: T) throws -> T { try self.add(value) } + guard let r = try self.encoder.encode(value, forKey: self.nextCodingKey) else { return } + _ = try _openExistential(r, do: _go(_:)) + } + func nestedContainer(keyedBy keyType: NK.Type) -> KeyedEncodingContainer where NK: CodingKey { + try! .init(self.add(EncoderImpl(subdecoding: self.encoder, withKey: self.nextCodingKey).rawContainer(keyedBy: NK.self))) + } + func nestedUnkeyedContainer() -> UnkeyedEncodingContainer { + try! self.add(EncoderImpl(subdecoding: self.encoder, withKey: self.nextCodingKey).unkeyedContainer() as! EncoderUnkeyedContainerImpl) + } + func superEncoder() -> Encoder { + try! self.add(EncoderImpl(subdecoding: self.encoder, withKey: self.nextCodingKey)) + } } } diff --git a/Sources/Leaf/LeafRenderer+ViewRenderer.swift b/Sources/Leaf/LeafRenderer+ViewRenderer.swift index c1da361..cfc1c57 100644 --- a/Sources/Leaf/LeafRenderer+ViewRenderer.swift +++ b/Sources/Leaf/LeafRenderer+ViewRenderer.swift @@ -27,7 +27,7 @@ extension LeafRenderer { { let data: [String: LeafData] do { - data = try LeafEncoder().encode(context) + data = try LeafEncoder.encode(context) } catch { return self.eventLoop.makeFailedFuture(error) } diff --git a/Tests/LeafTests/LeafEncoderTests.swift b/Tests/LeafTests/LeafEncoderTests.swift new file mode 100644 index 0000000..32c1b5c --- /dev/null +++ b/Tests/LeafTests/LeafEncoderTests.swift @@ -0,0 +1,139 @@ +import Leaf +import LeafKit +import XCTVapor +import Foundation + +final class LeafEncoderTests: XCTestCase { + private func testRender( + of testLeaf: String, + context: Encodable? = nil, + expect expectedStatus: HTTPStatus = .ok, + afterResponse: (XCTHTTPResponse) throws -> (), + file: StaticString = #filePath, line: UInt = #line + ) throws { + var test = TestFiles() + test.files["/foo.leaf"] = testLeaf + + let app = Application(.testing) + defer { app.shutdown() } + app.views.use(.leaf) + app.leaf.sources = .singleSource(test) + if let context = context { + func _defRoute(context: T) { app.get("foo") { $0.view.render("foo", context) } } + _openExistential(context, do: _defRoute(context:)) + } else { + app.get("foo") { $0.view.render("foo") } + } + + try app.test(.GET, "foo") { res in + XCTAssertEqual(res.status, expectedStatus, file: file, line: line) + try afterResponse(res) + } + + } + + func testEmptyContext() throws { + try testRender(of: "Hello!\n") { + XCTAssertEqual($0.body.string, "Hello!\n") + } + } + + func testSimpleScalarContext() throws { + struct Simple: Codable { + let value: Int + } + + try testRender(of: "Value #(value)", context: Simple(value: 1)) { + XCTAssertEqual($0.body.string, "Value 1") + } + } + + func testMultiValueContext() throws { + struct Multi: Codable { + let value: Int + let anotherValue: String + } + + try testRender(of: "Value #(value), string #(anotherValue)", context: Multi(value: 1, anotherValue: "one")) { + XCTAssertEqual($0.body.string, "Value 1, string one") + } + } + + func testArrayContextFails() throws { + try testRender(of: "[1, 2, 3, 4, 5]", context: [1, 2, 3, 4, 5], expect: .internalServerError) { + struct Err: Content { let error: Bool, reason: String } + let errInfo = try $0.content.decode(Err.self) + XCTAssertEqual(errInfo.error, true) + XCTAssert(errInfo.reason.contains("must be dictionaries")) + } + } + + func testNestedContainersContext() throws { + _ = try XCTUnwrap(ProcessInfo.processInfo.environment["SWIFT_DETERMINISTIC_HASHING"]) // required for this test to work + + struct Nested: Codable { let deepSixRedOctober: [Int: MoreNested] } + struct MoreNested: Codable { let things: [EvenMoreNested] } + struct EvenMoreNested: Codable { let thing: [String: Double] } + + try testRender(of: "Everything #(deepSixRedOctober)", context: Nested(deepSixRedOctober: [ + 1: .init(things: [ + .init(thing: ["a": 1.0, "b": 2.0]), + .init(thing: ["c": 4.0, "d": 8.0]) + ]), + 2: .init(things: [ + .init(thing: ["z": 67_108_864.0]) + ]) + ])) { + XCTAssertEqual($0.body.string, """ + Everything [2: "[things: "["[thing: "[z: "67108864.0"]"]"]"]", 1: "[things: "["[thing: "[a: "1.0", b: "2.0"]"]", "[thing: "[d: "8.0", c: "4.0"]"]"]"]"] + """) + } + } + + func testSuperEncoderContext() throws { + _ = try XCTUnwrap(ProcessInfo.processInfo.environment["SWIFT_DETERMINISTIC_HASHING"]) // required for this test to work + + struct BetterCallSuperGoodman: Codable { + let nestedId: Int + let value: String? + } + + struct BreakingCodable: Codable { + let justTheId: Int + let call: BetterCallSuperGoodman + + private enum CodingKeys: String, CodingKey { case id, call } + init(justTheId: Int, call: BetterCallSuperGoodman) { + self.justTheId = justTheId + self.call = call + } + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: Self.CodingKeys.self) + self.justTheId = try container.decode(Int.self, forKey: .id) + self.call = try .init(from: container.superDecoder(forKey: .call)) + } + func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: Self.CodingKeys.self) + try container.encode(self.justTheId, forKey: .id) + try self.call.encode(to: container.superEncoder(forKey: .call)) + } + } + + try testRender(of: """ + KHAAAAAAAAN!!!!!!!!! + + #(id), or you'd better call: + + #(call) + """, context: BreakingCodable(justTheId: 8675309, call: .init(nestedId: 8008, value: "Who R U?")) + ) { + XCTAssertEqual($0.body.string, """ + KHAAAAAAAAN!!!!!!!!! + + 8675309, or you'd better call: + + [nestedId: "8008", value: "Who R U?"] + """) + } + } +} From 02c1fbf3060ee23621618c42a6889d79464f979e Mon Sep 17 00:00:00 2001 From: Gwynne Raskind Date: Sat, 13 Aug 2022 12:50:48 -0500 Subject: [PATCH 2/6] Conform LeafError to AbortError and DebuggableError for better error reporting. --- Sources/Leaf/LeafError+AbortError.swift | 61 +++++++++++++++++++++++++ Tests/LeafTests/LeafTests.swift | 4 +- 2 files changed, 63 insertions(+), 2 deletions(-) create mode 100644 Sources/Leaf/LeafError+AbortError.swift diff --git a/Sources/Leaf/LeafError+AbortError.swift b/Sources/Leaf/LeafError+AbortError.swift new file mode 100644 index 0000000..bd02f6a --- /dev/null +++ b/Sources/Leaf/LeafError+AbortError.swift @@ -0,0 +1,61 @@ +import Vapor +import LeafKit + +extension LeafError { + /// This logic from ``LeafKit/LeafError`` must be duplicated here so we don't end up in infinite + /// recursion trying to access it via the ``localizedDescription`` property. + fileprivate var reasonString: String { + switch self.reason as Reason { + case .illegalAccess(let message): + return "\(message)" + case .unknownError(let message): + return "\(message)" + case .unsupportedFeature(let feature): + return "\(feature) is not implemented" + case .cachingDisabled: + return "Caching is globally disabled" + case .keyExists(let key): + return "Existing entry \(key); use insert with replace=true to overrride" + case .noValueForKey(let key): + return "No cache entry exists for \(key)" + case .unresolvedAST(let key, let dependencies): + return "Flat AST expected; \(key) has unresolved dependencies: \(dependencies)" + case .noTemplateExists(let key): + return "No template found for \(key)" + case .cyclicalReference(let key, let chain): + return "\(key) cyclically referenced in [\(chain.joined(separator: " -> "))]" + case .lexerError(let e): + return "Lexing error - \(e.localizedDescription)" + } + } +} + +/// Conforming ``LeafKit/LeafError`` to ``Vapor/AbortError`` significantly improves the quality of the +/// output generated by the `ErrorMiddleware` should such an error be the outcome a request. +extension LeafError: AbortError { + /// The use of `@_implements` here allows us to get away with the fact that ``Vapor/AbortError`` + /// requires a property named `reason` of type `String` while ``LeafKit/LeafError`` has an + /// identically named property of an enum type. + /// + /// See ``Vapor/AbortError/reason``. + @_implements(AbortError, reason) + public var abortReason: String { self.reasonString } + + /// See ``Vapor/AbortError/status``. + public var status: HTTPResponseStatus { .internalServerError } +} + +/// Conforming ``LeafKit/LeafError`` to ``Vapor/DebuggableError`` allows more and more useful information +/// to be reported when the error is logged to a ``Logging/Logger``. +extension LeafError: DebuggableError { + /// Again, the udnerscored attribute gets around the inconvenient naming collision. + /// + /// See ``Vapor/DebuggableError/reason``. + @_implements(DebuggableError, reason) + public var debuggableReason: String { self.reasonString } + + /// See ``Vapor/DebuggableError/source``. + public var source: ErrorSource? { + .init(file: self.file, function: self.function, line: self.line, column: self.column) + } +} diff --git a/Tests/LeafTests/LeafTests.swift b/Tests/LeafTests/LeafTests.swift index 6cab072..e180712 100644 --- a/Tests/LeafTests/LeafTests.swift +++ b/Tests/LeafTests/LeafTests.swift @@ -3,7 +3,7 @@ import LeafKit import XCTVapor import Foundation -class LeafTests: XCTestCase { +final class LeafTests: XCTestCase { func testApplication() throws { let app = Application(.testing) defer { app.shutdown() } @@ -59,7 +59,7 @@ class LeafTests: XCTestCase { try app.test(.GET, "allowed") { res in XCTAssertEqual(res.status, .internalServerError) - XCTAssert(res.body.string.contains("noTemplateExists")) + XCTAssert(res.body.string.contains("No template found")) } try app.test(.GET, "sandboxed") { res in From 1f7656fd6cce2eda39d13d8f8774f49d14ce68d0 Mon Sep 17 00:00:00 2001 From: Gwynne Raskind Date: Sat, 13 Aug 2022 15:38:28 -0500 Subject: [PATCH 3/6] Clean up and simplify encoder implementation; add a few doc comments, remove unnecessary usage of _openExistential(), remove some unneeded throws and optionals --- Sources/Leaf/LeafEncoder.swift | 83 ++++++++++++++++++++++++---------- 1 file changed, 59 insertions(+), 24 deletions(-) diff --git a/Sources/Leaf/LeafEncoder.swift b/Sources/Leaf/LeafEncoder.swift index 4c31e11..c48b4a7 100644 --- a/Sources/Leaf/LeafEncoder.swift +++ b/Sources/Leaf/LeafEncoder.swift @@ -72,16 +72,19 @@ extension LeafEncoder { return self.storage as! EncoderKeyedContainerImpl } + /// See ``Encoder/container(keyedBy:)``. func container(keyedBy type: Key.Type) -> KeyedEncodingContainer { .init(self.rawContainer(keyedBy: type)) } + /// See ``Encoder/unkeyedContainer()``. func unkeyedContainer() -> UnkeyedEncodingContainer { guard self.storage == nil else { fatalError("Can't encode to multiple containers at the same encoding level") } self.storage = EncoderUnkeyedContainerImpl(encoder: self) return self.storage as! EncoderUnkeyedContainerImpl } + /// See ``Encoder/singleValueContainer()``. func singleValueContainer() -> SingleValueEncodingContainer { guard self.storage == nil else { fatalError("Can't encode to multiple containers at the same encoding level") } self.storage = EncoderValueContainerImpl(encoder: self) @@ -106,11 +109,17 @@ extension LeafEncoder { private final class EncoderValueContainerImpl: SingleValueEncodingContainer, LeafEncodingResolvable { let encoder: EncoderImpl - var codingPath: [CodingKey] { self.encoder.codingPath } var resolvedData: LeafData? + + /// See ``SingleValueEncodingContainer/codingPath``. + var codingPath: [CodingKey] { self.encoder.codingPath } init(encoder: EncoderImpl) { self.encoder = encoder } + + /// See ``SingleValueEncodingContainer/encodeNil()``. func encodeNil() throws {} + + /// See ``SingleValueEncodingContainer/encode(_:)``. func encode(_ value: T) throws where T: Encodable { self.resolvedData = try self.encoder.encode(value, forKey: nil)?.resolvedData } @@ -118,67 +127,93 @@ extension LeafEncoder { private final class EncoderKeyedContainerImpl: KeyedEncodingContainerProtocol, LeafEncodingResolvable where Key: CodingKey { let encoder: EncoderImpl - var codingPath: [CodingKey] { self.encoder.codingPath } var data: [String: LeafEncodingResolvable] = [:] var resolvedData: LeafData? { let compact = self.data.compactMapValues { $0.resolvedData }; return compact.isEmpty ? nil : .dictionary(compact) } - + + /// See ``KeyedEncodingContainerProtocol/codingPath``. + var codingPath: [CodingKey] { self.encoder.codingPath } + init(encoder: EncoderImpl) { self.encoder = encoder } - func insert(_ value: T?, forKey key: CodingKey) -> T? { - guard let value = value else { return nil } + + func insert(_ value: LeafEncodingResolvable, forKey key: CodingKey, as: T.Type = T.self) -> T { self.data[key.stringValue] = value - return value + return value as! T } + + /// See ``KeyedEncodingContainerProtocol/encodeNil()``. func encodeNil(forKey key: Key) throws {} + + /// See ``KeyedEncodingContainerProtocol/encode(_:forKey:)``. func encode(_ value: T, forKey key: Key) throws where T : Encodable { - func _go(_ data: T) -> T? { self.insert(data, forKey: key) } - guard let r = try self.encoder.encode(value, forKey: key) else { return } - _ = _openExistential(r, do: _go(_:)) + guard let encodedValue = try self.encoder.encode(value, forKey: key) else { return } + self.data[key.stringValue] = encodedValue } + + /// See ``KeyedEncodingContainerProtocol/nestedContainer(keyedBy:forKey:)``. func nestedContainer(keyedBy keyType: NK.Type, forKey key: Key) -> KeyedEncodingContainer where NK: CodingKey { /// Use a subencoder to create a nested container so the coding paths are correctly maintained. /// Save the subcontainer in our data so it can be resolved later before returning it. - .init(self.insert(EncoderImpl(subdecoding: self.encoder, withKey: key).rawContainer(keyedBy: NK.self), forKey: key)!) + .init(self.insert(EncoderImpl(subdecoding: self.encoder, withKey: key).rawContainer(keyedBy: NK.self), forKey: key, as: EncoderKeyedContainerImpl.self)) } + + /// See ``KeyedEncodingContainerProtocol/nestedUnkeyedContainer(forKey:)``. func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer { - self.insert((EncoderImpl(subdecoding: self.encoder, withKey: key).unkeyedContainer() as! EncoderUnkeyedContainerImpl), forKey: key)! + self.insert((EncoderImpl(subdecoding: self.encoder, withKey: key).unkeyedContainer() as! EncoderUnkeyedContainerImpl), forKey: key) } + /// A super encoder is, in fact, just a subdecoder with delusions of grandeur and some rather haughty /// pretensions. (It's mostly Codable's fault anyway.) func superEncoder() -> Encoder { - self.insert(EncoderImpl(subdecoding: self.encoder, withKey: GenericCodingKey(stringValue: "super")), forKey: GenericCodingKey(stringValue: "super"))! + self.insert(EncoderImpl(subdecoding: self.encoder, withKey: GenericCodingKey(stringValue: "super")), forKey: GenericCodingKey(stringValue: "super")) } - func superEncoder(forKey key: Key) -> Encoder { self.insert(EncoderImpl(subdecoding: self.encoder, withKey: key), forKey: key)! } + + /// See ``KeyedEncodingContainerProtocol/superEncoder(forKey:)``. + func superEncoder(forKey key: Key) -> Encoder { self.insert(EncoderImpl(subdecoding: self.encoder, withKey: key), forKey: key) } } private final class EncoderUnkeyedContainerImpl: UnkeyedEncodingContainer, LeafEncodingResolvable { let encoder: EncoderImpl - var codingPath: [CodingKey] { self.encoder.codingPath } - var count: Int = 0 var data: [LeafEncodingResolvable] = [] var nextCodingKey: CodingKey { GenericCodingKey(intValue: self.count) } var resolvedData: LeafData? { let compact = data.compactMap(\.resolvedData); return compact.isEmpty ? nil : .array(compact) } + /// See ``UnkeyedEncodingContainer/codingPath``. + var codingPath: [CodingKey] { self.encoder.codingPath } + + /// See ``UnkeyedEncodingContainer/count``. + var count: Int = 0 + init(encoder: EncoderImpl) { self.encoder = encoder } - func add(_ value: T) throws -> T { - /// Don't increment count until after the append; we don't want to do so if it throws. + + func add(_ value: LeafEncodingResolvable, as: T.Type = T.self) -> T { self.data.append(value) self.count += 1 - return value + return value as! T } + + /// See ``UnkeyedEncodingContainer/encodeNil()``. func encodeNil() throws {} + + /// See ``UnkeyedEncodingContainer/encode(_:)``. func encode(_ value: T) throws where T: Encodable { - func _go(_ value: T) throws -> T { try self.add(value) } - guard let r = try self.encoder.encode(value, forKey: self.nextCodingKey) else { return } - _ = try _openExistential(r, do: _go(_:)) + guard let encodedValue = try self.encoder.encode(value, forKey: self.nextCodingKey) else { return } + self.data.append(encodedValue) + self.count += 1 } + + /// See ``UnkeyedEncodingContainer/nestedContainer(keyedBy:)``. func nestedContainer(keyedBy keyType: NK.Type) -> KeyedEncodingContainer where NK: CodingKey { - try! .init(self.add(EncoderImpl(subdecoding: self.encoder, withKey: self.nextCodingKey).rawContainer(keyedBy: NK.self))) + .init(self.add(EncoderImpl(subdecoding: self.encoder, withKey: self.nextCodingKey).rawContainer(keyedBy: NK.self), as: EncoderKeyedContainerImpl.self)) } + + /// See ``UnkeyedEncodingContainer/nestedUnkeyedContainer()``. func nestedUnkeyedContainer() -> UnkeyedEncodingContainer { - try! self.add(EncoderImpl(subdecoding: self.encoder, withKey: self.nextCodingKey).unkeyedContainer() as! EncoderUnkeyedContainerImpl) + self.add(EncoderImpl(subdecoding: self.encoder, withKey: self.nextCodingKey).unkeyedContainer() as! EncoderUnkeyedContainerImpl) } + + /// See ``UnkeyedEncodingContainer/superEncoder()``. func superEncoder() -> Encoder { - try! self.add(EncoderImpl(subdecoding: self.encoder, withKey: self.nextCodingKey)) + self.add(EncoderImpl(subdecoding: self.encoder, withKey: self.nextCodingKey)) } } } From c09a7af0f54750fe27d48f48dcf4f770da9d9c29 Mon Sep 17 00:00:00 2001 From: Gwynne Raskind Date: Mon, 15 Aug 2022 12:01:39 -0500 Subject: [PATCH 4/6] Add API check to workflow --- .github/workflows/test.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8a28744..0a10929 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,3 +7,4 @@ jobs: with: with_coverage: false with_tsan: true + with_public_api_check: true From 506093c5407e903bae0ab858a13645a67a2b3010 Mon Sep 17 00:00:00 2001 From: Gwynne Raskind Date: Tue, 16 Aug 2022 22:49:00 -0500 Subject: [PATCH 5/6] LeafEncoder needs the exact *opposite* behavior as that of Fluent's Fields+Codable conformance with regards to empty containers: They must be explicitly preserved. --- Sources/Leaf/LeafEncoder.swift | 4 ++-- Tests/LeafTests/LeafEncoderTests.swift | 12 ++++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/Sources/Leaf/LeafEncoder.swift b/Sources/Leaf/LeafEncoder.swift index c48b4a7..aa40558 100644 --- a/Sources/Leaf/LeafEncoder.swift +++ b/Sources/Leaf/LeafEncoder.swift @@ -128,7 +128,7 @@ extension LeafEncoder { private final class EncoderKeyedContainerImpl: KeyedEncodingContainerProtocol, LeafEncodingResolvable where Key: CodingKey { let encoder: EncoderImpl var data: [String: LeafEncodingResolvable] = [:] - var resolvedData: LeafData? { let compact = self.data.compactMapValues { $0.resolvedData }; return compact.isEmpty ? nil : .dictionary(compact) } + var resolvedData: LeafData? { .dictionary(self.data.compactMapValues { $0.resolvedData }) } /// See ``KeyedEncodingContainerProtocol/codingPath``. var codingPath: [CodingKey] { self.encoder.codingPath } @@ -175,7 +175,7 @@ extension LeafEncoder { let encoder: EncoderImpl var data: [LeafEncodingResolvable] = [] var nextCodingKey: CodingKey { GenericCodingKey(intValue: self.count) } - var resolvedData: LeafData? { let compact = data.compactMap(\.resolvedData); return compact.isEmpty ? nil : .array(compact) } + var resolvedData: LeafData? { .array(data.compactMap(\.resolvedData)) } /// See ``UnkeyedEncodingContainer/codingPath``. var codingPath: [CodingKey] { self.encoder.codingPath } diff --git a/Tests/LeafTests/LeafEncoderTests.swift b/Tests/LeafTests/LeafEncoderTests.swift index 32c1b5c..7b3f7fb 100644 --- a/Tests/LeafTests/LeafEncoderTests.swift +++ b/Tests/LeafTests/LeafEncoderTests.swift @@ -136,4 +136,16 @@ final class LeafEncoderTests: XCTestCase { """) } } + + func testEncodeDoesntElideEmptyContainers() throws { + struct CodableContainersNeedBetterSemantics: Codable { + let title: String + let todoList: [String] + let toundoList: [String: String] + } + + try testRender(of: "#count(todoList)\n#count(toundoList)", context: CodableContainersNeedBetterSemantics(title: "a", todoList: [], toundoList: [:])) { + XCTAssertEqual($0.body.string, "0\n0") + } + } } From 834d3c30b424382929adf2820461df2f29c7638e Mon Sep 17 00:00:00 2001 From: Gwynne Raskind Date: Tue, 16 Aug 2022 23:19:02 -0500 Subject: [PATCH 6/6] General cleanup of the code. A bit more commenting and better formatting, don't use a separate class for single value containers (should make encoding of complex contexts a little faster), simplify some names, remove unnecessary use of _openExistential() from the tests --- Sources/Leaf/LeafEncoder.swift | 157 +++++++++++++++---------- Tests/LeafTests/LeafEncoderTests.swift | 16 +-- 2 files changed, 100 insertions(+), 73 deletions(-) diff --git a/Sources/Leaf/LeafEncoder.swift b/Sources/Leaf/LeafEncoder.swift index aa40558..9e95e0e 100644 --- a/Sources/Leaf/LeafEncoder.swift +++ b/Sources/Leaf/LeafEncoder.swift @@ -27,10 +27,22 @@ internal struct LeafEncoder { /// other things in Codable. Sure would be nice if the stdlib had one instead of there being 1000-odd versions /// floating around various dependencies. fileprivate struct GenericCodingKey: CodingKey, Hashable { - let stringValue: String, intValue: Int? - init(stringValue: String) { (self.stringValue, self.intValue) = (stringValue, Int(stringValue)) } - init(intValue: Int) { (self.stringValue, self.intValue) = ("\(intValue)", intValue) } - var description: String { "GenericCodingKey(\"\(self.stringValue)\"\(self.intValue.map { ", int: \($0)" } ?? ""))" } + let stringValue: String + let intValue: Int? + + init(stringValue: String) { + self.stringValue = stringValue + self.intValue = Int(stringValue) + } + + init(intValue: Int) { + self.stringValue = "\(intValue)" + self.intValue = intValue + } + + var description: String { + "GenericCodingKey(\"\(self.stringValue)\"\(self.intValue.map { ", int: \($0)" } ?? ""))" + } } /// Helper protocol allowing a single existential representation for all of the possible nested storage patterns @@ -46,9 +58,14 @@ extension LeafData: LeafEncodingResolvable { extension LeafEncoder { /// The ``Encoder`` conformer. - private final class EncoderImpl: Encoder, LeafEncodingResolvable { - var userInfo: [CodingUserInfoKey: Any] + private final class EncoderImpl: Encoder, LeafEncodingResolvable, SingleValueEncodingContainer { + /// See ``Encoder/userinfo``. + let userInfo: [CodingUserInfoKey: Any] + + /// See ``Encoder/codingPath``. let codingPath: [CodingKey] + + /// This encoder's root stored value, if any has been encoded. var storage: LeafEncodingResolvable? /// An encoder can be resolved to the resolved value of its storage. This ability is used to support the @@ -60,16 +77,17 @@ extension LeafEncoder { self.codingPath = codingPath } - convenience init(subdecoding encoder: EncoderImpl, withKey key: CodingKey?) { + convenience init(from encoder: EncoderImpl, withKey key: CodingKey?) { self.init(userInfo: encoder.userInfo, codingPath: encoder.codingPath + [key].compacted()) } /// Need to expose the ability to access unwrapped keyed container to enable use of nested /// keyed containers (see the keyed and unkeyed containers). - func rawContainer(keyedBy type: Key.Type) -> EncoderKeyedContainerImpl { + func rawContainer(keyedBy type: Key.Type) -> KeyedContainerImpl { guard self.storage == nil else { fatalError("Can't encode to multiple containers at the same encoding level") } - self.storage = EncoderKeyedContainerImpl(encoder: self) - return self.storage as! EncoderKeyedContainerImpl + + self.storage = KeyedContainerImpl(encoder: self) + return self.storage as! KeyedContainerImpl } /// See ``Encoder/container(keyedBy:)``. @@ -80,17 +98,25 @@ extension LeafEncoder { /// See ``Encoder/unkeyedContainer()``. func unkeyedContainer() -> UnkeyedEncodingContainer { guard self.storage == nil else { fatalError("Can't encode to multiple containers at the same encoding level") } - self.storage = EncoderUnkeyedContainerImpl(encoder: self) - return self.storage as! EncoderUnkeyedContainerImpl + + self.storage = UnkeyedContainerImpl(encoder: self) + return self.storage as! UnkeyedContainerImpl } /// See ``Encoder/singleValueContainer()``. func singleValueContainer() -> SingleValueEncodingContainer { guard self.storage == nil else { fatalError("Can't encode to multiple containers at the same encoding level") } - self.storage = EncoderValueContainerImpl(encoder: self) - return self.storage as! EncoderValueContainerImpl + return self } - + + /// See ``SingleValueEncodingContainer/encodeNil()``. + func encodeNil() throws {} + + /// See ``SingleValueEncodingContainer/encode(_:)``. + func encode(_ value: T) throws where T: Encodable { + self.storage = try self.encode(value, forKey: nil) + } + /// Encode an arbitrary encodable input, optionally deepening the current coding path with a /// given key during encoding, and return it as a resolvable item. func encode(_ value: T, forKey key: CodingKey?) throws -> LeafEncodingResolvable? where T: Encodable { @@ -100,45 +126,25 @@ extension LeafEncoder { } else { /// Otherwise, route encoding through a new subdecoder based on self, with an appropriate /// coding path. This is the central recursion point of the entire Codable setup. - let subencoder = Self.init(subdecoding: self, withKey: key) + let subencoder = Self.init(from: self, withKey: key) + try value.encode(to: subencoder) return subencoder.storage?.resolvedData } } } - private final class EncoderValueContainerImpl: SingleValueEncodingContainer, LeafEncodingResolvable { - let encoder: EncoderImpl - var resolvedData: LeafData? - - /// See ``SingleValueEncodingContainer/codingPath``. - var codingPath: [CodingKey] { self.encoder.codingPath } + private final class KeyedContainerImpl: KeyedEncodingContainerProtocol, LeafEncodingResolvable where Key: CodingKey { + private let encoder: EncoderImpl + private var data: [String: LeafEncodingResolvable] = [:] - init(encoder: EncoderImpl) { self.encoder = encoder } - - /// See ``SingleValueEncodingContainer/encodeNil()``. - func encodeNil() throws {} - - /// See ``SingleValueEncodingContainer/encode(_:)``. - func encode(_ value: T) throws where T: Encodable { - self.resolvedData = try self.encoder.encode(value, forKey: nil)?.resolvedData - } - } - - private final class EncoderKeyedContainerImpl: KeyedEncodingContainerProtocol, LeafEncodingResolvable where Key: CodingKey { - let encoder: EncoderImpl - var data: [String: LeafEncodingResolvable] = [:] + /// See ``LeafEncodingResolvable/resolvedData``. var resolvedData: LeafData? { .dictionary(self.data.compactMapValues { $0.resolvedData }) } - /// See ``KeyedEncodingContainerProtocol/codingPath``. - var codingPath: [CodingKey] { self.encoder.codingPath } - init(encoder: EncoderImpl) { self.encoder = encoder } - func insert(_ value: LeafEncodingResolvable, forKey key: CodingKey, as: T.Type = T.self) -> T { - self.data[key.stringValue] = value - return value as! T - } + /// See ``KeyedEncodingContainerProtocol/codingPath``. + var codingPath: [CodingKey] { self.encoder.codingPath } /// See ``KeyedEncodingContainerProtocol/encodeNil()``. func encodeNil(forKey key: Key) throws {} @@ -150,31 +156,50 @@ extension LeafEncoder { } /// See ``KeyedEncodingContainerProtocol/nestedContainer(keyedBy:forKey:)``. - func nestedContainer(keyedBy keyType: NK.Type, forKey key: Key) -> KeyedEncodingContainer where NK: CodingKey { + func nestedContainer(keyedBy keyType: NestedKey.Type, forKey key: Key) -> KeyedEncodingContainer { /// Use a subencoder to create a nested container so the coding paths are correctly maintained. /// Save the subcontainer in our data so it can be resolved later before returning it. - .init(self.insert(EncoderImpl(subdecoding: self.encoder, withKey: key).rawContainer(keyedBy: NK.self), forKey: key, as: EncoderKeyedContainerImpl.self)) + .init(self.insert( + EncoderImpl(from: self.encoder, withKey: key).rawContainer(keyedBy: NestedKey.self), + forKey: key, + as: KeyedContainerImpl.self + )) } /// See ``KeyedEncodingContainerProtocol/nestedUnkeyedContainer(forKey:)``. func nestedUnkeyedContainer(forKey key: Key) -> UnkeyedEncodingContainer { - self.insert((EncoderImpl(subdecoding: self.encoder, withKey: key).unkeyedContainer() as! EncoderUnkeyedContainerImpl), forKey: key) + self.insert( + EncoderImpl(from: self.encoder, withKey: key).unkeyedContainer() as! UnkeyedContainerImpl, + forKey: key + ) } /// A super encoder is, in fact, just a subdecoder with delusions of grandeur and some rather haughty /// pretensions. (It's mostly Codable's fault anyway.) func superEncoder() -> Encoder { - self.insert(EncoderImpl(subdecoding: self.encoder, withKey: GenericCodingKey(stringValue: "super")), forKey: GenericCodingKey(stringValue: "super")) + self.insert( + EncoderImpl(from: self.encoder, withKey: GenericCodingKey(stringValue: "super")), + forKey: GenericCodingKey(stringValue: "super") + ) } /// See ``KeyedEncodingContainerProtocol/superEncoder(forKey:)``. - func superEncoder(forKey key: Key) -> Encoder { self.insert(EncoderImpl(subdecoding: self.encoder, withKey: key), forKey: key) } + func superEncoder(forKey key: Key) -> Encoder { + self.insert(EncoderImpl(from: self.encoder, withKey: key), forKey: key) + } + + /// Helper for the encoding methods. + private func insert(_ value: LeafEncodingResolvable, forKey key: CodingKey, as: T.Type = T.self) -> T { + self.data[key.stringValue] = value + return value as! T + } } - private final class EncoderUnkeyedContainerImpl: UnkeyedEncodingContainer, LeafEncodingResolvable { - let encoder: EncoderImpl - var data: [LeafEncodingResolvable] = [] - var nextCodingKey: CodingKey { GenericCodingKey(intValue: self.count) } + private final class UnkeyedContainerImpl: UnkeyedEncodingContainer, LeafEncodingResolvable { + private let encoder: EncoderImpl + private var data: [LeafEncodingResolvable] = [] + + /// See ``LeafEncodingResolvable/resolvedData``. var resolvedData: LeafData? { .array(data.compactMap(\.resolvedData)) } /// See ``UnkeyedEncodingContainer/codingPath``. @@ -185,35 +210,43 @@ extension LeafEncoder { init(encoder: EncoderImpl) { self.encoder = encoder } - func add(_ value: LeafEncodingResolvable, as: T.Type = T.self) -> T { - self.data.append(value) - self.count += 1 - return value as! T - } - /// See ``UnkeyedEncodingContainer/encodeNil()``. func encodeNil() throws {} /// See ``UnkeyedEncodingContainer/encode(_:)``. func encode(_ value: T) throws where T: Encodable { guard let encodedValue = try self.encoder.encode(value, forKey: self.nextCodingKey) else { return } + self.data.append(encodedValue) self.count += 1 } /// See ``UnkeyedEncodingContainer/nestedContainer(keyedBy:)``. - func nestedContainer(keyedBy keyType: NK.Type) -> KeyedEncodingContainer where NK: CodingKey { - .init(self.add(EncoderImpl(subdecoding: self.encoder, withKey: self.nextCodingKey).rawContainer(keyedBy: NK.self), as: EncoderKeyedContainerImpl.self)) + func nestedContainer(keyedBy keyType: NestedKey.Type) -> KeyedEncodingContainer { + .init(self.add( + EncoderImpl(from: self.encoder, withKey: self.nextCodingKey).rawContainer(keyedBy: NestedKey.self), + as: KeyedContainerImpl.self + )) } /// See ``UnkeyedEncodingContainer/nestedUnkeyedContainer()``. func nestedUnkeyedContainer() -> UnkeyedEncodingContainer { - self.add(EncoderImpl(subdecoding: self.encoder, withKey: self.nextCodingKey).unkeyedContainer() as! EncoderUnkeyedContainerImpl) + self.add(EncoderImpl(from: self.encoder, withKey: self.nextCodingKey).unkeyedContainer() as! UnkeyedContainerImpl) } /// See ``UnkeyedEncodingContainer/superEncoder()``. func superEncoder() -> Encoder { - self.add(EncoderImpl(subdecoding: self.encoder, withKey: self.nextCodingKey)) + self.add(EncoderImpl(from: self.encoder, withKey: self.nextCodingKey)) + } + + /// A `CodingKey` corresponding to the index that will be given to the next value added to the array. + private var nextCodingKey: CodingKey { GenericCodingKey(intValue: self.count) } + + /// Helper for the encoding methods. + private func add(_ value: LeafEncodingResolvable, as: T.Type = T.self) -> T { + self.data.append(value) + self.count += 1 + return value as! T } } } diff --git a/Tests/LeafTests/LeafEncoderTests.swift b/Tests/LeafTests/LeafEncoderTests.swift index 7b3f7fb..69fe8c4 100644 --- a/Tests/LeafTests/LeafEncoderTests.swift +++ b/Tests/LeafTests/LeafEncoderTests.swift @@ -4,9 +4,9 @@ import XCTVapor import Foundation final class LeafEncoderTests: XCTestCase { - private func testRender( + private func testRender( of testLeaf: String, - context: Encodable? = nil, + context: E? = nil, expect expectedStatus: HTTPStatus = .ok, afterResponse: (XCTHTTPResponse) throws -> (), file: StaticString = #filePath, line: UInt = #line @@ -19,8 +19,7 @@ final class LeafEncoderTests: XCTestCase { app.views.use(.leaf) app.leaf.sources = .singleSource(test) if let context = context { - func _defRoute(context: T) { app.get("foo") { $0.view.render("foo", context) } } - _openExistential(context, do: _defRoute(context:)) + app.get("foo") { $0.view.render("foo", context) } } else { app.get("foo") { $0.view.render("foo") } } @@ -33,7 +32,7 @@ final class LeafEncoderTests: XCTestCase { } func testEmptyContext() throws { - try testRender(of: "Hello!\n") { + try testRender(of: "Hello!\n", context: Bool?.none) { XCTAssertEqual($0.body.string, "Hello!\n") } } @@ -104,8 +103,7 @@ final class LeafEncoderTests: XCTestCase { private enum CodingKeys: String, CodingKey { case id, call } init(justTheId: Int, call: BetterCallSuperGoodman) { - self.justTheId = justTheId - self.call = call + (self.justTheId, self.call) = (justTheId, call) } init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: Self.CodingKeys.self) @@ -121,17 +119,13 @@ final class LeafEncoderTests: XCTestCase { try testRender(of: """ KHAAAAAAAAN!!!!!!!!! - #(id), or you'd better call: - #(call) """, context: BreakingCodable(justTheId: 8675309, call: .init(nestedId: 8008, value: "Who R U?")) ) { XCTAssertEqual($0.body.string, """ KHAAAAAAAAN!!!!!!!!! - 8675309, or you'd better call: - [nestedId: "8008", value: "Who R U?"] """) }