Skip to content

Add package release info API to registry client #6169

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 56 additions & 13 deletions Sources/PackageMetadata/PackageMetadata.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//
// This source file is part of the Swift open source project
//
// Copyright (c) 2022 Apple Inc. and the Swift project authors
// Copyright (c) 2022-2023 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See http://swift.org/LICENSE.txt for license information
Expand Down Expand Up @@ -70,6 +70,29 @@ public struct PackageSearchClient {
private func guessReadMeURL(baseURL: URL, defaultBranch: String) -> URL {
return baseURL.appendingPathComponent("raw").appendingPathComponent(defaultBranch).appendingPathComponent("README.md")
}

private func guessReadMeURL(alternateLocations: [URL]?) -> URL? {
if let alternateURL = alternateLocations?.first {
// FIXME: This is pretty crude, we should let the registry metadata provide the value instead.
return guessReadMeURL(baseURL: alternateURL, defaultBranch: "main")
}
return nil
}

private func getReadMeURL(
package: PackageIdentity,
version: Version,
callback: @escaping (Result<URL?, Error>) -> Void
) {
self.registryClient.getPackageVersionMetadata(
package: package,
version: version,
observabilityScope: observabilityScope,
callbackQueue: DispatchQueue.sharedConcurrent
) { result in
callback(result.tryMap { metadata in metadata.readmeURL })
}
}

public func findPackages(
_ query: String,
Expand Down Expand Up @@ -102,7 +125,10 @@ public struct PackageSearchClient {
}
}

// 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.
// 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.
let fetchStandalonePackageByURL = { (error: Error?) -> Void in
guard let url = URL(string: query) else {
return search(error)
Expand Down Expand Up @@ -134,23 +160,40 @@ public struct PackageSearchClient {
}
}

// 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`.
// 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`.
if isRegistryIdentity {
return self.registryClient.getPackageMetadata(package: identity, observabilityScope: observabilityScope, callbackQueue: DispatchQueue.sharedConcurrent) { result in
do {
let metadata = try result.get()
let readmeURL: URL?
if let alternateURL = metadata.alternateLocations?.first {
// FIXME: This is pretty crude, we should let the registry metadata provide the value instead.
readmeURL = guessReadMeURL(baseURL: alternateURL, defaultBranch: "main")
let versions = metadata.versions.sorted(by: >)

// See if the latest package version has readmeURL set
if let version = versions.first {
self.getReadMeURL(package: identity, version: version) { result in
let readmeURL: URL?
if case .success(.some(let readmeURLForVersion)) = result {
readmeURL = readmeURLForVersion
} else {
readmeURL = self.guessReadMeURL(alternateLocations: metadata.alternateLocations)
}

return callback(.success([Package(identity: identity,
versions: metadata.versions,
readmeURL: readmeURL,
source: .registry(url: metadata.registry.url)
)]))
}
} else {
readmeURL = nil
let readmeURL: URL? = self.guessReadMeURL(alternateLocations: metadata.alternateLocations)
return callback(.success([Package(identity: identity,
versions: metadata.versions,
readmeURL: readmeURL,
source: .registry(url: metadata.registry.url)
)]))
}
return callback(.success([Package(identity: identity,
versions: metadata.versions,
readmeURL: readmeURL,
source: .registry(url: metadata.registry.url)
)]))
} catch {
return fetchStandalonePackageByURL(error)
}
Expand Down
105 changes: 103 additions & 2 deletions Sources/PackageRegistry/RegistryClient.swift
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,68 @@ public final class RegistryClient: Cancellable {
)
}
}

public func getPackageVersionMetadata(
package: PackageIdentity,
version: Version,
timeout: DispatchTimeInterval? = .none,
observabilityScope: ObservabilityScope,
callbackQueue: DispatchQueue,
completion: @escaping (Result<PackageVersionMetadata, Error>) -> Void
) {
let completion = self.makeAsync(completion, on: callbackQueue)

guard case (let scope, let name)? = package.scopeAndName else {
return completion(.failure(RegistryError.invalidPackage(package)))
}

guard let registry = configuration.registry(for: scope) else {
return completion(.failure(RegistryError.registryNotConfigured(scope: scope)))
}

guard var components = URLComponents(url: registry.url, resolvingAgainstBaseURL: true) else {
return completion(.failure(RegistryError.invalidURL(registry.url)))
}
components.appendPathComponents("\(scope)", "\(name)", "\(version)")

guard let url = components.url else {
return completion(.failure(RegistryError.invalidURL(registry.url)))
}

let request = LegacyHTTPClient.Request(
method: .get,
url: url,
headers: [
"Accept": self.acceptHeader(mediaType: .json),
],
options: self.defaultRequestOptions(timeout: timeout, callbackQueue: callbackQueue)
)

self.httpClient.execute(request, observabilityScope: observabilityScope, progress: nil) { result in
completion(
result.tryMap { response in
switch response.statusCode {
case 200:
let versionMetadata = try response.parseJSON(
Serialization.VersionMetadata.self,
decoder: self.jsonDecoder
)

return PackageVersionMetadata(
registry: registry,
licenseURL: versionMetadata.metadata?.licenseURL.flatMap { URL(string: $0) },
readmeURL: versionMetadata.metadata?.readmeURL.flatMap { URL(string: $0) },
repositoryURLs: versionMetadata.metadata?.repositoryURLs?.compactMap { URL(string: $0) }
)
default:
throw self.unexpectedStatusError(response, expectedStatus: [200])
}
}.mapError {
RegistryError.failedRetrievingReleaseInfo($0)
}
)
}
}

public func getAvailableManifests(
package: PackageIdentity,
Expand Down Expand Up @@ -816,6 +878,7 @@ public enum RegistryError: Error, CustomStringConvertible {
case invalidChecksum(expected: String, actual: String)
case pathAlreadyExists(AbsolutePath)
case failedRetrievingReleases(Error)
case failedRetrievingReleaseInfo(Error)
case failedRetrievingReleaseChecksum(Error)
case failedRetrievingManifest(Error)
case failedDownloadingSourceArchive(Error)
Expand Down Expand Up @@ -868,6 +931,8 @@ public enum RegistryError: Error, CustomStringConvertible {
return "Path already exists '\(path)'"
case .failedRetrievingReleases(let error):
return "Failed fetching releases from registry: \(error)"
case .failedRetrievingReleaseInfo(let error):
return "Failed fetching release information from registry: \(error)"
case .failedRetrievingReleaseChecksum(let error):
return "Failed fetching release checksum from registry: \(error)"
case .failedRetrievingManifest(let error):
Expand Down Expand Up @@ -929,6 +994,13 @@ extension RegistryClient {
public let versions: [Version]
public let alternateLocations: [URL]?
}

public struct PackageVersionMetadata {
public let registry: Registry
public let licenseURL: URL?
public let readmeURL: URL?
public let repositoryURLs: [URL]?
}
}

extension RegistryClient {
Expand Down Expand Up @@ -1237,12 +1309,41 @@ extension RegistryClient {
}

public struct AdditionalMetadata: Codable {
public let author: Author?
public let description: String?

public init(description: String) {
public let licenseURL: String?
public let readmeURL: String?
public let repositoryURLs: [String]?

public init(
author: Author? = nil,
description: String,
licenseURL: String? = nil,
readmeURL: String? = nil,
repositoryURLs: [String]? = nil
) {
self.author = author
self.description = description
self.licenseURL = licenseURL
self.readmeURL = readmeURL
self.repositoryURLs = repositoryURLs
}
}

public struct Author: Codable {
public let name: String
public let email: String?
public let description: String?
public let organization: Organization?
public let url: String?
}

public struct Organization: Codable {
public let name: String
public let email: String?
public let description: String?
public let url: String?
}
}

// marked public for cross module visibility
Expand Down
7 changes: 5 additions & 2 deletions Sources/SPMTestSupport/MockRegistry.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//
// This source file is part of the Swift open source project
//
// Copyright (c) 2021 Apple Inc. and the Swift project authors
// Copyright (c) 2021-2023 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See http://swift.org/LICENSE.txt for license information
Expand Down Expand Up @@ -218,7 +218,10 @@ public class MockRegistry {
checksum: zipfileChecksum.hexadecimalRepresentation
),
],
metadata: .init(description: "\(packageIdentity) description")
metadata: .init(
description: "\(packageIdentity) description",
readmeURL: "http://\(packageIdentity)/readme"
)
)

var headers = HTTPClientHeaders()
Expand Down
Loading