1 /** 2 * Authors: Azbuka 3 * License: MIT, see LICENCE.md 4 * Copyright: Azbuka 2016 5 * See_Also: 6 * Semantic Versioning http://semver.org/ 7 */ 8 module BrightProof; 9 10 import std.traits : isImplicitlyConvertible; 11 12 /** 13 * Exception for easy error handling 14 */ 15 class SemVerException : Exception { 16 /** 17 * Params: 18 * msg = message 19 * file = file, where SemVerException have been throwed 20 * line = line number in file 21 * next = next exception 22 */ 23 @safe pure nothrow this(string msg, 24 string file = __FILE__, 25 size_t line = __LINE__, 26 Throwable next = null) { 27 super(msg, file, line, next); 28 } 29 } 30 31 /** 32 * Main struct 33 * Examples: 34 * --- 35 * SemVer("1.0.0"); 36 * SemVer("1.0.0+4444"); 37 * SemVer("1.0.0-eyyyyup"); 38 * SemVer("1.0.0-yay+build"); 39 * --- 40 */ 41 struct SemVer { 42 size_t Major, Minor, Patch; 43 string PreRelease, Build; 44 45 /** 46 * Create new SemVer 47 * Throws: SemVerException if there is any syntax errors. 48 */ 49 @safe @nogc pure nothrow this(size_t major, 50 size_t minor, 51 size_t patch, 52 string prerelease = "", 53 string build = "") { 54 this.Major = major; 55 this.Minor = minor; 56 this.Patch = patch; 57 this.PreRelease = prerelease; 58 this.Build = build; 59 } 60 61 pure this(T)(T i) if(isImplicitlyConvertible!(T, string)) { 62 import std.string : isNumeric; 63 import std.conv : to; 64 65 size_t MajorDot, MinorDot, PreReleaseStart, BuildStart; 66 67 for(size_t count = 0; count < i.length; count++) { 68 switch(i[count]) { 69 case '.': 70 if(!MajorDot) { 71 MajorDot = count; 72 break; 73 } 74 if(!MinorDot) 75 MinorDot = count; 76 break; 77 case '-': 78 if(!BuildStart && !PreReleaseStart) 79 PreReleaseStart = count; 80 break; 81 case '+': 82 BuildStart = count; 83 break; 84 default: break; 85 } 86 } 87 88 if(MajorDot == 0) { 89 // If first symbol is a dot there is no Major. 90 throw new SemVerException("There is no major version number"); 91 } else if(!MinorDot || (MinorDot - MajorDot < 2)) { 92 // If there is nothing between MajorDot and MinorDot. 93 throw new SemVerException("There is no minor version number"); 94 } else if( 95 (!PreReleaseStart && (i.length - MinorDot < 2)) || 96 (!PreReleaseStart && (PreReleaseStart - MinorDot < 2))) { 97 // There is no Patch if nothing follows MinorDot 98 throw new SemVerException("There is no patch version number"); 99 } else if( 100 (!BuildStart && (i.length - PreReleaseStart < 2)) || 101 ((BuildStart > 0) && (BuildStart - PreReleaseStart < 2))) { 102 // PreRelease is empty if nothing follows`-` . 103 throw new SemVerException("There is no prerelease version string"); 104 } else if(i.length - BuildStart < 2) { 105 // Build is empty if nothing follow `+`. 106 throw new SemVerException("There is no build version string"); 107 } 108 109 if(isNumeric(i[0..MajorDot])) { 110 if((MajorDot > 1) && (to!size_t(i[0..1]) == 0)) 111 throw new SemVerException("Major starts with '0'"); 112 113 this.Major = to!size_t(i[0..MajorDot]); 114 } else { 115 throw new SemVerException("There is a non-number character in major"); 116 } 117 118 if(isNumeric(i[MajorDot+1..MinorDot])) { 119 if((MinorDot - MajorDot > 2) && (to!size_t(i[MajorDot+1..MajorDot+2]) == 0)) 120 throw new SemVerException("Minor starts with '0'"); 121 122 this.Minor = to!size_t(i[MajorDot+1..MinorDot]); 123 } else { 124 throw new SemVerException("There is a non-number character in minor"); 125 } 126 127 if(PreReleaseStart) { 128 if(isNumeric(i[MinorDot+1..PreReleaseStart])) { 129 if((PreReleaseStart - MinorDot > 2) && (to!size_t(i[MinorDot+1..MinorDot+2]) == 0)) 130 throw new SemVerException("Patch starts with '0'"); 131 132 this.Patch = to!size_t(i[MinorDot+1..PreReleaseStart]); 133 } else { 134 throw new SemVerException("There is a non-number character in patch"); 135 } 136 if(BuildStart) { 137 this.PreRelease = i[PreReleaseStart+1..BuildStart].to!string; 138 } else { 139 this.PreRelease = i[PreReleaseStart+1..$].to!string; 140 } 141 } else { 142 if(BuildStart) { 143 if(isNumeric(i[MinorDot+1..BuildStart])) { 144 if((BuildStart - MinorDot > 2) && (to!size_t(i[MinorDot+1..MinorDot+2]) == 0)) 145 throw new SemVerException("Patch starts with '0'"); 146 147 this.Patch = to!size_t(i[MinorDot+1..BuildStart]); 148 } else { 149 throw new SemVerException("There is a non-number character in patch"); 150 } 151 this.Build = i[BuildStart+1..$].to!string; 152 } else { 153 if(isNumeric(i[MinorDot+1..$])) { 154 if((i.length - MinorDot > 2) && (to!size_t(i[MinorDot+1..MinorDot+2]) == 0)) 155 throw new SemVerException("Patch starts with '0'"); 156 157 this.Patch = to!size_t(i[MinorDot+1..$]); 158 } else { 159 throw new SemVerException("There is a non-number character in patch"); 160 } 161 } 162 } 163 } 164 165 /** 166 * Next Major/Minor/Patch version 167 * Increments version in semver way 168 * Example: 169 * 1.2.3 -> nextMajor -> 2.0.0 170 * 1.2.3 -> nextMinor -> 1.3.0 171 * 1.2.3 -> nextPatch -> 1.2.4 172 * 1.2.3-rc.1+build.5 -> nextPatch -> 1.2.4 173 */ 174 @safe @nogc pure nothrow SemVer nextMajor() { 175 this.Major++; 176 this.Minor = this.Patch = 0; 177 this.PreRelease = this.Build = ""; 178 return this; 179 } 180 /// ditto 181 @safe @nogc pure nothrow SemVer nextMinor() { 182 this.Minor++; 183 this.Patch = 0; 184 this.PreRelease = this.Build = ""; 185 return this; 186 } 187 /// ditto 188 @safe @nogc pure nothrow SemVer nextPatch() { 189 this.Patch++; 190 this.PreRelease = this.Build = ""; 191 return this; 192 } 193 194 /** 195 * Convert SemVer to string 196 * Returns: SemVer in string (MAJOR.MINOR.PATCH-PRERELEASE+BUILD) 197 */ 198 @safe pure string toString() { 199 import std.array : appender; 200 import std.format : formattedWrite; 201 202 auto writer = appender!string(); 203 writer.formattedWrite("%d.%d.%d", this.Major, this.Minor, this.Patch); 204 if(PreRelease != "") 205 writer.formattedWrite("-%s", this.PreRelease); 206 if(Build != "") 207 writer.formattedWrite("+%s", this.Build); 208 return writer.data; 209 } 210 211 /** 212 * true, if this == b 213 */ 214 @nogc pure nothrow const bool opEquals()(auto ref const SemVer b) { 215 return (this.Major == b.Major) && 216 (this.Minor == b.Minor) && 217 (this.Patch == b.Patch) && 218 (this.PreRelease == b.PreRelease); 219 } 220 221 /** 222 * Compares two SemVer structs. 223 */ 224 const int opCmp(ref const SemVer b) { 225 import natcmp; 226 227 if(this == b) 228 return 0; 229 230 if(this.Major != b.Major) 231 return this.Major < b.Major ? -1 : 1; 232 else if(this.Minor != b.Minor) 233 return this.Minor < b.Minor ? -1 : 1; 234 else if(this.Major != b.Major) 235 return this.Major < b.Major ? -1 : 1; 236 237 if((this.PreRelease != "") && (b.PreRelease != "")) { 238 int result = compareNatural(this.PreRelease, b.PreRelease); 239 if(result) { 240 return result; 241 } 242 } else if(this.PreRelease != "") { 243 return -1; 244 } else if(b.PreRelease != "") { 245 return 1; 246 } 247 248 throw new SemVerException("I don't know, how you got that error: SemVer is not an equal, but also not an different"); 249 } 250 /// ditto 251 const int opCmp(in SemVer b) { 252 return this.opCmp(b); 253 } 254 /// 255 unittest { 256 assert(SemVer("1.0.0-alpha") < SemVer("1.0.0-alpha.1")); 257 assert(SemVer("1.0.0-alpha.1") < SemVer("1.0.0-alpha.beta")); 258 assert(SemVer("1.0.0-alpha.beta") < SemVer("1.0.0-beta")); 259 assert(SemVer("1.0.0-beta") < SemVer("1.0.0-beta.2")); 260 assert(SemVer("1.0.0-beta.2") < SemVer("1.0.0-beta.11")); 261 assert(SemVer("1.0.0-beta.11") < SemVer("1.0.0-rc.1")); 262 assert(SemVer("1.0.0-rc.1") < SemVer("1.0.0")); 263 assert(SemVer("1.0.0-rc.1") == SemVer("1.0.0+build.9")); 264 assert(SemVer("1.0.0-rc.1") == SemVer("1.0.0-rc.1+build.5")); 265 assert(SemVer("1.0.0-rc.1+build.5") == SemVer("1.0.0-rc.1+build.5")); 266 } 267 }