This commit is contained in:
parent
8c93cf18a0
commit
f9fb920e07
5 changed files with 231 additions and 238 deletions
|
|
@ -81,6 +81,16 @@ internal sealed class SemanticVersionRangeTests
|
|||
await Assert.That(r.Contains(v)).IsEqualTo(expected);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task I_can_parse_exact_version_ranges_via_public_api()
|
||||
{
|
||||
var range = SemanticVersionRange.Parse("=1.2.3");
|
||||
|
||||
await Assert.That(range.Contains(SemanticVersion.Parse("1.2.3"))).IsTrue();
|
||||
await Assert.That(range.Contains(SemanticVersion.Parse("1.2.4"))).IsFalse();
|
||||
await Assert.That(range.ToString("n", null)).IsEqualTo("1.2.3");
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Arguments(">1.2.3 || <1.0.0", "1.2.4", true)]
|
||||
[Arguments(">1.2.3 || <1.0.0", "0.9.9", true)]
|
||||
|
|
@ -180,6 +190,7 @@ internal sealed class SemanticVersionRangeTests
|
|||
[Test]
|
||||
[Arguments(">=1.0.0 !2.0.0")]
|
||||
[Arguments("1.2.3 - ")]
|
||||
[Arguments("!=1.x")]
|
||||
public async Task I_can_not_parse_invalid_ranges(string range)
|
||||
{
|
||||
await Assert.That(() => SemanticVersionRange.Parse(range))
|
||||
|
|
|
|||
|
|
@ -203,7 +203,7 @@ internal sealed class SemanticVersionTests
|
|||
var obj = new { Version = new SemanticVersion(1, 0, 0, "rc.1", "metadata") };
|
||||
var json = JsonSerializer.Serialize(obj, RelaxedOptions);
|
||||
|
||||
await Assert.That(json).IsEqualTo("{\"Version\":\"1.0.0-rc.1+metadata\"}");
|
||||
await Assert.That(json).IsEqualTo(/*lang=json,strict*/ "{\"Version\":\"1.0.0-rc.1+metadata\"}");
|
||||
}
|
||||
|
||||
[Test]
|
||||
|
|
|
|||
|
|
@ -57,18 +57,18 @@ public readonly partial record struct SemanticVersionRange : ISpanFormattable
|
|||
}
|
||||
|
||||
var buf = new SpanBuffer(destination);
|
||||
var groups = _groups ?? [];
|
||||
var sets = _sets ?? [];
|
||||
|
||||
if (format is "m")
|
||||
{
|
||||
for (var i = 0; i < groups.Length; i++)
|
||||
for (var i = 0; i < sets.Length; i++)
|
||||
{
|
||||
if (i > 0 && !buf.TryWrite(','))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryFormatMaven(ref buf, groups[i]))
|
||||
if (!TryFormatMaven(ref buf, sets[i]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
|
@ -76,7 +76,7 @@ public readonly partial record struct SemanticVersionRange : ISpanFormattable
|
|||
}
|
||||
else
|
||||
{
|
||||
for (var i = 0; i < groups.Length; i++)
|
||||
for (var i = 0; i < sets.Length; i++)
|
||||
{
|
||||
if (i > 0 && !buf.TryWrite(" || "))
|
||||
{
|
||||
|
|
@ -85,14 +85,14 @@ public readonly partial record struct SemanticVersionRange : ISpanFormattable
|
|||
|
||||
if (format is "ns")
|
||||
{
|
||||
if (!TryFormatSimpleNpm(ref buf, groups[i]))
|
||||
if (!TryFormatSimpleNpm(ref buf, sets[i]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!TryFormatNormalNpm(ref buf, groups[i]))
|
||||
if (!TryFormatNormalNpm(ref buf, sets[i]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
|
@ -106,34 +106,34 @@ public readonly partial record struct SemanticVersionRange : ISpanFormattable
|
|||
|
||||
#endregion
|
||||
|
||||
private static bool TryFormatSimpleNpm(ref SpanBuffer buf, ComparatorGroup group)
|
||||
private static bool TryFormatSimpleNpm(ref SpanBuffer buf, ConstraintSet set)
|
||||
{
|
||||
if (group.Comparators is [])
|
||||
if (IsAny(set))
|
||||
{
|
||||
return buf.TryWrite('*');
|
||||
}
|
||||
|
||||
if (group.Comparators is [{ Op: ComparatorOp.Eq, Version: var version }])
|
||||
if (set.Constraints is [{ Operation: Comparison.Eq, Version: var version }])
|
||||
{
|
||||
return buf.TryWrite(version);
|
||||
}
|
||||
|
||||
if (group.Comparators.Length is not 2)
|
||||
if (set.Constraints.Length is not 2)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (group is not { Lower: { Op: ComparatorOp.Gte or ComparatorOp.Gt, Version: var lo } })
|
||||
if (set is not { Lower: { Operation: Comparison.Gte or Comparison.Gt, Version: var lo } })
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (group is not { Upper: { Op: ComparatorOp.Lt or ComparatorOp.Lte, Version: var hi } upper })
|
||||
if (set is not { Upper: { Operation: Comparison.Lt or Comparison.Lte, Version: var hi } upper })
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (upper.Op is ComparatorOp.Lt && hi is { Minor: 0, Patch: 0, Prerelease: not { Length: > 0 } })
|
||||
if (upper.Operation is Comparison.Lt && hi is { Minor: 0, Patch: 0, Prerelease: not { Length: > 0 } })
|
||||
{
|
||||
if (lo is { Major: > 0 } && hi.Major == lo.Major + 1)
|
||||
{
|
||||
|
|
@ -151,7 +151,7 @@ public readonly partial record struct SemanticVersionRange : ISpanFormattable
|
|||
}
|
||||
}
|
||||
|
||||
if (upper.Op is ComparatorOp.Lt && hi is { Patch: 0, Prerelease: not { Length: > 0 } })
|
||||
if (upper.Operation is Comparison.Lt && hi is { Patch: 0, Prerelease: not { Length: > 0 } })
|
||||
{
|
||||
if (hi.Major == lo.Major && hi.Minor == lo.Minor + 1)
|
||||
{
|
||||
|
|
@ -162,28 +162,28 @@ public readonly partial record struct SemanticVersionRange : ISpanFormattable
|
|||
return false;
|
||||
}
|
||||
|
||||
private static bool TryFormatNormalNpm(ref SpanBuffer buf, ComparatorGroup group)
|
||||
private static bool TryFormatNormalNpm(ref SpanBuffer buf, ConstraintSet set)
|
||||
{
|
||||
if (group.Comparators.Length is 0)
|
||||
if (IsAny(set))
|
||||
{
|
||||
return buf.TryWrite('*');
|
||||
}
|
||||
|
||||
for (var i = 0; i < group.Comparators.Length; i++)
|
||||
for (var i = 0; i < set.Constraints.Length; i++)
|
||||
{
|
||||
if (i > 0 && !buf.TryWrite(' '))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var @operator = group.Comparators[i].Op switch
|
||||
var @operator = set.Constraints[i].Operation switch
|
||||
{
|
||||
ComparatorOp.Neq => "!=",
|
||||
ComparatorOp.Lt => "<",
|
||||
ComparatorOp.Lte => "<=",
|
||||
ComparatorOp.Gt => ">",
|
||||
ComparatorOp.Gte => ">=",
|
||||
_ => ReadOnlySpan<char>.Empty,
|
||||
Comparison.Neq => "!=",
|
||||
Comparison.Lt => "<",
|
||||
Comparison.Lte => "<=",
|
||||
Comparison.Gt => ">",
|
||||
Comparison.Gte => ">=",
|
||||
Comparison.Eq or _ => ReadOnlySpan<char>.Empty,
|
||||
};
|
||||
|
||||
if (!@operator.IsEmpty && !buf.TryWrite(@operator))
|
||||
|
|
@ -191,7 +191,7 @@ public readonly partial record struct SemanticVersionRange : ISpanFormattable
|
|||
return false;
|
||||
}
|
||||
|
||||
if (!buf.TryWrite(group.Comparators[i].Version))
|
||||
if (!buf.TryWrite(set.Constraints[i].Version))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
|
@ -200,17 +200,27 @@ public readonly partial record struct SemanticVersionRange : ISpanFormattable
|
|||
return true;
|
||||
}
|
||||
|
||||
private static bool TryFormatMaven(ref SpanBuffer buf, ComparatorGroup group)
|
||||
private static bool TryFormatMaven(ref SpanBuffer buf, ConstraintSet set)
|
||||
{
|
||||
if (group.Comparators is [{ Op: ComparatorOp.Eq, Version: var version }])
|
||||
if (IsAny(set))
|
||||
{
|
||||
return buf.TryWrite("(,)");
|
||||
}
|
||||
|
||||
if (set.Constraints is [{ Operation: Comparison.Eq, Version: var version }])
|
||||
{
|
||||
return buf.TryWrite('[') && buf.TryWrite(version) && buf.TryWrite(']');
|
||||
}
|
||||
|
||||
return buf.TryWrite(group.Lower?.Op == ComparatorOp.Gte ? '[' : '(') &&
|
||||
(!group.Lower.HasValue || buf.TryWrite(group.Lower.Value.Version)) &&
|
||||
return buf.TryWrite(set.Lower?.Operation == Comparison.Gte ? '[' : '(') &&
|
||||
(!set.Lower.HasValue || buf.TryWrite(set.Lower.Value.Version)) &&
|
||||
buf.TryWrite(',') &&
|
||||
(!group.Upper.HasValue || buf.TryWrite(group.Upper.Value.Version)) &&
|
||||
buf.TryWrite(group.Upper?.Op == ComparatorOp.Lte ? ']' : ')');
|
||||
(!set.Upper.HasValue || buf.TryWrite(set.Upper.Value.Version)) &&
|
||||
buf.TryWrite(set.Upper?.Operation == Comparison.Lte ? ']' : ')');
|
||||
}
|
||||
|
||||
private static bool IsAny(ConstraintSet set)
|
||||
{
|
||||
return set.Constraints is [] or [{ Operation: Comparison.Gte, Version: { Major: 0, Minor: 0, Patch: 0 } }];
|
||||
}
|
||||
}
|
||||
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright (c) The Geekeey Authors
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Geekeey.SemVer;
|
||||
|
|
@ -90,29 +91,29 @@ public readonly partial record struct SemanticVersionRange : ISpanParsable<Seman
|
|||
private static bool TryParseNpm(ReadOnlySpan<char> s, out SemanticVersionRange result)
|
||||
{
|
||||
result = default;
|
||||
var groups = new List<ComparatorGroup>();
|
||||
var sets = new List<ConstraintSet>();
|
||||
|
||||
foreach (var range in new NodeSetGrouping(s))
|
||||
foreach (var range in new NpmSetGrouping(s))
|
||||
{
|
||||
if (!TryParseNpmGroup(s[range].Trim(), out var group))
|
||||
if (!TryParseNpmSet(s[range].Trim(), out var set))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
groups.Add(group);
|
||||
sets.Add(set);
|
||||
}
|
||||
|
||||
result = new SemanticVersionRange([.. groups]);
|
||||
result = new SemanticVersionRange([.. sets]);
|
||||
return true;
|
||||
}
|
||||
|
||||
private ref struct NodeSetGrouping
|
||||
private ref struct NpmSetGrouping
|
||||
{
|
||||
private readonly ReadOnlySpan<char> _span;
|
||||
private int _currentStart;
|
||||
private int _currentEnd;
|
||||
|
||||
public NodeSetGrouping(ReadOnlySpan<char> span)
|
||||
public NpmSetGrouping(ReadOnlySpan<char> span)
|
||||
{
|
||||
_span = span;
|
||||
_currentStart = 0;
|
||||
|
|
@ -136,15 +137,15 @@ public readonly partial record struct SemanticVersionRange : ISpanParsable<Seman
|
|||
return true;
|
||||
}
|
||||
|
||||
public readonly NodeSetGrouping GetEnumerator()
|
||||
public readonly NpmSetGrouping GetEnumerator()
|
||||
{
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryParseNpmGroup(ReadOnlySpan<char> s, out ComparatorGroup group)
|
||||
private static bool TryParseNpmSet(ReadOnlySpan<char> s, out ConstraintSet set)
|
||||
{
|
||||
group = default;
|
||||
set = default;
|
||||
|
||||
if (s.IsEmpty)
|
||||
{
|
||||
|
|
@ -152,60 +153,38 @@ public readonly partial record struct SemanticVersionRange : ISpanParsable<Seman
|
|||
}
|
||||
|
||||
// Hyphen range: "1.0.0 - 2.0.0" OR Caret: "^x.y.z" OR Tilde: "~x.y.z"
|
||||
if (TryParseHyphenRange(s, out group) || TryParseCaretRange(s, out group) || TryParseTildeRange(s, out group))
|
||||
if (TryParseHyphenRange(s, out set) || TryParseCaretRange(s, out set) || TryParseTildeRange(s, out set))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Space-separated AND comparators
|
||||
var comparators = new List<Comparator>();
|
||||
var tokens = 0;
|
||||
while (!s.IsEmpty)
|
||||
var comparators = new List<Constraint>();
|
||||
foreach (var range in s.Split(' '))
|
||||
{
|
||||
// Skip spaces
|
||||
s = s.TrimStart();
|
||||
if (s.IsEmpty)
|
||||
if (s[range] is not { IsEmpty: false } segment)
|
||||
{
|
||||
break;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Find end of this token (next space)
|
||||
var end = s.IndexOf(' ');
|
||||
var token = end >= 0 ? s[..end] : s;
|
||||
tokens++;
|
||||
|
||||
if (token.ContainsAny("*xX"))
|
||||
if (!TryParseCompare(segment, comparators))
|
||||
{
|
||||
if (!TryExpandWildcardComparators(token, comparators))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!TryParseComparator(token, out var comp))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
comparators.Add(comp);
|
||||
}
|
||||
|
||||
s = end >= 0 ? s[(end + 1)..] : [];
|
||||
}
|
||||
|
||||
if (comparators.Count is 0 && tokens is 0)
|
||||
if (comparators.Count is 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
group = new ComparatorGroup([.. comparators]);
|
||||
set = new ConstraintSet([.. comparators]);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryParseHyphenRange(ReadOnlySpan<char> s, out ComparatorGroup group)
|
||||
private static bool TryParseHyphenRange(ReadOnlySpan<char> s, out ConstraintSet set)
|
||||
{
|
||||
group = default;
|
||||
set = default;
|
||||
|
||||
for (var i = 0; i < s.Length; i++)
|
||||
{
|
||||
if (s[i..] is [' ', '-', ' ', ..])
|
||||
|
|
@ -220,7 +199,7 @@ public readonly partial record struct SemanticVersionRange : ISpanParsable<Seman
|
|||
return false;
|
||||
}
|
||||
|
||||
group = new ComparatorGroup([new Comparator(ComparatorOp.Gte, lo), new Comparator(ComparatorOp.Lte, hi)]);
|
||||
set = new ConstraintSet([new Constraint(Comparison.Gte, lo), new Constraint(Comparison.Lte, hi)]);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -228,9 +207,9 @@ public readonly partial record struct SemanticVersionRange : ISpanParsable<Seman
|
|||
return false;
|
||||
}
|
||||
|
||||
private static bool TryParseCaretRange(ReadOnlySpan<char> s, out ComparatorGroup group)
|
||||
private static bool TryParseCaretRange(ReadOnlySpan<char> s, out ConstraintSet set)
|
||||
{
|
||||
group = default;
|
||||
set = default;
|
||||
|
||||
if (s.IsEmpty || s[0] is not '^')
|
||||
{
|
||||
|
|
@ -259,13 +238,13 @@ public readonly partial record struct SemanticVersionRange : ISpanParsable<Seman
|
|||
hi = new SemanticVersion(0, 0, lo.Patch + 1);
|
||||
}
|
||||
|
||||
group = new ComparatorGroup([new Comparator(ComparatorOp.Gte, lo), new Comparator(ComparatorOp.Lt, hi)]);
|
||||
set = new ConstraintSet([new Constraint(Comparison.Gte, lo), new Constraint(Comparison.Lt, hi)]);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryParseTildeRange(ReadOnlySpan<char> s, out ComparatorGroup group)
|
||||
private static bool TryParseTildeRange(ReadOnlySpan<char> s, out ConstraintSet set)
|
||||
{
|
||||
group = default;
|
||||
set = default;
|
||||
|
||||
if (s.IsEmpty || s[0] is not '~')
|
||||
{
|
||||
|
|
@ -290,37 +269,72 @@ public readonly partial record struct SemanticVersionRange : ISpanParsable<Seman
|
|||
hi = new SemanticVersion(lo.Major, lo.Minor + 1, 0);
|
||||
}
|
||||
|
||||
group = new ComparatorGroup([new Comparator(ComparatorOp.Gte, lo), new Comparator(ComparatorOp.Lt, hi)]);
|
||||
set = new ConstraintSet([new Constraint(Comparison.Gte, lo), new Constraint(Comparison.Lt, hi)]);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryParseComparator(ReadOnlySpan<char> token, out Comparator result)
|
||||
private static bool TryParseCompare(ReadOnlySpan<char> s, List<Constraint> constraints)
|
||||
{
|
||||
result = default;
|
||||
var op = Comparison.Eq;
|
||||
|
||||
if (!TryParseComparatorPrefix(token.Trim(), out var op, out token))
|
||||
if (TryParseComparatorPrefix(s, out var comparison, out var rest))
|
||||
{
|
||||
if (!SemanticVersion.TryParse(token, null, out var version))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
op = comparison;
|
||||
s = rest;
|
||||
}
|
||||
|
||||
result = new Comparator(op, version);
|
||||
if (!SemanticVersion.TryParsePartially(s, out var lo, out var components))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (components is 0)
|
||||
{
|
||||
constraints.Add(new Constraint(Comparison.Gte, new SemanticVersion(0, 0, 0)));
|
||||
return op is Comparison.Eq;
|
||||
}
|
||||
|
||||
if (components is 3)
|
||||
{
|
||||
constraints.Add(new Constraint(op, lo));
|
||||
return true;
|
||||
}
|
||||
|
||||
SemanticVersion hi;
|
||||
|
||||
if (components is 1)
|
||||
{
|
||||
hi = new SemanticVersion(lo.Major + 1, 0, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!SemanticVersion.TryParse(token, null, out var version))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
hi = new SemanticVersion(lo.Major, lo.Minor + 1, 0);
|
||||
}
|
||||
|
||||
result = new Comparator(op, version);
|
||||
return true;
|
||||
switch (op)
|
||||
{
|
||||
case Comparison.Eq:
|
||||
case Comparison.Gte:
|
||||
constraints.Add(new Constraint(Comparison.Gte, lo));
|
||||
constraints.Add(new Constraint(Comparison.Lt, hi));
|
||||
return true;
|
||||
case Comparison.Gt:
|
||||
constraints.Add(new Constraint(Comparison.Gt, lo));
|
||||
constraints.Add(new Constraint(Comparison.Lt, hi));
|
||||
return true;
|
||||
case Comparison.Lte:
|
||||
constraints.Add(new Constraint(Comparison.Lt, hi));
|
||||
return true;
|
||||
case Comparison.Lt:
|
||||
constraints.Add(new Constraint(Comparison.Lt, lo));
|
||||
return true;
|
||||
case Comparison.Neq:
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryParseComparatorPrefix(ReadOnlySpan<char> s, out ComparatorOp op, out ReadOnlySpan<char> remainder)
|
||||
private static bool TryParseComparatorPrefix(ReadOnlySpan<char> s, out Comparison op, out ReadOnlySpan<char> remainder)
|
||||
{
|
||||
if (s.IsEmpty)
|
||||
{
|
||||
|
|
@ -332,27 +346,27 @@ public readonly partial record struct SemanticVersionRange : ISpanParsable<Seman
|
|||
switch (s)
|
||||
{
|
||||
case ['!', '=', ..]:
|
||||
op = ComparatorOp.Neq;
|
||||
op = Comparison.Neq;
|
||||
remainder = s[2..];
|
||||
return true;
|
||||
case ['>', '=', ..]:
|
||||
op = ComparatorOp.Gte;
|
||||
op = Comparison.Gte;
|
||||
remainder = s[2..];
|
||||
return true;
|
||||
case ['<', '=', ..]:
|
||||
op = ComparatorOp.Lte;
|
||||
op = Comparison.Lte;
|
||||
remainder = s[2..];
|
||||
return true;
|
||||
case ['>', ..]:
|
||||
op = ComparatorOp.Gt;
|
||||
op = Comparison.Gt;
|
||||
remainder = s[1..];
|
||||
return true;
|
||||
case ['<', ..]:
|
||||
op = ComparatorOp.Lt;
|
||||
op = Comparison.Lt;
|
||||
remainder = s[1..];
|
||||
return true;
|
||||
case ['=', ..]:
|
||||
op = ComparatorOp.Eq;
|
||||
op = Comparison.Eq;
|
||||
remainder = s[1..];
|
||||
return true;
|
||||
default:
|
||||
|
|
@ -362,133 +376,86 @@ public readonly partial record struct SemanticVersionRange : ISpanParsable<Seman
|
|||
}
|
||||
}
|
||||
|
||||
private static bool TryExpandWildcardComparators(ReadOnlySpan<char> s, List<Comparator> comparators)
|
||||
{
|
||||
s = s.Trim();
|
||||
if (s is ['*' or 'x' or 'X'])
|
||||
{
|
||||
// bare wildcards only make sense when it is an exact version comparison.
|
||||
return true;
|
||||
}
|
||||
|
||||
var op = ComparatorOp.Eq;
|
||||
if (TryParseComparatorPrefix(s, out var parsedOp, out var remainder))
|
||||
{
|
||||
op = parsedOp;
|
||||
s = remainder;
|
||||
}
|
||||
|
||||
if (!SemanticVersion.TryParsePartially(s, out var lo, out var wildcard))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (s is ['*' or 'x' or 'X'])
|
||||
{
|
||||
// bare wildcards only make sense when it is an exact version comparison.
|
||||
return op == ComparatorOp.Eq;
|
||||
}
|
||||
|
||||
SemanticVersion hi;
|
||||
|
||||
if (wildcard is 1)
|
||||
{
|
||||
hi = new SemanticVersion(lo.Major + 1, 0, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
hi = new SemanticVersion(lo.Major, lo.Minor + 1, 0);
|
||||
}
|
||||
|
||||
switch (op)
|
||||
{
|
||||
case ComparatorOp.Eq:
|
||||
case ComparatorOp.Gte:
|
||||
comparators.Add(new Comparator(ComparatorOp.Gte, lo));
|
||||
comparators.Add(new Comparator(ComparatorOp.Lt, hi));
|
||||
break;
|
||||
case ComparatorOp.Gt:
|
||||
comparators.Add(new Comparator(ComparatorOp.Gt, lo));
|
||||
comparators.Add(new Comparator(ComparatorOp.Lt, hi));
|
||||
break;
|
||||
case ComparatorOp.Lte:
|
||||
comparators.Add(new Comparator(ComparatorOp.Lt, hi));
|
||||
break;
|
||||
case ComparatorOp.Lt:
|
||||
comparators.Add(new Comparator(ComparatorOp.Lt, lo));
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryParseMaven(ReadOnlySpan<char> s, out SemanticVersionRange result)
|
||||
{
|
||||
result = default;
|
||||
var groups = new List<ComparatorGroup>();
|
||||
var sets = new List<ConstraintSet>();
|
||||
|
||||
while (!s.IsEmpty)
|
||||
foreach (var range in new MavenSetGrouping(s))
|
||||
{
|
||||
s = s.Trim();
|
||||
if (s.IsEmpty)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (s[0] is not '[' and not '(')
|
||||
if (!TryParseMavenSet(s[range].Trim(), out var set))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Find matching close bracket
|
||||
var close = FindMavenClose(s);
|
||||
if (close < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var bracket = s[..(close + 1)];
|
||||
if (!TryParseMavenGroup(bracket, out var g))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
groups.Add(g);
|
||||
|
||||
s = s[(close + 1)..].Trim();
|
||||
if (!s.IsEmpty && s[0] == ',')
|
||||
{
|
||||
s = s[1..]; // OR separator between groups
|
||||
}
|
||||
sets.Add(set);
|
||||
}
|
||||
|
||||
if (groups.Count == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
result = new SemanticVersionRange([.. groups]);
|
||||
result = new SemanticVersionRange([.. sets]);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int FindMavenClose(ReadOnlySpan<char> s)
|
||||
private ref struct MavenSetGrouping
|
||||
{
|
||||
for (var i = 1; i < s.Length; i++)
|
||||
private readonly ReadOnlySpan<char> _span;
|
||||
private int _currentStart;
|
||||
private int _currentEnd;
|
||||
|
||||
public MavenSetGrouping(ReadOnlySpan<char> readOnlySpan)
|
||||
{
|
||||
if (s[i] is ']' or ')')
|
||||
{
|
||||
return i;
|
||||
}
|
||||
_span = readOnlySpan;
|
||||
_currentStart = 0;
|
||||
_currentEnd = -1;
|
||||
}
|
||||
|
||||
return -1;
|
||||
public readonly Range Current => _currentStart.._currentEnd;
|
||||
|
||||
public bool MoveNext()
|
||||
{
|
||||
_currentStart = _currentEnd is -1 ? 0 : _currentEnd + 1;
|
||||
|
||||
if (_currentStart >= _span.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var depth = 0;
|
||||
var i = _currentStart;
|
||||
|
||||
while (i < _span.Length)
|
||||
{
|
||||
var ch = _span[i];
|
||||
|
||||
if (ch is '[' or '(')
|
||||
{
|
||||
depth++;
|
||||
}
|
||||
else if (ch is ']' or ')')
|
||||
{
|
||||
depth--;
|
||||
}
|
||||
else if (ch is ',' && depth is 0)
|
||||
{
|
||||
_currentEnd = i;
|
||||
return true;
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
_currentEnd = _span.Length;
|
||||
return true;
|
||||
}
|
||||
|
||||
public readonly MavenSetGrouping GetEnumerator()
|
||||
{
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryParseMavenGroup(ReadOnlySpan<char> s, out ComparatorGroup group)
|
||||
private static bool TryParseMavenSet(ReadOnlySpan<char> s, out ConstraintSet set)
|
||||
{
|
||||
group = default;
|
||||
set = default;
|
||||
|
||||
var loInclusive = s[0] is '[';
|
||||
var hiInclusive = s[^1] is ']';
|
||||
|
|
@ -507,11 +474,11 @@ public readonly partial record struct SemanticVersionRange : ISpanParsable<Seman
|
|||
return false;
|
||||
}
|
||||
|
||||
group = new ComparatorGroup([new Comparator(ComparatorOp.Eq, version)]);
|
||||
set = new ConstraintSet([new Constraint(Comparison.Eq, version)]);
|
||||
return true;
|
||||
}
|
||||
|
||||
var comps = new List<Comparator>();
|
||||
var comps = new List<Constraint>();
|
||||
|
||||
if (s[..i].Trim() is { IsEmpty: false } loStr)
|
||||
{
|
||||
|
|
@ -520,7 +487,7 @@ public readonly partial record struct SemanticVersionRange : ISpanParsable<Seman
|
|||
return false;
|
||||
}
|
||||
|
||||
comps.Add(new Comparator(loInclusive ? ComparatorOp.Gte : ComparatorOp.Gt, lo));
|
||||
comps.Add(new Constraint(loInclusive ? Comparison.Gte : Comparison.Gt, lo));
|
||||
}
|
||||
|
||||
if (s[(i + 1)..].Trim() is { IsEmpty: false } hiStr)
|
||||
|
|
@ -530,20 +497,20 @@ public readonly partial record struct SemanticVersionRange : ISpanParsable<Seman
|
|||
return false;
|
||||
}
|
||||
|
||||
comps.Add(new Comparator(hiInclusive ? ComparatorOp.Lte : ComparatorOp.Lt, hi));
|
||||
comps.Add(new Constraint(hiInclusive ? Comparison.Lte : Comparison.Lt, hi));
|
||||
}
|
||||
|
||||
// Check for exact match [a,a]
|
||||
if (comps is [{ Op: ComparatorOp.Gte, Version: var lhs }, { Op: ComparatorOp.Lte, Version: var rhs }])
|
||||
if (comps is [{ Operation: Comparison.Gte, Version: var lhs }, { Operation: Comparison.Lte, Version: var rhs }])
|
||||
{
|
||||
if (lhs == rhs)
|
||||
{
|
||||
group = new ComparatorGroup([new Comparator(ComparatorOp.Eq, lhs)]);
|
||||
set = new ConstraintSet([new Constraint(Comparison.Eq, lhs)]);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
group = new ComparatorGroup([.. comps]);
|
||||
set = new ConstraintSet([.. comps]);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
|
@ -10,11 +10,11 @@ namespace Geekeey.SemVer;
|
|||
public readonly partial record struct SemanticVersionRange
|
||||
{
|
||||
// OR of AND-groups. null == empty range (matches nothing).
|
||||
private readonly ComparatorGroup[]? _groups;
|
||||
private readonly ConstraintSet[]? _sets;
|
||||
|
||||
internal SemanticVersionRange(ComparatorGroup[] groups)
|
||||
internal SemanticVersionRange(ConstraintSet[] sets)
|
||||
{
|
||||
_groups = groups;
|
||||
_sets = sets;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
@ -24,70 +24,75 @@ public readonly partial record struct SemanticVersionRange
|
|||
/// <returns><c>true</c> if the version is contained in the range, otherwise <c>false</c>.</returns>
|
||||
public bool Contains(SemanticVersion version)
|
||||
{
|
||||
if (_groups is null || _groups.Length == 0)
|
||||
if (_sets is null || _sets.Length == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return _groups.Any(group => group.Includes(version));
|
||||
return _sets.Any(set => set.Includes(version));
|
||||
}
|
||||
}
|
||||
|
||||
internal enum ComparatorOp { Eq, Neq, Lt, Lte, Gt, Gte }
|
||||
internal enum Comparison { Eq, Neq, Lt, Lte, Gt, Gte }
|
||||
|
||||
internal readonly struct Comparator
|
||||
internal readonly struct Constraint
|
||||
{
|
||||
public readonly ComparatorOp Op;
|
||||
public readonly Comparison Operation;
|
||||
public readonly SemanticVersion Version;
|
||||
|
||||
public Comparator(ComparatorOp op, SemanticVersion version)
|
||||
public Constraint(Comparison operation, SemanticVersion version)
|
||||
{
|
||||
Op = op;
|
||||
Operation = operation;
|
||||
Version = version;
|
||||
}
|
||||
|
||||
public bool Includes(SemanticVersion v)
|
||||
{
|
||||
return Op switch
|
||||
return Operation switch
|
||||
{
|
||||
ComparatorOp.Eq => v == Version,
|
||||
ComparatorOp.Neq => v != Version,
|
||||
ComparatorOp.Lt => v < Version,
|
||||
ComparatorOp.Lte => v <= Version,
|
||||
ComparatorOp.Gt => v > Version,
|
||||
ComparatorOp.Gte => v >= Version,
|
||||
Comparison.Eq => v == Version,
|
||||
Comparison.Neq => v != Version,
|
||||
Comparison.Lt => v < Version,
|
||||
Comparison.Lte => v <= Version,
|
||||
Comparison.Gt => v > Version,
|
||||
Comparison.Gte => v >= Version,
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return Op switch
|
||||
return Operation switch
|
||||
{
|
||||
ComparatorOp.Neq => $"!={Version}",
|
||||
ComparatorOp.Lt => $"<{Version}",
|
||||
ComparatorOp.Lte => $"<={Version}",
|
||||
ComparatorOp.Gt => $">{Version}",
|
||||
ComparatorOp.Gte => $">={Version}",
|
||||
_ => $"{Version}",
|
||||
Comparison.Neq => $"!={Version}",
|
||||
Comparison.Lt => $"<{Version}",
|
||||
Comparison.Lte => $"<={Version}",
|
||||
Comparison.Gt => $">{Version}",
|
||||
Comparison.Gte => $">={Version}",
|
||||
Comparison.Eq or _ => $"{Version}",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// One AND-group of comparators (all must be satisfied).
|
||||
internal readonly struct ComparatorGroup(Comparator[] comparators)
|
||||
internal readonly struct ConstraintSet
|
||||
{
|
||||
public readonly Comparator[] Comparators = comparators;
|
||||
public readonly Constraint[] Constraints;
|
||||
|
||||
public Comparator? Upper => Comparators.Where(c => c.Op is ComparatorOp.Lt or ComparatorOp.Lte)
|
||||
.Select(it => (Comparator?)it).FirstOrDefault();
|
||||
public ConstraintSet(Constraint[] constraints)
|
||||
{
|
||||
Constraints = constraints;
|
||||
}
|
||||
|
||||
public Comparator? Lower => Comparators.Where(c => c.Op is ComparatorOp.Gt or ComparatorOp.Gte)
|
||||
.Select(it => (Comparator?)it).FirstOrDefault();
|
||||
public Constraint? Upper => Constraints.Where(c => c.Operation is Comparison.Lt or Comparison.Lte)
|
||||
.Select(it => (Constraint?)it).FirstOrDefault();
|
||||
|
||||
public Constraint? Lower => Constraints.Where(c => c.Operation is Comparison.Gt or Comparison.Gte)
|
||||
.Select(it => (Constraint?)it).FirstOrDefault();
|
||||
|
||||
public bool Includes(SemanticVersion v)
|
||||
{
|
||||
foreach (var c in Comparators)
|
||||
foreach (var c in Constraints)
|
||||
{
|
||||
if (!c.Includes(v))
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue