Skip to content

Commit 667b6d6

Browse files
authored
Add validation to version directives (#65)
1 parent f20fce1 commit 667b6d6

File tree

3 files changed

+346
-14
lines changed

3 files changed

+346
-14
lines changed
Lines changed: 314 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,314 @@
1+
// Licensed to Elasticsearch B.V under one or more agreements.
2+
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information
4+
5+
using System.Diagnostics.CodeAnalysis;
6+
using System.Globalization;
7+
using System.Text.RegularExpressions;
8+
9+
namespace Elastic.Markdown.Helpers;
10+
11+
/// <summary>
12+
/// A semver2 compatible version.
13+
/// </summary>
14+
public sealed class SemVersion :
15+
IEquatable<SemVersion>,
16+
IComparable<SemVersion>,
17+
IComparable
18+
{
19+
// https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
20+
private static readonly Regex Regex = new(@"^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-((?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?$");
21+
22+
/// <summary>
23+
/// The major version part.
24+
/// </summary>
25+
public int Major { get; }
26+
27+
/// <summary>
28+
/// The minor version part.
29+
/// </summary>
30+
public int Minor { get; }
31+
32+
/// <summary>
33+
/// The patch version part.
34+
/// </summary>
35+
public int Patch { get; }
36+
37+
/// <summary>
38+
/// The prerelease version part.
39+
/// </summary>
40+
public string Prerelease { get; }
41+
42+
/// <summary>
43+
/// The metadata version part.
44+
/// </summary>
45+
public string Metadata { get; }
46+
47+
/// <summary>
48+
/// Initializes a new <see cref="SemVersion"/> instance.
49+
/// </summary>
50+
/// <param name="major">The major version part.</param>
51+
/// <param name="minor">The minor version part.</param>
52+
/// <param name="patch">The patch version part.</param>
53+
public SemVersion(int major, int minor, int patch)
54+
{
55+
Major = major;
56+
Minor = minor;
57+
Patch = patch;
58+
Prerelease = string.Empty;
59+
Metadata = string.Empty;
60+
}
61+
62+
/// <summary>
63+
/// Initializes a new <see cref="SemVersion"/> instance.
64+
/// </summary>
65+
/// <param name="major">The major version part.</param>
66+
/// <param name="minor">The minor version part.</param>
67+
/// <param name="patch">The patch version part.</param>
68+
/// <param name="prerelease">The prerelease version part.</param>
69+
public SemVersion(int major, int minor, int patch, string? prerelease)
70+
{
71+
Major = major;
72+
Minor = minor;
73+
Patch = patch;
74+
Prerelease = prerelease ?? string.Empty;
75+
Metadata = string.Empty;
76+
}
77+
78+
/// <summary>
79+
/// Initializes a new <see cref="SemVersion"/> instance.
80+
/// </summary>
81+
/// <param name="major">The major version part.</param>
82+
/// <param name="minor">The minor version part.</param>
83+
/// <param name="patch">The patch version part.</param>
84+
/// <param name="prerelease">The prerelease version part.</param>
85+
/// <param name="metadata">The metadata version part.</param>
86+
public SemVersion(int major, int minor, int patch, string? prerelease, string? metadata)
87+
{
88+
Major = major;
89+
Minor = minor;
90+
Patch = patch;
91+
Prerelease = prerelease ?? string.Empty;
92+
Metadata = metadata ?? string.Empty;
93+
}
94+
95+
/// <summary>
96+
///
97+
/// </summary>
98+
/// <param name="left"></param>
99+
/// <param name="right"></param>
100+
/// <returns></returns>
101+
public static bool operator ==(SemVersion left, SemVersion right) => Equals(left, right);
102+
103+
/// <summary>
104+
///
105+
/// </summary>
106+
/// <param name="left"></param>
107+
/// <param name="right"></param>
108+
/// <returns></returns>
109+
public static bool operator !=(SemVersion left, SemVersion right) => !Equals(left, right);
110+
111+
/// <summary>
112+
///
113+
/// </summary>
114+
/// <param name="left"></param>
115+
/// <param name="right"></param>
116+
/// <returns></returns>
117+
public static bool operator >(SemVersion left, SemVersion right) => (left.CompareTo(right) > 0);
118+
119+
/// <summary>
120+
///
121+
/// </summary>
122+
/// <param name="left"></param>
123+
/// <param name="right"></param>
124+
/// <returns></returns>
125+
public static bool operator >=(SemVersion left, SemVersion right) => (left == right) || (left > right);
126+
127+
/// <summary>
128+
///
129+
/// </summary>
130+
/// <param name="left"></param>
131+
/// <param name="right"></param>
132+
/// <returns></returns>
133+
public static bool operator <(SemVersion left, SemVersion right) => (left.CompareTo(right) < 0);
134+
135+
/// <summary>
136+
///
137+
/// </summary>
138+
/// <param name="left"></param>
139+
/// <param name="right"></param>
140+
/// <returns></returns>
141+
public static bool operator <=(SemVersion left, SemVersion right) => (left == right) || (left < right);
142+
143+
/// <summary>
144+
/// Tries to initialize a new <see cref="SemVersion"/> instance from the given string.
145+
/// </summary>
146+
/// <param name="input">The semver2 compatible version string.</param>
147+
/// <param name="version">The parsed <see cref="SemVersion"/> instance.</param>
148+
/// <returns><c>True</c> if the passed string is a valid semver2 version string or <c>false</c>, if not.</returns>
149+
public static bool TryParse(string input, [NotNullWhen(true)] out SemVersion? version)
150+
{
151+
version = null;
152+
153+
var match = Regex.Match(input);
154+
if (!match.Success)
155+
return false;
156+
157+
if (!int.TryParse(match.Groups[1].Value, NumberStyles.Any, CultureInfo.InvariantCulture, out var major))
158+
return false;
159+
if (!int.TryParse(match.Groups[2].Value, NumberStyles.Any, CultureInfo.InvariantCulture, out var minor))
160+
return false;
161+
if (!int.TryParse(match.Groups[3].Value, NumberStyles.Any, CultureInfo.InvariantCulture, out var patch))
162+
return false;
163+
164+
version = new SemVersion(major, minor, patch, match.Groups[4].Value, match.Groups[5].Value);
165+
166+
return true;
167+
}
168+
169+
/// <summary>
170+
/// Returns a new <see cref="SemVersion"/> instance with updated components. Unchanged parts should be set to <c>null</c>.
171+
/// </summary>
172+
/// <param name="major">The major version part, or <c>null</c> to keep the current value.</param>
173+
/// <param name="minor">The minor version part, or <c>null</c> to keep the current value.</param>
174+
/// <param name="patch">The patch version part, or <c>null</c> to keep the current value.</param>
175+
/// <param name="prerelease">The prerelease version part, or <c>null</c> to keep the current value.</param>
176+
/// <param name="metadata">The metadata version part, or <c>null</c> to keep the current value.</param>
177+
/// <returns></returns>
178+
public SemVersion Update(int? major = null, int? minor = null, int? patch = null, string? prerelease = null, string? metadata = null) =>
179+
new(major ?? Major,
180+
minor ?? Minor,
181+
patch ?? Patch,
182+
prerelease ?? Prerelease,
183+
metadata ?? Metadata);
184+
185+
/// <summary>
186+
/// Compares the current version to another version in a natural way (by component/part precedence).
187+
/// </summary>
188+
/// <param name="other">The <see cref="SemVersion"/> to compare to.</param>
189+
/// <returns><c>0</c> if both versions are equal, a positive number, if the other version is lower or a negative number if the other version is higher.</returns>
190+
public int CompareByPrecedence(SemVersion? other)
191+
{
192+
if (ReferenceEquals(other, null))
193+
return 1;
194+
195+
var result = Major.CompareTo(other.Major);
196+
if (result != 0)
197+
return result;
198+
199+
result = Minor.CompareTo(other.Minor);
200+
if (result != 0)
201+
return result;
202+
203+
result = Patch.CompareTo(other.Patch);
204+
if (result != 0)
205+
return result;
206+
207+
result = CompareComponent(Prerelease, other.Prerelease, true);
208+
if (result != 0)
209+
return result;
210+
211+
return CompareComponent(Prerelease, other.Metadata, true);
212+
}
213+
214+
/// <inheritdoc cref="IComparable{T}.CompareTo"/>
215+
public int CompareTo(SemVersion? other)
216+
{
217+
if (ReferenceEquals(other, null))
218+
return 1;
219+
220+
return CompareByPrecedence(other);
221+
}
222+
223+
/// <inheritdoc cref="IComparable.CompareTo"/>
224+
public int CompareTo(object? obj) => CompareTo(obj as SemVersion);
225+
226+
/// <inheritdoc cref="IEquatable{T}.Equals(T)"/>
227+
public bool Equals(SemVersion? other)
228+
{
229+
if (ReferenceEquals(null, other))
230+
return false;
231+
232+
if (ReferenceEquals(this, other))
233+
return true;
234+
235+
return (Major == other.Major) && (Minor == other.Minor) && (Patch == other.Patch) &&
236+
(Prerelease == other.Prerelease) && (Metadata == other.Metadata);
237+
}
238+
239+
/// <inheritdoc cref="object.Equals(object)"/>
240+
public override bool Equals(object? obj) => ReferenceEquals(this, obj) || obj is SemVersion other && Equals(other);
241+
242+
/// <inheritdoc cref="object.GetHashCode"/>
243+
public override int GetHashCode()
244+
{
245+
unchecked
246+
{
247+
var hashCode = Major;
248+
hashCode = (hashCode * 397) ^ Minor;
249+
hashCode = (hashCode * 397) ^ Patch;
250+
hashCode = (hashCode * 397) ^ Prerelease.GetHashCode();
251+
hashCode = (hashCode * 397) ^ Metadata.GetHashCode();
252+
return hashCode;
253+
}
254+
}
255+
256+
/// <inheritdoc cref="object.ToString"/>
257+
public override string ToString()
258+
{
259+
var version = $"{Major}.{Minor}.{Patch}";
260+
261+
if (!string.IsNullOrEmpty(Prerelease))
262+
version += "-" + Prerelease;
263+
if (!string.IsNullOrEmpty(Metadata))
264+
version += "+" + Metadata;
265+
266+
return version;
267+
}
268+
269+
private static int CompareComponent(string a, string b, bool lower = false)
270+
{
271+
var aEmpty = string.IsNullOrEmpty(a);
272+
var bEmpty = string.IsNullOrEmpty(b);
273+
if (aEmpty && bEmpty)
274+
return 0;
275+
276+
if (aEmpty)
277+
return lower ? 1 : -1;
278+
if (bEmpty)
279+
return lower ? -1 : 1;
280+
281+
var aComps = a.Split('.');
282+
var bComps = b.Split('.');
283+
284+
var minLen = Math.Min(aComps.Length, bComps.Length);
285+
for (var i = 0; i < minLen; i++)
286+
{
287+
var ac = aComps[i];
288+
var bc = bComps[i];
289+
var isanum = int.TryParse(ac, out var anum);
290+
var isbnum = int.TryParse(bc, out var bnum);
291+
int r;
292+
if (isanum && isbnum)
293+
{
294+
r = anum.CompareTo(bnum);
295+
if (r != 0)
296+
return anum.CompareTo(bnum);
297+
}
298+
else
299+
{
300+
if (isanum)
301+
return -1;
302+
if (isbnum)
303+
return 1;
304+
305+
r = string.CompareOrdinal(ac, bc);
306+
if (r != 0)
307+
return r;
308+
}
309+
}
310+
311+
return aComps.Length.CompareTo(bComps.Length);
312+
}
313+
}
314+
Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,40 @@
11
// Licensed to Elasticsearch B.V under one or more agreements.
22
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
33
// See the LICENSE file in the project root for more information
4+
5+
using Elastic.Markdown.Helpers;
6+
47
namespace Elastic.Markdown.Myst.Directives;
58

69
public class VersionBlock(DirectiveBlockParser parser, string directive, Dictionary<string, string> properties)
710
: DirectiveBlock(parser, properties)
811
{
912
public override string Directive => directive;
1013
public string Class => directive.Replace("version", "");
14+
public SemVersion? Version { get; private set; }
15+
16+
public string Title { get; private set; } = string.Empty;
1117

12-
public string Title
18+
public override void FinalizeAndValidate(ParserContext context)
1319
{
14-
get
20+
var tokens = Arguments?.Split(" ", 2, StringSplitOptions.RemoveEmptyEntries) ?? [];
21+
if (tokens.Length < 1)
1522
{
16-
var title = Thread.CurrentThread.CurrentCulture.TextInfo.ToTitleCase(directive.Replace("version", "version "));
17-
if (!string.IsNullOrEmpty(Arguments))
18-
title += $" {Arguments}";
23+
EmitError(context, $"{directive} needs exactly 2 arguments: <version> <title>");
24+
return;
25+
}
1926

20-
return title;
27+
if (!SemVersion.TryParse(tokens[0], out var version))
28+
{
29+
EmitError(context, $"{tokens[0]} is not a valid version");
30+
return;
2131
}
22-
}
2332

24-
public override void FinalizeAndValidate(ParserContext context)
25-
{
33+
Version = version;
34+
var title = Thread.CurrentThread.CurrentCulture.TextInfo.ToTitleCase(directive.Replace("version", "version "));
35+
title += $" ({Version})";
36+
if (tokens.Length > 1 && !string.IsNullOrWhiteSpace(tokens[1]))
37+
title += $": {tokens[1]}";
38+
Title = title;
2639
}
2740
}

0 commit comments

Comments
 (0)