Skip to content

Commit 7801f56

Browse files
authored
fetch package metadata (#3041)
motivation: implement fetch package metadata API changes: * add PackageMetadataProvider API * add business logic to fetch metadata from collection sotre + provider and merge it * add tests open issue: implement more efficient storage for packages
1 parent e43fa3c commit 7801f56

File tree

8 files changed

+534
-84
lines changed

8 files changed

+534
-84
lines changed

Sources/PackageCollections/Model/CVE.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import struct Foundation.URL
1212

1313
extension PackageCollectionsModel {
1414
/// A representation of Common Vulnerabilities and Exposures (CVE)
15-
public struct CVE {
15+
public struct CVE: Equatable {
1616
/// CVE identifier
1717
public let identifier: String
1818

Sources/PackageCollections/Model/Package.swift

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import SourceControl
1515

1616
extension PackageCollectionsModel {
1717
/// Package metadata
18-
public struct Package {
18+
public struct Package: Equatable {
1919
/// Package reference
2020
public let reference: PackageReference
2121

@@ -87,7 +87,7 @@ extension PackageCollectionsModel {
8787

8888
extension PackageCollectionsModel.Package {
8989
/// A representation of package version
90-
public struct Version {
90+
public struct Version: Equatable {
9191
public typealias Target = PackageCollectionsModel.PackageTarget
9292
public typealias Product = PackageCollectionsModel.PackageProduct
9393

@@ -114,9 +114,6 @@ extension PackageCollectionsModel.Package {
114114
/// The package version's Swift versions verified to work
115115
public let verifiedSwiftVersions: [SwiftLanguageVersion]?
116116

117-
/// The package version's CVEs
118-
public let cves: [PackageCollectionsModel.CVE]?
119-
120117
/// The package version's license
121118
public let license: PackageCollectionsModel.License?
122119
}
@@ -149,7 +146,7 @@ extension PackageCollectionsModel {
149146

150147
extension PackageCollectionsModel.Package {
151148
/// A representation of package author
152-
public struct Author {
149+
public struct Author: Equatable, Codable {
153150
/// Author's username
154151
public let username: String
155152

@@ -160,7 +157,7 @@ extension PackageCollectionsModel.Package {
160157
public let service: Service?
161158

162159
/// A representation of user service
163-
public struct Service {
160+
public struct Service: Equatable, Codable {
164161
/// The service name
165162
public let name: String
166163
}

Sources/PackageCollections/Model/Search.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ extension PackageCollectionsModel {
1818
public struct Item {
1919
// Merged package metadata from across collections
2020
/// The matching package
21-
public let package: PackageCollectionsModel.Package
21+
public let package: PackageCollectionsModel.Collection.Package
2222

2323
/// Package collections that contain the package
2424
public let collections: [PackageCollectionsModel.CollectionIdentifier]

Sources/PackageCollections/PackageCollections.swift

Lines changed: 125 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,27 @@ import TSCBasic
1515
public struct PackageCollections: PackageCollectionsProtocol {
1616
private let configuration: Configuration
1717
private let storage: Storage
18-
private let providers: [PackageCollectionsModel.CollectionSourceType: PackageCollectionProvider]
18+
private let collectionProviders: [PackageCollectionsModel.CollectionSourceType: PackageCollectionProvider]
19+
private let metadataProvider: PackageMetadataProvider
1920

20-
init(configuration: Configuration, storage: Storage, providers: [PackageCollectionsModel.CollectionSourceType: PackageCollectionProvider]) {
21+
init(configuration: Configuration,
22+
storage: Storage,
23+
collectionProviders: [PackageCollectionsModel.CollectionSourceType: PackageCollectionProvider],
24+
metadataProvider: PackageMetadataProvider) {
2125
self.configuration = configuration
2226
self.storage = storage
23-
self.providers = providers
27+
self.collectionProviders = collectionProviders
28+
self.metadataProvider = metadataProvider
2429
}
2530

31+
// MARK: - Profiles
32+
2633
public func listProfiles(callback: @escaping (Result<[PackageCollectionsModel.Profile], Error>) -> Void) {
2734
self.storage.collectionsProfiles.listProfiles(callback: callback)
2835
}
2936

37+
// MARK: - Collections
38+
3039
public func listCollections(identifiers: Set<PackageCollectionsModel.CollectionIdentifier>? = nil,
3140
in profile: PackageCollectionsModel.Profile? = nil,
3241
callback: @escaping (Result<[PackageCollectionsModel.Collection], Error>) -> Void) {
@@ -60,28 +69,6 @@ public struct PackageCollections: PackageCollectionsProtocol {
6069
}
6170
}
6271

63-
public func findPackages(
64-
_ query: String,
65-
collections: Set<PackageCollectionsModel.CollectionIdentifier>? = nil,
66-
profile: PackageCollectionsModel.Profile? = nil,
67-
callback: @escaping (Result<PackageCollectionsModel.PackageSearchResult, Error>) -> Void
68-
) {
69-
let profile = profile ?? .default
70-
71-
self.storage.collectionsProfiles.listSources(in: profile) { result in
72-
switch result {
73-
case .failure(let error):
74-
callback(.failure(error))
75-
case .success(let sources):
76-
let identifiers = sources.map { .init(from: $0) }.filter { collections?.contains($0) ?? true }
77-
if identifiers.isEmpty {
78-
return callback(.success(PackageCollectionsModel.PackageSearchResult(items: [])))
79-
}
80-
self.storage.collections.searchPackages(identifiers: identifiers, query: query, callback: callback)
81-
}
82-
}
83-
}
84-
8572
public func refreshCollections(in profile: PackageCollectionsModel.Profile? = nil,
8673
callback: @escaping (Result<[PackageCollectionsModel.CollectionSource], Error>) -> Void) {
8774
let profile = profile ?? .default
@@ -179,7 +166,7 @@ public struct PackageCollections: PackageCollectionsProtocol {
179166
self.storage.collections.get(identifier: .init(from: source)) { result in
180167
switch result {
181168
case .failure:
182-
guard let provider = self.providers[source.type] else {
169+
guard let provider = self.collectionProviders[source.type] else {
183170
return callback(.failure(UnknownProvider(source.type)))
184171
}
185172
provider.get(source, callback: callback)
@@ -189,6 +176,63 @@ public struct PackageCollections: PackageCollectionsProtocol {
189176
}
190177
}
191178

179+
// MARK: - Packages
180+
181+
public func findPackages(
182+
_ query: String,
183+
collections: Set<PackageCollectionsModel.CollectionIdentifier>? = nil,
184+
profile: PackageCollectionsModel.Profile? = nil,
185+
callback: @escaping (Result<PackageCollectionsModel.PackageSearchResult, Error>) -> Void
186+
) {
187+
let profile = profile ?? .default
188+
189+
self.storage.collectionsProfiles.listSources(in: profile) { result in
190+
switch result {
191+
case .failure(let error):
192+
callback(.failure(error))
193+
case .success(let sources):
194+
let identifiers = sources.map { .init(from: $0) }.filter { collections?.contains($0) ?? true }
195+
if identifiers.isEmpty {
196+
return callback(.success(PackageCollectionsModel.PackageSearchResult(items: [])))
197+
}
198+
self.storage.collections.searchPackages(identifiers: identifiers, query: query, callback: callback)
199+
}
200+
}
201+
}
202+
203+
// MARK: - Package Metadata
204+
205+
public func getPackageMetadata(_ reference: PackageReference,
206+
profile: PackageCollectionsModel.Profile? = nil,
207+
callback: @escaping (Result<PackageCollectionsModel.PackageMetadata, Error>) -> Void) {
208+
let profile = profile ?? .default
209+
210+
// first find in storage
211+
self.findPackage(identifier: reference.identity, profile: profile) { result in
212+
switch result {
213+
case .failure(let error):
214+
callback(.failure(error))
215+
case .success(let packageSearchResult):
216+
// then try to get more metadata from provider (optional)
217+
self.metadataProvider.get(reference: reference) { result in
218+
switch result {
219+
case .failure(let error):
220+
callback(.failure(error))
221+
case .success(let basicMetadata):
222+
// finally merge the results
223+
let metadata = PackageCollectionsModel.PackageMetadata(
224+
package: Self.mergedPackageMetadata(package: packageSearchResult.package, basicMetadata: basicMetadata),
225+
collections: packageSearchResult.collections
226+
)
227+
callback(.success(metadata))
228+
}
229+
}
230+
}
231+
}
232+
}
233+
234+
// MARK: - Targets
235+
192236
public func listTargets(
193237
collections: Set<PackageCollectionsModel.CollectionIdentifier>? = nil,
194238
in profile: PackageCollectionsModel.Profile? = nil,
@@ -229,12 +273,7 @@ public struct PackageCollections: PackageCollectionsProtocol {
229273
}
230274
}
231275

232-
// FIXME:
233-
public func getPackageMetadata(_ reference: PackageReference,
234-
profile: PackageCollectionsModel.Profile? = nil,
235-
callback: @escaping (Result<PackageCollectionsModel.PackageMetadata, Error>) -> Void) {
236-
fatalError("not implemented")
237-
}
276+
// MARK: - Private
238277

239278
// Fetch the collection from the network and store it in local storage
240279
// This helps avoid network access in normal operations
@@ -245,7 +284,7 @@ public struct PackageCollections: PackageCollectionsProtocol {
245284
if let errors = source.validate() {
246285
return callback(.failure(MultipleErrors(errors)))
247286
}
248-
guard let provider = self.providers[source.type] else {
287+
guard let provider = self.collectionProviders[source.type] else {
249288
return callback(.failure(UnknownProvider(source.type)))
250289
}
251290
provider.get(source) { result in
@@ -258,6 +297,27 @@ public struct PackageCollections: PackageCollectionsProtocol {
258297
}
259298
}
260299

300+
func findPackage(
301+
identifier: PackageReference.PackageIdentity,
302+
profile: PackageCollectionsModel.Profile? = nil,
303+
callback: @escaping (Result<PackageCollectionsModel.PackageSearchResult.Item, Error>) -> Void
304+
) {
305+
let profile = profile ?? .default
306+
307+
self.storage.collectionsProfiles.listSources(in: profile) { result in
308+
switch result {
309+
case .failure(let error):
310+
callback(.failure(error))
311+
case .success(let sources):
312+
let identifiers = sources.map { PackageCollectionsModel.CollectionIdentifier(from: $0) }
313+
if identifiers.isEmpty {
314+
return callback(.failure(NotFoundError("\(identifier)")))
315+
}
316+
self.storage.collections.findPackage(identifier: identifier, collectionIdentifiers: identifiers, callback: callback)
317+
}
318+
}
319+
}
320+
261321
private func targetListResultFromCollections(_ collections: [PackageCollectionsModel.Collection]) -> PackageCollectionsModel.TargetListResult {
262322
var packageCollections = [PackageReference: (package: PackageCollectionsModel.Collection.Package, collections: Set<PackageCollectionsModel.CollectionIdentifier>)]()
263323
var targetsPackages = [String: (target: PackageCollectionsModel.PackageTarget, packages: Set<PackageReference>)]()
@@ -285,15 +345,43 @@ public struct PackageCollections: PackageCollectionsProtocol {
285345
.compactMap { packageCollections[$0] }
286346
.map { pair -> PackageCollectionsModel.TargetListResult.Package in
287347
let versions = pair.package.versions.map { PackageCollectionsModel.TargetListResult.PackageVersion(version: $0.version, packageName: $0.packageName) }
288-
return PackageCollectionsModel.TargetListResult.Package(repository: pair.package.repository,
289-
description: pair.package.summary,
290-
versions: versions,
291-
collections: Array(pair.collections))
348+
return .init(repository: pair.package.repository,
349+
description: pair.package.summary,
350+
versions: versions,
351+
collections: Array(pair.collections))
292352
}
293353

294354
return PackageCollectionsModel.TargetListItem(target: pair.target, packages: targetPackages)
295355
}
296356
}
357+
358+
internal static func mergedPackageMetadata(package: PackageCollectionsModel.Collection.Package,
359+
basicMetadata: PackageCollectionsModel.PackageBasicMetadata?) -> PackageCollectionsModel.Package {
360+
var versions = package.versions.map { packageVersion -> PackageCollectionsModel.Package.Version in
361+
.init(version: packageVersion.version,
362+
packageName: packageVersion.packageName,
363+
targets: packageVersion.targets,
364+
products: packageVersion.products,
365+
toolsVersion: packageVersion.toolsVersion,
366+
verifiedPlatforms: packageVersion.verifiedPlatforms,
367+
verifiedSwiftVersions: packageVersion.verifiedSwiftVersions,
368+
license: packageVersion.license)
369+
}
370+
371+
// uses TSCUtility.Version comparator
372+
versions.sort(by: { lhs, rhs in lhs.version > rhs.version })
373+
let latestVersion = versions.first
374+
375+
return .init(
376+
repository: package.repository,
377+
description: basicMetadata?.description ?? package.summary,
378+
versions: versions,
379+
latestVersion: latestVersion,
380+
watchersCount: basicMetadata?.watchersCount,
381+
readmeURL: basicMetadata?.readmeURL ?? package.readmeURL,
382+
authors: basicMetadata?.authors
383+
)
384+
}
297385
}
298386

299387
private struct UnknownProvider: Error {
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
This source file is part of the Swift.org open source project
3+
4+
Copyright (c) 2020 Apple Inc. and the Swift project authors
5+
Licensed under Apache License v2.0 with Runtime Library Exception
6+
7+
See http://swift.org/LICENSE.txt for license information
8+
See http://swift.org/CONTRIBUTORS.txt for Swift project authors
9+
*/
10+
11+
import struct Foundation.Date
12+
import struct Foundation.URL
13+
import PackageModel
14+
import TSCUtility
15+
16+
/// `PackageBasicMetadata` provider
17+
protocol PackageMetadataProvider {
18+
/// Retrieves metadata for a package at the given repository address.
19+
///
20+
/// - Parameters:
21+
/// - reference: The package's reference
22+
/// - callback: The closure to invoke when result becomes available
23+
func get(reference: PackageReference, callback: @escaping (Result<PackageCollectionsModel.PackageBasicMetadata?, Error>) -> Void)
24+
}
25+
26+
extension PackageCollectionsModel {
27+
struct PackageBasicMetadata: Equatable {
28+
let description: String?
29+
let versions: [TSCUtility.Version]
30+
let watchersCount: Int?
31+
let readmeURL: Foundation.URL?
32+
let authors: [PackageCollectionsModel.Package.Author]?
33+
let processedAt: Date
34+
}
35+
}

Sources/PackageCollections/Storage/PackageCollectionsStorage.swift

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import struct Foundation.Data
1414
import class Foundation.JSONDecoder
1515
import class Foundation.JSONEncoder
1616
import struct Foundation.URL
17+
import PackageModel
1718
import TSCBasic
1819
import TSCUtility
1920

@@ -62,6 +63,16 @@ public protocol PackageCollectionsStorage {
6263
query: String,
6364
callback: @escaping (Result<PackageCollectionsModel.PackageSearchResult, Error>) -> Void)
6465

66+
/// Returns optional `PackageSearchResult.Item` for the given package identity.
67+
///
68+
/// - Parameters:
69+
/// - identifier: The package identifier
70+
/// - collectionIdentifiers: Optional. The identifiers of the `PackageCollection`s
71+
/// - callback: The closure to invoke when result becomes available
72+
func findPackage(identifier: PackageReference.PackageIdentity,
73+
collectionIdentifiers: [PackageCollectionsModel.CollectionIdentifier]?,
74+
callback: @escaping (Result<PackageCollectionsModel.PackageSearchResult.Item, Error>) -> Void)
75+
6576
/// Returns `TargetSearchResult` for the given search criteria.
6677
///
6778
/// - Parameters:
@@ -233,6 +244,31 @@ final class SQLitePackageCollectionsStorage: PackageCollectionsStorage, Closable
233244
fatalError("not implemented")
234245
}
235246

247+
// FIXME: this is PoC for search, need a more performant version of this
248+
func findPackage(identifier: PackageReference.PackageIdentity,
249+
collectionIdentifiers: [PackageCollectionsModel.CollectionIdentifier]?,
250+
callback: @escaping (Result<PackageCollectionsModel.PackageSearchResult.Item, Error>) -> Void) {
251+
self.list(identifiers: collectionIdentifiers) { result in
252+
switch result {
253+
case .failure(let error):
254+
return callback(.failure(error))
255+
case .success(let collections):
256+
// sorting by collection processing date so the latest metadata is first
257+
let collectionPackages = collections.sorted(by: { lhs, rhs in lhs.lastProcessedAt > rhs.lastProcessedAt }).compactMap { collection in
258+
collection.packages
259+
.first(where: { $0.reference.identity == identifier })
260+
.flatMap { (collection: collection.identifier, package: $0) }
261+
}
262+
// first package should have latest processing date
263+
guard let package = collectionPackages.first?.package else {
264+
return callback(.failure(NotFoundError("\(identifier)")))
265+
}
266+
let collections = collectionPackages.map { $0.collection }
267+
callback(.success(.init(package: package, collections: collections)))
268+
}
269+
}
270+
}
271+
236272
// FIXME: implement this
237273
func searchTargets(identifiers: [PackageCollectionsModel.CollectionIdentifier]? = nil,
238274
query: String,

0 commit comments

Comments
 (0)