diff --git a/Sources/mas/Commands/Info.swift b/Sources/mas/Commands/Info.swift index 3b66c9be0..1dd231113 100644 --- a/Sources/mas/Commands/Info.swift +++ b/Sources/mas/Commands/Info.swift @@ -33,9 +33,34 @@ extension MAS { private func run(printer: Printer, searcher: some AppStoreSearcher) async { var spacing = "" await requiredAppIDsOptionGroup.forEachAppID(printer: printer) { appID in - printer.info("", AppInfoFormatter.format(app: try await searcher.lookup(appID: appID)), separator: spacing) + let result = try await searcher.lookup(appID: appID) + printer.info( + "", + """ + \(result.name) \(result.version) [\(result.formattedPrice)] + By: \(result.vendorName) + Released: \(result.releaseDate.humanReadableDate) + Minimum OS: \(result.minimumOSVersion) + Size: \(result.fileSizeBytes.humanReadableSize) + From: \(result.appStoreURL) + """, + separator: spacing + ) spacing = "\n" } } } } + +private extension String { + var humanReadableSize: String { + ByteCountFormatter.string(fromByteCount: Int64(self) ?? 0, countStyle: .file) + } + + var humanReadableDate: String { + ISO8601DateFormatter().date(from: self).map { date in + ISO8601DateFormatter.string(from: date, timeZone: .current, formatOptions: [.withFullDate]) + } + ?? "" // swiftformat:disable:this indent + } +} diff --git a/Sources/mas/Commands/List.swift b/Sources/mas/Commands/List.swift index a894d9135..b8bc59b4a 100644 --- a/Sources/mas/Commands/List.swift +++ b/Sources/mas/Commands/List.swift @@ -6,6 +6,7 @@ // internal import ArgumentParser +private import Foundation extension MAS { /// Lists all apps installed from the Mac App Store. @@ -39,7 +40,25 @@ extension MAS { """ ) } else { - printer.info(AppListFormatter.format(installedApps)) + guard let maxADAMIDLength = installedApps.map({ String(describing: $0.adamID).count }).max() else { + return + } + guard let maxAppNameLength = installedApps.map(\.name.count).max() else { + return + } + + let format = "%\(maxADAMIDLength)lu %@ (%@)" + printer.info( + installedApps.map { installedApp in + String( + format: format, + installedApp.adamID, + installedApp.name.padding(toLength: maxAppNameLength, withPad: " ", startingAt: 0), + installedApp.version + ) + } + .joined(separator: "\n") + ) } } } diff --git a/Sources/mas/Commands/Search.swift b/Sources/mas/Commands/Search.swift index 6aad33877..9e2836674 100644 --- a/Sources/mas/Commands/Search.swift +++ b/Sources/mas/Commands/Search.swift @@ -6,6 +6,7 @@ // internal import ArgumentParser +private import Foundation extension MAS { /// Searches for apps in the Mac App Store. @@ -37,8 +38,26 @@ extension MAS { guard !results.isEmpty else { throw MASError.noSearchResultsFound(for: searchTerm) } + guard let maxADAMIDLength = results.map({ String(describing: $0.adamID).count }).max() else { + return + } + guard let maxAppNameLength = results.map(\.name.count).max() else { + return + } - printer.info(SearchResultFormatter.format(results, includePrice: price)) + let format = "%\(maxADAMIDLength)lu %@ (%@)\(price ? " %@" : "")" + printer.info( + results.map { result in + String( + format: format, + result.adamID, + result.name.padding(toLength: maxAppNameLength, withPad: " ", startingAt: 0), + result.version, + result.formattedPrice + ) + } + .joined(separator: "\n") + ) } } } diff --git a/Sources/mas/Formatters/AppInfoFormatter.swift b/Sources/mas/Formatters/AppInfoFormatter.swift deleted file mode 100644 index f5b92a157..000000000 --- a/Sources/mas/Formatters/AppInfoFormatter.swift +++ /dev/null @@ -1,45 +0,0 @@ -// -// AppInfoFormatter.swift -// mas -// -// Copyright © 2019 mas-cli. All rights reserved. -// - -private import Foundation - -/// Formats text output for the info command. -enum AppInfoFormatter { - /// Formats text output with app info. - /// - /// - Parameter app: Search result with app data. - /// - Returns: Multiline text output. - static func format(app: SearchResult) -> String { - """ - \(app.name) \(app.version) [\(app.formattedPrice)] - By: \(app.vendorName) - Released: \(humanReadableDate(app.releaseDate)) - Minimum OS: \(app.minimumOSVersion) - Size: \(humanReadableSize(app.fileSizeBytes)) - From: \(app.appStoreURL) - """ - } - - /// Formats a file size. - /// - /// - Parameter size: Numeric string. - /// - Returns: Formatted file size description. - private static func humanReadableSize(_ size: String) -> String { - ByteCountFormatter.string(fromByteCount: Int64(size) ?? 0, countStyle: .file) - } - - /// Formats a date in ISO-8601 date-only format. - /// - /// - Parameter serverDate: String containing a date in ISO-8601 format. - /// - Returns: Simple date format. - private static func humanReadableDate(_ serverDate: String) -> String { - ISO8601DateFormatter().date(from: serverDate).map { date in - ISO8601DateFormatter.string(from: date, timeZone: .current, formatOptions: [.withFullDate]) - } - ?? "" // swiftformat:disable:this indent - } -} diff --git a/Sources/mas/Formatters/AppListFormatter.swift b/Sources/mas/Formatters/AppListFormatter.swift deleted file mode 100644 index fab790eae..000000000 --- a/Sources/mas/Formatters/AppListFormatter.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// AppListFormatter.swift -// mas -// -// Copyright © 2020 mas-cli. All rights reserved. -// - -private import Foundation - -/// Formats text output for the search command. -enum AppListFormatter { - /// Formats text output with list results. - /// - /// - Parameter installedApps: List of installed apps. - /// - Returns: Multiline text output. - static func format(_ installedApps: [InstalledApp]) -> String { - guard let maxADAMIDLength = installedApps.map({ String(describing: $0.adamID).count }).max() else { - return "" - } - guard let maxAppNameLength = installedApps.map(\.name.count).max() else { - return "" - } - - let format = "%\(maxADAMIDLength)lu %@ (%@)" - return - installedApps.map { installedApp in - String( - format: format, - installedApp.adamID, - installedApp.name.padding(toLength: maxAppNameLength, withPad: " ", startingAt: 0), - installedApp.version - ) - } - .joined(separator: "\n") - } -} diff --git a/Sources/mas/Formatters/SearchResultFormatter.swift b/Sources/mas/Formatters/SearchResultFormatter.swift deleted file mode 100644 index 5f98c6d46..000000000 --- a/Sources/mas/Formatters/SearchResultFormatter.swift +++ /dev/null @@ -1,39 +0,0 @@ -// -// SearchResultFormatter.swift -// mas -// -// Copyright © 2019 mas-cli. All rights reserved. -// - -private import Foundation - -/// Formats text output for the search command. -enum SearchResultFormatter { - /// Formats search results as text. - /// - /// - Parameters: - /// - results: Search results containing app data - /// - includePrice: Indicates whether to include prices in the output - /// - Returns: Multiline text output. - static func format(_ results: [SearchResult], includePrice: Bool = false) -> String { - guard let maxADAMIDLength = results.map({ String(describing: $0.adamID).count }).max() else { - return "" - } - guard let maxAppNameLength = results.map(\.name.count).max() else { - return "" - } - - let format = "%\(maxADAMIDLength)lu %@ (%@)\(includePrice ? " %@" : "")" - return - results.map { result in - String( - format: format, - result.adamID, - result.name.padding(toLength: maxAppNameLength, withPad: " ", startingAt: 0), - result.version, - result.formattedPrice - ) - } - .joined(separator: "\n") - } -} diff --git a/Sources/mas/Formatters/Printer.swift b/Sources/mas/Utilities/Printer.swift similarity index 100% rename from Sources/mas/Formatters/Printer.swift rename to Sources/mas/Utilities/Printer.swift diff --git a/Tests/MASTests/Formatters/MASTests+AppListFormatter.swift b/Tests/MASTests/Formatters/MASTests+AppListFormatter.swift deleted file mode 100644 index 53a50c9c5..000000000 --- a/Tests/MASTests/Formatters/MASTests+AppListFormatter.swift +++ /dev/null @@ -1,43 +0,0 @@ -// -// MASTests+AppListFormatter.swift -// mas -// -// Copyright © 2020 mas-cli. All rights reserved. -// - -@testable private import mas -internal import Testing - -private let format = AppListFormatter.format(_:) - -extension MASTests { - @Test - static func formatsEmptyAppListAsEmptyString() { - #expect(consequencesOf(format([])) == Consequences("")) - } - - @Test - static func formatsSingleInstalledApp() { - #expect( - consequencesOf( - format([InstalledApp(adamID: 12345, bundleID: "", name: "Awesome App", path: "", version: "19.2.1")]) - ) - == Consequences("12345 Awesome App (19.2.1)") // swiftformat:disable:this indent - ) - } - - @Test - static func formatsTwoInstalledApps() { - #expect( - consequencesOf( - format( - [ - InstalledApp(adamID: 12345, bundleID: "", name: "Awesome App", path: "", version: "19.2.1"), - InstalledApp(adamID: 67890, bundleID: "", name: "Even Better App", path: "", version: "1.2.0"), - ] - ) - ) // swiftformat:disable:next indent - == Consequences("12345 Awesome App (19.2.1)\n67890 Even Better App (1.2.0)") - ) - } -} diff --git a/Tests/MASTests/Formatters/MASTests+SearchResultFormatter.swift b/Tests/MASTests/Formatters/MASTests+SearchResultFormatter.swift deleted file mode 100644 index 483809825..000000000 --- a/Tests/MASTests/Formatters/MASTests+SearchResultFormatter.swift +++ /dev/null @@ -1,70 +0,0 @@ -// -// MASTests+SearchResultFormatter.swift -// mas -// -// Copyright © 2019 mas-cli. All rights reserved. -// - -@testable private import mas -internal import Testing - -private let format = SearchResultFormatter.format(_:includePrice:) - -extension MASTests { - @Test - static func formatsEmptySearchResultsAsEmptyString() { - #expect(consequencesOf(format([], false)) == Consequences("")) - } - - @Test - static func formatsSingleResult() { - #expect( - consequencesOf( - format([SearchResult(adamID: 12345, formattedPrice: "$9.87", name: "Awesome App", version: "19.2.1")], false) - ) - == Consequences("12345 Awesome App (19.2.1)") // swiftformat:disable:this indent - ) - } - - @Test - static func formatsSingleResultWithPrice() { - #expect( - consequencesOf( - format([SearchResult(adamID: 12345, formattedPrice: "$9.87", name: "Awesome App", version: "19.2.1")], true) - ) - == Consequences("12345 Awesome App (19.2.1) $9.87") // swiftformat:disable:this indent - ) - } - - @Test - static func formatsTwoResults() { - #expect( - consequencesOf( - format( - [ - SearchResult(adamID: 12345, formattedPrice: "$9.87", name: "Awesome App", version: "19.2.1"), - SearchResult(adamID: 67890, formattedPrice: "$0.01", name: "Even Better App", version: "1.2.0"), - ], - false - ) - ) // swiftformat:disable:next indent - == Consequences("12345 Awesome App (19.2.1)\n67890 Even Better App (1.2.0)") - ) - } - - @Test - static func formatsTwoResultsWithPrices() { - #expect( - consequencesOf( - format( - [ - SearchResult(adamID: 12345, formattedPrice: "$9.87", name: "Awesome App", version: "19.2.1"), - SearchResult(adamID: 67890, formattedPrice: "$0.01", name: "Even Better App", version: "1.2.0"), - ], - true - ) - ) // swiftformat:disable:next indent - == Consequences("12345 Awesome App (19.2.1) $9.87\n67890 Even Better App (1.2.0) $0.01") - ) - } -}