1 /++ Semantic Versioning.
2 	TODO: Make `Major`, `Minor` and `Patch` sub-types when D gets implicit conversion in argument passing.
3 	See_Also: https://docs.rs/semver/latest/semver/
4  +/
5 module nxt.semver;
6 
7 import nxt.result : Result;
8 
9 @safe:
10 
11 /++ Semantic version number.
12  +/
13 struct Version {
14 @safe pure nothrow @nogc:
15     this(Major major, Minor minor = 0, Patch patch = 0, Prerelease pre = Prerelease.init, BuildMetadata build = BuildMetadata.init) {
16         _major = major;
17         _minor = minor;
18         _patch = patch;
19         _pre = pre;
20         _build = build;
21 	}
22 @property:
23 	Major major() const scope => _major;
24 	Minor minor() const scope => _minor;
25 	Patch patch() const scope => _patch;
26 	Prerelease pre() const scope => _pre;
27 	BuildMetadata build() const scope => _build;
28 private:
29 	Major _major;
30 	Minor _minor;
31 	Patch _patch;
32     Prerelease _pre;
33     BuildMetadata _build;
34 }
35 
36 /++ Major part. +/
37 alias Major = VersionPart;
38 
39 /++ Minor part. +/
40 alias Minor = VersionPart;
41 
42 /++ Patch part. +/
43 alias Patch = VersionPart;
44 
45 /++ Parts of semantic version numbers. +/
46 alias VersionPart = uint;
47 
48 /++ Prerelease.
49 	See_also: https://docs.rs/semver/latest/semver/struct.Prerelease.html
50  +/
51 struct Prerelease {
52 	// TODO:
53 	// string str;
54 }
55 
56 /++ Build metadata.
57 	See_also: https://docs.rs/semver/latest/semver/struct.BuildMetadata.html
58  +/
59 struct BuildMetadata {
60 	// TODO:
61 }
62 
63 ///
64 @safe pure nothrow @nogc unittest {
65 	assert(Version(1)     == Version(1));
66 	assert(Version(1,0)   == Version(1,0));
67 	assert(Version(0,0,0) == Version(0,0,0));
68 	assert(Version(0,0,0) != Version(0,0,1));
69 	assert(Version(1,0,0).major == Version(1,0,0).major);
70 	assert(Version(1,0,0).minor == Version(1,0,0).minor);
71 	assert(Version(1,0,0).patch == Version(1,0,0).patch);
72 	assert(Version(1,0,0).pre   == Version(1,0,0).pre);
73 	assert(Version(1,0,0).build == Version(1,0,0).build);
74 }
75 
76 /++ Parse `s` as a semantic version number.
77 
78  	Semantic versions are usually represented as string as:
79 	`MAJOR[.MINOR[.PATCH]][-PRERELEASE][+BUILD]`.
80 
81  	For ease of use, a leading `v` or a leading `=` are also accepted.
82 
83  	See_Also: https://docs.rs/semver/latest/semver/struct.Version.html
84  +/
85 Result!Version tryParseVersion(scope const(char)[] s) pure nothrow @nogc {
86 	import nxt.algorithm.searching : findSplit;
87 	import nxt.conv : tryParse;
88 
89 	alias R = typeof(return);
90 	Version semver;
91 
92     if (s.length == 0)
93         return R.invalid;
94 
95 	if (s[0] == 'v' || s[0] == '=') // skip leading {'v'|'='}
96 		s = s[1 .. $];
97 
98 	// major
99 	if (auto sp = s.findSplit('.')) {
100 		if (const hit = sp.pre.tryParse!Major)
101 			semver._major = hit.value;
102 		else
103 			return R.invalid;
104 		() @trusted { s = sp.post; }(); // TODO: -dip1000 should allow this
105 	} else
106 		return R.invalid;
107 
108 	// minor
109 	if (auto sp = s.findSplit('.')) {
110 		if (const hit = sp.pre.tryParse!Minor)
111 			semver._minor = hit.value;
112 		else
113 			return R.invalid;
114 		() @trusted { s = sp.post; }(); // TODO: -dip1000 should allow this
115 	} else
116 		return R.invalid;
117 
118 	// patch
119     if (s.length == 0)
120         return R.invalid;
121 
122 	if (const hit = s.tryParse!Patch)
123 		semver._patch = hit.value;
124 	else
125 		return R.invalid;
126 
127 	return R(semver);
128 }
129 
130 ///
131 @safe pure nothrow @nogc unittest {
132 	assert(*tryParseVersion("0.0.0") == Version(0,0,0));
133 	assert(*tryParseVersion("0.0.1") == Version(0,0,1));
134 	assert(*tryParseVersion("0.1.1") == Version(0,1,1));
135 	assert(*tryParseVersion("1.1.1") == Version(1,1,1));
136 	assert(!tryParseVersion(""));
137 	assert(!tryParseVersion("").isValid);
138 	assert(!tryParseVersion("_").isValid);
139 	assert(!tryParseVersion("").isValid);
140 	assert(!tryParseVersion("1").isValid);
141 	assert(!tryParseVersion("_").isValid);
142 	assert(!tryParseVersion("1.").isValid);
143 	assert(!tryParseVersion("1._").isValid);
144 	assert(!tryParseVersion("1.1.").isValid);
145 	assert(!tryParseVersion("1.1").isValid);
146 	assert(!tryParseVersion("1.1_1").isValid);
147 	assert(!tryParseVersion("1-1-1").isValid);
148 	assert(!tryParseVersion("_._.__").isValid);
149 	assert(!tryParseVersion("1._.__").isValid);
150 	assert(!tryParseVersion("1.1.__").isValid);
151 	assert(*tryParseVersion("v1.1.1") == Version(1,1,1));
152 	assert(*tryParseVersion("=1.1.1") == Version(1,1,1));
153 }