Skip to content

Refactor list command to support json format #388

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 20, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion Documentation/SwiftlyDocs.docc/swiftly-cli-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ Finally, all installed toolchains can be uninstalled by specifying 'all':
List installed toolchains.

```
swiftly list [<toolchain-selector>] [--version] [--help]
swiftly list [<toolchain-selector>] [--format=<format>] [--version] [--help]
```

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


**--format=\<format\>:**

*Output format (text, json)*


**--version:**

*Show the version.*
Expand Down
61 changes: 12 additions & 49 deletions Sources/Swiftly/List.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import ArgumentParser
import Foundation
import SwiftlyCore

struct List: SwiftlyCommand {
Expand Down Expand Up @@ -33,8 +34,11 @@ struct List: SwiftlyCommand {
))
var toolchainSelector: String?

@Option(name: .long, help: "Output format (text, json)")
var format: SwiftlyCore.OutputFormat = .text

mutating func run() async throws {
try await self.run(Swiftly.createDefaultContext())
try await self.run(Swiftly.createDefaultContext(format: self.format))
}

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

let printToolchain = { (toolchain: ToolchainVersion) in
var message = "\(toolchain)"
if let inUse, toolchain == inUse {
message += " (in use)"
}
if toolchain == config.inUse {
message += " (default)"
}
await ctx.message(message)
let installedToolchainInfos = toolchains.compactMap { toolchain -> InstallToolchainInfo? in
InstallToolchainInfo(
version: toolchain,
inUse: inUse == toolchain,
isDefault: toolchain == config.inUse
)
}

if let selector {
let modifier = switch selector {
case let .stable(major, minor, nil):
if let minor {
"Swift \(major).\(minor) release"
} else {
"Swift \(major) release"
}
case .snapshot(.main, nil):
"main development snapshot"
case let .snapshot(.release(major, minor), nil):
"\(major).\(minor) development snapshot"
default:
"matching"
}

let message = "Installed \(modifier) toolchains"
await ctx.message(message)
await ctx.message(String(repeating: "-", count: message.count))
for toolchain in toolchains {
await printToolchain(toolchain)
}
} else {
await ctx.message("Installed release toolchains")
await ctx.message("----------------------------")
for toolchain in toolchains {
guard toolchain.isStableRelease() else {
continue
}
await printToolchain(toolchain)
}

await ctx.message("")
await ctx.message("Installed snapshot toolchains")
await ctx.message("-----------------------------")
for toolchain in toolchains where toolchain.isSnapshot() {
await printToolchain(toolchain)
}
}
try await ctx.output(InstalledToolchainsListInfo(toolchains: installedToolchainInfos, selector: selector))
}
}
190 changes: 177 additions & 13 deletions Sources/Swiftly/OutputSchema.swift
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,16 @@ enum ToolchainSource: Codable, CustomStringConvertible {
}
}

private enum ToolchainVersionCodingKeys: String, CodingKey {
case name
case type
case branch
case major
case minor
case patch
case date
}

