add typed JSON parser for brew outdated
parseOutdatedJSON decodes brew outdated --json into OutdatedPackages, which separates formulae from casks — the plain-text output is just names with no type information. Codable structs mirror the JSON shape, with convertFromSnakeCase mapping installed_versions to Swift naming. Undecodable input (brew error text, empty output) parses as .none. The legacy line parser stays until callers switch over next commit. Covered by 4 new unit tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
3a5a7ee623
commit
7b3468126a
|
|
@ -1,5 +1,36 @@
|
||||||
import Foundation
|
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 {
|
enum BrewParser {
|
||||||
static func parseOutdated(_ output: String) -> [String] {
|
static func parseOutdated(_ output: String) -> [String] {
|
||||||
output.split(separator: "\n")
|
output.split(separator: "\n")
|
||||||
|
|
@ -7,6 +38,21 @@ enum BrewParser {
|
||||||
.filter { !$0.isEmpty }
|
.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 {
|
static func parseVersion(_ output: String) -> String {
|
||||||
output.split(separator: "\n").first.map(String.init) ?? "Unknown"
|
output.split(separator: "\n").first.map(String.init) ?? "Unknown"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,56 @@ final class BrewParserTests: XCTestCase {
|
||||||
XCTAssertEqual(BrewParser.parseOutdated(output), ["wget"])
|
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
|
// MARK: - parseVersion
|
||||||
|
|
||||||
func testParseVersion_picksFirstLine() {
|
func testParseVersion_picksFirstLine() {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue