Skip to content

Commit 6e5eb79

Browse files
authored
Add package release info API to registry client (#6169)
- Update additional metadata to match newly proposed schema - Update `PackageSearchClient` to use readmeURL from the latest version, if any
1 parent b741798 commit 6e5eb79

File tree

4 files changed

+283
-17
lines changed

4 files changed

+283
-17
lines changed

Sources/PackageMetadata/PackageMetadata.swift

Lines changed: 56 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//
33
// This source file is part of the Swift open source project
44
//
5-
// Copyright (c) 2022 Apple Inc. and the Swift project authors
5+
// Copyright (c) 2022-2023 Apple Inc. and the Swift project authors
66
// Licensed under Apache License v2.0 with Runtime Library Exception
77
//
88
// See http://swift.org/LICENSE.txt for license information
@@ -70,6 +70,29 @@ public struct PackageSearchClient {
7070
private func guessReadMeURL(baseURL: URL, defaultBranch: String) -> URL {
7171
return baseURL.appendingPathComponent("raw").appendingPathComponent(defaultBranch).appendingPathComponent("README.md")
7272
}
73+
74+
private func guessReadMeURL(alternateLocations: [URL]?) -> URL? {
75+
if let alternateURL = alternateLocations?.first {
76+
// FIXME: This is pretty crude, we should let the registry metadata provide the value instead.
77+
return guessReadMeURL(baseURL: alternateURL, defaultBranch: "main")
78+
}
79+
return nil
80+
}
81+
82+
private func getReadMeURL(
83+
package: PackageIdentity,
84+
version: Version,
85+
callback: @escaping (Result<URL?, Error>) -> Void
86+
) {
87+
self.registryClient.getPackageVersionMetadata(
88+
package: package,
89+
version: version,
90+
observabilityScope: observabilityScope,
91+
callbackQueue: DispatchQueue.sharedConcurrent
92+
) { result in
93+
callback(result.tryMap { metadata in metadata.readmeURL })
94+
}
95+
}
7396

7497
public func findPackages(
7598
_ query: String,
@@ -102,7 +125,10 @@ public struct PackageSearchClient {
102125
}
103126
}
104127

105-
// Interpret the given search term as a URL and fetch the corresponding Git repository to determine the available version tags and branches. If the search term cannot be interpreted as a URL or there are any errors during the process, we fall back to searching the configured index or package collections.
128+
// Interpret the given search term as a URL and fetch the corresponding Git repository to
129+
// determine the available version tags and branches. If the search term cannot be interpreted
130+
// as a URL or there are any errors during the process, we fall back to searching the configured
131+
// index or package collections.
106132
let fetchStandalonePackageByURL = { (error: Error?) -> Void in
107133
guard let url = URL(string: query) else {
108134
return search(error)
@@ -134,23 +160,40 @@ public struct PackageSearchClient {
134160
}
135161
}
136162

137-
// If the given search term can be interpreted as a registry identity, try to get package metadata for it from the configured registry. If there are any errors or the search term does not work as a registry identity, we will fall back on `fetchStandalonePackageByURL`.
163+
// If the given search term can be interpreted as a registry identity, try to get
164+
// package metadata for it from the configured registry. If there are any errors
165+
// or the search term does not work as a registry identity, we will fall back on
166+
// `fetchStandalonePackageByURL`.
138167
if isRegistryIdentity {
139168
return self.registryClient.getPackageMetadata(package: identity, observabilityScope: observabilityScope, callbackQueue: DispatchQueue.sharedConcurrent) { result in
140169
do {
141170
let metadata = try result.get()
142-
let readmeURL: URL?
143-
if let alternateURL = metadata.alternateLocations?.first {
144-
// FIXME: This is pretty crude, we should let the registry metadata provide the value instead.
145-
readmeURL = guessReadMeURL(baseURL: alternateURL, defaultBranch: "main")
171+
let versions = metadata.versions.sorted(by: >)
172+
173+
// See if the latest package version has readmeURL set
174+
if let version = versions.first {
175+
self.getReadMeURL(package: identity, version: version) { result in
176+
let readmeURL: URL?
177+
if case .success(.some(let readmeURLForVersion)) = result {
178+
readmeURL = readmeURLForVersion
179+
} else {
180+
readmeURL = self.guessReadMeURL(alternateLocations: metadata.alternateLocations)
181+
}
182+
183+
return callback(.success([Package(identity: identity,
184+
versions: metadata.versions,
185+
readmeURL: readmeURL,
186+
source: .registry(url: metadata.registry.url)
187+
)]))
188+
}
146189
} else {
147-
readmeURL = nil
190+
let readmeURL: URL? = self.guessReadMeURL(alternateLocations: metadata.alternateLocations)
191+
return callback(.success([Package(identity: identity,
192+
versions: metadata.versions,
193+
readmeURL: readmeURL,
194+
source: .registry(url: metadata.registry.url)
195+
)]))
148196
}
149-
return callback(.success([Package(identity: identity,
150-
versions: metadata.versions,
151-
readmeURL: readmeURL,
152-
source: .registry(url: metadata.registry.url)
153-
)]))
154197
} catch {
155198
return fetchStandalonePackageByURL(error)
156199
}

Sources/PackageRegistry/RegistryClient.swift

Lines changed: 103 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,68 @@ public final class RegistryClient: Cancellable {
147147
)
148148
}
149149
}
150+
151+
public func getPackageVersionMetadata(
152+
package: PackageIdentity,
153+
version: Version,
154+
timeout: DispatchTimeInterval? = .none,
155+
observabilityScope: ObservabilityScope,
156+
callbackQueue: DispatchQueue,
157+
completion: @escaping (Result<PackageVersionMetadata, Error>) -> Void
158+
) {
159+
let completion = self.makeAsync(completion, on: callbackQueue)
160+
161+
guard case (let scope, let name)? = package.scopeAndName else {
162+
return completion(.failure(RegistryError.invalidPackage(package)))
163+
}
164+
165+
guard let registry = configuration.registry(for: scope) else {
166+
return completion(.failure(RegistryError.registryNotConfigured(scope: scope)))
167+
}
168+
169+
guard var components = URLComponents(url: registry.url, resolvingAgainstBaseURL: true) else {
170+
return completion(.failure(RegistryError.invalidURL(registry.url)))
171+
}
172+
components.appendPathComponents("\(scope)", "\(name)", "\(version)")
173+
174+
guard let url = components.url else {
175+
return completion(.failure(RegistryError.invalidURL(registry.url)))
176+
}
177+
178+
let request = LegacyHTTPClient.Request(
179+
method: .get,
180+
url: url,
181+
headers: [
182+
"Accept": self.acceptHeader(mediaType: .json),
183+
],
184+
options: self.defaultRequestOptions(timeout: timeout, callbackQueue: callbackQueue)
185+
)
186+
187+
self.httpClient.execute(request, observabilityScope: observabilityScope, progress: nil) { result in
188+
completion(
189+
result.tryMap { response in
190+
switch response.statusCode {
191+
case 200:
192+
let versionMetadata = try response.parseJSON(
193+
Serialization.VersionMetadata.self,
194+
decoder: self.jsonDecoder
195+
)
196+
197+
return PackageVersionMetadata(
198+
registry: registry,
199+
licenseURL: versionMetadata.metadata?.licenseURL.flatMap { URL(string: $0) },
200+
readmeURL: versionMetadata.metadata?.readmeURL.flatMap { URL(string: $0) },
201+
repositoryURLs: versionMetadata.metadata?.repositoryURLs?.compactMap { URL(string: $0) }
202+
)
203+
default:
204+
throw self.unexpectedStatusError(response, expectedStatus: [200])
205+
}
206+
}.mapError {
207+
RegistryError.failedRetrievingReleaseInfo($0)
208+
}
209+
)
210+
}
211+
}
150212

151213
public func getAvailableManifests(
152214
package: PackageIdentity,
@@ -816,6 +878,7 @@ public enum RegistryError: Error, CustomStringConvertible {
816878
case invalidChecksum(expected: String, actual: String)
817879
case pathAlreadyExists(AbsolutePath)
818880
case failedRetrievingReleases(Error)
881+
case failedRetrievingReleaseInfo(Error)
819882
case failedRetrievingReleaseChecksum(Error)
820883
case failedRetrievingManifest(Error)
821884
case failedDownloadingSourceArchive(Error)
@@ -868,6 +931,8 @@ public enum RegistryError: Error, CustomStringConvertible {
868931
return "Path already exists '\(path)'"
869932
case .failedRetrievingReleases(let error):
870933
return "Failed fetching releases from registry: \(error)"
934+
case .failedRetrievingReleaseInfo(let error):
935+
return "Failed fetching release information from registry: \(error)"
871936
case .failedRetrievingReleaseChecksum(let error):
872937
return "Failed fetching release checksum from registry: \(error)"
873938
case .failedRetrievingManifest(let error):
@@ -929,6 +994,13 @@ extension RegistryClient {
929994
public let versions: [Version]
930995
public let alternateLocations: [URL]?
931996
}
997+
998+
public struct PackageVersionMetadata {
999+
public let registry: Registry
1000+
public let licenseURL: URL?
1001+
public let readmeURL: URL?
1002+
public let repositoryURLs: [URL]?
1003+
}
9321004
}
9331005

9341006
extension RegistryClient {
@@ -1237,12 +1309,41 @@ extension RegistryClient {
12371309
}
12381310

12391311
public struct AdditionalMetadata: Codable {
1312+
public let author: Author?
12401313
public let description: String?
1241-
1242-
public init(description: String) {
1314+
public let licenseURL: String?
1315+
public let readmeURL: String?
1316+
public let repositoryURLs: [String]?
1317+
1318+
public init(
1319+
author: Author? = nil,
1320+
description: String,
1321+
licenseURL: String? = nil,
1322+
readmeURL: String? = nil,
1323+
repositoryURLs: [String]? = nil
1324+
) {
1325+
self.author = author
12431326
self.description = description
1327+
self.licenseURL = licenseURL
1328+
self.readmeURL = readmeURL
1329+
self.repositoryURLs = repositoryURLs
12441330
}
12451331
}
1332+
1333+
public struct Author: Codable {
1334+
public let name: String
1335+
public let email: String?
1336+
public let description: String?
1337+
public let organization: Organization?
1338+
public let url: String?
1339+
}
1340+
1341+
public struct Organization: Codable {
1342+
public let name: String
1343+
public let email: String?
1344+
public let description: String?
1345+
public let url: String?
1346+
}
12461347
}
12471348

12481349
// marked public for cross module visibility

Sources/SPMTestSupport/MockRegistry.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//
33
// This source file is part of the Swift open source project
44
//
5-
// Copyright (c) 2021 Apple Inc. and the Swift project authors
5+
// Copyright (c) 2021-2023 Apple Inc. and the Swift project authors
66
// Licensed under Apache License v2.0 with Runtime Library Exception
77
//
88
// See http://swift.org/LICENSE.txt for license information
@@ -218,7 +218,10 @@ public class MockRegistry {
218218
checksum: zipfileChecksum.hexadecimalRepresentation
219219
),
220220
],
221-
metadata: .init(description: "\(packageIdentity) description")
221+
metadata: .init(
222+
description: "\(packageIdentity) description",
223+
readmeURL: "http://\(packageIdentity)/readme"
224+
)
222225
)
223226

224227
var headers = HTTPClientHeaders()

0 commit comments

Comments
 (0)