diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..bc2b5de --- /dev/null +++ b/.github/CODE_OF_CONDUCT.md @@ -0,0 +1 @@ +This project adopts Swift's code of conduct: https://www.swift.org/code-of-conduct/ diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000..6fdf42b --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,15 @@ +# Contributing + +This document covers how you can report any issues you find or contribute with bug fixes and new features. + +## Reporting Issues + +Go ahead and open a new issue [on the repo](https://github.com/gwynne/swift-semver/issues/new). The owners will be notified and we should get back to you shortly. + +## Security Issues + +If you discover a security issue, please follow [the security procedure](https://github.com/gwynne/swift-semver/security/policy). Please **do not** publicly report an issue until it has been fixed. The disclosure policy and timelines are availble in the policy document. + +## Pull Requests + +We are glad to accept and review pull requests for bug fixes and new features. diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 0000000..0c5dcb8 --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,19 @@ +# Security Policy + +## Supported Versions + +At the time of this writing, no "stable" versions have yet been released, and support is provided only for the most recent tagged prerelease version. + +Should this file not yet be updated after this project reaches 1.0, the support policy is to support the most recent major release only until and unless otherwise specified. + +| Version | Supported | +| :-------: | :------------------: | +| - | - | + +## Reporting a Vulnerability + +This project asks that known and suspected vulnerabilities be privately and responsibly disclosed by [filling out a vulnerability report](https://github.com/gwynne/swift-semver/security/advisories/new) on Github[^1]. + +[^1]: See [Github's official documentation of the vulnerability report feature](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability) for additional privacy and safety details. + +**Please do not report security vulnerabilities through public GitHub issues, discussions, or pull requests.** diff --git a/Package.swift b/Package.swift index 96c95c3..bb17456 100644 --- a/Package.swift +++ b/Package.swift @@ -1,4 +1,4 @@ -// swift-tools-version:5.7 +// swift-tools-version:5.8 //===----------------------------------------------------------------------===// // // This source file is part of the swift-semver open source project @@ -13,14 +13,40 @@ //===----------------------------------------------------------------------===// import PackageDescription +let commonSwiftSettings: [PackageDescription.SwiftSetting] = [ + // We deliberately choose to opt in to several upcoming language features. + .enableUpcomingFeature("ConciseMagicFile"), + .enableUpcomingFeature("ForwardTrailingClosures"), + .enableUpcomingFeature("ExistentialAny"), + .enableUpcomingFeature("BareSlashRegexLiterals"), + .enableExperimentalFeature("StrictConcurrency=complete"), +] + let package = Package( name: "swift-semver", + platforms: [ + .macOS(.v13), + .macCatalyst(.v16), + .iOS(.v16), + .watchOS(.v9), + .tvOS(.v16), + ], products: [ .library(name: "SwiftSemver", targets: ["SwiftSemver"]), ], dependencies: [], targets: [ - .target(name: "SwiftSemver", dependencies: []), - .testTarget(name: "SwiftSemverTests", dependencies: [.target(name: "SwiftSemver")]), + .target( + name: "SwiftSemver", + dependencies: [], + swiftSettings: commonSwiftSettings + ), + .testTarget( + name: "SwiftSemverTests", + dependencies: [ + .target(name: "SwiftSemver"), + ], + swiftSettings: commonSwiftSettings + ), ] ) diff --git a/README.md b/README.md index 5f0e136..1e9b3e5 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,10 @@ # SwiftSemver +

+MIT License +Continuous Integration + +Swift 5.8+ +

