1 module dud.semver.parse;
2 
3 import std.array : array, back, empty, front, popFront;
4 import std.algorithm.searching : all, countUntil;
5 import std.algorithm.iteration : map, splitter;
6 import std.conv : to;
7 import std.format : format;
8 import std.exception : enforce;
9 import std.utf : byChar, byUTF;
10 
11 import dud.semver.semver;
12 import dud.semver.helper : isDigit;
13 import dud.semver.exception;
14 
15 @safe pure:
16 
17 SemVer parseSemVer(string input) {
18 	SemVer ret;
19 
20 	char[] inputRange = to!(char[])(input);
21 
22 	ret.major = splitOutNumber!isDot("Major", "first", inputRange);
23 	ret.minor = splitOutNumber!isDot("Minor", "second", inputRange);
24 	ret.patch = toNum("Patch", dropUntilPredOrEmpty!isPlusOrMinus(inputRange));
25 	if(!inputRange.empty && inputRange[0].isMinus()) {
26 		inputRange.popFront();
27 		ret.preRelease = splitter(dropUntilPredOrEmpty!isPlus(inputRange), '.')
28 			.map!(it => checkNotEmpty(it))
29 			.map!(it => checkASCII(it))
30 			.map!(it => to!string(it))
31 			.array;
32 	}
33 	if(!inputRange.empty) {
34 		enforce!InvalidSeperator(inputRange[0] == '+',
35 			format("Expected a '+' got '%s'", inputRange[0]));
36 		inputRange.popFront();
37 		ret.buildIdentifier =
38 			splitter(dropUntilPredOrEmpty!isFalse(inputRange), '.')
39 			.map!(it => checkNotEmpty(it))
40 			.map!(it => checkASCII(it))
41 			.map!(it => to!string(it))
42 			.array;
43 	}
44 	enforce!InputNotEmpty(inputRange.empty,
45 		format("Surprisingly input '%s' left", inputRange));
46 	return ret;
47 }
48 
49 char[] checkNotEmpty(char[] cs) {
50 	enforce!EmptyIdentifier(!cs.empty,
51 		"Build or prerelease identifier must not be empty");
52 	return cs;
53 }
54 
55 char[] checkASCII(char[] cs) {
56 	import std.ascii : isAlpha;
57 	foreach(it; cs.byUTF!char()) {
58 		enforce!NonAsciiChar(isDigit(it) || isAlpha(it) || it == '-', format(
59 			"Non ASCII character '%s' surprisingly found input '%s'",
60 			it, cs
61 		));
62 	}
63 	return cs;
64 }
65 
66 uint toNum(string numName, char[] input) {
67 	enforce!OnlyDigitAllowed(all!(isDigit)(input.byUTF!char()),
68 		format("%s range must solely consist of digits not '%s'",
69 			numName, input));
70 	return to!uint(input);
71 }
72 
73 uint splitOutNumber(alias pred)(const string numName, const string dotName,
74 		ref char[] input)
75 {
76 	const ptrdiff_t dot = input.byUTF!char().countUntil!pred();
77 	enforce!InvalidSeperator(dot != -1,
78 		format("Couldn't find the %s dot in '%s'", dotName, input));
79 	char[] num = input[0 .. dot];
80 	const uint ret = toNum(numName, num);
81 	enforce!EmptyInput(input.length > dot + 1,
82 		format("Input '%s' ended surprisingly after %s version",
83 			input, numName));
84 	input = input[dot + 1 .. $];
85 	return ret;
86 }
87 
88 @nogc nothrow:
89 
90 char[] dropUntilPredOrEmpty(alias pred)(ref char[] input) {
91 	size_t pos;
92 	while(pos < input.length && !pred(input[pos])) {
93 		++pos;
94 	}
95 	char[] ret = input[0 .. pos];
96 	input = input[pos .. $];
97 	return ret;
98 }
99 
100 bool isFalse(char c) {
101 	return false;
102 }
103 
104 bool isDot(char c) {
105 	return c == '.';
106 }
107 
108 bool isMinus(char c) {
109 	return c == '-';
110 }
111 
112 bool isPlus(char c) {
113 	return c == '+';
114 }
115 
116 bool isPlusOrMinus(char c) {
117 	return isPlus(c) || isMinus(c);
118 }