Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
eb20fb8077 |
|||
| fba1476d9c | |||
| f9531fe112 |
8 changed files with 201 additions and 5 deletions
|
|
@ -14,9 +14,16 @@ 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.
|
- `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
|
### Fixed
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
// 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
|
||||||
|
|
@ -282,4 +284,32 @@ 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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -257,8 +257,25 @@ 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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -37,5 +37,33 @@ 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));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
namespace Geekeey.SemVer;
|
namespace Geekeey.SemVer;
|
||||||
|
|
||||||
public readonly partial record struct SemanticVersionRange : ISpanFormattable
|
public readonly partial struct SemanticVersionRange : 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(SemanticVersionRangeJsonConverter))]
|
[JsonConverter(typeof(SemanticVersionRangeJsonConverter))]
|
||||||
public readonly partial record struct SemanticVersionRange
|
public readonly partial struct SemanticVersionRange
|
||||||
{
|
{
|
||||||
internal sealed class SemanticVersionRangeJsonConverter : JsonConverter<SemanticVersionRange>
|
internal sealed class SemanticVersionRangeJsonConverter : JsonConverter<SemanticVersionRange>
|
||||||
{
|
{
|
||||||
|
|
@ -37,5 +37,33 @@ public readonly partial record 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 record struct SemanticVersionRange : ISpanParsable<SemanticVersionRange>
|
public readonly partial struct SemanticVersionRange : ISpanParsable<SemanticVersionRange>
|
||||||
{
|
{
|
||||||
#region IParsable
|
#region IParsable
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ namespace Geekeey.SemVer;
|
||||||
/// 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 record struct SemanticVersionRange
|
public readonly partial struct SemanticVersionRange : IEquatable<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;
|
||||||
|
|
@ -31,6 +31,92 @@ public readonly partial record struct SemanticVersionRange
|
||||||
|
|
||||||
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)
|
||||||
|
{
|
||||||
|
return 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)
|
||||||
|
{
|
||||||
|
return left.Equals(right);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines whether two semantic version ranges differ.
|
||||||
|
/// </summary>
|
||||||
|
public static bool operator !=(SemanticVersionRange left, SemanticVersionRange right)
|
||||||
|
{
|
||||||
|
return !left.Equals(right);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal enum Comparison { Eq, Neq, Lt, Lte, Gt, Gte }
|
internal enum Comparison { Eq, Neq, Lt, Lte, Gt, Gte }
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue