From befaa33014d37acbe35bfd7bd6aeff42bbbd4433 Mon Sep 17 00:00:00 2001 From: Mattt Date: Tue, 10 Aug 2021 10:59:14 -0700 Subject: [PATCH 1/2] Add nested PackgeIdentity.Scope and PackageIdentity.Name types --- Sources/PackageModel/PackageIdentity.swift | 159 ++++++++++++++++++ .../PackageIdentityNameTests.swift | 86 ++++++++++ .../PackageIdentityScopeTests.swift | 65 +++++++ 3 files changed, 310 insertions(+) create mode 100644 Tests/PackageModelTests/PackageIdentityNameTests.swift create mode 100644 Tests/PackageModelTests/PackageIdentityScopeTests.swift diff --git a/Sources/PackageModel/PackageIdentity.swift b/Sources/PackageModel/PackageIdentity.swift index 0c4dd2e026b..03ddb50ab33 100644 --- a/Sources/PackageModel/PackageIdentity.swift +++ b/Sources/PackageModel/PackageIdentity.swift @@ -94,6 +94,165 @@ extension PackageIdentity: JSONMappable, JSONSerializable { // MARK: - +extension PackageIdentity { + /// Provides a namespace for related packages within a package registry. + public struct Scope: LosslessStringConvertible, Hashable, Equatable, Comparable, ExpressibleByStringLiteral { + public let description: String + + public init(validating description: String) throws { + guard !description.isEmpty else { + throw StringError("The minimum length of a package scope is 1 character.") + } + + guard description.count <= 39 else { + throw StringError("The maximum length of a package scope is 39 characters.") + } + + for (index, character) in zip(description.indices, description) { + guard character.isASCII, + character.isLetter || + character.isNumber || + character == "-" + else { + throw StringError("A package scope consists of alphanumeric characters and hyphens.") + } + + if character.isPunctuation { + switch (index, description.index(after: index)) { + case (description.startIndex, _): + throw StringError("Hyphens may not occur at the beginning of a scope.") + case (_, description.endIndex): + throw StringError("Hyphens may not occur at the end of a scope.") + case (_, let nextIndex) where description[nextIndex].isPunctuation: + throw StringError("Hyphens may not occur consecutively within a scope.") + default: + continue + } + } + } + + self.description = description + } + + public init?(_ description: String) { + guard let scope = try? Scope(validating: description) else { return nil } + self = scope + } + + // MARK: - Equatable & Comparable + + private func compare(to other: Scope) -> ComparisonResult { + // Package scopes are case-insensitive (for example, `mona` ≍ `MONA`). + return self.description.caseInsensitiveCompare(other.description) + } + + public static func == (lhs: Scope, rhs: Scope) -> Bool { + return lhs.compare(to: rhs) == .orderedSame + } + + public static func < (lhs: Scope, rhs: Scope) -> Bool { + return lhs.compare(to: rhs) == .orderedAscending + } + + public static func > (lhs: Scope, rhs: Scope) -> Bool { + return lhs.compare(to: rhs) == .orderedDescending + } + + // MARK: - Hashable + + public func hash(into hasher: inout Hasher) { + hasher.combine(description.lowercased()) + } + + // MARK: - ExpressibleByStringLiteral + + public init(stringLiteral value: StringLiteralType) { + try! self.init(validating: value) + } + } + + /// Uniquely identifies a package in a scope + public struct Name: LosslessStringConvertible, Hashable, Equatable, Comparable, ExpressibleByStringLiteral { + public let description: String + + public init(validating description: String) throws { + guard !description.isEmpty else { + throw StringError("The minimum length of a package name is 1 character.") + } + + guard description.count <= 100 else { + throw StringError("The maximum length of a package name is 100 characters.") + } + + for (index, character) in zip(description.indices, description) { + guard character.isASCII, + character.isLetter || + character.isNumber || + character == "-" || + character == "_" + else { + throw StringError("A package name consists of alphanumeric characters, underscores, and hyphens.") + } + + if character.isPunctuation { + if character.isPunctuation { + switch (index, description.index(after: index)) { + case (description.startIndex, _): + throw StringError("Hyphens and underscores may not occur at the beginning of a name.") + case (_, description.endIndex): + throw StringError("Hyphens and underscores may not occur at the end of a name.") + case (_, let nextIndex) where description[nextIndex].isPunctuation: + throw StringError("Hyphens and underscores may not occur consecutively within a name.") + default: + continue + } + } + } + } + + self.description = description + } + + public init?(_ description: String) { + guard let name = try? Name(validating: description) else { return nil } + self = name + } + + // MARK: - Equatable & Comparable + + private func compare(to other: Name) -> ComparisonResult { + // Package scopes are case-insensitive (for example, `LinkedList` ≍ `LINKEDLIST`). + return self.description.caseInsensitiveCompare(other.description) + } + + public static func == (lhs: Name, rhs: Name) -> Bool { + return lhs.compare(to: rhs) == .orderedSame + } + + public static func < (lhs: Name, rhs: Name) -> Bool { + return lhs.compare(to: rhs) == .orderedAscending + } + + public static func > (lhs: Name, rhs: Name) -> Bool { + return lhs.compare(to: rhs) == .orderedDescending + } + + // MARK: - Hashable + + public func hash(into hasher: inout Hasher) { + hasher.combine(description.lowercased()) + } + + // MARK: - ExpressibleByStringLiteral + + public init(stringLiteral value: StringLiteralType) { + try! self.init(validating: value) + } + } +} + +// MARK: - + struct LegacyPackageIdentity: PackageIdentityProvider, Equatable { /// A textual representation of this instance. public let description: String diff --git a/Tests/PackageModelTests/PackageIdentityNameTests.swift b/Tests/PackageModelTests/PackageIdentityNameTests.swift new file mode 100644 index 00000000000..71ac640d573 --- /dev/null +++ b/Tests/PackageModelTests/PackageIdentityNameTests.swift @@ -0,0 +1,86 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2021 Apple Inc. and the Swift project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See http://swift.org/LICENSE.txt for license information + See http://swift.org/CONTRIBUTORS.txt for Swift project authors +*/ + +import XCTest +import Basics +import TSCBasic +import PackageModel + +class PackageIdentityNameTests: XCTestCase { + func testValidNames() throws { + XCTAssertNoThrow(try PackageIdentity.Name(validating: "LinkedList")) + XCTAssertNoThrow(try PackageIdentity.Name(validating: "Linked-List")) + XCTAssertNoThrow(try PackageIdentity.Name(validating: "Linked_List")) + XCTAssertNoThrow(try PackageIdentity.Name(validating: "A")) + XCTAssertNoThrow(try PackageIdentity.Name(validating: "1")) + XCTAssertNoThrow(try PackageIdentity.Name(validating: String(repeating: "A", count: 100))) + } + + func testInvalidNames() throws { + XCTAssertThrowsError(try PackageIdentity.Name(validating: "")) { error in + XCTAssertEqual(error.localizedDescription, "The minimum length of a package name is 1 character.") + } + + XCTAssertThrowsError(try PackageIdentity.Name(validating: String(repeating: "a", count: 101))) { error in + XCTAssertEqual(error.localizedDescription, "The maximum length of a package name is 100 characters.") + } + + XCTAssertThrowsError(try PackageIdentity.Name(validating: "!")) { error in + XCTAssertEqual(error.localizedDescription, "A package name consists of alphanumeric characters, underscores, and hyphens.") + } + + XCTAssertThrowsError(try PackageIdentity.Name(validating: "あ")) { error in + XCTAssertEqual(error.localizedDescription, "A package name consists of alphanumeric characters, underscores, and hyphens.") + } + + XCTAssertThrowsError(try PackageIdentity.Name(validating: "🧍")) { error in + XCTAssertEqual(error.localizedDescription, "A package name consists of alphanumeric characters, underscores, and hyphens.") + } + + XCTAssertThrowsError(try PackageIdentity.Name(validating: "-a")) { error in + XCTAssertEqual(error.localizedDescription, "Hyphens and underscores may not occur at the beginning of a name.") + } + + XCTAssertThrowsError(try PackageIdentity.Name(validating: "_a")) { error in + XCTAssertEqual(error.localizedDescription, "Hyphens and underscores may not occur at the beginning of a name.") + } + + XCTAssertThrowsError(try PackageIdentity.Name(validating: "a-")) { error in + XCTAssertEqual(error.localizedDescription, "Hyphens and underscores may not occur at the end of a name.") + } + + XCTAssertThrowsError(try PackageIdentity.Name(validating: "a_")) { error in + XCTAssertEqual(error.localizedDescription, "Hyphens and underscores may not occur at the end of a name.") + } + + XCTAssertThrowsError(try PackageIdentity.Name(validating: "a_-a")) { error in + XCTAssertEqual(error.localizedDescription, "Hyphens and underscores may not occur consecutively within a name.") + } + + XCTAssertThrowsError(try PackageIdentity.Name(validating: "a-_a")) { error in + XCTAssertEqual(error.localizedDescription, "Hyphens and underscores may not occur consecutively within a name.") + } + + XCTAssertThrowsError(try PackageIdentity.Name(validating: "a--a")) { error in + XCTAssertEqual(error.localizedDescription, "Hyphens and underscores may not occur consecutively within a name.") + } + + XCTAssertThrowsError(try PackageIdentity.Name(validating: "a__a")) { error in + XCTAssertEqual(error.localizedDescription, "Hyphens and underscores may not occur consecutively within a name.") + } + } + + func testNamesAreCaseInsensitive() throws { + let lowercase: PackageIdentity.Name = "linkedlist" + let uppercase: PackageIdentity.Name = "LINKEDLIST" + + XCTAssertEqual(lowercase, uppercase) + } +} diff --git a/Tests/PackageModelTests/PackageIdentityScopeTests.swift b/Tests/PackageModelTests/PackageIdentityScopeTests.swift new file mode 100644 index 00000000000..c3ad3513cce --- /dev/null +++ b/Tests/PackageModelTests/PackageIdentityScopeTests.swift @@ -0,0 +1,65 @@ +/* + This source file is part of the Swift.org open source project + + Copyright (c) 2021 Apple Inc. and the Swift project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See http://swift.org/LICENSE.txt for license information + See http://swift.org/CONTRIBUTORS.txt for Swift project authors +*/ + +import XCTest +import Basics +import TSCBasic +import PackageModel + +class PackageIdentityScopeTests: XCTestCase { + func testValidScopes() throws { + XCTAssertNoThrow(try PackageIdentity.Scope(validating: "mona")) + XCTAssertNoThrow(try PackageIdentity.Scope(validating: "m-o-n-a")) + XCTAssertNoThrow(try PackageIdentity.Scope(validating: "a")) + XCTAssertNoThrow(try PackageIdentity.Scope(validating: "1")) + XCTAssertNoThrow(try PackageIdentity.Scope(validating: String(repeating: "a", count: 39))) + } + + func testInvalidScopes() throws { + XCTAssertThrowsError(try PackageIdentity.Scope(validating: "")) { error in + XCTAssertEqual(error.localizedDescription, "The minimum length of a package scope is 1 character.") + } + + XCTAssertThrowsError(try PackageIdentity.Scope(validating: String(repeating: "a", count: 100))) { error in + XCTAssertEqual(error.localizedDescription, "The maximum length of a package scope is 39 characters.") + } + + XCTAssertThrowsError(try PackageIdentity.Scope(validating: "!")) { error in + XCTAssertEqual(error.localizedDescription, "A package scope consists of alphanumeric characters and hyphens.") + } + + XCTAssertThrowsError(try PackageIdentity.Scope(validating: "あ")) { error in + XCTAssertEqual(error.localizedDescription, "A package scope consists of alphanumeric characters and hyphens.") + } + + XCTAssertThrowsError(try PackageIdentity.Scope(validating: "🧍")) { error in + XCTAssertEqual(error.localizedDescription, "A package scope consists of alphanumeric characters and hyphens.") + } + + XCTAssertThrowsError(try PackageIdentity.Scope(validating: "-a")) { error in + XCTAssertEqual(error.localizedDescription, "Hyphens may not occur at the beginning of a scope.") + } + + XCTAssertThrowsError(try PackageIdentity.Scope(validating: "a-")) { error in + XCTAssertEqual(error.localizedDescription, "Hyphens may not occur at the end of a scope.") + } + + XCTAssertThrowsError(try PackageIdentity.Scope(validating: "a--a")) { error in + XCTAssertEqual(error.localizedDescription, "Hyphens may not occur consecutively within a scope.") + } + } + + func testScopesAreCaseInsensitive() throws { + let lowercase: PackageIdentity.Scope = "mona" + let uppercase: PackageIdentity.Scope = "MONA" + + XCTAssertEqual(lowercase, uppercase) + } +} From 7d242f035c4b2930d0b0300d8d5f4b4247e3bf5a Mon Sep 17 00:00:00 2001 From: Mattt Date: Wed, 11 Aug 2021 10:56:02 -0700 Subject: [PATCH 2/2] Remove redundant conditional --- Sources/PackageModel/PackageIdentity.swift | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/Sources/PackageModel/PackageIdentity.swift b/Sources/PackageModel/PackageIdentity.swift index 03ddb50ab33..53e6779b97e 100644 --- a/Sources/PackageModel/PackageIdentity.swift +++ b/Sources/PackageModel/PackageIdentity.swift @@ -195,17 +195,15 @@ extension PackageIdentity { } if character.isPunctuation { - if character.isPunctuation { - switch (index, description.index(after: index)) { - case (description.startIndex, _): - throw StringError("Hyphens and underscores may not occur at the beginning of a name.") - case (_, description.endIndex): - throw StringError("Hyphens and underscores may not occur at the end of a name.") - case (_, let nextIndex) where description[nextIndex].isPunctuation: - throw StringError("Hyphens and underscores may not occur consecutively within a name.") - default: - continue - } + switch (index, description.index(after: index)) { + case (description.startIndex, _): + throw StringError("Hyphens and underscores may not occur at the beginning of a name.") + case (_, description.endIndex): + throw StringError("Hyphens and underscores may not occur at the end of a name.") + case (_, let nextIndex) where description[nextIndex].isPunctuation: + throw StringError("Hyphens and underscores may not occur consecutively within a name.") + default: + continue } } }