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 }