From 8e54501ab253ae39d0304bff9fc4fc0ae1c4335e Mon Sep 17 00:00:00 2001
From: Florian Friedrich <ffried@me.com>
Date: Tue, 12 Mar 2024 10:18:15 +0100
Subject: [PATCH] Add Swift 5.10 support

---
 .github/workflows/docs.yml                   |  2 +-
 .github/workflows/swift-test.yml             |  7 +--
 Package.swift                                | 11 ++--
 Package@swift-5.9.swift                      | 55 ++++++++++++++++++++
 Sources/RouteDocs/DocumentationDecoder.swift | 24 ++++++---
 5 files changed, 83 insertions(+), 16 deletions(-)
 create mode 100644 Package@swift-5.9.swift

diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
index 0b5e5e7..606b746 100644
--- a/.github/workflows/docs.yml
+++ b/.github/workflows/docs.yml
@@ -20,7 +20,7 @@ jobs:
     uses: sersoft-gmbh/oss-common-actions/.github/workflows/swift-generate-and-publish-docs.yml@main
     with:
       os: ubuntu
-      swift-version: '5.9'
+      swift-version: '5.10'
       organisation: ${{ github.repository_owner }}
       repository: ${{ github.event.repository.name }}
       pages-branch: gh-pages
diff --git a/.github/workflows/swift-test.yml b/.github/workflows/swift-test.yml
index ff8f66f..f6d4d3b 100644
--- a/.github/workflows/swift-test.yml
+++ b/.github/workflows/swift-test.yml
@@ -12,7 +12,7 @@ permissions:
 jobs:
   variables:
     outputs:
-      max-supported-swift-version: '5.9'
+      max-supported-swift-version: '5.10'
       xcode-scheme: route-docs
       xcode-platform-version: latest
       fail-if-codecov-fails: true
@@ -25,7 +25,7 @@ jobs:
     strategy:
       matrix:
         os: [ macOS, ubuntu ]
-        swift-version-offset: [ 0 ]
+        swift-version-offset: [ 0, 1 ]
     uses: sersoft-gmbh/oss-common-actions/.github/workflows/swift-test-spm.yml@main
     with:
       os: ${{ matrix.os }}
@@ -45,7 +45,8 @@ jobs:
           # - iPadOS
           # - tvOS
           # - watchOS
-        swift-version-offset: [ 0 ]
+          # - visionOS
+        swift-version-offset: [ 0, 1 ]
     uses: sersoft-gmbh/oss-common-actions/.github/workflows/swift-test-xcode.yml@main
     with:
       xcode-scheme: ${{ needs.variables.outputs.xcode-scheme }}
diff --git a/Package.swift b/Package.swift
index 78d5865..929112b 100644
--- a/Package.swift
+++ b/Package.swift
@@ -1,4 +1,4 @@
-// swift-tools-version:5.9
+// swift-tools-version:5.10
 // The swift-tools-version declares the minimum version of Swift required to build this package.
 
 import PackageDescription
