diff --git a/Package.swift b/Package.swift
index b372bed..3f7dd72 100644
--- a/Package.swift
+++ b/Package.swift
@@ -1,4 +1,4 @@
-// swift-tools-version:5.10
+// swift-tools-version:6.1
import PackageDescription
let package = Package(
@@ -31,7 +31,7 @@ let package = Package(
name: "LeafTests",
dependencies: [
.target(name: "Leaf"),
- .product(name: "XCTVapor", package: "vapor"),
+ .product(name: "VaporTesting", package: "vapor"),
],
exclude: [
"Views",
@@ -43,8 +43,9 @@ let package = Package(
var swiftSettings: [SwiftSetting] { [
.enableUpcomingFeature("ExistentialAny"),
- .enableUpcomingFeature("ConciseMagicFile"),
- .enableUpcomingFeature("ForwardTrailingClosures"),
- .enableUpcomingFeature("DisableOutwardActorInference"),
- .enableExperimentalFeature("StrictConcurrency=complete"),
+ //.enableUpcomingFeature("InternalImportsByDefault"),
+ .enableUpcomingFeature("MemberImportVisibility"),
+ .enableUpcomingFeature("InferIsolatedConformances"),
+ //.enableUpcomingFeature("NonisolatedNonsendingByDefault"),
+ .enableUpcomingFeature("ImmutableWeakCaptures"),
] }
diff --git a/README.md b/README.md
index 9f89689..c4bdee2 100644
--- a/README.md
+++ b/README.md
@@ -1,15 +1,15 @@
-
+
-

-

-

-

-

-

-
-
+[](https://docs.vapor.codes/4.0/)
+[](https://discord.gg/vapor)
+[](./LICENSE)
+[](https://github.com/vapor/leaf/actions/workflows/test.yml)
+[](https://codecov.io/github/vapor/leaf)
+[](https://swift.org)
+
+
-Leaf provides integrations between [LeafKit](https://github.com/vapor/leaf-kit) and [Vapor](https://github.com/vapor/vapor) to make it easy to use Leaf templates in your Vapor app. It provides extensions to make it easy to set up, configure and use Leaf as your renderer in Vapor. It also conforms ``LeafRenderer`` to ``ViewRenderer`` so it can be used to render generic Views in Vapor.
+Leaf provides integrations between [LeafKit](https://github.com/vapor/leaf-kit) and [Vapor](https://github.com/vapor/vapor) to make it easy to use Leaf templates in your Vapor app. It provides extensions to make it easy to set up, configure and use Leaf as your renderer in Vapor. It also conforms `LeafRenderer` to `ViewRenderer` so it can be used to render generic Views in Vapor.
diff --git a/Sources/Leaf/Docs.docc/theme-settings.json b/Sources/Leaf/Docs.docc/theme-settings.json
index dc99069..3b5fafc 100644
--- a/Sources/Leaf/Docs.docc/theme-settings.json
+++ b/Sources/Leaf/Docs.docc/theme-settings.json
@@ -1,21 +1,22 @@
{
"theme": {
- "aside": { "border-radius": "16px", "border-style": "double", "border-width": "3px" },
+ "aside": { "border-radius": "16px", "border-width": "3px", "border-style": "double" },
"border-radius": "0",
"button": { "border-radius": "16px", "border-width": "1px", "border-style": "solid" },
"code": { "border-radius": "16px", "border-width": "1px", "border-style": "solid" },
"color": {
"leaf": { "dark": "hsl(136, 43%, 53%)", "light": "hsl(136, 33%, 48%)" },
- "documentation-intro-fill": "radial-gradient(circle at top, var(--color-leaf) 30%, #000 100%)",
+ "documentation-intro-fill": {
+ "dark": "radial-gradient(circle at top, var(--color-leaf) 0%, #000000 100%)",
+ "light": "radial-gradient(circle at top, var(--color-leaf) 0%, #f0f0f0 100%)"
+ },
"documentation-intro-accent": "var(--color-leaf)",
- "documentation-intro-eyebrow": "white",
"documentation-intro-figure": "white",
- "documentation-intro-title": "white",
"logo-base": { "dark": "#fff", "light": "#000" },
"logo-shape": { "dark": "#000", "light": "#fff" },
"fill": { "dark": "#000", "light": "#fff" }
},
- "icons": { "technology": "/leaf/images/vapor-leaf-logo.svg" }
+ "icons": { "technology": "/leaf/images/Leaf/vapor-leaf-logo.svg" }
},
"features": {
"quickNavigation": { "enable": true },
diff --git a/Sources/Leaf/LeafEncoder.swift b/Sources/Leaf/LeafEncoder.swift
index 452311c..f6e3de8 100644
--- a/Sources/Leaf/LeafEncoder.swift
+++ b/Sources/Leaf/LeafEncoder.swift
@@ -64,6 +64,10 @@ extension LeafData: LeafEncodingResolvable {
}
extension LeafEncoder {
+ private protocol TransformableContainer {
+ func transform(to: NewKey.Type) -> KeyedContainerImpl
+ }
+
/// The ``Encoder`` conformer.
private final class EncoderImpl: Encoder, LeafEncodingResolvable, SingleValueEncodingContainer {
// See `Encoder.userinfo`.
@@ -93,11 +97,14 @@ extension LeafEncoder {
/// 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) -> KeyedContainerImpl {
- guard self.storage == nil else {
- fatalError("Can't encode to multiple containers at the same encoding level")
+ if self.storage == nil {
+ self.storage = KeyedContainerImpl(encoder: self)
+ } else if let transformable = self.storage as? any TransformableContainer {
+ self.storage = transformable.transform(to: Key.self)
+ } else {
+ fatalError("Can't change container types at the same encoding level.")
}
- self.storage = KeyedContainerImpl(encoder: self)
return self.storage as! KeyedContainerImpl
}
@@ -108,11 +115,14 @@ extension LeafEncoder {
// See `Encoder.unkeyedContainer()`.
func unkeyedContainer() -> any UnkeyedEncodingContainer {
- guard self.storage == nil else {
- fatalError("Can't encode to multiple containers at the same encoding level")
+ if self.storage == nil {
+ self.storage = UnkeyedContainerImpl(encoder: self)
+ } else {
+ guard self.storage! is UnkeyedContainerImpl else {
+ fatalError("Can't change container types at the same encoding level.")
+ }
}
- self.storage = UnkeyedContainerImpl(encoder: self)
return self.storage as! UnkeyedContainerImpl
}
@@ -128,6 +138,35 @@ extension LeafEncoder {
// See `SingleValueEncodingContainer.encodeNil()`.
func encodeNil() throws {}
+ // See `SingleValueEncodingContainer.encode(_:)`.
+ func encode(_ value: Bool) throws { self.storage = try self.encode(value, forKey: nil) }
+ // See `SingleValueEncodingContainer.encode(_:)`.
+ func encode(_ value: String) throws { self.storage = try self.encode(value, forKey: nil) }
+ // See `SingleValueEncodingContainer.encode(_:)`.
+ func encode(_ value: Double) throws { self.storage = try self.encode(value, forKey: nil) }
+ // See `SingleValueEncodingContainer.encode(_:)`.
+ func encode(_ value: Float) throws { self.storage = try self.encode(value, forKey: nil) }
+ // See `SingleValueEncodingContainer.encode(_:)`.
+ func encode(_ value: Int) throws { self.storage = try self.encode(value, forKey: nil) }
+ // See `SingleValueEncodingContainer.encode(_:)`.
+ func encode(_ value: Int8) throws { self.storage = try self.encode(value, forKey: nil) }
+ // See `SingleValueEncodingContainer.encode(_:)`.
+ func encode(_ value: Int16) throws { self.storage = try self.encode(value, forKey: nil) }
+ // See `SingleValueEncodingContainer.encode(_:)`.
+ func encode(_ value: Int32) throws { self.storage = try self.encode(value, forKey: nil) }
+ // See `SingleValueEncodingContainer.encode(_:)`.
+ func encode(_ value: Int64) throws { self.storage = try self.encode(value, forKey: nil) }
+ // See `SingleValueEncodingContainer.encode(_:)`.
+ func encode(_ value: UInt) throws { self.storage = try self.encode(value, forKey: nil) }
+ // See `SingleValueEncodingContainer.encode(_:)`.
+ func encode(_ value: UInt8) throws { self.storage = try self.encode(value, forKey: nil) }
+ // See `SingleValueEncodingContainer.encode(_:)`.
+ func encode(_ value: UInt16) throws { self.storage = try self.encode(value, forKey: nil) }
+ // See `SingleValueEncodingContainer.encode(_:)`.
+ func encode(_ value: UInt32) throws { self.storage = try self.encode(value, forKey: nil) }
+ // See `SingleValueEncodingContainer.encode(_:)`.
+ func encode(_ value: UInt64) throws { self.storage = try self.encode(value, forKey: nil) }
+
// See `SingleValueEncodingContainer.encode(_:)`.
func encode(_ value: some Encodable) throws {
self.storage = try self.encode(value, forKey: nil)
@@ -150,20 +189,38 @@ extension LeafEncoder {
}
}
- private final class KeyedContainerImpl: KeyedEncodingContainerProtocol, LeafEncodingResolvable where Key: CodingKey {
+ private final class RefWrapper {
+ var value: T
+
+ init(_ value: T) {
+ self.value = value
+ }
+ }
+
+ private final class KeyedContainerImpl: KeyedEncodingContainerProtocol, LeafEncodingResolvable, TransformableContainer where Key: CodingKey {
private unowned let encoder: EncoderImpl
- private var data: [String: any LeafEncodingResolvable] = [:]
- private var nestedEncoderCaptures: [AnyObject] = []
+ private var data: RefWrapper<[String: any LeafEncodingResolvable]> = .init([:])
+ private var nestedEncoderCaptures: RefWrapper<[AnyObject]> = .init([])
// See `LeafEncodingResolvable.resolvedData`.
var resolvedData: LeafData? {
- .dictionary(self.data.compactMapValues { $0.resolvedData })
+ .dictionary(self.data.value.compactMapValues { $0.resolvedData })
}
init(encoder: EncoderImpl) {
self.encoder = encoder
}
+ private init(encoder: EncoderImpl, data: RefWrapper<[String: any LeafEncodingResolvable]>, nestedEncoderCaptures: RefWrapper<[AnyObject]>) {
+ self.encoder = encoder
+ self.data = data
+ self.nestedEncoderCaptures = nestedEncoderCaptures
+ }
+
+ func transform(to: NewKey.Type = NewKey.self) -> KeyedContainerImpl {
+ .init(encoder: self.encoder, data: self.data, nestedEncoderCaptures: self.nestedEncoderCaptures)
+ }
+
// See `KeyedEncodingContainerProtocol.codingPath`.
var codingPath: [any CodingKey] {
self.encoder.codingPath
@@ -172,20 +229,49 @@ extension LeafEncoder {
// See `KeyedEncodingContainerProtocol.encodeNil()`.
func encodeNil(forKey key: Key) throws {}
+ // See `KeyedEncodingContainerProtocol.encode(_:forKey:)`.
+ func encode(_ value: Bool, forKey key: Key) throws { (try? self.encoder.encode(value, forKey: key))?.map { self.data.value[key.stringValue] = $0 } }
+ // See `KeyedEncodingContainerProtocol.encode(_:forKey:)`.
+ func encode(_ value: String, forKey key: Key) throws { (try? self.encoder.encode(value, forKey: key))?.map { self.data.value[key.stringValue] = $0 } }
+ // See `KeyedEncodingContainerProtocol.encode(_:forKey:)`.
+ func encode(_ value: Double, forKey key: Key) throws { (try? self.encoder.encode(value, forKey: key))?.map { self.data.value[key.stringValue] = $0 } }
+ // See `KeyedEncodingContainerProtocol.encode(_:forKey:)`.
+ func encode(_ value: Float, forKey key: Key) throws { (try? self.encoder.encode(value, forKey: key))?.map { self.data.value[key.stringValue] = $0 } }
+ // See `KeyedEncodingContainerProtocol.encode(_:forKey:)`.
+ func encode(_ value: Int, forKey key: Key) throws { (try? self.encoder.encode(value, forKey: key))?.map { self.data.value[key.stringValue] = $0 } }
+ // See `KeyedEncodingContainerProtocol.encode(_:forKey:)`.
+ func encode(_ value: Int8, forKey key: Key) throws { (try? self.encoder.encode(value, forKey: key))?.map { self.data.value[key.stringValue] = $0 } }
+ // See `KeyedEncodingContainerProtocol.encode(_:forKey:)`.
+ func encode(_ value: Int16, forKey key: Key) throws { (try? self.encoder.encode(value, forKey: key))?.map { self.data.value[key.stringValue] = $0 } }
+ // See `KeyedEncodingContainerProtocol.encode(_:forKey:)`.
+ func encode(_ value: Int32, forKey key: Key) throws { (try? self.encoder.encode(value, forKey: key))?.map { self.data.value[key.stringValue] = $0 } }
+ // See `KeyedEncodingContainerProtocol.encode(_:forKey:)`.
+ func encode(_ value: Int64, forKey key: Key) throws { (try? self.encoder.encode(value, forKey: key))?.map { self.data.value[key.stringValue] = $0 } }
+ // See `KeyedEncodingContainerProtocol.encode(_:forKey:)`.
+ func encode(_ value: UInt, forKey key: Key) throws { (try? self.encoder.encode(value, forKey: key))?.map { self.data.value[key.stringValue] = $0 } }
+ // See `KeyedEncodingContainerProtocol.encode(_:forKey:)`.
+ func encode(_ value: UInt8, forKey key: Key) throws { (try? self.encoder.encode(value, forKey: key))?.map { self.data.value[key.stringValue] = $0 } }
+ // See `KeyedEncodingContainerProtocol.encode(_:forKey:)`.
+ func encode(_ value: UInt16, forKey key: Key) throws { (try? self.encoder.encode(value, forKey: key))?.map { self.data.value[key.stringValue] = $0 } }
+ // See `KeyedEncodingContainerProtocol.encode(_:forKey:)`.
+ func encode(_ value: UInt32, forKey key: Key) throws { (try? self.encoder.encode(value, forKey: key))?.map { self.data.value[key.stringValue] = $0 } }
+ // See `KeyedEncodingContainerProtocol.encode(_:forKey:)`.
+ func encode(_ value: UInt64, forKey key: Key) throws { (try? self.encoder.encode(value, forKey: key))?.map { self.data.value[key.stringValue] = $0 } }
+
// See `KeyedEncodingContainerProtocol.encode(_:forKey:)`.
func encode(_ value: some Encodable, forKey key: Key) throws {
guard let encodedValue = try self.encoder.encode(value, forKey: key) else {
return
}
- self.data[key.stringValue] = encodedValue
+ self.data.value[key.stringValue] = encodedValue
}
// See `KeyedEncodingContainerProtocol.nestedContainer(keyedBy:forKey:)`.
func nestedContainer(keyedBy keyType: NestedKey.Type, forKey key: Key) -> KeyedEncodingContainer {
let nestedEncoder = EncoderImpl(from: self.encoder, withKey: key)
- self.nestedEncoderCaptures.append(nestedEncoder)
+ self.nestedEncoderCaptures.value.append(nestedEncoder)
/// 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.
@@ -200,7 +286,7 @@ extension LeafEncoder {
func nestedUnkeyedContainer(forKey key: Key) -> any UnkeyedEncodingContainer {
let nestedEncoder = EncoderImpl(from: self.encoder, withKey: key)
- self.nestedEncoderCaptures.append(nestedEncoder)
+ self.nestedEncoderCaptures.value.append(nestedEncoder)
return self.insert(
nestedEncoder.unkeyedContainer() as! UnkeyedContainerImpl,
@@ -224,7 +310,7 @@ extension LeafEncoder {
/// Helper for the encoding methods.
private func insert(_ value: any LeafEncodingResolvable, forKey key: any CodingKey, as: T.Type = T.self) -> T {
- self.data[key.stringValue] = value
+ self.data.value[key.stringValue] = value
return value as! T
}
}
@@ -257,11 +343,37 @@ extension LeafEncoder {
func encodeNil() throws {}
// See `UnkeyedEncodingContainer.encode(_:)`.
- func encode(_ value: some Encodable) throws {
- guard let encodedValue = try self.encoder.encode(value, forKey: self.nextCodingKey) else {
- return
- }
+ func encode(_ value: Bool) throws { (try? self.encoder.encode(value, forKey: self.nextCodingKey))?.map { self.data.append($0) } }
+ // See `UnkeyedEncodingContainer.encode(_:)`.
+ func encode(_ value: String) throws { (try? self.encoder.encode(value, forKey: self.nextCodingKey))?.map { self.data.append($0) } }
+ // See `UnkeyedEncodingContainer.encode(_:)`.
+ func encode(_ value: Double) throws { (try? self.encoder.encode(value, forKey: self.nextCodingKey))?.map { self.data.append($0) } }
+ // See `UnkeyedEncodingContainer.encode(_:)`.
+ func encode(_ value: Float) throws { (try? self.encoder.encode(value, forKey: self.nextCodingKey))?.map { self.data.append($0) } }
+ // See `UnkeyedEncodingContainer.encode(_:)`.
+ func encode(_ value: Int) throws { (try? self.encoder.encode(value, forKey: self.nextCodingKey))?.map { self.data.append($0) } }
+ // See `UnkeyedEncodingContainer.encode(_:)`.
+ func encode(_ value: Int8) throws { (try? self.encoder.encode(value, forKey: self.nextCodingKey))?.map { self.data.append($0) } }
+ // See `UnkeyedEncodingContainer.encode(_:)`.
+ func encode(_ value: Int16) throws { (try? self.encoder.encode(value, forKey: self.nextCodingKey))?.map { self.data.append($0) } }
+ // See `UnkeyedEncodingContainer.encode(_:)`.
+ func encode(_ value: Int32) throws { (try? self.encoder.encode(value, forKey: self.nextCodingKey))?.map { self.data.append($0) } }
+ // See `UnkeyedEncodingContainer.encode(_:)`.
+ func encode(_ value: Int64) throws { (try? self.encoder.encode(value, forKey: self.nextCodingKey))?.map { self.data.append($0) } }
+ // See `UnkeyedEncodingContainer.encode(_:)`.
+ func encode(_ value: UInt) throws { (try? self.encoder.encode(value, forKey: self.nextCodingKey))?.map { self.data.append($0) } }
+ // See `UnkeyedEncodingContainer.encode(_:)`.
+ func encode(_ value: UInt8) throws { (try? self.encoder.encode(value, forKey: self.nextCodingKey))?.map { self.data.append($0) } }
+ // See `UnkeyedEncodingContainer.encode(_:)`.
+ func encode(_ value: UInt16) throws { (try? self.encoder.encode(value, forKey: self.nextCodingKey))?.map { self.data.append($0) } }
+ // See `UnkeyedEncodingContainer.encode(_:)`.
+ func encode(_ value: UInt32) throws { (try? self.encoder.encode(value, forKey: self.nextCodingKey))?.map { self.data.append($0) } }
+ // See `UnkeyedEncodingContainer.encode(_:)`.
+ func encode(_ value: UInt64) throws { (try? self.encoder.encode(value, forKey: self.nextCodingKey))?.map { self.data.append($0) } }
+ // See `UnkeyedEncodingContainer.encode(_:)`.
+ func encode(_ value: some Encodable) throws {
+ guard let encodedValue = try self.encoder.encode(value, forKey: self.nextCodingKey) else { return }
self.data.append(encodedValue)
}
diff --git a/Tests/LeafTests/LeafEncoderTests.swift b/Tests/LeafTests/LeafEncoderTests.swift
index 60537c6..4a08060 100644
--- a/Tests/LeafTests/LeafEncoderTests.swift
+++ b/Tests/LeafTests/LeafEncoderTests.swift
@@ -1,15 +1,16 @@
import Leaf
import LeafKit
-import XCTest
-import XCTVapor
+import Testing
+import VaporTesting
-final class LeafEncoderTests: XCTestCase {
+@Suite("LeafEncoder Tests")
+struct LeafEncoderTests {
private func testRender(
of testLeaf: String,
context: (some Encodable & Sendable)? = nil,
expect expectedStatus: HTTPStatus = .ok,
afterResponse: (TestingHTTPResponse) async throws -> (),
- file: StaticString = #filePath, line: UInt = #line
+ sourceLocation: SourceLocation = #_sourceLocation()
) async throws {
var test = TestFiles()
test.files["/foo.leaf"] = testLeaf
@@ -23,55 +24,60 @@ final class LeafEncoderTests: XCTestCase {
app.get("foo") { try await $0.view.render("foo") }
}
- try await app.testable().test(.GET, "foo") { res async throws in
- XCTAssertEqual(res.status, expectedStatus, file: file, line: line)
+ try await app.test(.GET, "foo") { res async throws in
+ #expect(res.status == expectedStatus, sourceLocation: sourceLocation)
try await afterResponse(res)
}
}
}
-
- func testEmptyContext() async throws {
- try await testRender(of: "Hello!\n", context: Bool?.none) {
- XCTAssertEqual($0.body.string, "Hello!\n")
+
+ @Test("empty context")
+ func emptyContext() async throws {
+ try await self.testRender(of: "Hello!\n", context: Bool?.none) {
+ #expect($0.body.string == "Hello!\n")
}
}
-
- func testSimpleScalarContext() async throws {
+
+ @Test("simple scalar context")
+ func simpleScalarContext() async throws {
struct Simple: Codable {
let value: Int
}
- try await testRender(of: "Value #(value)", context: Simple(value: 1)) {
- XCTAssertEqual($0.body.string, "Value 1")
+ try await self.testRender(of: "Value #(value)", context: Simple(value: 1)) {
+ #expect($0.body.string == "Value 1")
}
}
- func testMultiValueContext() async throws {
+ @Test("multi value context")
+ func multiValueContext() async throws {
struct Multi: Codable {
let value: Int
let anotherValue: String
}
- try await testRender(of: "Value #(value), string #(anotherValue)", context: Multi(value: 1, anotherValue: "one")) {
- XCTAssertEqual($0.body.string, "Value 1, string one")
+ try await self.testRender(of: "Value #(value), string #(anotherValue)", context: Multi(value: 1, anotherValue: "one")) {
+ #expect($0.body.string == "Value 1, string one")
}
}
- func testArrayContextFails() async throws {
- try await testRender(of: "[1, 2, 3, 4, 5]", context: [1, 2, 3, 4, 5], expect: .internalServerError) {
+ @Test("array context fails")
+ func arrayContextFails() async throws {
+ try await self.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"))
+ #expect(errInfo.error)
+ #expect(errInfo.reason.contains("must be dictionaries"))
}
}
- func testNestedContainersContext() async throws {
+ @Test("nested containers context")
+ func nestedContainersContext() async throws {
struct Nested: Codable { let deepSixRedOctober: [Int: MoreNested] }
struct MoreNested: Codable { let things: [EvenMoreNested] }
struct EvenMoreNested: Codable { let thing: [String: Double] }
- try await testRender(of: "Everything #(deepSixRedOctober)", context: Nested(deepSixRedOctober: [
+ try await self.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]),
@@ -80,13 +86,14 @@ final class LeafEncoderTests: XCTestCase {
.init(thing: ["z": 67_108_864.0]),
])
])) {
- XCTAssertEqual($0.body.string, """
+ #expect($0.body.string == """
Everything [1: "[things: "["[thing: "[a: "1.0", b: "2.0"]"]", "[thing: "[c: "4.0", d: "8.0"]"]"]"]", 2: "[things: "["[thing: "[z: "67108864.0"]"]"]"]"]
""")
}
}
- func testSuperEncoderContext() async throws {
+ @Test("super encoder context")
+ func superEncoderContext() async throws {
struct BetterCallSuperGoodman: Codable {
let nestedId: Int
let value: String?
@@ -115,7 +122,7 @@ final class LeafEncoderTests: XCTestCase {
}
}
- try await testRender(of: """
+ try await self.testRender(of: """
#(id), or you'd better call:
#(call)
unless you called:
@@ -127,7 +134,7 @@ final class LeafEncoderTests: XCTestCase {
called: .init(nestedId: 1337, value: "Super!")
)
) {
- XCTAssertEqual($0.body.string, """
+ #expect($0.body.string == """
8675309, or you'd better call:
[nestedId: "8008", value: "Who R U?"]
unless you called:
@@ -136,15 +143,41 @@ final class LeafEncoderTests: XCTestCase {
}
}
- func testEncodeDoesntElideEmptyContainers() async throws {
+ @Test("encode doesn't elide empty containers")
+ func encodeDoesntElideEmptyContainers() async throws {
struct CodableContainersNeedBetterSemantics: Codable {
let title: String
let todoList: [String]
let toundoList: [String: String]
}
- try await testRender(of: "#count(todoList)\n#count(toundoList)", context: CodableContainersNeedBetterSemantics(title: "a", todoList: [], toundoList: [:])) {
- XCTAssertEqual($0.body.string, "0\n0")
+ try await self.testRender(of: "#count(todoList)\n#count(toundoList)", context: CodableContainersNeedBetterSemantics(title: "a", todoList: [], toundoList: [:])) {
+ #expect($0.body.string == "0\n0")
+ }
+ }
+
+ @Test("misbehaving encode method works as expected without fatalError()")
+ func misbehavingEncodeMethodWorksAsExpectedWithoutFatalError() async throws {
+ struct FunkilyEncoded: Encodable, Sendable {
+ private enum CodingKeys: String, CodingKey { case title }
+
+ let title: String
+ let subobject: T
+
+ func encode(to encoder: any Encoder) throws {
+ var container = encoder.container(keyedBy: Self.CodingKeys.self)
+ try container.encode(self.title, forKey: .title)
+ try subobject.encode(to: encoder)
+ }
+ }
+
+ struct Normal: Encodable {
+ let thing1: String
+ let thing2: String
+ }
+
+ try await self.testRender(of: "#(title) #(thing1) #(thing2)", context: FunkilyEncoded(title: "a", subobject: Normal(thing1: "b", thing2: "c"))) {
+ #expect($0.body.string == "a b c")
}
}
}
diff --git a/Tests/LeafTests/LeafMemoryGrowthTests.swift b/Tests/LeafTests/LeafMemoryGrowthTests.swift
deleted file mode 100644
index 074fe89..0000000
--- a/Tests/LeafTests/LeafMemoryGrowthTests.swift
+++ /dev/null
@@ -1,50 +0,0 @@
-import XCTest
-import Leaf
-import LeafKit
-import XCTVapor
-
-/*
- to profile memory growth, use an sh script like this:
- ```bash
- #!/bin/zsh
- swift test --filter LeafMemoryGrowthTests &
- sleep 5
- PID=$(ps aux | grep '[l]eafPackageTests' | awk '{print $2}' | head -n1)
- echo "leafPackageTests PID: $PID"
- leaks $PID
- ```
-*/
-final class LeafMemoryGrowthTests: XCTestCase {
-// func testRepeatedRenderMemoryGrowth() async throws {
-// sleep(3) // Keep process alive for leaks profiling
-// var test = TestFiles()
-// test.files["/foo.leaf"] = "Hello #(name)!"
-//
-// try await withApp { app in
-// app.views.use(.leaf)
-// app.leaf.sources = .singleSource(test)
-// struct Foo: Encodable { var name: String }
-// // Render with context 1000 times
-// for _ in 0..<1000 {
-// _ = try await app.leaf.renderer.render(path: "foo", context: Foo(name: "World")).get()
-// }
-// }
-// sleep(10) // Keep process alive for leaks profiling
-// }
-//
-// func testRepeatedRenderNoContextMemoryGrowth() async throws {
-// sleep(3) // Keep process alive for leaks profiling
-// var test = TestFiles()
-// test.files["/foo.leaf"] = "Hello!"
-//
-// try await withApp { app in
-// app.views.use(.leaf)
-// app.leaf.sources = .singleSource(test)
-// // Render without context 1000 times (pass nil context)
-// for _ in 0..<1000 {
-// _ = try await app.leaf.renderer.render(path: "foo", context: Optional.none).get()
-// }
-// }
-// sleep(10) // Keep process alive for leaks profiling
-// }
-}
diff --git a/Tests/LeafTests/LeafTests.swift b/Tests/LeafTests/LeafTests.swift
index 9c8d05f..aed7241 100644
--- a/Tests/LeafTests/LeafTests.swift
+++ b/Tests/LeafTests/LeafTests.swift
@@ -1,25 +1,14 @@
import Leaf
import LeafKit
import NIOConcurrencyHelpers
-import XCTest
-import XCTVapor
-
-public func withApp(_ block: (Application) async throws -> T) async throws -> T {
- let app = try await Application.make(.testing)
- let result: T
- do {
- result = try await block(app)
- } catch {
- try? await app.asyncShutdown()
- throw error
- }
- try await app.asyncShutdown()
- return result
-}
+import Testing
+import VaporTesting
-final class LeafTests: XCTestCase {
+@Suite("Leaf tests")
+struct LeafTests {
#if !os(Android)
- func testApplication() async throws {
+ @Test("application")
+ func application() async throws {
try await withApp { app in
app.views.use(.leaf)
app.leaf.configuration.rootDirectory = projectFolder
@@ -33,16 +22,17 @@ final class LeafTests: XCTestCase {
try await req.view.render("foo", ["foo": "bar"])
}
- try await app.testable().test(.GET, "test-file") { res async throws in
- XCTAssertEqual(res.status, .ok)
- XCTAssertEqual(res.headers.contentType, .html)
+ try await app.test(.GET, "test-file") { res async throws in
+ #expect(res.status == .ok)
+ #expect(res.headers.contentType == .html)
// test: #(foo)
- XCTAssertContains(res.body.string, "test: bar")
+ #expect(res.body.string.contains("test: bar"))
}
}
}
- func testSandboxing() async throws {
+ @Test("sandboxing")
+ func sandboxing() async throws {
try await withApp { app in
app.views.use(.leaf)
app.leaf.configuration.rootDirectory = templateFolder
@@ -63,26 +53,27 @@ final class LeafTests: XCTestCase {
try await req.view.render("../../../../hello")
}
- try await app.testable().test(.GET, "hello") { res async in
- XCTAssertEqual(res.status, .ok)
- XCTAssertEqual(res.headers.contentType, .html)
- XCTAssertEqual(res.body.string, "Hello, world!\n")
+ try await app.test(.GET, "hello") { res async in
+ #expect(res.status == .ok)
+ #expect(res.headers.contentType == .html)
+ #expect(res.body.string == "Hello, world!\n")
}
- try await app.testable().test(.GET, "allowed") { res async in
- XCTAssertEqual(res.status, .internalServerError)
- XCTAssert(res.body.string.contains("No template found"))
+ try await app.test(.GET, "allowed") { res async in
+ #expect(res.status == .internalServerError)
+ #expect(res.body.string.contains("No template found"))
}
- try await app.testable().test(.GET, "sandboxed") { res async in
- XCTAssertEqual(res.status, .internalServerError)
- XCTAssert(res.body.string.contains("Attempted to escape sandbox"))
+ try await app.test(.GET, "sandboxed") { res async in
+ #expect(res.status == .internalServerError)
+ #expect(res.body.string.contains("Attempted to escape sandbox"))
}
}
}
#endif
- func testContextRequest() async throws {
+ @Test("context request")
+ func contextRequest() async throws {
var test = TestFiles()
test.files["/foo.leaf"] = """
Hello #(name) @ #source()
@@ -112,21 +103,22 @@ final class LeafTests: XCTestCase {
])
}
- try await app.testable().test(.GET, "test-file") { res async in
- XCTAssertEqual(res.status, .ok)
- XCTAssertEqual(res.headers.contentType, .html)
- XCTAssertEqual(res.body.string, "Hello vapor @ /test-file")
+ try await app.test(.GET, "test-file") { res async in
+ #expect(res.status == .ok)
+ #expect(res.headers.contentType == .html)
+ #expect(res.body.string == "Hello vapor @ /test-file")
}
- try await app.testable().test(.GET, "test-file-with-application-renderer") { res async in
- XCTAssertEqual(res.status, .ok)
- XCTAssertEqual(res.headers.contentType, .html)
- XCTAssertEqual(res.body.string, "Hello World @ application")
+ try await app.test(.GET, "test-file-with-application-renderer") { res async in
+ #expect(res.status == .ok)
+ #expect(res.headers.contentType == .html)
+ #expect(res.body.string == "Hello World @ application")
}
}
}
-
- func testContextUserInfo() async throws {
+
+ @Test("context userInfo")
+ func contextUserInfo() async throws {
var test = TestFiles()
test.files["/foo.leaf"] = """
Hello #custom()! @ #source() app nil? #application()
@@ -167,49 +159,56 @@ final class LeafTests: XCTestCase {
try await req.application.leaf.renderer.render("foo")
}
- try await app.testable().test(.GET, "test-file") { res async in
- XCTAssertEqual(res.status, .ok)
- XCTAssertEqual(res.headers.contentType, .html)
- XCTAssertEqual(res.body.string, "Hello World! @ /test-file app nil? non-nil app")
+ try await app.test(.GET, "test-file") { res async in
+ #expect(res.status == .ok)
+ #expect(res.headers.contentType == .html)
+ #expect(res.body.string == "Hello World! @ /test-file app nil? non-nil app")
}
- try await app.testable().test(.GET, "test-file-with-application-renderer") { res async in
- XCTAssertEqual(res.status, .ok)
- XCTAssertEqual(res.headers.contentType, .html)
- XCTAssertEqual(res.body.string, "Hello World! @ application app nil? non-nil app")
+ try await app.test(.GET, "test-file-with-application-renderer") { res async in
+ #expect(res.status == .ok)
+ #expect(res.headers.contentType == .html)
+ #expect(res.body.string == "Hello World! @ application app nil? non-nil app")
}
}
}
- func testLeafCacheDisabledInDevelopment() async throws {
+ @Test("LeafCache is disabled in development")
+ func leafCacheDisabledInDevelopment() async throws {
let app = try await Application.make(.development)
app.views.use(.leaf)
- guard let renderer = app.view as? LeafRenderer else {
+ do {
+ let renderer = try #require(app.view as? LeafRenderer)
+
+ #expect(!renderer.cache.isEnabled)
+ } catch {
try? await app.asyncShutdown()
- return XCTFail("app.view is not a LeafRenderer")
+ throw error
}
-
- XCTAssertFalse(renderer.cache.isEnabled)
try await app.asyncShutdown()
}
- func testLeafCacheEnabledInProduction() async throws {
+ @Test("LeafCache is enabled in production")
+ func leafCacheEnabledInProduction() async throws {
let app = try await Application.make(.production)
app.views.use(.leaf)
- guard let renderer = app.view as? LeafRenderer else {
+ do {
+ let renderer = try #require(app.view as? LeafRenderer)
+
+ #expect(renderer.cache.isEnabled)
+ } catch {
try? await app.asyncShutdown()
- return XCTFail("app.view is not a LeafRenderer")
+ throw error
}
-
- XCTAssertTrue(renderer.cache.isEnabled)
try await app.asyncShutdown()
}
- func testLeafRendererWithEncodableContext() async throws {
+ @Test("LeafRenderer with Encodable context")
+ func leafRendererWithEncodableContext() async throws {
var test = TestFiles()
test.files["/foo.leaf"] = """
Hello #(name)!
@@ -240,15 +239,16 @@ final class LeafTests: XCTestCase {
return NotHTML(data: data)
}
- try await app.testable().test(.GET, "foo") { res async in
- XCTAssertEqual(res.status, .ok)
- XCTAssertEqual(res.headers.first(name: "content-type"), "application/not-html")
- XCTAssertEqual(res.body.string, "Hello World!")
+ try await app.test(.GET, "foo") { res async in
+ #expect(res.status == .ok)
+ #expect(res.headers.first(name: "content-type") == "application/not-html")
+ #expect(res.body.string == "Hello World!")
}
}
}
- func testNoFatalErrorWhenAttemptingToUseArrayAsContext() async throws {
+ @Test("no fatalError() when attempting to use array as context")
+ func noFatalErrorWhenAttemptingToUseArrayAsContext() async throws {
var test = TestFiles()
test.files["/foo.leaf"] = """
Hello #(name)!
@@ -269,14 +269,15 @@ final class LeafTests: XCTestCase {
return try await req.view.render("foo", context)
}
- try await app.testable().test(.GET, "noCrash") { res async in
- XCTAssertEqual(res.status, .internalServerError)
+ try await app.test(.GET, "noCrash") { res async in
+ #expect(res.status == .internalServerError)
}
}
}
// Test for GH Issue #197
- func testNoFatalErrorWhenAttemptingToUseArrayWithNil() async throws {
+ @Test("no fatalError() when attempting to use array with nil")
+ func noFatalErrorWhenAttemptingToUseArrayWithNil() async throws {
var test = TestFiles()
test.files["/foo.leaf"] = """
#(value)
@@ -300,12 +301,12 @@ final class LeafTests: XCTestCase {
return try await req.view.render("foo", context)
}
- try await app.testable().test(.GET, "noCrash") { res async in
+ try await app.test(.GET, "noCrash") { res async in
// Expected result .ok
- XCTAssertEqual(res.status, .ok)
+ #expect(res.status == .ok)
// Rendered result should match to all non-nil values
- XCTAssertEqual(res.body.string, "["\(id1)", "\(id2)"]")
+ #expect(res.body.string == "["\(id1)", "\(id2)"]")
}
}
}