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
+
+
+
+
+
+
+
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 {