Skip to content
Merged
81 changes: 81 additions & 0 deletions Sources/App/Controllers/CustomCollectionsController.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// Copyright Dave Verwer, Sven A. Schmidt, and other contributors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import Fluent
import Plot
import Vapor


enum CustomCollectionsController {

static func query(on database: Database, name: String, page: Int, pageSize: Int) async throws -> Page<Joined3<Package, Repository, Version>> {
try await Joined3<Package, Repository, Version>
.query(on: database, version: .defaultBranch)
.join(CustomCollectionPackage.self, on: \Package.$id == \CustomCollectionPackage.$package.$id)
.join(CustomCollection.self, on: \CustomCollection.$id == \CustomCollectionPackage.$customCollection.$id)
.field(Repository.self, \.$name)
.field(Repository.self, \.$owner)
.field(Repository.self, \.$lastActivityAt)
.field(Repository.self, \.$stars)
.field(Repository.self, \.$summary)
.field(Version.self, \.$packageName)
.filter(CustomCollection.self, \.$name == name)
.sort(Repository.self, \.$name)
.page(page, size: pageSize)
}

struct Query: Codable {
var page: Int
var pageSize: Int

static let defaultPage = 1
static let defaultPageSize = 20

enum CodingKeys: CodingKey {
case page
case pageSize
}

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.page = try container.decodeIfPresent(Int.self, forKey: CodingKeys.page) ?? Self.defaultPage
self.pageSize = try container.decodeIfPresent(Int.self, forKey: CodingKeys.pageSize) ?? Self.defaultPageSize
}
}

