Skip to content

Commit

Permalink
Merge pull request #26 from RougeWare/feature/25-Enforce-increment-only
Browse files Browse the repository at this point in the history
Enforce that a semantic version (excluding extensions) only increases normally
  • Loading branch information
KyNorthstar authored Oct 13, 2021
2 parents 7c48a9c + bfc2c1e commit e8faad3
Show file tree
Hide file tree
Showing 9 changed files with 1,323 additions and 44 deletions.
2 changes: 1 addition & 1 deletion Sources/SemVer/Conveniences.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// SemVer
//
// Created by Ben Leggiero on 2018-01-14.
// Copyright © 2020 Ben Leggiero BH-1-PS.
// Copyright © 2021 Ben Leggiero BH-1-PS.
//

import Foundation
Expand Down
2 changes: 1 addition & 1 deletion Sources/SemVer/Semantic Version + Hashable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// SemVer
//
// Created by Ben Leggiero on 2020-04-19.
// Copyright © 2020 Ben Leggiero BH-1-PS.
// Copyright © 2021 Ben Leggiero BH-1-PS.
//

import Foundation
Expand Down
191 changes: 162 additions & 29 deletions Sources/SemVer/Semantic Version.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
// SemVer
//
// Created by Ben Leggiero on 2018-01-09.
// Copyright © 2020 Ben Leggiero BH-1-PS.
// Copyright © 2021 Ben Leggiero BH-1-PS.
//