+ A small library which provides a `SemanticVersion` type, containing a complete implementation of the grammar (both parsing and serialization) and precedence behaviors described by [Semantic Versioning 2.0.0](https://semver.org/spec/v2.0.0.html). diff --git a/Sources/SwiftSemver/SemanticVersion.swift b/Sources/SwiftSemver/SemanticVersion.swift index 1f85864..85f0b5e 100644 --- a/Sources/SwiftSemver/SemanticVersion.swift +++ b/Sources/SwiftSemver/SemanticVersion.swift @@ -29,15 +29,18 @@ /// provides a detailed algorithm for making this determination. The most notable difference between this algorithm and /// an equality comparison is that precedence does not consider build metadata identifiers; this behavior may be /// observable via, e.g., the results of applying a sorting algorithm. +/// +/// > Note: While numeric version components are represented by `Int` rather than `UInt` for convenience, it is always +/// guaranteed that outputs are `>= 0` and it is a precondition for all APIs that integer inputs also be thus. public struct SemanticVersion: Sendable, Hashable { /// The major version number. - public var major: UInt + public var major: Int /// The minor version number. - public var minor: UInt + public var minor: Int /// The patch level number. - public var patch: UInt + public var patch: Int /// The prerelease identifiers. public var prereleaseIdentifiers: [String] @@ -46,23 +49,29 @@ public struct SemanticVersion: Sendable, Hashable { public var buildMetadataIdentifiers: [String] /// Create a semantic version from the individual components. + /// + /// - Parameters: + /// - major: The major version number. + /// - minor: The minor version number. + /// - patch: The patch level number. + /// - prereleaseIdentifiers: A list of prerelease identifiers. + /// - buildMetadataIdentifiers: A list of build metadata identifiers. /// - /// - Important: It is considered **programmer error** to provide prerelease identifiers and/or build metadata - /// identifiers containing invalid characters (e.g. anything other than `[A-Za-z0-9-]`); doing so will trigger a - /// a fatal error. + /// - Precondition: `major >= 0, minor >= 0, patch >= 0` + /// - Precondition: All elements of `prereleaseIdentifiers` and `buildMetadataIdentifiers` must contain only the + /// characters `[A-Za-z0-9-]`. + /// - Precondition: Elements of `prereleaseIdentifiers` consisting of only numeric digits may not start with `0`. public init( - _ major: UInt, - _ minor: UInt, - _ patch: UInt, + _ major: Int, + _ minor: Int, + _ patch: Int, prereleaseIdentifiers: [String] = [], buildMetadataIdentifiers: [String] = [] ) { - guard (prereleaseIdentifiers + buildMetadataIdentifiers).allSatisfy({ $0.allSatisfy(\.isValidInSemverIdentifier) }) else { - fatalError("Invalid character found in semver identifier, must match [A-Za-z0-9-]") - } - guard prereleaseIdentifiers.allSatisfy({ $0.isValidSemverPrereleaseIdentifier }) else { - fatalError("Invalid prerelease identifier found, must be alphanumeric, exactly 0, or not start with 0.") - } + precondition(major >= 0 && minor >= 0 && patch >= 0) + precondition(prereleaseIdentifiers.allSatisfy(\.semver_isValidPrereleaseIdentifier)) + precondition(buildMetadataIdentifiers.allSatisfy(\.semver_isValidBuildMetadataIdentifier)) + self.major = major self.minor = minor self.patch = patch @@ -71,116 +80,147 @@ public struct SemanticVersion: Sendable, Hashable { } /// Create a semantic version from a provided string, parsing it according to spec. + /// + /// ## Discussion + /// The regex used by this method is noticeably uglier than it needs to be, thanks to the engine not yet + /// supporting `\g<>` subpattern references or `(?(DEFINE))` conditions. Without these limitations, the + /// regex would look more like this (longer, but much more understandable): + /// + /// ```regex + /// #/ + /// (?nPi) + /// (?(DEFINE)(? 0|([1-9]\d*) )) # numeric identifier (may not start with 0 unless equal to 0) + /// (?(DEFINE)(? [a-z\d-]*[a-z-][a-z\d-]*)) # alphanumeric identifier (must contain at least one non-digit) + /// (?(DEFINE)(? \g|\g )) # prerelease identifier (numeric or alphanumeric) + /// (?(DEFINE)(? [a-z\d-]+ )) # build metadata identifier (any alphanumeric) /// + /// (?\d+) # major version number + /// \.(?\d+) # minor version number + /// \.(?\d+) # patch version number + /// (\-(?(\g\.)*\g))? # list of zero or more prerelease identifiers + /// (\+(? (\g \.)*\g ))? # list of zero or more build metadata identifiers + /// /# + /// ``` + /// + /// - Parameter string: The string or substring to parse. /// - Returns: `nil` if the provided string is not a valid semantic version. - /// - /// - TODO: Possibly throw more specific validation errors? Would this be useful? - /// - /// - TODO: This code, while it does check validity better than what was here before, is ugly as heck. Clean it up. - public init?(string: some StringProtocol) { - guard string.allSatisfy(\.isASCII) else { return nil } - - var idx = string.startIndex - func readNumber(usingIdx idx: inout String.Index) -> UInt? { - let startIdx = idx - idx = string[idx...].firstIndex(where: { !$0.isWholeNumber }) ?? string.endIndex - return UInt(string[startIdx ..< idx]) + public init?(string: S) + where S: StringProtocol, S.SubSequence == Substring + { + let pattern = #/ + (?nPi) + (?\d+)\.(?\d+)\.(?\d+) + (\-(?((0|([1-9]\d*)|([a-z\d-]*[a-z-][a-z\d-]*))\.)*(0|([1-9]\d*)|([a-z\d-]*[a-z-][a-z\d-]*))))? + (\+(?(([a-z\d-]+)\.)*([a-z\d-]+)))? + /# + + guard let match = string.wholeMatch(of: pattern) else { + return nil } - func readIdent(usingIdx idx: inout String.Index) -> String? { - let startIdx = idx - idx = string[idx...].firstIndex(where: { !$0.isValidInSemverIdentifier }) ?? string.endIndex - return idx > startIdx ? String(string[startIdx ..< idx]) : nil - } - - guard let major = readNumber(usingIdx: &idx) else { return nil } - guard idx < string.endIndex, string[idx] == "." else { return nil } - string.formIndex(after: &idx) - - guard let minor = readNumber(usingIdx: &idx) else { return nil } - guard idx < string.endIndex, string[idx] == "." else { return nil } - string.formIndex(after: &idx) - - guard let patch = readNumber(usingIdx: &idx) else { return nil } - - var prereleaseIdentifiers: [String] = [], buildMetadataIdentifiers: [String] = [] - var seenPlus = false - let identsStartIdx = idx - while idx < string.endIndex { - // String must be one of "-" (prerelease indicator), "+" (build metadata indicator), "." (identifier separator) - switch string[idx] { - case "+" where !seenPlus: - seenPlus = true // saw a build metadata indicator for the first time, read identifiers into that now - case "-" where idx == identsStartIdx: - break // saw a prerelease indicator at start of idents parsing - case "." where idx > identsStartIdx: - break // saw identifier separator in valid position - default: - return nil // invalid separator - } - string.formIndex(after: &idx) - guard let ident = readIdent(usingIdx: &idx) else { return nil } - if seenPlus { - buildMetadataIdentifiers.append(ident) - } else { - guard ident.isValidSemverPrereleaseIdentifier else { - return nil - } - prereleaseIdentifiers.append(ident) - } - } - - self.major = major - self.minor = minor - self.patch = patch - self.prereleaseIdentifiers = prereleaseIdentifiers - self.buildMetadataIdentifiers = buildMetadataIdentifiers + + self.init( + Int(match.output.major)!, + Int(match.output.minor)!, + Int(match.output.patch)!, + prereleaseIdentifiers: match.output.prerelids?.split(separator: ".").map(String.init(_:)) ?? [], + buildMetadataIdentifiers: match.output.buildids?.split(separator: ".").map(String.init(_:)) ?? [] + ) } } extension SemanticVersion: Comparable { /// Implements the "precedence" ordering specified by the semver specification. public static func < (lhs: SemanticVersion, rhs: SemanticVersion) -> Bool { - let lhsComponents = [lhs.major, lhs.minor, lhs.patch] - let rhsComponents = [rhs.major, rhs.minor, rhs.patch] - - guard lhsComponents == rhsComponents else { - return lhsComponents.lexicographicallyPrecedes(rhsComponents) + /// Unequal major versions - lower major version has lower precedence. + guard lhs.major == rhs.major else { + return lhs.major < rhs.major + } + + /// Unequal minor versions - lower minor version has lower precedence. + guard lhs.minor == rhs.minor else { + return lhs.minor < rhs.minor + } + + /// Unequal patch versions - lower patch version has lower precedence. + guard lhs.patch == rhs.patch else { + return lhs.patch < rhs.patch } - if lhs.prereleaseIdentifiers.isEmpty { return false } // Non-prerelease lhs >= potentially prerelease rhs - if rhs.prereleaseIdentifiers.isEmpty { return true } // Prerelease lhs < non-prerelease rhs + /// If there are prerelease identifiers on only one of the versions, the one without any + /// has higher precedence. If there are none on either, the versions are equal. + switch (lhs.prereleaseIdentifiers.isEmpty, rhs.prereleaseIdentifiers.isEmpty) { + case (true, true), (true, false): + return false // equal or higher precedence + case (false, true): + return true // lower precedence + case (false, false): + break // further comparison needed + } - switch zip(lhs.prereleaseIdentifiers, rhs.prereleaseIdentifiers) - .first(where: { $0 != $1 }) - .map({ ((Int($0) ?? $0) as Any, (Int($1) ?? $1) as Any) }) - { - case let .some((lId as Int, rId as Int)): return lId < rId - case let .some((lId as String, rId as String)): return lId < rId - case .some((is Int, _)): return true // numeric prerelease identifier always < non-numeric - case .some: return false // rhs > lhs - case .none: break // all prerelease identifiers are equal + /// Find the first pair of mismatched prerelease identifiers, if such a pair exists. + /// If it doesn't, precedence belongs to the version with more identifiers. + guard let (lhsIdent, rhsIdent) = zip(lhs.prereleaseIdentifiers, rhs.prereleaseIdentifiers) + .first(where: { $0 != $1 }) + else { + return lhs.prereleaseIdentifiers.count < rhs.prereleaseIdentifiers.count + } + + /// A numeric prerelease identifier always has lower precedence than an alphanumeric one. + if lhsIdent.allSatisfy(\.isWholeNumber) { + guard rhsIdent.allSatisfy(\.isWholeNumber) else { + return true + } + + /// For the sake of absolutely fanatical compliance with the spec, don't convert to Int + /// to compare numeric values, as this will fail for extremely large numbers. If the + /// values have different lengths, the shorter one has lower precedence; otherwise, a + /// lexicographic comparison will yield the correct result. + guard lhsIdent.count == rhsIdent.count else { + return lhsIdent.count < rhsIdent.count + } + } else { + guard !rhsIdent.allSatisfy(\.isWholeNumber) else { + return false + } } - return lhs.prereleaseIdentifiers.count < rhs.prereleaseIdentifiers.count + /// Alphanumeric identifiers and equal-length numeric identifiers are compared lexicographically. + return lhsIdent.lexicographicallyPrecedes(rhsIdent) } } extension SemanticVersion: LosslessStringConvertible { - /// An additional API guarantee is made by this type that this property will always yield a string - /// which is correctly formatted as a valid semantic version number. + // See `LosslessStringConvertible.init(_:)`. + public init?(_ description: String) { + self.init(string: description) + } + + // See `CustomStringConvertible.description`. public var description: String { """ \(self.major).\ \(self.minor).\ \(self.patch)\ - \(self.prereleaseIdentifiers.joined(separator: ".", prefix: "-"))\ - \(self.buildMetadataIdentifiers.joined(separator: ".", prefix: "+")) + \(self.prereleaseIdentifiers.isEmpty ? "" : "-")\ + \(self.prereleaseIdentifiers.joined(separator: "."))\ + \(self.buildMetadataIdentifiers.isEmpty ? "" : "+")\ + \(self.buildMetadataIdentifiers.joined(separator: ".")) """ } - - // See `LosslessStringConvertible.init(_:)`. Identical semantics to ``init?(string:)``. - public init?(_ description: String) { - self.init(string: description) +} + +extension SemanticVersion: CustomDebugStringConvertible { + // See `CustomDebugStringConvertible.debugDescripton`. + public var debugDescription: String { + """ + Version:{\ + Major: \(self.major) \ + Minor: \(self.minor) \ + Patch: \(self.patch) \ + Prerelease identifiers: [\(self.prereleaseIdentifiers.joined(separator: ", "))] \ + Build metadata identifiers: [\(self.buildMetadataIdentifiers.joined(separator: ", "))]\ + } + """ } } @@ -201,57 +241,3 @@ extension SemanticVersion: Codable { self = version } } - -fileprivate extension Array { - /// Identical to ``joined(separator:)``, except that when the result is non-empty, the provided `prefix` will be - /// prepended to it. This is a mildly silly solution to the issue of how best to implement "add a joiner character - /// between one interpolation and the next, but only if the second one is non-empty". - func joined(separator: String, prefix: String) -> String { - self.isEmpty ? "" : "\(prefix)\(self.joined(separator: separator))" - } -} - -fileprivate extension Character { - /// Valid characters in a semver identifier are defined by these BNF rules, - /// taken directly from [the SemVer BNF grammar][semver2bnf]: - /// - /// [semver2bnf]: https://semver.org/spec/v2.0.0.html#backusnaur-form-grammar-for-valid-semver-versions - /// - /// ```bnf - /// ::= - /// | - /// - /// ::= - /// | "-" - /// - /// ::= "0" - /// | - /// - /// ::= "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" - /// - /// ::= "A" | "B" | "C" | "D" | "E" | "F" | "G" | "H" | "I" | "J" - /// | "K" | "L" | "M" | "N" | "O" | "P" | "Q" | "R" | "S" | "T" - /// | "U" | "V" | "W" | "X" | "Y" | "Z" | "a" | "b" | "c" | "d" - /// | "e" | "f" | "g" | "h" | "i" | "j" | "k" | "l" | "m" | "n" - /// | "o" | "p" | "q" | "r" | "s" | "t" | "u" | "v" | "w" | "x" - /// | "y" | "z" - /// ``` - var isValidInSemverIdentifier: Bool { - self.isLetter || self.isWholeNumber || self == "-" - } -} - -fileprivate extension StringProtocol { - /// A valid prerelease identifier must either: - /// - /// - Be exactly "0", - /// - Not start with "0", or - /// - Contain non-numeric characters - /// - /// - var isValidSemverPrereleaseIdentifier: Bool { - self == "0" || - !self.starts(with: "0") || - self.contains { !$0.isWholeNumber } - } -} diff --git a/Sources/SwiftSemver/String+SemanticIdentifierValidation.swift b/Sources/SwiftSemver/String+SemanticIdentifierValidation.swift new file mode 100644 index 0000000..21328c3 --- /dev/null +++ b/Sources/SwiftSemver/String+SemanticIdentifierValidation.swift @@ -0,0 +1,29 @@ +extension String { + /// `true` if the string is valid for use as a SemVer prerelease identifier + /// + /// A valid prerelease identifier must be nonempty, contain only ASCII letters, numbers, and `-`, and + /// must obey **exactly one** of the following rules: + /// + /// - The identifier consists solely of a single zero digit (`0`), + /// - The identifier consists solely of numeric digits, of which the first is **not** zero, **or** + /// - The identifier contains at least one non-numeric character. + /// + /// See [the SemVer BNF grammar][semver2bnf] for the formal grammar specification.. + /// + /// [semver2bnf]: https://semver.org/spec/v2.0.0.html#backusnaur-form-grammar-for-valid-semver-versions + public var semver_isValidPrereleaseIdentifier: Bool { + self.wholeMatch(of: /(?inP)0|([1-9]\d*)|([a-z\d-]*[a-z-][a-z\d-]*)/) != nil + } + + /// `true` if the string is valid for use as a SemVer build metadata identifier + /// + /// A valid build metadata identifier must be nonempty and contain only ASCII letters, numbers, and `-`. + /// It is not subject to the additional rules governing prerelease identifiers. + /// + /// See [the SemVer BNF grammar][semver2bnf] for the formal grammar specification. + /// + /// [semver2bnf]: https://semver.org/spec/v2.0.0.html#backusnaur-form-grammar-for-valid-semver-versions + public var semver_isValidBuildMetadataIdentifier: Bool { + self.wholeMatch(of: /(?inP)[a-z\d-]+/) != nil + } +} diff --git a/Tests/SwiftSemverTests/SwiftSemverTests.swift b/Tests/SwiftSemverTests/SwiftSemverTests.swift index d29c504..398d68b 100644 --- a/Tests/SwiftSemverTests/SwiftSemverTests.swift +++ b/Tests/SwiftSemverTests/SwiftSemverTests.swift @@ -40,7 +40,7 @@ final class SwiftSemverTests: XCTestCase { SemanticVersion(1, 0, 0, prereleaseIdentifiers: ["b", "a"], buildMetadataIdentifiers: ["a", "b"])) } - func testParseVersionCore() throws { + func testParseVersion() throws { XCTAssertEqual(SemanticVersion("0.0.0"), SemanticVersion(0, 0, 0)) XCTAssertEqual(SemanticVersion("0.0.1"), SemanticVersion(0, 0, 1)) @@ -76,7 +76,7 @@ final class SwiftSemverTests: XCTestCase { } } - func testParseVersionCoreWithPreRelease() throws { + func testParseVersionWithPreRelease() throws { XCTAssertEqual(SemanticVersion("1.2.3-dev"), SemanticVersion(1, 2, 3, prereleaseIdentifiers: ["dev"])) XCTAssertEqual(SemanticVersion("1.2.3-dev1.dev2"), SemanticVersion(1, 2, 3, prereleaseIdentifiers: ["dev1", "dev2"])) XCTAssertEqual(SemanticVersion("1.2.3-dev.1.more"), SemanticVersion(1, 2, 3, prereleaseIdentifiers: ["dev", "1", "more"])) @@ -99,7 +99,7 @@ final class SwiftSemverTests: XCTestCase { } } - func testParseVersionCoreWithBuild() throws { + func testParseVersionWithBuild() throws { XCTAssertEqual(SemanticVersion("1.2.3+dev"), SemanticVersion(1, 2, 3, prereleaseIdentifiers: [], buildMetadataIdentifiers: ["dev"])) XCTAssertEqual(SemanticVersion("1.2.3+dev1.dev2"), SemanticVersion(1, 2, 3, prereleaseIdentifiers: [], buildMetadataIdentifiers: ["dev1", "dev2"])) XCTAssertEqual(SemanticVersion("1.2.3+dev.1.more"), SemanticVersion(1, 2, 3, prereleaseIdentifiers: [], buildMetadataIdentifiers: ["dev", "1", "more"])) @@ -118,7 +118,7 @@ final class SwiftSemverTests: XCTestCase { } } - func testParseVersionCoreWithPrereleaseAndBuild() throws { + func testParseVersionWithPrereleaseAndBuild() throws { XCTAssertEqual(SemanticVersion("1.2.3-dev+dev"), SemanticVersion(1, 2, 3, prereleaseIdentifiers: ["dev"], buildMetadataIdentifiers: ["dev"])) XCTAssertEqual(SemanticVersion("1.2.3-dev1.dev2+dev1.dev2"), SemanticVersion(1, 2, 3, prereleaseIdentifiers: ["dev1", "dev2"], buildMetadataIdentifiers: ["dev1", "dev2"])) XCTAssertEqual(SemanticVersion("1.2.3-dev.1.more+dev.1.more"), SemanticVersion(1, 2, 3, prereleaseIdentifiers: ["dev", "1", "more"], buildMetadataIdentifiers: ["dev", "1", "more"])) @@ -146,7 +146,40 @@ final class SwiftSemverTests: XCTestCase { XCTAssertEqual(SemanticVersion(1, 0, 0, prereleaseIdentifiers: ["1"], buildMetadataIdentifiers: ["1"]).description, "1.0.0-1+1") } - func testPrecedenceWithVersionCoreOnly() throws { + func testVersionDebugDescription() throws { + XCTAssertEqual(SemanticVersion(1, 0, 0, prereleaseIdentifiers: [], buildMetadataIdentifiers: []).debugDescription, + "Version:{Major: 1 Minor: 0 Patch: 0 Prerelease identifiers: [] Build metadata identifiers: []}") + XCTAssertEqual(SemanticVersion(1, 1, 0, prereleaseIdentifiers: [], buildMetadataIdentifiers: []).debugDescription, + "Version:{Major: 1 Minor: 1 Patch: 0 Prerelease identifiers: [] Build metadata identifiers: []}") + XCTAssertEqual(SemanticVersion(1, 1, 1, prereleaseIdentifiers: [], buildMetadataIdentifiers: []).debugDescription, + "Version:{Major: 1 Minor: 1 Patch: 1 Prerelease identifiers: [] Build metadata identifiers: []}") + XCTAssertEqual(SemanticVersion(1, 0, 0, prereleaseIdentifiers: ["a"], buildMetadataIdentifiers: []).debugDescription, + "Version:{Major: 1 Minor: 0 Patch: 0 Prerelease identifiers: [a] Build metadata identifiers: []}") + XCTAssertEqual(SemanticVersion(1, 0, 0, prereleaseIdentifiers: ["a", "b"], buildMetadataIdentifiers: []).debugDescription, + "Version:{Major: 1 Minor: 0 Patch: 0 Prerelease identifiers: [a, b] Build metadata identifiers: []}") + XCTAssertEqual(SemanticVersion(1, 0, 0, prereleaseIdentifiers: ["1", "a"], buildMetadataIdentifiers: []).debugDescription, + "Version:{Major: 1 Minor: 0 Patch: 0 Prerelease identifiers: [1, a] Build metadata identifiers: []}") + XCTAssertEqual(SemanticVersion(1, 0, 0, prereleaseIdentifiers: ["1"], buildMetadataIdentifiers: []).debugDescription, + "Version:{Major: 1 Minor: 0 Patch: 0 Prerelease identifiers: [1] Build metadata identifiers: []}") + XCTAssertEqual(SemanticVersion(1, 0, 0, prereleaseIdentifiers: [], buildMetadataIdentifiers: ["a"]).debugDescription, + "Version:{Major: 1 Minor: 0 Patch: 0 Prerelease identifiers: [] Build metadata identifiers: [a]}") + XCTAssertEqual(SemanticVersion(1, 0, 0, prereleaseIdentifiers: [], buildMetadataIdentifiers: ["a", "b"]).debugDescription, + "Version:{Major: 1 Minor: 0 Patch: 0 Prerelease identifiers: [] Build metadata identifiers: [a, b]}") + XCTAssertEqual(SemanticVersion(1, 0, 0, prereleaseIdentifiers: [], buildMetadataIdentifiers: ["1", "a"]).debugDescription, + "Version:{Major: 1 Minor: 0 Patch: 0 Prerelease identifiers: [] Build metadata identifiers: [1, a]}") + XCTAssertEqual(SemanticVersion(1, 0, 0, prereleaseIdentifiers: [], buildMetadataIdentifiers: ["1"]).debugDescription, + "Version:{Major: 1 Minor: 0 Patch: 0 Prerelease identifiers: [] Build metadata identifiers: [1]}") + XCTAssertEqual(SemanticVersion(1, 0, 0, prereleaseIdentifiers: ["a"], buildMetadataIdentifiers: ["a"]).debugDescription, + "Version:{Major: 1 Minor: 0 Patch: 0 Prerelease identifiers: [a] Build metadata identifiers: [a]}") + XCTAssertEqual(SemanticVersion(1, 0, 0, prereleaseIdentifiers: ["a", "b"], buildMetadataIdentifiers: ["a", "b"]).debugDescription, + "Version:{Major: 1 Minor: 0 Patch: 0 Prerelease identifiers: [a, b] Build metadata identifiers: [a, b]}") + XCTAssertEqual(SemanticVersion(1, 0, 0, prereleaseIdentifiers: ["1", "a"], buildMetadataIdentifiers: ["1", "a"]).debugDescription, + "Version:{Major: 1 Minor: 0 Patch: 0 Prerelease identifiers: [1, a] Build metadata identifiers: [1, a]}") + XCTAssertEqual(SemanticVersion(1, 0, 0, prereleaseIdentifiers: ["1"], buildMetadataIdentifiers: ["1"]).debugDescription, + "Version:{Major: 1 Minor: 0 Patch: 0 Prerelease identifiers: [1] Build metadata identifiers: [1]}") + } + + func testPrecedenceWithVersionOnly() throws { // Comparable < XCTAssertLessThan(SemanticVersion("0.0.0")!, SemanticVersion("0.0.1")!) XCTAssertLessThan(SemanticVersion("0.0.0")!, SemanticVersion("0.1.0")!) @@ -250,6 +283,28 @@ final class SwiftSemverTests: XCTestCase { } } } + + func testCodableBehavior() throws { + let encoder = JSONEncoder(), decoder = JSONDecoder() + + let testVersion = try XCTUnwrap(SemanticVersion("1.2.3-4.alpha.5+a.b.c")) + + let json = String(decoding: try encoder.encode(testVersion), as: UTF8.self) + XCTAssertEqual(json, "\"\(testVersion)\"") + + let result = try decoder.decode(SemanticVersion.self, from: Data(json.utf8)) + XCTAssertEqual(result.major, testVersion.major) + XCTAssertEqual(result.minor, testVersion.minor) + XCTAssertEqual(result.patch, testVersion.patch) + XCTAssertEqual(result.prereleaseIdentifiers, testVersion.prereleaseIdentifiers) + XCTAssertEqual(result.buildMetadataIdentifiers, testVersion.buildMetadataIdentifiers) + + XCTAssertThrowsError(try decoder.decode(SemanticVersion.self, from: Data("\"not a valid version string\"".utf8))) { + guard let decodingError = $0 as? DecodingError else { return XCTFail("Expected a DecodingError but got \($0)") } + guard case .dataCorrupted(let context) = decodingError else { return XCTFail("Expected DecodingError.dataCorrupted but got \(decodingError)") } + XCTAssert(context.codingPath.isEmpty) + } + } } fileprivate extension RangeReplaceableCollection {