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