Skip to content

Commit

Permalink
v18.0.0 - IParsable reuse - Lots of BREAKING CHANGES
Browse files Browse the repository at this point in the history
  • Loading branch information
monoman committed Apr 5, 2024
1 parent a75b268 commit fb50234
Show file tree
Hide file tree
Showing 12 changed files with 266 additions and 86 deletions.
72 changes: 30 additions & 42 deletions InterlockLedger.Commons.NUnit.Tests/System/LimitedRangeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,16 +41,10 @@ public class LimitedRangeTests
#pragma warning disable NUnit2009 // The same value has been provided as both the actual and the expected argument
public void Equality() {
Assert.That(LimitedRange.Empty, Is.EqualTo(LimitedRange.Empty));
var invalidByCause1 = LimitedRange.InvalidBy("Cause1");
var invalidByCause2 = LimitedRange.InvalidBy("Cause2");
Assert.Multiple(() => {
Assert.That(invalidByCause2, Is.EqualTo(invalidByCause1));
Assert.That(invalidByCause1, Is.EqualTo(invalidByCause2));
Assert.That(new LimitedRange(1, 10), Is.EqualTo(new LimitedRange(1, 10)));
});
Assert.Multiple(() => {
Assert.That(invalidByCause1, Is.Not.EqualTo(LimitedRange.Empty));
Assert.That(LimitedRange.Empty, Is.Not.EqualTo(invalidByCause1));
Assert.That(new LimitedRange(1, 10), Is.Not.EqualTo(LimitedRange.Empty));
Assert.That(LimitedRange.Empty, Is.Not.EqualTo(new LimitedRange(1, 10)));
Assert.That(new LimitedRange(1, 11), Is.Not.EqualTo(new LimitedRange(1, 10)));
Assert.That(new LimitedRange(1, 10), Is.Not.EqualTo(new LimitedRange(1, 11)));
});
Expand All @@ -68,17 +62,17 @@ public void DeserializeFromJson(string json, bool isInvalid, bool isEmpty, strin
AssertLimitedRange(lr, json.Replace("\"", ""), isInvalid, isEmpty, cause);
}

[TestCase("[]", false, true, "")]
[TestCase("[1]", false, false, "")]
[TestCase("[1-10]", false, false, "")]
[TestCase("[10-9]", true, false, "End of range (9) must be greater than the start (10)")]
[TestCase("[*]", true, false, """Input '[*]' does not match ^\[\d+(-\d+)?\]$""")]
[TestCase("1-10", true, false, """Input '1-10' does not match ^\[\d+(-\d+)?\]$""")]
[TestCase("[1-70000]", true, false, "Range is too wide (Count 70000 > 65535)")]
public void Parse(string text, bool isInvalid, bool isEmpty, string? cause) {
var lr = ITextual<LimitedRange>.Parse(text);
AssertLimitedRange(lr, text, isInvalid, isEmpty, cause);
}
//[TestCase("[]", false, true, "")]
//[TestCase("[1]", false, false, "")]
//[TestCase("[1-10]", false, false, "")]
//[TestCase("[10-9]", true, false, "End of range (9) must be greater than the start (10)")]
//[TestCase("[*]", true, false, """Input '[*]' does not match ^\[\d+(-\d+)?\]$""")]
//[TestCase("1-10", true, false, """Input '1-10' does not match ^\[\d+(-\d+)?\]$""")]
//[TestCase("[1-70000]", true, false, "Range is too wide (Count 70000 > 65535)")]
//public void Parse(string text, bool isInvalid, bool isEmpty, string? cause) {
// var lr = LimitedRange.Parse(text);
// AssertLimitedRange(lr, text, isInvalid, isEmpty, cause);
//}

[TestCase("[]", false, true, "")]
[TestCase("[1]", false, false, "")]
Expand All @@ -87,35 +81,14 @@ public void Parse(string text, bool isInvalid, bool isEmpty, string? cause) {
[TestCase("[*]", true, false, """Input '[*]' does not match ^\[\d+(-\d+)?\]$""")]
[TestCase("1-10", false, false, "")]
[TestCase("[1-70000]", true, false, "Range is too wide (Count 70000 > 65535)")]
public void Build(string text, bool isInvalid, bool isEmpty, string? cause) {
var lr = LimitedRange.Build(text);
public void ParseWithProvider(string text, bool isInvalid, bool isEmpty, string? cause) {
var lr = LimitedRange.Parse(text, provider: null);
AssertLimitedRange(lr, text, isInvalid, isEmpty, cause, text[0] != '[');
}

private static void AssertLimitedRange(LimitedRange lr, string text, bool isInvalid, bool isEmpty, string? cause = null, bool unwrapped = false) {
Assert.Multiple(() => {
Assert.That(lr.Textual.IsInvalid, Is.EqualTo(isInvalid), nameof(isInvalid));
Assert.That(lr.IsEmpty, Is.EqualTo(isEmpty), nameof(isEmpty));
});
if (!lr.Textual.IsInvalid && !unwrapped) {
Assert.That(lr.TextualRepresentation, Is.EqualTo(text));
string lrAsString = lr; // implicit string conversion
Assert.That(lrAsString, Is.EqualTo(text));
} else if (!cause.IsBlank())
Assert.That(lr.InvalidityCause, Is.EqualTo(cause).IgnoreCase);
TestContext.WriteLine(lr.ToString());
}

[Test]
public void MemberEmpty() => AssertLimitedRange(LimitedRange.Empty, LimitedRange.Empty.TextualRepresentation, false, true);

[Test]
public void MemberInvalidBy() {
var invalid = LimitedRange.InvalidBy("Test");
AssertLimitedRange(invalid, invalid.TextualRepresentation, isInvalid: true, isEmpty: false);
}


[Test]
public void OneToTen() => AssertLimitedRange(new LimitedRange(1, 10), "[1-10]", isInvalid: false, isEmpty: false);

Expand All @@ -130,4 +103,19 @@ public void WrapAround() => AssertLimitedRange(new LimitedRange(ulong.MaxValue,
cause: "Arithmetic operation resulted in an overflow");


private static void AssertLimitedRange(LimitedRange lr, string text, bool isInvalid, bool isEmpty, string? cause = null, bool unwrapped = false) {
Assert.Multiple(() => {
Assert.That(lr.IsInvalid(), Is.EqualTo(isInvalid), nameof(isInvalid));
Assert.That(lr.IsEmpty, Is.EqualTo(isEmpty), nameof(isEmpty));
});
if (!lr.IsInvalid() && !unwrapped) {
Assert.That(lr.TextualRepresentation, Is.EqualTo(text));
string lrAsString = lr; // implicit string conversion
Assert.That(lrAsString, Is.EqualTo(text));
} else if (!cause.IsBlank())
Assert.That(lr.InvalidityCause, Is.EqualTo(cause).IgnoreCase);
TestContext.WriteLine(lr.ToString());
}


}
24 changes: 21 additions & 3 deletions InterlockLedger.Commons/Extensions/System/StringExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,8 @@ public static ulong AsUlong(this string? s) =>

public static bool IsBlank([NotNullWhen(returnValue: false)] this string? value) =>
string.IsNullOrWhiteSpace(value);
public static bool IsNonBlank([NotNullWhen(returnValue: true)] this string? value) =>
!string.IsNullOrWhiteSpace(value);

public static bool IsEmptyOrMatches(this string? s, params string[] matches) =>
s.IsBlank() || matches.SkipBlanks().Any(m => s.Trim().Equals(m, StringComparison.OrdinalIgnoreCase));
Expand Down Expand Up @@ -108,6 +110,9 @@ public static string Parenthesize(this string s) =>
public static string ParenthesizeIf(this string s, bool condition = false) =>
condition ? $"({s})" : s;

public static T Parse<T>(this string? s) where T : IParsable<T> =>
T.Parse(s.Safe(), provider: null);

public static string? PascalToCamelCase(this string? value) =>
value.RegexReplace(@"(^\w)", ToLowerInvariant);

Expand All @@ -127,7 +132,14 @@ public static string RequiredUsing([NotNull] this string? value, Func<string?, E
value.IsBlank() ? throw exceptor.Required()(name) : value;

public static string Reversed(this string s) =>
s.IsBlank() ? string.Empty : new string(s.ToCharArray().Reverse().ToArray());
s.IsBlank() ? string.Empty : OnlyBmp(s) ? new string(s.ToCharArray().Reverse().ToArray()) : throw new InvalidOperationException("String is not reversible (non-BMP chars)");

private static bool OnlyBmp(string s) {
foreach (var rune in ((ReadOnlySpan<char>)s).EnumerateRunes())
if (!rune.IsBmp)
return false;
return true;
}

public static bool SafeEqualsTo(this string? s, string? other) =>
s is null ? other is null : s.Equals(other, StringComparison.Ordinal);
Expand All @@ -150,8 +162,7 @@ public static string SimplifyAsFileName([NotNull] this string? name) =>
value.RegexReplace("__+", "_").RegexReplace("^_", "");

public static IEnumerable<uint> SplitAsUints(this string? value, char splitChar) =>
value.Safe().Trim().Split(splitChar).Select(s =>
s.AsUint());
value.Safe().Trim().Split(splitChar).Select(s => s.AsUint());

public static TEnum ToEnum<TEnum>(this string? value, TEnum @default = default) where TEnum : struct =>
value.IsBlank() || !Enum.TryParse<TEnum>(value, ignoreCase: true, out var c) ? @default : c;
Expand Down Expand Up @@ -237,3 +248,10 @@ public static IEnumerable<string> SafeSkipBlanks(this IEnumerable<string?>? stri


}

public static class IInvalidableExtensions {

public static bool IsInvalid(this IInvalidable value) => value.InvalidityCause.IsNonBlank();
public static string FullRepresentation(this IInvalidable value) => value.IsInvalid() ? value.TextualRepresentation + Environment.NewLine + value.InvalidityCause! : value.TextualRepresentation;

}
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,7 @@

namespace System;

public static class ITextualOfTSelfExtensions
public interface IInvalidable : ITextualCore
{
public static TSelf ParseAs<TSelf>(this string? textualRepresentation) where TSelf : ITextual<TSelf> =>
ITextual<TSelf>.Parse(textualRepresentation);

public static bool TryParse<TSelf>([NotNullWhen(true)] this string? textualRepresentation, [MaybeNullWhen(false)] out TSelf result) where TSelf : ITextual<TSelf> =>
!(result = ParseAs<TSelf>(textualRepresentation)).IsInvalid;
string? InvalidityCause { get; }
}
16 changes: 6 additions & 10 deletions InterlockLedger.Commons/Interfaces/System/ITextual.cs
Original file line number Diff line number Diff line change
Expand Up @@ -32,35 +32,31 @@

namespace System;

public interface ITextual
#pragma warning disable CA1000 // Do not declare static members on generic types
public interface IEmptyable<TSelf>
{
bool IsEmpty { get; }
string TextualRepresentation { get; }
string? InvalidityCause { get; }
public static abstract TSelf Empty { get; }

bool IsInvalid => !InvalidityCause.IsBlank();
string FullRepresentation => IsInvalid ? TextualRepresentation + Environment.NewLine + InvalidityCause! : TextualRepresentation;
}

#pragma warning disable CA1000 // Do not declare static members on generic types
public interface ITextual<TSelf> : ITextual, IEquatable<TSelf> where TSelf : ITextual<TSelf>
public interface ITextual<TSelf> : IInvalidable, IEquatable<TSelf>, IParsable<TSelf>, IEmptyable<TSelf> where TSelf : ITextual<TSelf>
{
public ITextual<TSelf> Textual { get; }
public static abstract TSelf Empty { get; }
public static abstract Regex Mask { get; }
public static abstract TSelf InvalidBy(string cause);
public static abstract TSelf Build(string textualRepresentation);
public static TSelf Parse(string? textualRepresentation) =>
textualRepresentation.IsBlank() || textualRepresentation.SafeEqualsTo(TSelf.Empty.TextualRepresentation)
? TSelf.Empty
: TSelf.Mask.IsMatch(textualRepresentation)
? TSelf.Build(textualRepresentation!)
? TSelf.Parse(textualRepresentation!, CultureInfo.InvariantCulture)
: TSelf.InvalidBy($"Input '{textualRepresentation}' does not match {TSelf.Mask}");

public static string? Validate(string? textualRepresentation) {
if (textualRepresentation.IsBlank())
return "Missing value";
var resolved = Parse(textualRepresentation);
return resolved.IsInvalid ? resolved.InvalidityCause : null;
return resolved.IsInvalid() ? resolved.InvalidityCause : null;
}
}
39 changes: 39 additions & 0 deletions InterlockLedger.Commons/Interfaces/System/ITextualCore.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// ******************************************************************************************************************************
//
// Copyright (c) 2018-2023 InterlockLedger Network
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met
//
// * Redistributions of source code must retain the above copyright notice, this
// list of conditions and the following disclaimer.
//
// * Redistributions in binary form must reproduce the above copyright notice,
// this list of conditions and the following disclaimer in the documentation
// and/or other materials provided with the distribution.
//
// * Neither the name of the copyright holder nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES, LOSS OF USE, DATA, OR PROFITS, OR BUSINESS INTERRUPTION) HOWEVER
// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//
// ******************************************************************************************************************************

namespace System;

public interface ITextualCore
{
string TextualRepresentation { get; }

}
37 changes: 37 additions & 0 deletions InterlockLedger.Commons/Interfaces/System/ITextualLight.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// ******************************************************************************************************************************
//
// Copyright (c) 2018-2023 InterlockLedger Network
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met
//
// * Redistributions of source code must retain the above copyright notice, this
// list of conditions and the following disclaimer.
//
// * Redistributions in binary form must reproduce the above copyright notice,
// this list of conditions and the following disclaimer in the documentation
// and/or other materials provided with the distribution.
//
// * Neither the name of the copyright holder nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES, LOSS OF USE, DATA, OR PROFITS, OR BUSINESS INTERRUPTION) HOWEVER
// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//
// ******************************************************************************************************************************

namespace System;

public interface ITextualLight<T> : ITextualCore, IEquatable<T>, IParsable<T> where T : notnull, IParsable<T>
{
}
4 changes: 2 additions & 2 deletions InterlockLedger.Commons/InterlockLedger.Commons.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@
<Product>InterlockLedger</Product>
<RepositoryType>git</RepositoryType>
<RepositoryUrl>https://github.com/interlockledger/interlockledger-commons.git</RepositoryUrl>
<Version>17.0.2</Version>
<Version>18.0.0</Version>
<PackageId>InterlockLedger.Commons</PackageId>
<PackageReleaseNotes>Bringing more abstractions from upper projects</PackageReleaseNotes>
<PackageReleaseNotes>IParsable reuse - Lots of BREAKING CHANGES</PackageReleaseNotes>
<PackageIcon>il2.png</PackageIcon>
<EnableNETAnalyzers>true</EnableNETAnalyzers>
<EnforceCodeStyleInBuild>true</EnforceCodeStyleInBuild>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destina
destinationType == typeof(InstanceDescriptor) || destinationType == typeof(string);

public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) =>
value is string text ? text.ParseAs<T>() : base.ConvertFrom(context, culture, value);
value is string text ? T.Parse(text, culture) : base.ConvertFrom(context, culture, value);

public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type? destinationType) =>
destinationType.Required() == typeof(string) && value is T typedValue
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// ******************************************************************************************************************************
//
// Copyright (c) 2018-2023 InterlockLedger Network
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are met
//
// * Redistributions of source code must retain the above copyright notice, this
// list of conditions and the following disclaimer.
//
// * Redistributions in binary form must reproduce the above copyright notice,
// this list of conditions and the following disclaimer in the documentation
// and/or other materials provided with the distribution.
//
// * Neither the name of the copyright holder nor the names of its
// contributors may be used to endorse or promote products derived from
// this software without specific prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES, LOSS OF USE, DATA, OR PROFITS, OR BUSINESS INTERRUPTION) HOWEVER
// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//
// ******************************************************************************************************************************

using System.ComponentModel.Design.Serialization;

namespace System.ComponentModel;

public class TypeNotNullConverter<T> : TypeConverter where T : notnull, ITextualLight<T>
{
public TypeNotNullConverter() { }

public override bool CanConvertFrom(ITypeDescriptorContext? context, Type sourceType) => sourceType == typeof(string);

public override bool CanConvertTo(ITypeDescriptorContext? context, Type? destinationType) =>
destinationType == typeof(InstanceDescriptor) || destinationType == typeof(string);

public override object? ConvertFrom(ITypeDescriptorContext? context, CultureInfo? culture, object value) =>
value is string text ? T.Parse(text, culture) : base.ConvertFrom(context, culture, value);

public override object? ConvertTo(ITypeDescriptorContext? context, CultureInfo? culture, object? value, Type? destinationType) =>
destinationType.Required() == typeof(string) && value is T typedValue
? typedValue.TextualRepresentation
: throw new InvalidOperationException($"Can only convert {typeof(T).Name} to string!!!");
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ public override bool CanConvert(Type typeToConvert) =>
public override T Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) =>
reader.TokenType switch {
JsonTokenType.Null => T.Empty,
JsonTokenType.String => reader.GetString().ParseAs<T>(),
JsonTokenType.String => reader.GetString().Parse<T>(),
_ => throw new NotSupportedException(),
};
#pragma warning restore IDE0072 // Add missing cases
Expand Down
Loading

0 comments on commit fb50234

Please sign in to comment.