diff --git a/source/Octopus.Versioning.Tests/Docker/DockerVersionCompareTests.cs b/source/Octopus.Versioning.Tests/Docker/DockerVersionCompareTests.cs index 2530044..1b6b37a 100644 --- a/source/Octopus.Versioning.Tests/Docker/DockerVersionCompareTests.cs +++ b/source/Octopus.Versioning.Tests/Docker/DockerVersionCompareTests.cs @@ -45,7 +45,7 @@ public void MatchingVersionsShouldBeGroupedCorrectly(string version) var ver1 = VersionFactory.CreateDockerTag(version); var ver2 = VersionFactory.CreateDockerTag(version); - var items = new List {ver1, ver2}.GroupBy(i => i).ToList(); + var items = new List {ver1, ver2}.GroupBy(i => i).ToList(); Assert.AreEqual(1, items.Count); } diff --git a/source/Octopus.Versioning.Tests/Maven/MavenVersionCompareTests.cs b/source/Octopus.Versioning.Tests/Maven/MavenVersionCompareTests.cs index f74f0f7..68df331 100644 --- a/source/Octopus.Versioning.Tests/Maven/MavenVersionCompareTests.cs +++ b/source/Octopus.Versioning.Tests/Maven/MavenVersionCompareTests.cs @@ -15,7 +15,7 @@ public void TestMatchingVersionsAreGroupedCorrectly(string version) var ver1 = VersionFactory.CreateMavenVersion(version); var ver2 = VersionFactory.CreateMavenVersion(version); - var items = new List {ver1, ver2}.GroupBy(i => i).ToList(); + var items = new List {ver1, ver2}.GroupBy(i => i).ToList(); Assert.AreEqual(1, items.Count); } diff --git a/source/Octopus.Versioning.Tests/Maven/Ranges/RangeTests.cs b/source/Octopus.Versioning.Tests/Maven/Ranges/RangeTests.cs index a553240..1bd9bb1 100644 --- a/source/Octopus.Versioning.Tests/Maven/Ranges/RangeTests.cs +++ b/source/Octopus.Versioning.Tests/Maven/Ranges/RangeTests.cs @@ -691,25 +691,25 @@ void checkInvalidRange(string version) [Test] public void testContains() { - IVersion actualVersion = new MavenVersionParser().Parse("2.0.5"); - Assert.IsTrue(enforceVersion("2.0.5", actualVersion)); - Assert.IsTrue(enforceVersion("2.0.4", actualVersion)); - Assert.IsTrue(enforceVersion("[2.0.5]", actualVersion)); - Assert.IsFalse(enforceVersion("[2.0.6,)", actualVersion)); - Assert.IsFalse(enforceVersion("[2.0.6]", actualVersion)); - Assert.IsTrue(enforceVersion("[2.0,2.1]", actualVersion)); - Assert.IsFalse(enforceVersion("[2.0,2.0.3]", actualVersion)); - Assert.IsTrue(enforceVersion("[2.0,2.0.5]", actualVersion)); - Assert.IsFalse(enforceVersion("[2.0,2.0.5)", actualVersion)); + ISortableVersion actualSortableVersion = new MavenVersionParser().Parse("2.0.5"); + Assert.IsTrue(enforceVersion("2.0.5", actualSortableVersion)); + Assert.IsTrue(enforceVersion("2.0.4", actualSortableVersion)); + Assert.IsTrue(enforceVersion("[2.0.5]", actualSortableVersion)); + Assert.IsFalse(enforceVersion("[2.0.6,)", actualSortableVersion)); + Assert.IsFalse(enforceVersion("[2.0.6]", actualSortableVersion)); + Assert.IsTrue(enforceVersion("[2.0,2.1]", actualSortableVersion)); + Assert.IsFalse(enforceVersion("[2.0,2.0.3]", actualSortableVersion)); + Assert.IsTrue(enforceVersion("[2.0,2.0.5]", actualSortableVersion)); + Assert.IsFalse(enforceVersion("[2.0,2.0.5)", actualSortableVersion)); } - public bool enforceVersion(string requiredMavenVersionRange, IVersion actualVersion) + public bool enforceVersion(string requiredMavenVersionRange, ISortableVersion actualSortableVersion) { MavenVersionRange vr = null; vr = MavenVersionRange.CreateFromVersionSpec(requiredMavenVersionRange); - return vr.ContainsVersion(actualVersion); + return vr.ContainsVersion(actualSortableVersion); } [Test] diff --git a/source/Octopus.Versioning.Tests/Octopus/OctopusVersionCompareTests.cs b/source/Octopus.Versioning.Tests/Octopus/OctopusVersionCompareTests.cs index c1b15a7..f879f30 100644 --- a/source/Octopus.Versioning.Tests/Octopus/OctopusVersionCompareTests.cs +++ b/source/Octopus.Versioning.Tests/Octopus/OctopusVersionCompareTests.cs @@ -132,7 +132,7 @@ public void MatchingVersionsShouldBeGroupedCorrectly(string version) var ver1 = VersionFactory.CreateOctopusVersion(version); var ver2 = VersionFactory.CreateOctopusVersion(version); - var items = new List {ver1, ver2}.GroupBy(i => i).ToList(); + var items = new List {ver1, ver2}.GroupBy(i => i).ToList(); Assert.AreEqual(1, items.Count); } diff --git a/source/Octopus.Versioning.Tests/Octopus/OctopusVersionMaskParserTests.cs b/source/Octopus.Versioning.Tests/Octopus/OctopusVersionMaskParserTests.cs index 98c2c05..a23198b 100644 --- a/source/Octopus.Versioning.Tests/Octopus/OctopusVersionMaskParserTests.cs +++ b/source/Octopus.Versioning.Tests/Octopus/OctopusVersionMaskParserTests.cs @@ -203,7 +203,7 @@ public void IsMask(string mask, bool isMask) [TestCase("1.i.i", "2.0.0", null)] public void GetLatestVersionMask(string version, string latestVersion, string expected) { - var latestVersions = new List + var latestVersions = new List { new OctopusVersionParser().Parse(latestVersion) }; diff --git a/source/Octopus.Versioning.Tests/PreviousImplementation/SemanticVersionMask.cs b/source/Octopus.Versioning.Tests/PreviousImplementation/SemanticVersionMask.cs index b642381..171caad 100644 --- a/source/Octopus.Versioning.Tests/PreviousImplementation/SemanticVersionMask.cs +++ b/source/Octopus.Versioning.Tests/PreviousImplementation/SemanticVersionMask.cs @@ -46,7 +46,7 @@ public static bool IsMask(string versionString) ); } - public static IVersion GetLatestMaskedVersion(string mask, List versions) + public static ISortableVersion GetLatestMaskedVersion(string mask, List versions) { var maskMatch = new MaskMatchedVersion(mask); @@ -87,18 +87,18 @@ public static IVersion GetLatestMaskedVersion(string mask, List versio .FirstOrDefault(); } - public static IVersion ApplyMask(string mask, IVersion currentVersion) + public static ISortableVersion ApplyMask(string mask, ISortableVersion currentSortableVersion) { var match = FormatRegex.Match(mask); if (!match.Success) return VersionFactory.CreateSemanticVersion(mask); - return currentVersion == null + return currentSortableVersion == null ? GenerateVersionFromMask(new MaskMatchedVersion(mask)) - : GenerateVersionFromCurrent(new MaskMatchedVersion(mask), new MaskMatchedVersion(currentVersion.ToString())); + : GenerateVersionFromCurrent(new MaskMatchedVersion(mask), new MaskMatchedVersion(currentSortableVersion.ToString())); } - static IVersion GenerateVersionFromMask(MaskMatchedVersion mask) + static ISortableVersion GenerateVersionFromMask(MaskMatchedVersion mask) { var result = new StringBuilder(); result.Append(mask.Major.EvaluateFromMask()); @@ -110,7 +110,7 @@ static IVersion GenerateVersionFromMask(MaskMatchedVersion mask) return VersionFactory.CreateSemanticVersion(result.ToString()); } - static IVersion GenerateVersionFromCurrent(MaskMatchedVersion mask, MaskMatchedVersion current) + static ISortableVersion GenerateVersionFromCurrent(MaskMatchedVersion mask, MaskMatchedVersion current) { var result = new StringBuilder(); result.Append(mask.Major.Substitute(current.Major)); diff --git a/source/Octopus.Versioning.Tests/Semver/SemanticVersionCompareTests.cs b/source/Octopus.Versioning.Tests/Semver/SemanticVersionCompareTests.cs index 3c8feb5..ccb6bc3 100644 --- a/source/Octopus.Versioning.Tests/Semver/SemanticVersionCompareTests.cs +++ b/source/Octopus.Versioning.Tests/Semver/SemanticVersionCompareTests.cs @@ -14,7 +14,7 @@ public void TestMatchingVersionsAreGroupedCorrectly() var ver1 = VersionFactory.CreateSemanticVersion(version); var ver2 = VersionFactory.CreateSemanticVersion(version); - var items = new List {ver1, ver2}.GroupBy(i => i).ToList(); + var items = new List {ver1, ver2}.GroupBy(i => i).ToList(); Assert.AreEqual(1, items.Count); } diff --git a/source/Octopus.Versioning.Tests/Unsortable/UnsortableVersionCompareTests.cs b/source/Octopus.Versioning.Tests/Unsortable/UnsortableVersionCompareTests.cs new file mode 100644 index 0000000..838385f --- /dev/null +++ b/source/Octopus.Versioning.Tests/Unsortable/UnsortableVersionCompareTests.cs @@ -0,0 +1,62 @@ +using NUnit.Framework; +using Octopus.Versioning.Unsortable; + +namespace Octopus.Versioning.Tests.Unsortable; + +public class UnsortableVersionCompareTests +{ + static readonly UnsortableVersionParser UnsortableVersionParser = new(); + + // [Test] + // [TestCase("release", "release", 0)] + // [TestCase("release", "qrelease", 1)] + // [TestCase("release", "srelease", -1)] + // [TestCase("123", "123", 0)] + // [TestCase("123", "100", 1)] + // [TestCase("123", "321", -1)] + // [TestCase("123Release", "123Release", 0)] + // [TestCase("123Release", "100Release", 1)] + // [TestCase("123Release", "321Release", -1)] + // [TestCase("release-1", "release-1", 0)] + // [TestCase("release-1", "release-0", 1)] + // [TestCase("release-1", "release-2", -1)] + // [TestCase("release.1", "release.1", 0)] + // [TestCase("release.1", "release.0", 1)] + // [TestCase("release.1", "release.2", -1)] + // [TestCase("release_1", "release_1", 0)] + // [TestCase("release_1", "release_0", 1)] + // [TestCase("release_1", "release_2", -1)] + // [TestCase("release-1", "release_1", 0)] + // [TestCase("release.1", "release_1", 0)] + // [TestCase("release-1", "release_0", 1)] + // [TestCase("release.1", "release_2", -1)] + // [TestCase("release+123", "release+321", 0)] + // [TestCase("release-1+123", "release-1+321", 0)] + // [TestCase("release-1+123", "release-0+321", 1)] + // [TestCase("release-1+123", "release-2+321", -1)] + // public void TestComparisons(string version1, string version2, int result) + // { + // var parsedVersion1 = UnsortableVersionParser.Parse(version1); + // var parsedVersion2 = UnsortableVersionParser.Parse(version2); + // Assert.AreEqual(result, parsedVersion1.CompareTo(parsedVersion2)); + // } + + // [Test] + // [TestCase("release-1", "release-1", true)] + // [TestCase("release-1", "release-2", false)] + // public void TestEquality(string version1, string version2, bool result) + // { + // var parsedVersion1 = UnsortableVersionParser.Parse(version1); + // var parsedVersion2 = UnsortableVersionParser.Parse(version2); + // Assert.AreEqual(result, Equals(parsedVersion1, parsedVersion2)); + // } + // + // [Test] + // public void TestGetHashCode() + // { + // var versionString = "release-1"; + // var parsedVersion = UnsortableVersionParser.Parse(versionString); + // + // Assert.AreEqual(versionString.GetHashCode(), parsedVersion.GetHashCode()); + // } +} \ No newline at end of file diff --git a/source/Octopus.Versioning.Tests/Unsortable/UnsortableVersionParserTests.cs b/source/Octopus.Versioning.Tests/Unsortable/UnsortableVersionParserTests.cs new file mode 100644 index 0000000..7ddad4c --- /dev/null +++ b/source/Octopus.Versioning.Tests/Unsortable/UnsortableVersionParserTests.cs @@ -0,0 +1,81 @@ +using System; +using NUnit.Framework; +using Octopus.Versioning.Unsortable; + +namespace Octopus.Versioning.Tests.Unsortable; + +[TestFixture] +public class UnsortableVersionParserTests +{ + [Test] + // Release + [TestCase("foobar", "foobar", "")] + [TestCase("2db4a87840113c", "2db4a87840113c", "")] + [TestCase("123456", "123456", "")] + [TestCase("foobar-qwerty", "foobar-qwerty", "")] + [TestCase("foobar.qwerty", "foobar.qwerty", "")] + [TestCase("foobar_qwerty", "foobar_qwerty", "")] + [TestCase("foobar-12345", "foobar-12345", "")] + // Metadata + [TestCase("foobar+12345", "foobar", "12345")] + [TestCase("foobar+123.456", "foobar", "123.456")] + [TestCase("foobar+123_456", "foobar", "123_456")] + [TestCase("foobar+123-456", "foobar", "123-456")] + [TestCase("foobar+123+456", "foobar", "123+456")] + [TestCase("foobar+1.2_3-4+5", "foobar", "1.2_3-4+5")] + [TestCase("foobar+qwerty", "foobar", "qwerty")] + [TestCase("foobar-qwerty+12345", "foobar-qwerty", "12345")] + // Fail Cases + [TestCase("!@#$%^", "", "")] + [TestCase("foobar-!@#$%", "", "")] + [TestCase("foobar-qwerty+!@#$%", "", "")] + [TestCase("foo bar", "", "")] + [TestCase("foobar-qwe ty", "", "")] + [TestCase("foobar+123 456", "", "")] + [TestCase("foo bar-qwe ty+123 456", "", "")] + [TestCase("!foobar", "", "")] + [TestCase("foo!bar", "", "")] + [TestCase("foobar!", "", "")] + public void ShouldParseSuccessfully(string input, string expectedRelease, string expectedMetadata) + { + _ = new UnsortableVersionParser().TryParse(input, out var parsedVersion); + AssertVersionNumbersAreZero(parsedVersion); + Assert.AreEqual(expectedRelease, parsedVersion.Release); + Assert.AreEqual(expectedMetadata, parsedVersion.Metadata); + } + + [Test] + public void ShouldThrowExceptionOnEmptyInput() + { + var input = ""; + Assert.Catch(() => new UnsortableVersionParser().Parse(input)); + } + + [Test] + public void ShouldThrowExceptionOnWhiteSpaceInput() + { + var input = " "; + Assert.Catch(() => new UnsortableVersionParser().Parse(input)); + } + + [Test] + public void ShouldThrowExceptionOnNullInput() + { + Assert.Catch(() => new UnsortableVersionParser().Parse(null)); + } + + [Test] + public void ShouldThrowExceptionOnFailureToParse() + { + var input = "bad versions string"; + Assert.Catch(() => new UnsortableVersionParser().Parse(input)); + } + + void AssertVersionNumbersAreZero(UnsortableSortableVersion sortableVersion) + { + Assert.AreEqual(0, sortableVersion.Major); + Assert.AreEqual(0, sortableVersion.Minor); + Assert.AreEqual(0, sortableVersion.Patch); + Assert.AreEqual(0, sortableVersion.Revision); + } +} \ No newline at end of file diff --git a/source/Octopus.Versioning/ISortableVersion.cs b/source/Octopus.Versioning/ISortableVersion.cs new file mode 100644 index 0000000..53df82d --- /dev/null +++ b/source/Octopus.Versioning/ISortableVersion.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; + +namespace Octopus.Versioning +{ + /// + /// Represents the base components of a version recognised by Octopus. + /// The terminolgy here comes from SemVer, but can be mapped to other + /// versioning schemes like Maven. + /// + /// All classes should reference this interface, but there are some + /// exceptions. + /// + /// Some modelling classes need to reference the SemanticVersionConverter + /// to ensure that constructor paramaters and properties of the type + /// IVersion are converted to and from a SemanticVersion object. + /// + /// JSON converters like SemanticVersionConverter assume that + /// any version object being stored in the database is a SemanticVersion + /// object. This is because Octopus assumes the use of SemanticVersion + /// in all processes other than external feeds, which do not have + /// version information directly saved in the database. + /// + /// The version factory classes also need to know about the conrete + /// classes. + /// + /// Outside of those two use cases, all other references to a version + /// should be through this interface. + /// + public interface ISortableVersion : IVersion, IComparable + { + } +} \ No newline at end of file diff --git a/source/Octopus.Versioning/IVersion.cs b/source/Octopus.Versioning/IVersion.cs index b994f71..c0023e6 100644 --- a/source/Octopus.Versioning/IVersion.cs +++ b/source/Octopus.Versioning/IVersion.cs @@ -1,33 +1,8 @@ -using System; using System.Collections.Generic; namespace Octopus.Versioning { - /// - /// Represents the base components of a version recognised by Octopus. - /// The terminolgy here comes from SemVer, but can be mapped to other - /// versioning schemes like Maven. - /// - /// All classes should reference this interface, but there are some - /// exceptions. - /// - /// Some modelling classes need to reference the SemanticVersionConverter - /// to ensure that constructor paramaters and properties of the type - /// IVersion are converted to and from a SemanticVersion object. - /// - /// JSON converters like SemanticVersionConverter assume that - /// any version object being stored in the database is a SemanticVersion - /// object. This is because Octopus assumes the use of SemanticVersion - /// in all processes other than external feeds, which do not have - /// version information directly saved in the database. - /// - /// The version factory classes also need to know about the conrete - /// classes. - /// - /// Outside of those two use cases, all other references to a version - /// should be through this interface. - /// - public interface IVersion : IComparable + public interface IVersion { int Major { get; } int Minor { get; } diff --git a/source/Octopus.Versioning/IVersionComparer.cs b/source/Octopus.Versioning/IVersionComparer.cs index fbe9d32..dc59263 100644 --- a/source/Octopus.Versioning/IVersionComparer.cs +++ b/source/Octopus.Versioning/IVersionComparer.cs @@ -10,7 +10,7 @@ namespace Octopus.Versioning /// IVersionComparer represents a version comparer capable of sorting and determining the equality of /// SemanticVersion objects. /// - public interface IVersionComparer : IEqualityComparer, IComparer + public interface IVersionComparer : IEqualityComparer, IComparer { } } \ No newline at end of file diff --git a/source/Octopus.Versioning/Maven/MavenPackageID.cs b/source/Octopus.Versioning/Maven/MavenPackageID.cs index 82b7c7e..2a7a30b 100644 --- a/source/Octopus.Versioning/Maven/MavenPackageID.cs +++ b/source/Octopus.Versioning/Maven/MavenPackageID.cs @@ -84,15 +84,15 @@ public MavenPackageID([NotNull] public MavenPackageID([NotNull] string? id, - IVersion version) : this(id) + ISortableVersion sortableVersion) : this(id) { if (string.IsNullOrWhiteSpace(id) || id.Split(':').Length != 2) throw new ArgumentException("Package ID must be in the format Group:Artifact e.g. com.google.guava:guava or junit:junit."); - if (version == null) + if (sortableVersion == null) throw new ArgumentException("version can not be null"); - Version = version.ToString(); + Version = sortableVersion.ToString(); } /// @@ -154,7 +154,7 @@ public MavenPackageID([NotNull] public string? Classifier { get; } public string DisplayName => ToString(DISPLAY_DELIMITER); - public IVersion? SemanticVersion => Version == null ? null : new MavenVersionParser().Parse(Version); + public ISortableVersion? SemanticVersion => Version == null ? null : new MavenVersionParser().Parse(Version); /// /// The path to the metadata file for the artifact @@ -248,7 +248,7 @@ public string DefaultArtifactPath /// The optional version /// A MavenPackageID created to match the package id an optional packaging defined in the UI /// thrown if the input is not in the correct format - public static MavenPackageID CreatePackageIdFromOctopusInput(string input, IVersion? version = null) + public static MavenPackageID CreatePackageIdFromOctopusInput(string input, ISortableVersion? version = null) { var splitVersion = input.Split(':').ToList(); if (!(splitVersion.Count >= 2 && splitVersion.Count <= 4)) diff --git a/source/Octopus.Versioning/Maven/MavenVersion.cs b/source/Octopus.Versioning/Maven/MavenVersion.cs index 4f6f387..18cd224 100644 --- a/source/Octopus.Versioning/Maven/MavenVersion.cs +++ b/source/Octopus.Versioning/Maven/MavenVersion.cs @@ -5,7 +5,7 @@ namespace Octopus.Versioning.Maven { - public class MavenVersion : IVersion + public class MavenVersion : ISortableVersion { public MavenVersion(int major, int minor, diff --git a/source/Octopus.Versioning/Maven/Ranges/MavenVersionRange.cs b/source/Octopus.Versioning/Maven/Ranges/MavenVersionRange.cs index d5f7237..aef362c 100644 --- a/source/Octopus.Versioning/Maven/Ranges/MavenVersionRange.cs +++ b/source/Octopus.Versioning/Maven/Ranges/MavenVersionRange.cs @@ -9,14 +9,14 @@ namespace Octopus.Versioning.Maven.Ranges public class MavenVersionRange { MavenVersionRange( - IVersion? recommendedVersion, + ISortableVersion? recommendedVersion, List restrictions ) { RecommendedVersion = recommendedVersion; Restrictions = restrictions; } - public IVersion? RecommendedVersion { get; } + public ISortableVersion? RecommendedVersion { get; } public List Restrictions { get; } @@ -56,9 +56,9 @@ public MavenVersionRange CloneOf() var restrictions = new List(); var process = spec; - IVersion? version = null; - IVersion? upperBound = null; - IVersion? lowerBound = null; + ISortableVersion? version = null; + ISortableVersion? upperBound = null; + ISortableVersion? lowerBound = null; while ( process.StartsWith( "[" ) || process.StartsWith( "(" ) ) { @@ -117,9 +117,9 @@ static Restriction ParseRestriction( string spec ) if ( !lowerBoundInclusive || !upperBoundInclusive ) throw new InvalidVersionSpecificationException( "Single version must be surrounded by []: " + spec ); - IVersion version = new MavenVersionParser().Parse( process ); + ISortableVersion sortableVersion = new MavenVersionParser().Parse( process ); - restriction = new Restriction( version, lowerBoundInclusive, version, upperBoundInclusive ); + restriction = new Restriction( sortableVersion, lowerBoundInclusive, sortableVersion, upperBoundInclusive ); } else { @@ -128,10 +128,10 @@ static Restriction ParseRestriction( string spec ) if ( lowerBound.Equals( upperBound ) ) throw new InvalidVersionSpecificationException( "Range cannot have identical boundaries: " + spec ); - IVersion? lowerVersion = null; + ISortableVersion? lowerVersion = null; if ( lowerBound.Length > 0 ) lowerVersion = new MavenVersionParser().Parse( lowerBound ); - IVersion? upperVersion = null; + ISortableVersion? upperVersion = null; if ( upperBound.Length > 0 ) upperVersion = new MavenVersionParser().Parse( upperBound ); @@ -189,7 +189,7 @@ public MavenVersionRange Restrict( MavenVersionRange restriction ) else restrictions = Intersection( r1, r2 ); - IVersion? version = null; + ISortableVersion? version = null; if ( restrictions.Count > 0 ) { foreach (var r in restrictions) @@ -248,8 +248,8 @@ List Intersection( List r1, List r2 ) if ( res1.UpperBound == null || res2.LowerBound == null || res1.UpperBound.CompareTo( res2.LowerBound ) >= 0 ) { - IVersion? lower; - IVersion? upper; + ISortableVersion? lower; + ISortableVersion? upper; bool lowerInclusive; bool upperInclusive; @@ -359,9 +359,9 @@ List Intersection( List r1, List r2 ) return restrictions; } - public IVersion? GetSelectedVersion( ) + public ISortableVersion? GetSelectedVersion( ) { - IVersion? version = null; + ISortableVersion? version = null; if ( RecommendedVersion != null ) { version = RecommendedVersion; @@ -397,11 +397,11 @@ public override string ToString() return string.Join(",", Restrictions); } - public IVersion? MatchVersion( List versions ) + public ISortableVersion? MatchVersion( List versions ) { // TODO could be more efficient by sorting the list and then moving along the restrictions in order? - IVersion? matched = null; + ISortableVersion? matched = null; foreach (var version in versions) if ( ContainsVersion( version ) ) // valid - check if it is greater than the currently matched version @@ -410,10 +410,10 @@ public override string ToString() return matched; } - public bool ContainsVersion( IVersion version ) + public bool ContainsVersion( ISortableVersion sortableVersion ) { foreach (var restriction in Restrictions) - if ( restriction.ContainsVersion( version ) ) + if ( restriction.ContainsVersion( sortableVersion ) ) return true; return false; } diff --git a/source/Octopus.Versioning/Maven/Ranges/Restriction.cs b/source/Octopus.Versioning/Maven/Ranges/Restriction.cs index 8ac8dd1..e39596c 100644 --- a/source/Octopus.Versioning/Maven/Ranges/Restriction.cs +++ b/source/Octopus.Versioning/Maven/Ranges/Restriction.cs @@ -11,9 +11,9 @@ public class Restriction public static readonly Restriction EVERYTHING = new Restriction(null, false, null, false); public Restriction( - IVersion? lowerBound, + ISortableVersion? lowerBound, bool lowerBoundInclusive, - IVersion? upperBound, + ISortableVersion? upperBound, bool upperBoundInclusive) { LowerBound = lowerBound; @@ -22,16 +22,16 @@ public Restriction( IsUpperBoundInclusive = upperBoundInclusive; } - public IVersion? LowerBound { get; } + public ISortableVersion? LowerBound { get; } public bool IsLowerBoundInclusive { get; } - public IVersion? UpperBound { get; } + public ISortableVersion? UpperBound { get; } public bool IsUpperBoundInclusive { get; } - public bool ContainsVersion(IVersion version) + public bool ContainsVersion(ISortableVersion sortableVersion) { if (LowerBound != null) { - var comparison = LowerBound.CompareTo(version); + var comparison = LowerBound.CompareTo(sortableVersion); if (comparison == 0 && !IsLowerBoundInclusive) return false; @@ -41,7 +41,7 @@ public bool ContainsVersion(IVersion version) if (UpperBound != null) { - var comparison = UpperBound.CompareTo(version); + var comparison = UpperBound.CompareTo(sortableVersion); if (comparison == 0 && !IsUpperBoundInclusive) return false; diff --git a/source/Octopus.Versioning/Octopus/OctopusVersion.cs b/source/Octopus.Versioning/Octopus/OctopusVersion.cs index 2b2e540..e64cce4 100644 --- a/source/Octopus.Versioning/Octopus/OctopusVersion.cs +++ b/source/Octopus.Versioning/Octopus/OctopusVersion.cs @@ -4,7 +4,7 @@ namespace Octopus.Versioning.Octopus { - public class OctopusVersion : IVersion + public class OctopusVersion : ISortableVersion { public OctopusVersion(string? prefix, int major, @@ -46,7 +46,7 @@ public OctopusVersion(string? prefix, public virtual int CompareTo(object obj) { - if (obj is IVersion objVersion) + if (obj is ISortableVersion objVersion) { if (Major.CompareTo(objVersion.Major) != 0) return Major.CompareTo(objVersion.Major); if (Minor.CompareTo(objVersion.Minor) != 0) return Minor.CompareTo(objVersion.Minor); @@ -83,7 +83,7 @@ public override string ToString() public override bool Equals(object obj) { - if (obj is IVersion objVersion) + if (obj is ISortableVersion objVersion) return CompareTo(objVersion) == 0; return false; diff --git a/source/Octopus.Versioning/Octopus/OctopusVersionMask.cs b/source/Octopus.Versioning/Octopus/OctopusVersionMask.cs index 0223ab5..6ed58c7 100644 --- a/source/Octopus.Versioning/Octopus/OctopusVersionMask.cs +++ b/source/Octopus.Versioning/Octopus/OctopusVersionMask.cs @@ -55,7 +55,7 @@ public OctopusVersionMask( public bool IsMask => DidParse && (Major.IsSubstitute || Minor.IsSubstitute || Patch.IsSubstitute || Release.IsSubstitute || Revision.IsSubstitute || Metadata.IsSubstitute); - public IVersion? GetLatestMaskedVersion(List versions) + public ISortableVersion? GetLatestMaskedVersion(List versions) { var maskMajor = Major.IsPresent && !Major.IsSubstitute ? int.Parse(Major.Value) : 0; var maskMinor = Minor.IsPresent && !Minor.IsSubstitute ? int.Parse(Minor.Value) : 0; @@ -96,7 +96,7 @@ public OctopusVersionMask( .FirstOrDefault(); } - public IVersion GenerateVersionFromMask() + public ISortableVersion GenerateVersionFromMask() { var result = new StringBuilder(); result.Append(Prefix); @@ -109,7 +109,7 @@ public IVersion GenerateVersionFromMask() return VersionFactory.CreateOctopusVersion(result.ToString()); } - public IVersion GenerateVersionFromCurrent(OctopusVersionMask current) + public ISortableVersion GenerateVersionFromCurrent(OctopusVersionMask current) { var result = new StringBuilder(); result.Append(Prefix); diff --git a/source/Octopus.Versioning/Octopus/OctopusVersionMaskParser.cs b/source/Octopus.Versioning/Octopus/OctopusVersionMaskParser.cs index 41d0cd8..74a84f4 100644 --- a/source/Octopus.Versioning/Octopus/OctopusVersionMaskParser.cs +++ b/source/Octopus.Versioning/Octopus/OctopusVersionMaskParser.cs @@ -47,7 +47,7 @@ public OctopusVersionMask Parse(string? version) new OctopusVersionMask.MetadataComponent(result.Groups[Meta])); } - public IVersion ApplyMask(string? mask, IVersion? currentVersion) + public ISortableVersion ApplyMask(string? mask, ISortableVersion? currentVersion) { var parsedMask = Parse(mask); diff --git a/source/Octopus.Versioning/Semver/SemVerFactory.cs b/source/Octopus.Versioning/Semver/SemVerFactory.cs index 6fd3948..324b2e9 100644 --- a/source/Octopus.Versioning/Semver/SemVerFactory.cs +++ b/source/Octopus.Versioning/Semver/SemVerFactory.cs @@ -20,7 +20,7 @@ public static SemanticVersion CreateVersion(string input, bool preserveMissingCo return ver; } - public static IVersion? CreateVersionOrNone(string input, bool preserveMissingComponents = false) + public static ISortableVersion? CreateVersionOrNone(string input, bool preserveMissingComponents = false) { return TryCreateVersion(input, preserveMissingComponents); } diff --git a/source/Octopus.Versioning/Semver/StrictSemanticVersion.cs b/source/Octopus.Versioning/Semver/StrictSemanticVersion.cs index ea069ad..8e91b4d 100644 --- a/source/Octopus.Versioning/Semver/StrictSemanticVersion.cs +++ b/source/Octopus.Versioning/Semver/StrictSemanticVersion.cs @@ -10,7 +10,7 @@ namespace Octopus.Versioning.Semver /// /// A strict SemVer implementation /// - public partial class StrictSemanticVersion : IVersion + public partial class StrictSemanticVersion : ISortableVersion { static readonly ISemanticVersionUtils utils = new SemanticVersionUtils(); readonly IEnumerable? releaseLabels; diff --git a/source/Octopus.Versioning/Semver/VersionComparer.cs b/source/Octopus.Versioning/Semver/VersionComparer.cs index 579ea8d..e0d76b0 100644 --- a/source/Octopus.Versioning/Semver/VersionComparer.cs +++ b/source/Octopus.Versioning/Semver/VersionComparer.cs @@ -54,7 +54,7 @@ public VersionComparer(VersionComparison versionComparison) /// /// Determines if both versions are equal. /// - public bool Equals(IVersion? x, IVersion? y) + public bool Equals(ISortableVersion? x, ISortableVersion? y) { return Compare(x, y) == 0; } @@ -62,7 +62,7 @@ public bool Equals(IVersion? x, IVersion? y) /// /// Compares the given versions using the VersionComparison mode. /// - public static int Compare(IVersion? version1, IVersion? version2, VersionComparison versionComparison) + public static int Compare(ISortableVersion? version1, ISortableVersion? version2, VersionComparison versionComparison) { IVersionComparer comparer = new VersionComparer(versionComparison); return comparer.Compare(version1, version2); @@ -71,7 +71,7 @@ public static int Compare(IVersion? version1, IVersion? version2, VersionCompari /// /// Gives a hash code based on the normalized version string. /// - public int GetHashCode(IVersion? version) + public int GetHashCode(ISortableVersion? version) { if (ReferenceEquals(version, null)) return 0; @@ -102,7 +102,7 @@ public int GetHashCode(IVersion? version) /// /// Compare versions. /// - public int Compare(IVersion? x, IVersion? y) + public int Compare(ISortableVersion? x, ISortableVersion? y) { if (ReferenceEquals(x, y)) return 0; diff --git a/source/Octopus.Versioning/Unsortable/UnsortableSortableVersion.cs b/source/Octopus.Versioning/Unsortable/UnsortableSortableVersion.cs new file mode 100644 index 0000000..c14f67f --- /dev/null +++ b/source/Octopus.Versioning/Unsortable/UnsortableSortableVersion.cs @@ -0,0 +1,136 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Octopus.Versioning.Unsortable +{ + public class UnsortableSortableVersion: IVersion + { + public UnsortableSortableVersion(string release, string? metadata, string? originalString) + { + Metadata = metadata; + Release = release; + OriginalString = originalString ?? string.Empty; + } + + public int Major => 0; + public int Minor => 0; + public int Patch => 0; + public int Revision => 0; + public bool IsPrerelease => false; + public IEnumerable ReleaseLabels => Enumerable.Empty(); + public string? Metadata { get; } + public bool HasMetadata => !string.IsNullOrWhiteSpace(Metadata); + public string Release { get; } + public string OriginalString { get; } + + public VersionFormat Format => VersionFormat.Unsortable; + + // public int CompareTo(object obj) + // { + // if (!(obj is ISortableVersion objVersion)) + // return -1; + // + // if (string.Compare(Release.AlphaNumericOnly(), (objVersion.Release ?? string.Empty).AlphaNumericOnly(), StringComparison.Ordinal) != 0) + // return CompareReleaseLabels(Release.AlphaNumericOnly().Split('.', '-', '_'), (objVersion.Release ?? string.Empty).AlphaNumericOnly().Split('.', '-', '_')); + // + // return 0; + // } + // + // public override string ToString() + // { + // return OriginalString; + // } + // + // public override bool Equals(object obj) + // { + // if (obj is ISortableVersion objVersion) + // return CompareTo(objVersion) == 0; + // + // return false; + // } + // + // public override int GetHashCode() + // { + // return Release.GetHashCode(); + // } + // + // /// + // /// Compares sets of release labels. + // /// + // static int CompareReleaseLabels(IEnumerable version1, IEnumerable version2) + // { + // var result = 0; + // + // using var a = version1.GetEnumerator(); + // using var b = version2.GetEnumerator(); + // + // var aExists = a.MoveNext(); + // var bExists = b.MoveNext(); + // + // while (aExists || bExists) + // { + // if (!aExists && bExists) + // return -1; + // + // if (aExists && !bExists) + // return 1; + // + // // compare the labels + // result = CompareRelease(a.Current, b.Current); + // + // if (result != 0) + // return result; + // + // aExists = a.MoveNext(); + // bExists = b.MoveNext(); + // } + // + // return result; + // } + // + // /// + // /// Release labels are compared as numbers if they are numeric, otherwise they will be compared + // /// as strings. + // /// + // static int CompareRelease(string version1, string version2) + // { + // var version1Num = 0; + // var version2Num = 0; + // var result = 0; + // + // // check if the identifiers are numeric + // var v1IsNumeric = int.TryParse(version1, out version1Num); + // var v2IsNumeric = int.TryParse(version2, out version2Num); + // + // // if both are numeric compare them as numbers + // if (v1IsNumeric && v2IsNumeric) + // { + // result = version1Num.CompareTo(version2Num); + // } + // else if (v1IsNumeric || v2IsNumeric) + // { + // // numeric labels come before alpha labels + // if (v1IsNumeric) + // result = -1; + // else + // result = 1; + // } + // else + // { + // // Ignoring 2.0.0 case sensitive compare. Everything will be compared case insensitively as 2.0.1 specifies. + // var stringCompareResult = StringComparer.OrdinalIgnoreCase.Compare(version1, version2); + // if (stringCompareResult < 0) + // { + // result = -1; + // } + // else if (stringCompareResult > 0) + // { + // result = 1; + // } + // } + // + // return result; + // } + } +} \ No newline at end of file diff --git a/source/Octopus.Versioning/Unsortable/UnsortableVersionParser.cs b/source/Octopus.Versioning/Unsortable/UnsortableVersionParser.cs new file mode 100644 index 0000000..2fd3ed3 --- /dev/null +++ b/source/Octopus.Versioning/Unsortable/UnsortableVersionParser.cs @@ -0,0 +1,56 @@ +using System; +using System.Text.RegularExpressions; + +namespace Octopus.Versioning.Unsortable +{ + public class UnsortableVersionParser + { + const string Release = "release"; + const string Meta = "buildmetadata"; + + static readonly Regex VersionRegex = new Regex($@"^(?<{Release}>([A-Za-z0-9]*?)([.\-_\\]([A-Za-z0-9.\-_\\]*?)?)?)?" + + $@"(?:\+(?<{Meta}>[A-Za-z0-9_\-.\\+]*?))?\s*$" + ); + + public UnsortableSortableVersion Parse(string? version) + { + if (string.IsNullOrWhiteSpace(version)) + throw new ArgumentException("The version can not be an empty string"); + + var sanitisedVersion = version ?? string.Empty; + // SemVerFactory treated the original string as if it had no spaces at all + var noSpaces = sanitisedVersion.Replace(" ", ""); + + // We parse on the original string. This *does not* tolerate spaces in prerelease fields or metadata + // just like SemVerFactory. + var result = VersionRegex.Match(sanitisedVersion); + + if (!result.Success) + throw new ArgumentException("The supplied version was not valid"); + + return new UnsortableSortableVersion( + result.Groups[Release].Success ? result.Groups[Release].Value : string.Empty, + result.Groups[Meta].Success ? result.Groups[Meta].Value : string.Empty, + noSpaces + ); + } + + public bool TryParse(string version, out UnsortableSortableVersion parsedSortableVersion) + { + try + { + parsedSortableVersion = Parse(version); + return true; + } + catch + { + parsedSortableVersion = new UnsortableSortableVersion( + string.Empty, + string.Empty, + null + ); + return false; + } + } + } +} \ No newline at end of file diff --git a/source/Octopus.Versioning/VersionFactory.cs b/source/Octopus.Versioning/VersionFactory.cs index d079b93..634e4b4 100644 --- a/source/Octopus.Versioning/VersionFactory.cs +++ b/source/Octopus.Versioning/VersionFactory.cs @@ -4,12 +4,13 @@ using Octopus.Versioning.Maven; using Octopus.Versioning.Octopus; using Octopus.Versioning.Semver; +using Octopus.Versioning.Unsortable; namespace Octopus.Versioning { public static class VersionFactory { - public static IVersion CreateVersion(string input, VersionFormat format) + public static ISortableVersion CreateVersion(string input, VersionFormat format) { switch (format) { @@ -24,7 +25,7 @@ public static IVersion CreateVersion(string input, VersionFormat format) } } - public static IVersion? TryCreateVersion(string input, VersionFormat format) + public static ISortableVersion? TryCreateVersion(string input, VersionFormat format) { switch (format) { @@ -39,37 +40,37 @@ public static IVersion CreateVersion(string input, VersionFormat format) } } - public static IVersion CreateMavenVersion(string input) + public static ISortableVersion CreateMavenVersion(string input) { return new MavenVersionParser().Parse(input); } - public static IVersion CreateSemanticVersion(string input, bool preserveMissingComponents = false) + public static ISortableVersion CreateSemanticVersion(string input, bool preserveMissingComponents = false) { return SemVerFactory.CreateVersion(input, preserveMissingComponents); } - public static IVersion CreateSemanticVersion(int major, int minor, int patch, string releaseLabel) + public static ISortableVersion CreateSemanticVersion(int major, int minor, int patch, string releaseLabel) { return new SemanticVersion(major, minor, patch, releaseLabel); } - public static IVersion CreateSemanticVersion(int major, int minor, int patch) + public static ISortableVersion CreateSemanticVersion(int major, int minor, int patch) { return new SemanticVersion(major, minor, patch); } - public static IVersion CreateSemanticVersion(int major, int minor, int patch, int revision) + public static ISortableVersion CreateSemanticVersion(int major, int minor, int patch, int revision) { return new SemanticVersion(major, minor, patch, revision); } - public static IVersion CreateSemanticVersion(Version version, string? releaseLabel = null, string? metadata = null) + public static ISortableVersion CreateSemanticVersion(Version version, string? releaseLabel = null, string? metadata = null) { return new SemanticVersion(version, releaseLabel, metadata); } - public static IVersion CreateSemanticVersion(int major, + public static ISortableVersion CreateSemanticVersion(int major, int minor, int patch, int revision, @@ -85,7 +86,7 @@ public static IVersion CreateSemanticVersion(int major, metadata); } - public static IVersion TryCreateMavenVersion(string input) + public static ISortableVersion TryCreateMavenVersion(string input) { /* * Any version is valid for Maven @@ -93,17 +94,17 @@ public static IVersion TryCreateMavenVersion(string input) return new MavenVersionParser().Parse(input); } - public static IVersion? TryCreateSemanticVersion(string input, bool preserveMissingComponents = false) + public static ISortableVersion? TryCreateSemanticVersion(string input, bool preserveMissingComponents = false) { return SemVerFactory.TryCreateVersion(input, preserveMissingComponents); } - public static IVersion? CreateSemanticVersionOrNone(string input, bool preserveMissingComponents = false) + public static ISortableVersion? CreateSemanticVersionOrNone(string input, bool preserveMissingComponents = false) { return SemVerFactory.CreateVersionOrNone(input, preserveMissingComponents); } - public static IVersion CreateSemanticVersion(Version version, + public static ISortableVersion CreateSemanticVersion(Version version, IEnumerable releaseLabels, string metadata, string originalVersion) @@ -115,12 +116,12 @@ public static IVersion CreateSemanticVersion(Version version, originalVersion); } - public static IVersion CreateDockerTag(string input) + public static ISortableVersion CreateDockerTag(string input) { return new DockerTag(new OctopusVersionParser().Parse(input)); } - public static IVersion? TryCreateDockerTag(string input) + public static ISortableVersion? TryCreateDockerTag(string input) { try { @@ -133,12 +134,12 @@ public static IVersion CreateDockerTag(string input) } } - public static IVersion CreateOctopusVersion(string input) + public static ISortableVersion CreateOctopusVersion(string input) { return new OctopusVersionParser().Parse(input); } - public static IVersion? TryCreateOctopusVersion(string input) + public static ISortableVersion? TryCreateOctopusVersion(string input) { try { @@ -150,5 +151,22 @@ public static IVersion CreateOctopusVersion(string input) return null; } } + + public static IVersion CreateUnsortableVersion(string input) + { + return new UnsortableVersionParser().Parse(input); + } + + public static IVersion? TryCreateUnsortableVersion(string input) + { + try + { + return CreateUnsortableVersion(input); + } + catch + { + return null; + } + } } } \ No newline at end of file diff --git a/source/Octopus.Versioning/VersionFormat.cs b/source/Octopus.Versioning/VersionFormat.cs index 12051fc..14e84f7 100644 --- a/source/Octopus.Versioning/VersionFormat.cs +++ b/source/Octopus.Versioning/VersionFormat.cs @@ -7,6 +7,7 @@ public enum VersionFormat Semver, Maven, Docker, - Octopus + Octopus, + Unsortable } } \ No newline at end of file diff --git a/source/Octopus.Versioning/VersionFormatter.cs b/source/Octopus.Versioning/VersionFormatter.cs index ab4c1e0..f458637 100644 --- a/source/Octopus.Versioning/VersionFormatter.cs +++ b/source/Octopus.Versioning/VersionFormatter.cs @@ -21,7 +21,7 @@ public class VersionFormatter : IFormatProvider, ICustomFormatter if (argType == typeof(IFormattable)) formatted = ((IFormattable)arg).ToString(format, formatProvider); else if (!string.IsNullOrEmpty(format)) - if (arg is IVersion version) + if (arg is ISortableVersion version) { // single char identifiers if (format.Length == 1) @@ -52,62 +52,62 @@ public class VersionFormatter : IFormatProvider, ICustomFormatter public object? GetFormat(Type formatType) { if (formatType == typeof(ICustomFormatter) || - typeof(IVersion).IsAssignableFrom(formatType)) + typeof(ISortableVersion).IsAssignableFrom(formatType)) return this; return null; } - static string GetNormalizedString(IVersion version) + static string GetNormalizedString(ISortableVersion sortableVersion) { var sb = new StringBuilder(); - sb.Append(Format('V', version)); + sb.Append(Format('V', sortableVersion)); - if (version.IsPrerelease) + if (sortableVersion.IsPrerelease) { sb.Append('-'); - sb.Append(version.Release); + sb.Append(sortableVersion.Release); } - if (version.HasMetadata) + if (sortableVersion.HasMetadata) { sb.Append('+'); - sb.Append(version.Metadata); + sb.Append(sortableVersion.Metadata); } return sb.ToString(); } - static string? Format(char c, IVersion version) + static string? Format(char c, ISortableVersion sortableVersion) { string? s = null; switch (c) { case 'N': - s = GetNormalizedString(version); + s = GetNormalizedString(sortableVersion); break; case 'R': - s = version.Release; + s = sortableVersion.Release; break; case 'M': - s = version.Metadata; + s = sortableVersion.Metadata; break; case 'V': - s = FormatVersion(version); + s = FormatVersion(sortableVersion); break; case 'x': - s = string.Format(CultureInfo.InvariantCulture, "{0}", version.Major); + s = string.Format(CultureInfo.InvariantCulture, "{0}", sortableVersion.Major); break; case 'y': - s = string.Format(CultureInfo.InvariantCulture, "{0}", version.Minor); + s = string.Format(CultureInfo.InvariantCulture, "{0}", sortableVersion.Minor); break; case 'z': - s = string.Format(CultureInfo.InvariantCulture, "{0}", version.Patch); + s = string.Format(CultureInfo.InvariantCulture, "{0}", sortableVersion.Patch); break; case 'r': - var nuGetVersion = version as SemanticVersion; + var nuGetVersion = sortableVersion as SemanticVersion; s = string.Format(CultureInfo.InvariantCulture, "{0}", nuGetVersion != null && nuGetVersion.IsLegacyVersion ? nuGetVersion.Version.Revision : 0); break; } @@ -115,16 +115,16 @@ static string GetNormalizedString(IVersion version) return s; } - static string FormatVersion(IVersion version) + static string FormatVersion(ISortableVersion sortableVersion) { - var nuGetVersion = version as SemanticVersion; + var nuGetVersion = sortableVersion as SemanticVersion; var legacy = nuGetVersion != null && nuGetVersion.IsLegacyVersion; return string.Format(CultureInfo.InvariantCulture, "{0}.{1}.{2}{3}", - version.Major, - version.Minor, - version.Patch, + sortableVersion.Major, + sortableVersion.Minor, + sortableVersion.Patch, legacy ? string.Format(CultureInfo.InvariantCulture, ".{0}", nuGetVersion?.Version.Revision) : null); } }