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 : isNarrowString; 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 * Constructor 47 * Params: 48 * i = input string 49 * Throws: SemVerException if there is any syntax errors. 50 */ 51 pure this(T)(T i) 52 if(isNarrowString!T){ 53 import std.string : isNumeric; 54 import std.conv : to; 55 56 size_t MajorDot, MinorDot, PreReleaseStart, BuildStart; 57 58 for(size_t count = 0; count < i.length; count++) { 59 switch(i[count]) { 60 case '.': 61 if(!MajorDot) { 62 MajorDot = count; 63 break; 64 } 65 if(!MinorDot) 66 MinorDot = count; 67 break; 68 case '-': 69 if(!BuildStart && !PreReleaseStart) 70 PreReleaseStart = count; 71 break; 72 case '+': 73 BuildStart = count; 74 break; 75 default: break; 76 } 77 } 78 79 if(MajorDot == 0) { 80 // If first symbol is a dot there is no Major. 81 throw new SemVerException("There is no major version number"); 82 } else if(!MinorDot || (MinorDot - MajorDot < 2)) { 83 // If there is nothing between MajorDot and MinorDot. 84 throw new SemVerException("There is no minor version number"); 85 } else if( 86 (!PreReleaseStart && (i.length - MinorDot < 2)) || 87 (!PreReleaseStart && (PreReleaseStart - MinorDot < 2))) { 88 // There is no Patch if nothing follows MinorDot 89 throw new SemVerException("There is no patch version number"); 90 } else if( 91 (!BuildStart && (i.length - PreReleaseStart < 2)) || 92 ((BuildStart > 0) && (BuildStart - PreReleaseStart < 2))) { 93 // PreRelease is empty if nothing follows`-` . 94 throw new SemVerException("There is no prerelease version string"); 95 } else if(i.length - BuildStart < 2) { 96 // Build is empty if nothing follow `+`. 97 throw new SemVerException("There is no build version string"); 98 } 99 100 if(isNumeric(i[0..MajorDot])) { 101 if((MajorDot > 1) && (to!size_t(i[0..1]) == 0)) 102 throw new SemVerException("Major starts with '0'"); 103 104 this.Major = to!size_t(i[0..MajorDot]); 105 } else { 106 throw new SemVerException("There is a non-number character in major"); 107 } 108 109 if(isNumeric(i[MajorDot+1..MinorDot])) { 110 if((MinorDot - MajorDot > 2) && (to!size_t(i[MajorDot+1..MajorDot+2]) == 0)) 111 throw new SemVerException("Minor starts with '0'"); 112 113 this.Minor = to!size_t(i[MajorDot+1..MinorDot]); 114 } else { 115 throw new SemVerException("There is a non-number character in minor"); 116 } 117 118 if(PreReleaseStart) { 119 if(isNumeric(i[MinorDot+1..PreReleaseStart])) { 120 if((PreReleaseStart - MinorDot > 2) && (to!size_t(i[MinorDot+1..MinorDot+2]) == 0)) 121 throw new SemVerException("Patch starts with '0'"); 122 123 this.Patch = to!size_t(i[MinorDot+1..PreReleaseStart]); 124 } else { 125 throw new SemVerException("There is a non-number character in patch"); 126 } 127 if(BuildStart) { 128 this.PreRelease = i[PreReleaseStart+1..BuildStart]; 129 } else { 130 this.PreRelease = i[PreReleaseStart+1..$]; 131 } 132 } else { 133 if(BuildStart) { 134 if(isNumeric(i[MinorDot+1..BuildStart])) { 135 if((BuildStart - MinorDot > 2) && (to!size_t(i[MinorDot+1..MinorDot+2]) == 0)) 136 throw new SemVerException("Patch starts with '0'"); 137 138 this.Patch = to!size_t(i[MinorDot+1..BuildStart]); 139 } else { 140 throw new SemVerException("There is a non-number character in patch"); 141 } 142 this.Build = i[BuildStart+1..$]; 143 } else { 144 if(isNumeric(i[MinorDot+1..$])) { 145 if((i.length - MinorDot > 2) && (to!size_t(i[MinorDot+1..MinorDot+2]) == 0)) 146 throw new SemVerException("Patch starts with '0'"); 147 148 this.Patch = to!size_t(i[MinorDot+1..$]); 149 } else { 150 throw new SemVerException("There is a non-number character in patch"); 151 } 152 } 153 } 154 } 155 156 /** 157 * Next Major/Minor/Patch version 158 * Increments version in semver way 159 * Example: 160 * 1.2.3 -> nextMajor -> 2.0.0 161 * 1.2.3 -> nextMinor -> 1.3.0 162 * 1.2.3 -> nextPatch -> 1.2.4 163 * 1.2.3-rc.1+build.5 -> nextPatch -> 1.2.4 164 */ 165 @safe @nogc pure nothrow SemVer nextMajor() { 166 this.Major++; 167 this.Minor = this.Patch = 0; 168 this.PreRelease = this.Build = ""; 169 return this; 170 } 171 /// ditto 172 @safe @nogc pure nothrow SemVer nextMinor() { 173 this.Minor++; 174 this.Patch = 0; 175 this.PreRelease = this.Build = ""; 176 return this; 177 } 178 /// ditto 179 @safe @nogc pure nothrow SemVer nextPatch() { 180 this.Patch++; 181 this.PreRelease = this.Build = ""; 182 return this; 183 } 184 185 /** 186 * Convert SemVer to string 187 * Returns: SemVer in string (MAJOR.MINOR.PATCH-PRERELEASE+BUILD) 188 */ 189 @safe pure string toString() { 190 import std.array : appender; 191 import std.format : formattedWrite; 192 193 auto writer = appender!string(); 194 writer.formattedWrite("%d.%d.%d", this.Major, this.Minor, this.Patch); 195 if(PreRelease != "") 196 writer.formattedWrite("-%s", this.PreRelease); 197 if(Build != "") 198 writer.formattedWrite("+%s", this.Build); 199 return writer.data; 200 } 201 202 /** 203 * true, if this == b 204 */ 205 @safe @nogc pure nothrow const bool opEquals()(auto ref const SemVer b) { 206 return (this.Major == b.Major) && 207 (this.Minor == b.Minor) && 208 (this.Patch == b.Patch) && 209 (this.PreRelease == b.PreRelease); 210 } 211 212 /** 213 * Compares two SemVer structs. 214 */ 215 version(Have_natcmp): 216 @safe const int opCmp(ref const SemVer b) { 217 import natcmp; 218 219 if(this == b) 220 return 0; 221 222 if(this.Major != b.Major) 223 return this.Major < b.Major ? -1 : 1; 224 else if(this.Minor != b.Minor) 225 return this.Minor < b.Minor ? -1 : 1; 226 else if(this.Major != b.Major) 227 return this.Major < b.Major ? -1 : 1; 228 229 if((this.PreRelease != "") && (b.PreRelease != "")) { 230 int result = compareNatural(this.PreRelease, b.PreRelease); 231 if(result) { 232 return result; 233 } 234 } else if(this.PreRelease != "") { 235 return -1; 236 } else if(b.PreRelease != "") { 237 return 1; 238 } 239 240 throw new SemVerException("I don't know, how you got that error: SemVer is not an equal, but also not an different"); 241 } 242 /// ditto 243 @safe const int opCmp(in SemVer b) { 244 return this.opCmp(b); 245 } 246 /// 247 unittest { 248 assert(SemVer("1.0.0-alpha") < SemVer("1.0.0-alpha.1")); 249 assert(SemVer("1.0.0-alpha.1") < SemVer("1.0.0-alpha.beta")); 250 assert(SemVer("1.0.0-alpha.beta") < SemVer("1.0.0-beta")); 251 assert(SemVer("1.0.0-beta") < SemVer("1.0.0-beta.2")); 252 assert(SemVer("1.0.0-beta.2") < SemVer("1.0.0-beta.11")); 253 assert(SemVer("1.0.0-beta.11") < SemVer("1.0.0-rc.1")); 254 assert(SemVer("1.0.0-rc.1") < SemVer("1.0.0")); 255 assert(SemVer("1.0.0-rc.1") == SemVer("1.0.0+build.9")); 256 assert(SemVer("1.0.0-rc.1") == SemVer("1.0.0-rc.1+build.5")); 257 assert(SemVer("1.0.0-rc.1+build.5") == SemVer("1.0.0-rc.1+build.5")); 258 } 259 }