Skip to content

Commit 4fa8b10

Browse files
authored
Refactor list-available command to support json format (#387)
1 parent cbc8bdc commit 4fa8b10

File tree

9 files changed

+511
-151
lines changed

9 files changed

+511
-151
lines changed

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ written to this file as commands that can be run after the installation.
107107
List toolchains available for install.
108108

109109
```
110-
swiftly list-available [<toolchain-selector>] [--version] [--help]
110+
swiftly list-available [<toolchain-selector>] [--format=<format>] [--version] [--help]
111111
```
112112

113113
**toolchain-selector:**
@@ -135,6 +135,11 @@ The installed snapshots for a given development branch can be listed by specifyi
135135
Note that listing available snapshots before the latest release (major and minor number) is unsupported.
136136

137137

138+
**--format=\<format\>:**
139+
140+
*Output format (text, json)*
141+
142+
138143
**--version:**
139144

140145
*Show the version.*

Sources/Swiftly/ListAvailable.swift

Lines changed: 14 additions & 45 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 ListAvailable: SwiftlyCommand {
@@ -35,12 +36,11 @@ struct ListAvailable: SwiftlyCommand {
3536
))
3637
var toolchainSelector: String?
3738

38-
private enum CodingKeys: String, CodingKey {
39-
case toolchainSelector
40-
}
39+
@Option(name: .long, help: "Output format (text, json)")
40+
var format: SwiftlyCore.OutputFormat = .text
4141

4242
mutating func run() async throws {
43-
try await self.run(Swiftly.createDefaultContext())
43+
try await self.run(Swiftly.createDefaultContext(format: self.format))
4444
}
4545

4646
mutating func run(_ ctx: SwiftlyCoreContext) async throws {
@@ -76,48 +76,17 @@ struct ListAvailable: SwiftlyCommand {
7676
let installedToolchains = Set(config.listInstalledToolchains(selector: selector))
7777
let (inUse, _) = try await selectToolchain(ctx, config: &config)
7878

79-
let printToolchain = { (toolchain: ToolchainVersion) in
80-
var message = "\(toolchain)"
81-
if installedToolchains.contains(toolchain) {
82-
message += " (installed)"
83-
}
84-
if let inUse, toolchain == inUse {
85-
message += " (in use)"
86-
}
87-
if toolchain == config.inUse {
88-
message += " (default)"
89-
}
90-
await ctx.message(message)
91-
}
92-
93-
if let selector {
94-
let modifier = switch selector {
95-
case let .stable(major, minor, nil):
96-
if let minor {
97-
"Swift \(major).\(minor) release"
98-
} else {
99-
"Swift \(major) release"
100-
}
101-
case .snapshot(.main, nil):
102-
"main development snapshot"
103-
case let .snapshot(.release(major, minor), nil):
104-
"\(major).\(minor) development snapshot"
105-
default:
106-
"matching"
107-
}
79+
let filteredToolchains = selector == nil ? toolchains.filter { $0.isStableRelease() } : toolchains
10880

109-
let message = "Available \(modifier) toolchains"
110-
await ctx.message(message)
111-
await ctx.message(String(repeating: "-", count: message.count))
112-
for toolchain in toolchains {
113-
await printToolchain(toolchain)
114-
}
115-
} else {
116-
await ctx.message("Available release toolchains")
117-
await ctx.message("----------------------------")
118-
for toolchain in toolchains where toolchain.isStableRelease() {
119-
await printToolchain(toolchain)
120-
}
81+
let availableToolchainInfos = filteredToolchains.compactMap { toolchain -> AvailableToolchainInfo? in
82+
AvailableToolchainInfo(
83+
version: toolchain,
84+
inUse: inUse == toolchain,
85+
isDefault: toolchain == config.inUse,
86+
installed: installedToolchains.contains(toolchain)
87+
)
12188
}
89+
90+
try await ctx.output(AvailableToolchainsListInfo(toolchains: availableToolchainInfos, selector: selector))
12291
}
12392
}

Sources/Swiftly/OutputSchema.swift

Lines changed: 121 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,10 @@ struct ToolchainSetInfo: OutputData {
3333
let versionFile: String?
3434

3535
var description: String {
36-
var message = self.isGlobal ? "The global default toolchain has been set to `\(self.version)`" : "The file `\(self.versionFile ?? ".swift-version")` has been set to `\(self.version)`"
36+
var message =
37+
self.isGlobal
38+
? "The global default toolchain has been set to `\(self.version)`"
39+
: "The file `\(self.versionFile ?? ".swift-version")` has been set to `\(self.version)`"
3740
if let previousVersion = previousVersion {
3841
message += " (was \(previousVersion.name))"
3942
}
@@ -55,3 +58,120 @@ enum ToolchainSource: Codable, CustomStringConvertible {
5558
}
5659
}
5760
}
61+
62+
struct AvailableToolchainInfo: OutputData {
63+
let version: ToolchainVersion
64+
let inUse: Bool
65+
let isDefault: Bool
66+
let installed: Bool
67+
68+
var description: String {
69+
var message = "\(version)"
70+
if self.installed {
71+
message += " (installed)"
72+
}
73+
if self.inUse {
74+
message += " (in use)"
75+
}
76+
if self.isDefault {
77+
message += " (default)"
78+
}
79+
return message
80+
}
81+
82+
private enum CodingKeys: String, CodingKey {
83+
case version
84+
case inUse
85+
case `default`
86+
case installed
87+
}
88+
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+
99+
public func encode(to encoder: Encoder) throws {
100+
var container = encoder.container(keyedBy: CodingKeys.self)
101+
try container.encode(self.inUse, forKey: .inUse)
102+
try container.encode(self.isDefault, forKey: .default)
103+
try container.encode(self.installed, forKey: .installed)
104+
105+
// Encode the version as a object
106+
var versionContainer = container.nestedContainer(
107+
keyedBy: ToolchainVersionCodingKeys.self, forKey: .version
108+
)
109+
try versionContainer.encode(self.version.name, forKey: .name)
110+
111+
switch self.version {
112+
case let .stable(release):
113+
try versionContainer.encode("stable", forKey: .type)
114+
try versionContainer.encode(release.major, forKey: .major)
115+
try versionContainer.encode(release.minor, forKey: .minor)
116+
try versionContainer.encode(release.patch, forKey: .patch)
117+
case let .snapshot(snapshot):
118+
try versionContainer.encode("snapshot", forKey: .type)
119+
try versionContainer.encode(snapshot.date, forKey: .date)
120+
try versionContainer.encode(snapshot.branch.name, forKey: .branch)
121+
122+
if let major = snapshot.branch.major,
123+
let minor = snapshot.branch.minor
124+
{
125+
try versionContainer.encode(major, forKey: .major)
126+
try versionContainer.encode(minor, forKey: .minor)
127+
}
128+
}
129+
}
130+
}
131+
132+
struct AvailableToolchainsListInfo: OutputData {
133+
let toolchains: [AvailableToolchainInfo]
134+
let selector: ToolchainSelector?
135+
136+
init(toolchains: [AvailableToolchainInfo], selector: ToolchainSelector? = nil) {
137+
self.toolchains = toolchains
138+
self.selector = selector
139+
}
140+
141+
private enum CodingKeys: String, CodingKey {
142+
case toolchains
143+
}
144+
145+
var description: String {
146+
var lines: [String] = []
147+
148+
if let selector = selector {
149+
let modifier =
150+
switch selector
151+
{
152+
case let .stable(major, minor, nil):
153+
if let minor {
154+
"Swift \(major).\(minor) release"
155+
} else {
156+
"Swift \(major) release"
157+
}
158+
case .snapshot(.main, nil):
159+
"main development snapshot"
160+
case let .snapshot(.release(major, minor), nil):
161+
"\(major).\(minor) development snapshot"
162+
default:
163+
"matching"
164+
}
165+
166+
let header = "Available \(modifier) toolchains"
167+
lines.append(header)
168+
lines.append(String(repeating: "-", count: header.count))
169+
} else {
170+
lines.append("Available release toolchains")
171+
lines.append("----------------------------")
172+
}
173+
174+
lines.append(contentsOf: self.toolchains.map(\.description))
175+
return lines.joined(separator: "\n")
176+
}
177+
}

Sources/Swiftly/Use.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ struct Use: SwiftlyCommand {
8888

8989
if self.printLocation {
9090
let location = LocationInfo(path: "\(Swiftly.currentPlatform.findToolchainLocation(ctx, selectedVersion))")
91-
await ctx.output(location)
91+
try await ctx.output(location)
9292
return
9393
}
9494

@@ -100,7 +100,7 @@ struct Use: SwiftlyCommand {
100100
}
101101

102102
let toolchainInfo = ToolchainInfo(version: selectedVersion, source: source)
103-
await ctx.output(toolchainInfo)
103+
try await ctx.output(toolchainInfo)
104104

105105
return
106106
}
@@ -150,7 +150,7 @@ struct Use: SwiftlyCommand {
150150
configFile = nil
151151
}
152152

153-
await ctx.output(ToolchainSetInfo(
153+
try await ctx.output(ToolchainSetInfo(
154154
version: toolchain,
155155
previousVersion: selectedVersion,
156156
isGlobal: isGlobal,

Sources/SwiftlyCore/OutputFormatter.swift

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,10 @@ public enum OutputFormat: String, Sendable, CaseIterable, ExpressibleByArgument
1111
}
1212

1313
public protocol OutputFormatter {
14-
func format(_ data: OutputData) -> String
14+
func format(_ data: OutputData) throws -> String
1515
}
1616

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

@@ -26,19 +26,21 @@ public struct TextOutputFormatter: OutputFormatter {
2626
}
2727
}
2828

29+
public enum OutputFormatterError: Error {
30+
case encodingError(String)
31+
}
32+
2933
public struct JSONOutputFormatter: OutputFormatter {
3034
public init() {}
3135

32-
public func format(_ data: OutputData) -> String {
36+
public func format(_ data: OutputData) throws -> String {
3337
let encoder = JSONEncoder()
3438
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
3539

36-
let jsonData = try? encoder.encode(data)
37-
38-
guard let jsonData = jsonData, let result = String(data: jsonData, encoding: .utf8) else {
39-
return "{}"
40+
let jsonData = try encoder.encode(data)
41+
guard let result = String(data: jsonData, encoding: .utf8) else {
42+
throw OutputFormatterError.encodingError("Failed to encode JSON data as a string in UTF-8.")
4043
}
41-
4244
return result
4345
}
4446
}

Sources/SwiftlyCore/SwiftlyCore.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -98,13 +98,13 @@ public struct SwiftlyCoreContext: Sendable {
9898
}
9999
}
100100

101-
public func output(_ data: OutputData) async {
101+
public func output(_ data: OutputData) async throws {
102102
let formattedOutput: String
103103
switch self.format {
104104
case .text:
105105
formattedOutput = TextOutputFormatter().format(data)
106106
case .json:
107-
formattedOutput = JSONOutputFormatter().format(data)
107+
formattedOutput = try JSONOutputFormatter().format(data)
108108
}
109109
await self.print(formattedOutput)
110110
}

0 commit comments

Comments
 (0)