From 4c642bec61bd1ed4084123ad0f1b889f8fa4459e Mon Sep 17 00:00:00 2001 From: bmazzarol Date: Wed, 3 Apr 2024 23:07:06 +0800 Subject: [PATCH] feat: ensure refinements are typed --- Tuxedo.Tests/BooleanRefinementTests.cs | 8 +-- Tuxedo.Tests/IntRefinementTests.cs | 24 +++---- Tuxedo.Tests/StringRefinementTests.cs | 55 ++++++++------ Tuxedo/IRefinement.cs | 12 ++-- Tuxedo/Refined.cs | 18 ++--- Tuxedo/Refinements/AbsoluteUri.cs | 28 -------- Tuxedo/Refinements/And.cs | 13 ++-- Tuxedo/Refinements/AnyUri.cs | 28 -------- Tuxedo/Refinements/Empty.cs | 34 --------- Tuxedo/Refinements/Empty/Empty.Collection.cs | 27 +++++++ Tuxedo/Refinements/Empty/Empty.Object.cs | 25 +++++++ .../{Even.cs => Even/Even.Decimal.cs} | 9 ++- Tuxedo/Refinements/Even/Even.Double.cs | 13 ++++ Tuxedo/Refinements/Even/Even.Float.cs | 13 ++++ Tuxedo/Refinements/Even/Even.Int.cs | 12 ++++ Tuxedo/Refinements/Even/Even.Long.cs | 12 ++++ Tuxedo/Refinements/Even/Even.Short.cs | 12 ++++ Tuxedo/Refinements/False.cs | 6 +- Tuxedo/Refinements/MatchesRegex.cs | 18 ++--- Tuxedo/Refinements/NonEmpty.cs | 34 --------- .../NonEmpty/NonEmpty.Collection.cs | 27 +++++++ .../Refinements/NonEmpty/NonEmpty.Object.cs | 35 +++++++++ Tuxedo/Refinements/Not.cs | 9 +-- Tuxedo/Refinements/Positive.cs | 33 --------- .../Refinements/Positive/Positive.Decimal.cs | 12 ++++ .../Refinements/Positive/Positive.Double.cs | 12 ++++ Tuxedo/Refinements/Positive/Positive.Float.cs | 12 ++++ Tuxedo/Refinements/Positive/Positive.Int.cs | 12 ++++ Tuxedo/Refinements/Positive/Positive.Long.cs | 12 ++++ Tuxedo/Refinements/Positive/Positive.Short.cs | 12 ++++ Tuxedo/Refinements/RelativeUri.cs | 28 -------- Tuxedo/Refinements/Size.cs | 18 ++--- Tuxedo/Refinements/StartsWith.cs | 8 +-- Tuxedo/Refinements/Trimmed.cs | 38 +++++----- Tuxedo/Refinements/True.cs | 6 +- Tuxedo/Refinements/Uri.cs | 72 +++++++++++++++++++ Tuxedo/Refinements/Uuid.cs | 10 +-- 37 files changed, 452 insertions(+), 305 deletions(-) delete mode 100644 Tuxedo/Refinements/AbsoluteUri.cs delete mode 100644 Tuxedo/Refinements/AnyUri.cs delete mode 100644 Tuxedo/Refinements/Empty.cs create mode 100644 Tuxedo/Refinements/Empty/Empty.Collection.cs create mode 100644 Tuxedo/Refinements/Empty/Empty.Object.cs rename Tuxedo/Refinements/{Even.cs => Even/Even.Decimal.cs} (82%) create mode 100644 Tuxedo/Refinements/Even/Even.Double.cs create mode 100644 Tuxedo/Refinements/Even/Even.Float.cs create mode 100644 Tuxedo/Refinements/Even/Even.Int.cs create mode 100644 Tuxedo/Refinements/Even/Even.Long.cs create mode 100644 Tuxedo/Refinements/Even/Even.Short.cs delete mode 100644 Tuxedo/Refinements/NonEmpty.cs create mode 100644 Tuxedo/Refinements/NonEmpty/NonEmpty.Collection.cs create mode 100644 Tuxedo/Refinements/NonEmpty/NonEmpty.Object.cs delete mode 100644 Tuxedo/Refinements/Positive.cs create mode 100644 Tuxedo/Refinements/Positive/Positive.Decimal.cs create mode 100644 Tuxedo/Refinements/Positive/Positive.Double.cs create mode 100644 Tuxedo/Refinements/Positive/Positive.Float.cs create mode 100644 Tuxedo/Refinements/Positive/Positive.Int.cs create mode 100644 Tuxedo/Refinements/Positive/Positive.Long.cs create mode 100644 Tuxedo/Refinements/Positive/Positive.Short.cs delete mode 100644 Tuxedo/Refinements/RelativeUri.cs create mode 100644 Tuxedo/Refinements/Uri.cs diff --git a/Tuxedo.Tests/BooleanRefinementTests.cs b/Tuxedo.Tests/BooleanRefinementTests.cs index 5963dbd..a18443b 100644 --- a/Tuxedo.Tests/BooleanRefinementTests.cs +++ b/Tuxedo.Tests/BooleanRefinementTests.cs @@ -67,21 +67,21 @@ public static void Case6() [Fact(DisplayName = "A refinement can be inverted")] public static void Case7() { - Refined> refined = false; + Refined> refined = false; (refined is { Value: false }).Should().BeTrue(); - Refined.TryRefine>(false, out _).Should().BeTrue(); + Refined.TryRefine>(false, out _).Should().BeTrue(); } [Fact(DisplayName = "An inverted refinement fail")] public static void Case8() { - Refined.TryRefine>(true, out _).Should().BeFalse(); + Refined.TryRefine>(true, out _).Should().BeFalse(); } [Fact(DisplayName = "An inverted refinement fail and throws")] public static void Case9() { - var act = () => (Refined>)true; + var act = () => (Refined>)true; act.Should() .Throw() .WithMessage("Not: Value must be true") diff --git a/Tuxedo.Tests/IntRefinementTests.cs b/Tuxedo.Tests/IntRefinementTests.cs index b035090..170fa62 100644 --- a/Tuxedo.Tests/IntRefinementTests.cs +++ b/Tuxedo.Tests/IntRefinementTests.cs @@ -78,21 +78,21 @@ public static void Case8() [Fact(DisplayName = "Using Not odd integers can be refined with even")] public static void Case9() { - Refined> refined = 1; + Refined> refined = 1; refined.Value.Should().Be(1); - Refined.TryRefine>(1, out _).Should().BeTrue(); + Refined.TryRefine>(1, out _).Should().BeTrue(); } [Fact(DisplayName = "Using Not even integers cannot be refined with even")] public static void Case10() { - Refined.TryRefine>(2, out _).Should().BeFalse(); + Refined.TryRefine>(2, out _).Should().BeFalse(); } [Fact(DisplayName = "Using Not even integers cannot be refined with even and throws")] public static void Case11() { - var act = () => Refined.Refine>(2); + var act = () => Refined.Refine>(2); act.Should() .Throw() .WithMessage("Not: Value must be an even number, but found 2") @@ -103,28 +103,22 @@ public static void Case11() [Fact(DisplayName = "A non default integer can be refined")] public static void Case12() { - Refined refined = 1; + Refined> refined = 1; refined.Value.Should().Be(1); - Refined.TryRefine(1, out _).Should().BeTrue(); + Refined.TryRefine>(1, out _).Should().BeTrue(); } [Fact(DisplayName = "A default integer cannot be refined")] public static void Case13() { - Refined.TryRefine(default, out _).Should().BeFalse(); + Refined.TryRefine>(default, out _).Should().BeFalse(); } [Fact(DisplayName = "A default integer can be refined with empty")] public static void Case14() { - Refined refined = default; + Refined> refined = default(int); refined.Value.Should().Be(default); - Refined.TryRefine(default, out _).Should().BeTrue(); - } - - [Fact(DisplayName = "A int cannot be refined by size")] - public static void Case15() - { - Refined.TryRefine>(1, out _).Should().BeFalse(); + Refined.TryRefine>(default, out _).Should().BeTrue(); } } diff --git a/Tuxedo.Tests/StringRefinementTests.cs b/Tuxedo.Tests/StringRefinementTests.cs index d8cd1e2..c55e08a 100644 --- a/Tuxedo.Tests/StringRefinementTests.cs +++ b/Tuxedo.Tests/StringRefinementTests.cs @@ -39,9 +39,9 @@ public static void Case4() [Fact(DisplayName = "A non-empty string refinement can be inverted")] public static void Case5() { - Refined> refined = string.Empty; + Refined> refined = string.Empty; refined.Value.Should().BeEmpty(); - Refined.TryRefine>(string.Empty, out _).Should().BeTrue(); + Refined.TryRefine>(string.Empty, out _).Should().BeTrue(); } [Fact(DisplayName = "A trimmed string can be refined")] @@ -77,7 +77,7 @@ public static void Case9() var act = () => (Refined)" Hello, World! "; act.Should() .Throw() - .WithMessage("Value must have no leading or trailing whitespace") + .WithMessage("Value must be trimmed, but found ' Hello, World! '") .And.Value.Should() .Be(" Hello, World! "); } @@ -85,19 +85,22 @@ public static void Case9() [Fact(DisplayName = "A refinement can be combined with logical AND")] public static void Case10() { - Refined> refined = "Hello, World!"; + Refined> refined = "Hello, World!"; ((string)refined).Should().Be("Hello, World!"); - Refined.TryRefine>("Hello, World!", out _).Should().BeTrue(); + Refined + .TryRefine>("Hello, World!", out _) + .Should() + .BeTrue(); } [Fact(DisplayName = "A refinement can be combined with logical AND and throws")] public static void Case11() { - var act = () => (Refined>)" Hello, World! "; + var act = () => (Refined>)" Hello, World! "; act.Should() .Throw() .WithMessage( - "Value cannot be empty and Value must have no leading or trailing whitespace" + "Value cannot be empty and Value must be trimmed, but found ' Hello, World! '" ) .And.Value.Should() .Be(" Hello, World! "); @@ -130,18 +133,23 @@ public static void Case13() [Fact(DisplayName = "A absolute uri can be refined")] public static void Case14() { - Refined refined = "https://www.bmazzarol.com.au"; + Refined> refined = "https://www.bmazzarol.com.au"; ((string)refined).Should().Be("https://www.bmazzarol.com.au"); Refined - .TryRefine("https://www.bmazzarol.com.au", out _) + .TryRefine>("https://www.bmazzarol.com.au", out _) .Should() .BeTrue(); - Refined refined2 = "http://www.bmazzarol.com.au"; + Refined> refined2 = "http://www.bmazzarol.com.au"; ((string)refined2).Should().Be("http://www.bmazzarol.com.au"); - ((Uri)refined2).Should().Be(new Uri("http://www.bmazzarol.com.au", UriKind.Absolute)); + ((System.Uri)refined2) + .Should() + .Be(new System.Uri("http://www.bmazzarol.com.au", UriKind.Absolute)); Refined - .TryRefine("http://www.bmazzarol.com.au", out _) + .TryRefine>( + "http://www.bmazzarol.com.au", + out _ + ) .Should() .BeTrue(); } @@ -149,30 +157,33 @@ public static void Case14() [Fact(DisplayName = "A absolute uri cannot be refined and throws")] public static void Case15() { - var act = () => (Refined)"/some/path"; + var act = () => (Refined>)"/some/path"; act.Should() .Throw() - .WithMessage("Value must be a valid absolute URI") + .WithMessage("Value must be a valid URI") .And.Value.Should() .Be("/some/path"); - Refined.TryRefine("/some/path", out _).Should().BeFalse(); + Refined + .TryRefine>("/some/path", out _) + .Should() + .BeFalse(); } [Fact(DisplayName = "A relative uri can be refined")] public static void Case16() { - Refined refined = "/some/path"; + Refined> refined = "/some/path"; ((string)refined).Should().Be("/some/path"); - Refined.TryRefine("/some/path", out _).Should().BeTrue(); + Refined.TryRefine>("/some/path", out _).Should().BeTrue(); } [Fact(DisplayName = "A relative uri cannot be refined and throws")] public static void Case17() { - var act = () => (Refined)"https://www.bmazzarol.com.au"; + var act = () => (Refined>)"https://www.bmazzarol.com.au"; act.Should() .Throw() - .WithMessage("Value must be a valid relative URI") + .WithMessage("Value must be a valid URI") .And.Value.Should() .Be("https://www.bmazzarol.com.au"); } @@ -182,16 +193,16 @@ public static void Case17() [InlineData("https://www.bmazzarol.com.au")] public static void Case18(string uri) { - Refined refined = uri; + Refined refined = uri; ((string)refined).Should().Be(uri); - Refined.TryRefine(uri, out _).Should().BeTrue(); + Refined.TryRefine(uri, out _).Should().BeTrue(); } [Fact(DisplayName = "A non uri cannot be refined and throws")] public static void Case19() { // invalid uri characters - var act = () => (Refined)null!; + var act = () => (Refined)null!; act.Should() .Throw() .WithMessage("Value must be a valid URI") diff --git a/Tuxedo/IRefinement.cs b/Tuxedo/IRefinement.cs index 7c45a42..8d0a6b8 100644 --- a/Tuxedo/IRefinement.cs +++ b/Tuxedo/IRefinement.cs @@ -9,7 +9,8 @@ namespace Tuxedo; /// A refined type is a type that is a subset of another type, limited by some predicate. /// /// the refinement instance; must be a struct type to enable lookup -public interface IRefinement +/// type to refine +public interface IRefinement where TThis : struct { /// @@ -17,22 +18,23 @@ public interface IRefinement /// /// value to test for refinement /// true if the value can be refined; otherwise, false - bool CanBeRefined(T value); + bool CanBeRefined(T value); /// /// Builds a failure message for the given value when it cannot be refined. /// /// value that cannot be refined /// failure message - string BuildFailureMessage(T value); + string BuildFailureMessage(T value); } /// /// Defines that a contract can be used to refine some type T, with a refined result. /// /// the refinement instance; must be a struct type to enable lookup +/// input type to refine /// result of the refinement -public interface IRefinementResult : IRefinement +public interface IRefinement : IRefinement where TThis : struct { /// @@ -41,5 +43,5 @@ public interface IRefinementResult : IRefinement /// value to test for refinement /// refined value /// true if the value can be refined; otherwise, false - bool TryRefine(TIn value, [NotNullWhen(true)] out TOut? refinedValue); + bool TryRefine(TIn value, [NotNullWhen(true)] out TOut? refinedValue); } diff --git a/Tuxedo/Refined.cs b/Tuxedo/Refined.cs index c219428..147c515 100644 --- a/Tuxedo/Refined.cs +++ b/Tuxedo/Refined.cs @@ -4,12 +4,12 @@ namespace Tuxedo; /// -/// Represents a refined type, the refinement is enforced by the TRefinement type which is an implementation of +/// Represents a refined type, the refinement is enforced by the TRefinement type which is an implementation of /// /// refined type /// refinement on the type public readonly record struct Refined - where TRefinement : struct, IRefinement + where TRefinement : struct, IRefinement { /// /// The underlying value of the refined type @@ -48,14 +48,14 @@ public void Deconstruct(out T value) } /// -/// Represents a refined type, the refinement is enforced by the TRefinement type which is an implementation of +/// Represents a refined type, the refinement is enforced by the TRefinement type which is an implementation of /// /// raw refined type /// refined type /// refinement on the type [StructLayout(LayoutKind.Auto)] public readonly record struct Refined - where TRefinement : struct, IRefinementResult + where TRefinement : struct, IRefinement { /// /// The underlying value of the refined type @@ -124,7 +124,7 @@ public static class Refined /// refinement applied to the type /// true if the value was refined; otherwise, false public static bool TryRefine(T value, out Refined refined) - where TRefinement : struct, IRefinement + where TRefinement : struct, IRefinement { var refinement = default(TRefinement); if (!refinement.CanBeRefined(value)) @@ -150,7 +150,7 @@ public static bool TryRefine( TRaw value, out Refined refined ) - where TRefinement : struct, IRefinementResult + where TRefinement : struct, IRefinement { var refinement = default(TRefinement); if (!refinement.TryRefine(value, out var refinedValue)) @@ -167,7 +167,7 @@ out Refined refined [SuppressMessage("Design", "MA0026:Fix TODO comment")] [SuppressMessage("Info Code Smell", "S1135:Track uses of \"TODO\" tags")] private static void Throw(T value, TRefinement refinement) - where TRefinement : struct, IRefinement + where TRefinement : struct, IRefinement { // TODO: find a way to rewind the stack trace to the caller throw new RefinementFailureException(value, refinement.BuildFailureMessage(value)); @@ -182,7 +182,7 @@ private static void Throw(T value, TRefinement refinement) /// refined value /// thrown if the value cannot be refined public static Refined Refine(T value) - where TRefinement : struct, IRefinement + where TRefinement : struct, IRefinement { var refinement = default(TRefinement); if (refinement.CanBeRefined(value)) @@ -206,7 +206,7 @@ public static Refined Refine(T value) public static Refined Refine( TRaw value ) - where TRefinement : struct, IRefinementResult + where TRefinement : struct, IRefinement { var refinement = default(TRefinement); if (refinement.TryRefine(value, out var refinedValue)) diff --git a/Tuxedo/Refinements/AbsoluteUri.cs b/Tuxedo/Refinements/AbsoluteUri.cs deleted file mode 100644 index 6954197..0000000 --- a/Tuxedo/Refinements/AbsoluteUri.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Diagnostics.CodeAnalysis; - -namespace Tuxedo; - -/// -/// Enforces that a string value is a valid absolute -/// -public readonly struct AbsoluteUri : IRefinementResult -{ - /// - public bool CanBeRefined(T value) => TryRefine(value, out _); - - /// - public bool TryRefine(TIn value, [NotNullWhen(true)] out Uri? refinedValue) - { - if (value is string s && Uri.TryCreate(s, UriKind.Absolute, out var uri)) - { - refinedValue = uri; - return true; - } - - refinedValue = null; - return false; - } - - /// - public string BuildFailureMessage(T value) => "Value must be a valid absolute URI"; -} diff --git a/Tuxedo/Refinements/And.cs b/Tuxedo/Refinements/And.cs index efb9120..6507298 100644 --- a/Tuxedo/Refinements/And.cs +++ b/Tuxedo/Refinements/And.cs @@ -5,13 +5,14 @@ /// /// first refinement /// second refinement -public readonly struct And - : IRefinement> - where TFirstRefinement : struct, IRefinement - where TSecondRefinement : struct, IRefinement +/// type of value +public readonly struct And + : IRefinement, T> + where TFirstRefinement : struct, IRefinement + where TSecondRefinement : struct, IRefinement { /// - public bool CanBeRefined(T value) + public bool CanBeRefined(T value) { var firstRefinement = default(TFirstRefinement); var secondRefinement = default(TSecondRefinement); @@ -19,7 +20,7 @@ public bool CanBeRefined(T value) } /// - public string BuildFailureMessage(T value) + public string BuildFailureMessage(T value) { var firstRefinement = default(TFirstRefinement); var secondRefinement = default(TSecondRefinement); diff --git a/Tuxedo/Refinements/AnyUri.cs b/Tuxedo/Refinements/AnyUri.cs deleted file mode 100644 index 8287ac7..0000000 --- a/Tuxedo/Refinements/AnyUri.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Diagnostics.CodeAnalysis; - -namespace Tuxedo; - -/// -/// Enforces that a string value is a valid -/// -public readonly struct AnyUri : IRefinementResult -{ - /// - public bool CanBeRefined(T value) => TryRefine(value, out _); - - /// - public bool TryRefine(T value, [NotNullWhen(true)] out Uri? refinedValue) - { - if (value is string s && Uri.TryCreate(s, UriKind.RelativeOrAbsolute, out var uri)) - { - refinedValue = uri; - return true; - } - - refinedValue = null; - return false; - } - - /// - public string BuildFailureMessage(T value) => "Value must be a valid URI"; -} diff --git a/Tuxedo/Refinements/Empty.cs b/Tuxedo/Refinements/Empty.cs deleted file mode 100644 index aaa4042..0000000 --- a/Tuxedo/Refinements/Empty.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System.Collections; - -namespace Tuxedo; - -/// -/// Enforces that a value is empty -/// -public readonly struct Empty : IRefinement -{ - /// - public bool CanBeRefined(T value) - { - switch (value) - { - case null: - return true; - case string text: - return string.IsNullOrEmpty(text); - case ICollection collection: - return collection.Count == 0; - case IEnumerable enumerable: - { - var enumerator = enumerable.GetEnumerator(); - using var disposable = enumerator as IDisposable; - return !enumerator.MoveNext(); - } - default: - return EqualityComparer.Default.Equals(value, default!); - } - } - - /// - public string BuildFailureMessage(T value) => "Value must be empty"; -} diff --git a/Tuxedo/Refinements/Empty/Empty.Collection.cs b/Tuxedo/Refinements/Empty/Empty.Collection.cs new file mode 100644 index 0000000..5a9ff0d --- /dev/null +++ b/Tuxedo/Refinements/Empty/Empty.Collection.cs @@ -0,0 +1,27 @@ +using System.Collections; + +namespace Tuxedo; + +/// +/// Enforces that a value is empty +/// +public readonly struct Empty : IRefinement +{ + bool IRefinement.CanBeRefined(IEnumerable? value) + { + switch (value) + { + case null: + return true; + case ICollection collection: + return collection.Count == 0; + } + + var enumerator = value.GetEnumerator(); + using var disposable = enumerator as IDisposable; + return !enumerator.MoveNext(); + } + + string IRefinement.BuildFailureMessage(IEnumerable value) => + "Value must be empty"; +} diff --git a/Tuxedo/Refinements/Empty/Empty.Object.cs b/Tuxedo/Refinements/Empty/Empty.Object.cs new file mode 100644 index 0000000..f7cb5db --- /dev/null +++ b/Tuxedo/Refinements/Empty/Empty.Object.cs @@ -0,0 +1,25 @@ +namespace Tuxedo; + +/// +/// Enforces that a value is empty +/// +public readonly struct Empty : IRefinement, T> + where TComparer : struct, IConstant> +{ + bool IRefinement, T>.CanBeRefined(T value) => + default(TComparer).Value.Equals(value, default); + + string IRefinement, T>.BuildFailureMessage(T value) => + "Value must be empty"; +} + +/// +/// Enforces that a value is empty +/// +public readonly struct Empty : IRefinement, T> +{ + bool IRefinement, T>.CanBeRefined(T value) => + default(DefaultComparer).Value!.Equals(value, default); + + string IRefinement, T>.BuildFailureMessage(T value) => "Value must be empty"; +} diff --git a/Tuxedo/Refinements/Even.cs b/Tuxedo/Refinements/Even/Even.Decimal.cs similarity index 82% rename from Tuxedo/Refinements/Even.cs rename to Tuxedo/Refinements/Even/Even.Decimal.cs index 2eda9a2..2f53755 100644 --- a/Tuxedo/Refinements/Even.cs +++ b/Tuxedo/Refinements/Even/Even.Decimal.cs @@ -3,9 +3,9 @@ namespace Tuxedo; /// -/// Enforces that an integer value is even +/// Enforces that an numeric value is even /// -public readonly struct Even : IRefinement +public readonly partial struct Even : IRefinement { /// public bool CanBeRefined(T value) => @@ -40,4 +40,9 @@ IEnumerable enumerable => $"Value must have an even count, but found {enumerable.Cast().Count()}", _ => "Value must be even" }; + + bool IRefinement.CanBeRefined(decimal value) => value % 2 == 0; + + string IRefinement.BuildFailureMessage(decimal value) => + $"Value must be an even number, but found {value}"; } diff --git a/Tuxedo/Refinements/Even/Even.Double.cs b/Tuxedo/Refinements/Even/Even.Double.cs new file mode 100644 index 0000000..faeecc2 --- /dev/null +++ b/Tuxedo/Refinements/Even/Even.Double.cs @@ -0,0 +1,13 @@ +namespace Tuxedo; + +/// +/// Enforces that an numeric value is even +/// +public readonly partial struct Even : IRefinement +{ + bool IRefinement.CanBeRefined(double value) => + Math.Abs(value % 2) - 0 < double.Epsilon; + + string IRefinement.BuildFailureMessage(double value) => + $"Value must be an even number, but found {value}"; +} diff --git a/Tuxedo/Refinements/Even/Even.Float.cs b/Tuxedo/Refinements/Even/Even.Float.cs new file mode 100644 index 0000000..d775d9d --- /dev/null +++ b/Tuxedo/Refinements/Even/Even.Float.cs @@ -0,0 +1,13 @@ +namespace Tuxedo; + +/// +/// Enforces that an numeric value is even +/// +public readonly partial struct Even : IRefinement +{ + bool IRefinement.CanBeRefined(float value) => + Math.Abs(value % 2) - 0 < float.Epsilon; + + string IRefinement.BuildFailureMessage(float value) => + $"Value must be an even number, but found {value}"; +} diff --git a/Tuxedo/Refinements/Even/Even.Int.cs b/Tuxedo/Refinements/Even/Even.Int.cs new file mode 100644 index 0000000..db2b7a9 --- /dev/null +++ b/Tuxedo/Refinements/Even/Even.Int.cs @@ -0,0 +1,12 @@ +namespace Tuxedo; + +/// +/// Enforces that an numeric value is even +/// +public readonly partial struct Even : IRefinement +{ + bool IRefinement.CanBeRefined(int value) => value % 2 == 0; + + string IRefinement.BuildFailureMessage(int value) => + $"Value must be an even number, but found {value}"; +} diff --git a/Tuxedo/Refinements/Even/Even.Long.cs b/Tuxedo/Refinements/Even/Even.Long.cs new file mode 100644 index 0000000..2d50446 --- /dev/null +++ b/Tuxedo/Refinements/Even/Even.Long.cs @@ -0,0 +1,12 @@ +namespace Tuxedo; + +/// +/// Enforces that an numeric value is even +/// +public readonly partial struct Even : IRefinement +{ + bool IRefinement.CanBeRefined(long value) => value % 2 == 0; + + string IRefinement.BuildFailureMessage(long value) => + $"Value must be an even number, but found {value}"; +} diff --git a/Tuxedo/Refinements/Even/Even.Short.cs b/Tuxedo/Refinements/Even/Even.Short.cs new file mode 100644 index 0000000..4eb02ff --- /dev/null +++ b/Tuxedo/Refinements/Even/Even.Short.cs @@ -0,0 +1,12 @@ +namespace Tuxedo; + +/// +/// Enforces that an numeric value is even +/// +public readonly partial struct Even : IRefinement +{ + bool IRefinement.CanBeRefined(short value) => value % 2 == 0; + + string IRefinement.BuildFailureMessage(short value) => + $"Value must be an even number, but found {value}"; +} diff --git a/Tuxedo/Refinements/False.cs b/Tuxedo/Refinements/False.cs index 574d044..50d1c37 100644 --- a/Tuxedo/Refinements/False.cs +++ b/Tuxedo/Refinements/False.cs @@ -3,11 +3,11 @@ /// /// Enforces that a boolean value is false /// -public readonly struct False : IRefinement +public readonly struct False : IRefinement { /// - public bool CanBeRefined(T value) => value is false; + public bool CanBeRefined(bool value) => !value; /// - public string BuildFailureMessage(T value) => "Value must be false"; + public string BuildFailureMessage(bool value) => "Value must be false"; } diff --git a/Tuxedo/Refinements/MatchesRegex.cs b/Tuxedo/Refinements/MatchesRegex.cs index 4b2e630..121e942 100644 --- a/Tuxedo/Refinements/MatchesRegex.cs +++ b/Tuxedo/Refinements/MatchesRegex.cs @@ -7,23 +7,17 @@ namespace Tuxedo; /// /// the regular expression to match public readonly struct MatchesRegex - : IRefinementResult, MatchCollection> + : IRefinement, string, MatchCollection> where TRegex : struct, IConstant { /// - public bool CanBeRefined(T value) => value is string s && TryRefine(s, out _); + public bool CanBeRefined(string value) => TryRefine(value, out _); /// - public bool TryRefine(T value, out MatchCollection refinedValue) + public bool TryRefine(string value, out MatchCollection refinedValue) { - if (value is not string s) - { - refinedValue = default!; - return false; - } - refinedValue = Regex.Matches( - s, + value, default(TRegex).Value, RegexOptions.None, TimeSpan.FromSeconds(30) @@ -31,7 +25,7 @@ public bool TryRefine(T value, out MatchCollection refinedValue) return refinedValue.Count > 0; } - /// - public string BuildFailureMessage(T value) => + /// + public string BuildFailureMessage(string value) => $"Value must match the regular expression '{default(TRegex).Value}'"; } diff --git a/Tuxedo/Refinements/NonEmpty.cs b/Tuxedo/Refinements/NonEmpty.cs deleted file mode 100644 index 0e4de00..0000000 --- a/Tuxedo/Refinements/NonEmpty.cs +++ /dev/null @@ -1,34 +0,0 @@ -using System.Collections; - -namespace Tuxedo; - -/// -/// Enforces that a value is not empty -/// -public readonly struct NonEmpty : IRefinement -{ - /// - public bool CanBeRefined(T value) - { - switch (value) - { - case null: - return false; - case string text: - return text.Length > 0; - case ICollection collection: - return collection.Count > 0; - case IEnumerable enumerable: - { - var enumerator = enumerable.GetEnumerator(); - using var disposable = enumerator as IDisposable; - return enumerator.MoveNext(); - } - default: - return !EqualityComparer.Default.Equals(value, default!); - } - } - - /// - public string BuildFailureMessage(T value) => "Value cannot be empty"; -} diff --git a/Tuxedo/Refinements/NonEmpty/NonEmpty.Collection.cs b/Tuxedo/Refinements/NonEmpty/NonEmpty.Collection.cs new file mode 100644 index 0000000..6acd2da --- /dev/null +++ b/Tuxedo/Refinements/NonEmpty/NonEmpty.Collection.cs @@ -0,0 +1,27 @@ +using System.Collections; + +namespace Tuxedo; + +/// +/// Enforces that a value is not empty +/// +public readonly struct NonEmpty : IRefinement +{ + bool IRefinement.CanBeRefined(IEnumerable value) + { + switch (value) + { + case null: + return false; + case ICollection collection: + return collection.Count > 0; + } + + var enumerator = value.GetEnumerator(); + using var disposable = enumerator as IDisposable; + return enumerator.MoveNext(); + } + + string IRefinement.BuildFailureMessage(IEnumerable value) => + "Value cannot be empty"; +} diff --git a/Tuxedo/Refinements/NonEmpty/NonEmpty.Object.cs b/Tuxedo/Refinements/NonEmpty/NonEmpty.Object.cs new file mode 100644 index 0000000..1500008 --- /dev/null +++ b/Tuxedo/Refinements/NonEmpty/NonEmpty.Object.cs @@ -0,0 +1,35 @@ +namespace Tuxedo; + +/// +/// Enforces that a value is not empty +/// +public readonly struct NonEmpty : IRefinement, T> + where TComparer : struct, IConstant> +{ + bool IRefinement, T>.CanBeRefined(T value) => + !default(TComparer).Value.Equals(value, default); + + string IRefinement, T>.BuildFailureMessage(T value) => + "Value cannot be empty"; +} + +/// +/// Enforces that a value is not empty +/// +public readonly struct NonEmpty : IRefinement, T> +{ + bool IRefinement, T>.CanBeRefined(T value) => + !default(DefaultComparer).Value!.Equals(value, default); + + string IRefinement, T>.BuildFailureMessage(T value) => "Value cannot be empty"; +} + +/// +/// Default comparer for a type +/// +/// type +public readonly struct DefaultComparer : IConstant, IEqualityComparer> +{ + /// + public IEqualityComparer Value => EqualityComparer.Default; +} diff --git a/Tuxedo/Refinements/Not.cs b/Tuxedo/Refinements/Not.cs index 0a83373..ec57ead 100644 --- a/Tuxedo/Refinements/Not.cs +++ b/Tuxedo/Refinements/Not.cs @@ -4,13 +4,14 @@ /// Inverts a refinement /// /// refinement to invert -public readonly struct Not : IRefinement> - where TRefinement : struct, IRefinement +/// type of value +public readonly struct Not : IRefinement, T> + where TRefinement : struct, IRefinement { /// - public bool CanBeRefined(T value) => !default(TRefinement).CanBeRefined(value); + public bool CanBeRefined(T value) => !default(TRefinement).CanBeRefined(value); /// - public string BuildFailureMessage(T value) => + public string BuildFailureMessage(T value) => $"Not: {default(TRefinement).BuildFailureMessage(value)}"; } diff --git a/Tuxedo/Refinements/Positive.cs b/Tuxedo/Refinements/Positive.cs deleted file mode 100644 index 3296379..0000000 --- a/Tuxedo/Refinements/Positive.cs +++ /dev/null @@ -1,33 +0,0 @@ -namespace Tuxedo; - -/// -/// Ensures that an integer value is positive -/// -public readonly struct Positive : IRefinement -{ - /// - public bool CanBeRefined(T value) => - value switch - { - short s => s > 0, - int i => i > 0, - long l => l > 0, - float f => f > 0, - double d => d > 0, - decimal d => d > 0, - _ => false - }; - - /// - public string BuildFailureMessage(T value) => - value switch - { - short s => $"Value must be positive, but found {s}", - int i => $"Value must be positive, but found {i}", - long l => $"Value must be positive, but found {l}", - float f => $"Value must be positive, but found {f}", - double d => $"Value must be positive, but found {d}", - decimal d => $"Value must be positive, but found {d}", - _ => "Value must be positive" - }; -} diff --git a/Tuxedo/Refinements/Positive/Positive.Decimal.cs b/Tuxedo/Refinements/Positive/Positive.Decimal.cs new file mode 100644 index 0000000..9fa99fc --- /dev/null +++ b/Tuxedo/Refinements/Positive/Positive.Decimal.cs @@ -0,0 +1,12 @@ +namespace Tuxedo; + +/// +/// Ensures that an numeric value is positive +/// +public readonly partial struct Positive : IRefinement +{ + bool IRefinement.CanBeRefined(decimal value) => value > 0; + + string IRefinement.BuildFailureMessage(decimal value) => + $"Value must be positive, but found {value}"; +} diff --git a/Tuxedo/Refinements/Positive/Positive.Double.cs b/Tuxedo/Refinements/Positive/Positive.Double.cs new file mode 100644 index 0000000..605fdad --- /dev/null +++ b/Tuxedo/Refinements/Positive/Positive.Double.cs @@ -0,0 +1,12 @@ +namespace Tuxedo; + +/// +/// Ensures that an numeric value is positive +/// +public readonly partial struct Positive : IRefinement +{ + bool IRefinement.CanBeRefined(double value) => value > 0; + + string IRefinement.BuildFailureMessage(double value) => + $"Value must be positive, but found {value}"; +} diff --git a/Tuxedo/Refinements/Positive/Positive.Float.cs b/Tuxedo/Refinements/Positive/Positive.Float.cs new file mode 100644 index 0000000..e6dc05e --- /dev/null +++ b/Tuxedo/Refinements/Positive/Positive.Float.cs @@ -0,0 +1,12 @@ +namespace Tuxedo; + +/// +/// Ensures that an numeric value is positive +/// +public readonly partial struct Positive : IRefinement +{ + bool IRefinement.CanBeRefined(float value) => value > 0; + + string IRefinement.BuildFailureMessage(float value) => + $"Value must be positive, but found {value}"; +} diff --git a/Tuxedo/Refinements/Positive/Positive.Int.cs b/Tuxedo/Refinements/Positive/Positive.Int.cs new file mode 100644 index 0000000..1136d7b --- /dev/null +++ b/Tuxedo/Refinements/Positive/Positive.Int.cs @@ -0,0 +1,12 @@ +namespace Tuxedo; + +/// +/// Ensures that an numeric value is positive +/// +public readonly partial struct Positive : IRefinement +{ + bool IRefinement.CanBeRefined(int value) => value > 0; + + string IRefinement.BuildFailureMessage(int value) => + $"Value must be positive, but found {value}"; +} diff --git a/Tuxedo/Refinements/Positive/Positive.Long.cs b/Tuxedo/Refinements/Positive/Positive.Long.cs new file mode 100644 index 0000000..6de619f --- /dev/null +++ b/Tuxedo/Refinements/Positive/Positive.Long.cs @@ -0,0 +1,12 @@ +namespace Tuxedo; + +/// +/// Ensures that an numeric value is positive +/// +public readonly partial struct Positive : IRefinement +{ + bool IRefinement.CanBeRefined(long value) => value > 0; + + string IRefinement.BuildFailureMessage(long value) => + $"Value must be positive, but found {value}"; +} diff --git a/Tuxedo/Refinements/Positive/Positive.Short.cs b/Tuxedo/Refinements/Positive/Positive.Short.cs new file mode 100644 index 0000000..22de534 --- /dev/null +++ b/Tuxedo/Refinements/Positive/Positive.Short.cs @@ -0,0 +1,12 @@ +namespace Tuxedo; + +/// +/// Ensures that an numeric value is positive +/// +public readonly partial struct Positive : IRefinement +{ + bool IRefinement.CanBeRefined(short value) => value > 0; + + string IRefinement.BuildFailureMessage(short value) => + $"Value must be positive, but found {value}"; +} diff --git a/Tuxedo/Refinements/RelativeUri.cs b/Tuxedo/Refinements/RelativeUri.cs deleted file mode 100644 index dee977a..0000000 --- a/Tuxedo/Refinements/RelativeUri.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Diagnostics.CodeAnalysis; - -namespace Tuxedo; - -/// -/// Enforces that a string value is a valid relative -/// -public readonly struct RelativeUri : IRefinementResult -{ - /// - public bool CanBeRefined(T value) => TryRefine(value, out _); - - /// - public bool TryRefine(TIn value, [NotNullWhen(true)] out Uri? refinedValue) - { - if (value is string s && Uri.TryCreate(s, UriKind.Relative, out var uri)) - { - refinedValue = uri; - return true; - } - - refinedValue = null; - return false; - } - - /// - public string BuildFailureMessage(T value) => "Value must be a valid relative URI"; -} diff --git a/Tuxedo/Refinements/Size.cs b/Tuxedo/Refinements/Size.cs index 5ddd35a..43d661c 100644 --- a/Tuxedo/Refinements/Size.cs +++ b/Tuxedo/Refinements/Size.cs @@ -6,27 +6,27 @@ namespace Tuxedo; /// Enforces that a value has a specific size /// /// size refinement -public readonly struct Size : IRefinement> - where TSize : struct, IRefinement +public readonly struct Size : IRefinement, IEnumerable> + where TSize : struct, IRefinement { /// - public bool CanBeRefined(T value) + public bool CanBeRefined(IEnumerable value) { switch (value) { case ICollection collection: return default(TSize).CanBeRefined(collection.Count); - case IEnumerable enumerable: + default: { - var count = enumerable.Cast().Count(); + var count = 0; + foreach (var _ in value) + count++; return default(TSize).CanBeRefined(count); } - default: - return false; } } /// - public string BuildFailureMessage(T value) => - $"The values size failed refinement: {default(TSize).BuildFailureMessage(default(int))}"; + public string BuildFailureMessage(IEnumerable value) => + $"The values size failed refinement: {default(TSize).BuildFailureMessage(default)}"; } diff --git a/Tuxedo/Refinements/StartsWith.cs b/Tuxedo/Refinements/StartsWith.cs index 3a4eb88..fb986c6 100644 --- a/Tuxedo/Refinements/StartsWith.cs +++ b/Tuxedo/Refinements/StartsWith.cs @@ -4,14 +4,14 @@ /// Enforces that a string value starts with a specific prefix /// /// prefix type -public readonly struct StartsWith : IRefinement> +public readonly struct StartsWith : IRefinement, string> where TPrefix : struct, IConstant { /// - public bool CanBeRefined(T value) => - value is string s && s.StartsWith(default(TPrefix).Value, StringComparison.Ordinal); + public bool CanBeRefined(string value) => + value.StartsWith(default(TPrefix).Value, StringComparison.Ordinal); /// - public string BuildFailureMessage(T value) => + public string BuildFailureMessage(string value) => $"Value must start with '{default(TPrefix).Value}'"; } diff --git a/Tuxedo/Refinements/Trimmed.cs b/Tuxedo/Refinements/Trimmed.cs index f909ee3..4884821 100644 --- a/Tuxedo/Refinements/Trimmed.cs +++ b/Tuxedo/Refinements/Trimmed.cs @@ -5,26 +5,30 @@ namespace Tuxedo; /// /// Ensures that a string value has no leading or trailing whitespace /// -public readonly struct Trimmed : IRefinementResult +public readonly struct Trimmed : IRefinement { - /// - public bool CanBeRefined(T value) => - value is string s && s.Equals(s.Trim(), StringComparison.Ordinal); - - /// - public bool TryRefine(TIn value, [NotNullWhen(true)] out string? refinedValue) + /// + /// Determines if the value is trimmed + /// + /// The value to check + /// True if the value is trimmed, otherwise false + public bool CanBeRefined(string value) { - if (value is string s) - { - refinedValue = s.Trim(); - return true; - } - - refinedValue = null; - return false; + return string.Equals(value.Trim(), value, StringComparison.Ordinal); } + /// + /// Builds a failure message + /// + /// The value that failed the refinement + /// A message describing the failure + public string BuildFailureMessage(string value) => + $"Value must be trimmed, but found '{value}'"; + /// - public string BuildFailureMessage(T value) => - "Value must have no leading or trailing whitespace"; + public bool TryRefine(string value, [NotNullWhen(true)] out string? refinedValue) + { + refinedValue = value.Trim(); + return true; + } } diff --git a/Tuxedo/Refinements/True.cs b/Tuxedo/Refinements/True.cs index de6fc57..90d30fa 100644 --- a/Tuxedo/Refinements/True.cs +++ b/Tuxedo/Refinements/True.cs @@ -3,11 +3,11 @@ /// /// Enforces that a boolean value is true /// -public readonly struct True : IRefinement +public readonly struct True : IRefinement { /// - public bool CanBeRefined(T value) => value is true; + public bool CanBeRefined(bool value) => value; /// - public string BuildFailureMessage(T value) => "Value must be true"; + public string BuildFailureMessage(bool value) => "Value must be true"; } diff --git a/Tuxedo/Refinements/Uri.cs b/Tuxedo/Refinements/Uri.cs new file mode 100644 index 0000000..3e8fd5f --- /dev/null +++ b/Tuxedo/Refinements/Uri.cs @@ -0,0 +1,72 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Tuxedo; + +/// +/// Enforces that a string value is a valid matching a specific +/// +public readonly struct Uri : IRefinement, string, System.Uri> + where TKind : struct, IConstant +{ + /// + public bool CanBeRefined(string value) => TryRefine(value, out _); + + /// + public bool TryRefine(string value, [NotNullWhen(true)] out System.Uri? refinedValue) + { + if (System.Uri.TryCreate(value, default(TKind).Value, out var uri)) + { + refinedValue = uri; + return true; + } + + refinedValue = null; + return false; + } + + /// + public string BuildFailureMessage(string value) => "Value must be a valid URI"; +} + +/// +/// Enforces that a string value is a valid +/// +public readonly struct Uri : IRefinement +{ + /// + public bool CanBeRefined(string value) => TryRefine(value, out _); + + /// + public bool TryRefine(string value, [NotNullWhen(true)] out System.Uri? refinedValue) + { + if (System.Uri.TryCreate(value, UriKind.RelativeOrAbsolute, out var uri)) + { + refinedValue = uri; + return true; + } + + refinedValue = null; + return false; + } + + /// + public string BuildFailureMessage(string value) => "Value must be a valid URI"; +} + +/// +/// Represents a at the type level +/// +public readonly struct UriKindAbsolute : IConstant +{ + /// + public UriKind Value => UriKind.Absolute; +} + +/// +/// Represents a at the type level +/// +public readonly struct UriKindRelative : IConstant +{ + /// + public UriKind Value => UriKind.Relative; +} diff --git a/Tuxedo/Refinements/Uuid.cs b/Tuxedo/Refinements/Uuid.cs index 599f250..f106462 100644 --- a/Tuxedo/Refinements/Uuid.cs +++ b/Tuxedo/Refinements/Uuid.cs @@ -3,15 +3,15 @@ /// /// Enforces that a string value is a valid /// -public readonly struct Uuid : IRefinementResult +public readonly struct Uuid : IRefinement { /// - public bool CanBeRefined(T value) => TryRefine(value, out _); + public bool CanBeRefined(string value) => TryRefine(value, out _); /// - public bool TryRefine(T value, out Guid refinedValue) => - value is string s && Guid.TryParse(s, out refinedValue); + public bool TryRefine(string value, out Guid refinedValue) => + Guid.TryParse(value, out refinedValue); /// - public string BuildFailureMessage(T value) => "Value must be a valid GUID"; + public string BuildFailureMessage(string value) => "Value must be a valid GUID"; }