Skip to content

Commit

Permalink
Merge pull request #305 from Lombiq/issue/OFFI-193
Browse files Browse the repository at this point in the history
OFFI-193: VersionTree
  • Loading branch information
wAsnk authored Jan 6, 2025
2 parents 770df18 + 65cffe5 commit f595083
Show file tree
Hide file tree
Showing 6 changed files with 350 additions and 0 deletions.
1 change: 1 addition & 0 deletions Lombiq.HelpfulLibraries.Common/Docs/Utilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@
- `Sha256Helper`: A static helper class with the `ComputeHash()` utility function that converts text into [SHA-256](https://en.wikipedia.org/wiki/SHA-256) hash string.
- `StringHelper`: A static helper class with utility functions for concatenating and generating strings, particularly in a culture invariant manner.
- `Union`: A container type which is a union of two different types. Only either of the two types can be set at the same time.
- `VersionTree`: A read-only `Version` collection that organizes the provided versions into a tree structure by version parts. Can be used to resolve a floating (partial) version from a collection of specific versions.
- `XmlHelper`: Utility class to streamline XML serialization/deserialization.
64 changes: 64 additions & 0 deletions Lombiq.HelpfulLibraries.Common/Utilities/VersionTree.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#nullable enable

using System;
using System.Collections.Generic;
using System.Linq;

namespace Lombiq.HelpfulLibraries.Common.Utilities;

/// <summary>
/// Groups <see cref="Version"/> instances into a tree structure based on the specified version parts, so they can be
/// selected by number using indexing.
/// </summary>
/// <param name="Versions">All versions in the current subtree.</param>
/// <param name="SubVersions">Versions grouped by the directly next version part.</param>
public record VersionTree(IReadOnlyList<Version> Versions, IReadOnlyDictionary<int, VersionTree> SubVersions)
{
/// <summary>
/// Gets the subtree for the provided key, if it exists (otherwise <see langword="null"/>). If the <paramref
/// name="index"/> is negative, this instance is returned instead.
/// </summary>
public VersionTree? this[int index]
{
get
{
if (index < 0) return this;
return SubVersions.TryGetValue(index, out var sub) ? sub : null;
}
}

/// <summary>
/// Gets all versions that match the provided <paramref name="version"/>, that can be partial/floating.
/// </summary>
public VersionTree? this[Version? version] =>
version is null
? this
: this[version.Major]?[version.Minor]?[version.Build]?[version.Revision];

/// <summary>
/// Creates a new tree from a copy of the provided <paramref name="versions"/>.
/// </summary>
public static VersionTree Create(IEnumerable<Version> versions)
{
var allVersions = versions.ToList();

var majorVersions =
FromVersions(allVersions, version => version.Major, major =>
FromVersions(major, version => version.Minor, minor =>
FromVersions(minor, version => version.Build, revision =>
FromVersions(revision, version => version.Revision, _ => []))));

return new(allVersions, majorVersions);
}

private static Dictionary<int, VersionTree> FromVersions(
IEnumerable<Version> versions,
Func<Version, int> selector,
Func<IEnumerable<Version>, Dictionary<int, VersionTree>> subSelector) =>
versions
.Where(version => selector(version) >= 0)
.GroupBy(selector)
.ToDictionary(
group => group.Key,
items => new VersionTree([.. items], subSelector(items)));
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,10 @@
<AdditionalFiles Include="package.json" />
</ItemGroup>

<ItemGroup>
<None Update="VersionTreeTests.*.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
using Lombiq.HelpfulLibraries.Common.Utilities;
using Shouldly;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using Xunit;

namespace Lombiq.HelpfulLibraries.Tests.UnitTests.Utilities;

public class VersionTreeTests
{
private static readonly JsonSerializerOptions _indentedJsonOption = new() { WriteIndented = true };

private static readonly List<Version> _versions =
"1.0 1.2 1.2.1 1.2.2 1.2.3 1.3 1.4 1.4.1 1.4.1.1 1.4.1.2 1.4.1.3 1.4.2.1 1.4.2.2 1.4.2.4"
.Split()
.Select(Version.Parse)
.ToList();

[Fact]
public void VersionTreeShouldHaveExpectedStructure()
{
var tree = VersionTree.Create(_versions);
tree.Versions.ShouldBe(_versions);

ShouldMatchJson(tree, File.ReadAllText("VersionTreeTests.FullStructure.json"));
}

[Fact]
public void IndexingByNumberAndVersionShouldWork()
{
var tree = VersionTree.Create(_versions);
var expectedRevision = new Version(1, 4, 1, 1);

tree[1]![4]![1]![1]!.Versions.ShouldBe([expectedRevision]);
tree[expectedRevision]!.Versions.ShouldBe([expectedRevision]);
}

[Fact]
public void VersionSubtreeShouldHaveExpectedStructure()
{
var tree = VersionTree.Create(_versions);
var expectedSubtree = File.ReadAllText("VersionTreeTests.Subtree.json");

// Verify full subtree structure.
ShouldMatchJson(tree[1]![4], expectedSubtree);

// Verify that indexing -1 results in the same.
ShouldMatchJson(tree[1]![4]![-1]![-1], expectedSubtree);
ShouldMatchJson(tree[new Version(1, 4)], expectedSubtree);
}

private static void ShouldMatchJson(VersionTree tree, string expectedJson)
{
var actualJson = JsonSerializer.Serialize(tree, _indentedJsonOption);
(string.Join(string.Empty, actualJson.Split()) == string.Join(string.Empty, expectedJson.Split()))
.ShouldBeTrue($"Actual JSON:\n{actualJson}\nExpected JSON: {expectedJson}\n(whitespace does not matter)");
}
}
149 changes: 149 additions & 0 deletions Lombiq.HelpfulLibraries.Tests/VersionTreeTests.FullStructure.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
{
"Versions": [
"1.0",
"1.2",
"1.2.1",
"1.2.2",
"1.2.3",
"1.3",
"1.4",
"1.4.1",
"1.4.1.1",
"1.4.1.2",
"1.4.1.3",
"1.4.2.1",
"1.4.2.2",
"1.4.2.4"
],
"SubVersions": {
"1": {
"Versions": [
"1.0",
"1.2",
"1.2.1",
"1.2.2",
"1.2.3",
"1.3",
"1.4",
"1.4.1",
"1.4.1.1",
"1.4.1.2",
"1.4.1.3",
"1.4.2.1",
"1.4.2.2",
"1.4.2.4"
],
"SubVersions": {
"0": {
"Versions": [
"1.0"
],
"SubVersions": {}
},
"2": {
"Versions": [
"1.2",
"1.2.1",
"1.2.2",
"1.2.3"
],
"SubVersions": {
"1": {
"Versions": [
"1.2.1"
],
"SubVersions": {}
},
"2": {
"Versions": [
"1.2.2"
],
"SubVersions": {}
},
"3": {
"Versions": [
"1.2.3"
],
"SubVersions": {}
}
}
},
"3": {
"Versions": [
"1.3"
],
"SubVersions": {}
},
"4": {
"Versions": [
"1.4",
"1.4.1",
"1.4.1.1",
"1.4.1.2",
"1.4.1.3",
"1.4.2.1",
"1.4.2.2",
"1.4.2.4"
],
"SubVersions": {
"1": {
"Versions": [
"1.4.1",
"1.4.1.1",
"1.4.1.2",
"1.4.1.3"
],
"SubVersions": {
"1": {
"Versions": [
"1.4.1.1"
],
"SubVersions": {}
},
"2": {
"Versions": [
"1.4.1.2"
],
"SubVersions": {}
},
"3": {
"Versions": [
"1.4.1.3"
],
"SubVersions": {}
}
}
},
"2": {
"Versions": [
"1.4.2.1",
"1.4.2.2",
"1.4.2.4"
],
"SubVersions": {
"1": {
"Versions": [
"1.4.2.1"
],
"SubVersions": {}
},
"2": {
"Versions": [
"1.4.2.2"
],
"SubVersions": {}
},
"4": {
"Versions": [
"1.4.2.4"
],
"SubVersions": {}
}
}
}
}
}
}
}
}
}
69 changes: 69 additions & 0 deletions Lombiq.HelpfulLibraries.Tests/VersionTreeTests.Subtree.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
{
"Versions": [
"1.4",
"1.4.1",
"1.4.1.1",
"1.4.1.2",
"1.4.1.3",
"1.4.2.1",
"1.4.2.2",
"1.4.2.4"
],
"SubVersions": {
"1": {
"Versions": [
"1.4.1",
"1.4.1.1",
"1.4.1.2",
"1.4.1.3"
],
"SubVersions": {
"1": {
"Versions": [
"1.4.1.1"
],
"SubVersions": {}
},
"2": {
"Versions": [
"1.4.1.2"
],
"SubVersions": {}
},
"3": {
"Versions": [
"1.4.1.3"
],
"SubVersions": {}
}
}
},
"2": {
"Versions": [
"1.4.2.1",
"1.4.2.2",
"1.4.2.4"
],
"SubVersions": {
"1": {
"Versions": [
"1.4.2.1"
],
"SubVersions": {}
},
"2": {
"Versions": [
"1.4.2.2"
],
"SubVersions": {}
},
"4": {
"Versions": [
"1.4.2.4"
],
"SubVersions": {}
}
}
}
}
}

0 comments on commit f595083

Please sign in to comment.