@@ -8,9 +8,12 @@ let swiftSettings: Array<SwiftSetting> = [
     .enableUpcomingFeature("ExistentialAny"),
     .enableUpcomingFeature("BareSlashRegexLiterals"),
     .enableUpcomingFeature("DisableOutwardActorInference"),
+    .enableUpcomingFeature("IsolatedDefaultValues"),
+    .enableUpcomingFeature("DeprecateApplicationMain"),
+    .enableExperimentalFeature("StrictConcurrency"),
+    .enableExperimentalFeature("GlobalConcurrency"),
 //    .enableExperimentalFeature("AccessLevelOnImport"),
 //    .enableExperimentalFeature("VariadicGenerics"),
-//    .unsafeFlags(["-warn-concurrency"], .when(configuration: .debug)),
 ]
 
 let package = Package(
@@ -27,8 +30,8 @@ let package = Package(
     dependencies: [
         // Dependencies declare other packages that this package depends on.
         .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"),
-        .package(url: "https://github.com/apple/swift-nio", from: "2.59.0"),
-        .package(url: "https://github.com/vapor/vapor", from: "4.84.0"),
+        .package(url: "https://github.com/apple/swift-nio", from: "2.64.0"),
+        .package(url: "https://github.com/vapor/vapor", from: "4.92.0"),
         .package(url: "https://github.com/vapor/leaf-kit", from: "1.10.0"),
         .package(url: "https://github.com/vapor/leaf", from: "4.2.0"),
     ],
diff --git a/Package@swift-5.9.swift b/Package@swift-5.9.swift
new file mode 100644
index 0000000..4ef2b81
--- /dev/null
+++ b/Package@swift-5.9.swift
@@ -0,0 +1,55 @@
+// swift-tools-version:5.9
+// The swift-tools-version declares the minimum version of Swift required to build this package.
+
+import PackageDescription
+
+let swiftSettings: Array<SwiftSetting> = [
+    .enableUpcomingFeature("ConciseMagicFile"),
+    .enableUpcomingFeature("ExistentialAny"),
+    .enableUpcomingFeature("BareSlashRegexLiterals"),
+    .enableUpcomingFeature("DisableOutwardActorInference"),
+    .enableExperimentalFeature("StrictConcurrency"),
+//    .enableExperimentalFeature("AccessLevelOnImport"),
+//    .enableExperimentalFeature("VariadicGenerics"),
+]
+
+let package = Package(
+    name: "route-docs",
+    platforms: [
+        .macOS(.v10_15),
+    ],
+    products: [
+        // Products define the executables and libraries produced by a package, and make them visible to other packages.
+        .library(
+            name: "RouteDocs",
+            targets: ["RouteDocs"]),
+    ],
+    dependencies: [
+        // Dependencies declare other packages that this package depends on.
+        .package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"),
+        .package(url: "https://github.com/apple/swift-nio", from: "2.59.0"),
+        .package(url: "https://github.com/vapor/vapor", from: "4.84.0"),
+        .package(url: "https://github.com/vapor/leaf-kit", from: "1.10.0"),
+        .package(url: "https://github.com/vapor/leaf", from: "4.2.0"),
+    ],
+    targets: [
+        // Targets are the basic building blocks of a package. A target can define a module or a test suite.
+        // Targets can depend on other targets in this package, and on products in packages which this package depends on.
+        .target(
+            name: "RouteDocs",
+            dependencies: [
+                .product(name: "NIOConcurrencyHelpers", package: "swift-nio"),
+                .product(name: "Vapor", package: "vapor"),
+                .product(name: "LeafKit", package: "leaf-kit"),
+                .product(name: "Leaf", package: "leaf"),
+            ],
+            resources: [
+                .copy("DefaultDocsView"),
+            ],
+            swiftSettings: swiftSettings),
+        .testTarget(
+            name: "RouteDocsTests",
+            dependencies: ["RouteDocs"],
+            swiftSettings: swiftSettings),
+    ]
+)
diff --git a/Sources/RouteDocs/DocumentationDecoder.swift b/Sources/RouteDocs/DocumentationDecoder.swift
index 60bdc44..5b2a9d6 100644
--- a/Sources/RouteDocs/DocumentationDecoder.swift
+++ b/Sources/RouteDocs/DocumentationDecoder.swift
@@ -59,6 +59,10 @@ public struct DocumentationObject: Sendable, Hashable, CustomStringConvertible {
         }
     }
 
+    public static func clearTypeCaches() {
+        DocumentationDecoder.Cache.clear()
+    }
+
     public let type: Any.Type
     public fileprivate(set) var body: Body
 
@@ -166,7 +170,7 @@ fileprivate struct DocumentationDecoder: Decoder {
         self.init(storage: .init(type: type), codingPath: .init(), userInfo: userInfo)
     }
 
-    func push(key: some CodingKey) -> DocumentationDecoder {
+    func pushKey(_ key: some CodingKey) -> DocumentationDecoder {
         .init(storage: storage, codingPath: codingPath + CollectionOfOne<any CodingKey>(key), userInfo: userInfo)
     }
 
@@ -201,7 +205,7 @@ fileprivate struct DocumentationDecoder: Decoder {
             return result
         }
         try setType(type, for: key)
-        let result = try T(from: push(key: key))
+        let result = try T(from: pushKey(key))
         try cache(result)
         return result
     }
@@ -283,7 +287,7 @@ extension DocumentationDecoder {
     }
 
     fileprivate enum Cache {
-        struct Entry {
+        struct Entry: @unchecked Sendable {
             let object: any Decodable
             let documentation: DocumentationObject
         }
@@ -298,6 +302,10 @@ extension DocumentationDecoder {
             // We must use the doc's type here, otherwise we mix up optionals vs. non-optionals.
             storage.withLockedValue { $0[ObjectIdentifier(entry.documentation.type)] = entry }
         }
+
+        static func clear() {
+            storage.withLockedValue { $0.removeAll() }
+        }
     }
 
     fileprivate final class TypeBuilder {
@@ -445,15 +453,15 @@ extension DocumentationDecoder {
         func nestedContainer<NestedKey>(keyedBy type: NestedKey.Type, forKey key: Key) throws -> KeyedDecodingContainer<NestedKey>
         where NestedKey: CodingKey
         {
-            KeyedDecodingContainer(KeyedContainer<NestedKey>(decoder: decoder.push(key: key)))
+            KeyedDecodingContainer(KeyedContainer<NestedKey>(decoder: decoder.pushKey(key)))
         }
 
         func nestedUnkeyedContainer(forKey key: Key) throws -> any UnkeyedDecodingContainer {
-            UnkeyedContainer(decoder: decoder.push(key: key))
+            UnkeyedContainer(decoder: decoder.pushKey(key))
         }
 
         func superDecoder() throws -> any Decoder { decoder }
-        func superDecoder(forKey key: Key) throws -> any Decoder { decoder.push(key: key) }
+        func superDecoder(forKey key: Key) throws -> any Decoder { decoder.pushKey(key) }
     }
 
     fileprivate struct UnkeyedContainer: UnkeyedDecodingContainer {
@@ -595,12 +603,12 @@ extension DocumentationDecoder {
         where NestedKey: CodingKey
         {
             defer { currentIndex += 1 }
-            return KeyedDecodingContainer(KeyedContainer(decoder: decoder.push(key: IndexKey(index: currentIndex))))
+            return KeyedDecodingContainer(KeyedContainer(decoder: decoder.pushKey(IndexKey(index: currentIndex))))
         }
 
         mutating func nestedUnkeyedContainer() throws -> any UnkeyedDecodingContainer {
             defer { currentIndex += 1 }
-            return UnkeyedContainer(decoder: decoder.push(key: IndexKey(index: currentIndex)))
+            return UnkeyedContainer(decoder: decoder.pushKey(IndexKey(index: currentIndex)))
         }
 
         mutating func superDecoder() throws -> any Decoder { decoder }