Skip to content

Commit 777e46b

Browse files
committed
Refactor list-available command to support json format
1 parent cbc8bdc commit 777e46b

File tree

8 files changed

+293
-59
lines changed

8 files changed

+293
-59
lines changed

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)
79+
let availableToolchainInfos = toolchains.compactMap { toolchain -> AvailableToolchainInfo? in
80+
AvailableToolchainInfo(
81+
version: toolchain,
82+
inUse: inUse == toolchain,
83+
default: toolchain == config.inUse,
84+
installed: installedToolchains.contains(toolchain)
85+
)
9186
}
9287

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-
}
108-
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-
}
121-
}
88+
let filteredToolchains = selector != nil ? availableToolchainInfos : availableToolchainInfos.filter { $0.version.isStableRelease() }
89+
let listInfo = AvailableToolchainsListInfo(toolchains: filteredToolchains, selector: selector)
90+
try await ctx.output(listInfo)
12291
}
12392
}

Sources/Swiftly/OutputSchema.swift

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,82 @@ enum ToolchainSource: Codable, CustomStringConvertible {
5555
}
5656
}
5757
}
58+
59+
struct AvailableToolchainInfo: OutputData {
60+
let version: ToolchainVersion
61+
let inUse: Bool
62+
let `default`: Bool
63+
let installed: Bool
64+
65+
init(version: ToolchainVersion, inUse: Bool, default: Bool, installed: Bool) {
66+
self.version = version
67+
self.inUse = inUse
68+
self.default = `default`
69+
self.installed = installed
70+
}
71+
72+
var description: String {
73+
var message = "\(version.name)"
74+
if self.installed {
75+
message += " (installed)"
76+
}
77+
if self.inUse {
78+
message += " (in use)"
79+
}
80+
if self.default {
81+
message += " (default)"
82+
}
83+
return message
84+
}
85+
}
86+
87+
struct AvailableToolchainsListInfo: OutputData {
88+
let toolchains: [AvailableToolchainInfo]
89+
let selector: ToolchainSelector?
90+
91+
init(toolchains: [AvailableToolchainInfo], selector: ToolchainSelector? = nil) {
92+
self.toolchains = toolchains
93+
self.selector = selector
94+
}
95+
96+
private enum CodingKeys: String, CodingKey {
97+
case toolchains
98+
}
99+
100+
init(from decoder: Decoder) throws {
101+
let container = try decoder.container(keyedBy: CodingKeys.self)
102+
self.toolchains = try container.decode([AvailableToolchainInfo].self, forKey: .toolchains)
103+
self.selector = nil
104+
}
105+
106+
var description: String {
107+
var lines: [String] = []
108+
109+
if let selector = selector {
110+
let modifier = switch selector {
111+
case let .stable(major, minor, nil):
112+
if let minor {
113+
"Swift \(major).\(minor) release"
114+
} else {
115+
"Swift \(major) release"
116+
}
117+
case .snapshot(.main, nil):
118+
"main development snapshot"
119+
case let .snapshot(.release(major, minor), nil):
120+
"\(major).\(minor) development snapshot"
121+
default:
122+
"matching"
123+
}
124+
125+
let header = "Available \(modifier) toolchains"
126+
lines.append(header)
127+
lines.append(String(repeating: "-", count: header.count))
128+
} else {
129+
lines.append("Available release toolchains")
130+
lines.append("----------------------------")
131+
}
132+
133+
lines.append(contentsOf: self.toolchains.map(\.description))
134+
return lines.joined(separator: "\n")
135+
}
136+
}

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: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ 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

1717
public protocol OutputData: Codable, CustomStringConvertible {
@@ -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 convert JSON data to string.")
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
}

Sources/SwiftlyCore/ToolchainVersion.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import _StringProcessing
33
/// Enum representing a fully resolved toolchain version (e.g. 5.6.7 or 5.7-snapshot-2022-07-05).
44
public enum ToolchainVersion: Sendable {
55
public struct Snapshot: Equatable, Hashable, CustomStringConvertible, Comparable, Sendable {
6-
public enum Branch: Equatable, Hashable, CustomStringConvertible, Sendable {
6+
public enum Branch: Equatable, Hashable, CustomStringConvertible, Sendable, Codable {
77
case main
88
case release(major: Int, minor: Int)
99

0 commit comments

Comments
 (0)