Skip to content

Commit 351a278

Browse files
authored
Convert swift.org API usages to use OpenAPI generated API (#253)
* Rework the layer that does the filtering of releases * Add release mocking to the toolchain downloader with a mocked set of releases * Increase accuracy of release objects for macOS/xcode in mock toolchain downloader * Decouple from the known list of platforms and branches so that swiftly can operate independently of its current schema version * Add convenience constructors so that clients do not have to know about the value<x> field of the anyOf to assign.
1 parent ffdda2c commit 351a278

File tree

7 files changed

+263
-80
lines changed

7 files changed

+263
-80
lines changed

Sources/SwiftlyCore/HTTPClient.swift

Lines changed: 123 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -50,9 +50,17 @@ extension Components.Schemas.SwiftlyReleasePlatformArtifacts {
5050
}
5151
}
5252

53+
extension Components.Schemas.SwiftlyPlatformIdentifier {
54+
public init(_ knownSwiftlyPlatformIdentifier: Components.Schemas.KnownSwiftlyPlatformIdentifier) {
55+
self.init(value1: knownSwiftlyPlatformIdentifier)
56+
}
57+
}
58+
5359
public protocol HTTPRequestExecutor {
5460
func execute(_ request: HTTPClientRequest, timeout: TimeAmount) async throws -> HTTPClientResponse
5561
func getCurrentSwiftlyRelease() async throws -> Components.Schemas.SwiftlyRelease
62+
func getReleaseToolchains() async throws -> [Components.Schemas.Release]
63+
func getSnapshotToolchains(branch: Components.Schemas.SourceBranch, platform: Components.Schemas.PlatformIdentifier) async throws -> Components.Schemas.DevToolchains
5664
}
5765

5866
struct SwiftlyUserAgentMiddleware: ClientMiddleware {
@@ -128,6 +136,36 @@ class HTTPRequestExecutorImpl: HTTPRequestExecutor {
128136
let response = try await client.getCurrentSwiftlyRelease()
129137
return try response.ok.body.json
130138
}
139+
140+
public func getReleaseToolchains() async throws -> [Components.Schemas.Release] {
141+
let config = AsyncHTTPClientTransport.Configuration(client: self.httpClient, timeout: .seconds(30))
142+
let swiftlyUserAgent = SwiftlyUserAgentMiddleware()
143+
144+
let client = Client(
145+
serverURL: try Servers.Server1.url(),
146+
transport: AsyncHTTPClientTransport(configuration: config),
147+
middlewares: [swiftlyUserAgent]
148+
)
149+
150+
let response = try await client.listReleases()
151+
152+
return try response.ok.body.json
153+
}
154+
155+
public func getSnapshotToolchains(branch: Components.Schemas.SourceBranch, platform: Components.Schemas.PlatformIdentifier) async throws -> Components.Schemas.DevToolchains {
156+
let config = AsyncHTTPClientTransport.Configuration(client: self.httpClient, timeout: .seconds(30))
157+
let swiftlyUserAgent = SwiftlyUserAgentMiddleware()
158+
159+
let client = Client(
160+
serverURL: try Servers.Server1.url(),
161+
transport: AsyncHTTPClientTransport(configuration: config),
162+
middlewares: [swiftlyUserAgent]
163+
)
164+
165+
let response = try await client.listDevToolchains(.init(path: .init(branch: branch, platform: platform)))
166+
167+
return try response.ok.body.json
168+
}
131169
}
132170

