Skip to content

Commit 6d85742

Browse files
authored
json output install (#392)
* Json Progress File * Improve JSON progress file handling and error reporting * Refactor progress file parsing and improve test validation * Refactored JSON Output for Install Command
1 parent 06bc674 commit 6d85742

File tree

4 files changed

+86
-10
lines changed

4 files changed

+86
-10
lines changed

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ swiftly [--version] [--help]
2323
Install a new toolchain.
2424

2525
```
26-
swiftly install [<version>] [--use] [--verify|no-verify] [--post-install-file=<post-install-file>] [--progress-file=<progress-file>] [--assume-yes] [--verbose] [--version] [--help]
26+
swiftly install [<version>] [--use] [--verify|no-verify] [--post-install-file=<post-install-file>] [--progress-file=<progress-file>] [--format=<format>] [--assume-yes] [--verbose] [--version] [--help]
2727
```
2828

2929
**version:**
@@ -89,6 +89,11 @@ Each progress entry contains timestamp, progress percentage, and a descriptive m
8989
The file must be writable, else an error will be thrown.
9090

9191

92+
**--format=\<format\>:**
93+
94+
*Output format (text, json)*
95+
96+
9297
**--assume-yes:**
9398

9499
*Disable confirmation prompts by assuming 'yes'*

Sources/Swiftly/Install.swift

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -81,14 +81,17 @@ struct Install: SwiftlyCommand {
8181
))
8282
var progressFile: FilePath?
8383

84+
@Option(name: .long, help: "Output format (text, json)")
85+
var format: SwiftlyCore.OutputFormat = .text
86+
8487
@OptionGroup var root: GlobalOptions
8588

8689
private enum CodingKeys: String, CodingKey {
87-
case version, use, verify, postInstallFile, root, progressFile
90+
case version, use, verify, postInstallFile, root, progressFile, format
8891
}
8992

9093
mutating func run() async throws {
91-
try await self.run(Swiftly.createDefaultContext())
94+
try await self.run(Swiftly.createDefaultContext(format: self.format))
9295
}
9396

9497
private func swiftlyHomeDir(_ ctx: SwiftlyCoreContext) -> FilePath {
@@ -266,7 +269,10 @@ struct Install: SwiftlyCommand {
266269
progressFile: FilePath? = nil
267270
) async throws -> (postInstall: String?, pathChanged: Bool) {
268271
guard !config.installedToolchains.contains(version) else {
269-
await ctx.message("\(version) is already installed.")
272+
let installInfo = InstallInfo(
273+
version: version, alreadyInstalled: true
274+
)
275+
try await ctx.output(installInfo)
270276
return (nil, false)
271277
}
272278

@@ -312,16 +318,18 @@ struct Install: SwiftlyCommand {
312318
}
313319
}
314320

315-
let animation: ProgressReporterProtocol =
321+
let animation: ProgressReporterProtocol? =
316322
if let progressFile
317323
{
318324
try JsonFileProgressReporter(ctx, filePath: progressFile)
325+
} else if ctx.format == .json {
326+
ConsoleProgressReporter(stream: stderrStream, header: "Downloading \(version)")
319327
} else {
320328
ConsoleProgressReporter(stream: stdoutStream, header: "Downloading \(version)")
321329
}
322330

323331
defer {
324-
try? animation.close()
332+
try? animation?.close()
325333
}
326334

327335
var lastUpdate = Date()
@@ -351,7 +359,7 @@ struct Install: SwiftlyCommand {
351359
lastUpdate = Date()
352360

353361
do {
354-
try await animation.update(
362+
try await animation?.update(
355363
step: progress.receivedBytes,
356364
total: progress.totalBytes!,
357365
text:
@@ -368,10 +376,10 @@ struct Install: SwiftlyCommand {
368376
throw SwiftlyError(
369377
message: "\(version) does not exist at URL \(notFound.url), exiting")
370378
} catch {
371-
try? await animation.complete(success: false)
379+
try? await animation?.complete(success: false)
372380
throw error
373381
}
374-
try await animation.complete(success: true)
382+
try await animation?.complete(success: true)
375383

376384
if verifySignature {
377385
try await Swiftly.currentPlatform.verifyToolchainSignature(
@@ -427,7 +435,11 @@ struct Install: SwiftlyCommand {
427435
return (pathChanged, config)
428436
}
429437
config = newConfig
430-
await ctx.message("\(version) installed successfully!")
438+
let installInfo = InstallInfo(
439+
version: version,
440+
alreadyInstalled: false
441+
)
442+
try await ctx.output(installInfo)
431443
return (postInstallScript, pathChanged)
432444
}
433445
}

Sources/Swiftly/OutputSchema.swift

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,3 +339,28 @@ struct InstalledToolchainsListInfo: OutputData {
339339
return lines.joined(separator: "\n")
340340
}
341341
}
342+
343+
struct InstallInfo: OutputData {
344+
let version: ToolchainVersion
345+
let alreadyInstalled: Bool
346+
347+
init(version: ToolchainVersion, alreadyInstalled: Bool) {
348+
self.version = version
349+
self.alreadyInstalled = alreadyInstalled
350+
}
351+
352+
var description: String {
353+
"\(self.version) is \(self.alreadyInstalled ? "already installed" : "installed successfully!")"
354+
}
355+
356+
private enum CodingKeys: String, CodingKey {
357+
case version
358+
case alreadyInstalled
359+
}
360+
361+
public func encode(to encoder: Encoder) throws {
362+
var container = encoder.container(keyedBy: CodingKeys.self)
363+
try container.encode(self.version.name, forKey: .version)
364+
try container.encode(self.alreadyInstalled, forKey: .alreadyInstalled)
365+
}
366+
}

Tests/SwiftlyTests/InstallTests.swift

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,4 +389,38 @@ import Testing
389389
}
390390
}
391391
#endif
392+
393+
/// Tests that `install` command with JSON format outputs correctly structured JSON.
394+
@Test(.testHomeMockedToolchain()) func installJsonFormat() async throws {
395+
let output = try await SwiftlyTests.runWithMockedIO(
396+
Install.self, ["install", "5.7.0", "--post-install-file=\(fs.mktemp())", "--format", "json"], format: .json
397+
)
398+
399+
let installInfo = try JSONDecoder().decode(
400+
InstallInfo.self,
401+
from: output[0].data(using: .utf8)!
402+
)
403+
404+
#expect(installInfo.version.name == "5.7.0")
405+
#expect(installInfo.alreadyInstalled == false)
406+
}
407+
408+
/// Tests that `install` command with JSON format correctly indicates when toolchain is already installed.
409+
@Test(.testHomeMockedToolchain()) func installJsonFormatAlreadyInstalled() async throws {
410+
// First install the toolchain
411+
try await SwiftlyTests.runCommand(Install.self, ["install", "5.7.0", "--post-install-file=\(fs.mktemp())"])
412+
413+
// Then try to install it again with JSON format
414+
let output = try await SwiftlyTests.runWithMockedIO(
415+
Install.self, ["install", "5.7.0", "--post-install-file=\(fs.mktemp())", "--format", "json"], format: .json
416+
)
417+
418+
let installInfo = try JSONDecoder().decode(
419+
InstallInfo.self,
420+
from: output[0].data(using: .utf8)!
421+
)
422+
423+
#expect(installInfo.version.name == "5.7.0")
424+
#expect(installInfo.alreadyInstalled == true)
425+
}
392426
}

0 commit comments

Comments
 (0)