diff --git a/.github/workflows/IPSWDownloads.yml b/.github/workflows/IPSWDownloads.yml index 140d168..71725c8 100644 --- a/.github/workflows/IPSWDownloads.yml +++ b/.github/workflows/IPSWDownloads.yml @@ -15,7 +15,7 @@ jobs: strategy: matrix: runs-on: [ubuntu-20.04, ubuntu-22.04] - swift-version: [5.9] + swift-version: [5.9, "5.10"] steps: - uses: actions/checkout@v4 - name: Cache swift package modules diff --git a/.swiftformat b/.swiftformat index ae26e16..a5a6030 100644 --- a/.swiftformat +++ b/.swiftformat @@ -1,5 +1,5 @@ --indent 2 ---header "\n .*?\.swift\n IPSWDownloads\n\n Created by {file}\n Copyright © {year} BrightDigit.\n\n Permission is hereby granted, free of charge, to any person\n obtaining a copy of this software and associated documentation\n files (the “Software”), to deal in the Software without\n restriction, including without limitation the rights to use,\n copy, modify, merge, publish, distribute, sublicense, and/or\n sell copies of the Software, and to permit persons to whom the\n Software is furnished to do so, subject to the following\n conditions:\n \n The above copyright notice and this permission notice shall be\n included in all copies or substantial portions of the Software.\n\n THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,\n EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\n OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\n HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\n WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\n OTHER DEALINGS IN THE SOFTWARE.\n" +--header "\n {file}.swift\n IPSWDownloads\n\n Created by Leo Dion.\n Copyright © {year} BrightDigit.\n\n Permission is hereby granted, free of charge, to any person\n obtaining a copy of this software and associated documentation\n files (the “Software”), to deal in the Software without\n restriction, including without limitation the rights to use,\n copy, modify, merge, publish, distribute, sublicense, and/or\n sell copies of the Software, and to permit persons to whom the\n Software is furnished to do so, subject to the following\n conditions:\n \n The above copyright notice and this permission notice shall be\n included in all copies or substantial portions of the Software.\n\n THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,\n EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES\n OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND\n NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT\n HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,\n WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR\n OTHER DEALINGS IN THE SOFTWARE.\n" --commas inline --disable wrapMultilineStatementBraces, redundantInternal --extensionacl on-declarations diff --git a/Package.resolved b/Package.resolved index b5db432..ae4e3d7 100644 --- a/Package.resolved +++ b/Package.resolved @@ -59,8 +59,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/apple/swift-openapi-generator", "state" : { - "revision" : "76994bfc77061c6cfa3b82415613a9dfbfb47f28", - "version" : "1.1.0" + "revision" : "7992d77065f2787e7651cf6d9be9b99ad38f5166", + "version" : "1.2.1" } }, { diff --git a/Package.swift b/Package.swift index e9d8af7..c0b12d0 100644 --- a/Package.swift +++ b/Package.swift @@ -19,6 +19,16 @@ let package = Package( dependencies: [ .product(name: "OpenAPIRuntime", package: "swift-openapi-runtime"), .product(name: "OpenAPIURLSession", package: "swift-openapi-urlsession") + ], + swiftSettings: [ + .enableUpcomingFeature("BareSlashRegexLiterals"), + .enableUpcomingFeature("ConciseMagicFile"), + .enableUpcomingFeature("ExistentialAny"), + .enableUpcomingFeature("ForwardTrailingClosures"), + .enableUpcomingFeature("ImplicitOpenExistentials"), + .enableUpcomingFeature("StrictConcurrency"), + .enableUpcomingFeature("DisableOutwardActorInference"), + .enableExperimentalFeature("StrictConcurrency") ] ), .testTarget( diff --git a/Scripts/lint.sh b/Scripts/lint.sh index 4c0d481..eaedd3f 100755 --- a/Scripts/lint.sh +++ b/Scripts/lint.sh @@ -33,11 +33,10 @@ $MINT_CMD bootstrap -m Mintfile if [ -z "$CI" ]; then $MINT_RUN swiftformat . - $MINT_RUN swiftlint autocorrect + $MINT_RUN swiftlint --autocorrect fi if [ -z "$FORMAT_ONLY"]; then - $MINT_RUN periphery scan $MINT_RUN swiftformat --lint $SWIFTFORMAT_OPTIONS . $MINT_RUN swiftlint lint $SWIFTLINT_OPTIONS fi diff --git a/Sources/IPSWDownloads/Board.swift b/Sources/IPSWDownloads/Board.swift index b8c06ad..91c08fb 100644 --- a/Sources/IPSWDownloads/Board.swift +++ b/Sources/IPSWDownloads/Board.swift @@ -2,7 +2,7 @@ // Board.swift // IPSWDownloads // -// Created by Board.swift +// Created by Leo Dion. // Copyright © 2024 BrightDigit. // // Permission is hereby granted, free of charge, to any person @@ -30,7 +30,7 @@ import Foundation /// A struct representing a board with configuration details. -public struct Board { +public struct Board: Sendable, Codable, Hashable, Equatable { /// The configuration of the board. public let boardconfig: String diff --git a/Sources/IPSWDownloads/Data.swift b/Sources/IPSWDownloads/Data.swift index 2f61a4b..6d96730 100644 --- a/Sources/IPSWDownloads/Data.swift +++ b/Sources/IPSWDownloads/Data.swift @@ -2,7 +2,7 @@ // Data.swift // IPSWDownloads // -// Created by Data.swift +// Created by Leo Dion. // Copyright © 2024 BrightDigit. // // Permission is hereby granted, free of charge, to any person diff --git a/Sources/IPSWDownloads/Device.swift b/Sources/IPSWDownloads/Device.swift index 93fe93f..f2636ac 100644 --- a/Sources/IPSWDownloads/Device.swift +++ b/Sources/IPSWDownloads/Device.swift @@ -2,7 +2,7 @@ // Device.swift // IPSWDownloads // -// Created by Device.swift +// Created by Leo Dion. // Copyright © 2024 BrightDigit. // // Permission is hereby granted, free of charge, to any person @@ -30,7 +30,7 @@ import Foundation /// A struct representing an Apple device along with its firmware and supported boards. -public struct Device { +public struct Device: Sendable, Codable, Hashable, Equatable { /// The name of the Apple device. public let name: String diff --git a/Sources/IPSWDownloads/Firmware.swift b/Sources/IPSWDownloads/Firmware.swift index 03eaa3c..ca146b4 100644 --- a/Sources/IPSWDownloads/Firmware.swift +++ b/Sources/IPSWDownloads/Firmware.swift @@ -2,7 +2,7 @@ // Firmware.swift // IPSWDownloads // -// Created by Firmware.swift +// Created by Leo Dion. // Copyright © 2024 BrightDigit. // // Permission is hereby granted, free of charge, to any person @@ -30,7 +30,7 @@ import Foundation /// A struct representing firmware details of a device. -public struct Firmware { +public struct Firmware: Sendable, Codable, Hashable, Equatable { /// The unique identifier of the firmware. public let identifier: String diff --git a/Sources/IPSWDownloads/FirmwareType.swift b/Sources/IPSWDownloads/FirmwareType.swift index 2b80090..5aed0f2 100644 --- a/Sources/IPSWDownloads/FirmwareType.swift +++ b/Sources/IPSWDownloads/FirmwareType.swift @@ -2,7 +2,7 @@ // FirmwareType.swift // IPSWDownloads // -// Created by FirmwareType.swift +// Created by Leo Dion. // Copyright © 2024 BrightDigit. // // Permission is hereby granted, free of charge, to any person @@ -30,7 +30,7 @@ import Foundation /// Type of Firmware file. -public enum FirmwareType: String { +public enum FirmwareType: String, Sendable, Codable, Hashable, Equatable { /// IPSW File case ipsw /// OTA Firmware diff --git a/Sources/IPSWDownloads/IPSWDownloads.swift b/Sources/IPSWDownloads/IPSWDownloads.swift index e852613..13c9aac 100644 --- a/Sources/IPSWDownloads/IPSWDownloads.swift +++ b/Sources/IPSWDownloads/IPSWDownloads.swift @@ -2,7 +2,7 @@ // IPSWDownloads.swift // IPSWDownloads // -// Created by IPSWDownloads.swift +// Created by Leo Dion. // Copyright © 2024 BrightDigit. // // Permission is hereby granted, free of charge, to any person @@ -32,7 +32,7 @@ import OpenAPIRuntime /// Client for downloading current and previous versions /// of Apple's iOS Firmware, iTunes and OTA updates. -public struct IPSWDownloads { +public struct IPSWDownloads: Sendable { // swiftlint:disable:next force_try private static let serverURL = try! Servers.server1() diff --git a/Sources/IPSWDownloads/OperatingSystemVersion+Codable.swift b/Sources/IPSWDownloads/OperatingSystemVersion+Codable.swift new file mode 100644 index 0000000..72cdf43 --- /dev/null +++ b/Sources/IPSWDownloads/OperatingSystemVersion+Codable.swift @@ -0,0 +1,175 @@ +// +// OperatingSystemVersion+Codable.swift +// IPSWDownloads +// +// Created by Leo Dion. +// Copyright © 2024 BrightDigit. +// +// Permission is hereby granted, free of charge, to any person +// obtaining a copy of this software and associated documentation +// files (the “Software”), to deal in the Software without +// restriction, including without limitation the rights to use, +// copy, modify, merge, publish, distribute, sublicense, and/or +// sell copies of the Software, and to permit persons to whom the +// Software is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice shall be +// included in all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, +// EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +// OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +// NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +// HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +// WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +// OTHER DEALINGS IN THE SOFTWARE. +// + +import Foundation + +extension OperatingSystemVersion: + Codable, + CustomDebugStringConvertible, + CustomStringConvertible { + public enum CodingKeys: String, CodingKey { + case majorVersion + case minorVersion + case patchVersion + } + + public enum Error: Swift.Error, Equatable { + case invalidFormatString(String) + case invalidComponentsCount(Int) + } + + public var debugDescription: String { + // swiftlint:disable:next line_length + "OperatingSystemVersion(majorVersion: \(majorVersion), minorVersion: \(minorVersion), patchVersion: \(patchVersion))" + } + + public var description: String { + string() + } + + public init(string: String) throws { + let components = Self.componentsFrom(string) + + guard components.count == 2 || components.count == 3 else { + throw RuntimeError.invalidVersion(string) + } + + self.init( + majorVersion: components[0], + minorVersion: components[1], + patchVersion: components.count == 3 ? components[2] : 0 + ) + } + + private init(container: any SingleValueDecodingContainer) throws { + let intArrayResult = Result { + try container.decode(String.self) + } + .map(Self.componentsFrom(_:)) + .flatMapError { _ in + Result { + try container.decode([Int].self) + } + } + try self.init(components: intArrayResult.get()) + } + + private init(compositeFrom decoder: any Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + let majorVersion: Int = try container.decode(Int.self, forKey: .majorVersion) + let minorVersion: Int = try container.decode(Int.self, forKey: .minorVersion) + let patchVersion: Int = try container.decode(Int.self, forKey: .patchVersion) + self.init( + majorVersion: majorVersion, + minorVersion: minorVersion, + patchVersion: patchVersion + ) + } + + public init(components: [Int]) throws { + guard components.count == 2 || components.count == 3 else { + throw Self.Error.invalidComponentsCount(components.count) + } + + self.init( + majorVersion: components[0], + minorVersion: components[1], + patchVersion: components.count == 3 ? components[2] : 0 + ) + } + + // swiftlint:disable:next function_body_length + public init(from decoder: any Decoder) throws { + let throwingError: (any Swift.Error)? + if let container = Self.singleStringDecodingContainer(from: decoder) { + do { + try self.init(container: container) + return + } catch { + throwingError = error + } + } else { + throwingError = nil + } + do { + try self.init(compositeFrom: decoder) + } catch { + throw throwingError ?? error + } + } + + private static func singleStringDecodingContainer( + from decoder: any Decoder + ) -> (any SingleValueDecodingContainer)? { + try? decoder.singleValueContainer() + } + + private static func componentsFrom(_ string: String) -> [Int] { + string.components(separatedBy: ".").compactMap(Int.init) + } + + public func encode(to encoder: any Encoder) throws { + let throwingError: any Swift.Error + do { + try encodeAsString(to: encoder) + return + } catch { + throwingError = error + } + do { + try encodeAsComposite(to: encoder) + } catch { + throw throwingError + } + } + + internal func string(trimZeroPatch: Bool = false) -> String { + let values: [Int?] = [ + majorVersion, + minorVersion, + (!trimZeroPatch || patchVersion > 0) ? patchVersion : nil + ] + return values.compactMap { + $0?.description + } + .joined(separator: ".") + } + + private func encodeAsString(to encoder: any Encoder) throws { + var container = encoder.singleValueContainer() + try container.encode(description) + } + + private func encodeAsComposite(to encoder: any Encoder) throws { + var container = encoder.container(keyedBy: Self.CodingKeys.self) + try container.encode(majorVersion, forKey: .majorVersion) + try container.encode(minorVersion, forKey: .minorVersion) + try container.encode(patchVersion, forKey: .patchVersion) + } +} diff --git a/Sources/IPSWDownloads/OperatingSystemVersion.swift b/Sources/IPSWDownloads/OperatingSystemVersion+Hashable.swift similarity index 57% rename from Sources/IPSWDownloads/OperatingSystemVersion.swift rename to Sources/IPSWDownloads/OperatingSystemVersion+Hashable.swift index 1fc8536..e1bcb9a 100644 --- a/Sources/IPSWDownloads/OperatingSystemVersion.swift +++ b/Sources/IPSWDownloads/OperatingSystemVersion+Hashable.swift @@ -1,8 +1,8 @@ // -// OperatingSystemVersion.swift +// OperatingSystemVersion+Hashable.swift // IPSWDownloads // -// Created by OperatingSystemVersion.swift +// Created by Leo Dion. // Copyright © 2024 BrightDigit. // // Permission is hereby granted, free of charge, to any person @@ -29,18 +29,35 @@ import Foundation -extension OperatingSystemVersion { - internal init(string: String) throws { - let components = string.components(separatedBy: ".").compactMap(Int.init) +extension OperatingSystemVersion: + Hashable, + Equatable, + Comparable { + public static func == ( + lhs: OperatingSystemVersion, + rhs: OperatingSystemVersion + ) -> Bool { + lhs.majorVersion == rhs.majorVersion && + lhs.minorVersion == rhs.minorVersion && + lhs.patchVersion == rhs.patchVersion + } - guard components.count == 2 || components.count == 3 else { - throw RuntimeError.invalidVersion(string) + public static func < ( + lhs: OperatingSystemVersion, + rhs: OperatingSystemVersion + ) -> Bool { + guard lhs.majorVersion == rhs.majorVersion else { + return lhs.majorVersion < rhs.majorVersion + } + guard lhs.minorVersion == rhs.minorVersion else { + return lhs.minorVersion < rhs.minorVersion } + return lhs.patchVersion < rhs.patchVersion + } - self.init( - majorVersion: components[0], - minorVersion: components[1], - patchVersion: components.count == 3 ? components[2] : 0 - ) + public func hash(into hasher: inout Hasher) { + majorVersion.hash(into: &hasher) + minorVersion.hash(into: &hasher) + patchVersion.hash(into: &hasher) } } diff --git a/Sources/IPSWDownloads/RuntimeError.swift b/Sources/IPSWDownloads/RuntimeError.swift index 4cefb55..b5429f1 100644 --- a/Sources/IPSWDownloads/RuntimeError.swift +++ b/Sources/IPSWDownloads/RuntimeError.swift @@ -2,7 +2,7 @@ // RuntimeError.swift // IPSWDownloads // -// Created by RuntimeError.swift +// Created by Leo Dion. // Copyright © 2024 BrightDigit. // // Permission is hereby granted, free of charge, to any person @@ -29,7 +29,7 @@ import Foundation -internal enum RuntimeError: Error { +internal enum RuntimeError: Error, Sendable { case invalidURL(String) case invalidVersion(String) case invalidDataHexString(String) diff --git a/Sources/IPSWDownloads/URL.swift b/Sources/IPSWDownloads/URL.swift index 98d4c0f..5c8fdd7 100644 --- a/Sources/IPSWDownloads/URL.swift +++ b/Sources/IPSWDownloads/URL.swift @@ -2,7 +2,7 @@ // URL.swift // IPSWDownloads // -// Created by URL.swift +// Created by Leo Dion. // Copyright © 2024 BrightDigit. // // Permission is hereby granted, free of charge, to any person diff --git a/Tests/IPSWDownloadsTests/DataTest.swift b/Tests/IPSWDownloadsTests/DataTest.swift index f5fe7c7..a8206ed 100644 --- a/Tests/IPSWDownloadsTests/DataTest.swift +++ b/Tests/IPSWDownloadsTests/DataTest.swift @@ -2,7 +2,7 @@ // DataTest.swift // IPSWDownloads // -// Created by DataTest.swift +// Created by Leo Dion. // Copyright © 2024 BrightDigit. // // Permission is hereby granted, free of charge, to any person @@ -82,7 +82,7 @@ public class DataTests: XCTestCase { XCTAssertEqual(string, actual) } } - + func testInitStringEmpty() throws { try XCTAssertNil(Data(hexString: "", emptyIsNil: true)) } diff --git a/Tests/IPSWDownloadsTests/IPSWDownloadsTest.swift b/Tests/IPSWDownloadsTests/IPSWDownloadsTest.swift index f22804d..257db20 100644 --- a/Tests/IPSWDownloadsTests/IPSWDownloadsTest.swift +++ b/Tests/IPSWDownloadsTests/IPSWDownloadsTest.swift @@ -2,7 +2,7 @@ // IPSWDownloadsTest.swift // IPSWDownloads // -// Created by IPSWDownloadsTest.swift +// Created by Leo Dion. // Copyright © 2024 BrightDigit. // // Permission is hereby granted, free of charge, to any person diff --git a/Tests/IPSWDownloadsTests/OperatingSystemVersionTest.swift b/Tests/IPSWDownloadsTests/OperatingSystemVersionTest.swift index cd4b1c0..4ee33fd 100644 --- a/Tests/IPSWDownloadsTests/OperatingSystemVersionTest.swift +++ b/Tests/IPSWDownloadsTests/OperatingSystemVersionTest.swift @@ -2,7 +2,7 @@ // OperatingSystemVersionTest.swift // IPSWDownloads // -// Created by OperatingSystemVersionTest.swift +// Created by Leo Dion. // Copyright © 2024 BrightDigit. // // Permission is hereby granted, free of charge, to any person @@ -30,13 +30,7 @@ @testable import IPSWDownloads import XCTest -extension OperatingSystemVersion: Equatable { - public static func == (lhs: OperatingSystemVersion, rhs: OperatingSystemVersion) -> Bool { - lhs.majorVersion == rhs.majorVersion && - lhs.minorVersion == rhs.minorVersion && - lhs.patchVersion == rhs.patchVersion - } - +extension OperatingSystemVersion { static func random() -> OperatingSystemVersion { .init( majorVersion: .random(in: 1 ... 25), @@ -45,19 +39,33 @@ extension OperatingSystemVersion: Equatable { ) } - func string(trimZeroPatch: Bool) -> String { - let values: [Int?] = [ + func jsonStringText() -> String { + """ + { + "majorVersion" : \(majorVersion), + "minorVersion" : \(minorVersion), + "patchVersion" : \(patchVersion), + } + """ + } + + func jsonString(using encoding: String.Encoding = .utf8) -> Data? { + jsonStringText().data(using: encoding) + } + + func semverString(trimZeroPatch: Bool, using _: String.Encoding = .utf8) -> Data? { + let description = string(trimZeroPatch: trimZeroPatch) + return "\"\(description)\"".data(using: .utf8) + } + + func intArrayString(trimZeroPatch: Bool, using encoding: String.Encoding = .utf8) -> Data? { + let values: [Int] = [ majorVersion, minorVersion, (!trimZeroPatch || patchVersion > 0) ? patchVersion : nil - ] - return values.compactMap { - $0?.description - }.joined(separator: ".") - } - - func parsed(trimZeroPatch: Bool) throws -> OperatingSystemVersion { - try .init(string: string(trimZeroPatch: trimZeroPatch)) + ].compactMap { $0 } + let listString: String = values.map(\.description).joined(separator: ",") + return "[\(listString)]".data(using: encoding) } } @@ -68,8 +76,92 @@ public class OperatingSystemVersionTests: XCTestCase { .random() } for value in values { - try XCTAssertEqual(value, value.parsed(trimZeroPatch: false)) - try XCTAssertEqual(value, value.parsed(trimZeroPatch: true)) + try XCTAssertEqual(value, + .init(string: + value.description)) + try XCTAssertEqual(value, + .init(string: + value.string(trimZeroPatch: false))) + try XCTAssertEqual(value, + .init(string: value.string(trimZeroPatch: true))) + } + } + + func testInitIntComponents() throws { + let validCount = Int.random(in: 20 ... 50) + let values: [OperatingSystemVersion] = (0 ..< validCount).map { _ in + .random() + } + + for value in values { + try XCTAssertEqual( + .init(components: [value.majorVersion, value.minorVersion, value.patchVersion]), value + ) + guard value.patchVersion == 0 else { + continue + } + + try XCTAssertEqual( + .init(components: [value.majorVersion, value.minorVersion]), value + ) + } + } + + func testInitIntFailure() throws { + let invalidCount = Int.random(in: 20 ... 50) + let componentSets: [[Int]] = (0 ..< invalidCount).map { _ in + let value = Int.random(in: 2 ... 10) + let count = value <= 3 ? value - 2 : value + return (0 ..< count).map { _ in + .random(in: 0 ... 100) + } + } + + for components in componentSets { + XCTAssertThrowsError(try OperatingSystemVersion(components: components)) { error in + let error = error as? OperatingSystemVersion.Error + let count: Int? = if case let .invalidComponentsCount(value) = error { + value + } else { + nil + } + XCTAssertEqual(count, components.count) + } + } + } + + func testCompare() throws { + let validCount = Int.random(in: 20 ... 50) + let values: [OperatingSystemVersion] = (0 ..< validCount).map { _ in + .random() + } + + for value in values { + let greaterThanMajor = OperatingSystemVersion( + majorVersion: value.majorVersion + 1, + minorVersion: value.minorVersion, + patchVersion: value.patchVersion + ) + let greaterThanMinor = OperatingSystemVersion( + majorVersion: value.majorVersion, + minorVersion: value.minorVersion + 1, + patchVersion: value.patchVersion + ) + let greaterThanPatch = OperatingSystemVersion( + majorVersion: value.majorVersion, + minorVersion: value.minorVersion, + patchVersion: value.patchVersion + 1 + ) + let lessThanMajor = OperatingSystemVersion( + majorVersion: value.majorVersion - 1, + minorVersion: value.minorVersion, + patchVersion: value.patchVersion + ) + + XCTAssertGreaterThan(greaterThanMajor, value) + XCTAssertGreaterThan(greaterThanMinor, value) + XCTAssertGreaterThan(greaterThanPatch, value) + XCTAssertGreaterThan(value, lessThanMajor) } } @@ -88,4 +180,78 @@ public class OperatingSystemVersionTests: XCTestCase { XCTAssertEqual(string, actual) } } + + func testHashable() { + let validCount = Int.random(in: 20 ... 50) + let values: [OperatingSystemVersion] = (0 ..< validCount).map { _ in + .random() + } + for value in values { + XCTAssertNotNil(value.hashValue) + } + } + + func testDecoding() throws { + let validCount = Int.random(in: 20 ... 50) + let values: [OperatingSystemVersion] = (0 ..< validCount).map { _ in + .random() + } + let decoder = JSONDecoder() + for value in values { + XCTAssertEqual( + value, + try decoder.decodeVersion(value, with: { $0.jsonString(using: $1) }) + ) + XCTAssertEqual( + value, + try decoder.decodeVersion(value, with: { $0.intArrayString(trimZeroPatch: false, using: $1) }) + ) + XCTAssertEqual( + value, + try decoder.decodeVersion(value, with: { $0.intArrayString(trimZeroPatch: true, using: $1) }) + ) + XCTAssertEqual( + value, + try decoder.decodeVersion(value, with: { $0.semverString(trimZeroPatch: false, using: $1) }) + ) + XCTAssertEqual( + value, + try decoder.decodeVersion(value, with: { $0.semverString(trimZeroPatch: true, using: $1) }) + ) + } + } + + func testEncoder() throws { + let encoder = JSONEncoder() + + let validCount = Int.random(in: 20 ... 50) + let values: [OperatingSystemVersion] = (0 ..< validCount).map { _ in + .random() + } + for value in values { + try XCTAssertEqual("\"\(value.description)\"", encoder.encodeVersion(value)) + } + } +} + +extension JSONEncoder { + func encodeVersion( + _ version: OperatingSystemVersion + ) throws -> String? { + let data = try encode(version) + return String(data: data, encoding: .utf8) + } +} + +extension JSONDecoder { + func decodeVersion( + _ version: OperatingSystemVersion, + using encoding: String.Encoding = .utf8, + with closure: @escaping (OperatingSystemVersion, String.Encoding) -> Data? + ) throws -> OperatingSystemVersion? { + let data = closure(version, encoding) + return try data.map { + try self.decode(OperatingSystemVersion.self, from: $0) + } + } }