Skip to content

Commit 757e43a

Browse files
Merge pull request #3587 from SwiftPackageIndex/issue-3469-dependency-transition-15
Issue 3469 dependency transition 15
2 parents 47b1f9b + ca29bd6 commit 757e43a

File tree

10 files changed

+104
-162
lines changed

10 files changed

+104
-162
lines changed

Sources/App/Commands/Ingestion.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,10 +267,11 @@ enum Ingestion {
267267
// Sending 'github' into async let risks causing data races between async let uses and local uses
268268
@Dependency(\.github.fetchMetadata) var fetchMetadata
269269
@Dependency(\.github.fetchLicense) var fetchLicense
270+
@Dependency(\.github.fetchReadme) var fetchReadme
270271

271272
async let metadata = try await fetchMetadata(owner, repository)
272273
async let license = await fetchLicense(owner, repository)
273-
async let readme = await Current.fetchReadme(client, owner, repository)
274+
async let readme = await fetchReadme(owner, repository)
274275

275276
do {
276277
return try await (metadata, license, readme)

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 fetchReadme: @Sendable (_ client: Client, _ owner: String, _ repository: String) async -> Github.Readme?
2726
var fetchS3Readme: @Sendable (_ client: Client, _ owner: String, _ repository: String) async throws -> String
2827
var fileManager: FileManager
2928
var getStatusCount: @Sendable (_ client: Client, _ status: Gitlab.Builder.Status) async throws -> Int
@@ -64,7 +63,6 @@ extension AppEnvironment {
6463
nonisolated(unsafe) static var logger: Logger!
6564

6665
static let live = AppEnvironment(
67-
fetchReadme: { client, owner, repo in await Github.fetchReadme(client:client, owner: owner, repository: repo) },
6866
fetchS3Readme: { client, owner, repo in try await S3Readme.fetchReadme(client:client, owner: owner, repository: repo) },
6967
fileManager: .live,
7068
getStatusCount: { client, status in

Sources/App/Core/Dependencies/GithubClient.swift

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,14 +23,16 @@ import IssueReporting
2323
struct GithubClient {
2424
var fetchLicense: @Sendable (_ owner: String, _ repository: String) async -> Github.License?
2525
var fetchMetadata: @Sendable (_ owner: String, _ repository: String) async throws(Github.Error) -> Github.Metadata = { _,_ in reportIssue("fetchMetadata"); return .init() }
26+
var fetchReadme: @Sendable (_ owner: String, _ repository: String) async -> Github.Readme?
2627
}
2728

2829

2930
extension GithubClient: DependencyKey {
3031
static var liveValue: Self {
3132
.init(
3233
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) }
34+
fetchMetadata: { owner, repo throws(Github.Error) in try await Github.fetchMetadata(owner: owner, repository: repo) },
35+
fetchReadme: { owner, repo in await Github.fetchReadme(owner: owner, repository: repo) }
3436
)
3537
}
3638
}
@@ -40,7 +42,8 @@ extension GithubClient: TestDependencyKey {
4042
static var testValue: Self {
4143
.init(
4244
fetchLicense: { _, _ in unimplemented("fetchLicense"); return nil },
43-
fetchMetadata: { _, _ in unimplemented("fetchMetadata"); return .init() }
45+
fetchMetadata: { _, _ in unimplemented("fetchMetadata"); return .init() },
46+
fetchReadme: { _, _ in unimplemented("fetchReadme"); return nil }
4447
)
4548
}
4649
}

Sources/App/Core/Github.swift

Lines changed: 10 additions & 87 deletions
Original file line numberDiff line numberDiff line change
@@ -37,15 +37,6 @@ enum Github {
3737
return decoder
3838
}
3939

40-
@available(*, deprecated)
41-
static func rateLimit(response: ClientResponse) -> Int? {
42-
guard
43-
let header = response.headers.first(name: "X-RateLimit-Remaining"),
44-
let limit = Int(header)
45-
else { return nil }
46-
return limit
47-
}
48-
4940
static func rateLimit(response: HTTPClient.Response) -> Int? {
5041
guard
5142
let header = response.headers.first(name: "X-RateLimit-Remaining"),
@@ -54,13 +45,6 @@ enum Github {
5445
return limit
5546
}
5647

57-
@available(*, deprecated)
58-
static func isRateLimited(_ response: ClientResponse) -> Bool {
59-
guard let limit = rateLimit(response: response) else { return false }
60-
AppMetrics.githubRateLimitRemainingCount?.set(limit)
61-
return response.status == .forbidden && limit == 0
62-
}
63-
6448
static func isRateLimited(_ response: HTTPClient.Response) -> Bool {
6549
guard let limit = rateLimit(response: response) else { return false }
6650
AppMetrics.githubRateLimitRemainingCount?.set(limit)
@@ -95,36 +79,29 @@ extension Github {
9579
case readme
9680
}
9781

98-
@available(*, deprecated)
99-
static func apiUri(owner: String, repository: String, resource: Resource) -> URI {
100-
switch resource {
101-
case .license, .readme:
102-
return URI(string: "https://api.github.com/repos/\(owner)/\(repository)/\(resource.rawValue)")
103-
}
104-
}
105-
10682
static func apiURL(owner: String, repository: String, resource: Resource) -> String {
10783
switch resource {
10884
case .license, .readme:
10985
return "https://api.github.com/repos/\(owner)/\(repository)/\(resource.rawValue)"
11086
}
11187
}
11288

113-
@available(*, deprecated)
114-
static func fetch(client: Client, uri: URI, headers: [(String, String)] = []) async throws -> (content: String, etag: String?) {
89+
static func fetch(url: String, headers: [(String, String)] = []) async throws -> (content: String, etag: String?) {
11590
guard let token = Current.githubToken() else {
11691
throw Error.missingToken
11792
}
11893

119-
let response = try await client.get(uri, headers: defaultHeaders(with: token).adding(contentsOf: headers))
94+
@Dependency(\.httpClient) var httpClient
95+
96+
let response = try await httpClient.get(url: url, headers: defaultHeaders(with: token).adding(contentsOf: headers))
12097

12198
guard !isRateLimited(response) else {
122-
Current.logger().critical("rate limited while fetching uri \(uri)")
99+
Current.logger().critical("rate limited while fetching \(url)")
123100
throw Error.requestFailed(.tooManyRequests)
124101
}
125102

126103
guard response.status == .ok else {
127-
Current.logger().warning("Github.fetch of '\(uri.path)' failed with status \(response.status)")
104+
Current.logger().warning("Github.fetch of '\(url)' failed with status \(response.status)")
128105
throw Error.requestFailed(response.status)
129106
}
130107

@@ -136,26 +113,6 @@ extension Github {
136113
return (body.asString(), response.headers.first(name: .eTag))
137114
}
138115

139-
@available(*, deprecated)
140-
static func fetchResource<T: Decodable>(_ type: T.Type, client: Client, uri: URI) async throws -> T {
141-
guard let token = Current.githubToken() else {
142-
throw Error.missingToken
143-
}
144-
145-
let response = try await client.get(uri, headers: defaultHeaders(with: token))
146-
147-
guard !isRateLimited(response) else {
148-
Current.logger().critical("rate limited while fetching resource \(uri)")
149-
throw Error.requestFailed(.tooManyRequests)
150-
}
151-
152-
guard response.status == .ok else {
153-
throw Error.requestFailed(response.status)
154-
}
155-
156-
return try response.content.decode(T.self, using: decoder)
157-
}
158-
159116
static func fetchResource<T: Decodable>(_ type: T.Type, url: String) async throws -> T {
160117
guard let token = Current.githubToken() else {
161118
throw Error.missingToken
@@ -181,11 +138,11 @@ extension Github {
181138
return try? await Github.fetchResource(Github.License.self, url: url)
182139
}
183140

184-
static func fetchReadme(client: Client, owner: String, repository: String) async -> Readme? {
185-
let uri = Github.apiUri(owner: owner, repository: repository, resource: .readme)
141+
static func fetchReadme(owner: String, repository: String) async -> Readme? {
142+
let url = Github.apiURL(owner: owner, repository: repository, resource: .readme)
186143

187144
// Fetch readme html content
188-
let readme = try? await Github.fetch(client: client, uri: uri, headers: [
145+
let readme = try? await Github.fetch(url: url, headers: [
189146
("Accept", "application/vnd.github.html+json")
190147
])
191148
guard var html = readme?.content else { return nil }
@@ -195,7 +152,7 @@ extension Github {
195152
struct Response: Decodable {
196153
var htmlUrl: String
197154
}
198-
return try? await Github.fetchResource(Response.self, client: client, uri: uri).htmlUrl
155+
return try? await Github.fetchResource(Response.self, url: url).htmlUrl
199156
}()
200157
guard let htmlUrl else { return nil }
201158

@@ -212,46 +169,12 @@ extension Github {
212169

213170
extension Github {
214171

215-
@available(*, deprecated)
216-
static let graphQLApiUri = URI(string: "https://api.github.com/graphql")
217172
static let graphQLApiURL = "https://api.github.com/graphql"
218173

219174
struct GraphQLQuery: Content {
220175
var query: String
221176
}
222177

223-
@available(*, deprecated)
224-
static func fetchResource<T: Decodable>(_ type: T.Type, client: Client, query: GraphQLQuery) async throws(Github.Error) -> T {
225-
guard let token = Current.githubToken() else {
226-
throw Error.missingToken
227-
}
228-
229-
let response: ClientResponse
230-
do {
231-
response = try await client.post(Self.graphQLApiUri, headers: defaultHeaders(with: token)) {
232-
try $0.content.encode(query)
233-
}
234-
} catch {
235-
throw .postRequestFailed(graphQLApiUri.string, error)
236-
}
237-
238-
guard !isRateLimited(response) else {
239-
Current.logger().critical("rate limited while fetching resource \(T.self)")
240-
throw Error.requestFailed(.tooManyRequests)
241-
}
242-
243-
guard response.status == .ok else {
244-
Current.logger().warning("fetchResource<\(T.self)> request failed with status \(response.status)")
245-
throw Error.requestFailed(response.status)
246-
}
247-
248-
do {
249-
return try response.content.decode(T.self, using: decoder)
250-
} catch {
251-
throw .decodeContentFailed(graphQLApiUri.string, error)
252-
}
253-
}
254-
255178
static func fetchResource<T: Decodable>(_ type: T.Type, query: GraphQLQuery) async throws(Github.Error) -> T {
256179
guard let token = Current.githubToken() else {
257180
throw Error.missingToken

Tests/AppTests/GithubTests.swift

Lines changed: 44 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -349,48 +349,52 @@ class GithubTests: AppTestCase {
349349
// setup
350350
Current.githubToken = { "secr3t" }
351351
let requestCount = QueueIsolated(0)
352-
let client = MockClient { req, resp in
353-
requestCount.increment()
354-
switch req.headers[.accept] {
355-
case ["application/vnd.github.html+json"]:
356-
resp.status = .ok
357-
resp.body = makeBody("readme html")
358-
resp.headers.add(name: .eTag, value: "etag")
359-
case []:
360-
resp.status = .ok
361-
struct Response: Encodable {
362-
var htmlUrl: String
363-
}
364-
resp.body = makeBody(try! JSONEncoder().encode(Response(htmlUrl: "readme url")))
365-
default:
366-
XCTFail("unexpected accept header")
352+
await withDependencies {
353+
$0.httpClient.get = { @Sendable _, headers in
354+
requestCount.increment()
355+
switch headers[.accept] {
356+
case ["application/vnd.github.html+json"]:
357+
return .ok(body: "readme html", headers: ["ETag": "etag"])
358+
case []:
359+
struct Response: Encodable {
360+
var htmlUrl: String
361+
}
362+
return try .ok(jsonEncode: Response(htmlUrl: "readme url"))
363+
default:
364+
XCTFail("unexpected accept header")
365+
}
366+
enum Error: Swift.Error { case unexpectedCodePath }
367+
throw Error.unexpectedCodePath
367368
}
368-
}
369+
} operation: {
370+
// MUT
371+
let res = await Github.fetchReadme(owner: "foo", repository: "bar")
369372

370-
// MUT
371-
let res = await Github.fetchReadme(client: client, owner: "foo", repository: "bar")
372-
373-
// validate
374-
XCTAssertEqual(requestCount.value, 2)
375-
XCTAssertEqual(
376-
res,
377-
.init(etag: "etag",
378-
html: "readme html",
379-
htmlUrl: "readme url",
380-
imagesToCache: [])
381-
)
373+
// validate
374+
XCTAssertEqual(requestCount.value, 2)
375+
XCTAssertEqual(
376+
res,
377+
.init(etag: "etag",
378+
html: "readme html",
379+
htmlUrl: "readme url",
380+
imagesToCache: [])
381+
)
382+
}
382383
}
383384

384385
func test_fetchReadme_notFound() async throws {
385386
// setup
386387
Current.githubToken = { "secr3t" }
387-
let client = MockClient { _, resp in resp.status = .notFound }
388388

389-
// MUT
390-
let res = await Github.fetchReadme(client: client, owner: "foo", repository: "bar")
389+
await withDependencies {
390+
$0.httpClient.get = { @Sendable _, headers in .notFound }
391+
} operation: {
392+
// MUT
393+
let res = await Github.fetchReadme(owner: "foo", repository: "bar")
391394

392-
// validate
393-
XCTAssertEqual(res, nil)
395+
// validate
396+
XCTAssertEqual(res, nil)
397+
}
394398
}
395399

396400
func test_extractImagesRequiringCaching() async throws {
@@ -482,11 +486,16 @@ class GithubTests: AppTestCase {
482486

483487

484488
private extension HTTPClient.Response {
485-
static func ok(body: String) -> Self {
486-
.init(status: .ok, body: .init(string: body))
489+
static func ok(body: String, headers: HTTPHeaders = .init()) -> Self {
490+
.init(status: .ok, headers: headers, body: .init(string: body))
487491
}
488492

489493
static func ok(fixture: String) throws -> Self {
490494
try .init(status: .ok, body: .init(data: fixtureData(for: fixture)))
491495
}
496+
497+
static func ok<T: Encodable>(jsonEncode value: T) throws -> Self {
498+
let data = try JSONEncoder().encode(value)
499+
return .init(status: .ok, body: .init(data: data))
500+
}
492501
}

0 commit comments

Comments
 (0)