Skip to content

Commit 93f60fd

Browse files
committed
Merge branch 'add-custom-package-collection-link' into custom-collection-page
2 parents f2fd092 + 3c84f04 commit 93f60fd

15 files changed

+258
-21
lines changed

Sources/App/Controllers/PackageCollectionController.swift

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,40 @@ enum PackageCollectionController {
2020
static func generate(req: Request) async throws -> SignedCollection {
2121
AppMetrics.packageCollectionGetTotal?.inc()
2222

23-
guard let owner = req.parameters.get("owner") else { throw Abort(.notFound) }
23+
guard let collectionType = getCollectionType(req: req) else {
24+
throw Abort(.notFound)
25+
}
2426

2527
do {
26-
return try await SignedCollection.generate(
27-
db: req.db,
28-
filterBy: .author(owner),
29-
authorName: "\(owner) via the Swift Package Index"
30-
)
28+
switch collectionType {
29+
case let .author(owner):
30+
return try await SignedCollection.generate(
31+
db: req.db,
32+
filterBy: .author(owner),
33+
authorName: "\(owner) via the Swift Package Index"
34+
)
35+
case let .custom(name):
36+
return try await SignedCollection.generate(
37+
db: req.db,
38+
filterBy: .customCollection(name),
39+
authorName: "Swift Package Index",
40+
collectionName: name,
41+
overview: "A custom package collection generated by the Swift Package Index"
42+
)
43+
}
3144
} catch PackageCollection.Error.noResults {
3245
throw Abort(.notFound)
3346
}
3447
}
48+
49+
enum CollectionType {
50+
case author(String)
51+
case custom(String)
52+
}
53+
54+
static func getCollectionType(req: Request) -> CollectionType? {
55+
if let owner = req.parameters.get("owner") { return .author(owner) }
56+
if let name = req.parameters.get("name") { return .custom(name) }
57+
return nil
58+
}
3559
}

Sources/App/Core/Extensions/String+ext.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,10 @@ extension String {
5858
var pathEncoded: Self {
5959
replacingOccurrences(of: "/", with: "-")
6060
}
61+
62+
var urlPathEncoded: Self {
63+
addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? self
64+
}
6165
}
6266

6367

