feat: add initial project setup
This commit is contained in:
commit
ed1e31314d
32 changed files with 3397 additions and 0 deletions
15
src/semver.tests/.editorconfig
Normal file
15
src/semver.tests/.editorconfig
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
|
||||
[*.{cs,vb}]
|
||||
# disable CA1822: Mark members as static
|
||||
# -> TUnit requiring instance methods for test cases
|
||||
dotnet_diagnostic.CA1822.severity = none
|
||||
# disable CA1707: Identifiers should not contain underscores
|
||||
dotnet_diagnostic.CA1707.severity = none
|
||||
# disable IDE0060: Remove unused parameter
|
||||
dotnet_diagnostic.IDE0060.severity = none
|
||||
# disable IDE0005: Unnecessary using directive
|
||||
dotnet_diagnostic.IDE0005.severity = none
|
||||
# disable IDE0390: Method can be made synchronous
|
||||
dotnet_diagnostic.IDE0390.severity = none
|
||||
# disable IDE0391: Method can be made synchronous
|
||||
dotnet_diagnostic.IDE0391.severity = none
|
||||
21
src/semver.tests/Geekeey.SemVer.Tests.csproj
Normal file
21
src/semver.tests/Geekeey.SemVer.Tests.csproj
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="TUnit" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AssemblyAttribute Include="System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverageAttribute" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\semver\Geekeey.SemVer.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
115
src/semver.tests/SemanticVersionComparerTests.cs
Normal file
115
src/semver.tests/SemanticVersionComparerTests.cs
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
// Copyright (c) The Geekeey Authors
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
namespace Geekeey.SemVer.Tests;
|
||||
|
||||
internal sealed class SemanticVersionComparerTests
|
||||
{
|
||||
[Test]
|
||||
[Arguments("1.0.0", "2.0.0", -1)]
|
||||
[Arguments("2.0.0", "1.0.0", 1)]
|
||||
[Arguments("1.0.0", "1.1.0", -1)]
|
||||
[Arguments("1.1.0", "1.0.0", 1)]
|
||||
[Arguments("1.0.0", "1.0.1", -1)]
|
||||
[Arguments("1.0.1", "1.0.0", 1)]
|
||||
[Arguments("1.0.0-alpha", "1.0.0", -1)]
|
||||
[Arguments("1.0.0", "1.0.0-alpha", 1)]
|
||||
[Arguments("1.0.0-alpha", "1.0.0-alpha.1", -1)]
|
||||
[Arguments("1.0.0-alpha.1", "1.0.0-alpha.beta", -1)]
|
||||
[Arguments("1.0.0-alpha.beta", "1.0.0-beta", -1)]
|
||||
[Arguments("1.0.0-beta", "1.0.0-beta.2", -1)]
|
||||
[Arguments("1.0.0-beta.2", "1.0.0-beta.11", -1)]
|
||||
[Arguments("1.0.0-beta.11", "1.0.0-rc.1", -1)]
|
||||
[Arguments("1.0.0-rc.1", "1.0.0", -1)]
|
||||
[Arguments("1.0.0-alpha.5", "1.0.0-alpha.10", -1)]
|
||||
[Arguments("1.0.0-alpha.10", "1.0.0-alpha.5", 1)]
|
||||
[Arguments("1.0.0-alpha.beta", "1.0.0-alpha.5", 1)]
|
||||
[Arguments("1.0.0-alpha.5", "1.0.0-alpha.beta", -1)]
|
||||
[Arguments("1.0.0-alpha.1.2", "1.0.0-alpha.1.2.3", -1)]
|
||||
[Arguments("1.0.0-alpha.1.2.3", "1.0.0-alpha.1.2", 1)]
|
||||
[Arguments("1.0.0-alpha-b", "1.0.0-alpha-a", 1)]
|
||||
[Arguments("1.0.0-0.3.7", "1.0.0-x.7.z.92", -1)]
|
||||
[Arguments("1.0.0-x.7.z.92", "1.0.0-0.3.7", 1)]
|
||||
[Arguments("1.0.0+build.1", "1.0.0+build.2", 0)]
|
||||
[Arguments("1.0.0-alpha+build.1", "1.0.0-alpha+build.2", 0)]
|
||||
public async Task I_can_compare_by_precedence(string v1Str, string v2Str, int expected)
|
||||
{
|
||||
var v1 = SemanticVersion.Parse(v1Str);
|
||||
var v2 = SemanticVersion.Parse(v2Str);
|
||||
var result = SemanticVersionComparer.Priority.Compare(v1, v2);
|
||||
await Assert.That(Math.Sign(result)).IsEqualTo(expected);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Arguments("1.0.0+build.1", "1.0.0+build.2", 0)]
|
||||
[Arguments("1.0.0-alpha+build.1", "1.0.0-alpha+build.2", 0)]
|
||||
public async Task I_can_confirm_precedence_ignores_metadata(string v1Str, string v2Str, int expected)
|
||||
{
|
||||
var v1 = SemanticVersion.Parse(v1Str);
|
||||
var v2 = SemanticVersion.Parse(v2Str);
|
||||
var result = SemanticVersionComparer.Priority.Compare(v1, v2);
|
||||
await Assert.That(result).IsEqualTo(expected);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Arguments("1.0.0", "1.0.0+build.1", -1)]
|
||||
[Arguments("1.0.0+build.1", "1.0.0", 1)]
|
||||
[Arguments("1.0.0+build.1", "1.0.0+build.2", -1)]
|
||||
[Arguments("1.0.0+build.1.1", "1.0.0+build.1", 1)]
|
||||
[Arguments("1.0.0+a.b", "1.0.0+a.a", 1)]
|
||||
[Arguments("1.0.0+a.a", "1.0.0+a.b", -1)]
|
||||
[Arguments("1.0.0+a.b", "1.0.0+a.b.c", -1)]
|
||||
[Arguments("1.0.0+a.b.c", "1.0.0+a.b", 1)]
|
||||
[Arguments("1.0.0+1.2.3", "1.0.0+1.2.4", -1)]
|
||||
[Arguments("1.0.0+1.2.4", "1.0.0+1.2.3", 1)]
|
||||
[Arguments("1.0.0+01", "1.0.0+1", -1)]
|
||||
public async Task I_can_compare_by_sort_order(string v1Str, string v2Str, int expected)
|
||||
{
|
||||
var v1 = SemanticVersion.Parse(v1Str);
|
||||
var v2 = SemanticVersion.Parse(v2Str);
|
||||
var result = SemanticVersionComparer.SortOrder.Compare(v1, v2);
|
||||
await Assert.That(Math.Sign(result)).IsEqualTo(expected);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task I_can_confirm_precedence_equality_ignores_metadata()
|
||||
{
|
||||
var v1 = SemanticVersion.Parse("1.0.0-alpha+build.1");
|
||||
var v2 = SemanticVersion.Parse("1.0.0-alpha+build.2");
|
||||
|
||||
await Assert.That(SemanticVersionComparer.Priority.Equals(v1, v2)).IsTrue();
|
||||
await Assert.That(SemanticVersionComparer.Priority.GetHashCode(v1)).IsEqualTo(SemanticVersionComparer.Priority.GetHashCode(v2));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task I_can_confirm_sort_order_equality_includes_metadata()
|
||||
{
|
||||
var v1 = SemanticVersion.Parse("1.0.0-alpha+build.1");
|
||||
var v2 = SemanticVersion.Parse("1.0.0-alpha+build.2");
|
||||
|
||||
await Assert.That(SemanticVersionComparer.SortOrder.Equals(v1, v2)).IsFalse();
|
||||
await Assert.That(SemanticVersionComparer.SortOrder.GetHashCode(v1)).IsNotEqualTo(SemanticVersionComparer.SortOrder.GetHashCode(v2));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task I_can_confirm_equality_for_identical_versions()
|
||||
{
|
||||
var v1 = SemanticVersion.Parse("1.0.0-alpha.1");
|
||||
var v2 = SemanticVersion.Parse("1.0.0-alpha.1");
|
||||
|
||||
await Assert.That(SemanticVersionComparer.Priority.Equals(v1, v2)).IsTrue();
|
||||
await Assert.That(SemanticVersionComparer.SortOrder.Equals(v1, v2)).IsTrue();
|
||||
await Assert.That(SemanticVersionComparer.Priority.GetHashCode(v1)).IsEqualTo(SemanticVersionComparer.Priority.GetHashCode(v2));
|
||||
await Assert.That(SemanticVersionComparer.SortOrder.GetHashCode(v1)).IsEqualTo(SemanticVersionComparer.SortOrder.GetHashCode(v2));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task I_can_confirm_equality_for_different_versions()
|
||||
{
|
||||
var v1 = SemanticVersion.Parse("1.0.0-alpha.1");
|
||||
var v2 = SemanticVersion.Parse("1.0.0-alpha.2");
|
||||
|
||||
await Assert.That(SemanticVersionComparer.Priority.Equals(v1, v2)).IsFalse();
|
||||
await Assert.That(SemanticVersionComparer.SortOrder.Equals(v1, v2)).IsFalse();
|
||||
}
|
||||
}
|
||||
261
src/semver.tests/SemanticVersionRangeTests.cs
Normal file
261
src/semver.tests/SemanticVersionRangeTests.cs
Normal file
|
|
@ -0,0 +1,261 @@
|
|||
// Copyright (c) The Geekeey Authors
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
namespace Geekeey.SemVer.Tests;
|
||||
|
||||
internal sealed class SemanticVersionRangeTests
|
||||
{
|
||||
[Test]
|
||||
[Arguments("1.2.3", "1.2.3", true)]
|
||||
[Arguments("1.2.3", "1.2.4", false)]
|
||||
[Arguments(">1.2.3", "1.2.4", true)]
|
||||
[Arguments(">1.2.3", "1.2.3", false)]
|
||||
[Arguments(">=1.2.3", "1.2.3", true)]
|
||||
[Arguments("<1.2.3", "1.2.2", true)]
|
||||
[Arguments("<=1.2.3", "1.2.3", true)]
|
||||
public async Task I_can_satisfy_npm_basic_ranges(string range, string version, bool expected)
|
||||
{
|
||||
var r = SemanticVersionRange.Parse(range);
|
||||
var v = SemanticVersion.Parse(version);
|
||||
await Assert.That(r.Contains(v)).IsEqualTo(expected);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Arguments("1.2.3 - 2.3.4", "1.2.3", true)]
|
||||
[Arguments("1.2.3 - 2.3.4", "2.3.4", true)]
|
||||
[Arguments("1.2.3 - 2.3.4", "1.2.2", false)]
|
||||
[Arguments("1.2.3 - 2.3.4", "2.3.5", false)]
|
||||
public async Task I_can_satisfy_npm_hyphen_ranges(string range, string version, bool expected)
|
||||
{
|
||||
var r = SemanticVersionRange.Parse(range);
|
||||
var v = SemanticVersion.Parse(version);
|
||||
await Assert.That(r.Contains(v)).IsEqualTo(expected);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Arguments("^1.2.3", "1.2.3", true)]
|
||||
[Arguments("^1.2.3", "1.9.9", true)]
|
||||
[Arguments("^1.2.3", "2.0.0", false)]
|
||||
[Arguments("^0.2.3", "0.2.3", true)]
|
||||
[Arguments("^0.2.3", "0.2.4", true)]
|
||||
[Arguments("^0.2.3", "0.3.0", false)]
|
||||
[Arguments("^0.0.3", "0.0.3", true)]
|
||||
[Arguments("^0.0.3", "0.0.4", false)]
|
||||
public async Task I_can_satisfy_npm_caret_ranges(string range, string version, bool expected)
|
||||
{
|
||||
var r = SemanticVersionRange.Parse(range);
|
||||
var v = SemanticVersion.Parse(version);
|
||||
await Assert.That(r.Contains(v)).IsEqualTo(expected);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Arguments("~1.2.3", "1.2.3", true)]
|
||||
[Arguments("~1.2.3", "1.2.9", true)]
|
||||
[Arguments("~1.2.3", "1.3.0", false)]
|
||||
[Arguments("~1.2", "1.2.0", true)]
|
||||
[Arguments("~1.2", "1.2.9", true)]
|
||||
[Arguments("~1.2", "1.3.0", false)]
|
||||
[Arguments("~1", "1.0.0", true)]
|
||||
[Arguments("~1", "1.9.9", true)]
|
||||
[Arguments("~1", "2.0.0", false)]
|
||||
public async Task I_can_satisfy_npm_tilde_ranges(string range, string version, bool expected)
|
||||
{
|
||||
var r = SemanticVersionRange.Parse(range);
|
||||
var v = SemanticVersion.Parse(version);
|
||||
await Assert.That(r.Contains(v)).IsEqualTo(expected);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Arguments("1.2.x", "1.2.0", true)]
|
||||
[Arguments("1.2.x", "1.2.9", true)]
|
||||
[Arguments("1.2.x", "1.3.0", false)]
|
||||
[Arguments("1.x", "1.0.0", true)]
|
||||
[Arguments("1.x", "1.9.9", true)]
|
||||
[Arguments("1.x", "2.0.0", false)]
|
||||
[Arguments("*", "0.0.0", true)]
|
||||
[Arguments("*", "9.9.9", true)]
|
||||
public async Task I_can_satisfy_npm_wildcard_ranges(string range, string version, bool expected)
|
||||
{
|
||||
var r = SemanticVersionRange.Parse(range);
|
||||
var v = SemanticVersion.Parse(version);
|
||||
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)]
|
||||
[Arguments(">1.2.3 || <1.0.0", "1.1.0", false)]
|
||||
[Arguments(">1.0.0 <=3.2.6 || >=1.0.0 <6.0.0", "2.0.0", true)]
|
||||
[Arguments(">1.0.0 <=3.2.6 || >=1.0.0 <6.0.0", "5.0.0", true)]
|
||||
[Arguments(">1.0.0 <=3.2.6 || >=1.0.0 <6.0.0", "7.0.0", false)]
|
||||
public async Task I_can_satisfy_npm_or_ranges(string range, string version, bool expected)
|
||||
{
|
||||
var r = SemanticVersionRange.Parse(range);
|
||||
var v = SemanticVersion.Parse(version);
|
||||
await Assert.That(r.Contains(v)).IsEqualTo(expected);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Arguments("^6.2.3-alpha.1", "6.2.3-alpha.1", true)]
|
||||
[Arguments("^6.2.3-alpha.1", "6.2.3-beta", true)]
|
||||
[Arguments("^6.2.3-alpha.1", "6.2.2", false)]
|
||||
[Arguments("^6.2.3-alpha.1", "7.0.0", false)]
|
||||
public async Task I_can_satisfy_complex_npm_ranges(string range, string version, bool expected)
|
||||
{
|
||||
var r = SemanticVersionRange.Parse(range);
|
||||
var v = SemanticVersion.Parse(version);
|
||||
await Assert.That(r.Contains(v)).IsEqualTo(expected);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task I_can_handle_two_constraints_without_combining_them()
|
||||
{
|
||||
var range = SemanticVersionRange.Parse(">=1.0.0 !=2.0.0");
|
||||
|
||||
await Assert.That(range.Contains(SemanticVersion.Parse("1.5.0"))).IsTrue();
|
||||
await Assert.That(range.Contains(SemanticVersion.Parse("2.0.0"))).IsFalse();
|
||||
await Assert.That(range.ToString("n", null)).IsEqualTo(">=1.0.0 !=2.0.0");
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Arguments("^5.*", "5.1.1", "6.1.0", ">=5.0.0 <6.0.0")]
|
||||
[Arguments("5.*", "5.1.1", "6.1.0", ">=5.0.0 <6.0.0")]
|
||||
[Arguments(">=5.*", "5.1.1", "6.1.0", ">=5.0.0 <6.0.0")]
|
||||
[Arguments(">5.*", "5.0.1", "5.0.0", ">5.0.0 <6.0.0")]
|
||||
[Arguments("<=5.*", "5.9.9", "6.0.0", "<6.0.0")]
|
||||
[Arguments("<5.*", "4.9.9", "5.0.0", "<5.0.0")]
|
||||
public async Task I_can_handle_ranges_with_wildcard(string range, string inside, string outside, string expected)
|
||||
{
|
||||
var r = SemanticVersionRange.Parse(range);
|
||||
|
||||
await Assert.That(r.Contains(SemanticVersion.Parse(inside))).IsTrue();
|
||||
await Assert.That(r.Contains(SemanticVersion.Parse(outside))).IsFalse();
|
||||
await Assert.That(r.ToString("n", null)).IsEqualTo(expected);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Arguments("[1.2.3]", "1.2.3", true)]
|
||||
[Arguments("[1.2.3]", "1.2.4", false)]
|
||||
[Arguments("[1.2.3, 1.4.0)", "1.2.3", true)]
|
||||
[Arguments("[1.2.3, 1.4.0)", "1.3.9", true)]
|
||||
[Arguments("[1.2.3, 1.4.0)", "1.4.0", false)]
|
||||
[Arguments("(1.2.3, 1.4.0]", "1.2.3", false)]
|
||||
[Arguments("(1.2.3, 1.4.0]", "1.4.0", true)]
|
||||
[Arguments("[1.2.3,)", "1.2.3", true)]
|
||||
[Arguments("[1.2.3,)", "9.9.9", true)]
|
||||
[Arguments("(,1.4.0]", "1.4.0", true)]
|
||||
[Arguments("(,1.4.0]", "0.0.0", true)]
|
||||
[Arguments("(,)", "0.0.0", true)]
|
||||
[Arguments("(,)", "9.9.9", true)]
|
||||
public async Task I_can_satisfy_maven_ranges(string range, string version, bool expected)
|
||||
{
|
||||
var r = SemanticVersionRange.Parse(range);
|
||||
var v = SemanticVersion.Parse(version);
|
||||
await Assert.That(r.Contains(v)).IsEqualTo(expected);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Arguments("[1.2,1.3],[1.5,)", "1.2.0", true)]
|
||||
[Arguments("[1.2,1.3],[1.5,)", "1.4.0", false)]
|
||||
[Arguments("[1.2,1.3],[1.5,)", "1.5.0", true)]
|
||||
public async Task I_can_satisfy_maven_or_ranges(string range, string version, bool expected)
|
||||
{
|
||||
var r = SemanticVersionRange.Parse(range);
|
||||
var v = SemanticVersion.Parse(version);
|
||||
await Assert.That(r.Contains(v)).IsEqualTo(expected);
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Arguments("^1.2.3", "1.2.3-alpha", false)]
|
||||
[Arguments("^1.2.3-alpha", "1.2.3-beta", true)]
|
||||
[Arguments("^1.2.3-alpha", "1.2.4", true)]
|
||||
[Arguments("^1.2.3-alpha", "1.3.0-alpha", true)]
|
||||
public async Task I_can_satisfy_prerelease_rules(string range, string version, bool expected)
|
||||
{
|
||||
var r = SemanticVersionRange.Parse(range);
|
||||
var v = SemanticVersion.Parse(version);
|
||||
await Assert.That(r.Contains(v)).IsEqualTo(expected);
|
||||
}
|
||||
|
||||
[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))
|
||||
.Throws<FormatException>();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task I_can_serialize_to_json()
|
||||
{
|
||||
var r = SemanticVersionRange.Parse("[1.2.3,2.0.0)");
|
||||
var json = System.Text.Json.JsonSerializer.Serialize(r);
|
||||
await Assert.That(json).IsEqualTo("\"^1.2.3\"");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task I_can_deserialize_from_json()
|
||||
{
|
||||
var json = "\"^1.2.3\"";
|
||||
var r = System.Text.Json.JsonSerializer.Deserialize<SemanticVersionRange>(json);
|
||||
await Assert.That(r.ToString()).IsEqualTo("^1.2.3");
|
||||
await Assert.That(r.Contains(new SemanticVersion(1, 2, 4))).IsTrue();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task I_use_npm_short_format_by_default()
|
||||
{
|
||||
var value = SemanticVersionRange.Parse("[1.2.3,2.0.0)");
|
||||
await Assert.That(value.ToString()).IsEqualTo("^1.2.3");
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Arguments("^1.2.3", "m", "[1.2.3,2.0.0)")]
|
||||
[Arguments("~1.2.3", "m", "[1.2.3,1.3.0)")]
|
||||
[Arguments("1.2.3", "m", "[1.2.3]")]
|
||||
[Arguments("[1.2.3,2.0.0)", "n", ">=1.2.3 <2.0.0")]
|
||||
[Arguments("[1.2.3,2.0.0)", "ns", "^1.2.3")]
|
||||
[Arguments("[1.2.3,1.3.0)", "ns", "~1.2.3")]
|
||||
[Arguments("[1.2,1.3],[1.5,)", "n", ">=1.2.0 <=1.3.0 || >=1.5.0")]
|
||||
[Arguments("*", "m", "(,)")]
|
||||
[Arguments("(,)", "ns", "*")]
|
||||
public async Task I_can_convert_range_formats(string range, string format, string expected)
|
||||
{
|
||||
var value = SemanticVersionRange.Parse(range);
|
||||
await Assert.That(value.ToString(format, null)).IsEqualTo(expected);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task I_can_format_ranges_to_chars()
|
||||
{
|
||||
var value = SemanticVersionRange.Parse("[1.2.3,2.0.0)");
|
||||
var destination = new char[32];
|
||||
var success = value.TryFormat(destination, out var charsWritten, "ns", null);
|
||||
|
||||
await Assert.That(success).IsTrue();
|
||||
await Assert.That(new string(destination[..charsWritten])).IsEqualTo("^1.2.3");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task I_fail_formatting_when_the_tentative_short_form_overflows()
|
||||
{
|
||||
var value = SemanticVersionRange.Parse("[1.2.3,2.0.0)");
|
||||
var destination = new char[5];
|
||||
var success = value.TryFormat(destination, out var charsWritten, "ns", null);
|
||||
|
||||
await Assert.That(success).IsFalse();
|
||||
await Assert.That(charsWritten).IsEqualTo(0);
|
||||
}
|
||||
}
|
||||
225
src/semver.tests/SemanticVersionTests.cs
Normal file
225
src/semver.tests/SemanticVersionTests.cs
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
// Copyright (c) The Geekeey Authors
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Geekeey.SemVer.Tests;
|
||||
|
||||
internal sealed class SemanticVersionTests
|
||||
{
|
||||
private static readonly JsonSerializerOptions RelaxedOptions = new()
|
||||
{
|
||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
|
||||
};
|
||||
|
||||
[Test]
|
||||
[Arguments(0, 0, 0, 0)]
|
||||
[Arguments(1, 1, 1, 0)]
|
||||
[Arguments(1, 2, 0, 0)]
|
||||
[Arguments(45, 2, 4, 0)]
|
||||
public async Task I_can_create_semver_from_version(int major, int minor, int build, int revision)
|
||||
{
|
||||
var version = new Version(major, minor, build, revision);
|
||||
var v = new SemanticVersion(version);
|
||||
|
||||
await Assert.That(v.Major).IsEqualTo((ulong)major);
|
||||
await Assert.That(v.Minor).IsEqualTo((ulong)minor);
|
||||
await Assert.That(v.Patch).IsEqualTo((ulong)build);
|
||||
await Assert.That(v.Prerelease).IsNull();
|
||||
await Assert.That(v.Metadata).IsNull();
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Arguments(1UL)]
|
||||
[Arguments(0UL)]
|
||||
public async Task I_can_create_semver_with_major(ulong major)
|
||||
{
|
||||
var v = new SemanticVersion(major);
|
||||
|
||||
await Assert.That(v.Major).IsEqualTo(major);
|
||||
await Assert.That(v.Minor).IsEqualTo(0UL);
|
||||
await Assert.That(v.Patch).IsEqualTo(0UL);
|
||||
await Assert.That(v.Prerelease).IsNull();
|
||||
await Assert.That(v.Metadata).IsNull();
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Arguments(1UL, 2UL)]
|
||||
public async Task I_can_create_semver_with_major_and_minor(ulong major, ulong minor)
|
||||
{
|
||||
var v = new SemanticVersion(major, minor);
|
||||
|
||||
await Assert.That(v.Major).IsEqualTo(major);
|
||||
await Assert.That(v.Minor).IsEqualTo(minor);
|
||||
await Assert.That(v.Patch).IsEqualTo(0UL);
|
||||
await Assert.That(v.Prerelease).IsNull();
|
||||
await Assert.That(v.Metadata).IsNull();
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Arguments(1UL, 2UL, 3UL)]
|
||||
public async Task I_can_create_semver_with_major_minor_and_patch(ulong major, ulong minor, ulong patch)
|
||||
{
|
||||
var v = new SemanticVersion(major, minor, patch);
|
||||
|
||||
await Assert.That(v.Major).IsEqualTo(major);
|
||||
await Assert.That(v.Minor).IsEqualTo(minor);
|
||||
await Assert.That(v.Patch).IsEqualTo(patch);
|
||||
await Assert.That(v.Prerelease).IsNull();
|
||||
await Assert.That(v.Metadata).IsNull();
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Arguments(1UL, 2UL, 3UL, "alpha", "build.1")]
|
||||
[Arguments(1UL, 2UL, 3UL, null, null)]
|
||||
public async Task I_can_create_semver_with_all_components(ulong major, ulong minor, ulong patch, string? prerelease, string? metadata)
|
||||
{
|
||||
var v = new SemanticVersion(major, minor, patch, prerelease, metadata);
|
||||
|
||||
await Assert.That(v.Major).IsEqualTo(major);
|
||||
await Assert.That(v.Minor).IsEqualTo(minor);
|
||||
await Assert.That(v.Patch).IsEqualTo(patch);
|
||||
await Assert.That(v.Prerelease).IsEqualTo(prerelease);
|
||||
await Assert.That(v.Metadata).IsEqualTo(metadata);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task I_can_compare_identical_versions()
|
||||
{
|
||||
var v1 = new SemanticVersion(1, 2, 3);
|
||||
var v2 = new SemanticVersion(1, 2, 3);
|
||||
|
||||
await Assert.That(v1.CompareTo(v2)).IsEqualTo(0);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task I_can_compare_with_objects()
|
||||
{
|
||||
var v1 = new SemanticVersion(1, 2, 3);
|
||||
object v2 = new SemanticVersion(1, 2, 3);
|
||||
|
||||
await Assert.That(v1.CompareTo(v2)).IsEqualTo(0);
|
||||
await Assert.That(v1.CompareTo(null)).IsEqualTo(1);
|
||||
await Assert.That(v1.CompareTo("not a version")).IsEqualTo(1);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task I_can_confirm_operators_work()
|
||||
{
|
||||
var v1 = new SemanticVersion(1, 2, 3);
|
||||
var v2 = new SemanticVersion(2, 0, 0);
|
||||
|
||||
await Assert.That(v1 < v2).IsTrue();
|
||||
await Assert.That(v1 <= v2).IsTrue();
|
||||
await Assert.That(v1 > v2).IsFalse();
|
||||
await Assert.That(v1 >= v2).IsFalse();
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task I_can_parse_from_string()
|
||||
{
|
||||
var result = SemanticVersion.Parse("1.2.3");
|
||||
await Assert.That(result.Major).IsEqualTo(1UL);
|
||||
await Assert.That(result.Minor).IsEqualTo(2UL);
|
||||
await Assert.That(result.Patch).IsEqualTo(3UL);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task I_can_try_parse_from_string()
|
||||
{
|
||||
var success = SemanticVersion.TryParse("1.2.3-alpha+build.1", out var result);
|
||||
await Assert.That(success).IsTrue();
|
||||
await Assert.That(result.Major).IsEqualTo(1UL);
|
||||
await Assert.That(result.Minor).IsEqualTo(2UL);
|
||||
await Assert.That(result.Patch).IsEqualTo(3UL);
|
||||
await Assert.That(result.Prerelease).IsEqualTo("alpha");
|
||||
await Assert.That(result.Metadata).IsEqualTo("build.1");
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Arguments(1, 2, 3, null, null, "1.2.3")]
|
||||
[Arguments(1, 2, 3, "alpha", null, "1.2.3-alpha")]
|
||||
[Arguments(1, 2, 3, "alpha", "build.1", "1.2.3-alpha+build.1")]
|
||||
public async Task I_can_get_string_representation(ulong major, ulong minor, ulong patch, string? pre, string? meta, string expected)
|
||||
{
|
||||
var v = new SemanticVersion(major, minor, patch, pre, meta);
|
||||
// Note: format "f" is needed for full version including metadata based on code
|
||||
var format = string.IsNullOrEmpty(meta) ? (string.IsNullOrEmpty(pre) ? null : "s") : "f";
|
||||
|
||||
await Assert.That(v.ToString(format, null)).IsEqualTo(expected);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task I_can_format_to_chars()
|
||||
{
|
||||
var v = new SemanticVersion(1, 2, 3, "beta", "456");
|
||||
var dest = new char[32];
|
||||
var success = v.TryFormat(dest, out var charsWritten, "f", null);
|
||||
|
||||
await Assert.That(success).IsTrue();
|
||||
await Assert.That(new string(dest[..charsWritten])).IsEqualTo("1.2.3-beta+456");
|
||||
}
|
||||
|
||||
[Test]
|
||||
[Arguments(1, 2, 3, null, null, "\"1.2.3\"")]
|
||||
[Arguments(1, 2, 3, "alpha", null, "\"1.2.3-alpha\"")]
|
||||
[Arguments(1, 2, 3, "alpha", "build.1", "\"1.2.3-alpha+build.1\"")]
|
||||
public async Task I_can_serialize_to_json(ulong major, ulong minor, ulong patch, string? pre, string? meta, string expectedJson)
|
||||
{
|
||||
var v = new SemanticVersion(major, minor, patch, pre, meta);
|
||||
var json = JsonSerializer.Serialize(v, RelaxedOptions);
|
||||
|
||||
await Assert.That(json).IsEqualTo(expectedJson);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task I_can_deserialize_from_json()
|
||||
{
|
||||
var json = "\"1.2.3-beta+789\"";
|
||||
var v = JsonSerializer.Deserialize<SemanticVersion>(json);
|
||||
|
||||
await Assert.That(v.Major).IsEqualTo(1UL);
|
||||
await Assert.That(v.Minor).IsEqualTo(2UL);
|
||||
await Assert.That(v.Patch).IsEqualTo(3UL);
|
||||
await Assert.That(v.Prerelease).IsEqualTo("beta");
|
||||
await Assert.That(v.Metadata).IsEqualTo("789");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task I_can_handle_invalid_json_token()
|
||||
{
|
||||
var json = "123"; // Number instead of string
|
||||
|
||||
await Assert.That(() => JsonSerializer.Deserialize<SemanticVersion>(json))
|
||||
.Throws<JsonException>()
|
||||
.WithMessage("Expected string");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task I_can_serialize_as_part_of_object()
|
||||
{
|
||||
var obj = new { Version = new SemanticVersion(1, 0, 0, "rc.1", "metadata") };
|
||||
var json = JsonSerializer.Serialize(obj, RelaxedOptions);
|
||||
|
||||
await Assert.That(json).IsEqualTo(/*lang=json,strict*/ "{\"Version\":\"1.0.0-rc.1+metadata\"}");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task I_can_deserialize_null()
|
||||
{
|
||||
var json = "null";
|
||||
var v = JsonSerializer.Deserialize<SemanticVersion>(json);
|
||||
|
||||
await Assert.That(v).IsEqualTo(default(SemanticVersion));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task I_can_deserialize_empty_string()
|
||||
{
|
||||
var json = "\"\"";
|
||||
|
||||
await Assert.That(() => JsonSerializer.Deserialize<SemanticVersion>(json))
|
||||
.Throws<JsonException>();
|
||||
}
|
||||
}
|
||||
31
src/semver/Geekeey.SemVer.csproj
Normal file
31
src/semver/Geekeey.SemVer.csproj
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Library</OutputType>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<IsPackable>true</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<GenerateDocumentationFile>true</GenerateDocumentationFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup Condition="'$(Configuration)' == 'Debug'">
|
||||
<InternalsVisibleTo Include="Geekeey.SemVer.Tests" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<PackageReadmeFile>package-readme.md</PackageReadmeFile>
|
||||
<PackageDescription>A .NET library for parsing, comparing, formatting, and serializing semantic versions and version ranges.</PackageDescription>
|
||||
<PackageIcon>package-icon.png</PackageIcon>
|
||||
<PackageProjectUrl>https://code.geekeey.de/geekeey/semver/src/branch/main/src/semver</PackageProjectUrl>
|
||||
<PackageLicenseExpression>EUPL-1.2</PackageLicenseExpression>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Include=".\package-icon.png" Pack="true" PackagePath="\" Visible="false" />
|
||||
<None Include=".\package-readme.md" Pack="true" PackagePath="\" Visible="false" />
|
||||
<None Include="..\..\LICENSE.md" Pack="true" PackagePath="\" Visible="false" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
51
src/semver/SemanticVersion.Comparison.cs
Normal file
51
src/semver/SemanticVersion.Comparison.cs
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
// Copyright (c) The Geekeey Authors
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
namespace Geekeey.SemVer;
|
||||
|
||||
public readonly partial record struct SemanticVersion : IComparable, IComparable<SemanticVersion>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public int CompareTo(object? obj)
|
||||
{
|
||||
return obj is not SemanticVersion other ? 1 : CompareTo(other);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public int CompareTo(SemanticVersion other)
|
||||
{
|
||||
return SemanticVersionComparer.Priority.Compare(this, other);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether one version is less than another version.
|
||||
/// </summary>
|
||||
public static bool operator <(SemanticVersion left, SemanticVersion right)
|
||||
{
|
||||
return left.CompareTo(right) < 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether one version is less than or equal to another version.
|
||||
/// </summary>
|
||||
public static bool operator <=(SemanticVersion left, SemanticVersion right)
|
||||
{
|
||||
return left.CompareTo(right) <= 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether one version is greater than another version.
|
||||
/// </summary>
|
||||
public static bool operator >(SemanticVersion left, SemanticVersion right)
|
||||
{
|
||||
return left.CompareTo(right) > 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether one version is greater than or equal to another version.
|
||||
/// </summary>
|
||||
public static bool operator >=(SemanticVersion left, SemanticVersion right)
|
||||
{
|
||||
return left.CompareTo(right) >= 0;
|
||||
}
|
||||
}
|
||||
112
src/semver/SemanticVersion.Formatting.cs
Normal file
112
src/semver/SemanticVersion.Formatting.cs
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
// Copyright (c) The Geekeey Authors
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Geekeey.SemVer;
|
||||
|
||||
public readonly partial record struct SemanticVersion : ISpanFormattable
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return ToString(null, null);
|
||||
}
|
||||
|
||||
#region IFormattable
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <remarks>
|
||||
/// <list type="bullet">
|
||||
/// <item><description>s - Default SemVer - [1.2.3-beta.4]</description></item>
|
||||
/// <item><description>f - Full SemVer - [1.2.3-beta.4+5]</description></item>
|
||||
/// <item><description>r - Just the SemVer part relevant for compatibility comparison - [1.2.3]</description></item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public string ToString(string? format, IFormatProvider? formatProvider)
|
||||
{
|
||||
if (format is not null and not "s" and not "f" and not "r")
|
||||
{
|
||||
throw new FormatException($"The format string '{format}' is not supported.");
|
||||
}
|
||||
|
||||
var handler = new DefaultInterpolatedStringHandler(0, 1, formatProvider);
|
||||
handler.AppendFormatted(this, format);
|
||||
return handler.ToStringAndClear();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ISpanFormattable
|
||||
|
||||
/// <summary>
|
||||
/// Tries to format the semantic version into the specified span of characters.
|
||||
/// </summary>
|
||||
/// <see cref="TryFormat(Span{char},out int,ReadOnlySpan{char},IFormatProvider)">ISpanFormattable.TryFormat</see>
|
||||
public bool TryFormat(Span<char> destination, out int charsWritten)
|
||||
{
|
||||
return TryFormat(destination, out charsWritten, default, null);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <remarks>
|
||||
/// <list type="bullet">
|
||||
/// <item><description>s - Default SemVer - [1.2.3-beta.4]</description></item>
|
||||
/// <item><description>f - Full SemVer - [1.2.3-beta.4+5]</description></item>
|
||||
/// <item><description>r - Just the SemVer part relevant for compatibility comparison - [1.2.3]</description></item>
|
||||
/// </list>
|
||||
/// </remarks>
|
||||
public bool TryFormat(Span<char> destination, out int charsWritten, ReadOnlySpan<char> format, IFormatProvider? provider)
|
||||
{
|
||||
charsWritten = 0;
|
||||
|
||||
if (format.IsEmpty)
|
||||
{
|
||||
format = "s";
|
||||
}
|
||||
|
||||
if (format is not "s" and not "f" and not "r")
|
||||
{
|
||||
throw new FormatException($"The format string '{format}' is not supported.");
|
||||
}
|
||||
|
||||
if (!destination.TryWrite($"{Major}.{Minor}.{Patch}", out var written))
|
||||
{
|
||||
charsWritten = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
destination = destination[written..];
|
||||
charsWritten += written;
|
||||
|
||||
if (Prerelease is { Length: > 0 } && format is "s" or "f")
|
||||
{
|
||||
if (!destination.TryWrite(provider, $"-{Prerelease}", out written))
|
||||
{
|
||||
charsWritten = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
destination = destination[written..];
|
||||
charsWritten += written;
|
||||
}
|
||||
|
||||
if (Metadata is { Length: > 0 } && format is "f")
|
||||
{
|
||||
if (!destination.TryWrite(provider, $"+{Metadata}", out written))
|
||||
{
|
||||
charsWritten = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
destination = destination[written..];
|
||||
charsWritten += written;
|
||||
}
|
||||
|
||||
_ = destination;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
41
src/semver/SemanticVersion.JsonConverter.cs
Normal file
41
src/semver/SemanticVersion.JsonConverter.cs
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
// Copyright (c) The Geekeey Authors
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Geekeey.SemVer;
|
||||
|
||||
[JsonConverter(typeof(SemanticVersionJsonConverter))]
|
||||
public readonly partial record struct SemanticVersion
|
||||
{
|
||||
internal sealed class SemanticVersionJsonConverter : JsonConverter<SemanticVersion>
|
||||
{
|
||||
public override SemanticVersion Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType is JsonTokenType.Null)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
if (reader.TokenType is not JsonTokenType.String || reader.GetString() is not { } value)
|
||||
{
|
||||
throw new JsonException("Expected string");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return Parse(value);
|
||||
}
|
||||
catch (FormatException exception)
|
||||
{
|
||||
throw new JsonException(exception.Message, exception);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, SemanticVersion value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteStringValue(value.ToString("f", null));
|
||||
}
|
||||
}
|
||||
}
|
||||
155
src/semver/SemanticVersion.Parsing.cs
Normal file
155
src/semver/SemanticVersion.Parsing.cs
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
// Copyright (c) The Geekeey Authors
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
|
||||
namespace Geekeey.SemVer;
|
||||
|
||||
public readonly partial record struct SemanticVersion : ISpanParsable<SemanticVersion>
|
||||
{
|
||||
#region IParsable
|
||||
|
||||
/// <summary>
|
||||
/// Parses a string into a <see cref="SemanticVersion"/>.
|
||||
/// </summary>
|
||||
/// <see cref="Parse(string, IFormatProvider)"/>
|
||||
public static SemanticVersion Parse(string s)
|
||||
{
|
||||
return Parse(s.AsSpan(), null);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public static SemanticVersion Parse(string s, IFormatProvider? provider)
|
||||
{
|
||||
return Parse(s.AsSpan(), provider);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to parse a string into a <see cref="SemanticVersion"/>.
|
||||
/// </summary>
|
||||
/// <see cref="TryParse(string, IFormatProvider, out SemanticVersion)"/>
|
||||
public static bool TryParse([NotNullWhen(true)] string? s, [MaybeNullWhen(false)] out SemanticVersion result)
|
||||
{
|
||||
return TryParse(s, null, out result);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [MaybeNullWhen(false)] out SemanticVersion result)
|
||||
{
|
||||
return TryParse(s.AsSpan(), provider, out result);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ISpanParsable
|
||||
|
||||
/// <summary>
|
||||
/// Parses a span of characters into a <see cref="SemanticVersion"/>.
|
||||
/// </summary>
|
||||
/// <see cref="Parse(ReadOnlySpan{char}, IFormatProvider)"/>
|
||||
public static SemanticVersion Parse(ReadOnlySpan<char> s)
|
||||
{
|
||||
return Parse(s, null);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public static SemanticVersion Parse(ReadOnlySpan<char> s, IFormatProvider? provider)
|
||||
{
|
||||
if (!TryParse(s, provider, out var result))
|
||||
{
|
||||
throw new FormatException($"The input string '{s}' was not in a correct format.");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to parse a span of characters into a <see cref="SemanticVersion"/>.
|
||||
/// </summary>
|
||||
/// <see cref="TryParse(ReadOnlySpan{char}, IFormatProvider, out SemanticVersion)"/>
|
||||
public static bool TryParse(ReadOnlySpan<char> s, [MaybeNullWhen(false)] out SemanticVersion result)
|
||||
{
|
||||
return TryParse(s, null, out result);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public static bool TryParse(ReadOnlySpan<char> s, IFormatProvider? provider, [MaybeNullWhen(false)] out SemanticVersion result)
|
||||
{
|
||||
return TryParsePartially(s, out result, out var components) && components is 3;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
// components = 0: wildcard at major
|
||||
// components = 1: wildcard at minor
|
||||
// components = 2: wildcard at patch
|
||||
// components = 3: no wildcards
|
||||
internal static bool TryParsePartially(ReadOnlySpan<char> s, out SemanticVersion version, out int components)
|
||||
{
|
||||
version = default;
|
||||
components = 0;
|
||||
|
||||
if (s.IsEmpty)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var metadata = default(string);
|
||||
if (s.IndexOf('+') is >= 0 and var metadataIndex)
|
||||
{
|
||||
if (s[(metadataIndex + 1)..] is not { IsEmpty: false } value)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
metadata = new string(value);
|
||||
s = s[..metadataIndex];
|
||||
}
|
||||
|
||||
var prerelease = default(string);
|
||||
if (s.IndexOf('-') is >= 0 and var prereleaseIndex)
|
||||
{
|
||||
if (s[(prereleaseIndex + 1)..] is not { IsEmpty: false } value)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
prerelease = new string(value);
|
||||
s = s[..prereleaseIndex];
|
||||
}
|
||||
|
||||
Span<Range> destination = stackalloc Range[3];
|
||||
Span<ulong> component = stackalloc ulong[3];
|
||||
|
||||
foreach (var range in destination[..s.Split(destination, '.')])
|
||||
{
|
||||
if (s[range] is not { IsEmpty: false } segment)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (segment is "*" or "x" or "X")
|
||||
{
|
||||
version = components switch
|
||||
{
|
||||
0 => default,
|
||||
1 => new SemanticVersion(component[0], 0, 0),
|
||||
2 => new SemanticVersion(component[0], component[1], 0),
|
||||
_ => throw new InvalidOperationException(),
|
||||
};
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!ulong.TryParse(segment, NumberStyles.None, CultureInfo.InvariantCulture, out var value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
component[components++] = value;
|
||||
}
|
||||
|
||||
version = new SemanticVersion(component[0], component[1], component[2], prerelease, metadata);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
108
src/semver/SemanticVersion.cs
Normal file
108
src/semver/SemanticVersion.cs
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
// Copyright (c) The Geekeey Authors
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
namespace Geekeey.SemVer;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a semantic version that adheres to the Semantic Versioning 2.0.0 specification.
|
||||
/// </summary>
|
||||
public readonly partial record struct SemanticVersion
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts a <see cref="Version"/> into the equivalent semantic version.
|
||||
/// </summary>
|
||||
/// <param name="version">The version to be converted to a semantic version.</param>
|
||||
/// <remarks>
|
||||
/// <see cref="Version"/> numbers have the form <em>major</em>.<em>minor</em>[.<em>build</em>[.<em>revision</em>]]
|
||||
/// where square brackets ('[' and ']') indicate optional components. The first three parts are converted to the
|
||||
/// major, minor, and patch version numbers of a semantic version.
|
||||
/// </remarks>
|
||||
public SemanticVersion(Version version)
|
||||
: this((ulong)version.Major, (ulong)version.Minor, (ulong)version.Build)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new instance of the <see cref="SemanticVersion" /> class.
|
||||
/// </summary>
|
||||
/// <param name="major">The major version number.</param>
|
||||
public SemanticVersion(ulong major)
|
||||
: this(major, 0, 0, default, default)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new instance of the <see cref="SemanticVersion" /> class.
|
||||
/// </summary>
|
||||
/// <param name="major">The major version number.</param>
|
||||
/// <param name="minor">The minor version number.</param>
|
||||
public SemanticVersion(ulong major, ulong minor)
|
||||
: this(major, minor, 0, default, default)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new instance of the <see cref="SemanticVersion" /> class.
|
||||
/// </summary>
|
||||
/// <param name="major">The major version number.</param>
|
||||
/// <param name="minor">The minor version number.</param>
|
||||
/// <param name="patch">The patch version number.</param>
|
||||
public SemanticVersion(ulong major, ulong minor, ulong patch)
|
||||
: this(major, minor, patch, default, default)
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Constructs a new instance of the <see cref="SemanticVersion" /> class.
|
||||
/// </summary>
|
||||
/// <param name="major">The major version number.</param>
|
||||
/// <param name="minor">The minor version number.</param>
|
||||
/// <param name="patch">The patch version number.</param>
|
||||
/// <param name="prerelease">The prerelease identifiers.</param>
|
||||
/// <param name="metadata">The build metadata identifiers.</param>
|
||||
public SemanticVersion(ulong major, ulong minor, ulong patch, string? prerelease, string? metadata)
|
||||
{
|
||||
Major = major;
|
||||
Minor = minor;
|
||||
Patch = patch;
|
||||
Prerelease = prerelease;
|
||||
Metadata = metadata;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the major version number of the semantic version. This value is a non-negative integer
|
||||
/// representing the most significant component of the version, typically incremented when
|
||||
/// making incompatible API changes.
|
||||
/// </summary>
|
||||
public ulong Major { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the minor version number of the semantic version. This value is a non-negative integer
|
||||
/// representing the second most significant component of the version, typically incremented
|
||||
/// when adding backward-compatible functionalities.
|
||||
/// </summary>
|
||||
public ulong Minor { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the patch version number of the semantic version. This value is a non-negative integer
|
||||
/// representing the least significant component of the version, typically incremented for backwards-compatible
|
||||
/// bug fixes or other small changes.
|
||||
/// </summary>
|
||||
public ulong Patch { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the prerelease label of the semantic version. This value is an optional string
|
||||
/// indicating a version that is still in development or not yet considered stable.
|
||||
/// Examples of prerelease labels include "alpha", "beta", or "rc".
|
||||
/// When present, the prerelease label is used to differentiate between versions with
|
||||
/// the same major, minor, and patch numbers.
|
||||
/// </summary>
|
||||
public string? Prerelease { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the build metadata associated with the semantic version. This optional value provides
|
||||
/// additional information about the build, such as build number, commit hash, or other relevant
|
||||
/// details. It does not affect the precedence of the version.
|
||||
/// </summary>
|
||||
public string? Metadata { get; }
|
||||
}
|
||||
307
src/semver/SemanticVersionComparer.cs
Normal file
307
src/semver/SemanticVersionComparer.cs
Normal file
|
|
@ -0,0 +1,307 @@
|
|||
// Copyright (c) The Geekeey Authors
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
using System.Collections;
|
||||
|
||||
namespace Geekeey.SemVer;
|
||||
|
||||
/// <summary>
|
||||
/// Provides comparers for <see cref="SemanticVersion"/>.
|
||||
/// </summary>
|
||||
public abstract class SemanticVersionComparer : IEqualityComparer<SemanticVersion?>, IEqualityComparer, IComparer<SemanticVersion?>, IComparer
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the comparer that determines the precedence of semantic versions.
|
||||
/// </summary>
|
||||
public static SemanticVersionComparer Priority { get; } = new PrecedenceSemanticVersionComparer();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the comparer that determines the sort order of semantic versions.
|
||||
/// </summary>
|
||||
public static SemanticVersionComparer SortOrder { get; } = new SortOrderSemanticVersionComparer();
|
||||
|
||||
bool IEqualityComparer.Equals(object? x, object? y)
|
||||
{
|
||||
if (ReferenceEquals(x, y))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (x is null || y is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (x is SemanticVersion v1 && y is SemanticVersion v2)
|
||||
{
|
||||
return Equals(v1, v2);
|
||||
}
|
||||
|
||||
throw new ArgumentException($"Type of argument is not {nameof(SemanticVersion)}.");
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public abstract bool Equals(SemanticVersion? x, SemanticVersion? y);
|
||||
|
||||
int IEqualityComparer.GetHashCode(object? obj)
|
||||
{
|
||||
if (obj is null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (obj is SemanticVersion v)
|
||||
{
|
||||
return GetHashCode(v);
|
||||
}
|
||||
|
||||
throw new ArgumentException($"Type of argument is not {nameof(SemanticVersion)}.");
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public abstract int GetHashCode(SemanticVersion? v);
|
||||
|
||||
int IComparer.Compare(object? x, object? y)
|
||||
{
|
||||
if (x is null && y is null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (x is null)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (y is null)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (x is SemanticVersion v1 && y is SemanticVersion v2)
|
||||
{
|
||||
return Compare(v1, v2);
|
||||
}
|
||||
|
||||
throw new ArgumentException($"Type of argument is not {nameof(SemanticVersion)}.");
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public abstract int Compare(SemanticVersion? x, SemanticVersion? y);
|
||||
|
||||
/// <para>Precedence order is determined by comparing the major, minor, patch, and prerelease portion in order from left to right. Versions that differ only by build metadata have the same precedence. The major, minor, and patch version numbers are compared numerically. A prerelease version precedes a release version.</para>
|
||||
/// <para>The prerelease portion is compared by comparing each prerelease identifier from left to right. Numeric prerelease identifiers precede alphanumeric identifiers. Numeric identifiers are compared numerically. Alphanumeric identifiers are compared lexically in ASCII sort order. A longer series of prerelease identifiers follows a shorter series if all the preceding identifiers are equal.</para>
|
||||
private sealed class PrecedenceSemanticVersionComparer : SemanticVersionComparer
|
||||
{
|
||||
public override bool Equals(SemanticVersion? x, SemanticVersion? y)
|
||||
{
|
||||
if (x is null && y is null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (x is null || y is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var v1 = x.Value;
|
||||
var v2 = y.Value;
|
||||
|
||||
return v1.Major == v2.Major &&
|
||||
v1.Minor == v2.Minor &&
|
||||
v1.Patch == v2.Patch &&
|
||||
string.Equals(v1.Prerelease, v2.Prerelease, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
public override int GetHashCode(SemanticVersion? v)
|
||||
{
|
||||
if (v is null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var ver = v.Value;
|
||||
return HashCode.Combine(ver.Major, ver.Minor, ver.Patch, ver.Prerelease);
|
||||
}
|
||||
|
||||
public override int Compare(SemanticVersion? x, SemanticVersion? y)
|
||||
{
|
||||
if (x is null && y is null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (x is null)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (y is null)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
var v1 = x.Value;
|
||||
var v2 = y.Value;
|
||||
|
||||
var cmp = v1.Major.CompareTo(v2.Major);
|
||||
if (cmp is not 0)
|
||||
{
|
||||
return cmp;
|
||||
}
|
||||
|
||||
cmp = v1.Minor.CompareTo(v2.Minor);
|
||||
if (cmp is not 0)
|
||||
{
|
||||
return cmp;
|
||||
}
|
||||
|
||||
cmp = v1.Patch.CompareTo(v2.Patch);
|
||||
if (cmp is not 0)
|
||||
{
|
||||
return cmp;
|
||||
}
|
||||
|
||||
return ComparePrerelease(v1.Prerelease, v2.Prerelease);
|
||||
}
|
||||
}
|
||||
|
||||
/// <remarks>
|
||||
/// <para>Sort order is consistent with precedence order, but provides an order for versions with the same precedence. Sort order is determined by comparing the major, minor, patch, prerelease portion, and build metadata in order from left to right. The major, minor, and patch version numbers are compared numerically. A prerelease version precedes a release version.</para>
|
||||
/// <para>The prerelease portion is compared by comparing each prerelease identifier from left to right. Numeric prerelease identifiers precede alphanumeric identifiers. Numeric identifiers are compared numerically. Alphanumeric identifiers are compared lexically in ASCII sort order. A longer series of prerelease identifiers follows a shorter series if all the preceding identifiers are equal.</para>
|
||||
/// <para>Otherwise, equal versions without build metadata precede those with metadata. The build metadata is compared by comparing each metadata identifier. Identifiers are compared lexically in ASCII sort order. A longer series of metadata identifiers follows a shorter series if all the preceding identifiers are equal.</para>
|
||||
/// </remarks>
|
||||
private sealed class SortOrderSemanticVersionComparer : SemanticVersionComparer
|
||||
{
|
||||
public override bool Equals(SemanticVersion? x, SemanticVersion? y)
|
||||
{
|
||||
if (x is null && y is null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (x is null || y is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var v1 = x.Value;
|
||||
var v2 = y.Value;
|
||||
|
||||
return v1.Major == v2.Major &&
|
||||
v1.Minor == v2.Minor &&
|
||||
v1.Patch == v2.Patch &&
|
||||
string.Equals(v1.Prerelease, v2.Prerelease, StringComparison.Ordinal) &&
|
||||
string.Equals(v1.Metadata, v2.Metadata, StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
public override int GetHashCode(SemanticVersion? v)
|
||||
{
|
||||
if (v is null)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var ver = v.Value;
|
||||
return HashCode.Combine(ver.Major, ver.Minor, ver.Patch, ver.Prerelease, ver.Metadata);
|
||||
}
|
||||
|
||||
public override int Compare(SemanticVersion? x, SemanticVersion? y)
|
||||
{
|
||||
var cmp = Priority.Compare(x, y);
|
||||
return cmp is not 0 ? cmp : CompareMetadata(x?.Metadata, y?.Metadata);
|
||||
}
|
||||
}
|
||||
|
||||
private static int ComparePrerelease(string? x, string? y)
|
||||
{
|
||||
if (string.Equals(x, y, StringComparison.Ordinal))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (x is null)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (y is null)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
var xParts = x.Split('.');
|
||||
var yParts = y.Split('.');
|
||||
|
||||
var length = Math.Min(xParts.Length, yParts.Length);
|
||||
for (var i = 0; i < length; i++)
|
||||
{
|
||||
var xPart = xParts[i];
|
||||
var yPart = yParts[i];
|
||||
|
||||
var xIsNumeric = ulong.TryParse(xPart, out var xNum);
|
||||
var yIsNumeric = ulong.TryParse(yPart, out var yNum);
|
||||
|
||||
if (xIsNumeric && yIsNumeric)
|
||||
{
|
||||
var cmp = xNum.CompareTo(yNum);
|
||||
if (cmp is not 0)
|
||||
{
|
||||
return cmp;
|
||||
}
|
||||
}
|
||||
else if (xIsNumeric)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
else if (yIsNumeric)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
var cmp = string.CompareOrdinal(xPart, yPart);
|
||||
if (cmp is not 0)
|
||||
{
|
||||
return cmp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return xParts.Length.CompareTo(yParts.Length);
|
||||
}
|
||||
|
||||
private static int CompareMetadata(string? x, string? y)
|
||||
{
|
||||
if (string.Equals(x, y, StringComparison.Ordinal))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (x is null)
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (y is null)
|
||||
{
|
||||
return 1;
|
||||
}
|
||||
|
||||
var xParts = x.Split('.');
|
||||
var yParts = y.Split('.');
|
||||
|
||||
var length = Math.Min(xParts.Length, yParts.Length);
|
||||
for (var i = 0; i < length; i++)
|
||||
{
|
||||
var cmp = string.CompareOrdinal(xParts[i], yParts[i]);
|
||||
if (cmp is not 0)
|
||||
{
|
||||
return cmp;
|
||||
}
|
||||
}
|
||||
|
||||
return xParts.Length.CompareTo(yParts.Length);
|
||||
}
|
||||
}
|
||||
226
src/semver/SemanticVersionRange.Formatting.cs
Normal file
226
src/semver/SemanticVersionRange.Formatting.cs
Normal file
|
|
@ -0,0 +1,226 @@
|
|||
// Copyright (c) The Geekeey Authors
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Geekeey.SemVer;
|
||||
|
||||
public readonly partial record struct SemanticVersionRange : ISpanFormattable
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return ToString(null, null);
|
||||
}
|
||||
|
||||
#region IFormattable
|
||||
|
||||
/// <inheritdoc />
|
||||
public string ToString(string? format, IFormatProvider? formatProvider)
|
||||
{
|
||||
if (format is not null and not "m" and not "n" and not "ns")
|
||||
{
|
||||
throw new FormatException($"The format string '{format}' is not supported.");
|
||||
}
|
||||
|
||||
var handler = new DefaultInterpolatedStringHandler(0, 1, formatProvider);
|
||||
handler.AppendFormatted(this, format);
|
||||
return handler.ToStringAndClear();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ISpanFormattable
|
||||
|
||||
/// <summary>
|
||||
/// Tries to format the semantic version range into the specified span of characters.
|
||||
/// </summary>
|
||||
/// <see cref="TryFormat(Span{char},out int,ReadOnlySpan{char},IFormatProvider)">ISpanFormattable.TryFormat</see>
|
||||
public bool TryFormat(Span<char> destination, out int charsWritten)
|
||||
{
|
||||
return TryFormat(destination, out charsWritten, default, null);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool TryFormat(Span<char> destination, out int charsWritten, ReadOnlySpan<char> format, IFormatProvider? provider)
|
||||
{
|
||||
charsWritten = 0;
|
||||
|
||||
if (format.IsEmpty)
|
||||
{
|
||||
format = "ns";
|
||||
}
|
||||
|
||||
if (format is not "m" and not "n" and not "ns")
|
||||
{
|
||||
throw new FormatException($"The format string '{format}' is not supported.");
|
||||
}
|
||||
|
||||
var buf = new SpanBuffer(destination);
|
||||
var sets = _sets ?? [];
|
||||
|
||||
if (format is "m")
|
||||
{
|
||||
for (var i = 0; i < sets.Length; i++)
|
||||
{
|
||||
if (i > 0 && !buf.TryWrite(','))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryFormatMaven(ref buf, sets[i]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (var i = 0; i < sets.Length; i++)
|
||||
{
|
||||
if (i > 0 && !buf.TryWrite(" || "))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (format is "ns")
|
||||
{
|
||||
if (!TryFormatSimpleNpm(ref buf, sets[i]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!TryFormatNormalNpm(ref buf, sets[i]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
charsWritten = buf.Written;
|
||||
return true;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private static bool TryFormatSimpleNpm(ref SpanBuffer buf, ConstraintSet set)
|
||||
{
|
||||
if (IsAny(set))
|
||||
{
|
||||
return buf.TryWrite('*');
|
||||
}
|
||||
|
||||
if (set.Constraints is [{ Operation: Comparison.Eq, Version: var version }])
|
||||
{
|
||||
return buf.TryWrite(version);
|
||||
}
|
||||
|
||||
if (set.Constraints.Length is not 2)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (set is not { Lower: { Operation: Comparison.Gte or Comparison.Gt, Version: var lo } })
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (set is not { Upper: { Operation: Comparison.Lt or Comparison.Lte, Version: var hi } upper })
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
return buf.TryWrite('^') && buf.TryWrite(lo);
|
||||
}
|
||||
|
||||
if (lo is { Major: 0, Minor: > 0 } && hi is { Major: 0 } && hi.Minor == lo.Minor + 1)
|
||||
{
|
||||
return buf.TryWrite('^') && buf.TryWrite(lo);
|
||||
}
|
||||
|
||||
if (lo is { Major: 0, Minor: 0 } && hi is { Major: 0, Minor: 0 } && hi.Patch == lo.Patch + 1)
|
||||
{
|
||||
return buf.TryWrite('^') && buf.TryWrite(lo);
|
||||
}
|
||||
}
|
||||
|
||||
if (upper.Operation is Comparison.Lt && hi is { Patch: 0, Prerelease: not { Length: > 0 } })
|
||||
{
|
||||
if (hi.Major == lo.Major && hi.Minor == lo.Minor + 1)
|
||||
{
|
||||
return buf.TryWrite('~') && buf.TryWrite(lo);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryFormatNormalNpm(ref SpanBuffer buf, ConstraintSet set)
|
||||
{
|
||||
if (IsAny(set))
|
||||
{
|
||||
return buf.TryWrite('*');
|
||||
}
|
||||
|
||||
for (var i = 0; i < set.Constraints.Length; i++)
|
||||
{
|
||||
if (i > 0 && !buf.TryWrite(' '))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var @operator = set.Constraints[i].Operation switch
|
||||
{
|
||||
Comparison.Neq => "!=",
|
||||
Comparison.Lt => "<",
|
||||
Comparison.Lte => "<=",
|
||||
Comparison.Gt => ">",
|
||||
Comparison.Gte => ">=",
|
||||
Comparison.Eq or _ => ReadOnlySpan<char>.Empty,
|
||||
};
|
||||
|
||||
if (!@operator.IsEmpty && !buf.TryWrite(@operator))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!buf.TryWrite(set.Constraints[i].Version))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryFormatMaven(ref SpanBuffer buf, ConstraintSet set)
|
||||
{
|
||||
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(set.Lower?.Operation == Comparison.Gte ? '[' : '(') &&
|
||||
(!set.Lower.HasValue || buf.TryWrite(set.Lower.Value.Version)) &&
|
||||
buf.TryWrite(',') &&
|
||||
(!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 } }];
|
||||
}
|
||||
}
|
||||
41
src/semver/SemanticVersionRange.JsonConverter.cs
Normal file
41
src/semver/SemanticVersionRange.JsonConverter.cs
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
// Copyright (c) The Geekeey Authors
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Geekeey.SemVer;
|
||||
|
||||
[JsonConverter(typeof(SemanticVersionRangeJsonConverter))]
|
||||
public readonly partial record struct SemanticVersionRange
|
||||
{
|
||||
internal sealed class SemanticVersionRangeJsonConverter : JsonConverter<SemanticVersionRange>
|
||||
{
|
||||
public override SemanticVersionRange Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
{
|
||||
if (reader.TokenType is JsonTokenType.Null)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
if (reader.TokenType is not JsonTokenType.String || reader.GetString() is not { } value)
|
||||
{
|
||||
throw new JsonException("Expected string");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return Parse(value);
|
||||
}
|
||||
catch (FormatException exception)
|
||||
{
|
||||
throw new JsonException(exception.Message, exception);
|
||||
}
|
||||
}
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, SemanticVersionRange value, JsonSerializerOptions options)
|
||||
{
|
||||
writer.WriteStringValue(value.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
515
src/semver/SemanticVersionRange.Parsing.cs
Normal file
515
src/semver/SemanticVersionRange.Parsing.cs
Normal file
|
|
@ -0,0 +1,515 @@
|
|||
// Copyright (c) The Geekeey Authors
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Geekeey.SemVer;
|
||||
|
||||
public readonly partial record struct SemanticVersionRange : ISpanParsable<SemanticVersionRange>
|
||||
{
|
||||
#region IParsable
|
||||
|
||||
/// <summary>
|
||||
/// Parses a string into a <see cref="SemanticVersionRange"/>.
|
||||
/// </summary>
|
||||
/// <see cref="Parse(string, IFormatProvider)"/>
|
||||
public static SemanticVersionRange Parse(string s)
|
||||
{
|
||||
return Parse(s.AsSpan(), null);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public static SemanticVersionRange Parse(string s, IFormatProvider? provider)
|
||||
{
|
||||
return Parse(s.AsSpan(), provider);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to parse a string into a <see cref="SemanticVersionRange"/>.
|
||||
/// </summary>
|
||||
/// <see cref="TryParse(string, IFormatProvider, out SemanticVersionRange)"/>
|
||||
public static bool TryParse([NotNullWhen(true)] string? s, [MaybeNullWhen(false)] out SemanticVersionRange result)
|
||||
{
|
||||
return TryParse(s, null, out result);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public static bool TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, [MaybeNullWhen(false)] out SemanticVersionRange result)
|
||||
{
|
||||
return TryParse(s.AsSpan(), provider, out result);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ISpanParsable
|
||||
|
||||
/// <summary>
|
||||
/// Parses a span of characters into a <see cref="SemanticVersionRange"/>.
|
||||
/// </summary>
|
||||
/// <see cref="Parse(ReadOnlySpan{char}, IFormatProvider)"/>
|
||||
public static SemanticVersionRange Parse(ReadOnlySpan<char> s)
|
||||
{
|
||||
return Parse(s, null);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public static SemanticVersionRange Parse(ReadOnlySpan<char> s, IFormatProvider? provider)
|
||||
{
|
||||
if (!TryParse(s, provider, out var result))
|
||||
{
|
||||
throw new FormatException($"The input string '{s}' was not in a correct format.");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Tries to parse a span of characters into a <see cref="SemanticVersionRange"/>.
|
||||
/// </summary>
|
||||
/// <see cref="TryParse(ReadOnlySpan{char}, IFormatProvider, out SemanticVersionRange)"/>
|
||||
public static bool TryParse(ReadOnlySpan<char> s, [MaybeNullWhen(false)] out SemanticVersionRange result)
|
||||
{
|
||||
return TryParse(s, null, out result);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public static bool TryParse(ReadOnlySpan<char> s, IFormatProvider? provider, [MaybeNullWhen(false)] out SemanticVersionRange result)
|
||||
{
|
||||
result = default;
|
||||
|
||||
if (s.IsEmpty)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return s[0] is '[' or '(' ? TryParseMaven(s, out result) : TryParseNpm(s, out result);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private static bool TryParseNpm(ReadOnlySpan<char> s, out SemanticVersionRange result)
|
||||
{
|
||||
result = default;
|
||||
var sets = new List<ConstraintSet>();
|
||||
|
||||
foreach (var range in new NpmSetGrouping(s))
|
||||
{
|
||||
if (!TryParseNpmSet(s[range].Trim(), out var set))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
sets.Add(set);
|
||||
}
|
||||
|
||||
result = new SemanticVersionRange([.. sets]);
|
||||
return true;
|
||||
}
|
||||
|
||||
private ref struct NpmSetGrouping
|
||||
{
|
||||
private readonly ReadOnlySpan<char> _span;
|
||||
private int _currentStart;
|
||||
private int _currentEnd;
|
||||
|
||||
public NpmSetGrouping(ReadOnlySpan<char> span)
|
||||
{
|
||||
_span = span;
|
||||
_currentStart = 0;
|
||||
_currentEnd = -1;
|
||||
}
|
||||
|
||||
public readonly Range Current => _currentStart.._currentEnd;
|
||||
|
||||
public bool MoveNext()
|
||||
{
|
||||
_currentStart = _currentEnd is -1 ? 0 : _currentEnd + 2;
|
||||
|
||||
if (_currentStart >= _span.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var index = _span[_currentStart..].IndexOf("||");
|
||||
_currentEnd = index >= 0 ? _currentStart + index : _span.Length;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public readonly NpmSetGrouping GetEnumerator()
|
||||
{
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryParseNpmSet(ReadOnlySpan<char> s, out ConstraintSet set)
|
||||
{
|
||||
set = default;
|
||||
|
||||
if (s.IsEmpty)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Hyphen range: "1.0.0 - 2.0.0" OR Caret: "^x.y.z" OR Tilde: "~x.y.z"
|
||||
if (TryParseHyphenRange(s, out set) || TryParseCaretRange(s, out set) || TryParseTildeRange(s, out set))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var comparators = new List<Constraint>();
|
||||
foreach (var range in s.Split(' '))
|
||||
{
|
||||
if (s[range] is not { IsEmpty: false } segment)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!TryParseCompare(segment, comparators))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (comparators.Count is 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
set = new ConstraintSet([.. comparators]);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryParseHyphenRange(ReadOnlySpan<char> s, out ConstraintSet set)
|
||||
{
|
||||
set = default;
|
||||
|
||||
for (var i = 0; i < s.Length; i++)
|
||||
{
|
||||
if (s[i..] is [' ', '-', ' ', ..])
|
||||
{
|
||||
if (!SemanticVersion.TryParse(s[..(i + 0)].Trim(), null, out var lo))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!SemanticVersion.TryParse(s[(i + 3)..].Trim(), null, out var hi))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
set = new ConstraintSet([new Constraint(Comparison.Gte, lo), new Constraint(Comparison.Lte, hi)]);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryParseCaretRange(ReadOnlySpan<char> s, out ConstraintSet set)
|
||||
{
|
||||
set = default;
|
||||
|
||||
if (s.IsEmpty || s[0] is not '^')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
s = s[1..];
|
||||
|
||||
if (!SemanticVersion.TryParsePartially(s, out var lo, out _))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
SemanticVersion hi;
|
||||
|
||||
if (lo.Major > 0)
|
||||
{
|
||||
hi = new SemanticVersion(lo.Major + 1, 0, 0);
|
||||
}
|
||||
else if (lo.Minor > 0)
|
||||
{
|
||||
hi = new SemanticVersion(0, lo.Minor + 1, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
hi = new SemanticVersion(0, 0, lo.Patch + 1);
|
||||
}
|
||||
|
||||
set = new ConstraintSet([new Constraint(Comparison.Gte, lo), new Constraint(Comparison.Lt, hi)]);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryParseTildeRange(ReadOnlySpan<char> s, out ConstraintSet set)
|
||||
{
|
||||
set = default;
|
||||
|
||||
if (s.IsEmpty || s[0] is not '~')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
s = s[1..];
|
||||
|
||||
if (!SemanticVersion.TryParsePartially(s, out var lo, out var wildcard))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
SemanticVersion hi;
|
||||
|
||||
if (wildcard is 1)
|
||||
{
|
||||
hi = new SemanticVersion(lo.Major + 1, 0, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
hi = new SemanticVersion(lo.Major, lo.Minor + 1, 0);
|
||||
}
|
||||
|
||||
set = new ConstraintSet([new Constraint(Comparison.Gte, lo), new Constraint(Comparison.Lt, hi)]);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryParseCompare(ReadOnlySpan<char> s, List<Constraint> constraints)
|
||||
{
|
||||
var op = Comparison.Eq;
|
||||
|
||||
if (TryParseComparatorPrefix(s, out var comparison, out var rest))
|
||||
{
|
||||
op = comparison;
|
||||
s = rest;
|
||||
}
|
||||
|
||||
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
|
||||
{
|
||||
hi = new SemanticVersion(lo.Major, lo.Minor + 1, 0);
|
||||
}
|
||||
|
||||
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 Comparison op, out ReadOnlySpan<char> remainder)
|
||||
{
|
||||
if (s.IsEmpty)
|
||||
{
|
||||
op = default;
|
||||
remainder = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
switch (s)
|
||||
{
|
||||
case ['!', '=', ..]:
|
||||
op = Comparison.Neq;
|
||||
remainder = s[2..];
|
||||
return true;
|
||||
case ['>', '=', ..]:
|
||||
op = Comparison.Gte;
|
||||
remainder = s[2..];
|
||||
return true;
|
||||
case ['<', '=', ..]:
|
||||
op = Comparison.Lte;
|
||||
remainder = s[2..];
|
||||
return true;
|
||||
case ['>', ..]:
|
||||
op = Comparison.Gt;
|
||||
remainder = s[1..];
|
||||
return true;
|
||||
case ['<', ..]:
|
||||
op = Comparison.Lt;
|
||||
remainder = s[1..];
|
||||
return true;
|
||||
case ['=', ..]:
|
||||
op = Comparison.Eq;
|
||||
remainder = s[1..];
|
||||
return true;
|
||||
default:
|
||||
remainder = s;
|
||||
op = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryParseMaven(ReadOnlySpan<char> s, out SemanticVersionRange result)
|
||||
{
|
||||
result = default;
|
||||
var sets = new List<ConstraintSet>();
|
||||
|
||||
foreach (var range in new MavenSetGrouping(s))
|
||||
{
|
||||
if (!TryParseMavenSet(s[range].Trim(), out var set))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
sets.Add(set);
|
||||
}
|
||||
|
||||
result = new SemanticVersionRange([.. sets]);
|
||||
return true;
|
||||
}
|
||||
|
||||
private ref struct MavenSetGrouping
|
||||
{
|
||||
private readonly ReadOnlySpan<char> _span;
|
||||
private int _currentStart;
|
||||
private int _currentEnd;
|
||||
|
||||
public MavenSetGrouping(ReadOnlySpan<char> readOnlySpan)
|
||||
{
|
||||
_span = readOnlySpan;
|
||||
_currentStart = 0;
|
||||
_currentEnd = -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 TryParseMavenSet(ReadOnlySpan<char> s, out ConstraintSet set)
|
||||
{
|
||||
set = default;
|
||||
|
||||
var loInclusive = s[0] is '[';
|
||||
var hiInclusive = s[^1] is ']';
|
||||
|
||||
s = s[1..^1];
|
||||
|
||||
if (s.IndexOf(',') is not (>= 0 and var i))
|
||||
{
|
||||
if (!loInclusive || !hiInclusive)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!SemanticVersion.TryParsePartially(s.Trim(), out var version, out _))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
set = new ConstraintSet([new Constraint(Comparison.Eq, version)]);
|
||||
return true;
|
||||
}
|
||||
|
||||
var comps = new List<Constraint>();
|
||||
|
||||
if (s[..i].Trim() is { IsEmpty: false } loStr)
|
||||
{
|
||||
if (!SemanticVersion.TryParsePartially(loStr, out var lo, out _))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
comps.Add(new Constraint(loInclusive ? Comparison.Gte : Comparison.Gt, lo));
|
||||
}
|
||||
|
||||
if (s[(i + 1)..].Trim() is { IsEmpty: false } hiStr)
|
||||
{
|
||||
if (!SemanticVersion.TryParsePartially(hiStr, out var hi, out _))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
comps.Add(new Constraint(hiInclusive ? Comparison.Lte : Comparison.Lt, hi));
|
||||
}
|
||||
|
||||
// Check for exact match [a,a]
|
||||
if (comps is [{ Operation: Comparison.Gte, Version: var lhs }, { Operation: Comparison.Lte, Version: var rhs }])
|
||||
{
|
||||
if (lhs == rhs)
|
||||
{
|
||||
set = new ConstraintSet([new Constraint(Comparison.Eq, lhs)]);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
set = new ConstraintSet([.. comps]);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
105
src/semver/SemanticVersionRange.cs
Normal file
105
src/semver/SemanticVersionRange.cs
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
// Copyright (c) The Geekeey Authors
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
namespace Geekeey.SemVer;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a semantic version range, which is a set of version constraints
|
||||
/// used to match specific semantic versions based on defined ranges or patterns.
|
||||
/// </summary>
|
||||
public readonly partial record struct SemanticVersionRange
|
||||
{
|
||||
// OR of AND-groups. null == empty range (matches nothing).
|
||||
private readonly ConstraintSet[]? _sets;
|
||||
|
||||
internal SemanticVersionRange(ConstraintSet[] sets)
|
||||
{
|
||||
_sets = sets;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified version is contained in the range.
|
||||
/// </summary>
|
||||
/// <param name="version">The version to check.</param>
|
||||
/// <returns><c>true</c> if the version is contained in the range, otherwise <c>false</c>.</returns>
|
||||
public bool Contains(SemanticVersion version)
|
||||
{
|
||||
if (_sets is null || _sets.Length == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return _sets.Any(set => set.Includes(version));
|
||||
}
|
||||
}
|
||||
|
||||
internal enum Comparison { Eq, Neq, Lt, Lte, Gt, Gte }
|
||||
|
||||
internal readonly struct Constraint
|
||||
{
|
||||
public readonly Comparison Operation;
|
||||
public readonly SemanticVersion Version;
|
||||
|
||||
public Constraint(Comparison operation, SemanticVersion version)
|
||||
{
|
||||
Operation = operation;
|
||||
Version = version;
|
||||
}
|
||||
|
||||
public bool Includes(SemanticVersion v)
|
||||
{
|
||||
return Operation switch
|
||||
{
|
||||
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 Operation switch
|
||||
{
|
||||
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 ConstraintSet
|
||||
{
|
||||
public readonly Constraint[] Constraints;
|
||||
|
||||
public ConstraintSet(Constraint[] constraints)
|
||||
{
|
||||
Constraints = constraints;
|
||||
}
|
||||
|
||||
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 Constraints)
|
||||
{
|
||||
if (!c.Includes(v))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
43
src/semver/SpanBuffer.cs
Normal file
43
src/semver/SpanBuffer.cs
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
// Copyright (c) The Geekeey Authors
|
||||
// SPDX-License-Identifier: EUPL-1.2
|
||||
|
||||
namespace Geekeey.SemVer;
|
||||
|
||||
internal ref struct SpanBuffer(Span<char> buffer)
|
||||
{
|
||||
private readonly Span<char> _buffer = buffer;
|
||||
public int Written { get; private set; }
|
||||
|
||||
public bool TryWrite(ReadOnlySpan<char> value)
|
||||
{
|
||||
if (!value.TryCopyTo(_buffer[Written..]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Written += value.Length;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryWrite<T>(T v) where T : ISpanFormattable
|
||||
{
|
||||
if (!v.TryFormat(_buffer[Written..], out var n, [], null))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Written += n;
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryWrite<T>(T v, ReadOnlySpan<char> fmt) where T : ISpanFormattable
|
||||
{
|
||||
if (!v.TryFormat(_buffer[Written..], out var n, fmt, null))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Written += n;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
BIN
src/semver/package-icon.png
Normal file
BIN
src/semver/package-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
70
src/semver/package-readme.md
Normal file
70
src/semver/package-readme.md
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
SemVer is a .NET library for parsing, comparing, formatting, and serializing semantic versions and version ranges.
|
||||
|
||||
## Features
|
||||
|
||||
- **Parsing:** parse semantic version strings into structured objects.
|
||||
- **Comparison:** compare versions with operators or `CompareTo`.
|
||||
- **Ranges:** evaluate npm-style and Maven-style version ranges.
|
||||
- **Serialization:** use `System.Text.Json` converters for versions and ranges.
|
||||
|
||||
## Getting Started
|
||||
|
||||
### Install the NuGet package:
|
||||
|
||||
```shell
|
||||
dotnet add package Geekeey.SemVer
|
||||
```
|
||||
|
||||
You may need to add our NuGet feed to your `nuget.config` this can be done by running the following command:
|
||||
|
||||
```shell
|
||||
dotnet nuget add source -n geekeey https://code.geekeey.de/api/packages/geekeey/nuget/index.json
|
||||
```
|
||||
|
||||
### Usage
|
||||
|
||||
```csharp
|
||||
using Geekeey.SemVer;
|
||||
|
||||
var version = SemanticVersion.Parse("1.2.3-beta+build.7");
|
||||
var next = new SemanticVersion(1, 3, 0);
|
||||
|
||||
Console.WriteLine(version);
|
||||
Console.WriteLine(version.Major);
|
||||
Console.WriteLine(version.Prerelease);
|
||||
Console.WriteLine(version.Metadata);
|
||||
Console.WriteLine(version < next);
|
||||
|
||||
if (SemanticVersion.TryParse("1.2.3-alpha", out var parsed))
|
||||
{
|
||||
Console.WriteLine(parsed);
|
||||
}
|
||||
```
|
||||
|
||||
Version ranges support npm-style and Maven-style syntax.
|
||||
|
||||
```csharp
|
||||
using Geekeey.SemVer;
|
||||
|
||||
var npmRange = SemanticVersionRange.Parse("^1.2.3");
|
||||
var mavenRange = SemanticVersionRange.Parse("[1.2.3,2.0.0)");
|
||||
var candidate = SemanticVersion.Parse("1.4.2");
|
||||
|
||||
Console.WriteLine(npmRange.Contains(candidate));
|
||||
Console.WriteLine(mavenRange.Contains(candidate));
|
||||
Console.WriteLine(npmRange.ToString("m", null));
|
||||
Console.WriteLine(mavenRange.ToString("ns", null));
|
||||
```
|
||||
|
||||
Both `SemanticVersion` and `SemanticVersionRange` integrate with `System.Text.Json`.
|
||||
|
||||
```csharp
|
||||
using System.Text.Json;
|
||||
using Geekeey.SemVer;
|
||||
|
||||
var versionJson = JsonSerializer.Serialize(SemanticVersion.Parse("1.2.3-beta+build.7"));
|
||||
var rangeJson = JsonSerializer.Serialize(SemanticVersionRange.Parse("^1.2.3"));
|
||||
|
||||
var roundTrippedVersion = JsonSerializer.Deserialize<SemanticVersion>(versionJson);
|
||||
var roundTrippedRange = JsonSerializer.Deserialize<SemanticVersionRange>(rangeJson);
|
||||
```
|
||||
Loading…
Reference in a new issue