Skip to content

Commit c779021

Browse files
committed
Refactor list command to support json format
1 parent 4fa8b10 commit c779021

File tree

6 files changed

+296
-78
lines changed

6 files changed

+296
-78
lines changed

Documentation/SwiftlyDocs.docc/swiftly-cli-reference.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -295,7 +295,7 @@ Finally, all installed toolchains can be uninstalled by specifying 'all':
295295
List installed toolchains.
296296

297297
```
298-
swiftly list [<toolchain-selector>] [--version] [--help]
298+
swiftly list [<toolchain-selector>] [--format=<format>] [--version] [--help]
299299
```
300300

301301
**toolchain-selector:**
@@ -321,6 +321,11 @@ The installed snapshots for a given development branch can be listed by specifyi
321321
$ swiftly list 5.7-snapshot
322322

323323

324+
**--format=\<format\>:**
325+
326+
*Output format (text, json)*
327+
328+
324329
**--version:**
325330

326331
*Show the version.*

Sources/Swiftly/List.swift

Lines changed: 12 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import ArgumentParser
2+
import Foundation
23
import SwiftlyCore
34

45
struct List: SwiftlyCommand {
@@ -33,8 +34,11 @@ struct List: SwiftlyCommand {
3334
))
3435
var toolchainSelector: String?
3536

37+
@Option(name: .long, help: "Output format (text, json)")
38+
var format: SwiftlyCore.OutputFormat = .text
39+
3640
mutating func run() async throws {
37-
try await self.run(Swiftly.createDefaultContext())
41+
try await self.run(Swiftly.createDefaultContext(format: self.format))
3842
}
3943

4044
mutating func run(_ ctx: SwiftlyCoreContext) async throws {
@@ -51,55 +55,14 @@ struct List: SwiftlyCommand {
5155
let toolchains = config.listInstalledToolchains(selector: selector).sorted { $0 > $1 }
5256
let (inUse, _) = try await selectToolchain(ctx, config: &config)
5357

54-
let printToolchain = { (toolchain: ToolchainVersion) in
55-
var message = "\(toolchain)"
56-
if let inUse, toolchain == inUse {
57-
message += " (in use)"
58-
}
59-
if toolchain == config.inUse {
60-
message += " (default)"
61-
}
62-
await ctx.message(message)
58+
let installedToolchainInfos = toolchains.compactMap { toolchain -> InstallToolchainInfo? in
59+
InstallToolchainInfo(
60+
version: toolchain,
61+
inUse: inUse == toolchain,
62+
isDefault: toolchain == config.inUse
63+
)
6364
}
6465

65-
if let selector {
66-
let modifier = switch selector {
67-
case let .stable(major, minor, nil):
68-
if let minor {
69-
"Swift \(major).\(minor) release"
70-
} else {
71-
"Swift \(major) release"
72-
}
73-
case .snapshot(.main, nil):
74-
"main development snapshot"
75-
case let .snapshot(.release(major, minor), nil):
76-
"\(major).\(minor) development snapshot"
77-
default:
78-
"matching"
79-
}
80-
81-
let message = "Installed \(modifier) toolchains"
82-
await ctx.message(message)
83-
await ctx.message(String(repeating: "-", count: message.count))
84-
for toolchain in toolchains {
85-
await printToolchain(toolchain)
86-
}
87-
} else {
88-
await ctx.message("Installed release toolchains")
89-
await ctx.message("----------------------------")
90-
for toolchain in toolchains {
91-
guard toolchain.isStableRelease() else {
92-
continue
93-
}
94-
await printToolchain(toolchain)
95-
}
96-
97-
await ctx.message("")
98-
await ctx.message("Installed snapshot toolchains")
99-
await ctx.message("-----------------------------")
100-
for toolchain in toolchains where toolchain.isSnapshot() {
101-
await printToolchain(toolchain)
102-
}
103-
}
66+
try await ctx.output(InstalledToolchainsListInfo(toolchains: installedToolchainInfos, selector: selector))
10467
}
10568
}

Sources/Swiftly/OutputSchema.swift

Lines changed: 177 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,16 @@ enum ToolchainSource: Codable, CustomStringConvertible {
5959
}
6060
}
6161

