Skip to content

Commit 3366916

Browse files
authored
Consolidate CLI formatting infrastructure (apple#1385)
## Motivation and Context This PR consolidates duplicated list-output formatting across the CLI into shared rendering infrastructure. Currently, each list command has its own copy of the same json/quiet/table branching. This PR pulls that into shared rendering infrastructure in `ContainerCommands`: - `ListDisplayable` protocol for table + quiet output - `renderJSON`, `renderTable`, `renderList` as pure functions that return strings - `emit()` as the single stdout boundary (no-ops on empty strings to avoid blank-line regressions) - `JSONOptions` so all JSON encoding goes through one path, including volume inspect's pretty + ISO 8601 case JSON encoding remains separate from display formatting: each command still chooses its own JSON model, while `ListDisplayable` is used only for table and quiet output. `ImageList` remains the intentional exception for quiet mode so it can avoid unnecessary async work. This change also replaces inline `JSONEncoder` usage with `renderJSON`, removes the old `Codable+JSON.swift` helper, and moves `TableOutput` and `ListFormat` into `ContainerCommands`. It also adds unit tests for the shared rendering helpers and expands integration coverage for image, network, and registry list formatting. ## Testing - [x] Tested locally - [x] Added/updated tests - [ ] Added/updated docs
1 parent f0e85c4 commit 3366916

26 files changed

Lines changed: 788 additions & 417 deletions

Package.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,13 @@ let package = Package(
122122
"ContainerBuild"
123123
]
124124
),
125+
.testTarget(
126+
name: "ContainerCommandsTests",
127+
dependencies: [
128+
"ContainerCommands",
129+
"ContainerResource",
130+
]
131+
),
125132
.executableTarget(
126133
name: "container-apiserver",
127134
dependencies: [

Sources/ContainerCommands/Application.swift

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -245,11 +245,6 @@ extension Application {
245245
print(altered)
246246
}
247247

248-
public enum ListFormat: String, CaseIterable, ExpressibleByArgument {
249-
case json
250-
case table
251-
}
252-
253248
func isTranslated() throws -> Bool {
254249
do {
255250
return try Sysctl.byName("sysctl.proc_translated") == 1

Sources/ContainerCommands/Builder/BuilderStatus.swift

Lines changed: 33 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -45,60 +45,53 @@ extension Application {
4545
do {
4646
let client = ContainerClient()
4747
let container = try await client.get(id: "buildkit")
48-
try printContainers(containers: [container], format: format)
48+
49+
if format == .json {
50+
try Output.emit(Output.renderJSON([PrintableContainer(container)]))
51+
return
52+
}
53+
54+
if quiet && container.status != .running {
55+
return
56+
}
57+
58+
Output.emit(Output.renderList([PrintableBuilder(container)], quiet: quiet))
4959
} catch {
50-
if error is ContainerizationError {
51-
if (error as? ContainerizationError)?.code == .notFound && !quiet {
60+
if let czError = error as? ContainerizationError, czError.code == .notFound {
61+
if !quiet {
5262
print("builder is not running")
5363
return
5464
}
5565
}
5666
throw error
5767
}
5868
}
69+
}
70+
}
5971

60-
private func createHeader() -> [[String]] {
61-
[["ID", "IMAGE", "STATE", "ADDR", "CPUS", "MEMORY"]]
62-
}
63-
64-
private func printContainers(containers: [ContainerSnapshot], format: ListFormat) throws {
65-
if format == .json {
66-
let printables = containers.map {
67-
PrintableContainer($0)
68-
}
69-
let data = try JSONEncoder().encode(printables)
70-
print(String(decoding: data, as: UTF8.self))
71-
72-
return
73-
}
74-
75-
if self.quiet {
76-
containers
77-
.filter { $0.status == .running }
78-
.forEach { print($0.id) }
79-
return
80-
}
72+
private struct PrintableBuilder: ListDisplayable {
73+
let snapshot: ContainerSnapshot
8174

82-
var rows = createHeader()
83-
for container in containers {
84-
rows.append(container.asRow)
85-
}
75+
init(_ snapshot: ContainerSnapshot) {
76+
self.snapshot = snapshot
77+
}
8678

87-
let formatter = TableOutput(rows: rows)
88-
print(formatter.format())
89-
}
79+
static var tableHeader: [String] {
80+
["ID", "IMAGE", "STATE", "ADDR", "CPUS", "MEMORY"]
9081
}
91-
}
9282

93-
extension ContainerSnapshot {
94-
fileprivate var asRow: [String] {
83+
var tableRow: [String] {
9584
[
96-
self.id,
97-
self.configuration.image.reference,
98-
self.status.rawValue,
99-
self.networks.compactMap { $0.ipv4Address.description }.joined(separator: ","),
100-
"\(self.configuration.resources.cpus)",
101-
"\(self.configuration.resources.memoryInBytes / (1024 * 1024)) MB",
85+
snapshot.id,
86+
snapshot.configuration.image.reference,
87+
snapshot.status.rawValue,
88+
snapshot.networks.map { $0.ipv4Address.description }.joined(separator: ","),
89+
"\(snapshot.configuration.resources.cpus)",
90+
"\(snapshot.configuration.resources.memoryInBytes / (1024 * 1024)) MB",
10291
]
10392
}
93+
94+
var quietValue: String {
95+
snapshot.id
96+
}
10497
}

Sources/ContainerCommands/Container/ContainerInspect.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,12 +36,12 @@ extension Application {
3636

3737
public func run() async throws {
3838
let client = ContainerClient()
39-
let objects: [any Codable] = try await client.list().filter {
39+
let containers = try await client.list().filter {
4040
containerIds.contains($0.id)
4141
}.map {
4242
PrintableContainer($0)
4343
}
44-
print(try objects.jsonArray())
44+
try Output.emit(Output.renderJSON(containers))
4545
}
4646
}
4747
}

Sources/ContainerCommands/Container/ContainerList.swift

Lines changed: 18 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -46,59 +46,37 @@ extension Application {
4646
let client = ContainerClient()
4747
let filters = self.all ? ContainerListFilters.all : ContainerListFilters(status: .running)
4848
let containers = try await client.list(filters: filters)
49-
try printContainers(containers: containers, format: format)
50-
}
51-
52-
private func createHeader() -> [[String]] {
53-
[["ID", "IMAGE", "OS", "ARCH", "STATE", "ADDR", "CPUS", "MEMORY", "STARTED"]]
54-
}
55-
56-
private func printContainers(containers: [ContainerSnapshot], format: ListFormat) throws {
57-
if format == .json {
58-
let printables = containers.map {
59-
PrintableContainer($0)
60-
}
61-
let data = try JSONEncoder().encode(printables)
62-
print(String(decoding: data, as: UTF8.self))
63-
64-
return
65-
}
66-
67-
if self.quiet {
68-
containers.forEach {
69-
print($0.id)
70-
}
71-
return
72-
}
73-
74-
var rows = createHeader()
75-
for container in containers {
76-
rows.append(container.asRow)
77-
}
78-
79-
let formatter = TableOutput(rows: rows)
80-
print(formatter.format())
49+
let items = containers.map { PrintableContainer($0) }
50+
try Output.render(json: items, display: items, format: format, quiet: quiet)
8151
}
8252
}
8353
}
8454

85-
extension ContainerSnapshot {
86-
fileprivate var asRow: [String] {
55+
extension PrintableContainer: ListDisplayable {
56+
static var tableHeader: [String] {
57+
["ID", "IMAGE", "OS", "ARCH", "STATE", "ADDR", "CPUS", "MEMORY", "STARTED"]
58+
}
59+
60+
var tableRow: [String] {
8761
[
88-
self.id,
62+
self.configuration.id,
8963
self.configuration.image.reference,
90-
self.platform.os,
91-
self.platform.architecture,
64+
self.configuration.platform.os,
65+
self.configuration.platform.architecture,
9266
self.status.rawValue,
93-
self.networks.compactMap { $0.ipv4Address.description }.joined(separator: ","),
67+
self.networks.map { $0.ipv4Address.description }.joined(separator: ","),
9468
"\(self.configuration.resources.cpus)",
9569
"\(self.configuration.resources.memoryInBytes / (1024 * 1024)) MB",
96-
self.startedDate.map { ISO8601DateFormatter().string(from: $0) } ?? "",
70+
self.startedDate?.ISO8601Format() ?? "",
9771
]
9872
}
73+
74+
var quietValue: String {
75+
self.configuration.id
76+
}
9977
}
10078

101-
struct PrintableContainer: Codable {
79+
struct PrintableContainer: Codable, Sendable {
10280
let status: RuntimeStatus
10381
let configuration: ContainerConfiguration
10482
let networks: [Attachment]

Sources/ContainerCommands/Container/ContainerStats.swift

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -104,8 +104,7 @@ extension Application {
104104

105105
if format == .json {
106106
let jsonStats = statsData.map { $0.stats2 }
107-
let data = try JSONEncoder().encode(jsonStats)
108-
print(String(decoding: data, as: UTF8.self))
107+
try Output.emit(Output.renderJSON(jsonStats))
109108
return
110109
}
111110

Sources/ContainerCommands/Image/ImageInspect.swift

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import ArgumentParser
1818
import ContainerAPIClient
1919
import ContainerLog
20+
import ContainerResource
2021
import ContainerizationError
2122
import Foundation
2223
import Logging
@@ -42,7 +43,7 @@ extension Application {
4243
}
4344

4445
public func run() async throws {
45-
var printable = [any Codable]()
46+
var printable: [ImageDetail] = []
4647
var succeededImages: [String] = []
4748
var allErrors: [(String, Error)] = []
4849

@@ -59,7 +60,7 @@ extension Application {
5960
}
6061

6162
if !printable.isEmpty {
62-
print(try printable.jsonArray())
63+
try Output.emit(Output.renderJSON(printable))
6364
}
6465

6566
if !allErrors.isEmpty {

0 commit comments

Comments
 (0)