struct AvailableToolchainInfo: OutputData {
let version: ToolchainVersion
let inUse: Bool
Expand All @@ -82,24 +92,14 @@ struct AvailableToolchainInfo: OutputData {
private enum CodingKeys: String, CodingKey {
case version
case inUse
case `default`
case isDefault
case installed
}

private enum ToolchainVersionCodingKeys: String, CodingKey {
case name
case type
case branch
case major
case minor
case patch
case date
}

public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.inUse, forKey: .inUse)
try container.encode(self.isDefault, forKey: .default)
try container.encode(self.isDefault, forKey: .isDefault)
try container.encode(self.installed, forKey: .installed)

// Encode the version as a object
Expand Down Expand Up @@ -131,7 +131,7 @@ struct AvailableToolchainInfo: OutputData {

struct AvailableToolchainsListInfo: OutputData {
let toolchains: [AvailableToolchainInfo]
let selector: ToolchainSelector?
var selector: ToolchainSelector?

init(toolchains: [AvailableToolchainInfo], selector: ToolchainSelector? = nil) {
self.toolchains = toolchains
Expand Down Expand Up @@ -175,3 +175,167 @@ struct AvailableToolchainsListInfo: OutputData {
return lines.joined(separator: "\n")
}
}

struct InstallToolchainInfo: OutputData {
let version: ToolchainVersion
let inUse: Bool
let isDefault: Bool

init(version: ToolchainVersion, inUse: Bool, isDefault: Bool) {
self.version = version
self.inUse = inUse
self.isDefault = isDefault
}

var description: String {
var message = "\(version)"

if self.inUse {
message += " (in use)"
}
if self.isDefault {
message += " (default)"
}
return message
}

private enum CodingKeys: String, CodingKey {
case version
case inUse
case isDefault
}

public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.inUse, forKey: .inUse)
try container.encode(self.isDefault, forKey: .isDefault)

// Encode the version as a object
var versionContainer = container.nestedContainer(
keyedBy: ToolchainVersionCodingKeys.self, forKey: .version
)
try versionContainer.encode(self.version.name, forKey: .name)

switch self.version {
case let .stable(release):
try versionContainer.encode("stable", forKey: .type)
try versionContainer.encode(release.major, forKey: .major)
try versionContainer.encode(release.minor, forKey: .minor)
try versionContainer.encode(release.patch, forKey: .patch)
case let .snapshot(snapshot):
try versionContainer.encode("snapshot", forKey: .type)
try versionContainer.encode(snapshot.date, forKey: .date)
try versionContainer.encode(snapshot.branch.name, forKey: .branch)

if let major = snapshot.branch.major,
let minor = snapshot.branch.minor
{
try versionContainer.encode(major, forKey: .major)
try versionContainer.encode(minor, forKey: .minor)
}
}
}

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.inUse = try container.decode(Bool.self, forKey: .inUse)
self.isDefault = try container.decode(Bool.self, forKey: .isDefault)

// Decode the version as a object
let versionContainer = try container.nestedContainer(
keyedBy: ToolchainVersionCodingKeys.self, forKey: .version
)
let name = try versionContainer.decode(String.self, forKey: .name)

switch try versionContainer.decode(String.self, forKey: .type) {
case "stable":
let major = try versionContainer.decode(Int.self, forKey: .major)
let minor = try versionContainer.decode(Int.self, forKey: .minor)
let patch = try versionContainer.decode(Int.self, forKey: .patch)
self.version = .stable(
ToolchainVersion.StableRelease(major: major, minor: minor, patch: patch))
case "snapshot":
let date = try versionContainer.decode(String.self, forKey: .date)
let branchName = try versionContainer.decode(String.self, forKey: .branch)
let branchMajor = try? versionContainer.decodeIfPresent(Int.self, forKey: .major)
let branchMinor = try? versionContainer.decodeIfPresent(Int.self, forKey: .minor)

// Determine the branch from the decoded data
let branch: ToolchainVersion.Snapshot.Branch
if branchName == "main" {
branch = .main
} else if let major = branchMajor, let minor = branchMinor {
branch = .release(major: major, minor: minor)
} else {
throw DecodingError.dataCorruptedError(
forKey: ToolchainVersionCodingKeys.branch,
in: versionContainer,
debugDescription: "Invalid branch format: \(branchName)"
)
}

self.version = .snapshot(
ToolchainVersion.Snapshot(
branch: branch,
date: date
))
default:
throw DecodingError.dataCorruptedError(
forKey: ToolchainVersionCodingKeys.type,
in: versionContainer,
debugDescription: "Unknown toolchain type"
)
}
}
}

struct InstalledToolchainsListInfo: OutputData {
let toolchains: [InstallToolchainInfo]
var selector: ToolchainSelector?

private enum CodingKeys: String, CodingKey {
case toolchains
}

var description: String {
var lines: [String] = []

if let selector = selector {
let modifier =
switch selector
{
case let .stable(major, minor, nil):
if let minor {
"Swift \(major).\(minor) release"
} else {
"Swift \(major) release"
}
case .snapshot(.main, nil):
"main development snapshot"
case let .snapshot(.release(major, minor), nil):
"\(major).\(minor) development snapshot"
default:
"matching"
}

let header = "Installed \(modifier) toolchains"
lines.append(header)
lines.append(String(repeating: "-", count: header.count))
lines.append(contentsOf: self.toolchains.map(\.description))
} else {
let releaseToolchains = self.toolchains.filter { $0.version.isStableRelease() }
let snapshotToolchains = self.toolchains.filter { $0.version.isSnapshot() }

lines.append("Installed release toolchains")
lines.append("----------------------------")
lines.append(contentsOf: releaseToolchains.map(\.description))

lines.append("")
lines.append("Installed snapshot toolchains")
lines.append("-----------------------------")
lines.append(contentsOf: snapshotToolchains.map(\.description))
}

return lines.joined(separator: "\n")
}
}
2 changes: 1 addition & 1 deletion Sources/SwiftlyCore/OutputFormatter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public protocol OutputFormatter {
func format(_ data: OutputData) throws -> String
}

public protocol OutputData: Encodable, CustomStringConvertible {
public protocol OutputData: CustomStringConvertible, Codable {
var description: String { get }
}

Expand Down
Loading