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 }