Sources/App/Core/PackageCollection+VersionResult.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@ extension PackageCollection.VersionResult {
5555
switch filter {
5656
case let .author(owner):
5757
query.filter(Repository.self, \Repository.$owner, .custom("ilike"), owner)
58+
case let .customCollection(name):
59+
query
60+
.join(CustomCollectionPackage.self, on: \Package.$id == \CustomCollectionPackage.$package.$id)
61+
.join(CustomCollection.self, on: \CustomCollection.$id == \CustomCollectionPackage.$customCollection.$id)
62+
.filter(CustomCollection.self, \.$name == name)
5863
case let .urls(packageURLs):
5964
query.filter(App.Package.self, \.$url ~~ packageURLs)
6065
}

Sources/App/Core/PackageCollection+generate.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ extension PackageCollection {
3434
enum Filter {
3535
case urls([String])
3636
case author(String)
37+
case customCollection(String)
3738
}
3839

3940
enum Error: Swift.Error {
@@ -97,10 +98,14 @@ extension PackageCollection {
9798
return owner
9899
case (.author, .some(let label)):
99100
return label
100-
case (.urls, .some(let label)):
101+
case (.customCollection(let name), .none):
102+
return name
103+
case (.customCollection, .some(let label)):
101104
return label
102105
case (.urls(let urls), .none):
103106
return author(for: urls)
107+
case (.urls, .some(let label)):
108+
return label
104109
}
105110
}
106111

Sources/App/Core/SiteURL.swift

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,8 @@ enum SiteURL: Resourceable, Sendable {
121121
case javascripts(String)
122122
case keywords(_ keyword: Parameter<String>)
123123
case package(_ owner: Parameter<String>, _ repository: Parameter<String>, PackagePathComponents?)
124-
case packageCollection(_ owner: Parameter<String>)
124+
case packageCollectionAuthor(_ owner: Parameter<String>)
125+
case packageCollectionCustom(_ name: Parameter<String>)
125126
case packageCollections
126127
case privacy
127128
case readyForSwift6
@@ -171,7 +172,7 @@ enum SiteURL: Resourceable, Sendable {
171172
return "build-monitor"
172173

173174
case let .collections(.value(name)):
174-
return "collections/\(name)"
175+
return "collections/\(name.urlPathEncoded)"
175176

176177
case .collections(.key):
177178
fatalError("path must not be called with a name parameter")
@@ -208,10 +209,16 @@ enum SiteURL: Resourceable, Sendable {
208209
case .package:
209210
fatalError("invalid path: \(self)")
210211

211-
case let .packageCollection(.value(owner)):
212+
case let .packageCollectionAuthor(.value(owner)):
212213
return "\(owner)/collection.json"
213214

214-
case .packageCollection(.key):
215+
case .packageCollectionAuthor(.key):
216+
fatalError("invalid path: \(self)")
217+
218+
case let .packageCollectionCustom(.value(name)):
219+
return "collections/\(name.urlPathEncoded)/collection.json"
220+
221+
case .packageCollectionCustom(.key):
215222
fatalError("invalid path: \(self)")
216223

217224
case .packageCollections:
@@ -311,10 +318,16 @@ enum SiteURL: Resourceable, Sendable {
311318
case .package:
312319
fatalError("pathComponents must not be called with a value parameter")
313320

314-
case .packageCollection(.key):
321+
case .packageCollectionAuthor(.key):
315322
return [":owner", "collection.json"]
316323

317-
case .packageCollection(.value):
324+
case .packageCollectionAuthor(.value):
325+
fatalError("pathComponents must not be called with a value parameter")
326+
327+
case .packageCollectionCustom(.key):
328+
return ["collections", ":name", "collection.json"]
329+
330+
case .packageCollectionCustom(.value):
318331
fatalError("pathComponents must not be called with a value parameter")
319332

320333
case .images, .javascripts, .stylesheets:

Sources/App/Views/Author/AuthorShow+View.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ enum AuthorShow {
5959
),
6060
.copyableInputForm(buttonName: "Copy Package Collection URL",
6161
eventName: "Copy Package Collection URL Button",
62-
valueToCopy: SiteURL.packageCollection(.value(model.owner)).absoluteURL()),
62+
valueToCopy: SiteURL.packageCollectionAuthor(.value(model.owner)).absoluteURL()),
6363
.hr(.class("minor")),
6464
.ul(
6565
.id("package-list"),

Sources/App/Views/CustomCollection/CustomCollectionShow+View.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,18 @@ enum CustomCollectionShow {
4848
.class("trimmed"),
4949
.text("Packages for collection “\(model.name)")
5050
),
51+
.p(
52+
.text("These packages are available as a package collection, "),
53+
.a(
54+
.href(SiteURL.packageCollections.relativeURL()),
55+
"usable in Xcode 13 or the Swift Package Manager 5.5"
56+
),
57+
.text(".")
58+
),
59+
.copyableInputForm(buttonName: "Copy Package Collection URL",
60+
eventName: "Copy Package Collection URL Button",
61+
valueToCopy: SiteURL.packageCollectionCustom(.value(model.name)).absoluteURL()),
62+
.hr(.class("minor")),
5163
.ul(
5264
.id("package-list"),
5365
.group(

Sources/App/routes.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,8 +84,10 @@ func routes(_ app: Application) throws {
8484
}
8585
}
8686

87-
do { // package collection page
88-
app.get(SiteURL.packageCollection(.key).pathComponents,
87+
do { // package collection pages
88+
app.get(SiteURL.packageCollectionAuthor(.key).pathComponents,
89+
use: PackageCollectionController.generate).excludeFromOpenAPI()
90+
app.get(SiteURL.packageCollectionCustom(.key).pathComponents,
8991
use: PackageCollectionController.generate).excludeFromOpenAPI()
9092
}
9193

Tests/AppTests/ApiTests.swift

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -809,7 +809,6 @@ class ApiTests: AppTestCase {
809809
try await v.save(on: app.db)
810810
try await Product(version: v, type: .library(.automatic), name: "lib")
811811
.save(on: app.db)
812-
try await Search.refresh(on: app.db)
813812

814813
let event = App.ActorIsolated<TestEvent?>(nil)
815814
Current.postPlausibleEvent = { @Sendable _, kind, path, _ in
@@ -897,7 +896,6 @@ class ApiTests: AppTestCase {
897896
try await Product(version: v, type: .library(.automatic), name: "p2")
898897
.save(on: app.db)
899898
}
900-
try await Search.refresh(on: app.db)
901899

902900
do { // MUT
903901
let body: ByteBuffer = .init(string: """

Tests/AppTests/PackageCollectionControllerTests.swift

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,62 @@ class PackageCollectionControllerTests: AppTestCase {
7474
}
7575
}
7676

77+
func test_custom_request() async throws {
78+
try XCTSkipIf(!isRunningInCI && Current.collectionSigningPrivateKey() == nil, "Skip test for local user due to unset COLLECTION_SIGNING_PRIVATE_KEY env variable")
79+
try await withDependencies {
80+
$0.date.now = .t0
81+
} operation: {
82+
let p = try await savePackage(on: app.db, "https://github.com/foo/1")
83+
do {
84+
let v = try Version(id: UUID(),
85+
package: p,
86+
packageName: "P1-main",
87+
reference: .branch("main"),
88+
toolsVersion: "5.0")
89+
try await v.save(on: app.db)
90+
try await Product(version: v, type: .library(.automatic), name: "P1Lib")
91+
.save(on: app.db)
92+
}
93+
do {
94+
let v = try Version(id: UUID(),
95+
package: p,
96+
latest: .release,
97+
packageName: "P1-tag",
98+
reference: .tag(1, 2, 3),
99+
toolsVersion: "5.1")
100+
try await v.save(on: app.db)
101+
try await Product(version: v, type: .library(.automatic), name: "P1Lib", targets: ["t1"])
102+
.save(on: app.db)
103+
try await Build(version: v,
104+
platform: .iOS,
105+
status: .ok,
106+
swiftVersion: .init(5, 6, 0)).save(on: app.db)
107+
try await Target(version: v, name: "t1").save(on: app.db)
108+
}
109+
try await Repository(package: p,
110+
defaultBranch: "main",
111+
license: .mit,
112+
licenseUrl: "https://foo/mit",
113+
owner: "foo",
114+
summary: "summary 1").create(on: app.db)
115+
let collection = CustomCollection(id: .id2, .init(name: "Custom Collection", url: "https://github.com/foo/bar/list.json"))
116+
try await collection.save(on: app.db)
117+
try await collection.$packages.attach(p, on: app.db)
118+
119+
// MUT
120+
let encoder = self.encoder
121+
try await app.test(
122+
.GET,
123+
"collections/Custom%20Collection/collection.json",
124+
afterResponse: { @MainActor res async throws in
125+
// validation
126+
XCTAssertEqual(res.status, .ok)
127+
let json = try res.content.decode(PackageCollection.self)
128+
assertSnapshot(of: json, as: .json(encoder))
129+
})
130+
}
131+
}
132+
77133
func test_nonexisting_404() throws {
78134
// Ensure a request for a non-existing collection returns a 404
79135
// MUT

0 commit comments

Comments
 (0)