Created
June 4, 2020 09:26
-
-
Save rjstelling/dfa3e6ac491806a63e359259a9a06dc3 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// SemanticVersioning.swift | |
// | |
// | |
// Created by Richard Stelling on 28/05/2020. | |
// | |
// The MIT License (MIT) | |
// | |
// Copyright (c) 2016-17 Richard Stelling (http://twitter.com/rjstelling) | |
// | |
// SemanticVersioning.swift: https://richardstelling.com/Swift-Gists/ | |
// | |
// Permission is hereby granted, free of charge, to any person obtaining a copy | |
// of this software and associated documentation files (the "Software"), to deal | |
// in the Software without restriction, including without limitation the rights | |
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
// copies of the Software, and to permit persons to whom the Software is | |
// furnished to do so, subject to the following conditions: | |
// | |
// The above copyright notice and this permission notice shall be included in all | |
// copies or substantial portions of the Software. | |
// | |
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
// FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT. IN NO EVENT SHALL THE | |
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
// SOFTWARE. | |
import Foundation | |
struct Version { | |
enum Error: Swift.Error { | |
case parse(String) // Hint | |
} | |
// MAJOR version when you make incompatible API changes, | |
// MINOR version when you add functionality in a backwards | |
// compatible manner, and | |
// PATCH version when you make backwards compatible bug fixes. | |
let major: UInt64, minor: UInt64, patch: UInt64 | |
// The string representation of the the version portion | |
// of the Semantic Versioning data | |
var versionString: String { [major, minor, patch].map(String.init).joined(separator: ".") } | |
// Additional labels for pre-release and build metadata | |
// are available as extensions to the MAJOR.MINOR.PATCH | |
// format. | |
var labelString: String? { identifiers?.joined(separator: ".") } | |
// Identifiers MUST comprise only ASCII alphanumerics and | |
// hyphen [0-9A-Za-z-]. Identifiers MUST NOT be empty. | |
// Numeric identifiers MUST NOT include leading zeroes | |
let identifiers: [String]? | |
init(_ versionString: String) throws { | |
// Split string at the firts hyphen, lhs will be the MAJOR.MINOR.PATCH | |
// rhs will be the label, comprised of any number of identifiers. | |
let versionLabel = versionString.split(separator: "-", maxSplits: 1, omittingEmptySubsequences: true) | |
guard let version = versionLabel.first else { throw Error.parse("Empty version string in: \(versionString)") } | |
// If we have a lable extract it | |
if versionLabel.indices.contains(1), let labelComponent = versionLabel.last { | |
// A pre-release version MAY be denoted by appending a hyphen and | |
// a series of dot separated identifiers immediately following | |
// the patch version. Identifiers MUST comprise only ASCII | |
// alphanumerics and hyphen [0-9A-Za-z-]. Identifiers MUST NOT be | |
// empty. Numeric identifiers MUST NOT include leading zeroes. | |
// 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. Examples: 1.0.0-alpha, 1.0.0-alpha.1, 1.0.0-0.3.7, | |
// 1.0.0-x.7.z.92. | |
self.identifiers = labelComponent.split(separator: ".").map(String.init) | |
} | |
else { | |
self.identifiers = nil | |
} | |
// Continue with version extraction... | |
// A normal version number MUST take the form X.Y.Z where X, Y, | |
// and Z are non-negative integers, and MUST NOT contain leading | |
// zeroes. X is the major version, Y is the minor version, and Z | |
// is the patch version. Each element MUST increase numerically. | |
// For instance: 1.9.0 -> 1.10.0 -> 1.11.0. | |
let versionElements: [UInt64] = try version.split(separator: ".").map(String.init).map { | |
guard let ele = UInt64($0) else { throw Error.parse("Version element is invalid: \($0)") } | |
return ele | |
} | |
guard 1...3 ~= versionElements.count else { throw Error.parse("Version elements are the wrong size, \(versionElements.count), it shoudl be >=1, <=3") } | |
self.major = versionElements[0] // this is guaranteed becase we check above | |
// Default minor value is zero | |
self.minor = versionElements.indices.contains(1) ? versionElements[1] : 0 | |
// Default patch value is zero | |
self.patch = versionElements.indices.contains(2) ? versionElements[2] : 0 | |
} | |
func isCompatible(_ version: Version) -> Bool { | |
self.major == version.major | |
} | |
} | |
extension Version: CustomStringConvertible { | |
var description: String { [versionString, labelString].compactMap { $0 }.joined(separator: "-") } | |
} | |
extension Version: ExpressibleByStringLiteral { | |
typealias StringLiteralType = String | |
init(stringLiteral value: Self.StringLiteralType) { | |
// We use asserts here because be can't propergate an error | |
// at runtime or validate a string at compile time. | |
do { | |
try self.init(value) | |
} | |
catch { | |
fatalError("String Literal failed to parse: \(value)") | |
} | |
} | |
} | |
extension Version: Comparable { | |
static func == (lhs: Version, rhs: Version) -> Bool { | |
lhs.major == rhs.major && | |
lhs.minor == rhs.minor && | |
lhs.patch == rhs.patch && | |
lhs.identifiers?.isEmpty == rhs.identifiers?.isEmpty | |
} | |
static func < (lhs: Version, rhs: Version) -> Bool { | |
guard lhs != rhs else { return false } | |
if lhs.major < rhs.major { return true } | |
else if lhs.major == rhs.major, lhs.minor < rhs.minor { return true } | |
else if lhs.major == rhs.major, lhs.minor == rhs.minor, lhs.patch < rhs.patch { return true } | |
else if lhs.major == rhs.major, lhs.minor == rhs.minor, lhs.patch == rhs.patch, | |
lhs.identifiers != nil, rhs.identifiers == nil { return true } | |
else { return false } | |
} | |
static func <= (lhs: Version, rhs: Version) -> Bool { | |
lhs == rhs || lhs < rhs | |
} | |
static func > (lhs: Version, rhs: Version) -> Bool { | |
guard lhs != rhs else { return false } | |
if lhs.major > rhs.major { return true } | |
else if lhs.major == rhs.major, lhs.minor > rhs.minor { return true } | |
else if lhs.major == rhs.major, lhs.minor == rhs.minor, lhs.patch > rhs.patch { return true } | |
else if lhs.major == rhs.major, lhs.minor == rhs.minor, lhs.patch == rhs.patch, | |
lhs.identifiers == nil, rhs.identifiers != nil { return true } | |
else { return false } | |
} | |
static func >= (lhs: Version, rhs: Version) -> Bool { | |
lhs == rhs || lhs > rhs | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment