From 9a193233d072444a180fbdfb675d76c5d9e56aef Mon Sep 17 00:00:00 2001 From: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> Date: Sun, 5 Oct 2025 18:10:52 -0400 Subject: [PATCH] Refactor `SearchResult` & `SearchResultList` to use model-oriented let properties instead of JSON-oriented var properties. Signed-off-by: Ross Goldberg <484615+rgoldberg@users.noreply.github.com> --- .swiftformat | 1 - Sources/mas/Commands/Home.swift | 2 +- Sources/mas/Commands/Open.swift | 2 +- Sources/mas/Commands/Vendor.swift | 2 +- Sources/mas/Formatters/AppInfoFormatter.swift | 10 +-- .../Formatters/SearchResultFormatter.swift | 6 +- Sources/mas/Models/SearchResult.swift | 70 ++++++++++++++----- Sources/mas/Models/SearchResultList.swift | 4 +- Tests/MASTests/Commands/MASTests+Info.swift | 14 ++-- .../MASTests/Commands/MASTests+Outdated.swift | 22 +++--- Tests/MASTests/Commands/MASTests+Search.swift | 2 +- ...ASTests+ITunesSearchAppStoreSearcher.swift | 10 +-- .../Controllers/MockAppStoreSearcher.swift | 2 +- .../MASTests+SearchResultFormatter.swift | 18 ++--- .../Models/MASTests+SearchResult.swift | 2 +- 15 files changed, 98 insertions(+), 69 deletions(-) diff --git a/.swiftformat b/.swiftformat index 700472cf9..0df72b21f 100644 --- a/.swiftformat +++ b/.swiftformat @@ -66,7 +66,6 @@ --max-width 120 --organization-mode type --organize-types actor,class,enum,extension,struct ---preserve-acronyms bundleId,minimumOsVersion,sellerUrl,trackId,trackViewUrl --property-types inferred --ranges no-space --redundant-async always diff --git a/Sources/mas/Commands/Home.swift b/Sources/mas/Commands/Home.swift index 9311cddf6..f5b79a596 100644 --- a/Sources/mas/Commands/Home.swift +++ b/Sources/mas/Commands/Home.swift @@ -32,7 +32,7 @@ extension MAS { private func run(printer: Printer, searcher: AppStoreSearcher) async { await requiredAppIDsOptionGroup.forEachAppID(printer: printer) { appID in - let urlString = try await searcher.lookup(appID: appID).trackViewUrl + let urlString = try await searcher.lookup(appID: appID).appStoreURL guard let url = URL(string: urlString) else { throw MASError.urlParsing(urlString) } diff --git a/Sources/mas/Commands/Open.swift b/Sources/mas/Commands/Open.swift index 99fa5db7a..1f76f21ff 100644 --- a/Sources/mas/Commands/Open.swift +++ b/Sources/mas/Commands/Open.swift @@ -47,7 +47,7 @@ extension MAS { forURLString: try await searcher.lookup( appID: AppID(from: appIDString, forceBundleID: forceBundleIDOptionGroup.forceBundleID) ) - .trackViewUrl + .appStoreURL ) } } diff --git a/Sources/mas/Commands/Vendor.swift b/Sources/mas/Commands/Vendor.swift index a88b41735..d3560240d 100644 --- a/Sources/mas/Commands/Vendor.swift +++ b/Sources/mas/Commands/Vendor.swift @@ -32,7 +32,7 @@ extension MAS { private func run(printer: Printer, searcher: AppStoreSearcher) async { await requiredAppIDsOptionGroup.forEachAppID(printer: printer) { appID in - guard let urlString = try await searcher.lookup(appID: appID).sellerUrl else { + guard let urlString = try await searcher.lookup(appID: appID).vendorURL else { throw MASError.noVendorWebsite(forAppID: appID) } guard let url = URL(string: urlString) else { diff --git a/Sources/mas/Formatters/AppInfoFormatter.swift b/Sources/mas/Formatters/AppInfoFormatter.swift index 7a5d3e19f..f5b92a157 100644 --- a/Sources/mas/Formatters/AppInfoFormatter.swift +++ b/Sources/mas/Formatters/AppInfoFormatter.swift @@ -15,12 +15,12 @@ enum AppInfoFormatter { /// - Returns: Multiline text output. static func format(app: SearchResult) -> String { """ - \(app.trackName) \(app.version) [\(app.outputPrice)] - By: \(app.sellerName) - Released: \(humanReadableDate(app.currentVersionReleaseDate)) - Minimum OS: \(app.minimumOsVersion) + \(app.name) \(app.version) [\(app.formattedPrice)] + By: \(app.vendorName) + Released: \(humanReadableDate(app.releaseDate)) + Minimum OS: \(app.minimumOSVersion) Size: \(humanReadableSize(app.fileSizeBytes)) - From: \(app.trackViewUrl) + From: \(app.appStoreURL) """ } diff --git a/Sources/mas/Formatters/SearchResultFormatter.swift b/Sources/mas/Formatters/SearchResultFormatter.swift index b18710b9c..5f98c6d46 100644 --- a/Sources/mas/Formatters/SearchResultFormatter.swift +++ b/Sources/mas/Formatters/SearchResultFormatter.swift @@ -19,7 +19,7 @@ enum SearchResultFormatter { guard let maxADAMIDLength = results.map({ String(describing: $0.adamID).count }).max() else { return "" } - guard let maxAppNameLength = results.map(\.trackName.count).max() else { + guard let maxAppNameLength = results.map(\.name.count).max() else { return "" } @@ -29,9 +29,9 @@ enum SearchResultFormatter { String( format: format, result.adamID, - result.trackName.padding(toLength: maxAppNameLength, withPad: " ", startingAt: 0), + result.name.padding(toLength: maxAppNameLength, withPad: " ", startingAt: 0), result.version, - result.outputPrice + result.formattedPrice ) } .joined(separator: "\n") diff --git a/Sources/mas/Models/SearchResult.swift b/Sources/mas/Models/SearchResult.swift index fa3fed938..37afbce5c 100644 --- a/Sources/mas/Models/SearchResult.swift +++ b/Sources/mas/Models/SearchResult.swift @@ -5,28 +5,64 @@ // Copyright © 2018 mas-cli. All rights reserved. // -struct SearchResult: Decodable { - var bundleId = "" - var currentVersionReleaseDate = "" - var fileSizeBytes = "0" - var formattedPrice = "0" as String? - var minimumOsVersion = "" - var sellerName = "" - var sellerUrl = "" as String? - var trackId = 0 as ADAMID - var trackName = "" - var trackViewUrl = "" - var version = "" +struct SearchResult: AppIdentifying { + let adamID: ADAMID + let appStoreURL: String + let bundleID: String + let fileSizeBytes: String + let formattedPrice: String + let minimumOSVersion: String + let name: String + let releaseDate: String + let vendorName: String + let vendorURL: String? + let version: String + + init( + adamID: ADAMID = 0, + appStoreURL: String = "", + bundleID: String = "", + fileSizeBytes: String = "0", + formattedPrice: String? = "0", + minimumOSVersion: String = "", + name: String = "", + releaseDate: String = "", + vendorName: String = "", + vendorURL: String? = nil, + version: String = "" + ) { + self.adamID = adamID + self.appStoreURL = appStoreURL + self.bundleID = bundleID + self.fileSizeBytes = fileSizeBytes + self.formattedPrice = formattedPrice ?? "?" + self.minimumOSVersion = minimumOSVersion + self.name = name + self.releaseDate = releaseDate + self.vendorName = vendorName + self.vendorURL = vendorURL + self.version = version + } } -extension SearchResult: AppIdentifying { - var adamID: ADAMID { trackId } - var bundleID: String { bundleId } - var outputPrice: String { formattedPrice ?? "?" } +extension SearchResult: Decodable { + enum CodingKeys: String, CodingKey { + case adamID = "trackId" + case appStoreURL = "trackViewUrl" + case bundleID = "bundleId" + case fileSizeBytes + case formattedPrice + case minimumOSVersion = "minimumOsVersion" + case name = "trackName" + case releaseDate = "currentVersionReleaseDate" + case vendorName = "sellerName" + case vendorURL = "sellerUrl" + case version + } } extension SearchResult: Hashable { func hash(into hasher: inout Hasher) { - hasher.combine(trackId) + hasher.combine(adamID) } } diff --git a/Sources/mas/Models/SearchResultList.swift b/Sources/mas/Models/SearchResultList.swift index 9a7e5b0cd..319e2a178 100644 --- a/Sources/mas/Models/SearchResultList.swift +++ b/Sources/mas/Models/SearchResultList.swift @@ -6,6 +6,6 @@ // struct SearchResultList: Decodable { - var resultCount: Int // swiftlint:disable:this unused_declaration - var results: [SearchResult] + let resultCount: Int // swiftlint:disable:this unused_declaration + let results: [SearchResult] } diff --git a/Tests/MASTests/Commands/MASTests+Info.swift b/Tests/MASTests/Commands/MASTests+Info.swift index 5ce4fff71..735a2db73 100644 --- a/Tests/MASTests/Commands/MASTests+Info.swift +++ b/Tests/MASTests/Commands/MASTests+Info.swift @@ -21,19 +21,19 @@ extension MASTests { @Test static func outputsAppInfo() async { let result = SearchResult( - currentVersionReleaseDate: "2019-01-07T18:53:13Z", + adamID: 1111, + appStoreURL: "https://awesome.app", fileSizeBytes: "1024", formattedPrice: "$2.00", - minimumOsVersion: "10.14", - sellerName: "Awesome Dev", - trackId: 1111, - trackName: "Awesome App", - trackViewUrl: "https://awesome.app", + minimumOSVersion: "10.14", + name: "Awesome App", + releaseDate: "2019-01-07T18:53:13Z", + vendorName: "Awesome Dev", version: "1.0" ) #expect( await consequencesOf( - try await MAS.Info.parse([String(result.trackId)]).run( + try await MAS.Info.parse([String(result.adamID)]).run( searcher: MockAppStoreSearcher([.adamID(result.adamID): result]) ) ) diff --git a/Tests/MASTests/Commands/MASTests+Outdated.swift b/Tests/MASTests/Commands/MASTests+Outdated.swift index d7cd21888..4d823a400 100644 --- a/Tests/MASTests/Commands/MASTests+Outdated.swift +++ b/Tests/MASTests/Commands/MASTests+Outdated.swift @@ -14,15 +14,15 @@ extension MASTests { static func outputsOutdatedApps() async { let result = SearchResult( - bundleId: "au.haroldchu.mac.Bandwidth", - currentVersionReleaseDate: "2024-09-02T00:27:00Z", + adamID: 490_461_369, + appStoreURL: "https://apps.apple.com/us/app/bandwidth/id490461369?mt=12&uo=4", + bundleID: "au.haroldchu.mac.Bandwidth", fileSizeBytes: "998130", - minimumOsVersion: "10.13", - sellerName: "Harold Chu", - sellerUrl: "https://example.com", - trackId: 490_461_369, - trackName: "Bandwidth+", - trackViewUrl: "https://apps.apple.com/us/app/bandwidth/id490461369?mt=12&uo=4", + minimumOSVersion: "10.13", + name: "Bandwidth+", + releaseDate: "2024-09-02T00:27:00Z", + vendorName: "Harold Chu", + vendorURL: "https://example.com", version: "1.28" ) #expect( @@ -30,9 +30,9 @@ extension MASTests { try await MAS.Outdated.parse([]).run( installedApps: [ InstalledApp( - adamID: result.trackId, - bundleID: result.bundleId, - name: result.trackName, + adamID: result.adamID, + bundleID: result.bundleID, + name: result.name, path: "/Applications/Bandwidth+.app", version: "1.27" ), diff --git a/Tests/MASTests/Commands/MASTests+Search.swift b/Tests/MASTests/Commands/MASTests+Search.swift index 6fdecaf60..39bbc6c87 100644 --- a/Tests/MASTests/Commands/MASTests+Search.swift +++ b/Tests/MASTests/Commands/MASTests+Search.swift @@ -12,7 +12,7 @@ internal import Testing extension MASTests { @Test static func searchesForSlack() async { - let result = SearchResult(trackId: 1111, trackName: "slack", trackViewUrl: "mas preview url", version: "0.0") + let result = SearchResult(adamID: 1111, name: "slack", version: "0.0") #expect( await consequencesOf( try await MAS.Search.parse(["slack"]).run( diff --git a/Tests/MASTests/Controllers/MASTests+ITunesSearchAppStoreSearcher.swift b/Tests/MASTests/Controllers/MASTests+ITunesSearchAppStoreSearcher.swift index f43935f44..6caf14b9c 100644 --- a/Tests/MASTests/Controllers/MASTests+ITunesSearchAppStoreSearcher.swift +++ b/Tests/MASTests/Controllers/MASTests+ITunesSearchAppStoreSearcher.swift @@ -47,11 +47,11 @@ extension MASTests { } #expect( - result.trackId == adamID - && result.sellerName == "Slack Technologies, Inc." // swiftformat:disable indent - && result.sellerUrl == "https://slack.com" - && result.trackName == "Slack" - && result.trackViewUrl == "https://itunes.apple.com/us/app/slack/id803453959?mt=12&uo=4" + result.adamID == adamID + && result.vendorName == "Slack Technologies, Inc." // swiftformat:disable indent + && result.vendorURL == "https://slack.com" + && result.name == "Slack" + && result.appStoreURL == "https://itunes.apple.com/us/app/slack/id803453959?mt=12&uo=4" && result.version == "3.3.3" ) // swiftformat:enable indent } diff --git a/Tests/MASTests/Controllers/MockAppStoreSearcher.swift b/Tests/MASTests/Controllers/MockAppStoreSearcher.swift index 92cb57de9..980a4e3c4 100644 --- a/Tests/MASTests/Controllers/MockAppStoreSearcher.swift +++ b/Tests/MASTests/Controllers/MockAppStoreSearcher.swift @@ -23,6 +23,6 @@ struct MockAppStoreSearcher: AppStoreSearcher { } func search(for searchTerm: String, inRegion _: String) -> [SearchResult] { - resultByAppID.filter { $1.trackName.contains(searchTerm) }.map { $1 } + resultByAppID.filter { $1.name.contains(searchTerm) }.map { $1 } } } diff --git a/Tests/MASTests/Formatters/MASTests+SearchResultFormatter.swift b/Tests/MASTests/Formatters/MASTests+SearchResultFormatter.swift index a65fb0acf..483809825 100644 --- a/Tests/MASTests/Formatters/MASTests+SearchResultFormatter.swift +++ b/Tests/MASTests/Formatters/MASTests+SearchResultFormatter.swift @@ -20,10 +20,7 @@ extension MASTests { static func formatsSingleResult() { #expect( consequencesOf( - format( - [SearchResult(formattedPrice: "$9.87", trackId: 12345, trackName: "Awesome App", version: "19.2.1")], - false - ) + 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 ) @@ -33,10 +30,7 @@ extension MASTests { static func formatsSingleResultWithPrice() { #expect( consequencesOf( - format( - [SearchResult(formattedPrice: "$9.87", trackId: 12345, trackName: "Awesome App", version: "19.2.1")], - true - ) + 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 ) @@ -48,8 +42,8 @@ extension MASTests { consequencesOf( format( [ - SearchResult(formattedPrice: "$9.87", trackId: 12345, trackName: "Awesome App", version: "19.2.1"), - SearchResult(formattedPrice: "$0.01", trackId: 67890, trackName: "Even Better App", version: "1.2.0"), + 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 ) @@ -64,8 +58,8 @@ extension MASTests { consequencesOf( format( [ - SearchResult(formattedPrice: "$9.87", trackId: 12345, trackName: "Awesome App", version: "19.2.1"), - SearchResult(formattedPrice: "$0.01", trackId: 67890, trackName: "Even Better App", version: "1.2.0"), + 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 ) diff --git a/Tests/MASTests/Models/MASTests+SearchResult.swift b/Tests/MASTests/Models/MASTests+SearchResult.swift index ee9f9ba2b..0d409db4b 100644 --- a/Tests/MASTests/Models/MASTests+SearchResult.swift +++ b/Tests/MASTests/Models/MASTests+SearchResult.swift @@ -14,7 +14,7 @@ extension MASTests { static func parsesSearchResultFromThingsThatGoBumpJSON() { #expect( consequencesOf( - try JSONDecoder().decode(SearchResult.self, from: Data(fromResource: "search/things-that-go-bump.json")).trackId + try JSONDecoder().decode(SearchResult.self, from: Data(fromResource: "search/things-that-go-bump.json")).adamID ) == Consequences(1_472_954_003) // swiftformat:disable:this indent )