Compare commits
12 changed files with 13 additions and 398 deletions
15
CHANGELOG.md
15
CHANGELOG.md
|
|
@ -14,23 +14,8 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
- `SemanticVersion` and `SemanticVersionRange` can now be used as `System.Text.Json`
|
|
||||||
dictionary keys. The custom converters override `ReadAsPropertyName`/`WriteAsPropertyName`
|
|
||||||
so `Dictionary<TKey, TValue>` round-trips correctly.
|
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- `SemanticVersion` `==`/`Equals`/`GetHashCode` now ignore build metadata, matching precedence comparison rules.
|
|
||||||
- `SemanticVersionRange` equality (`==`, `Equals`, `GetHashCode`) is now structural and based
|
|
||||||
on the constraint sets, so ranges that parse to the same comparators compare as equal
|
|
||||||
(previously it was reference-sensitive on the backing array).
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- Reject invalid SemVer identifiers when parsing versions.
|
|
||||||
- Range exact and non-equal comparators ignore build metadata when matching versions.
|
|
||||||
- Avoid infinite recursion when formatting ranges in the short npm (`ns`) form.
|
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
|
|
||||||
[1.0.0]: https://code.geekeey.de/geekeey/semver/releases/tag/1.0.0
|
[1.0.0]: https://code.geekeey.de/geekeey/semver/releases/tag/1.0.0
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
// Copyright (c) The Geekeey Authors
|
// Copyright (c) The Geekeey Authors
|
||||||
// SPDX-License-Identifier: EUPL-1.2
|
// SPDX-License-Identifier: EUPL-1.2
|
||||||
|
|
||||||
using System.Text.Json;
|
|
||||||
|
|
||||||
namespace Geekeey.SemVer.Tests;
|
namespace Geekeey.SemVer.Tests;
|
||||||
|
|
||||||
internal sealed class SemanticVersionRangeTests
|
internal sealed class SemanticVersionRangeTests
|
||||||
|
|
@ -250,30 +248,6 @@ internal sealed class SemanticVersionRangeTests
|
||||||
await Assert.That(new string(destination[..charsWritten])).IsEqualTo("^1.2.3");
|
await Assert.That(new string(destination[..charsWritten])).IsEqualTo("^1.2.3");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
|
||||||
public async Task I_can_match_exact_range_ignoring_build_metadata()
|
|
||||||
{
|
|
||||||
var range = SemanticVersionRange.Parse("[1.2.3]");
|
|
||||||
|
|
||||||
await Assert.That(range.Contains(SemanticVersion.Parse("1.2.3+build.7"))).IsTrue();
|
|
||||||
await Assert.That(range.Contains(SemanticVersion.Parse("1.2.4"))).IsFalse();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
[Arguments("[1.2.3,1.4.0)", ">=1.2.3 <1.4.0")]
|
|
||||||
[Arguments(">1.0.0", ">1.0.0")]
|
|
||||||
[Arguments("[1.2,1.3]", ">=1.2.0 <=1.3.0")]
|
|
||||||
[Arguments("(,1.4.0]", "<=1.4.0")]
|
|
||||||
public async Task I_can_format_ranges_as_short_npm_even_when_not_caret_tilde(string range, string expected)
|
|
||||||
{
|
|
||||||
var value = SemanticVersionRange.Parse(range);
|
|
||||||
var destination = new char[256];
|
|
||||||
var success = value.TryFormat(destination, out var charsWritten, "ns", null);
|
|
||||||
|
|
||||||
await Assert.That(success).IsTrue();
|
|
||||||
await Assert.That(new string(destination[..charsWritten])).IsEqualTo(expected);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public async Task I_fail_formatting_when_the_tentative_short_form_overflows()
|
public async Task I_fail_formatting_when_the_tentative_short_form_overflows()
|
||||||
{
|
{
|
||||||
|
|
@ -284,32 +258,4 @@ internal sealed class SemanticVersionRangeTests
|
||||||
await Assert.That(success).IsFalse();
|
await Assert.That(success).IsFalse();
|
||||||
await Assert.That(charsWritten).IsEqualTo(0);
|
await Assert.That(charsWritten).IsEqualTo(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
|
||||||
public async Task I_can_serialize_range_as_dictionary_key()
|
|
||||||
{
|
|
||||||
var dict = new Dictionary<SemanticVersionRange, int>
|
|
||||||
{
|
|
||||||
[SemanticVersionRange.Parse("^1.2.3")] = 1,
|
|
||||||
[SemanticVersionRange.Parse("^2.0.0")] = 2,
|
|
||||||
};
|
|
||||||
|
|
||||||
var json = JsonSerializer.Serialize(dict);
|
|
||||||
var roundTripped = JsonSerializer.Deserialize<Dictionary<SemanticVersionRange, int>>(json);
|
|
||||||
|
|
||||||
await Assert.That(roundTripped).IsNotNull();
|
|
||||||
await Assert.That(roundTripped!.Count).IsEqualTo(2);
|
|
||||||
await Assert.That(roundTripped[SemanticVersionRange.Parse("^1.2.3")]).IsEqualTo(1);
|
|
||||||
await Assert.That(roundTripped[SemanticVersionRange.Parse("^2.0.0")]).IsEqualTo(2);
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public async Task I_can_treat_structurally_equal_ranges_as_equal()
|
|
||||||
{
|
|
||||||
var a = SemanticVersionRange.Parse("^1.2.3");
|
|
||||||
var b = SemanticVersionRange.Parse("[1.2.3,2.0.0)");
|
|
||||||
|
|
||||||
await Assert.That(a == b).IsTrue();
|
|
||||||
await Assert.That(a.GetHashCode()).IsEqualTo(b.GetHashCode());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -186,45 +186,6 @@ internal sealed class SemanticVersionTests
|
||||||
await Assert.That(v.Metadata).IsEqualTo("789");
|
await Assert.That(v.Metadata).IsEqualTo("789");
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
|
||||||
[Arguments("1.2.3-@#")]
|
|
||||||
[Arguments("01.2.3")]
|
|
||||||
[Arguments("1.2.3-01")]
|
|
||||||
[Arguments("1.2.3-alpha_1")]
|
|
||||||
[Arguments("1.2.3+build.@")]
|
|
||||||
[Arguments("1.2.3-")]
|
|
||||||
[Arguments("1.2.3-..")]
|
|
||||||
public async Task I_can_not_parse_invalid_versions(string input)
|
|
||||||
{
|
|
||||||
await Assert.That(SemanticVersion.TryParse(input, out _)).IsFalse();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
[Arguments("1.2.3")]
|
|
||||||
[Arguments("1.2.3-alpha.1")]
|
|
||||||
[Arguments("1.0.0-0.3.7")]
|
|
||||||
[Arguments("1.0.0-x.7.z.92")]
|
|
||||||
[Arguments("10.20.30")]
|
|
||||||
[Arguments("1.2.3+build.01")]
|
|
||||||
[Arguments("1.0.0-alpha-b")]
|
|
||||||
public async Task I_can_parse_valid_versions(string input)
|
|
||||||
{
|
|
||||||
await Assert.That(SemanticVersion.TryParse(input, out _)).IsTrue();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
|
||||||
public async Task I_can_treat_versions_as_equal_when_only_build_metadata_differs()
|
|
||||||
{
|
|
||||||
var a = SemanticVersion.Parse("1.2.3");
|
|
||||||
var b = SemanticVersion.Parse("1.2.3+build.7");
|
|
||||||
|
|
||||||
await Assert.That(a == b).IsTrue();
|
|
||||||
await Assert.That(a.GetHashCode()).IsEqualTo(b.GetHashCode());
|
|
||||||
|
|
||||||
var c = SemanticVersion.Parse("1.2.3-alpha");
|
|
||||||
await Assert.That(a == c).IsFalse();
|
|
||||||
}
|
|
||||||
|
|
||||||
[Test]
|
[Test]
|
||||||
public async Task I_can_handle_invalid_json_token()
|
public async Task I_can_handle_invalid_json_token()
|
||||||
{
|
{
|
||||||
|
|
@ -257,25 +218,8 @@ internal sealed class SemanticVersionTests
|
||||||
public async Task I_can_deserialize_empty_string()
|
public async Task I_can_deserialize_empty_string()
|
||||||
{
|
{
|
||||||
var json = "\"\"";
|
var json = "\"\"";
|
||||||
|
|
||||||
await Assert.That(() => JsonSerializer.Deserialize<SemanticVersion>(json))
|
await Assert.That(() => JsonSerializer.Deserialize<SemanticVersion>(json))
|
||||||
.Throws<JsonException>();
|
.Throws<JsonException>();
|
||||||
}
|
}
|
||||||
|
|
||||||
[Test]
|
|
||||||
public async Task I_can_serialize_version_as_dictionary_key()
|
|
||||||
{
|
|
||||||
var dict = new Dictionary<SemanticVersion, int>
|
|
||||||
{
|
|
||||||
[new SemanticVersion(1, 2, 3)] = 1,
|
|
||||||
[new SemanticVersion(2, 0, 0, "rc.1", "build")] = 2,
|
|
||||||
};
|
|
||||||
|
|
||||||
var json = JsonSerializer.Serialize(dict);
|
|
||||||
var roundTripped = JsonSerializer.Deserialize<Dictionary<SemanticVersion, int>>(json);
|
|
||||||
|
|
||||||
await Assert.That(roundTripped).IsNotNull();
|
|
||||||
await Assert.That(roundTripped!.Count).IsEqualTo(2);
|
|
||||||
await Assert.That(roundTripped[new SemanticVersion(1, 2, 3)]).IsEqualTo(1);
|
|
||||||
await Assert.That(roundTripped[new SemanticVersion(2, 0, 0, "rc.1", "build")]).IsEqualTo(2);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
namespace Geekeey.SemVer;
|
namespace Geekeey.SemVer;
|
||||||
|
|
||||||
public readonly partial struct SemanticVersion : IComparable, IComparable<SemanticVersion>
|
public readonly partial record struct SemanticVersion : IComparable, IComparable<SemanticVersion>
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public int CompareTo(object? obj)
|
public int CompareTo(object? obj)
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
namespace Geekeey.SemVer;
|
namespace Geekeey.SemVer;
|
||||||
|
|
||||||
public readonly partial struct SemanticVersion : ISpanFormattable
|
public readonly partial record struct SemanticVersion : ISpanFormattable
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ using System.Text.Json.Serialization;
|
||||||
namespace Geekeey.SemVer;
|
namespace Geekeey.SemVer;
|
||||||
|
|
||||||
[JsonConverter(typeof(SemanticVersionJsonConverter))]
|
[JsonConverter(typeof(SemanticVersionJsonConverter))]
|
||||||
public readonly partial struct SemanticVersion
|
public readonly partial record struct SemanticVersion
|
||||||
{
|
{
|
||||||
internal sealed class SemanticVersionJsonConverter : JsonConverter<SemanticVersion>
|
internal sealed class SemanticVersionJsonConverter : JsonConverter<SemanticVersion>
|
||||||
{
|
{
|
||||||
|
|
@ -37,33 +37,5 @@ public readonly partial struct SemanticVersion
|
||||||
{
|
{
|
||||||
writer.WriteStringValue(value.ToString("f", null));
|
writer.WriteStringValue(value.ToString("f", null));
|
||||||
}
|
}
|
||||||
|
|
||||||
public override SemanticVersion ReadAsPropertyName(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
|
||||||
{
|
|
||||||
if (reader.TokenType is JsonTokenType.Null)
|
|
||||||
{
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
|
|
||||||
var name = reader.GetString();
|
|
||||||
if (name is null)
|
|
||||||
{
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return Parse(name);
|
|
||||||
}
|
|
||||||
catch (FormatException exception)
|
|
||||||
{
|
|
||||||
throw new JsonException(exception.Message, exception);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void WriteAsPropertyName(Utf8JsonWriter writer, SemanticVersion value, JsonSerializerOptions options)
|
|
||||||
{
|
|
||||||
writer.WritePropertyName(value.ToString("f", null));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ using System.Globalization;
|
||||||
|
|
||||||
namespace Geekeey.SemVer;
|
namespace Geekeey.SemVer;
|
||||||
|
|
||||||
public readonly partial struct SemanticVersion : ISpanParsable<SemanticVersion>
|
public readonly partial record struct SemanticVersion : ISpanParsable<SemanticVersion>
|
||||||
{
|
{
|
||||||
#region IParsable
|
#region IParsable
|
||||||
|
|
||||||
|
|
@ -85,9 +85,6 @@ public readonly partial struct SemanticVersion : ISpanParsable<SemanticVersion>
|
||||||
// components = 1: wildcard at minor
|
// components = 1: wildcard at minor
|
||||||
// components = 2: wildcard at patch
|
// components = 2: wildcard at patch
|
||||||
// components = 3: no wildcards
|
// components = 3: no wildcards
|
||||||
private static readonly System.Buffers.SearchValues<char> ValidIdentifierChars =
|
|
||||||
System.Buffers.SearchValues.Create("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-");
|
|
||||||
|
|
||||||
internal static bool TryParsePartially(ReadOnlySpan<char> s, out SemanticVersion version, out int components)
|
internal static bool TryParsePartially(ReadOnlySpan<char> s, out SemanticVersion version, out int components)
|
||||||
{
|
{
|
||||||
version = default;
|
version = default;
|
||||||
|
|
@ -149,76 +146,10 @@ public readonly partial struct SemanticVersion : ISpanParsable<SemanticVersion>
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Leading zeroes are forbidden for the numeric core components (SemVer 2.0.0 §9).
|
|
||||||
if (segment.Length > 1 && segment[0] is '0')
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
component[components++] = value;
|
component[components++] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Per SemVer 2.0.0 the core version numbers and numeric prerelease identifiers
|
|
||||||
// MUST NOT include leading zeroes, and prerelease/metadata identifiers MUST be
|
|
||||||
// composed only of ASCII alphanumerics and hyphens.
|
|
||||||
if (metadata is not null && !IsValidIdentifiers(metadata, forbidLeadingZeroInNumerics: false))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prerelease is not null && !IsValidIdentifiers(prerelease, forbidLeadingZeroInNumerics: true))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
version = new SemanticVersion(component[0], component[1], component[2], prerelease, metadata);
|
version = new SemanticVersion(component[0], component[1], component[2], prerelease, metadata);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsValidIdentifiers(ReadOnlySpan<char> value, bool forbidLeadingZeroInNumerics)
|
|
||||||
{
|
|
||||||
if (value.IsEmpty)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validation is per-identifier (dot-separated), not per-string: an empty
|
|
||||||
// segment ("a..b", "a.") is invalid, and the leading-zero ban applies only
|
|
||||||
// to numeric identifiers (so "0abc" stays valid while "02" does not).
|
|
||||||
var start = 0;
|
|
||||||
for (var i = 0; i <= value.Length; i++)
|
|
||||||
{
|
|
||||||
if (i != value.Length && value[i] is not '.')
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var identifier = value[start..i];
|
|
||||||
|
|
||||||
// Reject empty segments produced by adjacent or trailing dots.
|
|
||||||
if (identifier.IsEmpty)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Every character must be an allowed identifier character.
|
|
||||||
if (identifier.IndexOfAnyExcept(ValidIdentifierChars) >= 0)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Numeric identifiers must not include leading zeroes (SemVer 2.0.0 §9/§10).
|
|
||||||
// The TryParse gate ensures the rule fires only for all-digit identifiers,
|
|
||||||
// leaving an alphanumeric "0abc" (which is allowed) untouched.
|
|
||||||
if (forbidLeadingZeroInNumerics && identifier.Length > 1 && identifier[0] is '0' &&
|
|
||||||
ulong.TryParse(identifier, NumberStyles.None, CultureInfo.InvariantCulture, out _))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
start = i + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ namespace Geekeey.SemVer;
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a semantic version that adheres to the Semantic Versioning 2.0.0 specification.
|
/// Represents a semantic version that adheres to the Semantic Versioning 2.0.0 specification.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public readonly partial struct SemanticVersion : IEquatable<SemanticVersion>
|
public readonly partial record struct SemanticVersion
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Converts a <see cref="Version"/> into the equivalent semantic version.
|
/// Converts a <see cref="Version"/> into the equivalent semantic version.
|
||||||
|
|
@ -105,38 +105,4 @@ public readonly partial struct SemanticVersion : IEquatable<SemanticVersion>
|
||||||
/// details. It does not affect the precedence of the version.
|
/// details. It does not affect the precedence of the version.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string? Metadata { get; }
|
public string? Metadata { get; }
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public bool Equals(SemanticVersion other)
|
|
||||||
{
|
|
||||||
return SemanticVersionComparer.Priority.Equals(this, other);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override bool Equals(object? obj)
|
|
||||||
{
|
|
||||||
return obj is SemanticVersion other && Equals(other);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override int GetHashCode()
|
|
||||||
{
|
|
||||||
return SemanticVersionComparer.Priority.GetHashCode(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Determines whether two versions have equal precedence.
|
|
||||||
/// </summary>
|
|
||||||
public static bool operator ==(SemanticVersion left, SemanticVersion right)
|
|
||||||
{
|
|
||||||
return left.Equals(right);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Determines whether two versions differ in precedence.
|
|
||||||
/// </summary>
|
|
||||||
public static bool operator !=(SemanticVersion left, SemanticVersion right)
|
|
||||||
{
|
|
||||||
return !left.Equals(right);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
namespace Geekeey.SemVer;
|
namespace Geekeey.SemVer;
|
||||||
|
|
||||||
public readonly partial struct SemanticVersionRange : ISpanFormattable
|
public readonly partial record struct SemanticVersionRange : ISpanFormattable
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public override string ToString()
|
public override string ToString()
|
||||||
|
|
@ -85,26 +85,9 @@ public readonly partial struct SemanticVersionRange : ISpanFormattable
|
||||||
|
|
||||||
if (format is "ns")
|
if (format is "ns")
|
||||||
{
|
{
|
||||||
var writtenBefore = buf.Written;
|
|
||||||
|
|
||||||
// Try the compact caret/tilde form first. If the set can't be expressed
|
|
||||||
// that way (TryFormatSimpleNpm returns false without writing anything) fall
|
|
||||||
// back to the full comparator form so we never report failure for a
|
|
||||||
// representable range - otherwise the public ToString would recurse
|
|
||||||
// infinitely via the interpolated-string handler's buffer-overflow fallback.
|
|
||||||
if (!TryFormatSimpleNpm(ref buf, sets[i]))
|
if (!TryFormatSimpleNpm(ref buf, sets[i]))
|
||||||
{
|
{
|
||||||
if (buf.Written == writtenBefore)
|
return false;
|
||||||
{
|
|
||||||
if (!TryFormatNormalNpm(ref buf, sets[i]))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ using System.Text.Json.Serialization;
|
||||||
namespace Geekeey.SemVer;
|
namespace Geekeey.SemVer;
|
||||||
|
|
||||||
[JsonConverter(typeof(SemanticVersionRangeJsonConverter))]
|
[JsonConverter(typeof(SemanticVersionRangeJsonConverter))]
|
||||||
public readonly partial struct SemanticVersionRange
|
public readonly partial record struct SemanticVersionRange
|
||||||
{
|
{
|
||||||
internal sealed class SemanticVersionRangeJsonConverter : JsonConverter<SemanticVersionRange>
|
internal sealed class SemanticVersionRangeJsonConverter : JsonConverter<SemanticVersionRange>
|
||||||
{
|
{
|
||||||
|
|
@ -37,33 +37,5 @@ public readonly partial struct SemanticVersionRange
|
||||||
{
|
{
|
||||||
writer.WriteStringValue(value.ToString());
|
writer.WriteStringValue(value.ToString());
|
||||||
}
|
}
|
||||||
|
|
||||||
public override SemanticVersionRange ReadAsPropertyName(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
|
||||||
{
|
|
||||||
if (reader.TokenType is JsonTokenType.Null)
|
|
||||||
{
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
|
|
||||||
var name = reader.GetString();
|
|
||||||
if (name is null)
|
|
||||||
{
|
|
||||||
return default;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return Parse(name);
|
|
||||||
}
|
|
||||||
catch (FormatException exception)
|
|
||||||
{
|
|
||||||
throw new JsonException(exception.Message, exception);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public override void WriteAsPropertyName(Utf8JsonWriter writer, SemanticVersionRange value, JsonSerializerOptions options)
|
|
||||||
{
|
|
||||||
writer.WritePropertyName(value.ToString());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ using System.Diagnostics.CodeAnalysis;
|
||||||
|
|
||||||
namespace Geekeey.SemVer;
|
namespace Geekeey.SemVer;
|
||||||
|
|
||||||
public readonly partial struct SemanticVersionRange : ISpanParsable<SemanticVersionRange>
|
public readonly partial record struct SemanticVersionRange : ISpanParsable<SemanticVersionRange>
|
||||||
{
|
{
|
||||||
#region IParsable
|
#region IParsable
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,13 @@
|
||||||
// Copyright (c) The Geekeey Authors
|
// Copyright (c) The Geekeey Authors
|
||||||
// SPDX-License-Identifier: EUPL-1.2
|
// SPDX-License-Identifier: EUPL-1.2
|
||||||
|
|
||||||
using System;
|
|
||||||
|
|
||||||
namespace Geekeey.SemVer;
|
namespace Geekeey.SemVer;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a semantic version range, which is a set of version constraints
|
/// Represents a semantic version range, which is a set of version constraints
|
||||||
/// used to match specific semantic versions based on defined ranges or patterns.
|
/// used to match specific semantic versions based on defined ranges or patterns.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public readonly partial struct SemanticVersionRange : IEquatable<SemanticVersionRange>
|
public readonly partial record struct SemanticVersionRange
|
||||||
{
|
{
|
||||||
// OR of AND-groups. null == empty range (matches nothing).
|
// OR of AND-groups. null == empty range (matches nothing).
|
||||||
private readonly ConstraintSet[]? _sets;
|
private readonly ConstraintSet[]? _sets;
|
||||||
|
|
@ -33,86 +31,6 @@ public readonly partial struct SemanticVersionRange : IEquatable<SemanticVersion
|
||||||
|
|
||||||
return _sets.Any(set => set.Includes(version));
|
return _sets.Any(set => set.Includes(version));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public bool Equals(SemanticVersionRange other)
|
|
||||||
{
|
|
||||||
if (_sets is null || other._sets is null)
|
|
||||||
{
|
|
||||||
return _sets is null && other._sets is null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_sets.Length != other._sets.Length)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var i = 0; i < _sets.Length; i++)
|
|
||||||
{
|
|
||||||
if (!Equals(_sets[i], other._sets[i]))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool Equals(ConstraintSet x, ConstraintSet y)
|
|
||||||
{
|
|
||||||
if (x.Constraints.Length != y.Constraints.Length)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var i = 0; i < x.Constraints.Length; i++)
|
|
||||||
{
|
|
||||||
if (x.Constraints[i].Operation != y.Constraints[i].Operation ||
|
|
||||||
x.Constraints[i].Version != y.Constraints[i].Version)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override bool Equals(object? obj)
|
|
||||||
=> obj is SemanticVersionRange other && Equals(other);
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public override int GetHashCode()
|
|
||||||
{
|
|
||||||
if (_sets is null)
|
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
var hash = new HashCode();
|
|
||||||
foreach (var set in _sets)
|
|
||||||
{
|
|
||||||
foreach (var constraint in set.Constraints)
|
|
||||||
{
|
|
||||||
hash.Add(constraint.Operation);
|
|
||||||
hash.Add(constraint.Version);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return hash.ToHashCode();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Determines whether two semantic version ranges are equal.
|
|
||||||
/// </summary>
|
|
||||||
public static bool operator ==(SemanticVersionRange left, SemanticVersionRange right)
|
|
||||||
=> left.Equals(right);
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Determines whether two semantic version ranges differ.
|
|
||||||
/// </summary>
|
|
||||||
public static bool operator !=(SemanticVersionRange left, SemanticVersionRange right)
|
|
||||||
=> !left.Equals(right);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal enum Comparison { Eq, Neq, Lt, Lte, Gt, Gte }
|
internal enum Comparison { Eq, Neq, Lt, Lte, Gt, Gte }
|
||||||
|
|
@ -132,10 +50,8 @@ internal readonly struct Constraint
|
||||||
{
|
{
|
||||||
return Operation switch
|
return Operation switch
|
||||||
{
|
{
|
||||||
// Build metadata does not affect precedence (SemVer 2.0.0 §10), so exact
|
Comparison.Eq => v == Version,
|
||||||
// and non-equal comparisons on the version part ignore it.
|
Comparison.Neq => v != Version,
|
||||||
Comparison.Eq => SemanticVersionComparer.Priority.Equals(v, Version),
|
|
||||||
Comparison.Neq => !SemanticVersionComparer.Priority.Equals(v, Version),
|
|
||||||
Comparison.Lt => v < Version,
|
Comparison.Lt => v < Version,
|
||||||
Comparison.Lte => v <= Version,
|
Comparison.Lte => v <= Version,
|
||||||
Comparison.Gt => v > Version,
|
Comparison.Gt => v > Version,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue