diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c527b21..3f00321 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,7 +10,7 @@ jobs: strategy: matrix: # https://github.com/actions/virtual-environments - os: [macos-12, ubuntu-20.04] + os: [macos-13, ubuntu-24.04] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 @@ -23,8 +23,8 @@ jobs: run-tests: strategy: matrix: - swift-version: ['5.6', '5.7', '5.8', '5.9', '5.10'] - runs-on: ubuntu-20.04 + swift-version: ['5.10', '6.0', '6.1'] + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 - uses: fwal/setup-swift@v2 diff --git a/Package.swift b/Package.swift index bf4aa0c..6587f8a 100644 --- a/Package.swift +++ b/Package.swift @@ -18,6 +18,7 @@ import PackageDescription let package = Package( name: "SemanticVersion", + platforms: [.macOS("13.0")], products: [ .library( name: "SemanticVersion", diff --git a/Sources/SemanticVersion/NSRegularExpression+ext.swift b/Sources/SemanticVersion/NSRegularExpression+ext.swift deleted file mode 100644 index a7baa7a..0000000 --- a/Sources/SemanticVersion/NSRegularExpression+ext.swift +++ /dev/null @@ -1,48 +0,0 @@ -// Copyright Dave Verwer, Sven A. Schmidt, and other contributors. -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -import Foundation - - -extension NSRegularExpression { - convenience init(_ pattern: String, options: NSRegularExpression.Options) { - do { - try self.init(pattern: pattern, options: options) - } catch { - preconditionFailure("Illegal regular expression: \(pattern).") - } - } -} - - -extension NSRegularExpression { - func matches(_ string: String) -> Bool { - let range = NSRange(string.startIndex..., in: string) - return firstMatch(in: string, options: [], range: range) != nil - } - - func matchGroups(_ string: String, options: NSRegularExpression.Options = []) -> [String] { - let range = NSRange(string.startIndex..., in: string) - guard let match = firstMatch(in: string, options: [], range: range) else { return [] } - - // Skip over index 0 which is the range of the whole match - return (1...numberOfCaptureGroups).map { - if let r = Range(match.range(at: $0), in: string) { - return String(string[r]) - } else { - return "" - } - } - } -} diff --git a/Sources/SemanticVersion/SemanticVersion+Codable.swift b/Sources/SemanticVersion/SemanticVersion+Codable.swift index 8b106ae..653c222 100644 --- a/Sources/SemanticVersion/SemanticVersion+Codable.swift +++ b/Sources/SemanticVersion/SemanticVersion+Codable.swift @@ -12,6 +12,8 @@ // See the License for the specific language governing permissions and // limitations under the License. +#if canImport(Foundation) + import Foundation public enum SemanticVersionStrategy { @@ -100,3 +102,5 @@ extension SemanticVersion: Codable { } } } + +#endif diff --git a/Sources/SemanticVersion/SemanticVersion+LosslessStringConvertible.swift b/Sources/SemanticVersion/SemanticVersion+LosslessStringConvertible.swift new file mode 100644 index 0000000..c412830 --- /dev/null +++ b/Sources/SemanticVersion/SemanticVersion+LosslessStringConvertible.swift @@ -0,0 +1,64 @@ +// Copyright Dave Verwer, Sven A. Schmidt, and other contributors. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + +extension SemanticVersion: LosslessStringConvertible { + + /// Initialize a version from a string. Returns `nil` if the string is not a semantic version. + /// - Parameter string: Version string. + public init?(_ string: String) { + guard let match = string.wholeMatch(of: semVerRegex) else { return nil } + guard + let major = Int(match.major), + let minor = Int(match.minor), + let patch = Int(match.patch) + else { return nil } + self = .init(major, minor, patch, + match.prerelease.map(String.init) ?? "", + match.buildmetadata.map(String.init) ?? "") + } + + public var description: String { + let pre = preRelease.isEmpty ? "" : "-" + preRelease + let bld = build.isEmpty ? "" : "+" + build + return "\(major).\(minor).\(patch)\(pre)\(bld)" + } +} + + +// Source: https://regex101.com/r/Ly7O1x/3/ +// Linked from https://semver.org +let semVerRegex = #/ + ^ + v? # SPI extension: allow leading 'v' + (?0|[1-9]\d*) + \. + (?0|[1-9]\d*) + \. + (?0|[1-9]\d*) + (?:- + (? + (?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*) + (?:\. + (?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*) + ) + *) + )? + (?:\+ + (?[0-9a-zA-Z-]+ + (?:\.[0-9a-zA-Z-]+) + *) + )? + $ + /# diff --git a/Sources/SemanticVersion/SemanticVersion.swift b/Sources/SemanticVersion/SemanticVersion.swift index d5bbf0f..0f38b37 100644 --- a/Sources/SemanticVersion/SemanticVersion.swift +++ b/Sources/SemanticVersion/SemanticVersion.swift @@ -12,8 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -import Foundation - /// `SemanticVersion` is a struct representing a software or project version according to ["Semantic Versioning"](https://semver.org). /// @@ -50,28 +48,6 @@ public struct SemanticVersion: Equatable, Hashable { } } -extension SemanticVersion: LosslessStringConvertible { - - /// Initialize a version from a string. Returns `nil` if the string is not a semantic version. - /// - Parameter string: Version string. - public init?(_ string: String) { - let groups = semVerRegex.matchGroups(string) - guard - groups.count == semVerRegex.numberOfCaptureGroups, - let major = Int(groups[0]), - let minor = Int(groups[1]), - let patch = Int(groups[2]) - else { return nil } - self = .init(major, minor, patch, groups[3], groups[4]) - } - - public var description: String { - let pre = preRelease.isEmpty ? "" : "-" + preRelease - let bld = build.isEmpty ? "" : "+" + build - return "\(major).\(minor).\(patch)\(pre)\(bld)" - } -} - extension SemanticVersion: Comparable { public static func < (lhs: SemanticVersion, rhs: SemanticVersion) -> Bool { @@ -140,7 +116,8 @@ extension SemanticVersion.PreReleaseIdentifier: Comparable { } -extension Array: Comparable where Element == SemanticVersion.PreReleaseIdentifier { +#if compiler(>=6) +extension Array: @retroactive Comparable where Element == SemanticVersion.PreReleaseIdentifier { public static func < (lhs: Self, rhs: Self) -> Bool { // Per section 11.4 of the semver spec, compare left to right until a // difference is found. @@ -154,64 +131,24 @@ extension Array: Comparable where Element == SemanticVersion.PreReleaseIdentifie return lhs.count < rhs.count } } +#else +extension Array: Comparable where Element == SemanticVersion.PreReleaseIdentifier { + public static func < (lhs: Self, rhs: Self) -> Bool { + // Per section 11.4 of the semver spec, compare left to right until a + // difference is found. + // See: https://semver.org/#spec-item-11 + for (lhIdentifier, rhIdentifier) in zip(lhs, rhs) { + if lhIdentifier != rhIdentifier { return lhIdentifier < rhIdentifier } + } -#if swift(>=5.5) -extension SemanticVersion: Sendable {} + // 11.4.4 - A larger set of identifiers will have a higher precendence + // than a smaller set, if all the preceding identifiers are equal. + return lhs.count < rhs.count + } +} #endif -// Source: https://regex101.com/r/Ly7O1x/3/ -// Linked from https://semver.org -#if swift(>=5) - -let semVerRegex = NSRegularExpression(#""" -^ -v? # SPI extension: allow leading 'v' -(?0|[1-9]\d*) -\. -(?0|[1-9]\d*) -\. -(?0|[1-9]\d*) -(?:- - (? - (?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*) - (?:\. - (?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*) - ) - *) -)? -(?:\+ - (?[0-9a-zA-Z-]+ - (?:\.[0-9a-zA-Z-]+) - *) -)? -$ -"""#, options: [.allowCommentsAndWhitespace]) - -#else - -let semVerRegex = NSRegularExpression(""" -^ -v? # SPI extension: allow leading 'v' -(?0|[1-9]\\d*) -\\. -(?0|[1-9]\\d*) -\\. -(?0|[1-9]\\d*) -(?:- - (? - (?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*) - (?:\\. - (?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*) - ) - *) -)? -(?:\\+ - (?[0-9a-zA-Z-]+ - (?:\\.[0-9a-zA-Z-]+) - *) -)? -$ -""", options: [.allowCommentsAndWhitespace]) - +#if swift(>=5.5) +extension SemanticVersion: Sendable {} #endif diff --git a/Tests/SemanticVersionTests/SemanticVersionCodingTests.swift b/Tests/SemanticVersionTests/SemanticVersionCodableTests.swift similarity index 90% rename from Tests/SemanticVersionTests/SemanticVersionCodingTests.swift rename to Tests/SemanticVersionTests/SemanticVersionCodableTests.swift index b60d423..ee204c5 100644 --- a/Tests/SemanticVersionTests/SemanticVersionCodingTests.swift +++ b/Tests/SemanticVersionTests/SemanticVersionCodableTests.swift @@ -1,15 +1,24 @@ +// Copyright Dave Verwer, Sven A. Schmidt, and other contributors. // -// SemanticVersionCodingTests.swift -// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at // -// Created by Chris Eplett on 11/3/23. +// http://www.apache.org/licenses/LICENSE-2.0 // +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#if canImport(Foundation) import XCTest import SemanticVersion -final class SemanticVersionCodingTests: XCTestCase { +final class SemanticVersionCodableTests: XCTestCase { func test_defaultCodable_is_default() throws { XCTAssertEqual(.defaultCodable, JSONEncoder().semanticVersionEncodingStrategy) XCTAssertEqual(.defaultCodable, JSONDecoder().semanticVersionDecodingStrategy) @@ -191,3 +200,5 @@ final class SemanticVersionCodingTests: XCTestCase { } } } + +#endif diff --git a/Tests/SemanticVersionTests/SemanticVersionTests.swift b/Tests/SemanticVersionTests/SemanticVersionTests.swift index a9d32c0..fcfe380 100644 --- a/Tests/SemanticVersionTests/SemanticVersionTests.swift +++ b/Tests/SemanticVersionTests/SemanticVersionTests.swift @@ -18,83 +18,83 @@ import XCTest final class SemanticVersionTests: XCTestCase { func test_semVerRegex_valid() throws { - XCTAssert(semVerRegex.matches("0.0.4")) - XCTAssert(semVerRegex.matches("1.2.3")) - XCTAssert(semVerRegex.matches("10.20.30")) - XCTAssert(semVerRegex.matches("1.1.2-prerelease+meta")) - XCTAssert(semVerRegex.matches("1.1.2+meta")) - XCTAssert(semVerRegex.matches("1.1.2+meta-valid")) - XCTAssert(semVerRegex.matches("1.0.0-alpha")) - XCTAssert(semVerRegex.matches("1.0.0-beta")) - XCTAssert(semVerRegex.matches("1.0.0-alpha.beta")) - XCTAssert(semVerRegex.matches("1.0.0-alpha.beta.1")) - XCTAssert(semVerRegex.matches("1.0.0-alpha.1")) - XCTAssert(semVerRegex.matches("1.0.0-alpha0.valid")) - XCTAssert(semVerRegex.matches("1.0.0-alpha.0valid")) - XCTAssert(semVerRegex.matches("1.0.0-alpha-a.b-c-somethinglong+build.1-aef.1-its-okay")) - XCTAssert(semVerRegex.matches("1.0.0-rc.1+build.1")) - XCTAssert(semVerRegex.matches("2.0.0-rc.1+build.123")) - XCTAssert(semVerRegex.matches("1.2.3-beta")) - XCTAssert(semVerRegex.matches("10.2.3-DEV-SNAPSHOT")) - XCTAssert(semVerRegex.matches("1.2.3-SNAPSHOT-123")) - XCTAssert(semVerRegex.matches("1.0.0")) - XCTAssert(semVerRegex.matches("2.0.0")) - XCTAssert(semVerRegex.matches("1.1.7")) - XCTAssert(semVerRegex.matches("2.0.0+build.1848")) - XCTAssert(semVerRegex.matches("2.0.1-alpha.1227")) - XCTAssert(semVerRegex.matches("1.0.0-alpha+beta")) - XCTAssert(semVerRegex.matches("1.2.3----RC-SNAPSHOT.12.9.1--.12+788")) - XCTAssert(semVerRegex.matches("1.2.3----R-S.12.9.1--.12+meta")) - XCTAssert(semVerRegex.matches("1.2.3----RC-SNAPSHOT.12.9.1--.12")) - XCTAssert(semVerRegex.matches("1.0.0+0.build.1-rc.10000aaa-kk-0.1")) - XCTAssert(semVerRegex.matches("99999999999999999999999.999999999999999999.99999999999999999")) - XCTAssert(semVerRegex.matches("1.0.0-0A.is.legal")) + XCTAssertNotNil("0.0.4".wholeMatch(of: semVerRegex)) + XCTAssertNotNil("1.2.3".wholeMatch(of: semVerRegex)) + XCTAssertNotNil("10.20.30".wholeMatch(of: semVerRegex)) + XCTAssertNotNil("1.1.2-prerelease+meta".wholeMatch(of: semVerRegex)) + XCTAssertNotNil("1.1.2+meta".wholeMatch(of: semVerRegex)) + XCTAssertNotNil("1.1.2+meta-valid".wholeMatch(of: semVerRegex)) + XCTAssertNotNil("1.0.0-alpha".wholeMatch(of: semVerRegex)) + XCTAssertNotNil("1.0.0-beta".wholeMatch(of: semVerRegex)) + XCTAssertNotNil("1.0.0-alpha.beta".wholeMatch(of: semVerRegex)) + XCTAssertNotNil("1.0.0-alpha.beta.1".wholeMatch(of: semVerRegex)) + XCTAssertNotNil("1.0.0-alpha.1".wholeMatch(of: semVerRegex)) + XCTAssertNotNil("1.0.0-alpha0.valid".wholeMatch(of: semVerRegex)) + XCTAssertNotNil("1.0.0-alpha.0valid".wholeMatch(of: semVerRegex)) + XCTAssertNotNil("1.0.0-alpha-a.b-c-somethinglong+build.1-aef.1-its-okay".wholeMatch(of: semVerRegex)) + XCTAssertNotNil("1.0.0-rc.1+build.1".wholeMatch(of: semVerRegex)) + XCTAssertNotNil("2.0.0-rc.1+build.123".wholeMatch(of: semVerRegex)) + XCTAssertNotNil("1.2.3-beta".wholeMatch(of: semVerRegex)) + XCTAssertNotNil("10.2.3-DEV-SNAPSHOT".wholeMatch(of: semVerRegex)) + XCTAssertNotNil("1.2.3-SNAPSHOT-123".wholeMatch(of: semVerRegex)) + XCTAssertNotNil("1.0.0".wholeMatch(of: semVerRegex)) + XCTAssertNotNil("2.0.0".wholeMatch(of: semVerRegex)) + XCTAssertNotNil("1.1.7".wholeMatch(of: semVerRegex)) + XCTAssertNotNil("2.0.0+build.1848".wholeMatch(of: semVerRegex)) + XCTAssertNotNil("2.0.1-alpha.1227".wholeMatch(of: semVerRegex)) + XCTAssertNotNil("1.0.0-alpha+beta".wholeMatch(of: semVerRegex)) + XCTAssertNotNil("1.2.3----RC-SNAPSHOT.12.9.1--.12+788".wholeMatch(of: semVerRegex)) + XCTAssertNotNil("1.2.3----R-S.12.9.1--.12+meta".wholeMatch(of: semVerRegex)) + XCTAssertNotNil("1.2.3----RC-SNAPSHOT.12.9.1--.12".wholeMatch(of: semVerRegex)) + XCTAssertNotNil("1.0.0+0.build.1-rc.10000aaa-kk-0.1".wholeMatch(of: semVerRegex)) + XCTAssertNotNil("99999999999999999999999.999999999999999999.99999999999999999".wholeMatch(of: semVerRegex)) + XCTAssertNotNil("1.0.0-0A.is.legal".wholeMatch(of: semVerRegex)) } func test_allow_leading_v() throws { - XCTAssert(semVerRegex.matches("v0.0.4")) + XCTAssertNotNil("v0.0.4".wholeMatch(of: semVerRegex)) } func test_semVerRegex_invalid() throws { - XCTAssertFalse(semVerRegex.matches("1")) - XCTAssertFalse(semVerRegex.matches("1.2")) - XCTAssertFalse(semVerRegex.matches("1.2.3-0123")) - XCTAssertFalse(semVerRegex.matches("1.2.3-0123.0123")) - XCTAssertFalse(semVerRegex.matches("1.1.2+.123")) - XCTAssertFalse(semVerRegex.matches("+invalid")) - XCTAssertFalse(semVerRegex.matches("-invalid")) - XCTAssertFalse(semVerRegex.matches("-invalid+invalid")) - XCTAssertFalse(semVerRegex.matches("-invalid.01")) - XCTAssertFalse(semVerRegex.matches("alpha")) - XCTAssertFalse(semVerRegex.matches("alpha.beta")) - XCTAssertFalse(semVerRegex.matches("alpha.beta.1")) - XCTAssertFalse(semVerRegex.matches("alpha.1")) - XCTAssertFalse(semVerRegex.matches("alpha+beta")) - XCTAssertFalse(semVerRegex.matches("alpha_beta")) - XCTAssertFalse(semVerRegex.matches("alpha.")) - XCTAssertFalse(semVerRegex.matches("alpha..")) - XCTAssertFalse(semVerRegex.matches("beta")) - XCTAssertFalse(semVerRegex.matches("1.0.0-alpha_beta")) - XCTAssertFalse(semVerRegex.matches("-alpha.")) - XCTAssertFalse(semVerRegex.matches("1.0.0-alpha..")) - XCTAssertFalse(semVerRegex.matches("1.0.0-alpha..1")) - XCTAssertFalse(semVerRegex.matches("1.0.0-alpha...1")) - XCTAssertFalse(semVerRegex.matches("1.0.0-alpha....1")) - XCTAssertFalse(semVerRegex.matches("1.0.0-alpha.....1")) - XCTAssertFalse(semVerRegex.matches("1.0.0-alpha......1")) - XCTAssertFalse(semVerRegex.matches("1.0.0-alpha.......1")) - XCTAssertFalse(semVerRegex.matches("01.1.1")) - XCTAssertFalse(semVerRegex.matches("1.01.1")) - XCTAssertFalse(semVerRegex.matches("1.1.01")) - XCTAssertFalse(semVerRegex.matches("1.2")) - XCTAssertFalse(semVerRegex.matches("1.2.3.DEV")) - XCTAssertFalse(semVerRegex.matches("1.2-SNAPSHOT")) - XCTAssertFalse(semVerRegex.matches("1.2.31.2.3----RC-SNAPSHOT.12.09.1--..12+788")) - XCTAssertFalse(semVerRegex.matches("1.2-RC-SNAPSHOT")) - XCTAssertFalse(semVerRegex.matches("-1.0.3-gamma+b7718")) - XCTAssertFalse(semVerRegex.matches("+justmeta")) - XCTAssertFalse(semVerRegex.matches("9.8.7+meta+meta")) - XCTAssertFalse(semVerRegex.matches("9.8.7-whatever+meta+meta")) + XCTAssertNil("1".wholeMatch(of: semVerRegex)) + XCTAssertNil("1.2".wholeMatch(of: semVerRegex)) + XCTAssertNil("1.2.3-0123".wholeMatch(of: semVerRegex)) + XCTAssertNil("1.2.3-0123.0123".wholeMatch(of: semVerRegex)) + XCTAssertNil("1.1.2+.123".wholeMatch(of: semVerRegex)) + XCTAssertNil("+invalid".wholeMatch(of: semVerRegex)) + XCTAssertNil("-invalid".wholeMatch(of: semVerRegex)) + XCTAssertNil("-invalid+invalid".wholeMatch(of: semVerRegex)) + XCTAssertNil("-invalid.01".wholeMatch(of: semVerRegex)) + XCTAssertNil("alpha".wholeMatch(of: semVerRegex)) + XCTAssertNil("alpha.beta".wholeMatch(of: semVerRegex)) + XCTAssertNil("alpha.beta.1".wholeMatch(of: semVerRegex)) + XCTAssertNil("alpha.1".wholeMatch(of: semVerRegex)) + XCTAssertNil("alpha+beta".wholeMatch(of: semVerRegex)) + XCTAssertNil("alpha_beta".wholeMatch(of: semVerRegex)) + XCTAssertNil("alpha.".wholeMatch(of: semVerRegex)) + XCTAssertNil("alpha..".wholeMatch(of: semVerRegex)) + XCTAssertNil("beta".wholeMatch(of: semVerRegex)) + XCTAssertNil("1.0.0-alpha_beta".wholeMatch(of: semVerRegex)) + XCTAssertNil("-alpha.".wholeMatch(of: semVerRegex)) + XCTAssertNil("1.0.0-alpha..".wholeMatch(of: semVerRegex)) + XCTAssertNil("1.0.0-alpha..1".wholeMatch(of: semVerRegex)) + XCTAssertNil("1.0.0-alpha...1".wholeMatch(of: semVerRegex)) + XCTAssertNil("1.0.0-alpha....1".wholeMatch(of: semVerRegex)) + XCTAssertNil("1.0.0-alpha.....1".wholeMatch(of: semVerRegex)) + XCTAssertNil("1.0.0-alpha......1".wholeMatch(of: semVerRegex)) + XCTAssertNil("1.0.0-alpha.......1".wholeMatch(of: semVerRegex)) + XCTAssertNil("01.1.1".wholeMatch(of: semVerRegex)) + XCTAssertNil("1.01.1".wholeMatch(of: semVerRegex)) + XCTAssertNil("1.1.01".wholeMatch(of: semVerRegex)) + XCTAssertNil("1.2".wholeMatch(of: semVerRegex)) + XCTAssertNil("1.2.3.DEV".wholeMatch(of: semVerRegex)) + XCTAssertNil("1.2-SNAPSHOT".wholeMatch(of: semVerRegex)) + XCTAssertNil("1.2.31.2.3----RC-SNAPSHOT.12.09.1--..12+788".wholeMatch(of: semVerRegex)) + XCTAssertNil("1.2-RC-SNAPSHOT".wholeMatch(of: semVerRegex)) + XCTAssertNil("-1.0.3-gamma+b7718".wholeMatch(of: semVerRegex)) + XCTAssertNil("+justmeta".wholeMatch(of: semVerRegex)) + XCTAssertNil("9.8.7+meta+meta".wholeMatch(of: semVerRegex)) + XCTAssertNil("9.8.7-whatever+meta+meta".wholeMatch(of: semVerRegex)) } func test_init() throws {