Skip to content

Commit c61c582

Browse files
Merge pull request #3572 from SwiftPackageIndex/issue-3469-dependency-transition-13
Issue 3469 dependency transition 13
2 parents 17ee055 + 941b51e commit c61c582

File tree

11 files changed

+196
-73
lines changed

11 files changed

+196
-73
lines changed

Sources/App/Commands/Ingestion.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -258,12 +258,13 @@ 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
261262
if environment.shouldFail(failureMode: .fetchMetadataFailed) {
262263
throw Github.Error.requestFailed(.internalServerError)
263264
}
264265

265266
async let metadata = try await Current.fetchMetadata(client, owner, repository)
266-
async let license = await Current.fetchLicense(client, owner, repository)
267+
async let license = await github.fetchLicense(owner, repository)
267268
async let readme = await Current.fetchReadme(client, owner, repository)
268269

269270
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 fetchLicense: @Sendable (_ client: Client, _ owner: String, _ repository: String) async -> Github.License?
2726
var fetchMetadata: @Sendable (_ client: Client, _ owner: String, _ repository: String) async throws(Github.Error) -> Github.Metadata
2827
var fetchReadme: @Sendable (_ client: Client, _ owner: String, _ repository: String) async -> Github.Readme?
2928
var fetchS3Readme: @Sendable (_ client: Client, _ owner: String, _ repository: String) async throws -> String
@@ -66,7 +65,6 @@ extension AppEnvironment {
6665
nonisolated(unsafe) static var logger: Logger!
6766

6867
static let live = AppEnvironment(
69-
fetchLicense: { client, owner, repo in await Github.fetchLicense(client:client, owner: owner, repository: repo) },
7068
fetchMetadata: { client, owner, repo throws(Github.Error) in try await Github.fetchMetadata(client:client, owner: owner, repository: repo) },
7169
fetchReadme: { client, owner, repo in await Github.fetchReadme(client:client, owner: owner, repository: repo) },
7270
fetchS3Readme: { client, owner, repo in try await S3Readme.fetchReadme(client:client, owner: owner, repository: repo) },
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
// Copyright Dave Verwer, Sven A. Schmidt, and other contributors.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
16+
import Dependencies
17+
import DependenciesMacros
18+
19+
20+
@DependencyClient
21+
struct GithubClient {
22+
var fetchLicense: @Sendable (_ owner: String, _ repository: String) async -> Github.License?
23+
}
24+
25+
26+
extension GithubClient: DependencyKey {
27+
static var liveValue: Self {
28+
.init(
29+
fetchLicense: { owner, repo in await Github.fetchLicense(owner: owner, repository: repo) }
30+
)
31+
}
32+
}
33+
34+
35+
extension GithubClient: TestDependencyKey {
36+
static var testValue: Self { Self() }
37+
}
38+
39+
40+
extension DependencyValues {
41+
var github: GithubClient {
42+
get { self[GithubClient.self] }
43+
set { self[GithubClient.self] = newValue }
44+
}
45+
}

Sources/App/Core/Dependencies/HTTPClient.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ struct HTTPClient {
2323
typealias Request = Vapor.HTTPClient.Request
2424
typealias Response = Vapor.HTTPClient.Response
2525

26+
var get: @Sendable (_ url: String, _ headers: HTTPHeaders) async throws -> Response
2627
var post: @Sendable (_ url: String, _ headers: HTTPHeaders, _ body: Data?) async throws -> Response
28+
2729
var fetchDocumentation: @Sendable (_ url: URI) async throws -> Response
2830
var fetchHTTPStatusCode: @Sendable (_ url: String) async throws -> HTTPStatus
2931
var mastodonPost: @Sendable (_ message: String) async throws -> Void
@@ -33,6 +35,10 @@ struct HTTPClient {
3335
extension HTTPClient: DependencyKey {
3436
static var liveValue: HTTPClient {
3537
.init(
38+
get: { url, headers in
39+
let req = try Request(url: url, method: .GET, headers: headers)
40+
return try await Vapor.HTTPClient.shared.execute(request: req).get()
41+
},
3642
post: { url, headers, body in
3743
let req = try Request(url: url, method: .POST, headers: headers, body: body.map({.data($0)}))
3844
return try await Vapor.HTTPClient.shared.execute(request: req).get()

Sources/App/Core/Github.swift

Lines changed: 53 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
import Vapor
16-
import SwiftSoup
15+
import Dependencies
1716
import S3Store
17+
import SwiftSoup
18+
import Vapor
1819

1920

2021
enum Github {
@@ -35,6 +36,7 @@ enum Github {
3536
return decoder
3637
}
3738

39+
@available(*, deprecated)
3840
static func rateLimit(response: ClientResponse) -> Int? {
3941
guard
4042
let header = response.headers.first(name: "X-RateLimit-Remaining"),
@@ -43,12 +45,27 @@ enum Github {
4345
return limit
4446
}
4547

48+
static func rateLimit(response: HTTPClient.Response) -> Int? {
49+
guard
50+
let header = response.headers.first(name: "X-RateLimit-Remaining"),
51+
let limit = Int(header)
52+
else { return nil }
53+
return limit
54+
}
55+
56+
@available(*, deprecated)
4657
static func isRateLimited(_ response: ClientResponse) -> Bool {
4758
guard let limit = rateLimit(response: response) else { return false }
4859
AppMetrics.githubRateLimitRemainingCount?.set(limit)
4960
return response.status == .forbidden && limit == 0
5061
}
5162

63+
static func isRateLimited(_ response: HTTPClient.Response) -> Bool {
64+
guard let limit = rateLimit(response: response) else { return false }
65+
AppMetrics.githubRateLimitRemainingCount?.set(limit)
66+
return response.status == .forbidden && limit == 0
67+
}
68+
5269
static func parseOwnerName(url: String) throws(Github.Error) -> (owner: String, name: String) {
5370
let parts = url
5471
.droppingGithubComPrefix
@@ -77,13 +94,22 @@ extension Github {
7794
case readme
7895
}
7996

97+
@available(*, deprecated)
8098
static func apiUri(owner: String, repository: String, resource: Resource) -> URI {
8199
switch resource {
82100
case .license, .readme:
83101
return URI(string: "https://api.github.com/repos/\(owner)/\(repository)/\(resource.rawValue)")
84102
}
85103
}
86104

105+
static func apiURL(owner: String, repository: String, resource: Resource) -> String {
106+
switch resource {
107+
case .license, .readme:
108+
return "https://api.github.com/repos/\(owner)/\(repository)/\(resource.rawValue)"
109+
}
110+
}
111+
112+
@available(*, deprecated)
87113
static func fetch(client: Client, uri: URI, headers: [(String, String)] = []) async throws -> (content: String, etag: String?) {
88114
guard let token = Current.githubToken() else {
89115
throw Error.missingToken
@@ -109,6 +135,7 @@ extension Github {
109135
return (body.asString(), response.headers.first(name: .eTag))
110136
}
111137

138+
@available(*, deprecated)
112139
static func fetchResource<T: Decodable>(_ type: T.Type, client: Client, uri: URI) async throws -> T {
113140
guard let token = Current.githubToken() else {
114141
throw Error.missingToken
@@ -128,9 +155,29 @@ extension Github {
128155
return try response.content.decode(T.self, using: decoder)
129156
}
130157

131-
static func fetchLicense(client: Client, owner: String, repository: String) async -> License? {
132-
let uri = Github.apiUri(owner: owner, repository: repository, resource: .license)
133-
return try? await Github.fetchResource(Github.License.self, client: client, uri: uri)
158+
static func fetchResource<T: Decodable>(_ type: T.Type, url: String) async throws -> T {
159+
guard let token = Current.githubToken() else {
160+
throw Error.missingToken
161+
}
162+
163+
@Dependency(\.httpClient) var httpClient
164+
165+
let response = try await httpClient.get(url: url, headers: defaultHeaders(with: token))
166+
167+
guard !isRateLimited(response) else {
168+
Current.logger().critical("rate limited while fetching resource \(url)")
169+
throw Error.requestFailed(.tooManyRequests)
170+
}
171+
172+
guard response.status == .ok else { throw Error.requestFailed(response.status) }
173+
guard let body = response.body else { throw Github.Error.noBody }
174+
175+
return try decoder.decode(T.self, from: body)
176+
}
177+
178+
static func fetchLicense(owner: String, repository: String) async -> License? {
179+
let url = Github.apiURL(owner: owner, repository: repository, resource: .license)
180+
return try? await Github.fetchResource(Github.License.self, url: url)
134181
}
135182

136183
static func fetchReadme(client: Client, owner: String, repository: String) async -> Readme? {
@@ -466,7 +513,7 @@ extension Github {
466513
}
467514
}
468515
}
469-
516+
470517
struct Parent: Decodable, Equatable {
471518
var url: String?
472519
}

Tests/AppTests/GithubTests.swift

Lines changed: 38 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -261,28 +261,28 @@ class GithubTests: AppTestCase {
261261

262262
func test_isRateLimited() throws {
263263
do {
264-
let res = ClientResponse(status: .forbidden,
265-
headers: .init([("X-RateLimit-Remaining", "0")]))
264+
let res = HTTPClient.Response(status: .forbidden,
265+
headers: .init([("X-RateLimit-Remaining", "0")]))
266266
XCTAssertTrue(Github.isRateLimited(res))
267267
}
268268
do {
269-
let res = ClientResponse(status: .forbidden,
270-
headers: .init([("x-ratelimit-remaining", "0")]))
269+
let res = HTTPClient.Response(status: .forbidden,
270+
headers: .init([("x-ratelimit-remaining", "0")]))
271271
XCTAssertTrue(Github.isRateLimited(res))
272272
}
273273
do {
274-
let res = ClientResponse(status: .forbidden,
275-
headers: .init([("X-RateLimit-Remaining", "1")]))
274+
let res = HTTPClient.Response(status: .forbidden,
275+
headers: .init([("X-RateLimit-Remaining", "1")]))
276276
XCTAssertFalse(Github.isRateLimited(res))
277277
}
278278
do {
279-
let res = ClientResponse(status: .forbidden,
280-
headers: .init([("unrelated", "0")]))
279+
let res = HTTPClient.Response(status: .forbidden,
280+
headers: .init([("unrelated", "0")]))
281281
XCTAssertFalse(Github.isRateLimited(res))
282282
}
283283
do {
284-
let res = ClientResponse(status: .ok,
285-
headers: .init([("X-RateLimit-Remaining", "0")]))
284+
let res = HTTPClient.Response(status: .ok,
285+
headers: .init([("X-RateLimit-Remaining", "0")]))
286286
XCTAssertFalse(Github.isRateLimited(res))
287287
}
288288
}
@@ -318,40 +318,44 @@ class GithubTests: AppTestCase {
318318
}
319319
}
320320

321-
func test_apiUri() throws {
322-
XCTAssertEqual(Github.apiUri(owner: "foo", repository: "bar", resource: .license).string,
321+
func test_apiURL() throws {
322+
XCTAssertEqual(Github.apiURL(owner: "foo", repository: "bar", resource: .license),
323323
"https://api.github.com/repos/foo/bar/license")
324-
XCTAssertEqual(Github.apiUri(owner: "foo", repository: "bar", resource: .readme).string,
324+
XCTAssertEqual(Github.apiURL(owner: "foo", repository: "bar", resource: .readme),
325325
"https://api.github.com/repos/foo/bar/readme")
326326
}
327327

328328
func test_fetchLicense() async throws {
329329
// setup
330330
Current.githubToken = { "secr3t" }
331-
let data = try XCTUnwrap(try fixtureData(for: "github-license-response.json"))
332-
let client = MockClient { _, resp in
333-
resp.status = .ok
334-
resp.body = makeBody(data)
335-
}
336331

337-
// MUT
338-
let res = await Github.fetchLicense(client: client, owner: "PSPDFKit", repository: "PSPDFKit-SP")
332+
await withDependencies {
333+
$0.httpClient.get = { @Sendable _, _ in
334+
try .init(status: .ok, body: .fixture(named: "github-license-response.json"))
335+
}
336+
} operation: {
337+
// MUT
338+
let res = await Github.fetchLicense(owner: "PSPDFKit", repository: "PSPDFKit-SP")
339339

340-
// validate
341-
XCTAssertEqual(res?.htmlUrl, "https://github.com/PSPDFKit/PSPDFKit-SP/blob/master/LICENSE")
340+
// validate
341+
XCTAssertEqual(res?.htmlUrl, "https://github.com/PSPDFKit/PSPDFKit-SP/blob/master/LICENSE")
342+
}
342343
}
343344

344345
func test_fetchLicense_notFound() async throws {
345346
// https://github.com/SwiftPackageIndex/SwiftPackageIndex-Server/issues/761
346347
// setup
347348
Current.githubToken = { "secr3t" }
348-
let client = MockClient { _, resp in resp.status = .notFound }
349349

350-
// MUT
351-
let res = await Github.fetchLicense(client: client, owner: "foo", repository: "bar")
350+
await withDependencies {
351+
$0.httpClient.get = { @Sendable _, _ in .notFound }
352+
} operation: {
353+
// MUT
354+
let res = await Github.fetchLicense(owner: "foo", repository: "bar")
352355

353-
// validate
354-
XCTAssertEqual(res, nil)
356+
// validate
357+
XCTAssertEqual(res, nil)
358+
}
355359
}
356360

357361
func test_fetchReadme() async throws {
@@ -488,3 +492,10 @@ class GithubTests: AppTestCase {
488492
}
489493

490494
}
495+
496+
497+
private extension ByteBuffer {
498+
static func fixture(named filename: String) throws -> Self {
499+
.init(data: try fixtureData(for: filename))
500+
}
501+
}

0 commit comments

Comments
 (0)