62+
private enum ToolchainVersionCodingKeys: String, CodingKey {
63+
case name
64+
case type
65+
case branch
66+
case major
67+
case minor
68+
case patch
69+
case date
70+
}
71+
6272
struct AvailableToolchainInfo: OutputData {
6373
let version: ToolchainVersion
6474
let inUse: Bool
@@ -82,24 +92,14 @@ struct AvailableToolchainInfo: OutputData {
8292
private enum CodingKeys: String, CodingKey {
8393
case version
8494
case inUse
85-
case `default`
95+
case isDefault
8696
case installed
8797
}
8898

89-
private enum ToolchainVersionCodingKeys: String, CodingKey {
90-
case name
91-
case type
92-
case branch
93-
case major
94-
case minor
95-
case patch
96-
case date
97-
}
98-
9999
public func encode(to encoder: Encoder) throws {
100100
var container = encoder.container(keyedBy: CodingKeys.self)
101101
try container.encode(self.inUse, forKey: .inUse)
102-
try container.encode(self.isDefault, forKey: .default)
102+
try container.encode(self.isDefault, forKey: .isDefault)
103103
try container.encode(self.installed, forKey: .installed)
104104

105105
// Encode the version as a object
@@ -131,7 +131,7 @@ struct AvailableToolchainInfo: OutputData {
131131

132132
struct AvailableToolchainsListInfo: OutputData {
133133
let toolchains: [AvailableToolchainInfo]
134-
let selector: ToolchainSelector?
134+
var selector: ToolchainSelector?
135135

136136
init(toolchains: [AvailableToolchainInfo], selector: ToolchainSelector? = nil) {
137137
self.toolchains = toolchains
@@ -175,3 +175,167 @@ struct AvailableToolchainsListInfo: OutputData {
175175
return lines.joined(separator: "\n")
176176
}
177177
}
178+
179+
struct InstallToolchainInfo: OutputData {
180+
let version: ToolchainVersion
181+
let inUse: Bool
182+
let isDefault: Bool
183+
184+
init(version: ToolchainVersion, inUse: Bool, isDefault: Bool) {
185+
self.version = version
186+
self.inUse = inUse
187+
self.isDefault = isDefault
188+
}
189+
190+
var description: String {
191+
var message = "\(version)"
192+
193+
if self.inUse {
194+
message += " (in use)"
195+
}
196+
if self.isDefault {
197+
message += " (default)"
198+
}
199+
return message
200+
}
201+
202+
private enum CodingKeys: String, CodingKey {
203+
case version
204+
case inUse
205+
case isDefault
206+
}
207+
208+
public func encode(to encoder: Encoder) throws {
209+
var container = encoder.container(keyedBy: CodingKeys.self)
210+
try container.encode(self.inUse, forKey: .inUse)
211+
try container.encode(self.isDefault, forKey: .isDefault)
212+
213+
// Encode the version as a object
214+
var versionContainer = container.nestedContainer(
215+
keyedBy: ToolchainVersionCodingKeys.self, forKey: .version
216+
)
217+
try versionContainer.encode(self.version.name, forKey: .name)
218+
219+
switch self.version {
220+
case let .stable(release):
221+
try versionContainer.encode("stable", forKey: .type)
222+
try versionContainer.encode(release.major, forKey: .major)
223+
try versionContainer.encode(release.minor, forKey: .minor)
224+
try versionContainer.encode(release.patch, forKey: .patch)
225+
case let .snapshot(snapshot):
226+
try versionContainer.encode("snapshot", forKey: .type)
227+
try versionContainer.encode(snapshot.date, forKey: .date)
228+
try versionContainer.encode(snapshot.branch.name, forKey: .branch)
229+
230+
if let major = snapshot.branch.major,
231+
let minor = snapshot.branch.minor
232+
{
233+
try versionContainer.encode(major, forKey: .major)
234+
try versionContainer.encode(minor, forKey: .minor)
235+
}
236+
}
237+
}
238+
239+
init(from decoder: Decoder) throws {
240+
let container = try decoder.container(keyedBy: CodingKeys.self)
241+
self.inUse = try container.decode(Bool.self, forKey: .inUse)
242+
self.isDefault = try container.decode(Bool.self, forKey: .isDefault)
243+
244+
// Decode the version as a object
245+
let versionContainer = try container.nestedContainer(
246+
keyedBy: ToolchainVersionCodingKeys.self, forKey: .version
247+
)
248+
let name = try versionContainer.decode(String.self, forKey: .name)
249+
250+
switch try versionContainer.decode(String.self, forKey: .type) {
251+
case "stable":
252+
let major = try versionContainer.decode(Int.self, forKey: .major)
253+
let minor = try versionContainer.decode(Int.self, forKey: .minor)
254+
let patch = try versionContainer.decode(Int.self, forKey: .patch)
255+
self.version = .stable(
256+
ToolchainVersion.StableRelease(major: major, minor: minor, patch: patch))
257+
case "snapshot":
258+
let date = try versionContainer.decode(String.self, forKey: .date)
259+
let branchName = try versionContainer.decode(String.self, forKey: .branch)
260+
let branchMajor = try? versionContainer.decodeIfPresent(Int.self, forKey: .major)
261+
let branchMinor = try? versionContainer.decodeIfPresent(Int.self, forKey: .minor)
262+
263+
// Determine the branch from the decoded data
264+
let branch: ToolchainVersion.Snapshot.Branch
265+
if branchName == "main" {
266+
branch = .main
267+
} else if let major = branchMajor, let minor = branchMinor {
268+
branch = .release(major: major, minor: minor)
269+
} else {
270+
throw DecodingError.dataCorruptedError(
271+
forKey: ToolchainVersionCodingKeys.branch,
272+
in: versionContainer,
273+
debugDescription: "Invalid branch format: \(branchName)"
274+
)
275+
}
276+
277+
self.version = .snapshot(
278+
ToolchainVersion.Snapshot(
279+
branch: branch,
280+
date: date
281+
))
282+
default:
283+
throw DecodingError.dataCorruptedError(
284+
forKey: ToolchainVersionCodingKeys.type,
285+
in: versionContainer,
286+
debugDescription: "Unknown toolchain type"
287+
)
288+
}
289+
}
290+
}
291+
292+
struct InstalledToolchainsListInfo: OutputData {
293+
let toolchains: [InstallToolchainInfo]
294+
var selector: ToolchainSelector?
295+
296+
private enum CodingKeys: String, CodingKey {
297+
case toolchains
298+
}
299+
300+
var description: String {
301+
var lines: [String] = []
302+
303+
if let selector = selector {
304+
let modifier =
305+
switch selector
306+
{
307+
case let .stable(major, minor, nil):
308+
if let minor {
309+
"Swift \(major).\(minor) release"
310+
} else {
311+
"Swift \(major) release"
312+
}
313+
case .snapshot(.main, nil):
314+
"main development snapshot"
315+
case let .snapshot(.release(major, minor), nil):
316+
"\(major).\(minor) development snapshot"
317+
default:
318+
"matching"
319+
}
320+
321+
let header = "Installed \(modifier) toolchains"
322+
lines.append(header)
323+
lines.append(String(repeating: "-", count: header.count))
324+
lines.append(contentsOf: self.toolchains.map(\.description))
325+
} else {
326+
let releaseToolchains = self.toolchains.filter { $0.version.isStableRelease() }
327+
let snapshotToolchains = self.toolchains.filter { $0.version.isSnapshot() }
328+
329+
lines.append("Installed release toolchains")
330+
lines.append("----------------------------")
331+
lines.append(contentsOf: releaseToolchains.map(\.description))
332+
333+
lines.append("")
334+
lines.append("Installed snapshot toolchains")
335+
lines.append("-----------------------------")
336+
lines.append(contentsOf: snapshotToolchains.map(\.description))
337+
}
338+
339+
return lines.joined(separator: "\n")
340+
}
341+
}

Sources/SwiftlyCore/OutputFormatter.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ public protocol OutputFormatter {
1414
func format(_ data: OutputData) throws -> String
1515
}
1616

17-
public protocol OutputData: Encodable, CustomStringConvertible {
17+
public protocol OutputData: CustomStringConvertible, Codable {
1818
var description: String { get }
1919
}
2020

0 commit comments

Comments
 (0)