diff --git a/BrewBar/BrewParser.swift b/BrewBar/BrewParser.swift index ef97bd0..b8d0c32 100644 --- a/BrewBar/BrewParser.swift +++ b/BrewBar/BrewParser.swift @@ -1,5 +1,36 @@ import Foundation +struct OutdatedPackage: Codable, Equatable { + let name: String + let installedVersions: [String] + let currentVersion: String + + /// Menu label, e.g. "wget 1.21 → 1.22" + var label: String { + let installed = installedVersions.joined(separator: ", ") + return "\(name) \(installed) → \(currentVersion)" + } +} + +struct OutdatedPackages: Codable, Equatable { + let formulae: [OutdatedPackage] + let casks: [OutdatedPackage] + + var all: [OutdatedPackage] { + formulae + casks + } + + var isEmpty: Bool { + formulae.isEmpty && casks.isEmpty + } + + var count: Int { + formulae.count + casks.count + } + + static let none = OutdatedPackages(formulae: [], casks: []) +} + enum BrewParser { static func parseOutdated(_ output: String) -> [String] { output.split(separator: "\n") @@ -7,6 +38,21 @@ enum BrewParser { .filter { !$0.isEmpty } } + /// Parses `brew outdated --json` output. Returns .none for anything + /// undecodable (brew error text, empty output) — same effect as an + /// empty result, and the raw output is already in the log window. + static func parseOutdatedJSON(_ output: String) -> OutdatedPackages { + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase // installed_versions → installedVersions + + guard let data = output.data(using: .utf8), + let parsed = try? decoder.decode(OutdatedPackages.self, from: data) + else { + return .none + } + return parsed + } + static func parseVersion(_ output: String) -> String { output.split(separator: "\n").first.map(String.init) ?? "Unknown" } diff --git a/BrewBarTests/BrewBarTests.swift b/BrewBarTests/BrewBarTests.swift index d3cbdfb..9632e58 100644 --- a/BrewBarTests/BrewBarTests.swift +++ b/BrewBarTests/BrewBarTests.swift @@ -22,6 +22,56 @@ final class BrewParserTests: XCTestCase { XCTAssertEqual(BrewParser.parseOutdated(output), ["wget"]) } + // MARK: - parseOutdatedJSON + + func testParseOutdatedJSON_formulaeAndCasks() { + let output = """ + { + "formulae": [ + { + "name": "wget", + "installed_versions": ["1.21.3"], + "current_version": "1.21.4", + "pinned": false, + "pinned_version": null + } + ], + "casks": [ + { + "name": "tailscale-app", + "installed_versions": ["1.98.5"], + "current_version": "1.98.8", + "pinned": false, + "pinned_version": null + } + ] + } + """ + + let result = BrewParser.parseOutdatedJSON(output) + XCTAssertEqual(result.formulae.map(\.name), ["wget"]) + XCTAssertEqual(result.casks.map(\.name), ["tailscale-app"]) + XCTAssertEqual(result.count, 2) + XCTAssertFalse(result.isEmpty) + XCTAssertEqual(result.all.map(\.name), ["wget", "tailscale-app"]) + } + + func testParseOutdatedJSON_nothingOutdated() { + let result = BrewParser.parseOutdatedJSON("{\"formulae\": [], \"casks\": []}") + XCTAssertTrue(result.isEmpty) + XCTAssertEqual(result.count, 0) + } + + func testParseOutdatedJSON_invalidInputReturnsNone() { + XCTAssertEqual(BrewParser.parseOutdatedJSON(""), .none) + XCTAssertEqual(BrewParser.parseOutdatedJSON("Error: some brew failure"), .none) + } + + func testParseOutdatedJSON_packageLabel() { + let package = OutdatedPackage(name: "wget", installedVersions: ["1.21.3"], currentVersion: "1.21.4") + XCTAssertEqual(package.label, "wget 1.21.3 → 1.21.4") + } + // MARK: - parseVersion func testParseVersion_picksFirstLine() {