├── .codecov.yml ├── .github ├── CODEOWNERS ├── dependabot.yml └── workflows │ ├── docs.yml │ ├── enable-auto-merge.yml │ └── swift-test.yml ├── .gitignore ├── LICENSE ├── Package.resolved ├── Package.swift ├── Package@swift-5.10.swift ├── Package@swift-6.0.swift ├── README.md ├── Sources ├── SemVer │ ├── Version+Adjustments.swift │ ├── Version+Codable.swift │ ├── Version+FormattingOptions.swift │ ├── Version+PrereleaseIdentifier.swift │ └── Version.swift ├── SemVerMacros │ └── VersionMacro.swift ├── SemVerMacrosPlugin │ ├── SemVerMacrosCompilerPlugin.swift │ └── VersionMacro.swift └── SemVerParsing │ └── VersionParser.swift └── Tests ├── SemVerMacrosPluginTests ├── SemVerMacrosCompilerPluginTests.swift └── VersionMacroTests.swift ├── SemVerParsingTests └── VersionParserTests.swift └── SemVerTests ├── GitHubIssueTests.swift ├── VersionTests.swift ├── Version_AdjustmentTests.swift ├── Version_CodableTests.swift └── Version_PrereleaseIdentifierTests.swift /.codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | range: 70..95 3 | round: nearest 4 | precision: 2 5 | 6 | ignore: 7 | - ".build" 8 | - ".swiftpm" 9 | - "Tests" 10 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Global 2 | * @ffried 3 | 4 | # Workflow & Deployment related files 5 | .github/* @ffried 6 | .codecov.yml @ffried 7 | 8 | # Project & Source files 9 | *.swift @ffried 10 | /Package*.swift @ffried 11 | /Sources/* @ffried 12 | /Tests/* @ffried 13 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | updates: 4 | - package-ecosystem: github-actions 5 | directory: / 6 | open-pull-requests-limit: 10 7 | schedule: 8 | interval: daily 9 | time: '07:00' 10 | timezone: Europe/Berlin 11 | 12 | - package-ecosystem: swift 13 | directory: / 14 | open-pull-requests-limit: 10 15 | schedule: 16 | interval: daily 17 | time: '07:00' 18 | timezone: Europe/Berlin 19 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Publish Documentation 2 | 3 | on: 4 | release: 5 | types: 6 | - published 7 | - edited 8 | push: 9 | branches: [ main ] 10 | 11 | permissions: 12 | contents: write 13 | 14 | concurrency: 15 | group: ${{ github.ref }} 16 | cancel-in-progress: true 17 | 18 | jobs: 19 | generate-and-publish-docs: 20 | uses: sersoft-gmbh/oss-common-actions/.github/workflows/swift-generate-and-publish-docs.yml@main 21 | with: 22 | os: ubuntu 23 | swift-version: '6.1' 24 | organisation: ${{ github.repository_owner }} 25 | repository: ${{ github.event.repository.name }} 26 | pages-branch: gh-pages 27 | -------------------------------------------------------------------------------- /.github/workflows/enable-auto-merge.yml: -------------------------------------------------------------------------------- 1 | name: Auto-merge for Dependabot PRs 2 | 3 | on: 4 | pull_request: 5 | branches: [ main ] 6 | 7 | permissions: 8 | contents: write 9 | pull-requests: write 10 | 11 | jobs: 12 | enable-auto-merge: 13 | runs-on: ubuntu-latest 14 | if: ${{ github.actor == 'dependabot[bot]' }} 15 | steps: 16 | - name: Enable auto-merge 17 | run: gh pr merge --auto --rebase "${PR_URL}" 18 | env: 19 | PR_URL: ${{ github.event.pull_request.html_url }} 20 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 21 | -------------------------------------------------------------------------------- /.github/workflows/swift-test.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | variables: 14 | outputs: 15 | max-supported-swift-version: '6.1' 16 | xcode-scheme: semver-Package 17 | xcode-platform-version: latest 18 | fail-if-codecov-fails: 'true' 19 | runs-on: ubuntu-latest 20 | steps: 21 | - run: exit 0 22 | 23 | test-spm: 24 | needs: variables 25 | strategy: 26 | matrix: 27 | os: [ macOS, ubuntu ] 28 | swift-version-offset: [ 0, 1, 2 ] 29 | uses: sersoft-gmbh/oss-common-actions/.github/workflows/swift-test-spm.yml@main 30 | with: 31 | os: ${{ matrix.os }} 32 | max-swift-version: ${{ needs.variables.outputs.max-supported-swift-version }} 33 | swift-version-offset: ${{ matrix.swift-version-offset }} 34 | fail-if-codecov-fails: ${{ fromJson(needs.variables.outputs.fail-if-codecov-fails) }} 35 | secrets: 36 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 37 | 38 | test-xcode: 39 | needs: variables 40 | strategy: 41 | matrix: 42 | platform: 43 | - macOS 44 | - iOS 45 | - iPadOS 46 | - tvOS 47 | - watchOS 48 | - visionOS 49 | swift-version-offset: [ 0, 1, 2 ] 50 | uses: sersoft-gmbh/oss-common-actions/.github/workflows/swift-test-xcode.yml@main 51 | with: 52 | xcode-scheme: ${{ needs.variables.outputs.xcode-scheme }} 53 | max-swift-version: ${{ needs.variables.outputs.max-supported-swift-version }} 54 | swift-version-offset: ${{ matrix.swift-version-offset }} 55 | platform: ${{ matrix.platform }} 56 | platform-version: ${{ needs.variables.outputs.xcode-platform-version }} 57 | fail-if-codecov-fails: ${{ fromJson(needs.variables.outputs.fail-if-codecov-fails) }} 58 | secrets: 59 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | /.build 4 | /Packages 5 | xcuserdata/ 6 | DerivedData/ 7 | .netrc 8 | 9 | # We ignore the complete .swiftpm directory for now, since Xcode shows schemes from dependencies. 10 | # See discussion in https://github.com/sersoft-gmbh/xmlwrangler#122 for more details. 11 | .swiftpm/ 12 | 13 | # VS Code 14 | .vscode/* 15 | !.vscode/settings.json 16 | !.vscode/tasks.json 17 | !.vscode/launch.json 18 | !.vscode/extensions.json 19 | !.vscode/*.code-snippets 20 | 21 | # Local History for Visual Studio Code 22 | .history/ 23 | 24 | # Built Visual Studio Code Extensions 25 | *.vsix 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "originHash" : "1e77a86d9a7fbd4b644824ad6bdbd5c07faa4c8825367078c8152f5208faccd5", 3 | "pins" : [ 4 | { 5 | "identity" : "swift-docc-plugin", 6 | "kind" : "remoteSourceControl", 7 | "location" : "https://github.com/swiftlang/swift-docc-plugin", 8 | "state" : { 9 | "revision" : "85e4bb4e1cd62cec64a4b8e769dcefdf0c5b9d64", 10 | "version" : "1.4.3" 11 | } 12 | }, 13 | { 14 | "identity" : "swift-docc-symbolkit", 15 | "kind" : "remoteSourceControl", 16 | "location" : "https://github.com/swiftlang/swift-docc-symbolkit", 17 | "state" : { 18 | "revision" : "b45d1f2ed151d057b54504d653e0da5552844e34", 19 | "version" : "1.0.0" 20 | } 21 | }, 22 | { 23 | "identity" : "swift-syntax", 24 | "kind" : "remoteSourceControl", 25 | "location" : "https://github.com/swiftlang/swift-syntax", 26 | "state" : { 27 | "revision" : "f99ae8aa18f0cf0d53481901f88a0991dc3bd4a2", 28 | "version" : "601.0.1" 29 | } 30 | } 31 | ], 32 | "version" : 3 33 | } 34 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:6.1 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | import CompilerPluginSupport 6 | 7 | let swiftSettings: Array = [ 8 | .swiftLanguageMode(.v6), 9 | .enableUpcomingFeature("ExistentialAny"), 10 | .enableUpcomingFeature("InternalImportsByDefault"), 11 | ] 12 | 13 | let package = Package( 14 | name: "semver", 15 | platforms: [ 16 | .macOS(.v10_15), 17 | .iOS(.v13), 18 | .tvOS(.v13), 19 | .watchOS(.v6), 20 | .macCatalyst(.v13), 21 | .visionOS(.v1), 22 | ], 23 | products: [ 24 | .library( 25 | name: "SemVer", 26 | targets: ["SemVer"]), 27 | .library( 28 | name: "SemVerMacros", 29 | targets: ["SemVerMacros"]), 30 | ], 31 | dependencies: [ 32 | .package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.0.0"), 33 | .package(url: "https://github.com/swiftlang/swift-syntax", from: "601.0.0"), 34 | ], 35 | targets: [ 36 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 37 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 38 | .target( 39 | name: "SemVerParsing", 40 | swiftSettings: swiftSettings), 41 | .macro( 42 | name: "SemVerMacrosPlugin", 43 | dependencies: [ 44 | .product(name: "SwiftSyntax", package: "swift-syntax"), 45 | .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), 46 | .product(name: "SwiftDiagnostics", package: "swift-syntax"), 47 | .product(name: "SwiftCompilerPlugin", package: "swift-syntax"), 48 | "SemVerParsing", 49 | ], 50 | swiftSettings: swiftSettings), 51 | .target( 52 | name: "SemVer", 53 | dependencies: ["SemVerParsing"], 54 | swiftSettings: swiftSettings), 55 | .target( 56 | name: "SemVerMacros", 57 | dependencies: [ 58 | "SemVer", 59 | "SemVerMacrosPlugin", 60 | ], 61 | swiftSettings: swiftSettings), 62 | .testTarget( 63 | name: "SemVerParsingTests", 64 | dependencies: ["SemVerParsing"], 65 | swiftSettings: swiftSettings), 66 | .testTarget( 67 | name: "SemVerTests", 68 | dependencies: [ 69 | "SemVerParsing", 70 | "SemVer", 71 | "SemVerMacros", 72 | ], 73 | swiftSettings: swiftSettings), 74 | .testTarget( 75 | name: "SemVerMacrosPluginTests", 76 | dependencies: [ 77 | .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), 78 | .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"), 79 | "SemVerParsing", // Xcode fails otherwise... 80 | "SemVerMacrosPlugin", 81 | ], 82 | swiftSettings: swiftSettings), 83 | ] 84 | ) 85 | -------------------------------------------------------------------------------- /Package@swift-5.10.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:5.10 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | import CompilerPluginSupport 6 | 7 | let swiftSettings: Array = [ 8 | .enableUpcomingFeature("ConciseMagicFile"), 9 | .enableUpcomingFeature("ExistentialAny"), 10 | .enableUpcomingFeature("BareSlashRegexLiterals"), 11 | .enableUpcomingFeature("DisableOutwardActorInference"), 12 | .enableUpcomingFeature("IsolatedDefaultValues"), 13 | .enableUpcomingFeature("DeprecateApplicationMain"), 14 | .enableExperimentalFeature("AccessLevelOnImport"), 15 | .enableExperimentalFeature("StrictConcurrency"), 16 | .enableExperimentalFeature("GlobalConcurrency"), 17 | ] 18 | 19 | let package = Package( 20 | name: "semver", 21 | platforms: [ 22 | .macOS(.v10_15), 23 | .iOS(.v13), 24 | .tvOS(.v13), 25 | .watchOS(.v6), 26 | .macCatalyst(.v13), 27 | .visionOS(.v1), 28 | ], 29 | products: [ 30 | .library( 31 | name: "SemVer", 32 | targets: ["SemVer"]), 33 | .library( 34 | name: "SemVerMacros", 35 | targets: ["SemVerMacros"]), 36 | ], 37 | dependencies: [ 38 | .package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.0.0"), 39 | .package(url: "https://github.com/swiftlang/swift-syntax", from: "510.0.1"), 40 | ], 41 | targets: [ 42 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 43 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 44 | .target( 45 | name: "SemVerParsing", 46 | swiftSettings: swiftSettings), 47 | .macro( 48 | name: "SemVerMacrosPlugin", 49 | dependencies: [ 50 | .product(name: "SwiftSyntax", package: "swift-syntax"), 51 | .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), 52 | .product(name: "SwiftDiagnostics", package: "swift-syntax"), 53 | .product(name: "SwiftCompilerPlugin", package: "swift-syntax"), 54 | "SemVerParsing", 55 | ], 56 | swiftSettings: swiftSettings), 57 | .target( 58 | name: "SemVer", 59 | dependencies: ["SemVerParsing"], 60 | swiftSettings: swiftSettings), 61 | .target( 62 | name: "SemVerMacros", 63 | dependencies: [ 64 | "SemVer", 65 | "SemVerMacrosPlugin", 66 | ], 67 | swiftSettings: swiftSettings), 68 | .testTarget( 69 | name: "SemVerParsingTests", 70 | dependencies: ["SemVerParsing"], 71 | swiftSettings: swiftSettings), 72 | .testTarget( 73 | name: "SemVerTests", 74 | dependencies: [ 75 | "SemVerParsing", 76 | "SemVer", 77 | "SemVerMacros", 78 | ], 79 | swiftSettings: swiftSettings), 80 | .testTarget( 81 | name: "SemVerMacrosPluginTests", 82 | dependencies: [ 83 | .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), 84 | .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"), 85 | "SemVerParsing", // Xcode fails otherwise... 86 | "SemVerMacrosPlugin", 87 | ], 88 | swiftSettings: swiftSettings), 89 | ] 90 | ) 91 | -------------------------------------------------------------------------------- /Package@swift-6.0.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:6.0 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | import CompilerPluginSupport 6 | 7 | let swiftSettings: Array = [ 8 | .swiftLanguageMode(.v6), 9 | .enableUpcomingFeature("ExistentialAny"), 10 | .enableUpcomingFeature("InternalImportsByDefault"), 11 | ] 12 | 13 | let package = Package( 14 | name: "semver", 15 | platforms: [ 16 | .macOS(.v10_15), 17 | .iOS(.v13), 18 | .tvOS(.v13), 19 | .watchOS(.v6), 20 | .macCatalyst(.v13), 21 | .visionOS(.v1), 22 | ], 23 | products: [ 24 | .library( 25 | name: "SemVer", 26 | targets: ["SemVer"]), 27 | .library( 28 | name: "SemVerMacros", 29 | targets: ["SemVerMacros"]), 30 | ], 31 | dependencies: [ 32 | .package(url: "https://github.com/swiftlang/swift-docc-plugin", from: "1.0.0"), 33 | .package(url: "https://github.com/swiftlang/swift-syntax", from: "600.0.0"), 34 | ], 35 | targets: [ 36 | // Targets are the basic building blocks of a package. A target can define a module or a test suite. 37 | // Targets can depend on other targets in this package, and on products in packages which this package depends on. 38 | .target( 39 | name: "SemVerParsing", 40 | swiftSettings: swiftSettings), 41 | .macro( 42 | name: "SemVerMacrosPlugin", 43 | dependencies: [ 44 | .product(name: "SwiftSyntax", package: "swift-syntax"), 45 | .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), 46 | .product(name: "SwiftDiagnostics", package: "swift-syntax"), 47 | .product(name: "SwiftCompilerPlugin", package: "swift-syntax"), 48 | "SemVerParsing", 49 | ], 50 | swiftSettings: swiftSettings), 51 | .target( 52 | name: "SemVer", 53 | dependencies: ["SemVerParsing"], 54 | swiftSettings: swiftSettings), 55 | .target( 56 | name: "SemVerMacros", 57 | dependencies: [ 58 | "SemVer", 59 | "SemVerMacrosPlugin", 60 | ], 61 | swiftSettings: swiftSettings), 62 | .testTarget( 63 | name: "SemVerParsingTests", 64 | dependencies: ["SemVerParsing"], 65 | swiftSettings: swiftSettings), 66 | .testTarget( 67 | name: "SemVerTests", 68 | dependencies: [ 69 | "SemVerParsing", 70 | "SemVer", 71 | "SemVerMacros", 72 | ], 73 | swiftSettings: swiftSettings), 74 | .testTarget( 75 | name: "SemVerMacrosPluginTests", 76 | dependencies: [ 77 | .product(name: "SwiftSyntaxMacros", package: "swift-syntax"), 78 | .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"), 79 | "SemVerParsing", // Xcode fails otherwise... 80 | "SemVerMacrosPlugin", 81 | ], 82 | swiftSettings: swiftSettings), 83 | ] 84 | ) 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SemVer 2 | [![GitHub release](https://img.shields.io/github/release/sersoft-gmbh/semver.svg?style=flat)](https://github.com/sersoft-gmbh/semver/releases/latest) 3 | ![Tests](https://github.com/sersoft-gmbh/semver/workflows/Tests/badge.svg) 4 | [![Codacy Badge](https://app.codacy.com/project/badge/Grade/d36d463d4085404b914e5c5ffd45a725)](https://www.codacy.com/gh/sersoft-gmbh/semver/dashboard?utm_source=github.com&utm_medium=referral&utm_content=sersoft-gmbh/semver&utm_campaign=Badge_Grade) 5 | [![codecov](https://codecov.io/gh/sersoft-gmbh/semver/branch/main/graph/badge.svg)](https://codecov.io/gh/sersoft-gmbh/semver) 6 | [![Docs](https://img.shields.io/badge/-documentation-informational)](https://sersoft-gmbh.github.io/semver) 7 | 8 | This repository contains a complete implementation of a `Version` struct that conforms to the rules of semantic versioning which are described at [semver.org](https://semver.org). 9 | 10 | ## Installation 11 | 12 | Add the following dependency to your `Package.swift`: 13 | ```swift 14 | .package(url: "https://github.com/sersoft-gmbh/semver", from: "5.0.0"), 15 | ``` 16 | 17 | ## Compatibility 18 | 19 | | **Swift** | **SemVer Package** | 20 | |--------------------|---------------------| 21 | | < 5.3.0 | 1.x.y - 2.x.y | 22 | | >= 5.3.0, < 5.9.0 | 3.x.y | 23 | | >= 5.9.0 | 5.x.y | 24 | 25 | 26 | ## Usage 27 | 28 | ### Creating Versions 29 | 30 | You can create a version like this: 31 | 32 | ```swift 33 | let version = Version(major: 1, minor: 2, patch: 3, 34 | prerelease: "beta", 1, // prerelease could also be ["beta", 1] 35 | metadata: "exp", "test") // metadata could also be ["exp, test"] 36 | version.versionString() // -> "1.2.3-beta.1+exp.test" 37 | ``` 38 | 39 | Of course there are simpler ways: 40 | 41 | ```swift 42 | let initialRelease = Version(major: 1) 43 | initialRelease.versionString() // -> "1.0.0" 44 | 45 | let minorRelease = Version(major: 2, minor: 1) 46 | minorRelease.versionString() // -> "2.1.0" 47 | 48 | let patchRelease = Version(major: 3, minor: 2, patch: 1) 49 | patchRelease.versionString() // -> "3.2.1" 50 | ``` 51 | 52 | And there is also a Swift [macro](#macros) for statically creating versions. 53 | 54 | ### Version Strings 55 | 56 | As seen in above's examples, there's a func to return a string represenation of a `Version`. The `versionString(formattedWith options: FormattingOptions = default)` function allows to retrieve a formatted string using the options passed. By default the full version is returned. 57 | The following options currently exist: 58 | 59 | - `.dropPatchIfZero`: If `patch` is `0`, it won't be added to the version string. 60 | - `.dropMinorIfZero`: If `minor` and `patch` are both `0`, only the `major` number is added. Requires `.dropPatchIfZero`. 61 | - `.dropTrailingZeros`: A convenience combination of `.dropPatchIfZero` and `.dropMinorIfZero`. 62 | - `.includePrerelease`: If `prerelease` are not empty, they are added to the version string. 63 | - `.includeMetadata`: If `metadata` is not empty, it is added to the version string. 64 | - `.fullVersion`: A convenience combination of `.includePrerelease` and `.includeMetadata`. The default if you don't pass anything to `versionString`. 65 | 66 | ```swift 67 | let version = Version(major: 1, minor: 2, patch: 3, 68 | prerelease: "beta", 69 | metadata: "exp", "test") 70 | version.versionString(formattedWith: .includePrerelease]) // -> "1.2.3-beta" 71 | version.versionString(formattedWith: .includeMetadata) // -> "1.2.3+exp.test" 72 | version.versionString(formattedWith: []) // -> "1.2.3" 73 | 74 | let version2 = Version(major: 2) 75 | version2.versionString(formattedWith: .dropPatchIfZero) // -> "2.0" 76 | version2.versionString(formattedWith: .dropTrailingZeros) // -> "2" 77 | ``` 78 | 79 | A `Version` can also be created from a String. All Strings created by the `versionString` func should result in the same `Version` they were created from: 80 | 81 | ```swift 82 | let version = Version(major: 1, minor: 2, patch: 3, 83 | prerelease: "beta", 84 | metadata: "exp", "test") 85 | let str = version.versionString() // -> "1.2.3-beta+exp.test" 86 | let recreatedVersion = Version(str) // recreatedVersion is Optional 87 | recreatedVersion == version // -> true 88 | ``` 89 | 90 | ### Comparing Versions 91 | 92 | A `Version` can also be compared to other versions. This also follows the rules of semantic versioning. This means that `metadata` has no effect on comparing at all. This also means that a version with and without metadata are **treated as equal**: 93 | 94 | ```swift 95 | let versionWithMetadata = Version(major: 1, minor: 2, patch: 3, 96 | metadata: "exp", "test") 97 | let versionWithoutMetadata = Version(major: 1, minor: 2, patch: 3) 98 | versionWithMetadata == versionWithoutMetadata // -> true 99 | ``` 100 | 101 | Otherwise, comparing two `Version`'s basically compares their major/minor/patch numbers. A `Version` with `prerelease` identifiers is ordered **before** a the same version without `prerelease` identifiers: 102 | 103 | ```swift 104 | let preReleaseVersion = Version(major: 1, minor: 2, patch: 3, 105 | prerelease: "beta") 106 | let finalVersion = Version(major: 1, minor: 2, patch: 3) 107 | preReleaseVersion < finalVersion // -> true 108 | ``` 109 | 110 | If you need to check whether two versions are completely identical, there's the `isIdentical(to:)` method, which also checks `metadata`. 111 | 112 | ### Validity Checks 113 | 114 | `Version` performs some validity checks on its fields. This means, that no negative numbers are allowed for `major`, `minor` and `patch`. Also, the `prerelease` and `metadata` Strings must only contain alphanumeric characters plus `-` (hyphen). However, to keep working with `Version` production-safe, these rules are only checked in non-optimized builds (using `assert()`). The result of using not allowed numbers / characters in optimized builds is undetermined. While calling `versionString()` very likely won't break, it certainly won't be possible to recreate a version containing invalid numbers / characters using `init(_ description: String)`. 115 | 116 | ### Codable 117 | 118 | `Version` conforms to `Codable`! The encoding / decoding behavior can be controlled by using the `.versionEncodingStrategy` and `.versionDecodingStrategy` `CodingUserInfoKey`s. For `JSONEncoder`/`JSONDecoder` and `PropertyListEncoder`/`PropertyListDecoder`, there are the convenience properties `semverVersionEncodingStrategy`/`semverVersionDecodingStrategy` in place. 119 | 120 | ### Macros 121 | 122 | This package also provides a `SemVerMacros` product. It's a separate product, so that SwiftSyntax won't be compiled for users of SemVer if no macro is actually needed. 123 | If a `Version` should be constructed from a `String` that is known at compile time, the `#version` macro can be used. It will parse the `String` at compile time and generate code that initializes a `Version` from the result: 124 | 125 | ```swift 126 | let version = #version("1.2.3") 127 | ``` 128 | 129 | results in 130 | 131 | ```swift 132 | let version = Version(major: 1, minor: 2, patch: 3, prerelase: [], metadata: []) 133 | ``` 134 | 135 | ## Documentation 136 | 137 | The API is documented using header doc. If you prefer to view the documentation as a webpage, there is an [online version](https://sersoft-gmbh.github.io/semver) available for you. 138 | 139 | ## Contributing 140 | 141 | If you find a bug / like to see a new feature in SemVer there are a few ways of helping out: 142 | 143 | - If you can fix the bug / implement the feature yourself please do and open a PR! 144 | - If you know how to code (which you probably do), please add a (failing) test and open a PR. We'll try to get your test green ASAP. 145 | - If you can't do neither, then open an issue. While this might be the easiest way, it will likely take the longest for the bug to be fixed / feature to be implemented. 146 | 147 | ## License & Copyright 148 | 149 | See [LICENSE](./LICENSE) file. 150 | -------------------------------------------------------------------------------- /Sources/SemVer/Version+Adjustments.swift: -------------------------------------------------------------------------------- 1 | extension Version { 2 | /// Lists all the numeric parts of a version (major, minor and patch). 3 | public enum NumericPart: Sendable, Hashable, CustomStringConvertible { 4 | /// The major version part. 5 | case major 6 | /// The minor version part. 7 | case minor 8 | /// The patch version part. 9 | case patch 10 | 11 | public var description: String { 12 | switch self { 13 | case .major: "major" 14 | case .minor: "minor" 15 | case .patch: "patch" 16 | } 17 | } 18 | } 19 | 20 | /// Returns the next version, increasing the given numeric part, respecting any associated rules: 21 | /// - If the major version is increased, minor and patch are set to 0. 22 | /// - If the minor version is increased, patch is set to 0 23 | /// - If the patch version is increased, no other changes are made. 24 | /// 25 | /// - Parameters: 26 | /// - part: The numeric part to increase. 27 | /// - keepingPrerelease: Whether or not the ``Version/prerelease`` should be kept. Defaults to `false`. 28 | /// - keepingMetadata: Whether or not the ``Version/metadata`` should be kept. Defaults to `false`. 29 | /// - Returns: A new version that has the specified `part` increased, along with the necessary other changes. 30 | public func next(_ part: NumericPart, 31 | keepingPrerelease: Bool = false, 32 | keepingMetadata: Bool = false) -> Self { 33 | let newPrerelease = keepingPrerelease ? prerelease : .init() 34 | let newMetadata = keepingMetadata ? metadata : .init() 35 | switch part { 36 | case .major: return Version(major: major + 1, minor: 0, patch: 0, prerelease: newPrerelease, metadata: newMetadata) 37 | case .minor: return Version(major: major, minor: minor + 1, patch: 0, prerelease: newPrerelease, metadata: newMetadata) 38 | case .patch: return Version(major: major, minor: minor, patch: patch + 1, prerelease: newPrerelease, metadata: newMetadata) 39 | } 40 | } 41 | 42 | /// Increases the given numeric part of the version, respecting any associated rules: 43 | /// - If the major version is increased, minor and patch are set to 0. 44 | /// - If the minor version is increased, patch is set to 0 45 | /// - If the patch version is increased, no other changes are made. 46 | /// 47 | /// - Parameters: 48 | /// - part: The numeric part to increase. 49 | /// - keepingPrerelease: Whether or not the ``Version/prerelease`` should be kept. Defaults to `false`. 50 | /// - keepingMetadata: Whether or not the ``Version/metadata`` should be kept. Defaults to `false`. 51 | public mutating func increase( 52 | _ part: NumericPart, 53 | keepingPrerelease: Bool = false, 54 | keepingMetadata: Bool = false 55 | ) { 56 | switch part { 57 | case .major: 58 | major += 1 59 | (minor, patch) = (0, 0) 60 | case .minor: 61 | minor += 1 62 | patch = 0 63 | case .patch: 64 | patch += 1 65 | } 66 | if !keepingPrerelease { 67 | prerelease.removeAll() 68 | } 69 | if !keepingMetadata { 70 | metadata.removeAll() 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Sources/SemVer/Version+Codable.swift: -------------------------------------------------------------------------------- 1 | public import Foundation 2 | 3 | extension CodingUserInfoKey { 4 | /// The key used to configure an ``Swift/Encoder`` with the strategy that should be used for encoding a ``Version``. 5 | public static let versionEncodingStrategy = CodingUserInfoKey(rawValue: "de.sersoft.semver.version.encoding-strategy")! 6 | /// The key used to configure an ``Swift/Decoder`` with the strategy that should be used for decoding a ``Version``. 7 | public static let versionDecodingStrategy = CodingUserInfoKey(rawValue: "de.sersoft.semver.version.decoding-strategy")! 8 | } 9 | 10 | extension Dictionary where Key == CodingUserInfoKey, Value == Any { 11 | @inlinable 12 | subscript(_ key: Key) -> V? { 13 | get { self[key] as? V } 14 | set { self[key] = newValue } 15 | } 16 | } 17 | 18 | extension Version { 19 | fileprivate enum CodingKeys: String, CodingKey { 20 | case major, minor, patch, prerelease, metadata 21 | } 22 | 23 | /// The strategy for encoding a ``Version``. 24 | public enum EncodingStrategy: Sendable { 25 | /// Encodes the version's components as separate keys. 26 | /// - `prereleaseAsString` controls whether the ``Version/prerelease`` are encoded as list or string. 27 | /// - `metadataAsString` controls whether ``Version/metadata`` is encoded as list or string. 28 | case components(prereleaseAsString: Bool, metadataAsString: Bool) 29 | /// Encodes a version string using the given format. 30 | case string(Version.FormattingOptions) 31 | /// Uses the given closure for encoding a version. 32 | @preconcurrency 33 | case custom(@Sendable (Version, any Encoder) throws -> ()) 34 | 35 | /// Convenience accessor representing `.components(prereleaseAsString: true, metadataAsString: false)`, 36 | /// which was the default of the non-parameterized `components`. 37 | public static var components: Self { .components(prereleaseAsString: true, metadataAsString: false) } 38 | 39 | /// Convenience accessor representing `.string(.fullVersion)`. 40 | public static var string: Self { .string(.fullVersion) } 41 | 42 | @usableFromInline 43 | internal static var _default: Self { .components } 44 | } 45 | 46 | /// The strategy for decoding a ``Version``. 47 | public enum DecodingStrategy: Sendable { 48 | /// Decodes the version's components as separate keys. 49 | /// - `prereleaseAsString` controls whether the ``Version/prerelease`` are decoded as list or string. 50 | /// - `metadataAsString` controls whether ``Version/metadata`` is decode as list or string. 51 | case components(prereleaseAsString: Bool, metadataAsString: Bool) 52 | /// Decodes a version from a string. 53 | case string 54 | /// Uses the given closure for decoding a version. 55 | @preconcurrency 56 | case custom(@Sendable (any Decoder) throws -> Version) 57 | 58 | /// Convenience accessor representing `.components(prereleaseAsString: true, metadataAsString: false)`, 59 | /// which was the default of the non-parameterized `components`. 60 | public static var components: Self { .components(prereleaseAsString: true, metadataAsString: false) } 61 | 62 | @usableFromInline 63 | internal static var _default: Self { .components } 64 | } 65 | } 66 | 67 | extension Version: Encodable { 68 | /// Encodes the version to the given encoder using the given strategy. 69 | /// - Parameters: 70 | /// - encoder: The encoder to encode to. 71 | /// - strategy: The strategy to use for encoding. 72 | @usableFromInline 73 | internal func encode(to encoder: any Encoder, using strategy: EncodingStrategy) throws { 74 | switch strategy { 75 | case .components(let prereleaseAsString, let metadataAsString): 76 | var container = encoder.container(keyedBy: CodingKeys.self) 77 | try container.encode(major, forKey: .major) 78 | try container.encode(minor, forKey: .minor) 79 | try container.encode(patch, forKey: .patch) 80 | if prereleaseAsString { 81 | try container.encode(_prereleaseString, forKey: .prerelease) 82 | } else { 83 | try container.encode(prerelease, forKey: .prerelease) 84 | } 85 | if metadataAsString { 86 | try container.encode(_metadataString, forKey: .metadata) 87 | } else { 88 | try container.encode(metadata, forKey: .metadata) 89 | } 90 | case .string(let options): 91 | var container = encoder.singleValueContainer() 92 | try container.encode(versionString(formattedWith: options)) 93 | case .custom(let closure): 94 | try closure(self, encoder) 95 | } 96 | } 97 | 98 | @inlinable 99 | public func encode(to encoder: any Encoder) throws { 100 | try encode(to: encoder, using: encoder.userInfo[.versionEncodingStrategy] ?? ._default) 101 | } 102 | } 103 | 104 | extension Version: Decodable { 105 | /// Decodes a version from the given decoder using the given strategy. 106 | /// - Parameters: 107 | /// - decoder: The decoder to decode from. 108 | /// - strategy: The strategy to use for decoding. 109 | @usableFromInline 110 | internal init(from decoder: any Decoder, using strategy: DecodingStrategy) throws { 111 | switch strategy { 112 | case .components(let prereleaseAsString, let metadataAsString): 113 | let container = try decoder.container(keyedBy: CodingKeys.self) 114 | 115 | let major = try container.decode(Int.self, forKey: .major) 116 | guard major >= 0 117 | else { throw DecodingError.dataCorruptedError(forKey: .major, in: container, debugDescription: "Invalid major version component: \(major)") } 118 | let minor = try container.decodeIfPresent(Int.self, forKey: .minor) 119 | guard minor.map({ $0 >= 0 }) != false 120 | else { throw DecodingError.dataCorruptedError(forKey: .minor, in: container, debugDescription: "Invalid minor version component: \(minor!)") } 121 | let patch = try container.decodeIfPresent(Int.self, forKey: .patch) 122 | guard patch.map({ $0 >= 0 }) != false 123 | else { throw DecodingError.dataCorruptedError(forKey: .patch, in: container, debugDescription: "Invalid patch version component: \(patch!)") } 124 | 125 | let prerelease: Array? 126 | if prereleaseAsString { 127 | if let prereleaseIdentifiers = try container.decodeIfPresent(String.self, forKey: .prerelease).map(Self._splitIdentifiers) { 128 | guard Self._areValidIdentifiers(prereleaseIdentifiers) 129 | else { throw DecodingError.dataCorruptedError(forKey: .prerelease, in: container, debugDescription: "Invalid prerelease: \(prereleaseIdentifiers)") } 130 | prerelease = prereleaseIdentifiers.map(PrereleaseIdentifier.string) 131 | } else { 132 | prerelease = nil 133 | } 134 | } else { 135 | prerelease = try container.decodeIfPresent(Array.self, forKey: .prerelease) 136 | } 137 | 138 | let metadata: Array? 139 | if metadataAsString { 140 | metadata = try container.decodeIfPresent(String.self, forKey: .metadata).map(Self._splitIdentifiers) 141 | } else { 142 | metadata = try container.decodeIfPresent(Array.self, forKey: .metadata) 143 | } 144 | guard metadata.map(Self._areValidIdentifiers) != false 145 | else { throw DecodingError.dataCorruptedError(forKey: .patch, in: container, debugDescription: "Invalid metadata: \(metadata!)") } 146 | 147 | self.init( 148 | major: major, 149 | minor: minor ?? 0, 150 | patch: patch ?? 0, 151 | prerelease: prerelease ?? .init(), 152 | metadata: metadata ?? .init() 153 | ) 154 | case .string: 155 | let string = try decoder.singleValueContainer().decode(String.self) 156 | guard let version = Version(string) else { 157 | throw DecodingError.dataCorrupted(.init(codingPath: decoder.codingPath, 158 | debugDescription: "Cannot convert \(string) to \(Version.self)!")) 159 | } 160 | self = version 161 | case .custom(let closure): 162 | self = try closure(decoder) 163 | } 164 | } 165 | 166 | @inlinable 167 | public init(from decoder: any Decoder) throws { 168 | try self.init(from: decoder, using: decoder.userInfo[.versionDecodingStrategy] ?? ._default) 169 | } 170 | } 171 | 172 | extension JSONEncoder { 173 | /// The strategy to use for encoding a ``Version``. 174 | public var semverVersionEncodingStrategy: Version.EncodingStrategy { 175 | get { userInfo[.versionEncodingStrategy] ?? ._default } 176 | set { userInfo[.versionEncodingStrategy] = newValue } 177 | } 178 | } 179 | 180 | extension PropertyListEncoder { 181 | /// The strategy to use for encoding a ``Version``. 182 | public var semverVersionEncodingStrategy: Version.EncodingStrategy { 183 | get { userInfo[.versionEncodingStrategy] ?? ._default } 184 | set { userInfo[.versionEncodingStrategy] = newValue } 185 | } 186 | } 187 | 188 | extension JSONDecoder { 189 | /// The strategy to use for decoding a ``Version``. 190 | public var semverVersionDecodingStrategy: Version.DecodingStrategy { 191 | get { userInfo[.versionDecodingStrategy] ?? ._default } 192 | set { userInfo[.versionDecodingStrategy] = newValue } 193 | } 194 | } 195 | 196 | extension PropertyListDecoder { 197 | /// The strategy to use for decoding a ``Version``. 198 | public var semverVersionDecodingStrategy: Version.DecodingStrategy { 199 | get { userInfo[.versionDecodingStrategy] ?? ._default } 200 | set { userInfo[.versionDecodingStrategy] = newValue } 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /Sources/SemVer/Version+FormattingOptions.swift: -------------------------------------------------------------------------------- 1 | extension Version { 2 | /// Describes a set options that define the formatting behavior. 3 | @frozen 4 | public struct FormattingOptions: OptionSet, Hashable, Sendable { 5 | public typealias RawValue = Int 6 | 7 | public let rawValue: RawValue 8 | 9 | @inlinable 10 | public init(rawValue: RawValue) { 11 | self.rawValue = rawValue 12 | } 13 | } 14 | } 15 | 16 | extension Version.FormattingOptions { 17 | /// Leave out patch part if it's zero. 18 | public static let dropPatchIfZero = Version.FormattingOptions(rawValue: 1 << 0) 19 | /// Leave out minor part if it's zero. Requires `dropPatchIfZero`. 20 | public static let dropMinorIfZero = Version.FormattingOptions(rawValue: 1 << 1) 21 | /// Include the pre-release part of the version. 22 | public static let includePrerelease = Version.FormattingOptions(rawValue: 1 << 2) 23 | /// Include the metadata part of the version. 24 | public static let includeMetadata = Version.FormattingOptions(rawValue: 1 << 3) 25 | 26 | /// Combination of ``Version/FormattingOptions/includePrerelease`` and ``Version/FormattingOptions/includeMetadata``. 27 | @inlinable 28 | public static var fullVersion: Version.FormattingOptions { [.includePrerelease, .includeMetadata] } 29 | /// Combination of ``Version/FormattingOptions/dropPatchIfZero`` and ``Version/FormattingOptions/dropMinorIfZero``. 30 | @inlinable 31 | public static var dropTrailingZeros: Version.FormattingOptions { [.dropMinorIfZero, .dropPatchIfZero] } 32 | } 33 | -------------------------------------------------------------------------------- /Sources/SemVer/Version+PrereleaseIdentifier.swift: -------------------------------------------------------------------------------- 1 | @_spi(SemVerValidation) 2 | internal import SemVerParsing 3 | 4 | extension Version { 5 | /// Represents a prerelease identifier of a version. 6 | public struct PrereleaseIdentifier: Sendable, Hashable, Comparable, CustomStringConvertible, CustomDebugStringConvertible { 7 | internal typealias _Storage = VersionParser.VersionPrereleaseIdentifier 8 | 9 | internal var _storage: _Storage 10 | 11 | @inlinable 12 | public var description: String { string } 13 | 14 | public var debugDescription: String { 15 | switch _storage { 16 | case .number(let number): "number(\(number))" 17 | case .text(let text): "text(\(text))" 18 | } 19 | } 20 | 21 | /// The string representation of this identifier. 22 | /// Numbers will be converted to strings. 23 | public var string: String { 24 | switch _storage { 25 | case .number(let number): String(number) 26 | case .text(let text): text 27 | } 28 | } 29 | 30 | /// The number representation of this identifier. 31 | /// Returns `nil` if the receiver represents a text identifier. 32 | public var number: Int? { 33 | switch _storage { 34 | case .number(let number): number 35 | case .text(_): nil 36 | } 37 | } 38 | 39 | internal init(_storage: _Storage) { 40 | assert({ 41 | guard case .text(let text) = _storage 42 | else { return true } 43 | return Int(text) == nil 44 | }(), "Text storage is numeric!") 45 | self._storage = _storage 46 | } 47 | 48 | /// Creates a new identifier from a given number. 49 | /// - Parameter number: The number to store. 50 | public init(_ number: Int) { 51 | self.init(_storage: .number(number)) 52 | } 53 | 54 | /// Creates a new identifier from a given string. 55 | /// This will also attempt to parse numbers (e.g. `"1"` will behave like `1`). 56 | /// - Parameter string: The string to parse. Must only contain characters in ``Foundation/CharacterSet/versionSuffixAllowed`` 57 | /// - SeeAlso: ``Foundation/CharacterSet/versionSuffixAllowed`` 58 | public init(_ string: String) { 59 | assert(Version._isValidIdentifier(string)) 60 | self.init(_storage: Int(string).map { .number($0) } ?? .text(string)) 61 | } 62 | 63 | /// Creates a new identifier from a given string. 64 | /// This will **not** attempt to parse numbers! 65 | /// - Parameter string: The string to parse. Must only contain characters in ``Foundation/CharacterSet/versionSuffixAllowed`` 66 | /// - Precondition: The `string` cannot be represented as a number (`Int(string) == nil`)! 67 | public init(unchecked string: some StringProtocol) { 68 | assert(Version._isValidIdentifier(string)) 69 | let _string = String(string) 70 | precondition(Int(_string) == nil) 71 | self.init(_storage: .text(_string)) 72 | } 73 | 74 | /// Creates a new identifier from a given number. 75 | /// - Parameter number: The number to store. 76 | @inlinable 77 | public static func number(_ number: Int) -> Self { 78 | .init(number) 79 | } 80 | 81 | /// Creates a new identifier from a given string. 82 | /// This will also attempt to parse numbers (e.g. `"1"` will behave like `1`). 83 | /// - Parameter string: The string to parse. Must only contain characters in ``Foundation/CharacterSet/versionSuffixAllowed`` 84 | /// - Returns: A new prerelease identifier. 85 | @inlinable 86 | public static func string(_ string: some StringProtocol) -> Self { 87 | .init(String(string)) 88 | } 89 | 90 | public static func <(lhs: Self, rhs: Self) -> Bool { 91 | switch (lhs._storage, rhs._storage) { 92 | // Identifiers consisting of only digits are compared numerically. 93 | case (.number(let lhsNumber), .number(let rhsNumber)): lhsNumber < rhsNumber 94 | // Identifiers with letters or hyphens are compared lexically in ASCII sort order. 95 | case (.text(let lhsText), .text(let rhsText)): lhsText < rhsText 96 | // Numeric identifiers always have lower precedence than non-numeric identifiers. 97 | case (.number(_), .text(_)): true 98 | case (.text(_), .number(_)): false 99 | } 100 | } 101 | } 102 | } 103 | 104 | extension Version.PrereleaseIdentifier: ExpressibleByIntegerLiteral { 105 | public typealias IntegerLiteralType = Int 106 | 107 | @inlinable 108 | public init(integerLiteral value: IntegerLiteralType) { 109 | self.init(value) 110 | } 111 | } 112 | 113 | extension Version.PrereleaseIdentifier: ExpressibleByStringLiteral { 114 | public typealias StringLiteralType = String 115 | 116 | @inlinable 117 | public init(stringLiteral value: StringLiteralType) { 118 | self.init(value) 119 | } 120 | } 121 | 122 | extension Version.PrereleaseIdentifier: Codable { 123 | public init(from decoder: any Decoder) throws { 124 | let container = try decoder.singleValueContainer() 125 | let string = try container.decode(String.self) 126 | guard Version._isValidIdentifier(string) 127 | else { throw DecodingError.dataCorruptedError(in: container, debugDescription: #"Invalid prerelease identifier: "\#(string)""#) } 128 | self.init(string) 129 | } 130 | 131 | public func encode(to encoder: any Encoder) throws { 132 | var container = encoder.singleValueContainer() 133 | try container.encode(string) 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /Sources/SemVer/Version.swift: -------------------------------------------------------------------------------- 1 | public import struct Foundation.CharacterSet 2 | @_spi(SemVerValidation) 3 | internal import SemVerParsing 4 | 5 | extension CharacterSet { 6 | // Dance necessary because CharacterSet doesn't conform to Sendable in scf until Swift 6... 7 | // Compiler check is needed because `hasFeature` doesn't prevent the compiler from trying to parse the code. 8 | #if !canImport(Darwin) && compiler(>=5.10) && swift(<6.0) && hasFeature(StrictConcurrency) && hasFeature(GlobalConcurrency) 9 | /// Contains the allowed characters for a ``Version`` suffix (``Version/prerelease`` and ``Version/metadata``) 10 | /// Allowed are alphanumerics and hyphen. 11 | public static nonisolated(unsafe) let versionSuffixAllowed: CharacterSet = VersionParser.versionSuffixAllowedCharacterSet 12 | #else 13 | /// Contains the allowed characters for a ``Version`` suffix (``Version/prerelease`` and ``Version/metadata``) 14 | /// Allowed are alphanumerics and hyphen. 15 | public static let versionSuffixAllowed: CharacterSet = VersionParser.versionSuffixAllowedCharacterSet 16 | #endif 17 | } 18 | 19 | /// A Version struct that implements the rules of semantic versioning. 20 | /// - SeeAlso: [SemVer Specification](https://semver.org) 21 | public struct Version: Sendable, Hashable, Comparable, LosslessStringConvertible, CustomDebugStringConvertible { 22 | /// The major part of this version. Must be >= 0. 23 | public var major: Int { 24 | willSet { assert(newValue >= 0) } 25 | } 26 | /// The minor part of this version. Must be >= 0. 27 | public var minor: Int { 28 | willSet { assert(newValue >= 0) } 29 | } 30 | /// The patch part of this version. Must be >= 0. 31 | public var patch: Int { 32 | willSet { assert(newValue >= 0) } 33 | } 34 | /// The prelease identifiers of this version. 35 | public var prerelease: Array 36 | /// The metadata of this version. Must only contain characters in ``Foundation/CharacterSet/versionSuffixAllowed``. 37 | /// - SeeAlso: ``Foundation/CharacterSet/versionSuffixAllowed`` 38 | public var metadata: Array { 39 | willSet { assert(Self._areValidIdentifiers(newValue)) } 40 | } 41 | 42 | @inlinable 43 | public var description: String { versionString() } 44 | 45 | public var debugDescription: String { 46 | "Version(major: \(major), minor: \(minor), patch: \(patch), prelease: \"\(_prereleaseString)\", metadata: \"\(_metadataString)\")" 47 | } 48 | 49 | /// Creates a new version with the given parts. 50 | /// - Parameters: 51 | /// - major: The major part of this version. Must be >= 0. 52 | /// - minor: The minor part of this version. Must be >= 0. 53 | /// - patch: The patch part of this version. Must be >= 0. 54 | /// - prerelease: The prelease identifiers of this version. 55 | /// - metadata: The metadata of this version. Must only contain characters in ``Foundation/CharacterSet/versionSuffixAllowed``. 56 | public init(major: Int, minor: Int = 0, patch: Int = 0, 57 | prerelease: Array = .init(), 58 | metadata: Array = .init()) { 59 | assert(major >= 0) 60 | assert(minor >= 0) 61 | assert(patch >= 0) 62 | assert(Self._areValidIdentifiers(metadata)) 63 | 64 | self.major = major 65 | self.minor = minor 66 | self.patch = patch 67 | self.prerelease = prerelease 68 | self.metadata = metadata 69 | } 70 | 71 | /// Creates a new version with the given parts. 72 | /// - Parameters: 73 | /// - major: The major part of this version. Must be >= 0. 74 | /// - minor: The minor part of this version. Must be >= 0. 75 | /// - patch: The patch part of this version. Must be >= 0. 76 | /// - prerelease: The prelease identifiers of this version. 77 | /// - metadata: The metadata of this version. Must only contain characters in ``Foundation/CharacterSet/versionSuffixAllowed``. 78 | @inlinable 79 | public init(major: Int, minor: Int = 0, patch: Int = 0, prerelease: Array = .init(), metadata: String...) { 80 | self.init(major: major, minor: minor, patch: patch, prerelease: prerelease, metadata: metadata) 81 | } 82 | 83 | /// Creates a new version with the given parts. 84 | /// - Parameters: 85 | /// - major: The major part of this version. Must be >= 0. 86 | /// - minor: The minor part of this version. Must be >= 0. 87 | /// - patch: The patch part of this version. Must be >= 0. 88 | /// - prerelease: The prelease identifiers of this version. 89 | /// - metadata: The metadata of this version. Must only contain characters in ``Foundation/CharacterSet/versionSuffixAllowed``. 90 | @inlinable 91 | public init(major: Int, minor: Int = 0, patch: Int = 0, prerelease: PrereleaseIdentifier..., metadata: Array = .init()) { 92 | self.init(major: major, minor: minor, patch: patch, prerelease: prerelease, metadata: metadata) 93 | } 94 | 95 | /// Creates a new version with the given parts. 96 | /// - Parameters: 97 | /// - major: The major part of this version. Must be >= 0. 98 | /// - minor: The minor part of this version. Must be >= 0. 99 | /// - patch: The patch part of this version. Must be >= 0. 100 | /// - prerelease: The prelease identifiers of this version. 101 | /// - metadata: The metadata of this version. Must only contain characters in ``Foundation/CharacterSet/versionSuffixAllowed``. 102 | @inlinable 103 | public init(major: Int, minor: Int = 0, patch: Int = 0, prerelease: PrereleaseIdentifier..., metadata: String...) { 104 | self.init(major: major, minor: minor, patch: patch, prerelease: prerelease, metadata: metadata) 105 | } 106 | 107 | public init?(_ description: String) { 108 | guard let components = VersionParser.parseString(description) else { return nil } 109 | self.init(major: components.major, 110 | minor: components.minor, 111 | patch: components.patch, 112 | prerelease: components.prerelease.map(PrereleaseIdentifier.init(_storage:)), 113 | metadata: components.metadata) 114 | } 115 | 116 | public func hash(into hasher: inout Hasher) { 117 | hasher.combine(major) 118 | hasher.combine(minor) 119 | hasher.combine(patch) 120 | hasher.combine(prerelease) 121 | // metadata does not participate in hashing and equating 122 | } 123 | 124 | /// Creates a version string using the given options. 125 | /// 126 | /// - Parameter options: The options to use for creating the version string. 127 | /// - Returns: A string containing the version formatted with the given options. 128 | public func versionString(formattedWith options: FormattingOptions = .fullVersion) -> String { 129 | var versionString = String(major) 130 | if !options.contains(.dropPatchIfZero) || patch != 0 { 131 | versionString += ".\(minor).\(patch)" 132 | } else if !options.contains(.dropMinorIfZero) || minor != 0 { 133 | versionString += ".\(minor)" 134 | } 135 | if options.contains(.includePrerelease) && !prerelease.isEmpty { 136 | versionString += "-\(_prereleaseString)" 137 | } 138 | if options.contains(.includeMetadata) && !metadata.isEmpty { 139 | versionString += "+\(_metadataString)" 140 | } 141 | return versionString 142 | } 143 | } 144 | 145 | /* This currently does not work, due to the compiler ignoring the `init(_ description:)` for `Version("blah")` now. 146 | // MARK: - String Literal Conversion 147 | /// - Note: This conformance will crash if the given String literal is not a valid version! 148 | extension Version: ExpressibleByStringLiteral { 149 | public typealias StringLiteralType = String 150 | 151 | public init(stringLiteral value: StringLiteralType) { 152 | guard let version = Self.init(value) else { 153 | fatalError("'\(value)' is not a valid semantic version!") 154 | } 155 | self = version 156 | } 157 | } 158 | */ 159 | 160 | // MARK: - Comparison 161 | extension Version { 162 | /// Returns whether this version is identical to another version (on all properties, including ``Version/metadata``). 163 | /// - Parameters: 164 | /// - other: The version to compare to. 165 | /// - requireIdenticalMetadataOrdering: Whether the metadata of both versions need to have the same order to be considered identical. 166 | /// - Returns: Whether the receiver is identical to `other`. 167 | public func isIdentical(to other: Version, requireIdenticalMetadataOrdering: Bool = false) -> Bool { 168 | guard self == other else { return false } 169 | return requireIdenticalMetadataOrdering 170 | ? metadata == other.metadata 171 | : metadata.count == other.metadata.count && Set(metadata) == Set(other.metadata) 172 | } 173 | 174 | public static func ==(lhs: Self, rhs: Self) -> Bool { 175 | (lhs.major, lhs.minor, lhs.patch, lhs.prerelease) 176 | == 177 | (rhs.major, rhs.minor, rhs.patch, rhs.prerelease) 178 | } 179 | 180 | public static func <(lhs: Self, rhs: Self) -> Bool { 181 | if (lhs.major, lhs.minor, lhs.patch) < (rhs.major, rhs.minor, rhs.patch) { 182 | return true 183 | } 184 | if (lhs.major, lhs.minor, lhs.patch) > (rhs.major, rhs.minor, rhs.patch) { 185 | return false 186 | } 187 | // A version with a pre-release has a lower precedence than the same version without pre-release. 188 | guard !lhs.prerelease.isEmpty else { return false } 189 | guard !rhs.prerelease.isEmpty else { return true } 190 | 191 | // Skip all identifiers that are equal, then compare the first non-equal (if any). 192 | if let (lhsIdent, rhsIdent) = zip(lhs.prerelease, rhs.prerelease).first(where: { $0 != $1 }) { 193 | return lhsIdent < rhsIdent 194 | } 195 | 196 | // A larger set of pre-release fields has a higher precedence than a smaller set, if all of the preceding identifiers are equal. 197 | return lhs.prerelease.count < rhs.prerelease.count 198 | } 199 | } 200 | 201 | extension Version { 202 | @usableFromInline 203 | internal static func _isValidIdentifier(_ identifiers: some StringProtocol) -> Bool { 204 | VersionParser._isValidIdentifier(identifiers) 205 | } 206 | 207 | @inlinable 208 | internal static func _areValidIdentifiers(_ identifiers: some Sequence) -> Bool { 209 | identifiers.allSatisfy(_isValidIdentifier) 210 | } 211 | 212 | internal static func _splitIdentifiers(_ identifier: S) -> Array 213 | where S: StringProtocol, S.SubSequence == Substring 214 | { 215 | VersionParser._splitIdentifiers(identifier) 216 | } 217 | 218 | @usableFromInline 219 | internal var _prereleaseString: String { VersionParser._joinIdentifiers(prerelease.lazy.map(\.string)) } 220 | @usableFromInline 221 | internal var _metadataString: String { VersionParser._joinIdentifiers(metadata) } 222 | } 223 | -------------------------------------------------------------------------------- /Sources/SemVerMacros/VersionMacro.swift: -------------------------------------------------------------------------------- 1 | @_exported public import SemVer 2 | 3 | /// Parses a string to a ``Version`` at compile time. 4 | @freestanding(expression) 5 | public macro version(_ string: StaticString) -> Version = #externalMacro(module: "SemVerMacrosPlugin", type: "VersionMacro") 6 | -------------------------------------------------------------------------------- /Sources/SemVerMacrosPlugin/SemVerMacrosCompilerPlugin.swift: -------------------------------------------------------------------------------- 1 | #if canImport(SwiftCompilerPlugin) 2 | #if swift(<6.0) 3 | public import SwiftCompilerPlugin 4 | public import SwiftSyntaxMacros 5 | #else 6 | import SwiftCompilerPlugin 7 | import SwiftSyntaxMacros 8 | #endif 9 | 10 | @main 11 | struct SemVerMacrosCompilerPlugin: CompilerPlugin { 12 | let providingMacros: Array = [ 13 | VersionMacro.self, 14 | ] 15 | } 16 | #endif 17 | -------------------------------------------------------------------------------- /Sources/SemVerMacrosPlugin/VersionMacro.swift: -------------------------------------------------------------------------------- 1 | public import SwiftSyntax 2 | public import SwiftSyntaxMacros 3 | internal import SwiftDiagnostics 4 | internal import SemVerParsing 5 | 6 | fileprivate extension DiagnosticMessage where Self == VersionMacro.DiagnosticMessage { 7 | static var notAStringLiteral: Self { 8 | .init(severity: .error, message: "#\(VersionMacro.name) requires a literal string (without any interpolations)!") 9 | } 10 | 11 | static func notAValidVersion(_ string: String) -> Self { 12 | .init(severity: .error, message: "Invalid version string: '\(string)'") 13 | } 14 | } 15 | 16 | @frozen 17 | public enum VersionMacro: ExpressionMacro { 18 | internal struct DiagnosticMessage: SwiftDiagnostics.DiagnosticMessage { 19 | let diagnosticID: MessageID 20 | let severity: DiagnosticSeverity 21 | let message: String 22 | 23 | fileprivate init(id: String = #file, severity: DiagnosticSeverity, message: String) { 24 | self.diagnosticID = .init(domain: "de.sersoft.semver.version-macro", id: id) 25 | self.severity = severity 26 | self.message = message 27 | } 28 | } 29 | 30 | internal static let name = "version" 31 | 32 | public static func expansion(of node: some FreestandingMacroExpansionSyntax, 33 | in context: some MacroExpansionContext) throws -> ExprSyntax { 34 | #if swift(>=5.10) 35 | assert(node.macroName.text == name) 36 | guard let arg = node.arguments.first else { fatalError("Missing argument!") } 37 | #else 38 | assert(node.macro.text == name) 39 | guard let arg = node.argumentList.first else { fatalError("Missing argument!") } 40 | #endif 41 | guard let stringLiteralExpr = arg.expression.as(StringLiteralExprSyntax.self), 42 | let string = stringLiteralExpr.representedLiteralValue 43 | else { 44 | throw DiagnosticsError(diagnostics: [ 45 | Diagnostic(node: arg.expression, message: .notAStringLiteral), 46 | ]) 47 | } 48 | guard let components = VersionParser.parseString(string) else { 49 | throw DiagnosticsError(diagnostics: [ 50 | Diagnostic(node: arg.expression, message: .notAValidVersion(string)), 51 | ]) 52 | } 53 | return """ 54 | SemVer.Version( 55 | major: \(literal: components.major), 56 | minor: \(literal: components.minor), 57 | patch: \(literal: components.patch), 58 | prerelease: \(ArrayExprSyntax(expressions: components.prerelease.map { 59 | switch $0 { 60 | case .number(let number): return "SemVer.Version.PrereleaseIdentifier(\(literal: number))" 61 | case .text(let text): return "SemVer.Version.PrereleaseIdentifier(unchecked: \(literal: text))" 62 | } 63 | })), 64 | metadata: \(literal: components.metadata) 65 | ) 66 | """ 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Sources/SemVerParsing/VersionParser.swift: -------------------------------------------------------------------------------- 1 | package import struct Foundation.CharacterSet 2 | 3 | @frozen 4 | package enum VersionParser: Sendable { 5 | package enum VersionPrereleaseIdentifier: Sendable, Hashable { 6 | case number(Int) 7 | case text(String) 8 | } 9 | 10 | package typealias VersionComponents = ( 11 | major: Int, minor: Int, patch: Int, 12 | prerelease: Array, 13 | metadata: Array 14 | ) 15 | 16 | private static func _parsePrereleaseIdentifiers(_ identifiers: some Sequence) -> some Sequence 17 | where S: StringProtocol, S.SubSequence == Substring 18 | { 19 | assert(identifiers.allSatisfy(_isValidIdentifier)) 20 | return identifiers.lazy.map { Int($0).map { .number($0) } ?? .text(String($0)) } 21 | } 22 | 23 | private static func _parsePrereleaseIdentifiers(_ identifiers: S) -> Array 24 | where S: StringProtocol, S.SubSequence == Substring 25 | { 26 | Array(_parsePrereleaseIdentifiers(_splitIdentifiers(identifiers))) 27 | } 28 | 29 | @available(macOS 13, iOS 16, tvOS 16, watchOS 9, macCatalyst 16, *) 30 | @usableFromInline 31 | internal static func _parseModern(_ string: S) -> VersionComponents? 32 | where S: StringProtocol, S.SubSequence == Substring 33 | { 34 | assert(!string.isEmpty) 35 | let fullRegex = #/^(?'major'\d+)(?:\.(?'minor'\d+)(?:\.(?'patch'\d+))?)?(?'prelease'-(?:[0-9A-Za-z-]+\.?)*)?(?'build'\+(?:[0-9A-Za-z-]+(?:\.|$))*)?$/# 36 | guard let fullMatch = string.wholeMatch(of: fullRegex), 37 | fullMatch.output.prelease?.count != 1, 38 | fullMatch.output.build?.count != 1, 39 | fullMatch.output.prelease?.last != ".", 40 | fullMatch.output.build?.last != ".", 41 | let major = Int(fullMatch.output.major) 42 | else { return nil } 43 | let minor = fullMatch.output.minor.flatMap { Int($0) } ?? 0 44 | let patch = fullMatch.output.patch.flatMap { Int($0) } ?? 0 45 | let prerelease = (fullMatch.output.prelease?.dropFirst()).map(_parsePrereleaseIdentifiers) ?? .init() 46 | let metadata = (fullMatch.output.build?.dropFirst()).map(_splitIdentifiers) ?? .init() 47 | return (major: major, minor: minor, patch: patch, prerelease: prerelease, metadata: metadata) 48 | } 49 | 50 | @usableFromInline 51 | internal static func _parseLegacy(_ string: S) -> VersionComponents? 52 | where S: StringProtocol, S.SubSequence == Substring 53 | { 54 | assert(!string.isEmpty) 55 | guard string.range(of: #"^(?:[0-9]+\.){0,2}[0-9]+(?:-(?:[0-9A-Za-z-]+\.?)*)?(?:\+(?:[0-9A-Za-z-]+(?:\.|$))*)?$"#, 56 | options: .regularExpression) != nil 57 | else { return nil } 58 | 59 | // This should be fine after above's regular expression 60 | let idx = string.range(of: #"[0-9](\+|-)"#, options: .regularExpression) 61 | .map { string.index(before: $0.upperBound) } ?? string.endIndex 62 | var parts: Array = string[.. 70 | if let searchRange = string.range(of: #"(?:^|\.)[0-9]+-(?:[0-9A-Za-z-]+\.?)*(?:\+|$)"#, options: .regularExpression), 71 | case let substr = string[searchRange], 72 | let range = substr.range(of: #"[0-9]-(?:[0-9A-Za-z-]+\.?)+"#, options: .regularExpression) { 73 | let prereleaseString = substr[substr.index(range.lowerBound, offsetBy: 2).. 81 | if let range = string.range(of: #"\+(?:[0-9A-Za-z-]+(?:\.|$))+$"#, options: .regularExpression) { 82 | let metadataString = string[string.index(after: range.lowerBound)..(_ string: S) -> VersionComponents? 94 | where S: StringProtocol, S.SubSequence == Substring 95 | { 96 | guard !string.isEmpty else { return nil } 97 | if #available(macOS 13, iOS 16, tvOS 16, watchOS 9, macCatalyst 16, visionOS 1, *) { 98 | return _parseModern(string) 99 | } else { 100 | return _parseLegacy(string) 101 | } 102 | } 103 | } 104 | 105 | @_spi(SemVerValidation) 106 | extension VersionParser { 107 | private static var _identifierSeparator: Character { "." } 108 | 109 | // Dance necessary because CharacterSet doesn't conform to Sendable in scf... 110 | // Compiler check is needed because `hasFeature` doesn't prevent the compiler from trying to parse the code. 111 | #if !canImport(Darwin) && compiler(>=5.10) && swift(<6.0) && hasFeature(StrictConcurrency) && hasFeature(GlobalConcurrency) 112 | package static nonisolated(unsafe) let versionSuffixAllowedCharacterSet: CharacterSet = { 113 | var validCharset = CharacterSet.alphanumerics 114 | validCharset.insert("-") 115 | return validCharset 116 | }() 117 | #else 118 | package static let versionSuffixAllowedCharacterSet: CharacterSet = { 119 | var validCharset = CharacterSet.alphanumerics 120 | validCharset.insert("-") 121 | return validCharset 122 | }() 123 | #endif 124 | 125 | @inlinable 126 | package static func _isValidIdentifier(_ identifier: some StringProtocol) -> Bool { 127 | !identifier.isEmpty && CharacterSet(charactersIn: String(identifier)).isSubset(of: versionSuffixAllowedCharacterSet) 128 | } 129 | 130 | package static func _joinIdentifiers(_ identifiers: some Sequence) -> String { 131 | identifiers.joined(separator: String(_identifierSeparator)) 132 | } 133 | 134 | package static func _splitIdentifiers(_ identifier: S) -> Array 135 | where S: StringProtocol, S.SubSequence == Substring 136 | { 137 | identifier.split(separator: _identifierSeparator).map(String.init) 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /Tests/SemVerMacrosPluginTests/SemVerMacrosCompilerPluginTests.swift: -------------------------------------------------------------------------------- 1 | #if canImport(SemVerMacrosPlugin) 2 | import XCTest 3 | @testable import SemVerMacrosPlugin 4 | 5 | final class SemVerMacrosCompilerPluginTests: XCTestCase { 6 | func testProvidedMacros() { 7 | let macros = SemVerMacrosCompilerPlugin().providingMacros 8 | XCTAssertEqual(macros.count, 1) 9 | XCTAssertTrue(macros.contains { $0 == VersionMacro.self }) 10 | } 11 | } 12 | #endif 13 | -------------------------------------------------------------------------------- /Tests/SemVerMacrosPluginTests/VersionMacroTests.swift: -------------------------------------------------------------------------------- 1 | #if canImport(SemVerMacrosPlugin) 2 | import XCTest 3 | import SwiftSyntaxMacros 4 | import SwiftSyntaxMacrosTestSupport 5 | import SemVerMacrosPlugin 6 | 7 | final class VersionMacroTests: XCTestCase { 8 | let testMacros: Dictionary = [ 9 | "version": VersionMacro.self, 10 | ] 11 | 12 | func testValidApplications() { 13 | assertMacroExpansion(#"#version("1.2.3")"#, 14 | expandedSource: """ 15 | SemVer.Version( 16 | major: 1, 17 | minor: 2, 18 | patch: 3, 19 | prerelease: [], 20 | metadata: [] 21 | ) 22 | """, 23 | macros: testMacros) 24 | assertMacroExpansion(#"#version("1.2.3-beta.1")"#, 25 | expandedSource: """ 26 | SemVer.Version( 27 | major: 1, 28 | minor: 2, 29 | patch: 3, 30 | prerelease: [SemVer.Version.PrereleaseIdentifier(unchecked: "beta"), SemVer.Version.PrereleaseIdentifier(1)], 31 | metadata: [] 32 | ) 33 | """, 34 | macros: testMacros) 35 | assertMacroExpansion(#"#version("1.2.3-beta.1+annotation.x")"#, 36 | expandedSource: """ 37 | SemVer.Version( 38 | major: 1, 39 | minor: 2, 40 | patch: 3, 41 | prerelease: [SemVer.Version.PrereleaseIdentifier(unchecked: "beta"), SemVer.Version.PrereleaseIdentifier(1)], 42 | metadata: ["annotation", "x"] 43 | ) 44 | """, 45 | macros: testMacros) 46 | } 47 | 48 | func testInvalidApplications() { 49 | assertMacroExpansion(#"#version("1.\(myMinorComponent).3")"#, 50 | expandedSource: #"#version("1.\(myMinorComponent).3")"#, 51 | diagnostics: [ 52 | .init(message: "#version requires a literal string (without any interpolations)!", line: 1, column: 10) 53 | ], 54 | macros: testMacros) 55 | assertMacroExpansion(#"#version("a.b.c")"#, 56 | expandedSource: #"#version("a.b.c")"#, 57 | diagnostics: [ 58 | .init(message: "Invalid version string: 'a.b.c'", line: 1, column: 10) 59 | ], 60 | macros: testMacros) 61 | } 62 | } 63 | #endif 64 | -------------------------------------------------------------------------------- /Tests/SemVerParsingTests/VersionParserTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import SemVerParsing 3 | 4 | // FIXME: Remove this once Swift's tuple's are equatable or parameter packs cover this... 5 | // Parameter packs don't seem to work in this case, since our lhs parameter is a predefined tuple in this case. 6 | // And even the VariadicGenerics experimental flag doesn't cover concrete tuples just yet. 7 | fileprivate func XCTAssertEqual(_ lhs: @autoclosure () -> (T1, T2, T3, T4, T5)?, 8 | _ rhs: @autoclosure () -> (T1, T2, T3, T4, T5)?, 9 | _ message: @autoclosure () -> String = "", 10 | file: StaticString = #filePath, 11 | line: UInt = #line) 12 | where T1: Equatable, T2: Equatable, T3: Equatable, T4: Equatable, T5: Equatable 13 | { 14 | let (lhsComps, rhsComps) = (lhs(), rhs()) 15 | XCTAssertEqual(lhsComps?.0, rhsComps?.0, message(), file: file, line: line) 16 | XCTAssertEqual(lhsComps?.1, rhsComps?.1, message(), file: file, line: line) 17 | XCTAssertEqual(lhsComps?.2, rhsComps?.2, message(), file: file, line: line) 18 | XCTAssertEqual(lhsComps?.3, rhsComps?.3, message(), file: file, line: line) 19 | XCTAssertEqual(lhsComps?.4, rhsComps?.4, message(), file: file, line: line) 20 | } 21 | 22 | #if !canImport(Darwin) 23 | protocol XCTActivity: NSObjectProtocol { 24 | var name: String { get } 25 | } 26 | 27 | class XCTContext: NSObject { 28 | private final class _Activity: NSObject, XCTActivity { 29 | let name: String 30 | 31 | init(name: String) { 32 | self.name = name 33 | } 34 | } 35 | 36 | @MainActor 37 | @preconcurrency 38 | class func runActivity( 39 | named name: String, 40 | block: @MainActor (any XCTActivity) throws -> Result 41 | ) rethrows -> Result { 42 | try block(_Activity(name: name)) 43 | } 44 | } 45 | #endif 46 | 47 | final class VersionParserTests: XCTestCase { 48 | private typealias TestHandlerContext = (parser: (String) -> VersionParser.VersionComponents?, messagePrefix: String) 49 | @MainActor 50 | private static func performTests(with testHandler: @Sendable @MainActor (TestHandlerContext) throws -> ()) rethrows { 51 | try XCTContext.runActivity(named: "Legacy Parsing") { 52 | try testHandler(({ 53 | guard !$0.isEmpty else { return nil } 54 | return VersionParser._parseLegacy($0) 55 | }, $0.name)) 56 | } 57 | try XCTContext.runActivity(named: "Modern Parsing") { 58 | if #available(macOS 13, iOS 16, tvOS 16, watchOS 9, macCatalyst 16, *) { 59 | try testHandler(({ 60 | guard !$0.isEmpty else { return nil } 61 | return VersionParser._parseModern($0) 62 | }, $0.name)) 63 | } 64 | } 65 | try XCTContext.runActivity(named: "Automatic Parsing") { 66 | try testHandler((VersionParser.parseString, $0.name)) 67 | } 68 | } 69 | 70 | func testValidStrings() async { 71 | await Self.performTests { context in 72 | let v1 = context.parser("1.2.3-beta+exp.test") 73 | let v2 = context.parser("1-beta") 74 | let v3 = context.parser("2+exp.test") 75 | let v4 = context.parser("2") 76 | let v5 = context.parser("2+abc-1") 77 | let v6 = context.parser("22.33+abc-1") 78 | let v7 = context.parser("1.1") 79 | let v8 = context.parser("3.0.0") 80 | let v9 = context.parser("1.2.3-alpha.1.-2+exp.test") 81 | 82 | XCTAssertNotNil(v1, context.messagePrefix) 83 | XCTAssertNotNil(v2, context.messagePrefix) 84 | XCTAssertNotNil(v3, context.messagePrefix) 85 | XCTAssertNotNil(v4, context.messagePrefix) 86 | XCTAssertNotNil(v5, context.messagePrefix) 87 | XCTAssertNotNil(v6, context.messagePrefix) 88 | XCTAssertNotNil(v7, context.messagePrefix) 89 | XCTAssertNotNil(v8, context.messagePrefix) 90 | XCTAssertNotNil(v9, context.messagePrefix) 91 | 92 | XCTAssertEqual(v1, (1, 2, 3, [.text("beta")], ["exp", "test"]), context.messagePrefix) 93 | XCTAssertEqual(v2, (1, 0, 0, [.text("beta")], []), context.messagePrefix) 94 | XCTAssertEqual(v3, (2, 0, 0, [], ["exp", "test"]), context.messagePrefix) 95 | XCTAssertEqual(v4, (2, 0, 0, [], []), context.messagePrefix) 96 | XCTAssertEqual(v5, (2, 0, 0, [], ["abc-1"]), context.messagePrefix) 97 | XCTAssertEqual(v6, (22, 33, 0, [], ["abc-1"]), context.messagePrefix) 98 | XCTAssertEqual(v7, (1, 1, 0, [], []), context.messagePrefix) 99 | XCTAssertEqual(v8, (3, 0, 0, [], []), context.messagePrefix) 100 | XCTAssertEqual(v9, (1, 2, 3, [.text("alpha"), .number(1), .number(-2)], ["exp", "test"]), context.messagePrefix) 101 | } 102 | } 103 | 104 | func testInvalidStrings() async { 105 | await Self.performTests { context in 106 | XCTAssertNil(context.parser(""), context.messagePrefix) 107 | XCTAssertNil(context.parser("🥴"), context.messagePrefix) 108 | XCTAssertNil(context.parser("ABC"), context.messagePrefix) 109 | XCTAssertNil(context.parser("-1.2.0"), context.messagePrefix) 110 | XCTAssertNil(context.parser("1.2.3.4"), context.messagePrefix) 111 | XCTAssertNil(context.parser("1.2.3."), context.messagePrefix) 112 | XCTAssertNil(context.parser("1.2.3-beta."), context.messagePrefix) 113 | XCTAssertNil(context.parser("1.2.3+exp."), context.messagePrefix) 114 | XCTAssertNil(context.parser("\(UInt.max)"), context.messagePrefix) 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /Tests/SemVerTests/GitHubIssueTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import SemVer 3 | 4 | final class GitHubIssueTests: XCTestCase { 5 | func testGH107() throws { 6 | let _version = Version("1.0.0-beta.11") 7 | XCTAssertNotNil(_version) 8 | let version = try XCTUnwrap(_version) 9 | XCTAssertEqual(version.major, 1) 10 | XCTAssertEqual(version.minor, 0) 11 | XCTAssertEqual(version.patch, 0) 12 | XCTAssertEqual(version.prerelease, ["beta", "11"]) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Tests/SemVerTests/VersionTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import SemVer 3 | import SemVerMacros 4 | 5 | final class VersionTests: XCTestCase { 6 | func testFullVersionString() { 7 | let version = Version(major: 1, minor: 2, patch: 3, prerelease: "beta", metadata: "exp", "test") 8 | XCTAssertEqual(version.versionString(), "1.2.3-beta+exp.test") 9 | } 10 | 11 | func testFullVersionStringWithSuffixWithoutData() { 12 | let version = Version(major: 1, minor: 2, patch: 3) 13 | XCTAssertEqual(version.versionString(), "1.2.3") 14 | } 15 | 16 | func testFullVersionStringWithoutPrereleaseDataWithMetadataData() { 17 | let version = Version(major: 1, minor: 2, patch: 3, metadata: "exp-1", "test") 18 | XCTAssertEqual(version.versionString(), "1.2.3+exp-1.test") 19 | } 20 | 21 | func testFullVersionStringWithPrereleaseDataWithoutMetadataData() { 22 | let version = Version(major: 1, minor: 2, patch: 3, prerelease: "beta-1") 23 | XCTAssertEqual(version.versionString(), "1.2.3-beta-1") 24 | } 25 | 26 | func testVersionStringExcludingPrerelease() { 27 | let version = Version(major: 1, minor: 2, patch: 3, prerelease: "beta", metadata: "exp", "test") 28 | XCTAssertEqual(version.versionString(formattedWith: .includeMetadata), "1.2.3+exp.test") 29 | } 30 | 31 | func testVersionStringExcludingMetadata() { 32 | let version = Version(major: 1, minor: 2, patch: 3, prerelease: "beta", metadata: "exp", "test") 33 | XCTAssertEqual(version.versionString(formattedWith: .includePrerelease), "1.2.3-beta") 34 | } 35 | 36 | func testVersionStringWhenDroppingZeros() { 37 | let version1 = Version(major: 1, minor: 0, patch: 0) 38 | let version2 = Version(major: 2, minor: 0, patch: 1) 39 | let version3 = Version(major: 3, minor: 1, patch: 0) 40 | 41 | XCTAssertEqual(version1.versionString(formattedWith: [.fullVersion, .dropTrailingZeros]), "1") 42 | XCTAssertEqual(version1.versionString(formattedWith: [.fullVersion, .dropPatchIfZero]), "1.0") 43 | XCTAssertEqual(version1.versionString(formattedWith: [.fullVersion, .dropMinorIfZero]), "1.0.0") 44 | XCTAssertEqual(version2.versionString(formattedWith: [.fullVersion, .dropTrailingZeros]), "2.0.1") 45 | XCTAssertEqual(version2.versionString(formattedWith: [.fullVersion, .dropPatchIfZero]), "2.0.1") 46 | XCTAssertEqual(version2.versionString(formattedWith: [.fullVersion, .dropMinorIfZero]), "2.0.1") 47 | XCTAssertEqual(version3.versionString(formattedWith: [.fullVersion, .dropTrailingZeros]), "3.1") 48 | XCTAssertEqual(version3.versionString(formattedWith: [.fullVersion, .dropPatchIfZero]), "3.1") 49 | XCTAssertEqual(version3.versionString(formattedWith: [.fullVersion, .dropMinorIfZero]), "3.1.0") 50 | } 51 | 52 | func testDescriptionIsEqualToFullVersionString() { 53 | let version = Version(major: 1, minor: 2, patch: 3, prerelease: "beta", metadata: "exp", "test") 54 | XCTAssertEqual(String(describing: version), version.versionString()) 55 | } 56 | 57 | func testVersionEqualityWithBasicVersion() { 58 | let v1 = Version(major: 1, minor: 2, patch: 3) 59 | let v2 = Version(major: 1, minor: 2, patch: 3) 60 | let v3 = Version(major: 2, minor: 0, patch: 0) 61 | 62 | XCTAssertEqual(v1, v2) 63 | XCTAssertNotEqual(v1, v3) 64 | XCTAssertNotEqual(v2, v3) 65 | } 66 | 67 | func testVersionEqualityWithMetadataDifference() { 68 | let v1 = Version(major: 1, minor: 2, patch: 3, prerelease: "beta", metadata: "exp", "test") 69 | let v2 = Version(major: 1, minor: 2, patch: 3, prerelease: "beta") 70 | let v3 = Version(major: 1, minor: 2, patch: 3, prerelease: "beta2") 71 | 72 | XCTAssertEqual(v1, v2) 73 | XCTAssertNotEqual(v1, v3) 74 | XCTAssertNotEqual(v2, v3) 75 | } 76 | 77 | func testVersionIdenticalCheck() { 78 | let v1 = Version(major: 1, minor: 2, patch: 3, prerelease: "beta", metadata: "exp", "test") 79 | let v2 = Version(major: 1, minor: 2, patch: 3, prerelease: "beta") 80 | let v3 = Version(major: 1, minor: 2, patch: 3, prerelease: "beta", metadata: "test", "exp") 81 | let v4 = Version(major: 1, minor: 2, patch: 3, prerelease: "beta", metadata: "exp2") 82 | let v5 = Version(major: 1, minor: 3) 83 | 84 | XCTAssertTrue(v1.isIdentical(to: v1)) 85 | XCTAssertTrue(v1.isIdentical(to: v1, requireIdenticalMetadataOrdering: true)) 86 | XCTAssertFalse(v1.isIdentical(to: v2)) 87 | XCTAssertTrue(v1.isIdentical(to: v3)) 88 | XCTAssertFalse(v1.isIdentical(to: v3, requireIdenticalMetadataOrdering: true)) 89 | XCTAssertFalse(v1.isIdentical(to: v4)) 90 | XCTAssertFalse(v1.isIdentical(to: v5)) 91 | XCTAssertFalse(v1.isIdentical(to: v5, requireIdenticalMetadataOrdering: true)) 92 | } 93 | 94 | func testVersionComparisonWithBasicVersion() { 95 | let v0 = Version(major: 0, patch: 1) 96 | let v1 = Version(major: 1, minor: 2, patch: 3) 97 | let v2 = Version(major: 1, minor: 2, patch: 4) 98 | let v3 = Version(major: 2, minor: 0, patch: 0) 99 | let v3b = Version(major: 2, minor: 0, patch: 0, prerelease: "beta") 100 | let v3be = Version(major: 2, minor: 0, patch: 0, prerelease: "beta", metadata: "ext") 101 | let v4 = Version(major: 4) 102 | let v4b1 = Version(major: 4, prerelease: "beta1") 103 | let v4b2 = Version(major: 4, prerelease: "beta2") 104 | 105 | XCTAssertLessThan(v0, v1) 106 | XCTAssertLessThan(v1, v2) 107 | XCTAssertLessThan(v2, v3) 108 | XCTAssertLessThan(v3b, v3) 109 | XCTAssertLessThan(v3be, v3) 110 | XCTAssertLessThan(v4b1, v4b2) 111 | XCTAssertLessThan(v4b1, v4) 112 | XCTAssertLessThan(v4b2, v4) 113 | 114 | XCTAssertGreaterThan(v3, v0) 115 | XCTAssertGreaterThan(v3, v1) 116 | XCTAssertGreaterThan(v3, v2) 117 | XCTAssertGreaterThan(v3, v3b) 118 | XCTAssertGreaterThan(v3, v3be) 119 | XCTAssertGreaterThan(v4b2, v4b1) 120 | XCTAssertGreaterThan(v4, v4b1) 121 | XCTAssertGreaterThan(v4, v4b2) 122 | } 123 | 124 | func testVersionComparisonUsingOperators() { 125 | let v123 = Version(major: 1, minor: 2, patch: 3) 126 | let v124 = Version(major: 1, minor: 2, patch: 4) 127 | let v123Alpha = Version(major: 1, minor: 2, patch: 3, prerelease: "alpha") 128 | let v123Beta = Version(major: 1, minor: 2, patch: 3, prerelease: "beta") 129 | let v1AlphaBeta1 = Version(major: 1, prerelease: "alpha", "beta", 1) 130 | let v1Alpha1Beta = Version(major: 1, prerelease: "alpha", 1, "beta") 131 | 132 | XCTAssertFalse(v123 < v123) 133 | XCTAssertFalse(v123Alpha < v123Alpha) 134 | XCTAssertFalse(v123Beta < v123Beta) 135 | 136 | XCTAssertFalse(v123 > v123) 137 | XCTAssertFalse(v123Alpha > v123Alpha) 138 | XCTAssertFalse(v123Beta > v123Beta) 139 | 140 | XCTAssertTrue(v123Alpha < v123) 141 | XCTAssertTrue(v123Alpha < v123Beta) 142 | XCTAssertTrue(v123Alpha < v124) 143 | XCTAssertTrue(v123Beta < v123) 144 | XCTAssertTrue(v123Beta < v124) 145 | XCTAssertTrue(v123 < v124) 146 | 147 | XCTAssertFalse(v123 < v123Alpha) 148 | XCTAssertFalse(v123Beta < v123Alpha) 149 | XCTAssertFalse(v124 < v123Alpha) 150 | XCTAssertFalse(v123 < v123Beta) 151 | XCTAssertFalse(v124 < v123Beta) 152 | XCTAssertFalse(v124 < v123) 153 | 154 | XCTAssertFalse(v123Alpha > v123) 155 | XCTAssertFalse(v123Alpha > v123Beta) 156 | XCTAssertFalse(v123Alpha > v124) 157 | XCTAssertFalse(v123Beta > v123) 158 | XCTAssertFalse(v123Beta > v124) 159 | XCTAssertFalse(v123 > v124) 160 | 161 | XCTAssertTrue(v123 > v123Alpha) 162 | XCTAssertTrue(v123Beta > v123Alpha) 163 | XCTAssertTrue(v124 > v123Alpha) 164 | XCTAssertTrue(v123 > v123Beta) 165 | XCTAssertTrue(v124 > v123Beta) 166 | XCTAssertTrue(v124 > v123) 167 | 168 | XCTAssertFalse(v1Alpha1Beta > v1AlphaBeta1) 169 | } 170 | 171 | func testComparisonFromSpec() { 172 | // 1.0.0-alpha < 1.0.0-alpha.1 < 1.0.0-alpha.beta < 1.0.0-beta < 1.0.0-beta.2 < 1.0.0-beta.11 < 1.0.0-rc.1 < 1.0.0 173 | let v1 = Version(major: 1, prerelease: "alpha") 174 | let v2 = Version(major: 1, prerelease: "alpha", "1") 175 | let v3 = Version(major: 1, prerelease: "alpha", "beta") 176 | let v4 = Version(major: 1, prerelease: "beta") 177 | let v5 = Version(major: 1, prerelease: "beta", "2") 178 | let v6 = Version(major: 1, prerelease: "beta", "11") 179 | let v7 = Version(major: 1, prerelease: "rc", "1") 180 | let v8 = Version(major: 1) 181 | 182 | XCTAssertTrue(v1 < v2) 183 | XCTAssertTrue(v2 < v3) 184 | XCTAssertTrue(v3 < v4) 185 | XCTAssertTrue(v4 < v5) 186 | XCTAssertTrue(v5 < v6) 187 | XCTAssertTrue(v6 < v7) 188 | XCTAssertTrue(v7 < v8) 189 | XCTAssertTrue(v8 > v1) 190 | } 191 | 192 | func testLosslessStringConvertible() { 193 | let v1 = Version(major: 1, minor: 2, patch: 3, prerelease: "beta", metadata: "exp", "test") 194 | let v2 = Version(major: 1, prerelease: "beta") 195 | let v3 = Version(major: 2, metadata: "exp", "test") 196 | let v4 = Version(major: 2) 197 | let v5 = Version(major: 2, metadata: "abc-1") 198 | let v6 = Version(major: 22, minor: 33, metadata: "abc-1") 199 | let v7 = Version(major: 1, minor: 1) 200 | let v8 = Version(major: 3) 201 | 202 | let v1FromString = Version(String(describing: v1)) 203 | let v2FromString = Version(String(describing: v2)) 204 | let v3FromString = Version(String(describing: v3)) 205 | let v4FromString = Version(String(describing: v4)) 206 | let v5FromString = Version(String(describing: v5)) 207 | let v6FromString = Version(String(describing: v6)) 208 | let v7FromString = Version(v7.versionString(formattedWith: .dropTrailingZeros)) 209 | let v8FromString = Version(v8.versionString(formattedWith: .dropTrailingZeros)) 210 | 211 | XCTAssertNotNil(v1FromString) 212 | XCTAssertNotNil(v2FromString) 213 | XCTAssertNotNil(v3FromString) 214 | XCTAssertNotNil(v4FromString) 215 | XCTAssertNotNil(v5FromString) 216 | XCTAssertNotNil(v6FromString) 217 | XCTAssertNotNil(v7FromString) 218 | XCTAssertNotNil(v8FromString) 219 | 220 | // We need to compare metadata manually here, since it's not considered in equality check 221 | XCTAssertEqual(v1, v1FromString) 222 | XCTAssertEqual(v1.metadata, v1FromString?.metadata) 223 | XCTAssertEqual(v2, v2FromString) 224 | XCTAssertEqual(v2.metadata, v2FromString?.metadata) 225 | XCTAssertEqual(v3, v3FromString) 226 | XCTAssertEqual(v3.metadata, v3FromString?.metadata) 227 | XCTAssertEqual(v4, v4FromString) 228 | XCTAssertEqual(v4.metadata, v4FromString?.metadata) 229 | XCTAssertEqual(v5, v5FromString) 230 | XCTAssertEqual(v5.metadata, v5FromString?.metadata) 231 | XCTAssertEqual(v6, v6FromString) 232 | XCTAssertEqual(v6.metadata, v6FromString?.metadata) 233 | XCTAssertEqual(v7, v7FromString) 234 | XCTAssertEqual(v7.metadata, v7FromString?.metadata) 235 | XCTAssertEqual(v8, v8FromString) 236 | XCTAssertEqual(v8.metadata, v8FromString?.metadata) 237 | } 238 | 239 | func testCustomDebugStringRepresentable() { 240 | let v1 = Version(major: 1) 241 | let v2 = Version(major: 1, minor: 2) 242 | let v3 = Version(major: 1, minor: 2, patch: 3) 243 | let v4 = Version(major: 1, minor: 2, patch: 3, prerelease: "beta", metadata: "exp", "test") 244 | let v5 = Version(major: 1, prerelease: "beta") 245 | let v6 = Version(major: 1, metadata: "exp", "test") 246 | 247 | XCTAssertNotNil(v1.debugDescription, "Version(major: 1, minor: 0, patch: 0, prerelease: \"\", metadata: \"\")") 248 | XCTAssertNotNil(v2.debugDescription, "Version(major: 1, minor: 2, patch: 0, prerelease: \"\", metadata: \"\")") 249 | XCTAssertNotNil(v3.debugDescription, "Version(major: 1, minor: 2, patch: 3, prerelease: \"\", metadata: \"\")") 250 | XCTAssertNotNil(v4.debugDescription, "Version(major: 1, minor: 2, patch: 3, prerelease: \"beta\", metadata: \"exp+test\")") 251 | XCTAssertNotNil(v5.debugDescription, "Version(major: 1, minor: 0, patch: 0, prerelease: \"beta\", metadata: \"\")") 252 | XCTAssertNotNil(v6.debugDescription, "Version(major: 1, minor: 0, patch: 0, prerelease: \"\", metadata: \"exp+test\")") 253 | } 254 | 255 | func testHashable() { 256 | let v1 = Version(major: 1, minor: 2, patch: 3, prerelease: ["beta"], metadata: "exp", "test") 257 | let v2 = Version(major: 1, minor: 2, patch: 3, prerelease: ["beta"]) 258 | let v3 = Version(major: 3) 259 | 260 | let v1Hash: Int = { 261 | var hasher = Hasher() 262 | hasher.combine(v1.major) 263 | hasher.combine(v1.minor) 264 | hasher.combine(v1.patch) 265 | hasher.combine(v1.prerelease) 266 | return hasher.finalize() 267 | }() 268 | let v2Hash: Int = { 269 | var hasher = Hasher() 270 | hasher.combine(v2.major) 271 | hasher.combine(v2.minor) 272 | hasher.combine(v2.patch) 273 | hasher.combine(v2.prerelease) 274 | return hasher.finalize() 275 | }() 276 | let v3Hash: Int = { 277 | var hasher = Hasher() 278 | hasher.combine(v3.major) 279 | hasher.combine(v3.minor) 280 | hasher.combine(v3.patch) 281 | hasher.combine(v3.prerelease) 282 | return hasher.finalize() 283 | }() 284 | 285 | XCTAssertEqual(v1.hashValue, v1Hash) 286 | XCTAssertEqual(v2.hashValue, v2Hash) 287 | XCTAssertEqual(v3.hashValue, v3Hash) 288 | } 289 | 290 | func testModifying() { 291 | var version = Version(major: 1) 292 | version.major = 2 293 | version.minor = 1 294 | version.patch = 3 295 | version.prerelease = ["beta"] 296 | version.metadata = ["yea", "testing", "rocks"] 297 | 298 | let expectedVersion = Version(major: 2, minor: 1, patch: 3, prerelease: ["beta"], metadata: "yea", "testing", "rocks") 299 | XCTAssertEqual(version, expectedVersion) 300 | XCTAssertEqual(version.versionString(formattedWith: .fullVersion), 301 | expectedVersion.versionString(formattedWith: .fullVersion)) 302 | } 303 | 304 | func testInvalidStrings() { 305 | XCTAssertNil(Version("")) 306 | XCTAssertNil(Version("1.2.3.4")) 307 | XCTAssertNil(Version("ABC")) 308 | XCTAssertNil(Version("-1.2.0")) 309 | XCTAssertNil(Version("🥴")) 310 | } 311 | 312 | func testMacro() { 313 | let macroVersion = #version("1.2.3-alpha.1.-2+exp.test") 314 | XCTAssertEqual(macroVersion, Version(major: 1, minor: 2, patch: 3, prerelease: "alpha", 1, -2, metadata: ["exp", "test"])) 315 | } 316 | 317 | /* 318 | func testStringLiteralConversion() { 319 | XCTAssertEqual("1.2.3", Version(major: 1, minor: 2, patch: 3)) 320 | XCTAssertEqual("1.2.3-rc1+exp-1.test", Version(major: 1, minor: 2, patch: 3, prerelease: "rc1", metadata: "exp-1", "test")) 321 | XCTAssertEqual("1.2.3+exp-1.test", Version(major: 1, minor: 2, patch: 3, metadata: "exp-1", "test")) 322 | } 323 | */ 324 | } 325 | -------------------------------------------------------------------------------- /Tests/SemVerTests/Version_AdjustmentTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import SemVer 3 | 4 | final class Version_AdjustmentTests: XCTestCase { 5 | func testVersionNumericPartDescription() { 6 | XCTAssertEqual(String(describing: Version.NumericPart.major), "major") 7 | XCTAssertEqual(String(describing: Version.NumericPart.minor), "minor") 8 | XCTAssertEqual(String(describing: Version.NumericPart.patch), "patch") 9 | } 10 | 11 | func testNextVersion() { 12 | let prerelease: Array = ["beta", 42] 13 | let metadata = ["abc", "def"] 14 | let version = Version(major: 1, minor: 2, patch: 3, prerelease: prerelease, metadata: metadata) 15 | 16 | XCTAssertEqual(version.next(.major), Version(major: 2)) 17 | XCTAssertEqual(version.next(.minor), Version(major: 1, minor: 3)) 18 | XCTAssertEqual(version.next(.patch), Version(major: 1, minor: 2, patch: 4)) 19 | XCTAssertTrue(version.next(.major).metadata.isEmpty) 20 | XCTAssertTrue(version.next(.minor).metadata.isEmpty) 21 | XCTAssertTrue(version.next(.patch).metadata.isEmpty) 22 | XCTAssertEqual(version.next(.major, keepingPrerelease: true), Version(major: 2, prerelease: prerelease)) 23 | XCTAssertEqual(version.next(.minor, keepingPrerelease: true), Version(major: 1, minor: 3, prerelease: prerelease)) 24 | XCTAssertEqual(version.next(.patch, keepingPrerelease: true), Version(major: 1, minor: 2, patch: 4, prerelease: prerelease)) 25 | XCTAssertTrue(version.next(.major, keepingPrerelease: true).metadata.isEmpty) 26 | XCTAssertTrue(version.next(.minor, keepingPrerelease: true).metadata.isEmpty) 27 | XCTAssertTrue(version.next(.patch, keepingPrerelease: true).metadata.isEmpty) 28 | XCTAssertEqual(version.next(.major, keepingMetadata: true), Version(major: 2)) 29 | XCTAssertEqual(version.next(.minor, keepingMetadata: true), Version(major: 1, minor: 3)) 30 | XCTAssertEqual(version.next(.patch, keepingMetadata: true), Version(major: 1, minor: 2, patch: 4)) 31 | XCTAssertEqual(version.next(.major, keepingMetadata: true).metadata, metadata) 32 | XCTAssertEqual(version.next(.minor, keepingMetadata: true).metadata, metadata) 33 | XCTAssertEqual(version.next(.patch, keepingMetadata: true).metadata, metadata) 34 | } 35 | 36 | func testVersionIncrease() { 37 | let prerelease: Array = ["beta", 42] 38 | let metadata = ["abc", "def"] 39 | let version = Version(major: 1, minor: 2, patch: 3, prerelease: prerelease, metadata: metadata) 40 | 41 | var mutatingVersion = version 42 | mutatingVersion.increase(.major) 43 | XCTAssertEqual(mutatingVersion, Version(major: 2)) 44 | XCTAssertTrue(mutatingVersion.metadata.isEmpty) 45 | mutatingVersion = version 46 | mutatingVersion.increase(.minor) 47 | XCTAssertEqual(mutatingVersion, Version(major: 1, minor: 3)) 48 | XCTAssertTrue(mutatingVersion.metadata.isEmpty) 49 | 50 | mutatingVersion = version 51 | mutatingVersion.increase(.patch) 52 | XCTAssertEqual(mutatingVersion, Version(major: 1, minor: 2, patch: 4)) 53 | XCTAssertTrue(mutatingVersion.metadata.isEmpty) 54 | 55 | mutatingVersion = version 56 | mutatingVersion.increase(.major, keepingPrerelease: true) 57 | XCTAssertEqual(mutatingVersion, Version(major: 2, prerelease: prerelease)) 58 | XCTAssertTrue(mutatingVersion.metadata.isEmpty) 59 | 60 | mutatingVersion = version 61 | mutatingVersion.increase(.major, keepingMetadata: true) 62 | XCTAssertEqual(mutatingVersion, Version(major: 2)) 63 | XCTAssertEqual(mutatingVersion.metadata, metadata) 64 | 65 | mutatingVersion = version 66 | mutatingVersion.increase(.minor, keepingMetadata: true) 67 | XCTAssertEqual(mutatingVersion, Version(major: 1, minor: 3)) 68 | XCTAssertEqual(mutatingVersion.metadata, metadata) 69 | 70 | mutatingVersion = version 71 | mutatingVersion.increase(.patch, keepingMetadata: true) 72 | XCTAssertEqual(mutatingVersion, Version(major: 1, minor: 2, patch: 4)) 73 | XCTAssertEqual(mutatingVersion.metadata, metadata) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /Tests/SemVerTests/Version_CodableTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import Foundation 3 | import SemVer 4 | 5 | fileprivate extension Version.EncodingStrategy { 6 | func isComponents(prereleaseAsString expectedPreReleaseIdentifiersAsString: Bool, 7 | metadataAsString expectedMetadataAsString: Bool) -> Bool { 8 | switch self { 9 | case .components(let prereleaseAsString, let metadataAsString): 10 | return prereleaseAsString == expectedPreReleaseIdentifiersAsString && metadataAsString == expectedMetadataAsString 11 | default: return false 12 | } 13 | } 14 | 15 | func isString(with expectedOptions: Version.FormattingOptions) -> Bool { 16 | switch self { 17 | case .string(let options): return expectedOptions == options 18 | default: return false 19 | } 20 | } 21 | } 22 | 23 | fileprivate extension Version.DecodingStrategy { 24 | func isComponents(prereleaseAsString expectedPreReleaseIdentifiersAsString: Bool, 25 | metadataAsString expectedMetadataAsString: Bool) -> Bool { 26 | switch self { 27 | case .components(let prereleaseAsString, let metadataAsString): 28 | return prereleaseAsString == expectedPreReleaseIdentifiersAsString && metadataAsString == expectedMetadataAsString 29 | default: return false 30 | } 31 | } 32 | 33 | var isString: Bool { 34 | switch self { 35 | case .string: return true 36 | default: return false 37 | } 38 | } 39 | } 40 | 41 | final class Version_CodableTests: XCTestCase { 42 | func testFoundationCodersVersionStrategyExtensions() { 43 | let jsonEncoder = JSONEncoder() 44 | let jsonDecoder = JSONDecoder() 45 | let plistEncoder = PropertyListEncoder() 46 | let plistDecoder = PropertyListDecoder() 47 | 48 | XCTAssertTrue(jsonEncoder.semverVersionEncodingStrategy.isComponents(prereleaseAsString: true, metadataAsString: false)) 49 | XCTAssertTrue(jsonDecoder.semverVersionDecodingStrategy.isComponents(prereleaseAsString: true, metadataAsString: false)) 50 | XCTAssertTrue(plistEncoder.semverVersionEncodingStrategy.isComponents(prereleaseAsString: true, metadataAsString: false)) 51 | XCTAssertTrue(plistDecoder.semverVersionDecodingStrategy.isComponents(prereleaseAsString: true, metadataAsString: false)) 52 | 53 | jsonEncoder.semverVersionEncodingStrategy = .string 54 | jsonDecoder.semverVersionDecodingStrategy = .string 55 | plistEncoder.semverVersionEncodingStrategy = .string 56 | plistDecoder.semverVersionDecodingStrategy = .string 57 | 58 | XCTAssertTrue(jsonEncoder.semverVersionEncodingStrategy.isString(with: .fullVersion)) 59 | XCTAssertTrue(jsonDecoder.semverVersionDecodingStrategy.isString) 60 | XCTAssertTrue(plistEncoder.semverVersionEncodingStrategy.isString(with: .fullVersion)) 61 | XCTAssertTrue(plistDecoder.semverVersionDecodingStrategy.isString) 62 | 63 | } 64 | 65 | func testEncodingWithDefaultStrategy() throws { 66 | let jsonEncoder = JSONEncoder() 67 | jsonEncoder.outputFormatting = .sortedKeys // stable comparison 68 | 69 | let version = Version(major: 1, minor: 2, patch: 3, prerelease: "beta", metadata: "exp", "test") 70 | 71 | let json = try jsonEncoder.encode(version) 72 | 73 | XCTAssertEqual(String(decoding: json, as: UTF8.self), 74 | #"{"major":1,"metadata":["exp","test"],"minor":2,"patch":3,"prerelease":"beta"}"#) 75 | } 76 | 77 | func testDecodingWithDefaultStrategy() throws { 78 | let jsonDecoder = JSONDecoder() 79 | 80 | let fullJson = Data(#"{"major":1,"minor":2,"patch":3,"prerelease":"beta","metadata":["exp","test"]}"#.utf8) 81 | let minJson = Data(#"{"major":1}"#.utf8) 82 | 83 | let fullVersion = try jsonDecoder.decode(Version.self, from: fullJson) 84 | let minVersion = try jsonDecoder.decode(Version.self, from: minJson) 85 | 86 | XCTAssertEqual(fullVersion.major, 1) 87 | XCTAssertEqual(fullVersion.minor, 2) 88 | XCTAssertEqual(fullVersion.patch, 3) 89 | XCTAssertEqual(fullVersion.prerelease, ["beta"]) 90 | XCTAssertEqual(fullVersion.metadata, ["exp", "test"]) 91 | 92 | XCTAssertEqual(minVersion.major, 1) 93 | XCTAssertEqual(minVersion.minor, 0) 94 | XCTAssertEqual(minVersion.patch, 0) 95 | XCTAssertTrue(minVersion.prerelease.isEmpty) 96 | XCTAssertTrue(minVersion.metadata.isEmpty) 97 | } 98 | 99 | func testEncodingAsComponents() throws { 100 | let version = Version(major: 1, minor: 2, patch: 3, prerelease: "beta", metadata: "exp", "test") 101 | 102 | let jsonEncoder = JSONEncoder() 103 | jsonEncoder.outputFormatting = .sortedKeys // stable comparison 104 | 105 | jsonEncoder.semverVersionEncodingStrategy = .components 106 | let json1 = try jsonEncoder.encode(version) 107 | 108 | jsonEncoder.semverVersionEncodingStrategy = .components(prereleaseAsString: false, metadataAsString: false) 109 | let json2 = try jsonEncoder.encode(version) 110 | 111 | jsonEncoder.semverVersionEncodingStrategy = .components(prereleaseAsString: false, metadataAsString: true) 112 | let json3 = try jsonEncoder.encode(version) 113 | 114 | jsonEncoder.semverVersionEncodingStrategy = .components(prereleaseAsString: true, metadataAsString: true) 115 | let json4 = try jsonEncoder.encode(version) 116 | 117 | jsonEncoder.semverVersionEncodingStrategy = .components(prereleaseAsString: true, metadataAsString: false) 118 | let json5 = try jsonEncoder.encode(version) 119 | 120 | XCTAssertEqual(String(decoding: json1, as: UTF8.self), 121 | #"{"major":1,"metadata":["exp","test"],"minor":2,"patch":3,"prerelease":"beta"}"#) 122 | XCTAssertEqual(String(decoding: json2, as: UTF8.self), 123 | #"{"major":1,"metadata":["exp","test"],"minor":2,"patch":3,"prerelease":["beta"]}"#) 124 | XCTAssertEqual(String(decoding: json3, as: UTF8.self), 125 | #"{"major":1,"metadata":"exp.test","minor":2,"patch":3,"prerelease":["beta"]}"#) 126 | XCTAssertEqual(String(decoding: json4, as: UTF8.self), 127 | #"{"major":1,"metadata":"exp.test","minor":2,"patch":3,"prerelease":"beta"}"#) 128 | XCTAssertEqual(String(decoding: json5, as: UTF8.self), 129 | #"{"major":1,"metadata":["exp","test"],"minor":2,"patch":3,"prerelease":"beta"}"#) 130 | } 131 | 132 | func testDecodingAsComponents() throws { 133 | let jsonDecoder = JSONDecoder() 134 | jsonDecoder.semverVersionDecodingStrategy = .components 135 | 136 | let fullJson = Data(#"{"major":1,"minor":2,"patch":3,"prerelease":"beta","metadata":["exp","test"]}"#.utf8) 137 | let minJson = Data(#"{"major":1}"#.utf8) 138 | 139 | let fullVersion = try jsonDecoder.decode(Version.self, from: fullJson) 140 | let minVersion = try jsonDecoder.decode(Version.self, from: minJson) 141 | 142 | XCTAssertEqual(fullVersion.major, 1) 143 | XCTAssertEqual(fullVersion.minor, 2) 144 | XCTAssertEqual(fullVersion.patch, 3) 145 | XCTAssertEqual(fullVersion.prerelease, ["beta"]) 146 | XCTAssertEqual(fullVersion.metadata, ["exp", "test"]) 147 | 148 | XCTAssertEqual(minVersion.major, 1) 149 | XCTAssertEqual(minVersion.minor, 0) 150 | XCTAssertEqual(minVersion.patch, 0) 151 | XCTAssertTrue(minVersion.prerelease.isEmpty) 152 | XCTAssertTrue(minVersion.metadata.isEmpty) 153 | } 154 | 155 | func testDecodingAsComponentsWithNonStrings() throws { 156 | let jsonDecoder = JSONDecoder() 157 | jsonDecoder.semverVersionDecodingStrategy = .components(prereleaseAsString: false, metadataAsString: false) 158 | 159 | let fullJson = Data(#"{"major":1,"minor":2,"patch":3,"prerelease":["beta"],"metadata":["exp","test"]}"#.utf8) 160 | let minJson = Data(#"{"major":1}"#.utf8) 161 | 162 | let fullVersion = try jsonDecoder.decode(Version.self, from: fullJson) 163 | let minVersion = try jsonDecoder.decode(Version.self, from: minJson) 164 | 165 | XCTAssertEqual(fullVersion.major, 1) 166 | XCTAssertEqual(fullVersion.minor, 2) 167 | XCTAssertEqual(fullVersion.patch, 3) 168 | XCTAssertEqual(fullVersion.prerelease, ["beta"]) 169 | XCTAssertEqual(fullVersion.metadata, ["exp", "test"]) 170 | 171 | XCTAssertEqual(minVersion.major, 1) 172 | XCTAssertEqual(minVersion.minor, 0) 173 | XCTAssertEqual(minVersion.patch, 0) 174 | XCTAssertTrue(minVersion.prerelease.isEmpty) 175 | XCTAssertTrue(minVersion.metadata.isEmpty) 176 | } 177 | 178 | func testDecodingAsComponentsWithAllStrings() throws { 179 | let jsonDecoder = JSONDecoder() 180 | jsonDecoder.semverVersionDecodingStrategy = .components(prereleaseAsString: true, metadataAsString: true) 181 | 182 | let fullJson = Data(#"{"major":1,"minor":2,"patch":3,"prerelease":"beta","metadata":"exp.test"}"#.utf8) 183 | let minJson = Data(#"{"major":1}"#.utf8) 184 | 185 | let fullVersion = try jsonDecoder.decode(Version.self, from: fullJson) 186 | let minVersion = try jsonDecoder.decode(Version.self, from: minJson) 187 | 188 | XCTAssertEqual(fullVersion.major, 1) 189 | XCTAssertEqual(fullVersion.minor, 2) 190 | XCTAssertEqual(fullVersion.patch, 3) 191 | XCTAssertEqual(fullVersion.prerelease, ["beta"]) 192 | XCTAssertEqual(fullVersion.metadata, ["exp", "test"]) 193 | 194 | XCTAssertEqual(minVersion.major, 1) 195 | XCTAssertEqual(minVersion.minor, 0) 196 | XCTAssertEqual(minVersion.patch, 0) 197 | XCTAssertTrue(minVersion.prerelease.isEmpty) 198 | XCTAssertTrue(minVersion.metadata.isEmpty) 199 | } 200 | 201 | func testEncodingAsString() throws { 202 | let jsonEncoder = JSONEncoder() 203 | jsonEncoder.outputFormatting = .sortedKeys // stable comparison 204 | jsonEncoder.semverVersionEncodingStrategy = .string(.fullVersion) 205 | 206 | let version = Version(major: 1, minor: 2, patch: 3, prerelease: "beta", metadata: "exp", "test") 207 | 208 | let json = try jsonEncoder.encode(["version": version]) 209 | 210 | XCTAssertEqual(String(decoding: json, as: UTF8.self), #"{"version":"1.2.3-beta+exp.test"}"#) 211 | } 212 | 213 | func testDecodingAsString() throws { 214 | let jsonDecoder = JSONDecoder() 215 | jsonDecoder.semverVersionDecodingStrategy = .string 216 | 217 | let validJson = Data(#"{"version":"1.2.3-beta+exp.test"}"#.utf8) 218 | let invalidJSON = Data(#"{"version":"not-valid"}"#.utf8) 219 | 220 | let versionDict = try jsonDecoder.decode(Dictionary.self, from: validJson) 221 | let version = try XCTUnwrap(versionDict["version"]) 222 | 223 | XCTAssertEqual(version.major, 1) 224 | XCTAssertEqual(version.minor, 2) 225 | XCTAssertEqual(version.patch, 3) 226 | XCTAssertEqual(version.prerelease, ["beta"]) 227 | XCTAssertEqual(version.metadata, ["exp", "test"]) 228 | 229 | XCTAssertThrowsError(try jsonDecoder.decode(Dictionary.self, from: invalidJSON)) 230 | } 231 | 232 | func testCustomEncoding() throws { 233 | let jsonEncoder = JSONEncoder() 234 | jsonEncoder.outputFormatting = .sortedKeys // stable comparison 235 | jsonEncoder.semverVersionEncodingStrategy = .custom { 236 | var container = $1.singleValueContainer() 237 | try container.encode("\($0.major)-\($0.minor)-\($0.patch)-\($0.prerelease.lazy.map(\.string).joined(separator: "&"))-\($0.metadata.joined(separator: "#"))") 238 | } 239 | 240 | let version = Version(major: 1, minor: 2, patch: 3, prerelease: "beta", metadata: "exp", "test") 241 | 242 | let json = try jsonEncoder.encode(["version": version]) 243 | 244 | XCTAssertEqual(String(decoding: json, as: UTF8.self), #"{"version":"1-2-3-beta-exp#test"}"#) 245 | } 246 | 247 | func testCustomDecoding() throws { 248 | let jsonDecoder = JSONDecoder() 249 | jsonDecoder.semverVersionDecodingStrategy = .custom { 250 | let major = try $0.singleValueContainer().decode(Int.self) 251 | return .init(major: major) 252 | } 253 | 254 | let json = Data(#"{"version":2}"#.utf8) 255 | let versionDict = try jsonDecoder.decode(Dictionary.self, from: json) 256 | let version = try XCTUnwrap(versionDict["version"]) 257 | 258 | XCTAssertEqual(version.major, 2) 259 | XCTAssertEqual(version.minor, 0) 260 | XCTAssertEqual(version.patch, 0) 261 | XCTAssertTrue(version.prerelease.isEmpty) 262 | XCTAssertTrue(version.metadata.isEmpty) 263 | } 264 | 265 | func testInvalidDecoding() throws { 266 | let invalidJSON1 = Data(#"{"major":-1,"minor":2,"patch":3,"prerelease":"beta","metadata":["exp","test"]}"#.utf8) 267 | let invalidJSON2 = Data(#"{"major":1,"minor":-2,"patch":3,"prerelease":"beta","metadata":["exp","test"]}"#.utf8) 268 | let invalidJSON3 = Data(#"{"major":1,"minor":2,"patch":-3,"prerelease":"beta","metadata":["exp","test"]}"#.utf8) 269 | let invalidJSON4 = Data(#"{"major":1,"minor":2,"patch":3,"prerelease":"bet@","metadata":["exp","test"]}"#.utf8) 270 | let invalidJSON5 = Data(#"{"major":1,"minor":2,"patch":3,"prerelease":"beta","metadata":["exp","t%st"]}"#.utf8) 271 | let invalidJSON6 = Data(#"{"major":1,"minor":2,"patch":3,"prerelease":["bet@"],"metadata":["exp","test"]}"#.utf8) 272 | let jsonDecoder = JSONDecoder() 273 | 274 | XCTAssertThrowsError(try jsonDecoder.decode(Version.self, from: invalidJSON1)) { 275 | XCTAssertTrue($0 is DecodingError) 276 | guard case DecodingError.dataCorrupted(let context) = $0 277 | else { XCTFail("Invalid error: \($0)"); return } 278 | XCTAssertEqual(context.debugDescription, "Invalid major version component: -1") 279 | } 280 | XCTAssertThrowsError(try jsonDecoder.decode(Version.self, from: invalidJSON2)) { 281 | XCTAssertTrue($0 is DecodingError) 282 | guard case DecodingError.dataCorrupted(let context) = $0 283 | else { XCTFail("Invalid error: \($0)"); return } 284 | XCTAssertEqual(context.debugDescription, "Invalid minor version component: -2") 285 | } 286 | XCTAssertThrowsError(try jsonDecoder.decode(Version.self, from: invalidJSON3)) { 287 | XCTAssertTrue($0 is DecodingError) 288 | guard case DecodingError.dataCorrupted(let context) = $0 289 | else { XCTFail("Invalid error: \($0)"); return } 290 | XCTAssertEqual(context.debugDescription, "Invalid patch version component: -3") 291 | } 292 | XCTAssertThrowsError(try jsonDecoder.decode(Version.self, from: invalidJSON4)) { 293 | XCTAssertTrue($0 is DecodingError) 294 | guard case DecodingError.dataCorrupted(let context) = $0 295 | else { XCTFail("Invalid error: \($0)"); return } 296 | XCTAssertEqual(context.debugDescription, #"Invalid prerelease: ["bet@"]"#) 297 | } 298 | XCTAssertThrowsError(try jsonDecoder.decode(Version.self, from: invalidJSON5)) { 299 | XCTAssertTrue($0 is DecodingError) 300 | guard case DecodingError.dataCorrupted(let context) = $0 301 | else { XCTFail("Invalid error: \($0)"); return } 302 | XCTAssertEqual(context.debugDescription, #"Invalid metadata: ["exp", "t%st"]"#) 303 | } 304 | jsonDecoder.semverVersionDecodingStrategy = .components(prereleaseAsString: false, metadataAsString: false) 305 | XCTAssertThrowsError(try jsonDecoder.decode(Version.self, from: invalidJSON6)) { 306 | XCTAssertTrue($0 is DecodingError) 307 | guard case DecodingError.dataCorrupted(let context) = $0 308 | else { XCTFail("Invalid error: \($0)"); return } 309 | XCTAssertEqual(context.debugDescription, #"Invalid prerelease identifier: "bet@""#) 310 | } 311 | } 312 | } 313 | -------------------------------------------------------------------------------- /Tests/SemVerTests/Version_PrereleaseIdentifierTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | import SemVerParsing 3 | @testable import SemVer 4 | 5 | final class Version_PrereleaseIdentifierTests: XCTestCase { 6 | func testDescription() { 7 | let prereleaseIdentifierText = Version.PrereleaseIdentifier.string("test") 8 | let prereleaseIdentifierNumber = Version.PrereleaseIdentifier.number(1) 9 | 10 | XCTAssertEqual(prereleaseIdentifierText.description, prereleaseIdentifierText.string) 11 | XCTAssertEqual(prereleaseIdentifierNumber.description, prereleaseIdentifierNumber.string) 12 | } 13 | 14 | func testDebugDescription() { 15 | let prereleaseIdentifierText = Version.PrereleaseIdentifier.string("test") 16 | let prereleaseIdentifierNumber = Version.PrereleaseIdentifier.number(1) 17 | 18 | XCTAssertEqual(prereleaseIdentifierText.debugDescription, "text(\(prereleaseIdentifierText.string))") 19 | XCTAssertEqual(prereleaseIdentifierNumber.debugDescription, "number(\(prereleaseIdentifierNumber.string))") 20 | } 21 | 22 | func testString() { 23 | let prereleaseIdentifierText = Version.PrereleaseIdentifier.string("test") 24 | let prereleaseIdentifierNumber = Version.PrereleaseIdentifier.number(1) 25 | 26 | XCTAssertEqual(prereleaseIdentifierText.string, "test") 27 | XCTAssertEqual(prereleaseIdentifierNumber.string, "1") 28 | } 29 | 30 | func testNumber() { 31 | let prereleaseIdentifierText = Version.PrereleaseIdentifier.string("test") 32 | let prereleaseIdentifierNumber = Version.PrereleaseIdentifier.number(1) 33 | 34 | XCTAssertNil(prereleaseIdentifierText.number) 35 | XCTAssertEqual(prereleaseIdentifierNumber.number, 1) 36 | } 37 | 38 | func testCreation() { 39 | let prereleaseIdentifierText = Version.PrereleaseIdentifier.string("test") 40 | let prereleaseIdentifierNumber = Version.PrereleaseIdentifier.number(1) 41 | let prereleaseIdentifierNumberFromText = Version.PrereleaseIdentifier.string("42") 42 | let prereleaseIdentifierUncheckedText = Version.PrereleaseIdentifier(unchecked: "abc") 43 | 44 | XCTAssertEqual(prereleaseIdentifierText._storage, .text("test")) 45 | XCTAssertEqual(prereleaseIdentifierNumber._storage, .number(1)) 46 | XCTAssertEqual(prereleaseIdentifierNumberFromText._storage, .number(42)) 47 | XCTAssertEqual(prereleaseIdentifierUncheckedText._storage, .text("abc")) 48 | } 49 | 50 | func testCreationFromLiterals() { 51 | let prereleaseIdentifierText: Version.PrereleaseIdentifier = "test" 52 | let prereleaseIdentifierNumber: Version.PrereleaseIdentifier = 1 53 | let prereleaseIdentifierNumberFromText: Version.PrereleaseIdentifier = "42" 54 | 55 | XCTAssertEqual(prereleaseIdentifierText._storage, .text("test")) 56 | XCTAssertEqual(prereleaseIdentifierNumber._storage, .number(1)) 57 | XCTAssertEqual(prereleaseIdentifierNumberFromText._storage, .number(42)) 58 | } 59 | 60 | func testComparision() { 61 | let textABC: Version.PrereleaseIdentifier = "abc" 62 | let textDEF: Version.PrereleaseIdentifier = "def" 63 | let num42: Version.PrereleaseIdentifier = 42 64 | let num142: Version.PrereleaseIdentifier = 142 65 | 66 | XCTAssertTrue(textABC < textDEF) 67 | XCTAssertLessThan(textABC, textDEF) 68 | 69 | XCTAssertTrue(num42 < num142) 70 | XCTAssertLessThan(num42, num142) 71 | 72 | XCTAssertTrue(num42 < textABC) 73 | XCTAssertTrue(num42 < textDEF) 74 | XCTAssertTrue(num142 < textABC) 75 | XCTAssertTrue(num142 < textDEF) 76 | XCTAssertLessThan(num42, textABC) 77 | XCTAssertLessThan(num42, textDEF) 78 | XCTAssertLessThan(num142, textABC) 79 | XCTAssertLessThan(num142, textDEF) 80 | 81 | XCTAssertFalse(textABC < num42) 82 | XCTAssertFalse(textDEF < num42) 83 | XCTAssertFalse(textABC < num142) 84 | XCTAssertFalse(textDEF < num142) 85 | XCTAssertGreaterThan(textABC, num42) 86 | XCTAssertGreaterThan(textDEF, num42) 87 | XCTAssertGreaterThan(textABC, num142) 88 | XCTAssertGreaterThan(textDEF, num142) 89 | } 90 | } 91 | --------------------------------------------------------------------------------