Skip to content

Commit 8797eb4

Browse files
Merge pull request #3575 from SwiftPackageIndex/issue-3469-dependency-transition-14
Issue 3469 dependency transition 14
2 parents 0e391d7 + d1f9586 commit 8797eb4

14 files changed

+254
-204
lines changed

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import PackageDescription
1919
let package = Package(
2020
name: "SPI-Server",
2121
platforms: [
22-
.macOS(.v13)
22+
.macOS(.v15)
2323
],
2424
products: [
2525
.executable(name: "Run", targets: ["Run"]),

Sources/App/Commands/Ingestion.swift

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ enum Ingestion {
171171
// Even though we have a `Joined<Package, Repository>` as a parameter, we must not rely
172172
// on `repository` for owner/name as it will be nil when a package is first ingested.
173173
// The only way to get `owner` and `repository` here is by parsing them from the URL.
174-
let (owner, repository) = try await run {
174+
let (owner, repository) = try run {
175175
if environment.shouldFail(failureMode: .invalidURL) {
176176
throw Github.Error.invalidURL(package.model.url)
177177
}
@@ -258,13 +258,18 @@ enum Ingestion {
258258

259259
static func fetchMetadata(client: Client, package: Package, owner: String, repository: String) async throws(Github.Error) -> (Github.Metadata, Github.License?, Github.Readme?) {
260260
@Dependency(\.environment) var environment
261-
@Dependency(\.github) var github
262261
if environment.shouldFail(failureMode: .fetchMetadataFailed) {
263262
throw Github.Error.requestFailed(.internalServerError)
264263
}
265264

266-
async let metadata = try await Current.fetchMetadata(client, owner, repository)
267-
async let license = await github.fetchLicense(owner, repository)
265+
// Need to pull in github functions individually, because otherwise the `async let` will trigger a
266+
// concurrency error if github gets used more than once:
267+
// Sending 'github' into async let risks causing data races between async let uses and local uses
268+
@Dependency(\.github.fetchMetadata) var fetchMetadata
269+
@Dependency(\.github.fetchLicense) var fetchLicense
270+
271+
async let metadata = try await fetchMetadata(owner, repository)
272+
async let license = await fetchLicense(owner, repository)
268273
async let readme = await Current.fetchReadme(client, owner, repository)
269274

270275
do {

Sources/App/Core/AppEnvironment.swift

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ import FoundationNetworking
2323

2424

2525
struct AppEnvironment: Sendable {
26-
var fetchMetadata: @Sendable (_ client: Client, _ owner: String, _ repository: String) async throws(Github.Error) -> Github.Metadata
2726
var fetchReadme: @Sendable (_ client: Client, _ owner: String, _ repository: String) async -> Github.Readme?
2827
var fetchS3Readme: @Sendable (_ client: Client, _ owner: String, _ repository: String) async throws -> String
2928
var fileManager: FileManager
@@ -65,7 +64,6 @@ extension AppEnvironment {
6564
nonisolated(unsafe) static var logger: Logger!
6665

6766
static let live = AppEnvironment(
68-
fetchMetadata: { client, owner, repo throws(Github.Error) in try await Github.fetchMetadata(client:client, owner: owner, repository: repo) },
6967
fetchReadme: { client, owner, repo in await Github.fetchReadme(client:client, owner: owner, repository: repo) },
7068
fetchS3Readme: { client, owner, repo in try await S3Readme.fetchReadme(client:client, owner: owner, repository: repo) },
7169
fileManager: .live,

Sources/App/Core/Dependencies/GithubClient.swift

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,26 +14,35 @@
1414

1515

1616
import Dependencies
17-
import DependenciesMacros
17+
import IssueReporting
1818

1919

20-
@DependencyClient
20+
// We currently cannot use @DependencyClient here due to
21+
// https://github.com/pointfreeco/swift-dependencies/discussions/324
22+
//@DependencyClient
2123
struct GithubClient {
2224
var fetchLicense: @Sendable (_ owner: String, _ repository: String) async -> Github.License?
25+
var fetchMetadata: @Sendable (_ owner: String, _ repository: String) async throws(Github.Error) -> Github.Metadata = { _,_ in reportIssue("fetchMetadata"); return .init() }
2326
}
2427

2528

2629
extension GithubClient: DependencyKey {
2730
static var liveValue: Self {
2831
.init(
29-
fetchLicense: { owner, repo in await Github.fetchLicense(owner: owner, repository: repo) }
32+
fetchLicense: { owner, repo in await Github.fetchLicense(owner: owner, repository: repo) },
33+
fetchMetadata: { owner, repo throws(Github.Error) in try await Github.fetchMetadata(owner: owner, repository: repo) }
3034
)
3135
}
3236
}
3337

3438

3539
extension GithubClient: TestDependencyKey {
36-
static var testValue: Self { Self() }
40+
static var testValue: Self {
41+
.init(
42+
fetchLicense: { _, _ in unimplemented("fetchLicense"); return nil },
43+
fetchMetadata: { _, _ in unimplemented("fetchMetadata"); return .init() }
44+
)
45+
}
3746
}
3847

3948

Sources/App/Core/Dependencies/HTTPClient.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,8 +106,9 @@ extension HTTPClient.Response {
106106
self.init(host: "host", status: status, version: version, headers: headers, body: body)
107107
}
108108

109-
static var ok: Self { .init(status: .ok) }
110-
static var notFound: Self { .init(status: .notFound) }
111109
static var badRequest: Self { .init(status: .badRequest) }
110+
static var notFound: Self { .init(status: .notFound) }
111+
static var tooManyRequests: Self { .init(status: .tooManyRequests) }
112+
static var ok: Self { .init(status: .ok) }
112113
}
113114
#endif

Sources/App/Core/ErrorHandlingHelpers.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,15 @@ func run<T, E1: Error, E2: Error>(_ operation: () async throws(E1) -> T,
3737
throw transform(error)
3838
}
3939
}
40+
41+
42+
@discardableResult
43+
func run<T, E1: Error, E2: Error>(_ operation: () throws(E1) -> T,
44+
rethrowing transform: (E1) -> E2) throws(E2) -> T {
45+
do {
46+
let result = try operation()
47+
return result
48+
} catch {
49+
throw transform(error)
50+
}
51+
}

Sources/App/Core/Github.swift

Lines changed: 48 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,12 @@ import Vapor
2121
enum Github {
2222

2323
enum Error: Swift.Error {
24-
case decodeContentFailed(URI, Swift.Error)
24+
case decodeContentFailed(_ url: String, Swift.Error)
25+
case encodeContentFailed(_ url: String, Swift.Error)
2526
case missingToken
2627
case noBody
2728
case invalidURL(String)
28-
case postRequestFailed(URI, Swift.Error)
29+
case postRequestFailed(_ url: String, Swift.Error)
2930
case requestFailed(HTTPStatus)
3031
}
3132

@@ -108,7 +109,7 @@ extension Github {
108109
return "https://api.github.com/repos/\(owner)/\(repository)/\(resource.rawValue)"
109110
}
110111
}
111-
112+
112113
@available(*, deprecated)
113114
static func fetch(client: Client, uri: URI, headers: [(String, String)] = []) async throws -> (content: String, etag: String?) {
114115
guard let token = Current.githubToken() else {
@@ -211,12 +212,15 @@ extension Github {
211212

212213
extension Github {
213214

215+
@available(*, deprecated)
214216
static let graphQLApiUri = URI(string: "https://api.github.com/graphql")
217+
static let graphQLApiURL = "https://api.github.com/graphql"
215218

216219
struct GraphQLQuery: Content {
217220
var query: String
218221
}
219222

223+
@available(*, deprecated)
220224
static func fetchResource<T: Decodable>(_ type: T.Type, client: Client, query: GraphQLQuery) async throws(Github.Error) -> T {
221225
guard let token = Current.githubToken() else {
222226
throw Error.missingToken
@@ -228,7 +232,7 @@ extension Github {
228232
try $0.content.encode(query)
229233
}
230234
} catch {
231-
throw .postRequestFailed(Self.graphQLApiUri, error)
235+
throw .postRequestFailed(graphQLApiUri.string, error)
232236
}
233237

234238
guard !isRateLimited(response) else {
@@ -244,25 +248,57 @@ extension Github {
244248
do {
245249
return try response.content.decode(T.self, using: decoder)
246250
} catch {
247-
throw .decodeContentFailed(Self.graphQLApiUri, error)
251+
throw .decodeContentFailed(graphQLApiUri.string, error)
252+
}
253+
}
254+
255+
static func fetchResource<T: Decodable>(_ type: T.Type, query: GraphQLQuery) async throws(Github.Error) -> T {
256+
guard let token = Current.githubToken() else {
257+
throw Error.missingToken
258+
}
259+
260+
@Dependency(\.httpClient) var httpClient
261+
262+
let body = try run {
263+
try JSONEncoder().encode(query)
264+
} rethrowing: {
265+
Error.encodeContentFailed(graphQLApiURL, $0)
266+
}
267+
268+
let response = try await run {
269+
try await httpClient.post(url: graphQLApiURL, headers: defaultHeaders(with: token), body: body)
270+
} rethrowing: {
271+
Error.postRequestFailed(graphQLApiURL, $0)
272+
}
273+
274+
guard !isRateLimited(response) else {
275+
Current.logger().critical("rate limited while fetching resource \(T.self)")
276+
throw Error.requestFailed(.tooManyRequests)
277+
}
278+
279+
guard response.status == .ok else {
280+
Current.logger().warning("fetchResource<\(T.self)> request failed with status \(response.status)")
281+
throw Error.requestFailed(response.status)
282+
}
283+
284+
guard let body = response.body else { throw Github.Error.noBody }
285+
286+
return try run {
287+
try decoder.decode(T.self, from: body)
288+
} rethrowing: {
289+
Error.decodeContentFailed(graphQLApiURL, $0)
248290
}
249291
}
250292

251-
static func fetchMetadata(client: Client, owner: String, repository: String) async throws(Github.Error) -> Metadata {
293+
static func fetchMetadata(owner: String, repository: String) async throws(Github.Error) -> Metadata {
252294
struct Response<T: Decodable & Equatable>: Decodable, Equatable {
253295
var data: T
254296
}
255297
return try await fetchResource(Response<Metadata>.self,
256-
client: client,
257298
query: Metadata.query(owner: owner, repository: repository))
258299
.data
259300
}
260301

261-
static func fetchMetadata(client: Client, packageUrl: String) async throws -> Metadata {
262-
let (owner, name) = try parseOwnerName(url: packageUrl)
263-
return try await fetchMetadata(client: client, owner: owner, repository: name)
264-
}
265-
266302
}
267303

268304

Tests/AppTests/ErrorReportingTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,10 @@ class ErrorReportingTests: AppTestCase {
3434
func test_Ingestion_error_reporting() async throws {
3535
// setup
3636
try await Package(id: .id0, url: "1", processingStage: .reconciliation).save(on: app.db)
37-
Current.fetchMetadata = { _, _, _ throws(Github.Error) in throw Github.Error.invalidURL("1") }
3837

3938
try await withDependencies {
4039
$0.date.now = .now
40+
$0.github.fetchMetadata = { @Sendable _, _ throws(Github.Error) in throw Github.Error.invalidURL("1") }
4141
} operation: {
4242
// MUT
4343
try await Ingestion.ingest(client: app.client, database: app.db, mode: .limit(10))

0 commit comments

Comments
 (0)