Skip to content

Refactor list-available command to support json format #387

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
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 @@ -107,7 +107,7 @@ written to this file as commands that can be run after the installation.
List toolchains available for install.

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

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


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

*Output format (text, json)*


**--version:**

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

struct ListAvailable: SwiftlyCommand {
Expand Down Expand Up @@ -35,12 +36,11 @@ struct ListAvailable: SwiftlyCommand {
))
var toolchainSelector: String?

private enum CodingKeys: String, CodingKey {
case toolchainSelector
}
@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 Down Expand Up @@ -76,48 +76,17 @@ struct ListAvailable: SwiftlyCommand {
let installedToolchains = Set(config.listInstalledToolchains(selector: selector))
let (inUse, _) = try await selectToolchain(ctx, config: &config)

let printToolchain = { (toolchain: ToolchainVersion) in
var message = "\(toolchain)"
if installedToolchains.contains(toolchain) {
message += " (installed)"
}
if let inUse, toolchain == inUse {
message += " (in use)"
}
if toolchain == config.inUse {
message += " (default)"
}
await ctx.message(message)
}

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 filteredToolchains = selector == nil ? toolchains.filter { $0.isStableRelease() } : toolchains

let message = "Available \(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("Available release toolchains")
await ctx.message("----------------------------")
for toolchain in toolchains where toolchain.isStableRelease() {
await printToolchain(toolchain)
}
let availableToolchainInfos = filteredToolchains.compactMap { toolchain -> AvailableToolchainInfo? in
AvailableToolchainInfo(
version: toolchain,
inUse: inUse == toolchain,
isDefault: toolchain == config.inUse,
installed: installedToolchains.contains(toolchain)
)
}

try await ctx.output(AvailableToolchainsListInfo(toolchains: availableToolchainInfos, selector: selector))
}
}
122 changes: 121 additions & 1 deletion Sources/Swiftly/OutputSchema.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ struct ToolchainSetInfo: OutputData {
let versionFile: String?

var description: String {
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)`"
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)`"
if let previousVersion = previousVersion {
message += " (was \(previousVersion.name))"
}
Expand All @@ -55,3 +58,120 @@ enum ToolchainSource: Codable, CustomStringConvertible {
}
}
}

struct AvailableToolchainInfo: OutputData {
let version: ToolchainVersion
let inUse: Bool
let isDefault: Bool
let installed: Bool

var description: String {
var message = "\(version)"
if self.installed {
message += " (installed)"
}
if self.inUse {
message += " (in use)"
}
if self.isDefault {
message += " (default)"
}
return message
}

private enum CodingKeys: String, CodingKey {
case version
case inUse
case `default`
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.installed, forKey: .installed)

// 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)
}
}
}
}

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

init(toolchains: [AvailableToolchainInfo], selector: ToolchainSelector? = nil) {
self.toolchains = toolchains
self.selector = selector
}

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 = "Available \(modifier) toolchains"
lines.append(header)
lines.append(String(repeating: "-", count: header.count))
} else {
lines.append("Available release toolchains")
lines.append("----------------------------")
}

lines.append(contentsOf: self.toolchains.map(\.description))
return lines.joined(separator: "\n")
}
}
6 changes: 3 additions & 3 deletions Sources/Swiftly/Use.swift
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ struct Use: SwiftlyCommand {

if self.printLocation {
let location = LocationInfo(path: "\(Swiftly.currentPlatform.findToolchainLocation(ctx, selectedVersion))")
await ctx.output(location)
try await ctx.output(location)
return
}

Expand All @@ -100,7 +100,7 @@ struct Use: SwiftlyCommand {
}

let toolchainInfo = ToolchainInfo(version: selectedVersion, source: source)
await ctx.output(toolchainInfo)
try await ctx.output(toolchainInfo)

return
}
Expand Down Expand Up @@ -150,7 +150,7 @@ struct Use: SwiftlyCommand {
configFile = nil
}

await ctx.output(ToolchainSetInfo(
try await ctx.output(ToolchainSetInfo(
version: toolchain,
previousVersion: selectedVersion,
isGlobal: isGlobal,
Expand Down
18 changes: 10 additions & 8 deletions Sources/SwiftlyCore/OutputFormatter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ public enum OutputFormat: String, Sendable, CaseIterable, ExpressibleByArgument
}

public protocol OutputFormatter {
func format(_ data: OutputData) -> String
func format(_ data: OutputData) throws -> String
}

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

Expand All @@ -26,19 +26,21 @@ public struct TextOutputFormatter: OutputFormatter {
}
}

public enum OutputFormatterError: Error {
case encodingError(String)
}

public struct JSONOutputFormatter: OutputFormatter {
public init() {}

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

let jsonData = try? encoder.encode(data)

guard let jsonData = jsonData, let result = String(data: jsonData, encoding: .utf8) else {
return "{}"
let jsonData = try encoder.encode(data)
guard let result = String(data: jsonData, encoding: .utf8) else {
throw OutputFormatterError.encodingError("Failed to encode JSON data as a string in UTF-8.")
}

return result
}
}
4 changes: 2 additions & 2 deletions Sources/SwiftlyCore/SwiftlyCore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -98,13 +98,13 @@ public struct SwiftlyCoreContext: Sendable {
}
}

public func output(_ data: OutputData) async {
public func output(_ data: OutputData) async throws {
let formattedOutput: String
switch self.format {
case .text:
formattedOutput = TextOutputFormatter().format(data)
case .json:
formattedOutput = JSONOutputFormatter().format(data)
formattedOutput = try JSONOutputFormatter().format(data)
}
await self.print(formattedOutput)
}
Expand Down
Loading