Skip to content

Commit d996fe5

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

File tree

6 files changed

+293
-51
lines changed

6 files changed

+293
-51
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+
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/SwiftlyCore/OutputFormatter.swift

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,22 @@ public struct JSONOutputFormatter: OutputFormatter {
3333
let encoder = JSONEncoder()
3434
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
3535

36-
let jsonData = try? encoder.encode(data)
37-
38-
guard let jsonData = jsonData, let result = String(data: jsonData, encoding: .utf8) else {
36+
do {
37+
let jsonData = try encoder.encode(data)
38+
guard let result = String(data: jsonData, encoding: .utf8) else {
39+
Self.logError("Failed to convert JSON data to UTF-8 string")
40+
return "{}"
41+
}
42+
return result
43+
} catch {
44+
Self.logError("JSON encoding failed: \(error)")
3945
return "{}"
4046
}
47+
}
4148

42-
return result
49+
private static func logError(_ message: String) {
50+
if let data = "swiftly: JSON formatting error: \(message)\n".data(using: .utf8) {
51+
try? FileHandle.standardError.write(contentsOf: data)
52+
}
4353
}
4454
}

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

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import Foundation
2+
@testable import Swiftly
3+
@testable import SwiftlyCore
4+
import Testing
5+
6+
@Suite struct OutputSchemaTests {
7+
// MARK: - Test Data Setup
8+
9+
static let testStableToolchain = ToolchainVersion.stable(.init(major: 5, minor: 8, patch: 1))
10+
static let testMainSnapshot = ToolchainVersion.snapshot(.init(branch: .main, date: "2023-07-15"))
11+
static let testReleaseSnapshot = ToolchainVersion.snapshot(.init(branch: .release(major: 5, minor: 9), date: "2023-07-10"))
12+
13+
static let testStableVersionInfo = ToolchainVersion.stable(.init(major: 5, minor: 8, patch: 1))
14+
15+
static let testMainSnapshotVersionInfo = ToolchainVersion.snapshot(.init(branch: .main, date: "2023-07-15"))
16+
17+
static let testReleaseSnapshotVersionInfo = ToolchainVersion.snapshot(.init(branch: .release(major: 5, minor: 9), date: "2023-07-10"))
18+
19+
// MARK: - ToolchainVersion Tests
20+
21+
@Test func toolchainVersionName() async throws {
22+
#expect(Self.testStableVersionInfo.name == "5.8.1")
23+
#expect(Self.testMainSnapshotVersionInfo.name == "main-snapshot-2023-07-15")
24+
#expect(Self.testReleaseSnapshotVersionInfo.name == "5.9-snapshot-2023-07-10")
25+
}
26+
27+
// MARK: - AvailableToolchainInfo Tests
28+
29+
@Test func availableToolchainInfoDescription() async throws {
30+
let basicToolchain = AvailableToolchainInfo(
31+
version: Self.testStableVersionInfo,
32+
inUse: false,
33+
default: false,
34+
installed: false
35+
)
36+
#expect(basicToolchain.description == "5.8.1")
37+
38+
let installedToolchain = AvailableToolchainInfo(
39+
version: Self.testStableVersionInfo,
40+
inUse: false,
41+
default: false,
42+
installed: true
43+
)
44+
#expect(installedToolchain.description == "5.8.1 (installed)")
45+
46+
let inUseToolchain = AvailableToolchainInfo(
47+
version: Self.testStableVersionInfo,
48+
inUse: true,
49+
default: false,
50+
installed: true
51+
)
52+
#expect(inUseToolchain.description == "5.8.1 (installed) (in use)")
53+
54+
let defaultToolchain = AvailableToolchainInfo(
55+
version: Self.testStableVersionInfo,
56+
inUse: true,
57+
default: true,
58+
installed: true
59+
)
60+
#expect(defaultToolchain.description == "5.8.1 (installed) (in use) (default)")
61+
62+
let defaultOnlyToolchain = AvailableToolchainInfo(
63+
version: Self.testStableVersionInfo,
64+
inUse: false,
65+
default: true,
66+
installed: false
67+
)
68+
#expect(defaultOnlyToolchain.description == "5.8.1 (default)")
69+
}
70+
71+
@Test func availableToolchainsListInfoDescriptionNoSelector() async throws {
72+
let toolchains = [
73+
AvailableToolchainInfo(
74+
version: Self.testStableVersionInfo,
75+
inUse: true,
76+
default: true,
77+
installed: true
78+
),
79+
AvailableToolchainInfo(
80+
version: Self.testMainSnapshotVersionInfo,
81+
inUse: false,
82+
default: false,
83+
installed: false
84+
),
85+
]
86+
87+
let listInfo = AvailableToolchainsListInfo(toolchains: toolchains)
88+
let description = listInfo.description
89+
90+
#expect(description.contains("Available release toolchains"))
91+
#expect(description.contains("----------------------------"))
92+
#expect(description.contains("5.8.1 (installed) (in use) (default)"))
93+
#expect(description.contains("main-snapshot-2023-07-15"))
94+
}
95+
96+
@Test func availableToolchainsListInfoDescriptionWithStableSelector() async throws {
97+
let toolchains = [
98+
AvailableToolchainInfo(
99+
version: Self.testStableVersionInfo,
100+
inUse: true,
101+
default: true,
102+
installed: true
103+
),
104+
]
105+
106+
let selector = ToolchainSelector.stable(major: 5, minor: 8, patch: nil)
107+
let listInfo = AvailableToolchainsListInfo(toolchains: toolchains, selector: selector)
108+
let description = listInfo.description
109+
110+
#expect(description.contains("Available Swift 5.8 release toolchains"))
111+
#expect(description.contains("5.8.1 (installed) (in use) (default)"))
112+
}
113+
114+
@Test func availableToolchainsListInfoDescriptionWithMajorOnlySelector() async throws {
115+
let majorOnlySelector = ToolchainSelector.stable(major: 5, minor: nil, patch: nil)
116+
let majorOnlyListInfo = AvailableToolchainsListInfo(
117+
toolchains: [AvailableToolchainInfo(
118+
version: Self.testStableVersionInfo,
119+
inUse: false,
120+
default: false,
121+
installed: true
122+
)],
123+
selector: majorOnlySelector
124+
)
125+
#expect(majorOnlyListInfo.description.contains("Available Swift 5 release toolchains"))
126+
#expect(majorOnlyListInfo.description.contains("5.8.1 (installed)"))
127+
}
128+
129+
@Test func availableToolchainsListInfoDescriptionWithMainSnapshotSelector() async throws {
130+
let mainSnapshotSelector = ToolchainSelector.snapshot(branch: .main, date: nil)
131+
let mainSnapshotListInfo = AvailableToolchainsListInfo(
132+
toolchains: [AvailableToolchainInfo(
133+
version: Self.testMainSnapshotVersionInfo,
134+
inUse: false,
135+
default: false,
136+
installed: false
137+
)],
138+
selector: mainSnapshotSelector
139+
)
140+
#expect(mainSnapshotListInfo.description.contains("Available main development snapshot toolchains"))
141+
#expect(mainSnapshotListInfo.description.contains("main-snapshot-2023-07-15"))
142+
}
143+
144+
@Test func availableToolchainsListInfoDescriptionWithReleaseSnapshotSelector() async throws {
145+
let releaseSnapshotSelector = ToolchainSelector.snapshot(branch: .release(major: 5, minor: 9), date: nil)
146+
let releaseSnapshotListInfo = AvailableToolchainsListInfo(
147+
toolchains: [AvailableToolchainInfo(
148+
version: Self.testReleaseSnapshotVersionInfo,
149+
inUse: false,
150+
default: false,
151+
installed: true
152+
)],
153+
selector: releaseSnapshotSelector
154+
)
155+
#expect(releaseSnapshotListInfo.description.contains("Available 5.9 development snapshot toolchains"))
156+
#expect(releaseSnapshotListInfo.description.contains("5.9-snapshot-2023-07-10 (installed)"))
157+
}
158+
159+
@Test func availableToolchainsListInfoDescriptionWithSpecificVersionSelector() async throws {
160+
let specificSelector = ToolchainSelector.stable(major: 5, minor: 8, patch: 1)
161+
let specificListInfo = AvailableToolchainsListInfo(
162+
toolchains: [AvailableToolchainInfo(
163+
version: Self.testStableVersionInfo,
164+
inUse: false,
165+
default: false,
166+
installed: false
167+
)],
168+
selector: specificSelector
169+
)
170+
#expect(specificListInfo.description.contains("Available matching toolchains"))
171+
#expect(specificListInfo.description.contains("5.8.1"))
172+
}
173+
174+
@Test func availableToolchainsListInfoEmptyToolchains() async throws {
175+
let listInfo = AvailableToolchainsListInfo(toolchains: [])
176+
let description = listInfo.description
177+
178+
#expect(description.contains("Available release toolchains"))
179+
#expect(description.contains("----------------------------"))
180+
// Should not contain any toolchain entries
181+
#expect(!description.contains("5.8.1"))
182+
#expect(!description.contains("snapshot"))
183+
}
184+
}

Tests/SwiftlyTests/SwiftlyCoreTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import Testing
77
actor TestOutputCapture: OutputHandler {
88
private(set) var outputLines: [String] = []
99

10-
func handleOutputLine(_ string: String) {
10+
func handleOutputLine(_ string: String) async {
1111
self.outputLines.append(string)
1212
}
1313

0 commit comments

Comments
 (0)