@Sendable
static func show(req: Request) async throws -> HTML {
guard let name = req.parameters.get("name") else {
throw Abort(.notFound)
}
let query = try req.query.decode(Query.self)
let page = try await Self.query(on: req.db, name: name, page: query.page, pageSize: query.pageSize)

guard !page.results.isEmpty else {
throw Abort(.notFound)
}

let packageInfo = page.results.compactMap(PackageInfo.init(package:))

let model = CustomCollectionShow.Model(
name: name,
packages: packageInfo,
page: query.page,
hasMoreResults: page.hasMoreResults
)

return CustomCollectionShow.View(path: req.url.path, model: model).document()
}

}
6 changes: 6 additions & 0 deletions Sources/App/Controllers/KeywordController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ enum KeywordController {
static func query(on database: Database, keyword: String, page: Int, pageSize: Int) async throws -> Page<Joined3<Package, Repository, Version>> {
try await Joined3<Package, Repository, Version>
.query(on: database, version: .defaultBranch)
.field(Repository.self, \.$name)
.field(Repository.self, \.$owner)
.field(Repository.self, \.$lastActivityAt)
.field(Repository.self, \.$stars)
.field(Repository.self, \.$summary)
.field(Version.self, \.$packageName)
.filter(Repository.self, \.$keywords, .custom("@>"), [keyword])
.sort(\.$score, .descending)
.sort(Repository.self, \.$name)
Expand Down
36 changes: 30 additions & 6 deletions Sources/App/Controllers/PackageCollectionController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,40 @@ enum PackageCollectionController {
static func generate(req: Request) async throws -> SignedCollection {
AppMetrics.packageCollectionGetTotal?.inc()

guard let owner = req.parameters.get("owner") else { throw Abort(.notFound) }
guard let collectionType = getCollectionType(req: req) else {
throw Abort(.notFound)
}

do {
return try await SignedCollection.generate(
db: req.db,
filterBy: .author(owner),
authorName: "\(owner) via the Swift Package Index"
)
switch collectionType {
case let .author(owner):
return try await SignedCollection.generate(
db: req.db,
filterBy: .author(owner),
authorName: "\(owner) via the Swift Package Index"
)
case let .custom(name):
return try await SignedCollection.generate(
db: req.db,
filterBy: .customCollection(name),
authorName: "Swift Package Index",
collectionName: name,
overview: "A custom package collection generated by the Swift Package Index"
)
}
} catch PackageCollection.Error.noResults {
throw Abort(.notFound)
}
}

enum CollectionType {
case author(String)
case custom(String)
}

static func getCollectionType(req: Request) -> CollectionType? {
if let owner = req.parameters.get("owner") { return .author(owner) }
if let name = req.parameters.get("name") { return .custom(name) }
return nil
}
}
4 changes: 4 additions & 0 deletions Sources/App/Core/Extensions/String+ext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ extension String {
var pathEncoded: Self {
replacingOccurrences(of: "/", with: "-")
}

var urlPathEncoded: Self {
addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? self
}
}


Expand Down
5 changes: 5 additions & 0 deletions Sources/App/Core/PackageCollection+VersionResult.swift
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,11 @@ extension PackageCollection.VersionResult {
switch filter {
case let .author(owner):
query.filter(Repository.self, \Repository.$owner, .custom("ilike"), owner)
case let .customCollection(name):
query
.join(CustomCollectionPackage.self, on: \Package.$id == \CustomCollectionPackage.$package.$id)
.join(CustomCollection.self, on: \CustomCollection.$id == \CustomCollectionPackage.$customCollection.$id)
.filter(CustomCollection.self, \.$name == name)
case let .urls(packageURLs):
query.filter(App.Package.self, \.$url ~~ packageURLs)
}
Expand Down
7 changes: 6 additions & 1 deletion Sources/App/Core/PackageCollection+generate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ extension PackageCollection {
enum Filter {
case urls([String])
case author(String)
case customCollection(String)
}

enum Error: Swift.Error {
Expand Down Expand Up @@ -97,10 +98,14 @@ extension PackageCollection {
return owner
case (.author, .some(let label)):
return label
case (.urls, .some(let label)):
case (.customCollection(let name), .none):
return name
case (.customCollection, .some(let label)):
return label
case (.urls(let urls), .none):
return author(for: urls)
case (.urls, .some(let label)):
return label
}
}

Expand Down
36 changes: 31 additions & 5 deletions Sources/App/Core/SiteURL.swift
Original file line number Diff line number Diff line change
Expand Up @@ -113,14 +113,16 @@ enum SiteURL: Resourceable, Sendable {
case blogPost(_ slug: Parameter<String>)
case buildMonitor
case builds(_ id: Parameter<UUID>)
case collections(_ name: Parameter<String>)
case docs(Docs)
case faq
case home
case images(String)
case javascripts(String)
case keywords(_ keyword: Parameter<String>)
case package(_ owner: Parameter<String>, _ repository: Parameter<String>, PackagePathComponents?)
case packageCollection(_ owner: Parameter<String>)
case packageCollectionAuthor(_ owner: Parameter<String>)
case packageCollectionCustom(_ name: Parameter<String>)
case packageCollections
case privacy
case readyForSwift6
Expand Down Expand Up @@ -169,6 +171,12 @@ enum SiteURL: Resourceable, Sendable {
case .buildMonitor:
return "build-monitor"

case let .collections(.value(name)):
return "collections/\(name.urlPathEncoded)"

case .collections(.key):
fatalError("path must not be called with a name parameter")

case let .docs(next):
return "docs/\(next.path)"

Expand Down Expand Up @@ -201,10 +209,16 @@ enum SiteURL: Resourceable, Sendable {
case .package:
fatalError("invalid path: \(self)")

case let .packageCollection(.value(owner)):
case let .packageCollectionAuthor(.value(owner)):
return "\(owner)/collection.json"

case .packageCollection(.key):
case .packageCollectionAuthor(.key):
fatalError("invalid path: \(self)")

case let .packageCollectionCustom(.value(name)):
return "collections/\(name.urlPathEncoded)/collection.json"

case .packageCollectionCustom(.key):
fatalError("invalid path: \(self)")

case .packageCollections:
Expand Down Expand Up @@ -283,6 +297,12 @@ enum SiteURL: Resourceable, Sendable {
case .builds(.value):
fatalError("pathComponents must not be called with a value parameter")

case .collections(.key):
return ["collections", ":name"]

case .collections(.value):
fatalError("pathComponents must not be called with a value parameter")

case let .docs(next):
return ["docs"] + next.pathComponents

Expand All @@ -298,10 +318,16 @@ enum SiteURL: Resourceable, Sendable {
case .package:
fatalError("pathComponents must not be called with a value parameter")

case .packageCollection(.key):
case .packageCollectionAuthor(.key):
return [":owner", "collection.json"]

case .packageCollection(.value):
case .packageCollectionAuthor(.value):
fatalError("pathComponents must not be called with a value parameter")

case .packageCollectionCustom(.key):
return ["collections", ":name", "collection.json"]

case .packageCollectionCustom(.value):
fatalError("pathComponents must not be called with a value parameter")

case .images, .javascripts, .stylesheets:
Expand Down
2 changes: 1 addition & 1 deletion Sources/App/Views/Author/AuthorShow+View.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ enum AuthorShow {
),
.copyableInputForm(buttonName: "Copy Package Collection URL",
eventName: "Copy Package Collection URL Button",
valueToCopy: SiteURL.packageCollection(.value(model.owner)).absoluteURL()),
valueToCopy: SiteURL.packageCollectionAuthor(.value(model.owner)).absoluteURL()),
.hr(.class("minor")),
.ul(
.id("package-list"),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Copyright Dave Verwer, Sven A. Schmidt, and other contributors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

extension CustomCollectionShow {
struct Model {
var name: String
var packages: [PackageInfo]
var page: Int
var hasMoreResults: Bool

internal init(name: String,
packages: [PackageInfo],
page: Int,
hasMoreResults: Bool) {
self.name = name
self.packages = packages
self.page = page
self.hasMoreResults = hasMoreResults
}
}
}
Loading
Loading