From f2d557f9dfc868c18a6d82d1f5172e5752c4180c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=86=80=E5=8D=93=E7=96=8C?= <55120045+WowbaggersLiquidLunch@users.noreply.github.com> Date: Sat, 27 Nov 2021 14:44:59 +0800 Subject: [PATCH 1/3] correct semantic version comparison The logic is mostly pulled right from SwiftPM's `Version`'s and TSC's `Version`'s, with some improvements. Now semantic versions compare correctly, by taking into account the pre-release and build metadata identifiers. A lot of test cases are added. 1 initialiser is deprecated. TODO: disallow empty identifiers TODO: disallow leading zeros in numeric identifiers Note on `SemanticVersion`'s `Codable` conformance: empty arrays of pre-release and build metadata identifiers are not encoded, for round-trip equality empty strings in pre-release and build metadata identifiers are encoded, for round-trip equality and consistency with SwiftPM and TSC pre-release and build metadata arrays containing single empty string precede empty arrays in comparison, for consistency with SwiftPM and TSC. SemVer 2.0.0 is ambiguous about this. Can do follow-up commit/pr for this. Not guaranteed that SwiftPM and TSC can accept an alternative comparison rule. proceed to next pair of identifiers if current pair is numerically equal --- .../SymbolGraph/Misc/SemanticVersion.swift | 218 +++- .../LineList/SemanticVersionTests.swift | 26 - .../SymbolGraph/SemanticVersionTests.swift | 952 ++++++++++++++++++ 3 files changed, 1125 insertions(+), 71 deletions(-) delete mode 100644 Tests/SymbolKitTests/LineList/SemanticVersionTests.swift create mode 100644 Tests/SymbolKitTests/SymbolGraph/SemanticVersionTests.swift diff --git a/Sources/SymbolKit/SymbolGraph/Misc/SemanticVersion.swift b/Sources/SymbolKit/SymbolGraph/Misc/SemanticVersion.swift index d84c1a5..2cf834a 100644 --- a/Sources/SymbolKit/SymbolGraph/Misc/SemanticVersion.swift +++ b/Sources/SymbolKit/SymbolGraph/Misc/SemanticVersion.swift @@ -10,60 +10,188 @@ extension SymbolGraph { /// A [semantic version](https://semver.org). - public struct SemanticVersion: Codable, Equatable, CustomStringConvertible { - /** - * The major version number. - * - * For example, the `1` in `1.2.3` - */ - public var major: Int - /** - * The minor version number. - * - * For example, the `2` in `1.2.3` - */ - public var minor: Int - /** - * The patch version number. - * - * For example, the `3` in `1.2.3` - */ - public var patch: Int - - /// The optional prerelease version component, which may contain non-numeric characters. - /// - /// For example, the `4` in `1.2.3-4`. - public var prerelease: String? - - /// Optional build metadata. - public var buildMetadata: String? - - public init(major: Int, minor: Int, patch: Int, prerelease: String? = nil, buildMetadata: String? = nil) { + public struct SemanticVersion { + /// The major version according to the semantic versioning standard. + public let major: Int + /// The minor version according to the semantic versioning standard. + public let minor: Int + /// The patch version according to the semantic versioning standard. + public let patch: Int + /// The pre-release identifier according to the semantic versioning standard, such as `-beta.1`. + public let prereleaseIdentifiers: [String] + /// The build metadata of this version according to the semantic versioning standard, such as a commit hash. + public let buildMetadataIdentifiers: [String] + + /// Initializes a semantic version struct with the provided components of a semantic version. + /// - Parameters: + /// - major: The major version number. + /// - minor: The minor version number. + /// - patch: The patch version number. + /// - prereleaseIdentifiers: The pre-release identifiers. + /// - buildMetaDataIdentifiers: Build metadata that identifies a build. + public init( + _ major: Int, + _ minor: Int, + _ patch: Int, + prereleaseIdentifiers: [String] = [], + buildMetadataIdentifiers: [String] = [] + ) { + precondition(major >= 0 && minor >= 0 && patch >= 0, "Negative versioning is invalid.") + precondition( + prereleaseIdentifiers.allSatisfy { + $0.allSatisfy { $0.isASCII && ($0.isLetter || $0.isNumber || $0 == "-") } + }, + #"Pre-release identifiers can contain only ASCII alpha-numerical characters and "-"."# + ) + precondition( + buildMetadataIdentifiers.allSatisfy { + $0.allSatisfy { $0.isASCII && ($0.isLetter || $0.isNumber || $0 == "-") } + }, + #"Build metadata identifiers can contain only ASCII alpha-numerical characters and "-"."# + ) + self.major = major self.minor = minor self.patch = patch - self.prerelease = prerelease - self.buildMetadata = buildMetadata + self.prereleaseIdentifiers = prereleaseIdentifiers + self.buildMetadataIdentifiers = buildMetadataIdentifiers + } + + /// Initializes a semantic version struct with the provided components of a semantic version. + /// - Parameters: + /// - major: The major version number. + /// - minor: The minor version number. + /// - patch: The patch version number. + /// - prereleaseIdentifiers: The "."-separated pre-release identifiers. + /// - buildMetaDataIdentifiers: The "."-separated build metadata that identifies a build. + @_disfavoredOverload + public init( + _ major: Int, + _ minor: Int, + _ patch: Int, + prereleaseIdentifiers: String? = nil, + buildMetadataIdentifiers: String? = nil + ) { + self.init( + major, minor, patch, + prereleaseIdentifiers: prereleaseIdentifiers? + .split(separator: ".", omittingEmptySubsequences: false) + .map { String($0) } ?? [], + buildMetadataIdentifiers: buildMetadataIdentifiers? + .split(separator: ".", omittingEmptySubsequences: false) + .map { String($0) } ?? [] + ) + } + + @available(*, deprecated, renamed: "init(_:_:_:prereleaseIdentifiers:buildMetadataIdentifiers:)") + public init(major: Int, minor: Int, patch: Int, prerelease: String? = nil, buildMetadata: String? = nil) { + self.init(major, minor, patch, prereleaseIdentifiers: prerelease, buildMetadataIdentifiers: buildMetadata) } + } +} - public init(from decoder: Decoder) throws { - let container = try decoder.container(keyedBy: CodingKeys.self) - self.major = try container.decode(Int.self, forKey: .major) - self.minor = try container.decodeIfPresent(Int.self, forKey: .minor) ?? 0 - self.patch = try container.decodeIfPresent(Int.self, forKey: .patch) ?? 0 - self.prerelease = try container.decodeIfPresent(String.self, forKey: .prerelease) - self.buildMetadata = try container.decodeIfPresent(String.self, forKey: .buildMetadata) +extension SymbolGraph.SemanticVersion: Codable { + + // FIXME: Should this be public? + private enum CodingKeys: String, CodingKey { + case major + case minor + case patch + case prereleaseIdentifiers = "prerelease" + case buildMetadataIdentifiers = "buildMetadata" + } + + public init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.major = try container.decode(Int.self, forKey: .major) + self.minor = try container.decodeIfPresent(Int.self, forKey: .minor) ?? 0 + self.patch = try container.decodeIfPresent(Int.self, forKey: .patch) ?? 0 + self.prereleaseIdentifiers = try container.decodeIfPresent(String.self, forKey: .prereleaseIdentifiers)? + .split(separator: ".", omittingEmptySubsequences: false) + .map { String($0) } ?? [] + self.buildMetadataIdentifiers = try container.decodeIfPresent(String.self, forKey: .buildMetadataIdentifiers)? + .split(separator: ".", omittingEmptySubsequences: false) + .map { String($0) } ?? [] + } + + public func encode(to encoder: Encoder) throws { + var container = encoder.container(keyedBy: CodingKeys.self) + try container.encode(major, forKey: .major) + try container.encode(minor, forKey: .minor) + try container.encode(patch, forKey: .patch) + if !prereleaseIdentifiers.isEmpty { + try container.encode(prereleaseIdentifiers.joined(separator: "."), forKey: .prereleaseIdentifiers) } + if !buildMetadataIdentifiers.isEmpty { + try container.encode(buildMetadataIdentifiers.joined(separator: "."), forKey: .buildMetadataIdentifiers) + } + } + +} - public var description: String { - var result = "\(major).\(minor).\(patch)" - if let prerelease = prerelease { - result += "-\(prerelease)" +extension SymbolGraph.SemanticVersion: Comparable { + // Although `Comparable` inherits from `Equatable`, it does not provide a new default implementation of `==`, but instead uses `Equatable`'s default synthesised implementation. The compiler-synthesised `==`` is composed of [member-wise comparisons](https://github.com/apple/swift-evolution/blob/main/proposals/0185-synthesize-equatable-hashable.md#implementation-details), which leads to a false `false` when 2 semantic versions differ by only their build metadata identifiers, contradicting SemVer 2.0.0's [comparison rules](https://semver.org/#spec-item-10). + @inlinable + public static func == (lhs: Self, rhs: Self) -> Bool { + !(lhs < rhs) && !(lhs > rhs) + } + + public static func < (lhs: Self, rhs: Self) -> Bool { + let lhsComparators = [lhs.major, lhs.minor, lhs.patch] + let rhsComparators = [rhs.major, rhs.minor, rhs.patch] + + guard lhsComparators == rhsComparators else { + return lhsComparators.lexicographicallyPrecedes(rhsComparators) + } + + guard lhs.prereleaseIdentifiers.count > 0 else { + return false // non-pre-release lhs >= potentially pre-release rhs + } + + guard rhs.prereleaseIdentifiers.count > 0 else { + return true // pre-release lhs < non-pre-release rhs + } + + for (lhsPrereleaseIdentifier, rhsPrereleaseIdentifier) in zip(lhs.prereleaseIdentifiers, rhs.prereleaseIdentifiers) { + guard lhsPrereleaseIdentifier != rhsPrereleaseIdentifier else { + continue } - if let buildMetadata = buildMetadata { - result += "+\(buildMetadata)" + + // Check if either of the 2 pre-release identifiers is numeric. + let lhsNumericPrereleaseIdentifier = Int(lhsPrereleaseIdentifier) + let rhsNumericPrereleaseIdentifier = Int(rhsPrereleaseIdentifier) + + if let lhsNumericPrereleaseIdentifier = lhsNumericPrereleaseIdentifier, + let rhsNumericPrereleaseIdentifier = rhsNumericPrereleaseIdentifier { + // Semantic Versioning 2.0.0 considers 2 pre-release identifiers equal, if they're numerically equal _or_ textually equal. In other words, if 2 identifiers are numeric, they are unequal _if and only if_ they're numerically unequal. Identifiers that have entered this conditional block must have not been textually equal, but it is still possible for them to be numerically equal. For example: "100" and "00100" are textually unequal but numerically equal. If the 2 identifiers in comparison are indeed equal, then unless they're the last pair of pre-release identifiers, they cannot be the deciding pair for the precedence between the 2 semantic versions. + if lhsNumericPrereleaseIdentifier == rhsNumericPrereleaseIdentifier { + continue + } else { + return lhsNumericPrereleaseIdentifier < rhsNumericPrereleaseIdentifier + } + } else if lhsNumericPrereleaseIdentifier != nil { + return true // numeric pre-release < non-numeric pre-release + } else if rhsNumericPrereleaseIdentifier != nil { + return false // non-numeric pre-release > numeric pre-release + } else { + return lhsPrereleaseIdentifier < rhsPrereleaseIdentifier } - return result } + + return lhs.prereleaseIdentifiers.count < rhs.prereleaseIdentifiers.count + } +} + +extension SymbolGraph.SemanticVersion: CustomStringConvertible { + /// A textual description of the `Semantic Version` instance. + public var description: String { + var versionString = "\(major).\(minor).\(patch)" + if !prereleaseIdentifiers.isEmpty { + versionString += "-" + prereleaseIdentifiers.joined(separator: ".") + } + if !buildMetadataIdentifiers.isEmpty { + versionString += "+" + buildMetadataIdentifiers.joined(separator: ".") + } + return versionString } } diff --git a/Tests/SymbolKitTests/LineList/SemanticVersionTests.swift b/Tests/SymbolKitTests/LineList/SemanticVersionTests.swift deleted file mode 100644 index a0484ff..0000000 --- a/Tests/SymbolKitTests/LineList/SemanticVersionTests.swift +++ /dev/null @@ -1,26 +0,0 @@ -/* - This source file is part of the Swift.org open source project - - Copyright (c) 2021 Apple Inc. and the Swift project authors - Licensed under Apache License v2.0 with Runtime Library Exception - - See https://swift.org/LICENSE.txt for license information - See https://swift.org/CONTRIBUTORS.txt for Swift project authors -*/ - -import XCTest -@testable import SymbolKit - -class SemanticVersionTests: XCTestCase { - typealias SemanticVersion = SymbolGraph.SemanticVersion - - func testVersionInit() { - let version = SemanticVersion(major: 1, minor: 2, patch: 3, prerelease: "beta", buildMetadata: "enableX") - - XCTAssertEqual(version.major, 1) - XCTAssertEqual(version.minor, 2) - XCTAssertEqual(version.patch, 3) - XCTAssertEqual(version.prerelease, "beta") - XCTAssertEqual(version.buildMetadata, "enableX") - } -} diff --git a/Tests/SymbolKitTests/SymbolGraph/SemanticVersionTests.swift b/Tests/SymbolKitTests/SymbolGraph/SemanticVersionTests.swift new file mode 100644 index 0000000..d0bb4d8 --- /dev/null +++ b/Tests/SymbolKitTests/SymbolGraph/SemanticVersionTests.swift @@ -0,0 +1,952 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2021 Apple Inc. and the Swift project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information + See https://swift.org/CONTRIBUTORS.txt for Swift project authors + */ + +import XCTest +@testable import SymbolKit + +final class SemanticVersionTests: XCTestCase { + + typealias Version = SymbolGraph.SemanticVersion + + func testVersionInitialization() { + let v0 = Version(0, 0, 0, prereleaseIdentifiers: [], buildMetadataIdentifiers: []) + XCTAssertEqual(v0.minor, 0) + XCTAssertEqual(v0.minor, 0) + XCTAssertEqual(v0.patch, 0) + XCTAssertEqual(v0.prereleaseIdentifiers, []) + XCTAssertEqual(v0.buildMetadataIdentifiers, []) + + let v1 = Version(1, 1, 2, prereleaseIdentifiers: ["3", "5"], buildMetadataIdentifiers: ["8", "13"]) + XCTAssertEqual(v1.minor, 1) + XCTAssertEqual(v1.minor, 1) + XCTAssertEqual(v1.patch, 2) + XCTAssertEqual(v1.prereleaseIdentifiers, ["3", "5"]) + XCTAssertEqual(v1.buildMetadataIdentifiers, ["8", "13"]) + + XCTAssertEqual( + Version(3, 5, 8), + Version(3, 5, 8, prereleaseIdentifiers: [], buildMetadataIdentifiers: []) + ) + + XCTAssertEqual( + Version(13, 21, 34, prereleaseIdentifiers: ["55"]), + Version(13, 21, 34, prereleaseIdentifiers: ["55"], buildMetadataIdentifiers: []) + ) + + XCTAssertEqual( + Version(89, 144, 233, buildMetadataIdentifiers: ["377"]), + Version(89, 144, 233, prereleaseIdentifiers: [], buildMetadataIdentifiers: ["377"]) + ) + } + + func testDecodingFromJSONToVersion() { + let jsonDecoder = JSONDecoder() + + let versionObjectString1 = """ + { + "major": 1 + } + """ + let versionObjectData1 = Data(versionObjectString1.utf8) + let version1 = Version(1, 0, 0) + XCTAssertNoThrow( + try { + let decodedVersion1 = try jsonDecoder.decode(Version.self, from: versionObjectData1) + XCTAssertEqual(version1, decodedVersion1) + }() + ) + + let versionObjectString2 = """ + { + "major": 1, + "minor": 42 + } + """ + let versionObjectData2 = Data(versionObjectString2.utf8) + let version2 = Version(1, 42, 0) + XCTAssertNoThrow( + try { + let decodedVersion2 = try jsonDecoder.decode(Version.self, from: versionObjectData2) + XCTAssertEqual(version2, decodedVersion2) + }() + ) + + let versionObjectString3 = """ + { + "major": 4, + "patch": 2 + } + """ + let versionObjectData3 = Data(versionObjectString3.utf8) + let version3 = Version(4, 0, 2) + XCTAssertNoThrow( + try { + let decodedVersion3 = try jsonDecoder.decode(Version.self, from: versionObjectData3) + XCTAssertEqual(version3, decodedVersion3) + }() + ) + + let versionObjectString4 = """ + { + "major": 100, + "prerelease": "bvf7yuv" + } + """ + let versionObjectData4 = Data(versionObjectString4.utf8) + let version4 = Version( + 100, 0, 0, + prereleaseIdentifiers: ["bvf7yuv"] + ) + XCTAssertNoThrow( + try { + let decodedVersion4 = try jsonDecoder.decode(Version.self, from: versionObjectData4) + XCTAssertEqual(version4, decodedVersion4) + }() + ) + + let versionObjectString5 = """ + { + "major": 99999, + "buildMetadata": "UZryk-09btxguch" + } + """ + let versionObjectData5 = Data(versionObjectString5.utf8) + let version5 = Version( + 99999, 0, 0, + buildMetadataIdentifiers: ["UZryk-09btxguch"] + ) + XCTAssertNoThrow( + try { + let decodedVersion5 = try jsonDecoder.decode(Version.self, from: versionObjectData5) + XCTAssertEqual(version5, decodedVersion5) + }() + ) + + let versionObjectString6 = """ + { + "major": 1, + "minor": 2, + "patch": 3, + "prerelease": "abc.def-ghi", + "buildMetadata": "xyz.abc-def.gf765c7v.7867ft.ghi--uvw" + } + """ + let versionObjectData6 = Data(versionObjectString6.utf8) + let version6 = Version( + 1, 2, 3, + prereleaseIdentifiers: ["abc", "def-ghi"], + buildMetadataIdentifiers: ["xyz", "abc-def", "gf765c7v", "7867ft", "ghi--uvw"] + ) + XCTAssertNoThrow( + try { + let decodedVersion6 = try jsonDecoder.decode(Version.self, from: versionObjectData6) + XCTAssertEqual(version6, decodedVersion6) + }() + ) + + let versionObjectString7 = """ + { + "major": 1, + "minor": 2, + "patch": 3, + "prerelease": "" + } + """ + let versionObjectData7 = Data(versionObjectString7.utf8) + let version7 = Version( + 1, 2, 3, + prereleaseIdentifiers: [""], + buildMetadataIdentifiers: [] + ) + XCTAssertNoThrow( + try { + let decodedVersion7 = try jsonDecoder.decode(Version.self, from: versionObjectData7) + XCTAssertEqual(version7, decodedVersion7) + }() + ) + + let versionObjectString8 = """ + { + "major": 1, + "minor": 2, + "patch": 3, + "buildMetadata": "" + } + """ + let versionObjectData8 = Data(versionObjectString8.utf8) + let version8 = Version( + 1, 2, 3, + prereleaseIdentifiers: [], + buildMetadataIdentifiers: [""] + ) + XCTAssertNoThrow( + try { + let decodedVersion8 = try jsonDecoder.decode(Version.self, from: versionObjectData8) + XCTAssertEqual(version8, decodedVersion8) + }() + ) + + let versionObjectString9 = """ + { + "major": 1, + "minor": 2, + "patch": 3, + "prerelease": "", + "buildMetadata": "" + } + """ + let versionObjectData9 = Data(versionObjectString9.utf8) + let version9 = Version( + 1, 2, 3, + prereleaseIdentifiers: [""], + buildMetadataIdentifiers: [""] + ) + XCTAssertNoThrow( + try { + let decodedVersion9 = try jsonDecoder.decode(Version.self, from: versionObjectData9) + XCTAssertEqual(version9, decodedVersion9) + }() + ) + } + + func testEncodingFromVersionToJSON() { + let jsonEncoder1 = JSONEncoder() + jsonEncoder1.outputFormatting = [.withoutEscapingSlashes, .sortedKeys] + let versionObjectString1 = """ + { + "major": 1, + "minor": 0, + "patch": 0 + } + """ + let version1 = Version(1, 0, 0) + XCTAssertNoThrow( + try { + let encodedVersion1 = try jsonEncoder1.encode(version1) + XCTAssertEqual( + String(data: encodedVersion1, encoding: .utf8), + // Because Semantic Versioning 2.0.0 does not allow whitespace in identifiers, we can remove whitespace from the string with no worry. + versionObjectString1.filter { !$0.isWhitespace } + ) + }() + ) + + let jsonEncoder2 = JSONEncoder() + jsonEncoder2.outputFormatting = [.withoutEscapingSlashes, .sortedKeys] + let versionObjectString2 = """ + { + "buildMetadata": "", + "major": 1, + "minor": 0, + "patch": 0, + "prerelease": "" + } + """ + let version2 = Version(1, 0, 0, prereleaseIdentifiers: [""], buildMetadataIdentifiers: [""]) + XCTAssertNoThrow( + try { + let encodedVersion2 = try jsonEncoder2.encode(version2) + XCTAssertEqual( + String(data: encodedVersion2, encoding: .utf8), + // Because Semantic Versioning 2.0.0 does not allow whitespace in identifiers, we can remove whitespace from the string with no worry. + versionObjectString2.filter { !$0.isWhitespace } + ) + }() + ) + + let jsonEncoder3 = JSONEncoder() + jsonEncoder3.outputFormatting = [.withoutEscapingSlashes, .sortedKeys] + let versionObjectString3 = """ + { + "major": 1, + "minor": 42, + "patch": 0 + } + """ + let version3 = Version(1, 42, 0) + XCTAssertNoThrow( + try { + let encodedVersion3 = try jsonEncoder3.encode(version3) + XCTAssertEqual( + String(data: encodedVersion3, encoding: .utf8), + versionObjectString3.filter { !$0.isWhitespace } + ) + }() + ) + + let jsonEncoder4 = JSONEncoder() + jsonEncoder4.outputFormatting = [.withoutEscapingSlashes, .sortedKeys] + let versionObjectString4 = """ + { + "buildMetadata": "", + "major": 1, + "minor": 42, + "patch": 0, + "prerelease": "" + } + """ + let version4 = Version(1, 42, 0, prereleaseIdentifiers: [""], buildMetadataIdentifiers: [""]) + XCTAssertNoThrow( + try { + let encodedVersion4 = try jsonEncoder4.encode(version4) + XCTAssertEqual( + String(data: encodedVersion4, encoding: .utf8), + versionObjectString4.filter { !$0.isWhitespace } + ) + }() + ) + + let jsonEncoder5 = JSONEncoder() + jsonEncoder5.outputFormatting = [.withoutEscapingSlashes, .sortedKeys] + let versionObjectString5 = """ + { + "major": 4, + "minor": 0, + "patch": 2 + } + """ + let version5 = Version(4, 0, 2) + XCTAssertNoThrow( + try { + let encodedVersion5 = try jsonEncoder5.encode(version5) + XCTAssertEqual( + String(data: encodedVersion5, encoding: .utf8), + versionObjectString5.filter { !$0.isWhitespace } + ) + }() + ) + + let jsonEncoder6 = JSONEncoder() + jsonEncoder6.outputFormatting = [.withoutEscapingSlashes, .sortedKeys] + let versionObjectString6 = """ + { + "buildMetadata": "", + "major": 4, + "minor": 0, + "patch": 2, + "prerelease": "" + } + """ + let version6 = Version(4, 0, 2, prereleaseIdentifiers: [""], buildMetadataIdentifiers: [""]) + XCTAssertNoThrow( + try { + let encodedVersion6 = try jsonEncoder6.encode(version6) + XCTAssertEqual( + String(data: encodedVersion6, encoding: .utf8), + versionObjectString6.filter { !$0.isWhitespace } + ) + }() + ) + + let jsonEncoder7 = JSONEncoder() + jsonEncoder7.outputFormatting = [.withoutEscapingSlashes, .sortedKeys] + let versionObjectString7 = """ + { + "major": 100, + "minor": 0, + "patch": 0, + "prerelease": "-j9uh08y97" + } + """ + let version7 = Version( + 100, 0, 0, + prereleaseIdentifiers: ["-j9uh08y97"] + ) + XCTAssertNoThrow( + try { + let encodedVersion7 = try jsonEncoder7.encode(version7) + XCTAssertEqual( + String(data: encodedVersion7, encoding: .utf8), + versionObjectString7.filter { !$0.isWhitespace } + ) + }() + ) + + let jsonEncoder8 = JSONEncoder() + jsonEncoder8.outputFormatting = [.withoutEscapingSlashes, .sortedKeys] + let versionObjectString8 = """ + { + "buildMetadata": "", + "major": 100, + "minor": 0, + "patch": 0, + "prerelease": "-j9uh08y97" + } + """ + let version8 = Version( + 100, 0, 0, + prereleaseIdentifiers: ["-j9uh08y97"], + buildMetadataIdentifiers: [""] + ) + XCTAssertNoThrow( + try { + let encodedVersion8 = try jsonEncoder8.encode(version8) + XCTAssertEqual( + String(data: encodedVersion8, encoding: .utf8), + versionObjectString8.filter { !$0.isWhitespace } + ) + }() + ) + + let jsonEncoder9 = JSONEncoder() + jsonEncoder9.outputFormatting = [.withoutEscapingSlashes, .sortedKeys] + let versionObjectString9 = """ + { + "buildMetadata": "vvbcxqbo-bvy.HIu", + "major": 99999, + "minor": 0, + "patch": 0 + } + """ + let version9 = Version( + 99999, 0, 0, + buildMetadataIdentifiers: ["vvbcxqbo-bvy", "HIu"] + ) + XCTAssertNoThrow( + try { + let encodedVersion9 = try jsonEncoder9.encode(version9) + XCTAssertEqual( + String(data: encodedVersion9, encoding: .utf8), + versionObjectString9.filter { !$0.isWhitespace } + ) + }() + ) + + let jsonEncoder10 = JSONEncoder() + jsonEncoder10.outputFormatting = [.withoutEscapingSlashes, .sortedKeys] + let versionObjectString10 = """ + { + "buildMetadata": "vvbcxqbo-bvy.HIu", + "major": 99999, + "minor": 0, + "patch": 0, + "prerelease": "" + } + """ + let version10 = Version( + 99999, 0, 0, + prereleaseIdentifiers: [""], + buildMetadataIdentifiers: ["vvbcxqbo-bvy", "HIu"] + ) + XCTAssertNoThrow( + try { + let encodedVersion10 = try jsonEncoder10.encode(version10) + XCTAssertEqual( + String(data: encodedVersion10, encoding: .utf8), + versionObjectString10.filter { !$0.isWhitespace } + ) + }() + ) + + let jsonEncoder11 = JSONEncoder() + jsonEncoder11.outputFormatting = [.withoutEscapingSlashes, .sortedKeys] + let versionObjectString11 = """ + { + "buildMetadata": "xyz.abc-def.----..ghi-uvw", + "major": 1, + "minor": 2, + "patch": 3, + "prerelease": "abc.def-ghi" + } + """ + let version11 = Version( + 1, 2, 3, + prereleaseIdentifiers: ["abc", "def-ghi"], + buildMetadataIdentifiers: ["xyz", "abc-def", "----", "", "ghi-uvw"] + ) + XCTAssertNoThrow( + try { + let encodedVersion11 = try jsonEncoder11.encode(version11) + XCTAssertEqual( + String(data: encodedVersion11, encoding: .utf8), + versionObjectString11.filter { !$0.isWhitespace } + ) + }() + ) + } + + func testJSONRoundTrip() { + let jsonDecoder = JSONDecoder() + + let jsonEncoder1 = JSONEncoder() + let version1 = Version(1, 0, 0) + XCTAssertNoThrow( + try { + let roundTripVersion1 = try jsonDecoder.decode(Version.self, from: jsonEncoder1.encode(version1)) + print(version1.prereleaseIdentifiers) + print(roundTripVersion1.prereleaseIdentifiers) + XCTAssertEqual(version1, roundTripVersion1) + }() + ) + + let jsonEncoder2 = JSONEncoder() + let version2 = Version(1, 42, 0) + XCTAssertNoThrow( + try { + let roundTripVersion2 = try jsonDecoder.decode(Version.self, from: jsonEncoder2.encode(version2)) + XCTAssertEqual(version2, roundTripVersion2) + }() + ) + + let jsonEncoder3 = JSONEncoder() + let version3 = Version(4, 0, 2) + XCTAssertNoThrow( + try { + let roundTripVersion3 = try jsonDecoder.decode(Version.self, from: jsonEncoder3.encode(version3)) + XCTAssertEqual(version3, roundTripVersion3) + }() + ) + + let jsonEncoder4 = JSONEncoder() + let version4 = Version( + 100, 0, 0, + prereleaseIdentifiers: ["bvf7yuv"] + ) + XCTAssertNoThrow( + try { + let roundTripVersion4 = try jsonDecoder.decode(Version.self, from: jsonEncoder4.encode(version4)) + XCTAssertEqual(version4, roundTripVersion4) + }() + ) + + let jsonEncoder5 = JSONEncoder() + let version5 = Version( + 99999, 0, 0, + buildMetadataIdentifiers: ["UZryk-09btxguch"] + ) + XCTAssertNoThrow( + try { + let roundTripVersion5 = try jsonDecoder.decode(Version.self, from: jsonEncoder5.encode(version5)) + XCTAssertEqual(version5, roundTripVersion5) + }() + ) + + let jsonEncoder6 = JSONEncoder() + let version6 = Version( + 1, 2, 3, + prereleaseIdentifiers: ["abc", "def-ghi"], + buildMetadataIdentifiers: ["xyz", "abc-def", "gf765c7v", "7867ft", "ghi--uvw"] + ) + XCTAssertNoThrow( + try { + let roundTripVersion6 = try jsonDecoder.decode(Version.self, from: jsonEncoder6.encode(version6)) + XCTAssertEqual(version6, roundTripVersion6) + }() + ) + + let jsonEncoder7 = JSONEncoder() + let version7 = Version( + 1, 2, 3, + prereleaseIdentifiers: [""], + buildMetadataIdentifiers: [] + ) + XCTAssertNoThrow( + try { + let roundTripVersion7 = try jsonDecoder.decode(Version.self, from: jsonEncoder7.encode(version7)) + XCTAssertEqual(version7, roundTripVersion7) + }() + ) + + let jsonEncoder8 = JSONEncoder() + let version8 = Version( + 1, 2, 3, + prereleaseIdentifiers: [], + buildMetadataIdentifiers: [""] + ) + XCTAssertNoThrow( + try { + let roundTripVersion8 = try jsonDecoder.decode(Version.self, from: jsonEncoder8.encode(version8)) + XCTAssertEqual(version8, roundTripVersion8) + }() + ) + + let jsonEncoder9 = JSONEncoder() + let version9 = Version( + 1, 2, 3, + prereleaseIdentifiers: [""], + buildMetadataIdentifiers: [""] + ) + XCTAssertNoThrow( + try { + let roundTripVersion9 = try jsonDecoder.decode(Version.self, from: jsonEncoder9.encode(version9)) + XCTAssertEqual(version9, roundTripVersion9) + }() + ) + } + + func testVersionComparison() { + + // MARK: version core vs. version core + + XCTAssertGreaterThan(Version(2, 1, 1), Version(1, 2, 3)) + XCTAssertGreaterThan(Version(1, 3, 1), Version(1, 2, 3)) + XCTAssertGreaterThan(Version(1, 2, 4), Version(1, 2, 3)) + + XCTAssertFalse(Version(2, 1, 1) < Version(1, 2, 3)) + XCTAssertFalse(Version(1, 3, 1) < Version(1, 2, 3)) + XCTAssertFalse(Version(1, 2, 4) < Version(1, 2, 3)) + + // MARK: version core vs. version core + pre-release + + XCTAssertGreaterThan(Version(1, 2, 3), Version(1, 2, 3, prereleaseIdentifiers: [""])) + XCTAssertGreaterThan(Version(1, 2, 3), Version(1, 2, 3, prereleaseIdentifiers: ["beta"])) + XCTAssertFalse(Version(1, 2, 3) < Version(1, 2, 3, prereleaseIdentifiers: [""])) + XCTAssertFalse(Version(1, 2, 3) < Version(1, 2, 3, prereleaseIdentifiers: ["beta"])) + XCTAssertLessThan(Version(1, 2, 2), Version(1, 2, 3, prereleaseIdentifiers: ["beta"])) + + // MARK: version core + pre-release vs. version core + pre-release + + XCTAssertEqual(Version(1, 2, 3, prereleaseIdentifiers: [""]), Version(1, 2, 3, prereleaseIdentifiers: [""])) + + XCTAssertEqual(Version(1, 2, 3, prereleaseIdentifiers: ["beta"]), Version(1, 2, 3, prereleaseIdentifiers: ["beta"])) + XCTAssertLessThan(Version(1, 2, 3, prereleaseIdentifiers: ["alpha"]), Version(1, 2, 3, prereleaseIdentifiers: ["beta"])) + XCTAssertLessThan(Version(1, 2, 3, prereleaseIdentifiers: ["alpha1"]), Version(1, 2, 3, prereleaseIdentifiers: ["alpha2"])) + XCTAssertLessThan(Version(1, 2, 3, prereleaseIdentifiers: ["alpha"]), Version(1, 2, 3, prereleaseIdentifiers: ["alpha-"])) + XCTAssertLessThan(Version(1, 2, 3, prereleaseIdentifiers: ["beta", "alpha"]), Version(1, 2, 3, prereleaseIdentifiers: ["beta", "beta"])) + XCTAssertLessThan(Version(1, 2, 3, prereleaseIdentifiers: ["alpha", "beta"]), Version(1, 2, 3, prereleaseIdentifiers: ["beta", "alpha"])) + + XCTAssertEqual(Version(1, 2, 3, prereleaseIdentifiers: ["1"]), Version(1, 2, 3, prereleaseIdentifiers: ["1"])) + XCTAssertEqual(Version(1, 2, 3, prereleaseIdentifiers: ["1"]), Version(1, 2, 3, prereleaseIdentifiers: ["001"])) + XCTAssertLessThan(Version(1, 2, 3, prereleaseIdentifiers: ["1"]), Version(1, 2, 3, prereleaseIdentifiers: ["2"])) + XCTAssertLessThan(Version(1, 2, 3, prereleaseIdentifiers: ["1", "1"]), Version(1, 2, 3, prereleaseIdentifiers: ["1", "2"])) + XCTAssertLessThan(Version(1, 2, 3, prereleaseIdentifiers: ["001", "1"]), Version(1, 2, 3, prereleaseIdentifiers: ["1", "2"])) + XCTAssertLessThan(Version(1, 2, 3, prereleaseIdentifiers: ["1", "1"]), Version(1, 2, 3, prereleaseIdentifiers: ["001", "2"])) + XCTAssertLessThan(Version(1, 2, 3, prereleaseIdentifiers: ["1", "1"]), Version(1, 2, 3, prereleaseIdentifiers: ["1", "002"])) + XCTAssertLessThan(Version(1, 2, 3, prereleaseIdentifiers: ["1", "2"]), Version(1, 2, 3, prereleaseIdentifiers: ["2", "1"])) + + XCTAssertLessThan(Version(1, 2, 3, prereleaseIdentifiers: ["123"]), Version(1, 2, 3, prereleaseIdentifiers: ["123alpha"])) + XCTAssertLessThan(Version(1, 2, 3, prereleaseIdentifiers: ["223"]), Version(1, 2, 3, prereleaseIdentifiers: ["123alpha"])) + + XCTAssertGreaterThan(Version(1, 2, 3, prereleaseIdentifiers: ["abc"]), Version(1, 2, 3, prereleaseIdentifiers: ["123"])) + XCTAssertGreaterThan(Version(1, 2, 3, prereleaseIdentifiers: ["123abc"]), Version(1, 2, 3, prereleaseIdentifiers: ["223"])) + + XCTAssertFalse(Version(1, 2, 3, prereleaseIdentifiers: ["abc"]) < Version(1, 2, 3, prereleaseIdentifiers: ["123"])) + XCTAssertFalse(Version(1, 2, 3, prereleaseIdentifiers: ["123abc"]) < Version(1, 2, 3, prereleaseIdentifiers: ["223"])) + + XCTAssertGreaterThan(Version(1, 2, 3, prereleaseIdentifiers: ["baa"]), Version(1, 2, 3, prereleaseIdentifiers: ["azzz"])) + XCTAssertGreaterThan(Version(1, 2, 3, prereleaseIdentifiers: ["b", "z"]), Version(1, 2, 3, prereleaseIdentifiers: ["abc", "a", "zzz"])) + + XCTAssertFalse(Version(1, 2, 3, prereleaseIdentifiers: ["baa"]) < Version(1, 2, 3, prereleaseIdentifiers: ["azzz"])) + XCTAssertFalse(Version(1, 2, 3, prereleaseIdentifiers: ["b", "z"]) < Version(1, 2, 3, prereleaseIdentifiers: ["abc", "a", "zzz"])) + + // MARK: version core vs. version core + build metadata + + XCTAssertEqual(Version(1, 2, 3), Version(1, 2, 3, buildMetadataIdentifiers: [""])) + XCTAssertEqual(Version(1, 2, 3), Version(1, 2, 3, buildMetadataIdentifiers: ["beta"])) + XCTAssertLessThan(Version(1, 2, 2), Version(1, 2, 3, buildMetadataIdentifiers: ["beta"])) + + // MARK: version core + pre-release vs. version core + build metadata + + XCTAssertLessThan(Version(1, 2, 3, prereleaseIdentifiers: [""]), Version(1, 2, 3, buildMetadataIdentifiers: [""])) + XCTAssertLessThan(Version(1, 2, 3, prereleaseIdentifiers: ["beta"]), Version(1, 2, 3, buildMetadataIdentifiers: ["alpha"])) + XCTAssertLessThan(Version(1, 2, 3, prereleaseIdentifiers: ["alpha"]), Version(1, 2, 3, buildMetadataIdentifiers: ["beta"])) + XCTAssertLessThan(Version(1, 2, 3, prereleaseIdentifiers: ["beta"]), Version(1, 2, 3, buildMetadataIdentifiers: ["beta"])) + XCTAssertLessThan(Version(1, 2, 3, prereleaseIdentifiers: ["alpha"]), Version(1, 2, 3, buildMetadataIdentifiers: ["alpha-"])) + XCTAssertLessThan(Version(1, 2, 3, prereleaseIdentifiers: ["123"]), Version(1, 2, 3, buildMetadataIdentifiers: ["123alpha"])) + XCTAssertLessThan(Version(1, 2, 3, prereleaseIdentifiers: ["223"]), Version(1, 2, 3, buildMetadataIdentifiers: ["123alpha"])) + XCTAssertLessThan(Version(1, 2, 3, prereleaseIdentifiers: ["123alpha"]), Version(1, 2, 3, buildMetadataIdentifiers: ["123"])) + XCTAssertLessThan(Version(1, 2, 3, prereleaseIdentifiers: ["123alpha"]), Version(1, 2, 3, buildMetadataIdentifiers: ["223"])) + XCTAssertLessThan(Version(1, 2, 3, prereleaseIdentifiers: ["123alpha"]), Version(1, 2, 3, buildMetadataIdentifiers: ["223"])) + XCTAssertLessThan(Version(1, 2, 3, prereleaseIdentifiers: ["alpha"]), Version(1, 2, 3, buildMetadataIdentifiers: ["beta"])) + XCTAssertGreaterThan(Version(2, 2, 3, prereleaseIdentifiers: [""]), Version(1, 2, 3, buildMetadataIdentifiers: [""])) + XCTAssertGreaterThan(Version(1, 3, 3, prereleaseIdentifiers: ["alpha"]), Version(1, 2, 3, buildMetadataIdentifiers: ["beta"])) + XCTAssertGreaterThan(Version(1, 2, 4, prereleaseIdentifiers: ["223"]), Version(1, 2, 3, buildMetadataIdentifiers: ["123alpha"])) + + XCTAssertFalse(Version(2, 2, 3, prereleaseIdentifiers: [""]) < Version(1, 2, 3, buildMetadataIdentifiers: [""])) + XCTAssertFalse(Version(1, 3, 3, prereleaseIdentifiers: ["alpha"]) < Version(1, 2, 3, buildMetadataIdentifiers: ["beta"])) + XCTAssertFalse(Version(1, 2, 4, prereleaseIdentifiers: ["223"]) < Version(1, 2, 3, buildMetadataIdentifiers: ["123alpha"])) + + // MARK: version core + build metadata vs. version core + build metadata + + XCTAssertEqual(Version(1, 2, 3, buildMetadataIdentifiers: [""]), Version(1, 2, 3, buildMetadataIdentifiers: [""])) + + XCTAssertEqual(Version(1, 2, 3, buildMetadataIdentifiers: ["beta"]), Version(1, 2, 3, buildMetadataIdentifiers: ["beta"])) + XCTAssertEqual(Version(1, 2, 3, buildMetadataIdentifiers: ["alpha"]), Version(1, 2, 3, buildMetadataIdentifiers: ["beta"])) + XCTAssertEqual(Version(1, 2, 3, buildMetadataIdentifiers: ["alpha1"]), Version(1, 2, 3, buildMetadataIdentifiers: ["alpha2"])) + XCTAssertEqual(Version(1, 2, 3, buildMetadataIdentifiers: ["alpha"]), Version(1, 2, 3, buildMetadataIdentifiers: ["alpha-"])) + XCTAssertEqual(Version(1, 2, 3, buildMetadataIdentifiers: ["beta", "alpha"]), Version(1, 2, 3, buildMetadataIdentifiers: ["beta", "beta"])) + XCTAssertEqual(Version(1, 2, 3, buildMetadataIdentifiers: ["alpha", "beta"]), Version(1, 2, 3, buildMetadataIdentifiers: ["beta", "alpha"])) + + XCTAssertEqual(Version(1, 2, 3, buildMetadataIdentifiers: ["1"]), Version(1, 2, 3, buildMetadataIdentifiers: ["1"])) + XCTAssertEqual(Version(1, 2, 3, buildMetadataIdentifiers: ["1"]), Version(1, 2, 3, buildMetadataIdentifiers: ["2"])) + XCTAssertEqual(Version(1, 2, 3, buildMetadataIdentifiers: ["1", "1"]), Version(1, 2, 3, buildMetadataIdentifiers: ["1", "2"])) + XCTAssertEqual(Version(1, 2, 3, buildMetadataIdentifiers: ["1", "2"]), Version(1, 2, 3, buildMetadataIdentifiers: ["2", "1"])) + + XCTAssertEqual(Version(1, 2, 3, buildMetadataIdentifiers: ["123"]), Version(1, 2, 3, buildMetadataIdentifiers: ["123alpha"])) + XCTAssertEqual(Version(1, 2, 3, buildMetadataIdentifiers: ["223"]), Version(1, 2, 3, buildMetadataIdentifiers: ["123alpha"])) + + // MARK: version core vs. version core + pre-release + build metadata + + XCTAssertGreaterThan(Version(1, 2, 3), Version(1, 2, 3, prereleaseIdentifiers: [""], buildMetadataIdentifiers: [""])) + XCTAssertGreaterThan(Version(1, 2, 3), Version(1, 2, 3, prereleaseIdentifiers: [""], buildMetadataIdentifiers: ["123alpha"])) + XCTAssertGreaterThan(Version(1, 2, 3), Version(1, 2, 3, prereleaseIdentifiers: ["alpha"], buildMetadataIdentifiers: ["alpha"])) + XCTAssertGreaterThan(Version(1, 2, 3), Version(1, 2, 3, prereleaseIdentifiers: ["beta"], buildMetadataIdentifiers: ["123"])) + XCTAssertFalse(Version(1, 2, 3) < Version(1, 2, 3, prereleaseIdentifiers: [""], buildMetadataIdentifiers: [""])) + XCTAssertFalse(Version(1, 2, 3) < Version(1, 2, 3, prereleaseIdentifiers: [""], buildMetadataIdentifiers: ["123alpha"])) + XCTAssertFalse(Version(1, 2, 3) < Version(1, 2, 3, prereleaseIdentifiers: ["alpha"], buildMetadataIdentifiers: ["alpha"])) + XCTAssertFalse(Version(1, 2, 3) < Version(1, 2, 3, prereleaseIdentifiers: ["beta"], buildMetadataIdentifiers: ["123"])) + XCTAssertLessThan(Version(1, 2, 2), Version(1, 2, 3, prereleaseIdentifiers: ["beta"], buildMetadataIdentifiers: ["alpha", "beta"])) + XCTAssertLessThan(Version(1, 2, 2), Version(1, 2, 3, prereleaseIdentifiers: ["beta"], buildMetadataIdentifiers: ["alpha-"])) + + // MARK: version core + pre-release vs. version core + pre-release + build metadata + + XCTAssertEqual( + Version(1, 2, 3, prereleaseIdentifiers: [""]), + Version(1, 2, 3, prereleaseIdentifiers: [""], buildMetadataIdentifiers: [""]) + ) + + XCTAssertEqual( + Version(1, 2, 3, prereleaseIdentifiers: ["beta"]), + Version(1, 2, 3, prereleaseIdentifiers: ["beta"], buildMetadataIdentifiers: [""]) + ) + XCTAssertLessThan( + Version(1, 2, 3, prereleaseIdentifiers: ["alpha"]), + Version(1, 2, 3, prereleaseIdentifiers: ["beta"], buildMetadataIdentifiers: ["123alpha"]) + ) + XCTAssertLessThan( + Version(1, 2, 3, prereleaseIdentifiers: ["alpha1"]), + Version(1, 2, 3, prereleaseIdentifiers: ["alpha2"], buildMetadataIdentifiers: ["alpha"]) + ) + XCTAssertLessThan( + Version(1, 2, 3, prereleaseIdentifiers: ["alpha"]), + Version(1, 2, 3, prereleaseIdentifiers: ["alpha-"], buildMetadataIdentifiers: ["alpha", "beta"]) + ) + XCTAssertLessThan( + Version(1, 2, 3, prereleaseIdentifiers: ["beta", "alpha"]), + Version(1, 2, 3, prereleaseIdentifiers: ["beta", "beta"], buildMetadataIdentifiers: ["123"]) + ) + XCTAssertLessThan( + Version(1, 2, 3, prereleaseIdentifiers: ["alpha", "beta"]), + Version(1, 2, 3, prereleaseIdentifiers: ["beta", "alpha"], buildMetadataIdentifiers: ["alpha-"]) + ) + + XCTAssertEqual( + Version(1, 2, 3, prereleaseIdentifiers: ["1"]), + Version(1, 2, 3, prereleaseIdentifiers: ["1"], buildMetadataIdentifiers: [""]) + ) + XCTAssertEqual( + Version(1, 2, 3, prereleaseIdentifiers: ["1"]), + Version(1, 2, 3, prereleaseIdentifiers: ["001"], buildMetadataIdentifiers: [""]) + ) + XCTAssertEqual( + Version(1, 2, 3, prereleaseIdentifiers: ["0001"]), + Version(1, 2, 3, prereleaseIdentifiers: ["1"], buildMetadataIdentifiers: [""]) + ) + XCTAssertEqual( + Version(1, 2, 3, prereleaseIdentifiers: ["00001"]), + Version(1, 2, 3, prereleaseIdentifiers: ["000001"], buildMetadataIdentifiers: [""]) + ) + + XCTAssertLessThan( + Version(1, 2, 3, prereleaseIdentifiers: ["1"]), + Version(1, 2, 3, prereleaseIdentifiers: ["2"], buildMetadataIdentifiers: ["123alpha"]) + ) + XCTAssertLessThan( + Version(1, 2, 3, prereleaseIdentifiers: ["1", "1"]), + Version(1, 2, 3, prereleaseIdentifiers: ["1", "2"], buildMetadataIdentifiers: ["123"]) + ) + XCTAssertLessThan( + Version(1, 2, 3, prereleaseIdentifiers: ["1", "1"]), + Version(1, 2, 3, prereleaseIdentifiers: ["00000001", "2"], buildMetadataIdentifiers: ["123"]) + ) + XCTAssertLessThan( + Version(1, 2, 3, prereleaseIdentifiers: ["000000001", "1"]), + Version(1, 2, 3, prereleaseIdentifiers: ["1", "2"], buildMetadataIdentifiers: ["123"]) + ) + XCTAssertLessThan( + Version(1, 2, 3, prereleaseIdentifiers: ["01", "1"]), + Version(1, 2, 3, prereleaseIdentifiers: ["0000000001", "2"], buildMetadataIdentifiers: ["123"]) + ) + XCTAssertLessThan( + Version(1, 2, 3, prereleaseIdentifiers: ["1", "2"]), + Version(1, 2, 3, prereleaseIdentifiers: ["2", "1"], buildMetadataIdentifiers: ["alpha", "beta"]) + ) + + XCTAssertLessThan( + Version(1, 2, 3, prereleaseIdentifiers: ["123"]), + Version(1, 2, 3, prereleaseIdentifiers: ["123alpha"], buildMetadataIdentifiers: ["-alpha"]) + ) + XCTAssertLessThan( + Version(1, 2, 3, prereleaseIdentifiers: ["223"]), + Version(1, 2, 3, prereleaseIdentifiers: ["123alpha"], buildMetadataIdentifiers: ["123"]) + ) + + XCTAssertGreaterThan( + Version(1, 2, 3, prereleaseIdentifiers: ["xyz"]), + Version(1, 2, 3, prereleaseIdentifiers: ["123"], buildMetadataIdentifiers: ["hgjkalmfvdfua"]) + ) + XCTAssertGreaterThan( + Version(1, 2, 3, prereleaseIdentifiers: ["111uvw"]), + Version(1, 2, 3, prereleaseIdentifiers: ["999999"], buildMetadataIdentifiers: ["iouiytrdfghj", "3rfey89rr"]) + ) + + XCTAssertFalse( + Version(1, 2, 3, prereleaseIdentifiers: ["xyz"]) < + Version(1, 2, 3, prereleaseIdentifiers: ["123"], buildMetadataIdentifiers: ["hgjkalmfvdfua"]) + ) + XCTAssertFalse( + Version(1, 2, 3, prereleaseIdentifiers: ["111uvw"]) < + Version(1, 2, 3, prereleaseIdentifiers: ["999999"], buildMetadataIdentifiers: ["dfghjkiohgf", "3rfey89rr"]) + ) + + // MARK: version core + pre-release + build metadata vs. version core + pre-release + build metadata + + XCTAssertEqual( + Version(1, 2, 3, prereleaseIdentifiers: [""], buildMetadataIdentifiers: [""]), + Version(1, 2, 3, prereleaseIdentifiers: [""], buildMetadataIdentifiers: [""]) + ) + + XCTAssertEqual( + Version(1, 2, 3, prereleaseIdentifiers: ["beta"], buildMetadataIdentifiers: ["123"]), + Version(1, 2, 3, prereleaseIdentifiers: ["beta"], buildMetadataIdentifiers: [""]) + ) + XCTAssertLessThan( + Version(1, 2, 3, prereleaseIdentifiers: ["alpha"], buildMetadataIdentifiers: ["-alpha"]), + Version(1, 2, 3, prereleaseIdentifiers: ["beta"], buildMetadataIdentifiers: ["123alpha"]) + ) + XCTAssertLessThan( + Version(1, 2, 3, prereleaseIdentifiers: ["alpha1"], buildMetadataIdentifiers: ["alpha", "beta"]), + Version(1, 2, 3, prereleaseIdentifiers: ["alpha2"], buildMetadataIdentifiers: ["alpha"]) + ) + XCTAssertLessThan( + Version(1, 2, 3, prereleaseIdentifiers: ["alpha"], buildMetadataIdentifiers: ["123"]), + Version(1, 2, 3, prereleaseIdentifiers: ["alpha-"], buildMetadataIdentifiers: ["alpha", "beta"]) + ) + XCTAssertLessThan( + Version(1, 2, 3, prereleaseIdentifiers: ["beta", "alpha"], buildMetadataIdentifiers: ["123alpha"]), + Version(1, 2, 3, prereleaseIdentifiers: ["beta", "beta"], buildMetadataIdentifiers: ["123"]) + ) + XCTAssertLessThan( + Version(1, 2, 3, prereleaseIdentifiers: ["alpha", "beta"], buildMetadataIdentifiers: [""]), + Version(1, 2, 3, prereleaseIdentifiers: ["beta", "alpha"], buildMetadataIdentifiers: ["alpha-"]) + ) + + XCTAssertEqual( + Version(1, 2, 3, prereleaseIdentifiers: ["1"], buildMetadataIdentifiers: ["alpha-"]), + Version(1, 2, 3, prereleaseIdentifiers: ["1"], buildMetadataIdentifiers: [""]) + ) + XCTAssertEqual( + Version(1, 2, 3, prereleaseIdentifiers: ["01"], buildMetadataIdentifiers: ["alpha-"]), + Version(1, 2, 3, prereleaseIdentifiers: ["1"], buildMetadataIdentifiers: [""]) + ) + XCTAssertEqual( + Version(1, 2, 3, prereleaseIdentifiers: ["1"], buildMetadataIdentifiers: ["alpha-"]), + Version(1, 2, 3, prereleaseIdentifiers: ["01"], buildMetadataIdentifiers: [""]) + ) + XCTAssertEqual( + Version(1, 2, 3, prereleaseIdentifiers: ["001"], buildMetadataIdentifiers: ["alpha-"]), + Version(1, 2, 3, prereleaseIdentifiers: ["0001"], buildMetadataIdentifiers: [""]) + ) + + XCTAssertLessThan( + Version(1, 2, 3, prereleaseIdentifiers: ["1"], buildMetadataIdentifiers: ["123"]), + Version(1, 2, 3, prereleaseIdentifiers: ["2"], buildMetadataIdentifiers: ["123alpha"]) + ) + XCTAssertLessThan( + Version(1, 2, 3, prereleaseIdentifiers: ["1", "1"], buildMetadataIdentifiers: ["alpha", "beta"]), + Version(1, 2, 3, prereleaseIdentifiers: ["1", "2"], buildMetadataIdentifiers: ["123"]) + ) + XCTAssertLessThan( + Version(1, 2, 3, prereleaseIdentifiers: ["00001", "1"], buildMetadataIdentifiers: ["alpha", "beta"]), + Version(1, 2, 3, prereleaseIdentifiers: ["1", "2"], buildMetadataIdentifiers: ["123"]) + ) + XCTAssertLessThan( + Version(1, 2, 3, prereleaseIdentifiers: ["1", "1"], buildMetadataIdentifiers: ["alpha", "beta"]), + Version(1, 2, 3, prereleaseIdentifiers: ["000001", "2"], buildMetadataIdentifiers: ["123"]) + ) + XCTAssertLessThan( + Version(1, 2, 3, prereleaseIdentifiers: ["0000001", "1"], buildMetadataIdentifiers: ["alpha", "beta"]), + Version(1, 2, 3, prereleaseIdentifiers: ["001", "2"], buildMetadataIdentifiers: ["123"]) + ) + XCTAssertLessThan( + Version(1, 2, 3, prereleaseIdentifiers: ["1", "2"], buildMetadataIdentifiers: ["alpha"]), + Version(1, 2, 3, prereleaseIdentifiers: ["2", "1"], buildMetadataIdentifiers: ["alpha", "beta"]) + ) + + XCTAssertLessThan( + Version(1, 2, 3, prereleaseIdentifiers: ["123"], buildMetadataIdentifiers: ["123alpha"]), + Version(1, 2, 3, prereleaseIdentifiers: ["123alpha"], buildMetadataIdentifiers: ["-alpha"]) + ) + XCTAssertLessThan( + Version(1, 2, 3, prereleaseIdentifiers: ["223"], buildMetadataIdentifiers: ["123alpha"]), + Version(1, 2, 3, prereleaseIdentifiers: ["123alpha"], buildMetadataIdentifiers: ["123"]) + ) + + XCTAssertGreaterThan( + Version(1, 2, 3, prereleaseIdentifiers: ["xyz"], buildMetadataIdentifiers: ["-09tyfvgubh"]), + Version(1, 2, 3, prereleaseIdentifiers: ["123"], buildMetadataIdentifiers: ["765resdfu89"]) + ) + XCTAssertGreaterThan( + Version(1, 2, 3, prereleaseIdentifiers: ["111uvw"], buildMetadataIdentifiers: ["-----MNgyftcyvu---vcxzAQwsd-------"]), + Version(1, 2, 3, prereleaseIdentifiers: ["999999"], buildMetadataIdentifiers: ["bvgh--9-ygtfyvg", "hgvh-0vb-"]) + ) + + XCTAssertFalse( + Version(1, 2, 3, prereleaseIdentifiers: ["xyz"], buildMetadataIdentifiers: ["fhieaw98y76ftrcwrjk"]) < + Version(1, 2, 3, prereleaseIdentifiers: ["123"], buildMetadataIdentifiers: ["-jhgbivuy-gh"]) + ) + XCTAssertFalse( + Version(1, 2, 3, prereleaseIdentifiers: ["111uvw"], buildMetadataIdentifiers: ["bvcx67t"]) < + Version(1, 2, 3, prereleaseIdentifiers: ["999999"], buildMetadataIdentifiers: ["nuybvfcrd6ty", "3rfey89rr"]) + ) + + } + + func testCustomConversionFromVersionToString() { + + // MARK: Version.description + + XCTAssertEqual(Version(0, 0, 0).description, "0.0.0" as String) + XCTAssertEqual(Version(1, 2, 3).description, "1.2.3" as String) + XCTAssertEqual(Version(1, 2, 3, prereleaseIdentifiers: [""]).description, "1.2.3-" as String) + XCTAssertEqual(Version(1, 2, 3, prereleaseIdentifiers: ["", ""]).description, "1.2.3-." as String) + XCTAssertEqual(Version(1, 2, 3, prereleaseIdentifiers: ["beta1"]).description, "1.2.3-beta1" as String) + XCTAssertEqual(Version(1, 2, 3, prereleaseIdentifiers: ["beta", "1"]).description, "1.2.3-beta.1" as String) + XCTAssertEqual(Version(1, 2, 3, prereleaseIdentifiers: ["beta", "", "1"]).description, "1.2.3-beta..1" as String) + XCTAssertEqual(Version(1, 2, 3, prereleaseIdentifiers: ["be-ta", "", "1"]).description, "1.2.3-be-ta..1" as String) + XCTAssertEqual(Version(1, 2, 3, buildMetadataIdentifiers: [""]).description, "1.2.3+" as String) + XCTAssertEqual(Version(1, 2, 3, buildMetadataIdentifiers: ["", ""]).description, "1.2.3+." as String) + XCTAssertEqual(Version(1, 2, 3, buildMetadataIdentifiers: ["beta1"]).description, "1.2.3+beta1" as String) + XCTAssertEqual(Version(1, 2, 3, buildMetadataIdentifiers: ["beta", "1"]).description, "1.2.3+beta.1" as String) + XCTAssertEqual(Version(1, 2, 3, buildMetadataIdentifiers: ["beta", "", "1"]).description, "1.2.3+beta..1" as String) + XCTAssertEqual(Version(1, 2, 3, buildMetadataIdentifiers: ["be-ta", "", "1"]).description, "1.2.3+be-ta..1" as String) + XCTAssertEqual(Version(1, 2, 3, prereleaseIdentifiers: [""], buildMetadataIdentifiers: [""]).description, "1.2.3-+" as String) + XCTAssertEqual(Version(1, 2, 3, prereleaseIdentifiers: ["", ""], buildMetadataIdentifiers: ["", "-", ""]).description, "1.2.3-.+.-." as String) + XCTAssertEqual(Version(1, 2, 3, prereleaseIdentifiers: ["beta1"], buildMetadataIdentifiers: ["alpha1"]).description, "1.2.3-beta1+alpha1" as String) + XCTAssertEqual(Version(1, 2, 3, prereleaseIdentifiers: ["beta", "1"], buildMetadataIdentifiers: ["alpha", "1"]).description, "1.2.3-beta.1+alpha.1" as String) + XCTAssertEqual(Version(1, 2, 3, prereleaseIdentifiers: ["beta", "", "1"], buildMetadataIdentifiers: ["alpha", "", "1"]).description, "1.2.3-beta..1+alpha..1" as String) + XCTAssertEqual(Version(1, 2, 3, prereleaseIdentifiers: ["be-ta", "", "1"], buildMetadataIdentifiers: ["al-pha", "", "1"]).description, "1.2.3-be-ta..1+al-pha..1" as String) + + // MARK: String interpolation + + XCTAssertEqual("\(Version(0, 0, 0))", "0.0.0" as String) + XCTAssertEqual("\(Version(1, 2, 3))", "1.2.3" as String) + XCTAssertEqual("\(Version(1, 2, 3, prereleaseIdentifiers: [""]))", "1.2.3-" as String) + XCTAssertEqual("\(Version(1, 2, 3, prereleaseIdentifiers: ["", ""]))", "1.2.3-." as String) + XCTAssertEqual("\(Version(1, 2, 3, prereleaseIdentifiers: ["beta1"]))", "1.2.3-beta1" as String) + XCTAssertEqual("\(Version(1, 2, 3, prereleaseIdentifiers: ["beta", "1"]))", "1.2.3-beta.1" as String) + XCTAssertEqual("\(Version(1, 2, 3, prereleaseIdentifiers: ["beta", "", "1"]))", "1.2.3-beta..1" as String) + XCTAssertEqual("\(Version(1, 2, 3, prereleaseIdentifiers: ["be-ta", "", "1"]))", "1.2.3-be-ta..1" as String) + XCTAssertEqual("\(Version(1, 2, 3, buildMetadataIdentifiers: [""]))", "1.2.3+" as String) + XCTAssertEqual("\(Version(1, 2, 3, buildMetadataIdentifiers: ["", ""]))", "1.2.3+." as String) + XCTAssertEqual("\(Version(1, 2, 3, buildMetadataIdentifiers: ["beta1"]))", "1.2.3+beta1" as String) + XCTAssertEqual("\(Version(1, 2, 3, buildMetadataIdentifiers: ["beta", "1"]))", "1.2.3+beta.1" as String) + XCTAssertEqual("\(Version(1, 2, 3, buildMetadataIdentifiers: ["beta", "", "1"]))", "1.2.3+beta..1" as String) + XCTAssertEqual("\(Version(1, 2, 3, buildMetadataIdentifiers: ["be-ta", "", "1"]))", "1.2.3+be-ta..1" as String) + XCTAssertEqual("\(Version(1, 2, 3, prereleaseIdentifiers: [""], buildMetadataIdentifiers: [""]))", "1.2.3-+" as String) + XCTAssertEqual("\(Version(1, 2, 3, prereleaseIdentifiers: ["", ""], buildMetadataIdentifiers: ["", "-", ""]))", "1.2.3-.+.-." as String) + XCTAssertEqual("\(Version(1, 2, 3, prereleaseIdentifiers: ["beta1"], buildMetadataIdentifiers: ["alpha1"]))", "1.2.3-beta1+alpha1" as String) + XCTAssertEqual("\(Version(1, 2, 3, prereleaseIdentifiers: ["beta", "1"], buildMetadataIdentifiers: ["alpha", "1"]))", "1.2.3-beta.1+alpha.1" as String) + XCTAssertEqual("\(Version(1, 2, 3, prereleaseIdentifiers: ["beta", "", "1"], buildMetadataIdentifiers: ["alpha", "", "1"]))", "1.2.3-beta..1+alpha..1" as String) + XCTAssertEqual("\(Version(1, 2, 3, prereleaseIdentifiers: ["be-ta", "", "1"], buildMetadataIdentifiers: ["al-pha", "", "1"]))", "1.2.3-be-ta..1+al-pha..1" as String) + + } + +} From 6072bafc5e7ad23fff125634abdfadc53bfbd213 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=86=80=E5=8D=93=E7=96=8C?= <55120045+WowbaggersLiquidLunch@users.noreply.github.com> Date: Sat, 27 Nov 2021 14:27:30 +0800 Subject: [PATCH 2/3] move `LosslessStringConvertible` from docc to docc-symbolkit docc didn't exactly conform `SemanticVersion` to `LosslessStringConvertible`, but it extended it with all the functionalities. --- .../SymbolGraph/Misc/SemanticVersion.swift | 52 ++++++++ .../SymbolGraph/SemanticVersionTests.swift | 113 ++++++++++++++++++ 2 files changed, 165 insertions(+) diff --git a/Sources/SymbolKit/SymbolGraph/Misc/SemanticVersion.swift b/Sources/SymbolKit/SymbolGraph/Misc/SemanticVersion.swift index 2cf834a..dbc93b4 100644 --- a/Sources/SymbolKit/SymbolGraph/Misc/SemanticVersion.swift +++ b/Sources/SymbolKit/SymbolGraph/Misc/SemanticVersion.swift @@ -195,3 +195,55 @@ extension SymbolGraph.SemanticVersion: CustomStringConvertible { return versionString } } + +extension SymbolGraph.SemanticVersion: LosslessStringConvertible { + /// Initializes a version struct with the provided version string. + /// - Parameter version: A version string to use for creating a new version struct. + public init?(_ versionString: String) { + // SemVer 2.0.0 allows only ASCII alphanumerical characters and "-" in the version string, except for "." and "+" as delimiters. ("-" is used as a delimiter between the version core and pre-release identifiers, but it's allowed within pre-release and metadata identifiers as well.) + // Alphanumerics check will come later, after each identifier is split out (i.e. after the delimiters are removed). + guard versionString.allSatisfy(\.isASCII) else { return nil } + + let metadataDelimiterIndex = versionString.firstIndex(of: "+") + // SemVer 2.0.0 requires that pre-release identifiers come before build metadata identifiers + let prereleaseDelimiterIndex = versionString[..<(metadataDelimiterIndex ?? versionString.endIndex)].firstIndex(of: "-") + + let versionCore = versionString[..<(prereleaseDelimiterIndex ?? metadataDelimiterIndex ?? versionString.endIndex)] + let versionCoreIdentifiers = versionCore.split(separator: ".", omittingEmptySubsequences: false) + + guard + versionCoreIdentifiers.count == 3, + // Major, minor, and patch versions must be ASCII numbers, according to the semantic versioning standard. + // Converting each identifier from a substring to an integer doubles as checking if the identifiers have non-numeric characters. + let major = Int(versionCoreIdentifiers[0]), + let minor = Int(versionCoreIdentifiers[1]), + let patch = Int(versionCoreIdentifiers[2]) + else { return nil } + + self.major = major + self.minor = minor + self.patch = patch + + if let prereleaseDelimiterIndex = prereleaseDelimiterIndex { + let prereleaseStartIndex = versionString.index(after: prereleaseDelimiterIndex) + let prereleaseIdentifiers = versionString[prereleaseStartIndex..<(metadataDelimiterIndex ?? versionString.endIndex)].split(separator: ".", omittingEmptySubsequences: false) + guard prereleaseIdentifiers.allSatisfy( { + $0.allSatisfy( { $0.isLetter || $0.isNumber || $0 == "-" } ) + } ) else { return nil } + self.prereleaseIdentifiers = prereleaseIdentifiers.map { String($0) } + } else { + self.prereleaseIdentifiers = [] + } + + if let metadataDelimiterIndex = metadataDelimiterIndex { + let metadataStartIndex = versionString.index(after: metadataDelimiterIndex) + let buildMetadataIdentifiers = versionString[metadataStartIndex...].split(separator: ".", omittingEmptySubsequences: false) + guard buildMetadataIdentifiers.allSatisfy( { + $0.allSatisfy( { $0.isLetter || $0.isNumber || $0 == "-" } ) + } ) else { return nil } + self.buildMetadataIdentifiers = buildMetadataIdentifiers.map { String($0) } + } else { + self.buildMetadataIdentifiers = [] + } + } +} diff --git a/Tests/SymbolKitTests/SymbolGraph/SemanticVersionTests.swift b/Tests/SymbolKitTests/SymbolGraph/SemanticVersionTests.swift index d0bb4d8..0e8c2fd 100644 --- a/Tests/SymbolKitTests/SymbolGraph/SemanticVersionTests.swift +++ b/Tests/SymbolKitTests/SymbolGraph/SemanticVersionTests.swift @@ -949,4 +949,117 @@ final class SemanticVersionTests: XCTestCase { } + func testLosslessConversionFromStringToVersion() { + + // MARK: Well-formed version core + + XCTAssertNotNil(Version("0.0.0" as String)) + XCTAssertEqual(Version("0.0.0" as String), Version(0, 0, 0)) + + XCTAssertNotNil(Version("1.1.2" as String)) + XCTAssertEqual(Version("1.1.2" as String), Version(1, 1, 2)) + + // MARK: Malformed version core + + XCTAssertNil(Version("3" as String)) + XCTAssertNil(Version("3 5" as String)) + XCTAssertNil(Version("5.8" as String)) + XCTAssertNil(Version("-5.8.13" as String)) + XCTAssertNil(Version("8.-13.21" as String)) + XCTAssertNil(Version("13.21.-34" as String)) + XCTAssertNil(Version("-0.0.0" as String)) + XCTAssertNil(Version("0.-0.0" as String)) + XCTAssertNil(Version("0.0.-0" as String)) + XCTAssertNil(Version("21.34.55.89" as String)) + XCTAssertNil(Version("6 x 9 = 42" as String)) + XCTAssertNil(Version("forty two" as String)) + + // MARK: Well-formed version core, well-formed pre-release identifiers + + XCTAssertNotNil(Version("0.0.0-pre-alpha" as String)) + XCTAssertEqual(Version("0.0.0-pre-alpha" as String), Version(0, 0, 0, prereleaseIdentifiers: ["pre-alpha"])) + + XCTAssertNotNil(Version("55.89.144-beta.1" as String)) + XCTAssertEqual(Version("55.89.144-beta.1" as String), Version(55, 89, 144, prereleaseIdentifiers: ["beta", "1"])) + + XCTAssertNotNil(Version("89.144.233-a.whole..lot.of.pre-release.identifiers" as String)) + XCTAssertEqual(Version("89.144.233-a.whole..lot.of.pre-release.identifiers" as String), Version(89, 144, 233, prereleaseIdentifiers: ["a", "whole", "", "lot", "of", "pre-release", "identifiers"])) + + XCTAssertNotNil(Version("144.233.377-" as String)) + XCTAssertEqual(Version("144.233.377-" as String), Version(144, 233, 377, prereleaseIdentifiers: [""])) + + // MARK: Well-formed version core, malformed pre-release identifiers + + XCTAssertNil(Version("233.377.610-hello world" as String)) + + // MARK: Malformed version core, well-formed pre-release identifiers + + XCTAssertNil(Version("987-Hello.world--------" as String)) + XCTAssertNil(Version("987.1597-half-life.3" as String)) + XCTAssertNil(Version("1597.2584.4181.6765-a.whole.lot.of.pre-release.identifiers" as String)) + XCTAssertNil(Version("6 x 9 = 42-" as String)) + XCTAssertNil(Version("forty-two" as String)) + + // MARK: Well-formed version core, well-formed build metadata identifiers + + XCTAssertNotNil(Version("0.0.0+some-metadata" as String)) + XCTAssertEqual(Version("0.0.0+some-metadata" as String), Version(0, 0, 0, buildMetadataIdentifiers: ["some-metadata"])) + + XCTAssertNotNil(Version("4181.6765.10946+more.meta..more.data" as String)) + XCTAssertEqual(Version("4181.6765.10946+more.meta..more.data" as String), Version(4181, 6765, 10946, buildMetadataIdentifiers: ["more", "meta", "", "more", "data"])) + + XCTAssertNotNil(Version("6765.10946.17711+-a-very--long---build-----metadata--------identifier-------------with---------------------many----------------------------------hyphens-------------------------------------------------------" as String)) + XCTAssertEqual(Version("6765.10946.17711+-a-very--long---build-----metadata--------identifier-------------with---------------------many----------------------------------hyphens-------------------------------------------------------" as String), Version(6765, 10946, 17711, buildMetadataIdentifiers: ["-a-very--long---build-----metadata--------identifier-------------with---------------------many----------------------------------hyphens-------------------------------------------------------"])) + + XCTAssertNotNil(Version("10946.17711.28657+" as String)) + XCTAssertEqual(Version("10946.17711.28657+" as String), Version(10946, 17711, 28657, buildMetadataIdentifiers: [""])) + + // MARK: Well-formed version core, malformed build metadata identifiers + + XCTAssertNil(Version("17711.28657.46368+hello world" as String)) + XCTAssertNil(Version("28657.46368.75025+hello+world" as String)) + + // MARK: Malformed version core, well-formed build metadata identifiers + + XCTAssertNil(Version("121393+Hello.world--------" as String)) + XCTAssertNil(Version("121393.196418+half-life.3" as String)) + XCTAssertNil(Version("196418.317811.514229.832040+a.whole.lot.of.build.metadata.identifiers" as String)) + XCTAssertNil(Version("196418.317811.514229.832040+a.whole.lot.of.build.metadata.identifiers" as String)) + XCTAssertNil(Version("6 x 9 = 42+" as String)) + XCTAssertNil(Version("forty two+a-very-long-build-metadata-identifier-with-many-hyphens" as String)) + + // MARK: Well-formed version core, well-formed pre-release identifiers, well-formed build metadata identifiers + + XCTAssertNotNil(Version("0.0.0-beta.-42+42-42.42" as String)) + XCTAssertEqual(Version("0.0.0-beta.-42+42-42.42" as String), Version(0, 0, 0, prereleaseIdentifiers: ["beta", "-42"], buildMetadataIdentifiers: ["42-42", "42"])) + + // MARK: Well-formed version core, well-formed pre-release identifiers, malformed build metadata identifiers + + XCTAssertNil(Version("514229.832040.1346269-beta1+ " as String)) + + // MARK: Well-formed version core, malformed pre-release identifiers, well-formed build metadata identifiers + + XCTAssertNil(Version("832040.1346269.2178309-beta 1+-" as String)) + + // MARK: Well-formed version core, malformed pre-release identifiers, malformed build metadata identifiers + + XCTAssertNil(Version("1346269.2178309.3524578-beta 1++" as String)) + + // MARK: malformed version core, well-formed pre-release identifiers, well-formed build metadata identifiers + + XCTAssertNil(Version(" 832040.1346269.3524578-beta1+abc" as String)) + + // MARK: malformed version core, well-formed pre-release identifiers, malformed build metadata identifiers + + XCTAssertNil(Version("1346269.3524578.5702887-beta1+😀" as String)) + + // MARK: malformed version core, malformed pre-release identifiers, well-formed build metadata identifiers + + XCTAssertNil(Version("3524578.5702887.9227465-beta!@#$%^&*1+asdfghjkl123456789" as String)) + + // MARK: malformed version core, malformed pre-release identifiers, malformed build metadata identifiers + + XCTAssertNil(Version("5702887.9227465-bètá1+±" as String)) + + } } From df2c1cdd3201b5f72776e49ee94837eb81775cfd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=86=80=E5=8D=93=E7=96=8C?= <55120045+WowbaggersLiquidLunch@users.noreply.github.com> Date: Tue, 11 Jan 2022 22:35:27 +0800 Subject: [PATCH 3/3] reimplement `SemanticVersion` to fully comply with SemVer 2.0.0 Tests have not been updated from the previous commit, and there are still lots of documentation missing. But because of the renaming of so many files upstream, it's now difficult to rebase the changes here. Going to open a WIP PR as is for feedback, and reapply the changes at the current upstream head. --- .../SymbolGraph/Misc/SemanticVersion.swift | 267 ++++++++++-------- .../Misc/SemanticVersionError.swift | 106 +++++++ .../Misc/SemanticVersionPrerelease.swift | 135 +++++++++ 3 files changed, 394 insertions(+), 114 deletions(-) create mode 100644 Sources/SymbolKit/SymbolGraph/Misc/SemanticVersionError.swift create mode 100644 Sources/SymbolKit/SymbolGraph/Misc/SemanticVersionPrerelease.swift diff --git a/Sources/SymbolKit/SymbolGraph/Misc/SemanticVersion.swift b/Sources/SymbolKit/SymbolGraph/Misc/SemanticVersion.swift index dbc93b4..672e1b0 100644 --- a/Sources/SymbolKit/SymbolGraph/Misc/SemanticVersion.swift +++ b/Sources/SymbolKit/SymbolGraph/Misc/SemanticVersion.swift @@ -11,119 +11,140 @@ extension SymbolGraph { /// A [semantic version](https://semver.org). public struct SemanticVersion { - /// The major version according to the semantic versioning standard. + /// The major version. public let major: Int - /// The minor version according to the semantic versioning standard. + /// The minor version. public let minor: Int - /// The patch version according to the semantic versioning standard. + /// The patch version. public let patch: Int - /// The pre-release identifier according to the semantic versioning standard, such as `-beta.1`. - public let prereleaseIdentifiers: [String] - /// The build metadata of this version according to the semantic versioning standard, such as a commit hash. + /// Dot-separated pre-release identifiers. + public var prereleaseIdentifiers: [String] { prerelease.identifiers.map(\.description) } + /// Dot-separated build metadata identifiers. public let buildMetadataIdentifiers: [String] - /// Initializes a semantic version struct with the provided components of a semantic version. + /// The internal storage of pre-release identifiers. + internal let prerelease: Prerelease + + /// Creates a semantic version with the provided components of a semantic version. /// - Parameters: /// - major: The major version number. /// - minor: The minor version number. /// - patch: The patch version number. /// - prereleaseIdentifiers: The pre-release identifiers. - /// - buildMetaDataIdentifiers: Build metadata that identifies a build. + /// - buildMetaDataIdentifiers: The build metadata identifiers. public init( + // FIXME: Should `major`, `minor`, and `patch` be `UInt`? _ major: Int, _ minor: Int, _ patch: Int, prereleaseIdentifiers: [String] = [], buildMetadataIdentifiers: [String] = [] - ) { - precondition(major >= 0 && minor >= 0 && patch >= 0, "Negative versioning is invalid.") - precondition( - prereleaseIdentifiers.allSatisfy { - $0.allSatisfy { $0.isASCII && ($0.isLetter || $0.isNumber || $0 == "-") } - }, - #"Pre-release identifiers can contain only ASCII alpha-numerical characters and "-"."# - ) - precondition( - buildMetadataIdentifiers.allSatisfy { - $0.allSatisfy { $0.isASCII && ($0.isLetter || $0.isNumber || $0 == "-") } - }, - #"Build metadata identifiers can contain only ASCII alpha-numerical characters and "-"."# - ) - + ) throws { + guard major >= 0 else { throw SemanticVersionError.invalidNumericIdentifier(major.description, position: .major, errorKind: .negativeValue)} + guard minor >= 0 else { throw SemanticVersionError.invalidNumericIdentifier(minor.description, position: .minor, errorKind: .negativeValue)} + guard patch >= 0 else { throw SemanticVersionError.invalidNumericIdentifier(patch.description, position: .patch, errorKind: .negativeValue)} self.major = major self.minor = minor self.patch = patch - self.prereleaseIdentifiers = prereleaseIdentifiers + + self.prerelease = try Prerelease(prereleaseIdentifiers) + + guard buildMetadataIdentifiers.allSatisfy( { !$0.isEmpty } ) else { + throw SemanticVersionError.emptyIdentifier(position: .buildMetadata) + } + try buildMetadataIdentifiers.forEach { + guard $0.allSatisfy( { $0.isASCII && ( $0.isLetter || $0.isNumber || $0 == "-" ) } ) else { + throw SemanticVersionError.invalidCharacterInIdentifier($0, position: .buildMetadata) + } + } self.buildMetadataIdentifiers = buildMetadataIdentifiers } - - /// Initializes a semantic version struct with the provided components of a semantic version. + + /// Creates a semantic version with the provided components of a semantic version. /// - Parameters: /// - major: The major version number. /// - minor: The minor version number. /// - patch: The patch version number. - /// - prereleaseIdentifiers: The "."-separated pre-release identifiers. - /// - buildMetaDataIdentifiers: The "."-separated build metadata that identifies a build. - @_disfavoredOverload - public init( - _ major: Int, - _ minor: Int, - _ patch: Int, - prereleaseIdentifiers: String? = nil, - buildMetadataIdentifiers: String? = nil - ) { - self.init( + /// - prerelease: The dot-separated pre-release identifiers; `nil` if the version is not a pre-release. + /// - buildMetadata: The dot-separated build metadata identifiers; `nil` if build metadata is absent. + @available(*, deprecated, renamed: "init(_:_:_:prereleaseIdentifiers:buildMetadataIdentifiers:)") + public init(major: Int, minor: Int, patch: Int, prerelease: String? = nil, buildMetadata: String? = nil) { + try! self.init( major, minor, patch, - prereleaseIdentifiers: prereleaseIdentifiers? + prereleaseIdentifiers: prerelease? .split(separator: ".", omittingEmptySubsequences: false) .map { String($0) } ?? [], - buildMetadataIdentifiers: buildMetadataIdentifiers? + buildMetadataIdentifiers: buildMetadata? .split(separator: ".", omittingEmptySubsequences: false) .map { String($0) } ?? [] ) } - - @available(*, deprecated, renamed: "init(_:_:_:prereleaseIdentifiers:buildMetadataIdentifiers:)") - public init(major: Int, minor: Int, patch: Int, prerelease: String? = nil, buildMetadata: String? = nil) { - self.init(major, minor, patch, prereleaseIdentifiers: prerelease, buildMetadataIdentifiers: buildMetadata) - } } } +// MARK: - Inspecting a Semantic Version + +extension SymbolGraph.SemanticVersion { + /// A Boolean value indicating whether the version is a pre-release version. + public var isPrerelease: Bool { !prerelease.identifiers.isEmpty } +} + +// MARK: - + extension SymbolGraph.SemanticVersion: Codable { - - // FIXME: Should this be public? - private enum CodingKeys: String, CodingKey { + /// Keys for encoding and decoding `SemanticVersion` properties. + internal enum CodingKeys: String, CodingKey { + /// The major version number. case major + /// The minor version number. case minor + /// The patch version number. case patch - case prereleaseIdentifiers = "prerelease" - case buildMetadataIdentifiers = "buildMetadata" + /// The dot-separated pre-release identifiers. + case prerelease + /// The dot-separated build metadata identifiers. + case buildMetadata } + /// Creates a semantic version by decoding from the given decoder. + /// - Parameter decoder: The decoder to read data from. public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) + self.major = try container.decode(Int.self, forKey: .major) + guard major >= 0 else { throw SymbolGraph.SemanticVersionError.invalidNumericIdentifier(major.description, position: .major, errorKind: .negativeValue)} self.minor = try container.decodeIfPresent(Int.self, forKey: .minor) ?? 0 + guard minor >= 0 else { throw SymbolGraph.SemanticVersionError.invalidNumericIdentifier(minor.description, position: .minor, errorKind: .negativeValue)} self.patch = try container.decodeIfPresent(Int.self, forKey: .patch) ?? 0 - self.prereleaseIdentifiers = try container.decodeIfPresent(String.self, forKey: .prereleaseIdentifiers)? - .split(separator: ".", omittingEmptySubsequences: false) - .map { String($0) } ?? [] - self.buildMetadataIdentifiers = try container.decodeIfPresent(String.self, forKey: .buildMetadataIdentifiers)? + guard patch >= 0 else { throw SymbolGraph.SemanticVersionError.invalidNumericIdentifier(patch.description, position: .patch, errorKind: .negativeValue)} + + self.prerelease = try Prerelease(try container.decodeIfPresent(String.self, forKey: .prerelease)) + + self.buildMetadataIdentifiers = try container.decodeIfPresent(String.self, forKey: .buildMetadata)? .split(separator: ".", omittingEmptySubsequences: false) .map { String($0) } ?? [] + guard !buildMetadataIdentifiers.allSatisfy(\.isEmpty) else { + throw SymbolGraph.SemanticVersionError.emptyIdentifier(position: .buildMetadata) + } + try buildMetadataIdentifiers.forEach { identifier in + guard identifier.isSemanticVersionBuildMetadataIdentifier else { + throw SymbolGraph.SemanticVersionError.invalidCharacterInIdentifier(String(identifier), position: .buildMetadata) + } + } } + /// Encodes the semantic version into the given encoder. + /// - Parameter encoder: The encoder to write data to. public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) try container.encode(major, forKey: .major) try container.encode(minor, forKey: .minor) try container.encode(patch, forKey: .patch) - if !prereleaseIdentifiers.isEmpty { - try container.encode(prereleaseIdentifiers.joined(separator: "."), forKey: .prereleaseIdentifiers) + if isPrerelease { + try container.encode(prerelease.description, forKey: .prerelease) } if !buildMetadataIdentifiers.isEmpty { - try container.encode(buildMetadataIdentifiers.joined(separator: "."), forKey: .buildMetadataIdentifiers) + try container.encode(buildMetadataIdentifiers.joined(separator: "."), forKey: .buildMetadata) } } @@ -131,54 +152,30 @@ extension SymbolGraph.SemanticVersion: Codable { extension SymbolGraph.SemanticVersion: Comparable { // Although `Comparable` inherits from `Equatable`, it does not provide a new default implementation of `==`, but instead uses `Equatable`'s default synthesised implementation. The compiler-synthesised `==`` is composed of [member-wise comparisons](https://github.com/apple/swift-evolution/blob/main/proposals/0185-synthesize-equatable-hashable.md#implementation-details), which leads to a false `false` when 2 semantic versions differ by only their build metadata identifiers, contradicting SemVer 2.0.0's [comparison rules](https://semver.org/#spec-item-10). + /// <#Description#> + /// - Parameters: + /// - lhs: <#lhs description#> + /// - rhs: <#rhs description#> + /// - Returns: <#description#> @inlinable public static func == (lhs: Self, rhs: Self) -> Bool { !(lhs < rhs) && !(lhs > rhs) } + /// <#Description#> + /// - Parameters: + /// - lhs: <#lhs description#> + /// - rhs: <#rhs description#> + /// - Returns: <#description#> public static func < (lhs: Self, rhs: Self) -> Bool { - let lhsComparators = [lhs.major, lhs.minor, lhs.patch] - let rhsComparators = [rhs.major, rhs.minor, rhs.patch] - - guard lhsComparators == rhsComparators else { - return lhsComparators.lexicographicallyPrecedes(rhsComparators) - } - - guard lhs.prereleaseIdentifiers.count > 0 else { - return false // non-pre-release lhs >= potentially pre-release rhs - } - - guard rhs.prereleaseIdentifiers.count > 0 else { - return true // pre-release lhs < non-pre-release rhs - } + let lhsVersionCore = [lhs.major, lhs.minor, lhs.patch] + let rhsVersionCore = [rhs.major, rhs.minor, rhs.patch] - for (lhsPrereleaseIdentifier, rhsPrereleaseIdentifier) in zip(lhs.prereleaseIdentifiers, rhs.prereleaseIdentifiers) { - guard lhsPrereleaseIdentifier != rhsPrereleaseIdentifier else { - continue - } - - // Check if either of the 2 pre-release identifiers is numeric. - let lhsNumericPrereleaseIdentifier = Int(lhsPrereleaseIdentifier) - let rhsNumericPrereleaseIdentifier = Int(rhsPrereleaseIdentifier) - - if let lhsNumericPrereleaseIdentifier = lhsNumericPrereleaseIdentifier, - let rhsNumericPrereleaseIdentifier = rhsNumericPrereleaseIdentifier { - // Semantic Versioning 2.0.0 considers 2 pre-release identifiers equal, if they're numerically equal _or_ textually equal. In other words, if 2 identifiers are numeric, they are unequal _if and only if_ they're numerically unequal. Identifiers that have entered this conditional block must have not been textually equal, but it is still possible for them to be numerically equal. For example: "100" and "00100" are textually unequal but numerically equal. If the 2 identifiers in comparison are indeed equal, then unless they're the last pair of pre-release identifiers, they cannot be the deciding pair for the precedence between the 2 semantic versions. - if lhsNumericPrereleaseIdentifier == rhsNumericPrereleaseIdentifier { - continue - } else { - return lhsNumericPrereleaseIdentifier < rhsNumericPrereleaseIdentifier - } - } else if lhsNumericPrereleaseIdentifier != nil { - return true // numeric pre-release < non-numeric pre-release - } else if rhsNumericPrereleaseIdentifier != nil { - return false // non-numeric pre-release > numeric pre-release - } else { - return lhsPrereleaseIdentifier < rhsPrereleaseIdentifier - } + guard lhsVersionCore == rhsVersionCore else { + return lhsVersionCore.lexicographicallyPrecedes(rhsVersionCore) } - return lhs.prereleaseIdentifiers.count < rhs.prereleaseIdentifiers.count + return lhs.prerelease < rhs.prerelease // not lexicographically compared } } @@ -186,8 +183,8 @@ extension SymbolGraph.SemanticVersion: CustomStringConvertible { /// A textual description of the `Semantic Version` instance. public var description: String { var versionString = "\(major).\(minor).\(patch)" - if !prereleaseIdentifiers.isEmpty { - versionString += "-" + prereleaseIdentifiers.joined(separator: ".") + if !prerelease.identifiers.isEmpty { + versionString += "-\(prerelease)" } if !buildMetadataIdentifiers.isEmpty { versionString += "+" + buildMetadataIdentifiers.joined(separator: ".") @@ -200,10 +197,6 @@ extension SymbolGraph.SemanticVersion: LosslessStringConvertible { /// Initializes a version struct with the provided version string. /// - Parameter version: A version string to use for creating a new version struct. public init?(_ versionString: String) { - // SemVer 2.0.0 allows only ASCII alphanumerical characters and "-" in the version string, except for "." and "+" as delimiters. ("-" is used as a delimiter between the version core and pre-release identifiers, but it's allowed within pre-release and metadata identifiers as well.) - // Alphanumerics check will come later, after each identifier is split out (i.e. after the delimiters are removed). - guard versionString.allSatisfy(\.isASCII) else { return nil } - let metadataDelimiterIndex = versionString.firstIndex(of: "+") // SemVer 2.0.0 requires that pre-release identifiers come before build metadata identifiers let prereleaseDelimiterIndex = versionString[..<(metadataDelimiterIndex ?? versionString.endIndex)].firstIndex(of: "-") @@ -213,11 +206,9 @@ extension SymbolGraph.SemanticVersion: LosslessStringConvertible { guard versionCoreIdentifiers.count == 3, - // Major, minor, and patch versions must be ASCII numbers, according to the semantic versioning standard. - // Converting each identifier from a substring to an integer doubles as checking if the identifiers have non-numeric characters. - let major = Int(versionCoreIdentifiers[0]), - let minor = Int(versionCoreIdentifiers[1]), - let patch = Int(versionCoreIdentifiers[2]) + let major = validNumericIdentifier(versionCoreIdentifiers[0]), + let minor = validNumericIdentifier(versionCoreIdentifiers[1]), + let patch = validNumericIdentifier(versionCoreIdentifiers[2]) else { return nil } self.major = major @@ -227,23 +218,71 @@ extension SymbolGraph.SemanticVersion: LosslessStringConvertible { if let prereleaseDelimiterIndex = prereleaseDelimiterIndex { let prereleaseStartIndex = versionString.index(after: prereleaseDelimiterIndex) let prereleaseIdentifiers = versionString[prereleaseStartIndex..<(metadataDelimiterIndex ?? versionString.endIndex)].split(separator: ".", omittingEmptySubsequences: false) - guard prereleaseIdentifiers.allSatisfy( { - $0.allSatisfy( { $0.isLetter || $0.isNumber || $0 == "-" } ) - } ) else { return nil } - self.prereleaseIdentifiers = prereleaseIdentifiers.map { String($0) } + guard let prerelease = try? Prerelease(prereleaseIdentifiers) else { + return nil + } + self.prerelease = prerelease } else { - self.prereleaseIdentifiers = [] + self.prerelease = Prerelease(identifiers: []) // This is the member-wise initializer taking `[Identifier]` not `[S: StringProtocol]`. } if let metadataDelimiterIndex = metadataDelimiterIndex { let metadataStartIndex = versionString.index(after: metadataDelimiterIndex) let buildMetadataIdentifiers = versionString[metadataStartIndex...].split(separator: ".", omittingEmptySubsequences: false) - guard buildMetadataIdentifiers.allSatisfy( { - $0.allSatisfy( { $0.isLetter || $0.isNumber || $0 == "-" } ) - } ) else { return nil } + guard buildMetadataIdentifiers.allSatisfy(\.isSemanticVersionBuildMetadataIdentifier) else { + return nil + } self.buildMetadataIdentifiers = buildMetadataIdentifiers.map { String($0) } } else { self.buildMetadataIdentifiers = [] } + + /// Creates an integer-represented numeric identifier from the given identifier. + /// + /// Semantic Versioning 2.0.0 requires valid numeric identifiers to be "0" or ASCII digit sequence without leading "0"s. + /// + /// - Parameter identifier: The given identifier. + /// - Returns: The integer representation of the identifier, if the identifier is a valid Semantic Versioning 2.0.0 numeric identifier, and if it is representable by `Int`; `nil` otherwise. + func validNumericIdentifier(_ identifier: Substring) -> Int? { + // Converting each identifier from a substring to a signed integer doubles as asserting that the identifier is non-empty and that it has no non-ASCII-numeric characters other than an optional leading "+" or "-". + // `Int` is used here instead of `UInt`, because `Int` is a currency type, and because even with `UInt`, the literal '-0' and its leading-zeros variants can still slip through. + guard let numericIdentifier = Int(identifier) else { + return nil + } + // Although `Int.init(_:)` accepts a leading "+" in the argument, we don't need to be check for it here. "+" is the delimiter between pre-release and build metadata, and build metadata does not care for the validity of numeric identifiers. + guard identifier == "0" || (identifier.first != "-" && identifier.first != "0") else { + return nil + } + return numericIdentifier + } + } +} + +extension Character { + /// <#Description#> + internal var isSemanticVersionIdentifierCharacter: Bool { + isASCII && ( isLetter || isNumber || self == "-" ) + } + + /// <#Description#> + internal var isSemanticVersionNumericIdentifierCharacter: Bool { + isASCII && isNumber + } +} + +extension StringProtocol { + /// <#Description#> + internal var isSemanticVersionNumericIdentifier: Bool { + self == "0" || (first != "0" && allSatisfy(\.isSemanticVersionNumericIdentifierCharacter) && !isEmpty) + } + + /// <#Description#> + internal var isSemanticVersionAlphanumericIdentifier: Bool { + allSatisfy(\.isSemanticVersionIdentifierCharacter) && !allSatisfy(\.isSemanticVersionNumericIdentifierCharacter) && !isEmpty + } + + /// <#Description#> + internal var isSemanticVersionBuildMetadataIdentifier: Bool { + allSatisfy(\.isSemanticVersionIdentifierCharacter) && !isEmpty } } diff --git a/Sources/SymbolKit/SymbolGraph/Misc/SemanticVersionError.swift b/Sources/SymbolKit/SymbolGraph/Misc/SemanticVersionError.swift new file mode 100644 index 0000000..ac4898c --- /dev/null +++ b/Sources/SymbolKit/SymbolGraph/Misc/SemanticVersionError.swift @@ -0,0 +1,106 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2021 Apple Inc. and the Swift project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information + See https://swift.org/CONTRIBUTORS.txt for Swift project authors + */ + +extension SymbolGraph { + /// An error that occurs during the creation of a semantic version. + public enum SemanticVersionError: Error, CustomStringConvertible { + /// The identifier at the given position is empty. + /// - Parameter position: The empty identifier's position in the semantic version. + case emptyIdentifier(position: IdentifierPosition) + /// The identifier at the given position contains invalid character(s). + /// - Parameters: + /// - identifier: The identifier that contains invalid character(s). + /// - position: The given identifier's position in the semantic version. + case invalidCharacterInIdentifier(_ identifier: String, position: IdentifierPosition) + /// The numeric identifier at the given position is invalid for the given reason. + /// - Parameters: + /// - identifier: The invalid numeric identifier. + /// - position: The given numeric identifier's position in the semantic version. + /// - errorKind: The reason why the given numeric identifier is invalid. + case invalidNumericIdentifier(_ identifier: String, position: NumericIdentifierPosition, errorKind: NumericIdentifierErrorKind) + /// The version core contains an invalid number of Identifiers. + /// - Parameter identifiers: The version core identifiers in the version string. + case invalidVersionCoreIdentifierCount(identifiers: [String]) + + /// A position of an identifier in a semantic version. + public enum IdentifierPosition: String, CustomStringConvertible { + /// The major version number position in a semantic version. + case major + /// The minor version number position in a semantic version. + case minor + /// The patch version number position in a semantic version. + case patch + /// The pre-release position in a semantic version. + case prerelease + /// The build-metadata position in a semantic version. + case buildMetadata + + /// <#Description#> + public var description: String { + self.rawValue + } + } + + /// A position of a numeric identifier in a semantic version. + public enum NumericIdentifierPosition: String, CustomStringConvertible { + /// The major version number position. + case major + /// The minor version number position. + case minor + /// The patch version number position. + case patch + /// The pre-release position. + case prerelease + + /// <#Description#> + public var description: String { + self.rawValue + } + } + + /// A reason why a numeric identifier is invalid. + public enum NumericIdentifierErrorKind { + /// The numeric identifier contains leading "0" characters. + case leadingZeros + /// The numeric identifier represents a negative value. + case negativeValue + /// The numeric identifier contains non-numeric characters. + case nonNumericCharacter + } + + // this description follows [the "grammar and phrasing" section of Swift's diagnostics guidelines](https://github.com/apple/swift/blob/d1bb98b11ede375a1cee739f964b7d23b6657aaf/docs/Diagnostics.md#grammar-and-phrasing) + /// <#Description#> + public var description: String { + switch self { + case let .emptyIdentifier(position): + return "semantic version \(position) identifier cannot be empty" + case let .invalidCharacterInIdentifier(identifier, position): + return "semantic version \(position) identifier '\(identifier)' cannot conatin characters other than ASCII alphanumerics and hyphen-minus ([0-9A-Za-z-])" + case let .invalidNumericIdentifier(identifier, position, errorKind): + let fault: String + switch errorKind { + case .leadingZeros: + fault = "contain leading '0'" + case .negativeValue: + fault = "represent negative value" + case .nonNumericCharacter: + fault = "conatin non-numeric characters" + } + return "semantic version numeric \(position) identifier '\(identifier)' cannot \(fault)" + case let .invalidVersionCoreIdentifierCount(identifiers): + return """ + semantic version must conatins exactly 3 version core identifiers; \ + \(identifiers.count) given\(identifiers.isEmpty ? "" : " : ")\ + \(identifiers.map { "'\($0)'" } .joined(separator: ", ")) + """ + } + } + } +} diff --git a/Sources/SymbolKit/SymbolGraph/Misc/SemanticVersionPrerelease.swift b/Sources/SymbolKit/SymbolGraph/Misc/SemanticVersionPrerelease.swift new file mode 100644 index 0000000..a8b8098 --- /dev/null +++ b/Sources/SymbolKit/SymbolGraph/Misc/SemanticVersionPrerelease.swift @@ -0,0 +1,135 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2021 Apple Inc. and the Swift project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information + See https://swift.org/CONTRIBUTORS.txt for Swift project authors + */ + +extension SymbolGraph.SemanticVersion { + /// A storage for pre-release identifiers. + internal struct Prerelease { + /// The identifiers. + internal let identifiers: [Identifier] + /// A pre-release identifier. + internal enum Identifier { + /// A numeric pre-release identifier. + /// - Parameter identifier: The identifier. + case numeric(_ identifier: Int) + /// An alphanumeric pre-release identifier. + /// - Parameter identifier: The identifier. + case alphanumeric(_ identifier: String) + } + } +} + +// MARK: - Initializers + +extension SymbolGraph.SemanticVersion.Prerelease { + /// <#Description#> + /// + /// - Note: Empty string translates to an empty pre-release identifier, which is invalid. + /// - Parameter dotSeparatedIdentifiers: <#dotSeparatedIdentifiers description#> + internal init(_ dotSeparatedIdentifiers: String?) throws { + guard let dotSeparatedIdentifiers = dotSeparatedIdentifiers else { + // FIXME: initialize 'identifiers' directly here after [SR-15670](https://bugs.swift.org/projects/SR/issues/SR-15670?filter=allopenissues) is resolved + // currently 'identifiers' cannot be initialized directly because initializer delegation is flow-insensitive + // self.identifiers = [] + self.init(identifiers: []) + return + } + let identifiers = dotSeparatedIdentifiers.split( + separator: ".", + omittingEmptySubsequences: false // must preserve empty identifiers + ) + try self.init(identifiers) + } + + /// <#Description#> + internal init(_ identifiers: C) throws where C.Element == S { + self.identifiers = try identifiers.map { + try Identifier($0) + } + } +} + +extension SymbolGraph.SemanticVersion.Prerelease.Identifier { + /// <#Description#> + internal init(_ identifier: S) throws { + guard !identifier.isEmpty else { + throw SymbolGraph.SemanticVersionError.emptyIdentifier(position: .prerelease) + } + guard identifier.allSatisfy(\.isSemanticVersionIdentifierCharacter) else { + throw SymbolGraph.SemanticVersionError.invalidCharacterInIdentifier(String(identifier), position: .prerelease) + } + if identifier.allSatisfy(\.isNumber) { + // diagnose the identifier as a numeric identifier, if all characters are ASCII digits + guard identifier.first != "0" || identifier == "0" else { + throw SymbolGraph.SemanticVersionError.invalidNumericIdentifier(String(identifier), position: .prerelease, errorKind: .leadingZeros) + } + self = .numeric(Int(identifier)!) + } else { + self = .alphanumeric(String(identifier)) + } + } +} + +// MARK: - Comparable Conformances + +// Compiler synthesised `Equatable`-conformance is correct here. +extension SymbolGraph.SemanticVersion.Prerelease: Comparable { + /// <#Description#> + /// - Parameters: + /// - lhs: <#lhs description#> + /// - rhs: <#rhs description#> + /// - Returns: <#description#> + internal static func <(lhs: Self, rhs: Self) -> Bool { + guard !lhs.identifiers.isEmpty else { return false } // non-pre-release lhs >= potentially pre-release rhs + guard !rhs.identifiers.isEmpty else { return true } // pre-release lhs < non-pre-release rhs + return lhs.identifiers.lexicographicallyPrecedes(rhs.identifiers) + } +} + +// Compiler synthesised `Equatable`-conformance is correct here. +extension SymbolGraph.SemanticVersion.Prerelease.Identifier: Comparable { + /// <#Description#> + /// - Parameters: + /// - lhs: <#lhs description#> + /// - rhs: <#rhs description#> + /// - Returns: <#description#> + internal static func <(lhs: Self, rhs: Self) -> Bool { + switch (lhs, rhs) { + case let (.numeric(lhs), .numeric(rhs)): + return lhs < rhs + case let(.alphanumeric(lhs), .alphanumeric(rhs)): + return lhs < rhs + case (.numeric, .alphanumeric): + return true + case (.alphanumeric, .numeric): + return false + } + } +} + +// MARK: CustomStringConvertible Conformances + +extension SymbolGraph.SemanticVersion.Prerelease: CustomStringConvertible { + /// <#Description#> + internal var description: String { + identifiers.map(\.description).joined(separator: ".") + } +} + +extension SymbolGraph.SemanticVersion.Prerelease.Identifier: CustomStringConvertible { + /// <#Description#> + internal var description: String { + switch self { + case .numeric(let identifier): + return identifier.description + case .alphanumeric(let identifier): + return identifier.description + } + } +}