133171
private func makeRequest(url: String) -> HTTPClientRequest {
@@ -136,13 +174,56 @@ private func makeRequest(url: String) -> HTTPClientRequest {
136174
return request
137175
}
138176

139-
struct SwiftOrgPlatform: Codable {
140-
var name: String
141-
var archs: [String]?
177+
extension Components.Schemas.Release {
178+
var stableName: String {
179+
let components = self.name.components(separatedBy: ".")
180+
if components.count == 2 {
181+
return self.name + ".0"
182+
} else {
183+
return self.name
184+
}
185+
}
186+
}
142187

143-
/// platform is a mapping from the 'name' field of the swift.org platform object
188+
extension Components.Schemas.Architecture {
189+
public init(_ knownArchitecture: Components.Schemas.KnownArchitecture) {
190+
self.init(value1: knownArchitecture, value2: knownArchitecture.rawValue)
191+
}
192+
193+
public init(_ string: String) {
194+
self.init(value2: string)
195+
}
196+
}
197+
198+
extension Components.Schemas.PlatformIdentifier {
199+
public init(_ knownPlatformIdentifier: Components.Schemas.KnownPlatformIdentifier) {
200+
self.init(value1: knownPlatformIdentifier)
201+
}
202+
203+
public init(_ string: String) {
204+
self.init(value2: string)
205+
}
206+
}
207+
208+
extension Components.Schemas.SourceBranch {
209+
public init(_ knownSourceBranch: Components.Schemas.KnownSourceBranch) {
210+
self.init(value1: knownSourceBranch)
211+
}
212+
213+
public init(_ string: String) {
214+
self.init(value2: string)
215+
}
216+
}
217+
218+
extension Components.Schemas.Architecture {
219+
static var x8664: Components.Schemas.Architecture = .init(Components.Schemas.KnownArchitecture.x8664)
220+
static var aarch64: Components.Schemas.Architecture = .init(Components.Schemas.KnownArchitecture.aarch64)
221+
}
222+
223+
extension Components.Schemas.Platform {
224+
/// platformDef is a mapping from the 'name' field of the swift.org platform object
144225
/// to swiftly's PlatformDefinition, if possible.
145-
var platform: PlatformDefinition? {
226+
var platformDef: PlatformDefinition? {
146227
// NOTE: some of these platforms are represented on swift.org metadata, but not supported by swiftly and so they don't have constants in PlatformDefinition
147228
switch self.name {
148229
case "Ubuntu 14.04":
@@ -172,46 +253,24 @@ struct SwiftOrgPlatform: Codable {
172253
case "Ubuntu 24.04":
173254
PlatformDefinition(name: "ubuntu2404", nameFull: "ubuntu24.04", namePretty: "Ubuntu 24.04")
174255
case "Debian 12":
175-
PlatformDefinition(name: "debian12", nameFull: "debian12", namePretty: "Debian 12")
256+
PlatformDefinition(name: "debian12", nameFull: "debian12", namePretty: "Debian GNU/Linux 12")
176257
case "Fedora 39":
177-
PlatformDefinition(name: "fedora39", nameFull: "fedora39", namePretty: "Fedora 39")
258+
PlatformDefinition(name: "fedora39", nameFull: "fedora39", namePretty: "Fedora Linux 39")
178259
default:
179260
nil
180261
}
181262
}
182263

183264
func matches(_ platform: PlatformDefinition) -> Bool {
184-
guard let myPlatform = self.platform else {
265+
guard let myPlatform = self.platformDef else {
185266
return false
186267
}
187268

188269
return myPlatform.name == platform.name
189270
}
190271
}
191272

192-
public struct SwiftOrgRelease: Codable {
193-
var name: String
194-
var platforms: [SwiftOrgPlatform]
195-
196-
var stableName: String {
197-
let components = self.name.components(separatedBy: ".")
198-
if components.count == 2 {
199-
return self.name + ".0"
200-
} else {
201-
return self.name
202-
}
203-
}
204-
}
205-
206-
public struct SwiftOrgSnapshotList: Codable {
207-
var aarch64: [SwiftOrgSnapshot]?
208-
var x86_64: [SwiftOrgSnapshot]?
209-
var universal: [SwiftOrgSnapshot]?
210-
}
211-
212-
public struct SwiftOrgSnapshot: Codable {
213-
var dir: String
214-
273+
extension Components.Schemas.DevToolchainForArch {
215274
private static let snapshotRegex: Regex<(Substring, Substring?, Substring?, Substring)> =
216275
try! Regex("swift(?:-(\\d+)\\.(\\d+))?-DEVELOPMENT-SNAPSHOT-(\\d{4}-\\d{2}-\\d{2})")
217276

@@ -291,23 +350,22 @@ public struct SwiftlyHTTPClient {
291350
/// limit (default unlimited).
292351
public func getReleaseToolchains(
293352
platform: PlatformDefinition,
294-
arch a: String? = nil,
353+
arch a: Components.Schemas.Architecture? = nil,
295354
limit: Int? = nil,
296355
filter: ((ToolchainVersion.StableRelease) -> Bool)? = nil
297356
) async throws -> [ToolchainVersion.StableRelease] {
298357
let arch = a ?? cpuArch
299358

300-
let url = "https://www.swift.org/api/v1/install/releases.json"
301-
let swiftOrgReleases: [SwiftOrgRelease] = try await self.getFromJSON(url: url, type: [SwiftOrgRelease].self)
359+
let releases = try await SwiftlyCore.httpRequestExecutor.getReleaseToolchains()
302360

303-
var swiftOrgFiltered: [ToolchainVersion.StableRelease] = try swiftOrgReleases.compactMap { swiftOrgRelease in
361+
var swiftOrgFiltered: [ToolchainVersion.StableRelease] = try releases.compactMap { swiftOrgRelease in
304362
if platform.name != PlatformDefinition.macOS.name {
305363
// If the platform isn't xcode then verify that there is an offering for this platform name and arch
306364
guard let swiftOrgPlatform = swiftOrgRelease.platforms.first(where: { $0.matches(platform) }) else {
307365
return nil
308366
}
309367

310-
guard let archs = swiftOrgPlatform.archs, archs.contains(arch) else {
368+
guard case let archs = swiftOrgPlatform.archs, archs.contains(arch) else {
311369
return nil
312370
}
313371
}
@@ -349,35 +407,45 @@ public struct SwiftlyHTTPClient {
349407
limit: Int? = nil,
350408
filter: ((ToolchainVersion.Snapshot) -> Bool)? = nil
351409
) async throws -> [ToolchainVersion.Snapshot] {
352-
let arch = a ?? cpuArch
410+
let platformId: Components.Schemas.PlatformIdentifier = switch platform.name {
411+
// These are new platforms that aren't yet in the list of known platforms in the OpenAPI schema
412+
case PlatformDefinition.ubuntu2404.name, PlatformDefinition.debian12.name, PlatformDefinition.fedora39.name:
413+
.init(platform.name)
414+
415+
case PlatformDefinition.ubuntu2204.name:
416+
.init(.ubuntu2204)
417+
case PlatformDefinition.ubuntu2004.name:
418+
.init(.ubuntu2004)
419+
case PlatformDefinition.rhel9.name:
420+
.init(.ubi9)
421+
case PlatformDefinition.amazonlinux2.name:
422+
.init(.amazonlinux2)
423+
case PlatformDefinition.macOS.name:
424+
.init(.macos)
425+
default:
426+
throw SwiftlyError(message: "No snapshot toolchains available for platform \(platform.name)")
427+
}
353428

354-
let platformName = if platform.name == PlatformDefinition.macOS.name {
355-
"macos"
356-
} else {
357-
platform.name
429+
let sourceBranch: Components.Schemas.SourceBranch = switch branch {
430+
case .main:
431+
.init(.main)
432+
case let .release(major, minor):
433+
.init("\(major).\(minor)")
358434
}
359435

360-
let url = "https://www.swift.org/api/v1/install/dev/\(branch.name)/\(platformName).json"
436+
let devToolchains = try await SwiftlyCore.httpRequestExecutor.getSnapshotToolchains(branch: sourceBranch, platform: platformId)
361437

362-
// For a particular branch and platform the snapshots are listed underneath their architecture
363-
let swiftOrgSnapshotArchs: SwiftOrgSnapshotList
364-
do {
365-
swiftOrgSnapshotArchs = try await self.getFromJSON(url: url, type: SwiftOrgSnapshotList.self)
366-
} catch is JSONNotFoundError {
367-
throw SnapshotBranchNotFoundError(branch: branch)
368-
} catch {
369-
throw error
370-
}
438+
let arch = a ?? cpuArch.value2
371439

372440
// These are the available snapshots for the branch, platform, and architecture
373441
let swiftOrgSnapshots = if platform.name == PlatformDefinition.macOS.name {
374-
swiftOrgSnapshotArchs.universal ?? [SwiftOrgSnapshot]()
442+
devToolchains.universal ?? [Components.Schemas.DevToolchainForArch]()
375443
} else if arch == "aarch64" {
376-
swiftOrgSnapshotArchs.aarch64 ?? [SwiftOrgSnapshot]()
444+
devToolchains.aarch64 ?? [Components.Schemas.DevToolchainForArch]()
377445
} else if arch == "x86_64" {
378-
swiftOrgSnapshotArchs.x86_64 ?? [SwiftOrgSnapshot]()
446+
devToolchains.x8664 ?? [Components.Schemas.DevToolchainForArch]()
379447
} else {
380-
[SwiftOrgSnapshot]()
448+
[Components.Schemas.DevToolchainForArch]()
381449
}
382450

383451
// Convert these into toolchain snapshot versions that match the filter

Sources/SwiftlyCore/SwiftlyCore.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,9 +53,9 @@ public func readLine(prompt: String) -> String? {
5353
}
5454

5555
#if arch(x86_64)
56-
public let cpuArch = "x86_64"
56+
public let cpuArch = Components.Schemas.Architecture.x8664
5757
#elseif arch(arm64)
58-
public let cpuArch = "aarch64"
58+
public let cpuArch = Components.Schemas.Architecture.aarch64
5959
#else
6060
#error("Unsupported processor architecture")
6161
#endif

Sources/SwiftlyCore/openapi-generator-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@ generate:
44
namingStrategy: idiomatic
55
accessModifier: public
66
filter:
7-
tags: ["Swiftly"]
7+
tags: ["Swiftly", "Toolchains"]

Sources/SwiftlyCore/openapi.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,12 +37,12 @@ paths:
3737
in: path
3838
required: true
3939
schema:
40-
$ref: '#/components/schemas/KnownSourceBranch'
40+
$ref: '#/components/schemas/SourceBranch'
4141
- name: platform
4242
in: path
4343
required: true
4444
schema:
45-
$ref: '#/components/schemas/KnownPlatformIdentifier'
45+
$ref: '#/components/schemas/PlatformIdentifier'
4646
get:
4747
operationId: listDevToolchains
4848
summary: Fetch all development toolchains
@@ -61,7 +61,7 @@ paths:
6161
in: path
6262
required: true
6363
schema:
64-
$ref: '#/components/schemas/KnownSourceBranch'
64+
$ref: '#/components/schemas/SourceBranch'
6565
get:
6666
operationId: listStaticSDKDevToolchains
6767
summary: Fetch all static SDK development toolchains

Tests/SwiftlyTests/HTTPClientTests.swift

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ final class HTTPClientTests: SwiftlyTests {
66
func testGet() async throws {
77
// GIVEN: we have a swiftly http client
88
// WHEN: we make get request for a particular type of JSON
9-
var releases: [SwiftOrgRelease] = try await SwiftlyCore.httpClient.getFromJSON(
9+
var releases: [Components.Schemas.Release] = try await SwiftlyCore.httpClient.getFromJSON(
1010
url: "https://www.swift.org/api/v1/install/releases.json",
11-
type: [SwiftOrgRelease].self,
11+
type: [Components.Schemas.Release].self,
1212
headers: [:]
1313
)
1414
// THEN: we get a decoded JSON response
@@ -20,7 +20,7 @@ final class HTTPClientTests: SwiftlyTests {
2020
do {
2121
releases = try await SwiftlyCore.httpClient.getFromJSON(
2222
url: "https://www.swift.org/api/v1/install/releases-invalid.json",
23-
type: [SwiftOrgRelease].self,
23+
type: [Components.Schemas.Release].self,
2424
headers: [:]
2525
)
2626
} catch {
@@ -35,7 +35,7 @@ final class HTTPClientTests: SwiftlyTests {
3535
do {
3636
releases = try await SwiftlyCore.httpClient.getFromJSON(
3737
url: "https://invalid.swift.org/api/v1/install/releases.json",
38-
type: [SwiftOrgRelease].self,
38+
type: [Components.Schemas.Release].self,
3939
headers: [:]
4040
)
4141
} catch {
@@ -74,7 +74,7 @@ final class HTTPClientTests: SwiftlyTests {
7474
.release(major: 6, minor: 0), // This is available in swift.org API
7575
]
7676

77-
for arch in ["x86_64", "aarch64"] {
77+
for arch in [Components.Schemas.Architecture.x8664, Components.Schemas.Architecture.aarch64] {
7878
for platform in supportedPlatforms {
7979
// GIVEN: we have a swiftly http client with swift.org metadata capability
8080
// WHEN: we ask for the first five releases of a supported platform in a supported arch
@@ -87,7 +87,7 @@ final class HTTPClientTests: SwiftlyTests {
8787
for branch in branches {
8888
// GIVEN: we have a swiftly http client with swift.org metadata capability
8989
// WHEN: we ask for the first five snapshots on a branch for a supported platform and arch
90-
let snapshots = try await SwiftlyCore.httpClient.getSnapshotToolchains(platform: platform, arch: arch, branch: branch, limit: 5)
90+
let snapshots = try await SwiftlyCore.httpClient.getSnapshotToolchains(platform: platform, arch: arch.value2!, branch: branch, limit: 5)
9191
// THEN: we get at least 3 releases
9292
XCTAssertTrue(3 <= snapshots.count)
9393
}

0 commit comments

Comments
 (0)