├── .github └── workflows │ └── main.yml ├── .gitignore ├── LICENSE ├── README.md ├── dub.json └── source └── semver.d /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [ push, pull_request ] 3 | 4 | jobs: 5 | build: 6 | name: Run 7 | strategy: 8 | fail-fast: false 9 | matrix: 10 | os: [ ubuntu-latest, macos-latest, windows-latest ] 11 | dc: [ dmd-latest, ldc-latest ] 12 | 13 | runs-on: ${{ matrix.os }} 14 | timeout-minutes: 30 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | with: 19 | # Requiried for codecov action 20 | fetch-depth: 2 21 | 22 | - uses: dlang-community/setup-dlang@v1 23 | with: 24 | compiler: ${{ matrix.dc }} 25 | 26 | - name: 'Build & Test' 27 | shell: bash 28 | run: | 29 | dub test --compiler=$DC -b unittest-cov 30 | 31 | - name: Upload coverage to Codecov 32 | uses: codecov/codecov-action@v1 33 | with: 34 | flags: unittests 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files 2 | *.o 3 | *.obj 4 | 5 | # Compiled Dynamic libraries 6 | *.so 7 | *.dylib 8 | *.dll 9 | 10 | # Compiled Static libraries 11 | *.a 12 | *.lib 13 | 14 | # Executables 15 | *.exe 16 | 17 | # DUB 18 | .dub 19 | docs.json 20 | __dummy.html 21 | docs/ 22 | 23 | # DUB testing artifacts 24 | *-test-library 25 | *-test-application 26 | 27 | # Code coverage 28 | *.lst 29 | 30 | # Emacs 31 | *~ 32 | 33 | # Visual Studio Code 34 | .vscode 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Dragos Carp 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://github.com/dcarp/semver/workflows/CI/badge.svg)](https://github.com/dcarp/semver/actions?workflow=CI) 2 | 3 | semver 4 | ====== 5 | 6 | Semantic Versioning Library 7 | 8 | ## Implementation 9 | 10 | This library parses, validates and compares version numbers and version ranges. 11 | 12 | It uses the following formats: 13 | * Semantic Versioning 2.0.0 - http://semver.org 14 | * Semantic Versioning Range - https://github.com/isaacs/node-semver 15 | 16 | ## Usage 17 | 18 | ```D 19 | auto version1 = SemVer("1.0.0"); 20 | assert(version1.isValid); 21 | assert(version1.isStable); 22 | 23 | auto version2 = SemVer("1.0.0-rc.1"); 24 | assert(version2.isValid); 25 | assert(!version2.isStable); 26 | 27 | assert(SemVer("1.0.0") > SemVer("1.0.0+build.1")); 28 | assert(SemVer("1.0.0").differAt(SemVer("1.0.0+build.1")) == VersionPart.BUILD); 29 | 30 | auto versionRange = SemVerRange(">=1.0.0"); 31 | assert(versionRange.isValid); 32 | 33 | assert(SemVer("1.0.1").satisfies(versionRange)); 34 | assert(SemVer("1.1.0").satisfies(versionRange)); 35 | 36 | auto semVers = [SemVer("1.1.0"), SemVer("1.0.0"), SemVer("0.8.0")]; 37 | assert(semVers.maxSatisfying(SemVerRange("<=1.0.0")) == SemVer("1.0.0")); 38 | assert(semVers.maxSatisfying(SemVerRange(">=1.0")) == SemVer("1.1.0")); 39 | 40 | semVers = [SemVer("1.0.0+build.3"), SemVer("1.0.0+build.1"), SemVer("1.1.0")]; 41 | assert(semVers.maxSatisfying(SemVerRange("<=1.0.0")) == SemVer("1.0.0+build.3")); 42 | assert(semVers.maxSatisfying(SemVerRange(">=1.0")) == SemVer("1.1.0")); 43 | ``` 44 | -------------------------------------------------------------------------------- /dub.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "semver", 3 | "description": "Semantic Versioning Library", 4 | "copyright": "Copyright © 2014-2017 Dragoş Carp", 5 | "license": "MIT", 6 | "authors": ["Dragoş Carp"], 7 | "targetType": "library" 8 | } 9 | -------------------------------------------------------------------------------- /source/semver.d: -------------------------------------------------------------------------------- 1 | /** 2 | * Provide utilities to represent & manipulate versions and version ranges 3 | * following the Semantic Versioning 2.0 specifications. 4 | * 5 | * Semantic versioning define a syntax and a semantic to represent version 6 | * numbers, composed of five `VersionPart`. 7 | * Each difference in `VersionPart` represent a different expectation 8 | * with regards to the scope of the difference between two versions. 9 | * 10 | * `VersionPart.MAJOR` can differ in any way and are not considered to 11 | * be compatible with one another. 12 | * `VersionPart.MINOR` do not contain breaking changes but can contain new features, 13 | * hence newer versions are backward compatible but not forward compatible. 14 | * `VersionPart.PATCH` are both forward and backward compatible. 15 | * `VersionPart.PRERELEASE` are preview versions of their non-prerelease 16 | * counterpart, while `VersionPart.BUILD` are metadata without influence 17 | * on the version matching. 18 | * 19 | * In addition to the `SemVer` type to represent versions, a `SemVerRange` 20 | * type exists to express version constraints. 21 | * 22 | * For more information about SemVer, rules and practice, 23 | * see [the official website](https://semver.org). 24 | * 25 | * License: [MIT](http://opensource.org/licenses/MIT) 26 | * Authors: Dragos Carp 27 | * 28 | * See_Also: 29 | * - [Semantic Versioning 2.0](http://semver.org) 30 | * - [The semantic versioner for npm](https://github.com/isaacs/node-semver) 31 | */ 32 | 33 | module semver; 34 | 35 | import std.algorithm; 36 | import std.range; 37 | import std.regex : matchAll, matchFirst, ctRegex; 38 | 39 | /** 40 | * The different components of a version number. 41 | */ 42 | enum VersionPart 43 | { 44 | /** major number */ 45 | MAJOR, 46 | /** minor number */ 47 | MINOR, 48 | /** patch number */ 49 | PATCH, 50 | /** prerelease suffix */ 51 | PRERELEASE, 52 | /** build suffix */ 53 | BUILD, 54 | } 55 | 56 | /** 57 | * Represent a semantic version number 58 | * 59 | * Semantic versions are usually represented as string as: 60 | * `MAJOR[.MINOR[.PATCH]][-PRERELEASE][+BUILD]`. 61 | * For ease of use, a leading `v` or a leading `=` are also accepted. 62 | * Invalid input to the constructor will not throw, but the version 63 | * will be marked as invalid. This can be checked via `isValid`. 64 | */ 65 | struct SemVer 66 | { 67 | private uint[3] ids; 68 | private string[] prerelease; 69 | private string[] build; 70 | 71 | private bool _isValid; 72 | 73 | private static immutable RegExp = ctRegex!( 74 | `^(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:-([a-zA-Z\d-.]+))?(?:\+([a-zA-Z\d-.]+))?$`); 75 | 76 | /** 77 | * Creates and validates a version number from a string. 78 | * 79 | * If string format is invalid it just sets the $(D_PARAM isValid) property to $(D_KEYWORD false). 80 | */ 81 | this(string semVer) @safe 82 | { 83 | import std.array : array; 84 | import std.conv : to; 85 | 86 | if (semVer.length == 0) 87 | return; 88 | if (semVer[0] == 'v') 89 | semVer = semVer[1 .. $]; 90 | else if (semVer[0] == '=') 91 | semVer = semVer[1 .. $]; 92 | 93 | auto m = semVer.matchAll(RegExp); 94 | if (m.empty) 95 | return; 96 | 97 | foreach (i, ref id; ids) 98 | { 99 | if (!m.captures[i+1].empty) 100 | id = m.captures[i+1].to!uint; 101 | } 102 | 103 | if (!m.captures[4].empty) 104 | { 105 | prerelease = m.captures[4].splitter('.').array; 106 | if (prerelease.any!empty) 107 | return; 108 | } 109 | 110 | if (!m.captures[5].empty) 111 | { 112 | build = m.captures[5].splitter('.').array; 113 | if (build.any!empty) 114 | return; 115 | } 116 | 117 | _isValid = true; 118 | } 119 | 120 | /** 121 | * Creates a 'simple' version with only major, minor, patch components 122 | */ 123 | public this (uint major, uint minor = 0, uint patch = 0) 124 | @safe pure nothrow @nogc 125 | { 126 | this.ids[VersionPart.MAJOR] = major; 127 | this.ids[VersionPart.MINOR] = minor; 128 | this.ids[VersionPart.PATCH] = patch; 129 | this._isValid = true; 130 | } 131 | 132 | /** 133 | * Return the canonical string format. 134 | */ 135 | string toString() const scope @safe 136 | { 137 | import std.string : format; 138 | 139 | if (!_isValid) 140 | return ""; 141 | 142 | string semVer = "%(%s.%)".format(ids); 143 | if (!prerelease.empty) 144 | semVer ~= "-" ~ "%-(%s.%)".format(cast(const char[]) prerelease); 145 | if (!build.empty) 146 | semVer ~= "+" ~ "%-(%s.%)".format(cast(const char[]) build); 147 | return semVer; 148 | } 149 | 150 | /** 151 | * Property that indicates whether this $(D_PSYMBOL SemVer) is valid. 152 | */ 153 | bool isValid() const scope @safe pure nothrow @nogc 154 | { 155 | return _isValid; 156 | } 157 | 158 | /** 159 | * Property that indicates whether this $(D_PSYMBOL SemVer) is stable. 160 | */ 161 | bool isStable() const scope @safe pure nothrow @nogc 162 | { 163 | return prerelease.empty; 164 | } 165 | 166 | /** 167 | * Increment version number. 168 | */ 169 | SemVer increment(VersionPart versionPart) const scope @safe pure nothrow @nogc 170 | in 171 | { 172 | assert(this.isValid); 173 | } 174 | out(result) 175 | { 176 | assert(result.isValid); 177 | } 178 | do 179 | { 180 | SemVer result = SemVer(0); 181 | foreach (i; VersionPart.MAJOR .. versionPart) 182 | result.ids[i] = this.ids[i]; 183 | if (versionPart != VersionPart.PRERELEASE) 184 | result.ids[versionPart] = this.ids[versionPart]+1; 185 | return result; 186 | } 187 | 188 | private SemVer appendPrerelease0() return scope @safe pure nothrow 189 | { 190 | if (prerelease.empty) 191 | prerelease ~= "0"; 192 | return this; 193 | } 194 | 195 | unittest 196 | { 197 | assert(SemVer("1.2.3").increment(VersionPart.MAJOR) == SemVer("2.0.0")); 198 | assert(SemVer("1.2.3").increment(VersionPart.MINOR) == SemVer("1.3.0")); 199 | assert(SemVer("1.2.3-alpha").increment(VersionPart.MINOR) == SemVer("1.3.0")); 200 | assert(SemVer("1.2.3").increment(VersionPart.PATCH) == SemVer("1.2.4")); 201 | assert(SemVer("1.2.3-alpha").increment(VersionPart.PATCH) == SemVer("1.2.4")); 202 | assert(SemVer("1.2.3").increment(VersionPart.PRERELEASE) == SemVer("1.2.3")); 203 | assert(SemVer("1.2.3-alpha").increment(VersionPart.PRERELEASE) == SemVer("1.2.3")); 204 | } 205 | 206 | /** 207 | * Query a Major, Minor and Patch as integers 208 | */ 209 | int query(VersionPart versionPart) const 210 | in 211 | { 212 | assert(this.isValid); 213 | } 214 | do 215 | { 216 | import std.conv : to; 217 | import std.exception : enforce; 218 | int result; 219 | switch (versionPart) 220 | { 221 | case VersionPart.MAJOR: 222 | result = ids[0]; 223 | break; 224 | case VersionPart.MINOR: 225 | result = ids[1]; 226 | break; 227 | case VersionPart.PATCH: 228 | result = ids[2]; 229 | break; 230 | default: 231 | enforce(false, "Can't query " ~ versionPart.to!string ~ " as an integer."); 232 | break; 233 | } 234 | return result; 235 | } 236 | 237 | unittest 238 | { 239 | import std.exception : assertThrown; 240 | assert(SemVer("1.2.3").query(VersionPart.MAJOR) == 1); 241 | assert(SemVer("1.2.3").query(VersionPart.MINOR) == 2); 242 | assert(SemVer("1.2.3").query(VersionPart.PATCH) == 3); 243 | assertThrown(SemVer("1.2.3-alpha").query(VersionPart.BUILD)); 244 | assertThrown(SemVer("1.2.3-alpha+build").query(VersionPart.PRERELEASE)); 245 | } 246 | 247 | /** 248 | * Query a possibly decoded PRERELEASE and BUILD string 249 | */ 250 | string queryAsString(VersionPart versionPart) const 251 | in 252 | { 253 | assert(this.isValid); 254 | } 255 | do 256 | { 257 | import std.conv : to; 258 | import std.exception : enforce; 259 | string result; 260 | switch (versionPart) 261 | { 262 | case VersionPart.MAJOR: 263 | result = ids[0].to!string; 264 | break; 265 | case VersionPart.MINOR: 266 | result = ids[1].to!string; 267 | break; 268 | case VersionPart.PATCH: 269 | result = ids[2].to!string; 270 | break; 271 | case VersionPart.PRERELEASE: 272 | result = prerelease.join("."); 273 | break; 274 | case VersionPart.BUILD: 275 | result = build.join("."); 276 | break; 277 | default: 278 | enforce(false, "Can't query unknown " ~ versionPart.to!string ~ " as a string."); 279 | break; 280 | } 281 | return result; 282 | } 283 | 284 | unittest 285 | { 286 | import std.exception : assertThrown; 287 | assert(SemVer("1.2.3").queryAsString(VersionPart.MAJOR) == "1"); 288 | assert(SemVer("1.2.3").queryAsString(VersionPart.MINOR) == "2"); 289 | assert(SemVer("1.2.3").queryAsString(VersionPart.PATCH) == "3"); 290 | assert(SemVer("1.2.3-alpha-beta.2+build-seq.3").queryAsString(VersionPart.PRERELEASE) == "alpha-beta.2"); 291 | assert(SemVer("1.2.3-alpha-beta.2+build-seq.3").queryAsString(VersionPart.BUILD) == "build-seq.3"); 292 | } 293 | 294 | /** 295 | * Compare this $(D_PSYMBOL SemVer) with the $(D_PARAM other) $(D_PSYMBOL SemVer). 296 | * 297 | * Note that the build parts are considered for this operation. 298 | * Please use $(D_PSYMBOL differAt) to find whether the versions differ only on the build part. 299 | */ 300 | int opCmp(ref const SemVer other) const scope @safe pure 301 | in 302 | { 303 | assert(this.isValid); 304 | assert(other.isValid); 305 | } 306 | do 307 | { 308 | foreach (i; 0..ids.length) 309 | { 310 | if (ids[i] != other.ids[i]) 311 | return ids[i] < other.ids[i] ? -1 : 1; 312 | } 313 | 314 | int compareSufix(scope const string[] suffix, const string[] anotherSuffix) @safe pure 315 | { 316 | import std.conv : to; 317 | import std.string : isNumeric; 318 | 319 | if (!suffix.empty && anotherSuffix.empty) 320 | return -1; 321 | if (suffix.empty && !anotherSuffix.empty) 322 | return 1; 323 | 324 | foreach (a, b; zip(suffix, anotherSuffix)) 325 | { 326 | if (a.isNumeric && b.isNumeric) 327 | { 328 | if (a.to!uint != b.to!uint) 329 | return a.to!uint < b.to!uint ? -1 : 1; 330 | else 331 | continue; 332 | } 333 | if (a != b) 334 | return a < b ? -1 : 1; 335 | } 336 | if (suffix.length != anotherSuffix.length) 337 | return suffix.length < anotherSuffix.length ? -1 : 1; 338 | else 339 | return 0; 340 | } 341 | 342 | auto result = compareSufix(prerelease, other.prerelease); 343 | if (result != 0) 344 | return result; 345 | else 346 | return compareSufix(build, other.build); 347 | } 348 | 349 | /// ditto 350 | int opCmp(const SemVer other) const scope @safe pure 351 | { 352 | return this.opCmp(other); 353 | } 354 | 355 | /** 356 | * Check for equality between this $(D_PSYMBOL SemVer) and the $(D_PARAM other) $(D_PSYMBOL SemVer). 357 | * 358 | * Note that the build parts are considered for this operation. 359 | * Please use $(D_PSYMBOL differAt) to find whether the versions differ only on the build part. 360 | */ 361 | bool opEquals(ref const SemVer other) const scope @safe pure 362 | { 363 | return this.opCmp(other) == 0; 364 | } 365 | 366 | /// ditto 367 | bool opEquals(const SemVer other) const scope @safe pure 368 | { 369 | return this.opEquals(other); 370 | } 371 | 372 | /** 373 | * Compare two $(B different) versions and return the parte they differ on. 374 | */ 375 | VersionPart differAt(ref const SemVer other) const scope @safe pure 376 | in 377 | { 378 | assert(this != other); 379 | } 380 | do 381 | { 382 | foreach (i; VersionPart.MAJOR .. VersionPart.PRERELEASE) 383 | { 384 | if (ids[i] != other.ids[i]) 385 | return i; 386 | } 387 | 388 | if (prerelease != other.prerelease) 389 | return VersionPart.PRERELEASE; 390 | 391 | if (build != other.build) 392 | return VersionPart.BUILD; 393 | 394 | assert(0, "Call 'differAt' for unequal versions only"); 395 | } 396 | 397 | /// ditto 398 | VersionPart differAt(const SemVer other) const scope @safe pure 399 | { 400 | return this.differAt(other); 401 | } 402 | } 403 | 404 | unittest 405 | { 406 | assert(!SemVer().isValid); 407 | assert(!SemVer("1.2-.alpha.32").isValid); 408 | assert(!SemVer("1.2-alpha+").isValid); 409 | assert(!SemVer("1.2-alpha_").isValid); 410 | assert(!SemVer("1.2+32.").isValid); 411 | assert(!SemVer("1.2.5.6").isValid); 412 | assert(!SemVer("").isValid); 413 | assert(SemVer("1").isStable); 414 | assert(SemVer("1.0").isStable); 415 | assert(SemVer("1.0.0").isStable); 416 | assert(SemVer("1.0+build3.").isStable); 417 | assert(SemVer("1.0.0+build.5").isStable); 418 | assert(!SemVer("1.0.0-alpha").isStable); 419 | assert(!SemVer("1.0.0-alpha.1").isStable); 420 | 421 | assert(SemVer("1.0.0-alpha") < SemVer("1.0.0-alpha.1")); 422 | assert(SemVer("1.0.0-alpha.1") < SemVer("1.0.0-alpha.beta")); 423 | assert(SemVer("1.0.0-alpha.beta") < SemVer("1.0.0-beta")); 424 | assert(SemVer("1.0.0-beta") < SemVer("1.0.0-beta.2")); 425 | assert(SemVer("1.0.0-beta.2") < SemVer("1.0.0-beta.11")); 426 | assert(SemVer("1.0.0-beta.11") < SemVer("1.0.0-rc.1")); 427 | assert(SemVer("1.0.0-rc.1") < SemVer("1.0.0")); 428 | assert(SemVer("1.0.0-rc.1") > SemVer("1.0.0-rc.1+build.5")); 429 | assert(SemVer("1.0.0-rc.1+build.5") == SemVer("1.0.0-rc.1+build.5")); 430 | 431 | assert(SemVer("1.0.0").differAt(SemVer("2")) == VersionPart.MAJOR); 432 | assert(SemVer("1.0.0").differAt(SemVer("1.1.1")) == VersionPart.MINOR); 433 | assert(SemVer("1.0.0-rc.1").differAt(SemVer("1.0.1-rc.1")) == VersionPart.PATCH); 434 | assert(SemVer("1.0.0-alpha").differAt(SemVer("1.0.0-beta")) == VersionPart.PRERELEASE); 435 | assert(SemVer("1.0.0-rc.1").differAt(SemVer("1.0.0")) == VersionPart.PRERELEASE); 436 | assert(SemVer("1.0.0-rc.1").differAt(SemVer("1.0.0-rc.1+build.5")) == VersionPart.BUILD); 437 | } 438 | 439 | /** 440 | * Represent a semantic version range [~|~>|^|<|<=|=|>=|>]MAJOR[.MINOR[.PATCH]]. 441 | */ 442 | struct SemVerRange 443 | { 444 | private static immutable RegExp = ctRegex!( 445 | `(~|~>|\^|<|<=|=|>=|>)?[v]?(\d+|\*|X|x)(?:\.(\d+|\*|X|x))?(?:\.(\d+|\*|X|x))?([\S]*)`); 446 | 447 | private static immutable Wildcards = [ "", "*", "X", "x" ]; 448 | 449 | private struct SimpleRange 450 | { 451 | string op; 452 | SemVer semVer; 453 | 454 | string toString() const 455 | { 456 | return op ~ semVer.toString; 457 | } 458 | } 459 | 460 | private SimpleRange[][] ranges; 461 | 462 | invariant() 463 | { 464 | assert(ranges.all!(r => r.all!(r => ["<", "<=", "=", ">=", ">"].canFind(r.op)))); 465 | } 466 | 467 | private bool _isValid; 468 | 469 | /** 470 | * Creates and validates a semantic version range from a string. 471 | * 472 | * If string format is invalid it just sets the $(D_PARAM isValid) property to $(D_KEYWORD false). 473 | */ 474 | this(string semVerRange) 475 | { 476 | import std.exception : enforce; 477 | import std.string : format, strip, stripLeft; 478 | 479 | ranges = [SimpleRange[].init]; 480 | 481 | while (!semVerRange.stripLeft.empty) 482 | { 483 | auto m = semVerRange.matchFirst(RegExp); 484 | if (m.empty) 485 | return; 486 | 487 | auto operator = m.captures[1]; 488 | auto wildcard = wildcardAt([m.captures[2], m.captures[3], m.captures[4]]); 489 | auto expanded = expand([m.captures[2], m.captures[3], m.captures[4], m.captures[5]]); 490 | if (expanded.empty) 491 | return; 492 | 493 | auto semVer = SemVer(expanded); 494 | if (!semVer.isValid) 495 | return; 496 | 497 | switch (m.captures.pre.strip) 498 | { 499 | case "": 500 | break; 501 | case "-": 502 | if (ranges[$-1].empty || ranges[$-1][$-1].op != "=" || 503 | operator != "" || wildcard != VersionPart.PRERELEASE) 504 | return; 505 | ranges[$-1][$-1].op = ">="; 506 | operator = "<="; 507 | break; 508 | case "||": 509 | ranges ~= SimpleRange[].init; 510 | break; 511 | default: 512 | return; 513 | } 514 | 515 | switch (operator) 516 | { 517 | case "": 518 | case "=": 519 | final switch (wildcard) 520 | { 521 | case VersionPart.MAJOR: 522 | assert(semVer == SemVer("0.0.0")); 523 | ranges[$-1] ~= SimpleRange(">=", semVer.appendPrerelease0); 524 | break; 525 | case VersionPart.MINOR: 526 | case VersionPart.PATCH: 527 | ranges[$-1] ~= SimpleRange(">=", semVer.appendPrerelease0); 528 | ranges[$-1] ~= SimpleRange("<", semVer.increment(--wildcard).appendPrerelease0); 529 | break; 530 | case VersionPart.PRERELEASE: 531 | ranges[$-1] ~= SimpleRange("=", semVer); 532 | break; 533 | case VersionPart.BUILD: 534 | assert(0, "Unexpected build part wildcard"); 535 | } 536 | break; 537 | case "<": 538 | ranges[$-1] ~= SimpleRange(operator, semVer.appendPrerelease0); 539 | break; 540 | case "<=": 541 | case ">=": 542 | case ">": 543 | if (wildcard < VersionPart.PRERELEASE) 544 | semVer.appendPrerelease0; 545 | ranges[$-1] ~= SimpleRange(operator, semVer); 546 | break; 547 | case "~": 548 | final switch (wildcard) 549 | { 550 | case VersionPart.MAJOR: 551 | return; 552 | case VersionPart.MINOR: 553 | case VersionPart.PATCH: 554 | --wildcard; 555 | break; 556 | case VersionPart.PRERELEASE: 557 | --wildcard; 558 | --wildcard; 559 | break; 560 | case VersionPart.BUILD: 561 | assert(0, "Unexpected build part wildcard"); 562 | } 563 | ranges[$-1] ~= SimpleRange(">=", semVer.appendPrerelease0); 564 | ranges[$-1] ~= SimpleRange("<", semVer.increment(wildcard).appendPrerelease0); 565 | break; 566 | case "~>": 567 | final switch (wildcard) 568 | { 569 | case VersionPart.MAJOR: 570 | return; 571 | case VersionPart.MINOR: 572 | --wildcard; 573 | break; 574 | case VersionPart.PATCH: 575 | case VersionPart.PRERELEASE: 576 | --wildcard; 577 | --wildcard; 578 | break; 579 | case VersionPart.BUILD: 580 | assert(0, "Unexpected build part wildcard"); 581 | } 582 | ranges[$-1] ~= SimpleRange(">=", semVer.appendPrerelease0); 583 | ranges[$-1] ~= SimpleRange("<", semVer.increment(wildcard).appendPrerelease0); 584 | break; 585 | case "^": 586 | if (wildcard == VersionPart.MAJOR || !semVer.prerelease.empty) 587 | return; 588 | if (semVer.ids[VersionPart.MAJOR] != 0) 589 | { 590 | ranges[$-1] ~= SimpleRange(">=", semVer.appendPrerelease0); 591 | ranges[$-1] ~= SimpleRange("<", semVer.increment(VersionPart.MAJOR).appendPrerelease0); 592 | } 593 | else if (semVer.ids[VersionPart.MINOR] != 0) 594 | { 595 | ranges[$-1] ~= SimpleRange(">=", semVer.appendPrerelease0); 596 | ranges[$-1] ~= SimpleRange("<", semVer.increment(VersionPart.MINOR).appendPrerelease0); 597 | } 598 | else 599 | { 600 | ranges[$-1] ~= SimpleRange(">=", semVer.appendPrerelease0); 601 | ranges[$-1] ~= SimpleRange("<", semVer.increment(VersionPart.PATCH).appendPrerelease0); 602 | } 603 | break; 604 | default: 605 | enforce(false, "Unexpected operator %s".format(operator)); 606 | break; 607 | } 608 | semVerRange = m.captures.post; 609 | } 610 | _isValid = true; 611 | } 612 | 613 | private static VersionPart wildcardAt(string[3] semVer) 614 | { 615 | foreach (i; VersionPart.MAJOR..VersionPart.PRERELEASE) 616 | { 617 | if (Wildcards.canFind(semVer[i])) 618 | return i; 619 | } 620 | return VersionPart.PRERELEASE; 621 | } 622 | 623 | unittest 624 | { 625 | assert(wildcardAt(["*", "", ""]) == VersionPart.MAJOR); 626 | assert(wildcardAt(["X", "", ""]) == VersionPart.MAJOR); 627 | assert(wildcardAt(["1", "", ""]) == VersionPart.MINOR); 628 | assert(wildcardAt(["1", "x", ""]) == VersionPart.MINOR); 629 | assert(wildcardAt(["1", "2", ""]) == VersionPart.PATCH); 630 | assert(wildcardAt(["1", "2", "x"]) == VersionPart.PATCH); 631 | assert(wildcardAt(["1", "2", "3"]) == VersionPart.PRERELEASE); 632 | } 633 | 634 | private static string expand(string[4] semVer) 635 | { 636 | import std.string : format; 637 | 638 | VersionPart wildcard = wildcardAt(semVer[0..3]); 639 | if (wildcard != VersionPart.PRERELEASE) 640 | { 641 | if (semVer[wildcard+1..$].any!((a) => !Wildcards.canFind(a))) 642 | return ""; 643 | foreach (j; wildcard..VersionPart.PRERELEASE) 644 | semVer[j] = "0"; 645 | } 646 | string result = "%-(%s.%)".format(semVer[0..3]); 647 | if (!semVer[3].empty) 648 | result ~= semVer[3]; 649 | return result; 650 | } 651 | 652 | unittest 653 | { 654 | assert(expand(["*", "", "", ""]) == "0.0.0"); 655 | assert(expand(["X", "", "", ""]) == "0.0.0"); 656 | assert(expand(["1", "2", "3", ""]) == "1.2.3"); 657 | assert(expand(["1", "2", "3", "-abc"]) == "1.2.3-abc"); 658 | assert(expand(["1", "2", "", ""]) == "1.2.0"); 659 | assert(expand(["1", "2", "", "-abc"]) == ""); 660 | assert(expand(["1", "2", "x", ""]) == "1.2.0"); 661 | assert(expand(["1", "", "", ""]) == "1.0.0"); 662 | assert(expand(["1", "x", "", ""]) == "1.0.0"); 663 | } 664 | 665 | /** 666 | * Return expanded string representation. 667 | */ 668 | string toString() const 669 | { 670 | import std.string : format; 671 | 672 | if (!_isValid) 673 | return ""; 674 | 675 | return "%(%(%s %) || %)".format(ranges); 676 | } 677 | 678 | /** 679 | * Property that indicates whether this $(D_PSYMBOL SemVerRange) is valid. 680 | */ 681 | bool isValid() const scope @safe pure nothrow @nogc 682 | { 683 | return _isValid; 684 | } 685 | 686 | private static bool simpleRangeSatisfiedBy(SimpleRange simpleRange, SemVer semVer) 687 | in 688 | { 689 | assert(semVer.isValid); 690 | assert(["<", "<=", "=", ">=", ">"].canFind(simpleRange.op)); 691 | assert(simpleRange.semVer.isValid); 692 | } 693 | do 694 | { 695 | semVer.build = null; 696 | 697 | switch (simpleRange.op) 698 | { 699 | case "<": 700 | return semVer < simpleRange.semVer; 701 | case "<=": 702 | return semVer <= simpleRange.semVer; 703 | case "=": 704 | return semVer == simpleRange.semVer; 705 | case ">=": 706 | return semVer >= simpleRange.semVer; 707 | case ">": 708 | return semVer > simpleRange.semVer; 709 | default: 710 | return false; 711 | } 712 | } 713 | 714 | /** 715 | * Check if the $(D_PSYMBOL SemVer) $(D_PARAM semVer) satisfies this $(D_PSYMBOL SemVerRange). 716 | */ 717 | bool satisfiedBy(SemVer semVer) 718 | in 719 | { 720 | assert(semVer.isValid); 721 | assert(isValid); 722 | } 723 | do 724 | { 725 | return ranges.any!(r => r.all!(s => simpleRangeSatisfiedBy(s, semVer))); 726 | } 727 | 728 | } 729 | 730 | /** 731 | * Check if the $(D_PSYMBOL SemVer) $(D_PARAM semVer) satisfies $(LREF SemVerRange) $(D_PARAM semVerRange). 732 | */ 733 | bool satisfies(SemVer semVer, SemVerRange semVerRange) 734 | { 735 | return semVerRange.satisfiedBy(semVer); 736 | } 737 | 738 | /** 739 | * Return the latest $(D_PSYMBOL Semver) from $(D_PARAM semVers) array that satisfies 740 | * $(D_PARAM semVerRange) $(D_PSYMBOL SemVerRange). 741 | */ 742 | SemVer maxSatisfying(SemVer[] semVers, SemVerRange semVerRange) 743 | in 744 | { 745 | assert(semVers.all!"a.isValid"); 746 | assert(semVerRange.isValid); 747 | } 748 | do 749 | { 750 | auto found = semVers.sort!"a > b".find!(a => satisfies(a, semVerRange)); 751 | return found.empty ? SemVer("invalid") : found[0]; 752 | } 753 | 754 | unittest 755 | { 756 | assert(!SemVerRange().isValid); 757 | assert(SemVerRange("1.x || >=2.5.0 || 5.0.0 - 7.2.3").isValid); 758 | assert(!SemVerRange("blerg").isValid); 759 | assert(!SemVerRange("git+https://user:password0123@github.com/foo").isValid); 760 | 761 | assert(SemVer("1.2.3").satisfies(SemVerRange("1.x || >=2.5.0 || 5.0.0 - 7.2.3"))); 762 | 763 | assert(SemVer("1.2.3").satisfies(SemVerRange("1.0.0 - 2.0.0"))); 764 | assert(SemVer("1.0.0").satisfies(SemVerRange("1.0.0"))); 765 | assert(SemVer("1.0.0+build.5").satisfies(SemVerRange("1.0.0"))); 766 | assert(SemVer("0.2.4").satisfies(SemVerRange(">=*"))); 767 | assert(SemVer("1.2.3").satisfies(SemVerRange("*"))); 768 | assert(SemVer("v1.2.3-foo").satisfies(SemVerRange("*"))); 769 | assert(SemVer("1.0.0").satisfies(SemVerRange(">=1.0.0"))); 770 | assert(SemVer("1.0.1").satisfies(SemVerRange(">=1.0.0"))); 771 | assert(SemVer("1.1.0").satisfies(SemVerRange(">=1.0.0"))); 772 | assert(SemVer("1.0.1").satisfies(SemVerRange(">1.0.0"))); 773 | assert(SemVer("1.1.0").satisfies(SemVerRange(">1.0.0"))); 774 | assert(SemVer("2.0.0").satisfies(SemVerRange("<=2.0.0"))); 775 | assert(SemVer("1.9999.9999").satisfies(SemVerRange("<=2.0.0"))); 776 | assert(SemVer("0.2.9").satisfies(SemVerRange("<=2.0.0"))); 777 | assert(SemVer("1.9999.9999").satisfies(SemVerRange("<2.0.0"))); 778 | assert(SemVer("0.2.9").satisfies(SemVerRange("<2.0.0"))); 779 | assert(SemVer("1.0.0").satisfies(SemVerRange(">=1.0.0"))); 780 | assert(SemVer("1.0.1").satisfies(SemVerRange(">=1.0.0"))); 781 | assert(SemVer("1.1.0").satisfies(SemVerRange(">=1.0.0"))); 782 | assert(SemVer("1.0.1").satisfies(SemVerRange(">1.0.0"))); 783 | assert(SemVer("1.1.0").satisfies(SemVerRange(">1.0.0"))); 784 | assert(SemVer("2.0.0").satisfies(SemVerRange("<=2.0.0"))); 785 | assert(SemVer("1.9999.9999").satisfies(SemVerRange("<=2.0.0"))); 786 | assert(SemVer("0.2.9").satisfies(SemVerRange("<=2.0.0"))); 787 | assert(SemVer("1.9999.9999").satisfies(SemVerRange("<2.0.0"))); 788 | assert(SemVer("0.2.9").satisfies(SemVerRange("<2.0.0"))); 789 | assert(SemVer("v0.1.97").satisfies(SemVerRange(">=0.1.97"))); 790 | assert(SemVer("0.1.97").satisfies(SemVerRange(">=0.1.97"))); 791 | assert(SemVer("1.2.4").satisfies(SemVerRange("0.1.20 || 1.2.4"))); 792 | assert(SemVer("0.0.0").satisfies(SemVerRange(">=0.2.3 || <0.0.1"))); 793 | assert(SemVer("0.2.3").satisfies(SemVerRange(">=0.2.3 || <0.0.1"))); 794 | assert(SemVer("0.2.4").satisfies(SemVerRange(">=0.2.3 || <0.0.1"))); 795 | assert(SemVer("2.1.3").satisfies(SemVerRange("2.x.x"))); 796 | assert(SemVer("1.2.3").satisfies(SemVerRange("1.2.x"))); 797 | assert(SemVer("2.1.3").satisfies(SemVerRange("1.2.x || 2.x"))); 798 | assert(SemVer("1.2.3").satisfies(SemVerRange("1.2.x || 2.x"))); 799 | assert(SemVer("1.2.3").satisfies(SemVerRange("x"))); 800 | assert(SemVer("2.1.3").satisfies(SemVerRange("2.*.*"))); 801 | assert(SemVer("1.2.3").satisfies(SemVerRange("1.2.*"))); 802 | assert(SemVer("2.1.3").satisfies(SemVerRange("1.2.* || 2.*"))); 803 | assert(SemVer("1.2.3").satisfies(SemVerRange("1.2.* || 2.*"))); 804 | assert(SemVer("1.2.3").satisfies(SemVerRange("*"))); 805 | assert(SemVer("2.1.2").satisfies(SemVerRange("2"))); 806 | assert(SemVer("2.3.1").satisfies(SemVerRange("2.3"))); 807 | assert(SemVer("2.4.0").satisfies(SemVerRange("~2.4"))); 808 | assert(SemVer("2.4.5").satisfies(SemVerRange("~2.4"))); 809 | assert(SemVer("3.2.2").satisfies(SemVerRange("~>3.2.1"))); 810 | assert(SemVer("1.2.3").satisfies(SemVerRange("~1"))); 811 | assert(SemVer("1.2.3").satisfies(SemVerRange("~>1"))); 812 | assert(SemVer("1.0.2").satisfies(SemVerRange("~1.0"))); 813 | assert(SemVer("1.0.12").satisfies(SemVerRange("~1.0.3"))); 814 | assert(SemVer("1.0.0").satisfies(SemVerRange(">=1"))); 815 | assert(SemVer("1.1.1").satisfies(SemVerRange("<1.2"))); 816 | assert(SemVer("1.1.9").satisfies(SemVerRange("<=1.2"))); 817 | assert(SemVer("1.0.0-bet").satisfies(SemVerRange("1"))); 818 | assert(SemVer("0.5.5").satisfies(SemVerRange("~v0.5.4-pre"))); 819 | assert(SemVer("0.5.4").satisfies(SemVerRange("~v0.5.4-pre"))); 820 | assert(SemVer("0.7.2").satisfies(SemVerRange("=0.7.x"))); 821 | assert(SemVer("0.7.2").satisfies(SemVerRange(">=0.7.x"))); 822 | assert(SemVer("0.7.0-asdf").satisfies(SemVerRange("=0.7.x"))); 823 | assert(SemVer("0.7.0-asdf").satisfies(SemVerRange(">=0.7.x"))); 824 | assert(SemVer("0.6.2").satisfies(SemVerRange("<=0.7.x"))); 825 | assert(SemVer("1.2.3").satisfies(SemVerRange("~1.2.1 >=1.2.3"))); 826 | assert(SemVer("1.2.3").satisfies(SemVerRange("~1.2.1 =1.2.3"))); 827 | assert(SemVer("1.2.3").satisfies(SemVerRange("~1.2.1 1.2.3"))); 828 | assert(SemVer("1.2.3").satisfies(SemVerRange("~1.2.1 >=1.2.3 1.2.3"))); 829 | assert(SemVer("1.2.3").satisfies(SemVerRange("~1.2.1 1.2.3 >=1.2.3"))); 830 | assert(SemVer("1.2.3").satisfies(SemVerRange("~1.2.1 1.2.3"))); 831 | assert(SemVer("1.2.3").satisfies(SemVerRange(">=1.2.1 1.2.3"))); 832 | assert(SemVer("1.2.3").satisfies(SemVerRange("1.2.3 >=1.2.1"))); 833 | assert(SemVer("1.2.3").satisfies(SemVerRange(">=1.2.3 >=1.2.1"))); 834 | assert(SemVer("1.2.3").satisfies(SemVerRange(">=1.2.1 >=1.2.3"))); 835 | assert(SemVer("1.2.3-beta").satisfies(SemVerRange("<=1.2.3"))); 836 | assert(SemVer("1.3.0-beta").satisfies(SemVerRange(">1.2"))); 837 | assert(SemVer("1.2.8").satisfies(SemVerRange(">=1.2"))); 838 | assert(SemVer("1.8.1").satisfies(SemVerRange("^1.2.3"))); 839 | assert(SemVer("1.2.3-beta").satisfies(SemVerRange("^1.2.3"))); 840 | assert(SemVer("0.1.2").satisfies(SemVerRange("^0.1.2"))); 841 | assert(SemVer("0.1.2").satisfies(SemVerRange("^0.1"))); 842 | assert(SemVer("1.4.2").satisfies(SemVerRange("^1.2"))); 843 | assert(SemVer("1.4.2").satisfies(SemVerRange("^1.2 ^1"))); 844 | assert(SemVer("1.2.0-pre").satisfies(SemVerRange("^1.2"))); 845 | assert(SemVer("1.2.3-pre").satisfies(SemVerRange("^1.2.3"))); 846 | 847 | assert(!SemVer("2.2.3").satisfies(SemVerRange("1.0.0 - 2.0.0"))); 848 | assert(!SemVer("1.0.1").satisfies(SemVerRange("1.0.0"))); 849 | assert(!SemVer("0.0.0").satisfies(SemVerRange(">=1.0.0"))); 850 | assert(!SemVer("0.0.1").satisfies(SemVerRange(">=1.0.0"))); 851 | assert(!SemVer("0.1.0").satisfies(SemVerRange(">=1.0.0"))); 852 | assert(!SemVer("0.0.1").satisfies(SemVerRange(">1.0.0"))); 853 | assert(!SemVer("0.1.0").satisfies(SemVerRange(">1.0.0"))); 854 | assert(!SemVer("3.0.0").satisfies(SemVerRange("<=2.0.0"))); 855 | assert(!SemVer("2.9999.9999").satisfies(SemVerRange("<=2.0.0"))); 856 | assert(!SemVer("2.2.9").satisfies(SemVerRange("<=2.0.0"))); 857 | assert(!SemVer("2.9999.9999").satisfies(SemVerRange("<2.0.0"))); 858 | assert(!SemVer("2.2.9").satisfies(SemVerRange("<2.0.0"))); 859 | assert(!SemVer("v0.1.93").satisfies(SemVerRange(">=0.1.97"))); 860 | assert(!SemVer("0.1.93").satisfies(SemVerRange(">=0.1.97"))); 861 | assert(!SemVer("1.2.3").satisfies(SemVerRange("0.1.20 || 1.2.4"))); 862 | assert(!SemVer("0.0.3").satisfies(SemVerRange(">=0.2.3 || <0.0.1"))); 863 | assert(!SemVer("0.2.2").satisfies(SemVerRange(">=0.2.3 || <0.0.1"))); 864 | assert(!SemVer("1.1.3").satisfies(SemVerRange("2.x.x"))); 865 | assert(!SemVer("3.1.3").satisfies(SemVerRange("2.x.x"))); 866 | assert(!SemVer("1.3.3").satisfies(SemVerRange("1.2.x"))); 867 | assert(!SemVer("3.1.3").satisfies(SemVerRange("1.2.x || 2.x"))); 868 | assert(!SemVer("1.1.3").satisfies(SemVerRange("1.2.x || 2.x"))); 869 | assert(!SemVer("1.1.3").satisfies(SemVerRange("2.*.*"))); 870 | assert(!SemVer("3.1.3").satisfies(SemVerRange("2.*.*"))); 871 | assert(!SemVer("1.3.3").satisfies(SemVerRange("1.2.*"))); 872 | assert(!SemVer("3.1.3").satisfies(SemVerRange("1.2.* || 2.*"))); 873 | assert(!SemVer("1.1.3").satisfies(SemVerRange("1.2.* || 2.*"))); 874 | assert(!SemVer("1.1.2").satisfies(SemVerRange("2"))); 875 | assert(!SemVer("2.4.1").satisfies(SemVerRange("2.3"))); 876 | assert(!SemVer("2.5.0").satisfies(SemVerRange("~2.4"))); 877 | assert(!SemVer("2.3.9").satisfies(SemVerRange("~2.4"))); 878 | assert(!SemVer("3.3.2").satisfies(SemVerRange("~>3.2.1"))); 879 | assert(!SemVer("3.2.0").satisfies(SemVerRange("~>3.2.1"))); 880 | assert(!SemVer("0.2.3").satisfies(SemVerRange("~1"))); 881 | assert(!SemVer("2.2.3").satisfies(SemVerRange("~>1"))); 882 | assert(!SemVer("1.1.0").satisfies(SemVerRange("~1.0"))); 883 | assert(!SemVer("1.0.0").satisfies(SemVerRange("<1"))); 884 | assert(!SemVer("1.1.1").satisfies(SemVerRange(">=1.2"))); 885 | assert(!SemVer("1.3.0").satisfies(SemVerRange("<=1.2"))); 886 | assert(!SemVer("2.0.0-beta").satisfies(SemVerRange("1"))); 887 | assert(!SemVer("0.5.4-alpha").satisfies(SemVerRange("~v0.5.4-beta"))); 888 | assert(!SemVer("1.0.0-beta").satisfies(SemVerRange("<1"))); 889 | assert(!SemVer("0.8.2").satisfies(SemVerRange("=0.7.x"))); 890 | assert(!SemVer("0.6.2").satisfies(SemVerRange(">=0.7.x"))); 891 | assert(!SemVer("0.7.2").satisfies(SemVerRange("<=0.7.x"))); 892 | assert(!SemVer("1.2.3-beta").satisfies(SemVerRange("<1.2.3"))); 893 | assert(!SemVer("1.2.3-beta").satisfies(SemVerRange("=1.2.3"))); 894 | assert(!SemVer("1.2.8").satisfies(SemVerRange(">1.3"))); 895 | assert(!SemVer("2.0.0-alpha").satisfies(SemVerRange("^1.2.3"))); 896 | assert(!SemVer("1.2.2").satisfies(SemVerRange("^1.2.3"))); 897 | assert(!SemVer("1.1.9").satisfies(SemVerRange("^1.2"))); 898 | assert(!SemVer("2.0.0-pre").satisfies(SemVerRange("^1.2.3"))); 899 | 900 | auto semVers = [SemVer("1.1.0"), SemVer("1.0.0"), SemVer("0.8.0")]; 901 | assert(semVers.maxSatisfying(SemVerRange("<=1.0.0")) == SemVer("1.0.0")); 902 | assert(semVers.maxSatisfying(SemVerRange(">=1.0")) == SemVer("1.1.0")); 903 | 904 | semVers = [SemVer("1.0.0+build.3"), SemVer("1.0.0+build.1"), SemVer("1.1.0")]; 905 | assert(semVers.maxSatisfying(SemVerRange("<=1.0.0")) == SemVer("1.0.0+build.3")); 906 | assert(semVers.maxSatisfying(SemVerRange(">=1.0")) == SemVer("1.1.0")); 907 | } 908 | --------------------------------------------------------------------------------