1 /++ Semantic Versioning. 2 TODO: Make `Major`, `Minor` and `Patch` sub-types when D gets implicit conversion in argument passing. 3 See_Also: https://docs.rs/semver/latest/semver/ 4 +/ 5 module nxt.semver; 6 7 import nxt.result : Result; 8 9 @safe: 10 11 /++ Semantic version number. 12 +/ 13 struct Version { 14 @safe pure nothrow @nogc: 15 this(Major major, Minor minor = 0, Patch patch = 0, Prerelease pre = Prerelease.init, BuildMetadata build = BuildMetadata.init) { 16 _major = major; 17 _minor = minor; 18 _patch = patch; 19 _pre = pre; 20 _build = build; 21 } 22 @property: 23 Major major() const scope => _major; 24 Minor minor() const scope => _minor; 25 Patch patch() const scope => _patch; 26 Prerelease pre() const scope => _pre; 27 BuildMetadata build() const scope => _build; 28 private: 29 Major _major; 30 Minor _minor; 31 Patch _patch; 32 Prerelease _pre; 33 BuildMetadata _build; 34 } 35 36 /++ Major part. +/ 37 alias Major = VersionPart; 38 39 /++ Minor part. +/ 40 alias Minor = VersionPart; 41 42 /++ Patch part. +/ 43 alias Patch = VersionPart; 44 45 /++ Parts of semantic version numbers. +/ 46 alias VersionPart = uint; 47 48 /++ Prerelease. 49 See_also: https://docs.rs/semver/latest/semver/struct.Prerelease.html 50 +/ 51 struct Prerelease { 52 // TODO: 53 // string str; 54 } 55 56 /++ Build metadata. 57 See_also: https://docs.rs/semver/latest/semver/struct.BuildMetadata.html 58 +/ 59 struct BuildMetadata { 60 // TODO: 61 } 62 63 /// 64 @safe pure nothrow @nogc unittest { 65 assert(Version(1) == Version(1)); 66 assert(Version(1,0) == Version(1,0)); 67 assert(Version(0,0,0) == Version(0,0,0)); 68 assert(Version(0,0,0) != Version(0,0,1)); 69 assert(Version(1,0,0).major == Version(1,0,0).major); 70 assert(Version(1,0,0).minor == Version(1,0,0).minor); 71 assert(Version(1,0,0).patch == Version(1,0,0).patch); 72 assert(Version(1,0,0).pre == Version(1,0,0).pre); 73 assert(Version(1,0,0).build == Version(1,0,0).build); 74 } 75 76 /++ Parse `s` as a semantic version number. 77 78 Semantic versions are usually represented as string as: 79 `MAJOR[.MINOR[.PATCH]][-PRERELEASE][+BUILD]`. 80 81 For ease of use, a leading `v` or a leading `=` are also accepted. 82 83 See_Also: https://docs.rs/semver/latest/semver/struct.Version.html 84 +/ 85 Result!Version tryParseVersion(scope const(char)[] s) pure nothrow @nogc { 86 import nxt.algorithm.searching : findSplit; 87 import nxt.conv : tryParse; 88 89 alias R = typeof(return); 90 Version semver; 91 92 if (s.length == 0) 93 return R.invalid; 94 95 if (s[0] == 'v' || s[0] == '=') // skip leading {'v'|'='} 96 s = s[1 .. $]; 97 98 // major 99 if (auto sp = s.findSplit('.')) { 100 if (const hit = sp.pre.tryParse!Major) 101 semver._major = hit.value; 102 else 103 return R.invalid; 104 () @trusted { s = sp.post; }(); // TODO: -dip1000 should allow this 105 } else 106 return R.invalid; 107 108 // minor 109 if (auto sp = s.findSplit('.')) { 110 if (const hit = sp.pre.tryParse!Minor) 111 semver._minor = hit.value; 112 else 113 return R.invalid; 114 () @trusted { s = sp.post; }(); // TODO: -dip1000 should allow this 115 } else 116 return R.invalid; 117 118 // patch 119 if (s.length == 0) 120 return R.invalid; 121 122 if (const hit = s.tryParse!Patch) 123 semver._patch = hit.value; 124 else 125 return R.invalid; 126 127 return R(semver); 128 } 129 130 /// 131 @safe pure nothrow @nogc unittest { 132 assert(*tryParseVersion("0.0.0") == Version(0,0,0)); 133 assert(*tryParseVersion("0.0.1") == Version(0,0,1)); 134 assert(*tryParseVersion("0.1.1") == Version(0,1,1)); 135 assert(*tryParseVersion("1.1.1") == Version(1,1,1)); 136 assert(!tryParseVersion("")); 137 assert(!tryParseVersion("").isValid); 138 assert(!tryParseVersion("_").isValid); 139 assert(!tryParseVersion("").isValid); 140 assert(!tryParseVersion("1").isValid); 141 assert(!tryParseVersion("_").isValid); 142 assert(!tryParseVersion("1.").isValid); 143 assert(!tryParseVersion("1._").isValid); 144 assert(!tryParseVersion("1.1.").isValid); 145 assert(!tryParseVersion("1.1").isValid); 146 assert(!tryParseVersion("1.1_1").isValid); 147 assert(!tryParseVersion("1-1-1").isValid); 148 assert(!tryParseVersion("_._.__").isValid); 149 assert(!tryParseVersion("1._.__").isValid); 150 assert(!tryParseVersion("1.1.__").isValid); 151 assert(*tryParseVersion("v1.1.1") == Version(1,1,1)); 152 assert(*tryParseVersion("=1.1.1") == Version(1,1,1)); 153 }