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 }