import Foundation
Expand Down Expand Up @@ -41,6 +41,15 @@ public struct SemanticVersion {
public typealias Build = Extension.Build


/// Holds the raw, unmanaged value for `major`
private var _major: Major

/// Holds the raw, unmanaged value for `minor`
private var _minor: Minor

/// Holds the raw, unmanaged value for `patch`
private var _patch: Patch


/// The MAJOR version; increment this when you make incompatible API changes.
///
Expand All @@ -49,15 +58,32 @@ public struct SemanticVersion {
/// Patch and minor version MUST be reset to `0` when major version is incremented. This is done automatically if
/// you set this variable.
///
/// This MUST increase numerically. For instance: 1.9.0 ➡️ 2.0.0 ➡️ 3.0.0.
///
/// https://semver.org/spec/v2.0.0.html#spec-item-8
///
/// - Attention: Changing this immediately sets `minor` and `patch` to `0`.
/// - Attention: Increasing this immediately sets `minor` and `patch` to `0`.
/// - Note: If this is `0`, then further rules don't apply.
public var major: Major {
willSet {
minor = 0
patch = 0
set {
if newValue < _major {
assertionFailure(
"""
Major version number was decremented (went from \(_major) to \(newValue))!
In production builds, this will result in the Major version number not changing.
""")
}
else {
if newValue > _major {
_minor = 0
_patch = 0
}

_major = newValue
}
}

get { _major }
}

/// The MINOR version; increment this when you add functionality in a backwards-compatible manner.
Expand All @@ -69,22 +95,56 @@ public struct SemanticVersion {
/// Patch version MUST be reset to `0` when minor version is incremented. This is done automatically if you set
/// this variable.
///
/// This MUST increase numerically. For instance: 1.9.0 ➡️ 1.10.0 ➡️ 1.11.0.
///
/// https://semver.org/spec/v2.0.0.html#spec-item-7
///
/// - Attention: Changing this immediately sets `minor` and `patch` to `0`.
/// - Attention: Increasing this immediately sets `patch` to `0`.
public var minor: Minor {
willSet {
patch = 0
set {
if newValue < _minor {
assertionFailure(
"""
Minor version number was decremented (went from \(_minor) to \(newValue))!
In production builds, this will result in the Minor version number not changing.
""")
}
else {
if newValue > _minor {
_patch = 0
}

_minor = newValue
}
}

get { _minor }
}

/// The PATCH version; increment this when you make backwards compatible bug fixes.
///
/// This MUST be incremented if only backwards compatible bug fixes are introduced. A bug fix is defined as an
/// internal change that fixes incorrect behavior
///
/// This MUST increase numerically. For instance: 1.0.9.➡️ 1.0.10 ➡️ 1.0.11.
///
/// https://semver.org/spec/v2.0.0.html#spec-item-6
public var patch: Patch
public var patch: Patch {
set {
if newValue < _patch {
assertionFailure(
"""
Patch version number was decremented (went from \(_patch) to \(newValue))!
In production builds, this will result in the Patch version number not changing.
""")
}
else {
_patch = newValue
}
}

get { _patch }
}

/// The PRE-RELEASE extension; this identifies versions that are available before being declared stable.
///
Expand All @@ -94,7 +154,19 @@ public struct SemanticVersion {
/// Pre-release versions have a lower precedence than the associated normal version.
/// A pre-release version indicates that the version is unstable and might not satisfy the intended compatibility
/// requirements as denoted by its associated normal version.
public var preRelease: PreRelease?
public var preRelease: PreRelease? {
didSet {
if !isValid {
assertionFailure(
"""
Pre-Release extension set to an invalid value (\(preRelease ?? "nil"))!
In production builds, this will result in the Pre-Release extension being reset to its previous value.
""")

preRelease = oldValue
}
}
}

/// The BUILD metadata; this identifies some information about a particular build of a version, and is not
/// considered for equivalence nor precedence.
Expand All @@ -103,7 +175,30 @@ public struct SemanticVersion {
/// Identifiers MUST NOT be empty.
/// Build metadata MUST be ignored when determining version precedence.
/// Thus two versions that differ only in the build metadata, have the same precedence.
public var build: Build?
public var build: Build? {
didSet {
if !isValid {
assertionFailure(
"""
Build extension set to an invalid value (\(build ?? "nil"))!
In production builds, this will result in the Build extension being reset to its previous value.
""")

build = oldValue
}
}
}


/// Create a new Semantic Version without checking the validity of any of the pieces
/// - Parameter unchecked: All the semantic version values, without checking their validity
private init(unchecked: (major: Major, minor: Minor, patch: Patch, preRelease: PreRelease?, build: Build?)) {
self._major = unchecked.major
self._minor = unchecked.minor
self._patch = unchecked.patch
self.preRelease = unchecked.preRelease
self.build = unchecked.build
}


/// Create a new Semantic Version with explicit parts. This returns `nil` if the given parts would create an invalid regex, like `1.02.3`
Expand All @@ -115,17 +210,17 @@ public struct SemanticVersion {
/// - preRelease: Indicates some specific information about a pre-release build, like `RC.1`
/// - build: The build number, like `123` or `exp.sha.5114f85` or `2018.01.14.00.01`
public init?(major: Major, minor: Minor, patch: Patch, preRelease: PreRelease? = nil, build: Build? = nil) {
self.major = major
self.minor = minor
self.patch = patch
self.preRelease = preRelease
self.build = build
self.init(unchecked: (major: major,
minor: minor,
patch: patch,
preRelease: preRelease,
build: build))

guard isValid else { return nil }
}


/// Create a new Semantic Version with explicit (yet unlabelled) parts. This returns `nil` if the given parts would create an invalid regex, like `1.02.3`
/// Create a new Semantic Version with explicit (yet unlabelled) parts. This returns `nil` if the given parts would create an invalid SemVer, like `1.02.3`
///
/// - Parameters:
/// - major: The MAJOR version
Expand All @@ -136,6 +231,40 @@ public struct SemanticVersion {
public init?(_ major: Major, _ minor: Minor, _ patch: Patch, preRelease: PreRelease? = nil, build: Build? = nil) {
self.init(major: major, minor: minor, patch: patch, preRelease: preRelease, build: build)
}


/// Create a new simple Semantic Version with explicit parts and no extensions.
///
/// This always succeeds, unlike the more-freeform initializers, since it's guaranteed to be correct at compile-time. If you need to set the Pre-Release andor Build extension, you must use one of the failable initializers.
///
/// - Parameters:
/// - major: The MAJOR version
/// - minor: The MINOR version
/// - patch: The PATCH version
public init(major: UInt, minor: UInt, patch: UInt) {
self.init(unchecked: (major: major,
minor: minor,
patch: patch,
preRelease: nil,
build: nil))
}


/// Create a new simple Semantic Version with explicit (yet unlabelled) parts and no extensions.
///
/// This always succeeds, unlike the more-freeform initializers, since it's guaranteed to be correct at compile-time. If you need to set the Pre-Release andor Build extension, you must use one of the failable initializers.
///
/// - Parameters:
/// - major: The MAJOR version
/// - minor: The MINOR version
/// - patch: The PATCH version
public init(_ major: UInt, _ minor: UInt, _ patch: UInt) {
self.init(unchecked: (major: major,
minor: minor,
patch: patch,
preRelease: nil,
build: nil))
}
}


Expand Down Expand Up @@ -269,12 +398,14 @@ extension SemanticVersion: LosslessStringConvertible {
return nil
}

self.major = major
self.minor = minor
self.patch = patch

self.preRelease = matches[0].group("preRelease", in: stringValue).flatMap { PreRelease($0) }
self.build = matches[0].group("build", in: stringValue).flatMap { Build($0) }
self.init(unchecked: (
major: major,
minor: minor,
patch: patch,

preRelease: matches[0].group("preRelease", in: stringValue).flatMap { PreRelease($0) },
build: matches[0].group("build", in: stringValue).flatMap { Build($0) })
)
}
else { // FIXME: Remove ASAP
guard
Expand All @@ -287,12 +418,14 @@ extension SemanticVersion: LosslessStringConvertible {
return nil
}

self.major = major
self.minor = minor
self.patch = patch

self.preRelease = matches[0].group(4, in: stringValue).flatMap { PreRelease($0) }
self.build = matches[0].group(6, in: stringValue).flatMap { Build($0) }
self.init(unchecked: (
major: major,
minor: minor,
patch: patch,

preRelease: matches[0].group(4, in: stringValue).flatMap { PreRelease($0) },
build: matches[0].group(6, in: stringValue).flatMap { Build($0) })
)
}
}

Expand Down
29 changes: 29 additions & 0 deletions Sources/SemVer/Testing tools.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
//
// Testing tools.swift
// SemVer
//
// Created by Ky Leggiero on 2021-10-12.
// Copyright © 2021 Ben Leggiero BH-1-PS.
//

import Foundation



/// A bodge to change behavior based on whether a test suite is running.
///
/// Ideally, this would be a compile-time flag. However, it seems that isn't being respected with my current setup, so I don't trust it.
internal var isTesting = false



/// In non-test runs, this indicates that an internal sanity check failed.
///
/// To perform an assertion in test runs as well, use `Swift.assertionFailure` instead.
///
/// - Parameter message: _optional_ - A string to print in a playground or `-Onone` non-test build. Defaults to an empty string.
@inline(__always)
internal func assertionFailure(_ message: @autoclosure () -> String = "", file: StaticString = #file, line: UInt = #line) {
guard !isTesting else { return }
Swift.assertionFailure(message(), file: file, line: line)
}
8 changes: 5 additions & 3 deletions Tests/LinuxMain.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import XCTest

import SemVerTests

var tests = [XCTestCaseEntry]()
tests += SemVerTests.allTests()
XCTMain(tests)
isTesting = true

XCTMain(SemVerTests.allTests
+ SemVerHashableTests.allTests
+ SemVerMutationTests.allTests)
Loading

0 comments on commit e8faad3

Please sign in to comment.