Skip to content

Commit

Permalink
feat: ensure refinements are typed
Browse files Browse the repository at this point in the history
  • Loading branch information
bmazzarol committed Apr 3, 2024
1 parent 7e12054 commit 4c642be
Show file tree
Hide file tree
Showing 37 changed files with 452 additions and 305 deletions.
8 changes: 4 additions & 4 deletions Tuxedo.Tests/BooleanRefinementTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -67,21 +67,21 @@ public static void Case6()
[Fact(DisplayName = "A refinement can be inverted")]
public static void Case7()
{
Refined<bool, Not<True>> refined = false;
Refined<bool, Not<bool, True>> refined = false;
(refined is { Value: false }).Should().BeTrue();
Refined.TryRefine<bool, Not<True>>(false, out _).Should().BeTrue();
Refined.TryRefine<bool, Not<bool, True>>(false, out _).Should().BeTrue();
}

[Fact(DisplayName = "An inverted refinement fail")]
public static void Case8()
{
Refined.TryRefine<bool, Not<True>>(true, out _).Should().BeFalse();
Refined.TryRefine<bool, Not<bool, True>>(true, out _).Should().BeFalse();
}

[Fact(DisplayName = "An inverted refinement fail and throws")]
public static void Case9()
{
var act = () => (Refined<bool, Not<True>>)true;
var act = () => (Refined<bool, Not<bool, True>>)true;
act.Should()
.Throw<RefinementFailureException>()
.WithMessage("Not: Value must be true")
Expand Down
24 changes: 9 additions & 15 deletions Tuxedo.Tests/IntRefinementTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,21 +78,21 @@ public static void Case8()
[Fact(DisplayName = "Using Not odd integers can be refined with even")]
public static void Case9()
{
Refined<int, Not<Even>> refined = 1;
Refined<int, Not<int, Even>> refined = 1;
refined.Value.Should().Be(1);
Refined.TryRefine<int, Not<Even>>(1, out _).Should().BeTrue();
Refined.TryRefine<int, Not<int, Even>>(1, out _).Should().BeTrue();
}

[Fact(DisplayName = "Using Not even integers cannot be refined with even")]
public static void Case10()
{
Refined.TryRefine<int, Not<Even>>(2, out _).Should().BeFalse();
Refined.TryRefine<int, Not<int, Even>>(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<int, Not<Even>>(2);
var act = () => Refined.Refine<int, Not<int, Even>>(2);
act.Should()
.Throw<RefinementFailureException>()
.WithMessage("Not: Value must be an even number, but found 2")
Expand All @@ -103,28 +103,22 @@ public static void Case11()
[Fact(DisplayName = "A non default integer can be refined")]
public static void Case12()
{
Refined<int, NonEmpty> refined = 1;
Refined<int, NonEmpty<int>> refined = 1;
refined.Value.Should().Be(1);
Refined.TryRefine<int, NonEmpty>(1, out _).Should().BeTrue();
Refined.TryRefine<int, NonEmpty<int>>(1, out _).Should().BeTrue();
}

[Fact(DisplayName = "A default integer cannot be refined")]
public static void Case13()
{
Refined.TryRefine<int, NonEmpty>(default, out _).Should().BeFalse();
Refined.TryRefine<int, NonEmpty<int>>(default, out _).Should().BeFalse();
}

[Fact(DisplayName = "A default integer can be refined with empty")]
public static void Case14()
{
Refined<int, Empty> refined = default;
Refined<int, Empty<int>> refined = default(int);
refined.Value.Should().Be(default);
Refined.TryRefine<int, Empty>(default, out _).Should().BeTrue();
}

[Fact(DisplayName = "A int cannot be refined by size")]
public static void Case15()
{
Refined.TryRefine<int, Size<Even>>(1, out _).Should().BeFalse();
Refined.TryRefine<int, Empty<int>>(default, out _).Should().BeTrue();
}
}
55 changes: 33 additions & 22 deletions Tuxedo.Tests/StringRefinementTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,9 @@ public static void Case4()
[Fact(DisplayName = "A non-empty string refinement can be inverted")]
public static void Case5()
{
Refined<string, Not<NonEmpty>> refined = string.Empty;
Refined<string, Not<string, NonEmpty>> refined = string.Empty;
refined.Value.Should().BeEmpty();
Refined.TryRefine<string, Not<NonEmpty>>(string.Empty, out _).Should().BeTrue();
Refined.TryRefine<string, Not<string, NonEmpty>>(string.Empty, out _).Should().BeTrue();
}

[Fact(DisplayName = "A trimmed string can be refined")]
Expand Down Expand Up @@ -77,27 +77,30 @@ public static void Case9()
var act = () => (Refined<string, Trimmed>)" Hello, World! ";
act.Should()
.Throw<RefinementFailureException>()
.WithMessage("Value must have no leading or trailing whitespace")
.WithMessage("Value must be trimmed, but found ' Hello, World! '")
.And.Value.Should()
.Be(" Hello, World! ");
}

[Fact(DisplayName = "A refinement can be combined with logical AND")]
public static void Case10()
{
Refined<string, And<NonEmpty, Trimmed>> refined = "Hello, World!";
Refined<string, And<string, NonEmpty, Trimmed>> refined = "Hello, World!";
((string)refined).Should().Be("Hello, World!");
Refined.TryRefine<string, And<NonEmpty, Trimmed>>("Hello, World!", out _).Should().BeTrue();
Refined
.TryRefine<string, And<string, NonEmpty, Trimmed>>("Hello, World!", out _)
.Should()
.BeTrue();
}

[Fact(DisplayName = "A refinement can be combined with logical AND and throws")]
public static void Case11()
{
var act = () => (Refined<string, And<NonEmpty, Trimmed>>)" Hello, World! ";
var act = () => (Refined<string, And<string, NonEmpty, Trimmed>>)" Hello, World! ";
act.Should()
.Throw<RefinementFailureException>()
.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! ");
Expand Down Expand Up @@ -130,49 +133,57 @@ public static void Case13()
[Fact(DisplayName = "A absolute uri can be refined")]
public static void Case14()
{
Refined<string, AbsoluteUri> refined = "https://www.bmazzarol.com.au";
Refined<string, Uri<UriKindAbsolute>> refined = "https://www.bmazzarol.com.au";
((string)refined).Should().Be("https://www.bmazzarol.com.au");
Refined
.TryRefine<string, AbsoluteUri>("https://www.bmazzarol.com.au", out _)
.TryRefine<string, Uri<UriKindAbsolute>>("https://www.bmazzarol.com.au", out _)
.Should()
.BeTrue();

Refined<string, Uri, AbsoluteUri> refined2 = "http://www.bmazzarol.com.au";
Refined<string, System.Uri, Uri<UriKindAbsolute>> 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<string, Uri, AbsoluteUri>("http://www.bmazzarol.com.au", out _)
.TryRefine<string, System.Uri, Uri<UriKindAbsolute>>(
"http://www.bmazzarol.com.au",
out _
)
.Should()
.BeTrue();
}

[Fact(DisplayName = "A absolute uri cannot be refined and throws")]
public static void Case15()
{
var act = () => (Refined<string, AbsoluteUri>)"/some/path";
var act = () => (Refined<string, Uri<UriKindAbsolute>>)"/some/path";
act.Should()
.Throw<RefinementFailureException>()
.WithMessage("Value must be a valid absolute URI")
.WithMessage("Value must be a valid URI")
.And.Value.Should()
.Be("/some/path");
Refined.TryRefine<string, Uri, AbsoluteUri>("/some/path", out _).Should().BeFalse();
Refined
.TryRefine<string, System.Uri, Uri<UriKindAbsolute>>("/some/path", out _)
.Should()
.BeFalse();
}

[Fact(DisplayName = "A relative uri can be refined")]
public static void Case16()
{
Refined<string, RelativeUri> refined = "/some/path";
Refined<string, Uri<UriKindRelative>> refined = "/some/path";
((string)refined).Should().Be("/some/path");
Refined.TryRefine<string, RelativeUri>("/some/path", out _).Should().BeTrue();
Refined.TryRefine<string, Uri<UriKindRelative>>("/some/path", out _).Should().BeTrue();
}

[Fact(DisplayName = "A relative uri cannot be refined and throws")]
public static void Case17()
{
var act = () => (Refined<string, RelativeUri>)"https://www.bmazzarol.com.au";
var act = () => (Refined<string, Uri<UriKindRelative>>)"https://www.bmazzarol.com.au";
act.Should()
.Throw<RefinementFailureException>()
.WithMessage("Value must be a valid relative URI")
.WithMessage("Value must be a valid URI")
.And.Value.Should()
.Be("https://www.bmazzarol.com.au");
}
Expand All @@ -182,16 +193,16 @@ public static void Case17()
[InlineData("https://www.bmazzarol.com.au")]
public static void Case18(string uri)
{
Refined<string, AnyUri> refined = uri;
Refined<string, Uri> refined = uri;
((string)refined).Should().Be(uri);
Refined.TryRefine<string, AnyUri>(uri, out _).Should().BeTrue();
Refined.TryRefine<string, Uri>(uri, out _).Should().BeTrue();
}

[Fact(DisplayName = "A non uri cannot be refined and throws")]
public static void Case19()
{
// invalid uri characters
var act = () => (Refined<string, AnyUri>)null!;
var act = () => (Refined<string, Uri>)null!;
act.Should()
.Throw<RefinementFailureException>()
.WithMessage("Value must be a valid URI")
Expand Down
12 changes: 7 additions & 5 deletions Tuxedo/IRefinement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,30 +9,32 @@ namespace Tuxedo;
/// A refined type is a type that is a subset of another type, limited by some predicate.
/// </summary>
/// <typeparam name="TThis">the refinement instance; must be a struct type to enable lookup</typeparam>
public interface IRefinement<TThis>
/// <typeparam name="T">type to refine</typeparam>
public interface IRefinement<TThis, in T>
where TThis : struct
{
/// <summary>
/// Tests if the value can be refined by this instance.
/// </summary>
/// <param name="value">value to test for refinement</param>
/// <returns>true if the value can be refined; otherwise, false</returns>
bool CanBeRefined<T>(T value);
bool CanBeRefined(T value);

/// <summary>
/// Builds a failure message for the given value when it cannot be refined.
/// </summary>
/// <param name="value">value that cannot be refined</param>
/// <returns>failure message</returns>
string BuildFailureMessage<T>(T value);
string BuildFailureMessage(T value);
}

/// <summary>
/// Defines that a contract can be used to refine some type T, with a refined result.
/// </summary>
/// <typeparam name="TThis">the refinement instance; must be a struct type to enable lookup</typeparam>
/// <typeparam name="TIn">input type to refine</typeparam>
/// <typeparam name="TOut">result of the refinement</typeparam>
public interface IRefinementResult<TThis, TOut> : IRefinement<TThis>
public interface IRefinement<TThis, in TIn, TOut> : IRefinement<TThis, TIn>
where TThis : struct
{
/// <summary>
Expand All @@ -41,5 +43,5 @@ public interface IRefinementResult<TThis, TOut> : IRefinement<TThis>
/// <param name="value">value to test for refinement</param>
/// <param name="refinedValue">refined value</param>
/// <returns>true if the value can be refined; otherwise, false</returns>
bool TryRefine<TIn>(TIn value, [NotNullWhen(true)] out TOut? refinedValue);
bool TryRefine(TIn value, [NotNullWhen(true)] out TOut? refinedValue);
}
18 changes: 9 additions & 9 deletions Tuxedo/Refined.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@
namespace Tuxedo;

/// <summary>
/// Represents a refined type, the refinement is enforced by the TRefinement type which is an implementation of <see cref="IRefinement{TThis}"/>
/// Represents a refined type, the refinement is enforced by the TRefinement type which is an implementation of <see cref="IRefinement{TThis,T}"/>
/// </summary>
/// <typeparam name="T">refined type</typeparam>
/// <typeparam name="TRefinement">refinement on the type</typeparam>
public readonly record struct Refined<T, TRefinement>
where TRefinement : struct, IRefinement<TRefinement>
where TRefinement : struct, IRefinement<TRefinement, T>
{
/// <summary>
/// The underlying value of the refined type
Expand Down Expand Up @@ -48,14 +48,14 @@ public void Deconstruct(out T value)
}

/// <summary>
/// Represents a refined type, the refinement is enforced by the TRefinement type which is an implementation of <see cref="IRefinement{TThis}"/>
/// Represents a refined type, the refinement is enforced by the TRefinement type which is an implementation of <see cref="IRefinement{TThis,T}"/>
/// </summary>
/// <typeparam name="TRaw">raw refined type</typeparam>
/// <typeparam name="TRefined">refined type</typeparam>
/// <typeparam name="TRefinement">refinement on the type</typeparam>
[StructLayout(LayoutKind.Auto)]
public readonly record struct Refined<TRaw, TRefined, TRefinement>
where TRefinement : struct, IRefinementResult<TRefinement, TRefined>
where TRefinement : struct, IRefinement<TRefinement, TRaw, TRefined>
{
/// <summary>
/// The underlying value of the refined type
Expand Down Expand Up @@ -124,7 +124,7 @@ public static class Refined
/// <typeparam name="TRefinement">refinement applied to the type</typeparam>
/// <returns>true if the value was refined; otherwise, false</returns>
public static bool TryRefine<T, TRefinement>(T value, out Refined<T, TRefinement> refined)
where TRefinement : struct, IRefinement<TRefinement>
where TRefinement : struct, IRefinement<TRefinement, T>
{
var refinement = default(TRefinement);
if (!refinement.CanBeRefined(value))
Expand All @@ -150,7 +150,7 @@ public static bool TryRefine<TRaw, TRefined, TRefinement>(
TRaw value,
out Refined<TRaw, TRefined, TRefinement> refined
)
where TRefinement : struct, IRefinementResult<TRefinement, TRefined>
where TRefinement : struct, IRefinement<TRefinement, TRaw, TRefined>
{
var refinement = default(TRefinement);
if (!refinement.TryRefine(value, out var refinedValue))
Expand All @@ -167,7 +167,7 @@ out Refined<TRaw, TRefined, TRefinement> refined
[SuppressMessage("Design", "MA0026:Fix TODO comment")]
[SuppressMessage("Info Code Smell", "S1135:Track uses of \"TODO\" tags")]
private static void Throw<T, TRefinement>(T value, TRefinement refinement)
where TRefinement : struct, IRefinement<TRefinement>
where TRefinement : struct, IRefinement<TRefinement, T>
{
// TODO: find a way to rewind the stack trace to the caller
throw new RefinementFailureException(value, refinement.BuildFailureMessage(value));
Expand All @@ -182,7 +182,7 @@ private static void Throw<T, TRefinement>(T value, TRefinement refinement)
/// <returns>refined value</returns>
/// <exception cref="RefinementFailureException">thrown if the value cannot be refined</exception>
public static Refined<T, TRefinement> Refine<T, TRefinement>(T value)
where TRefinement : struct, IRefinement<TRefinement>
where TRefinement : struct, IRefinement<TRefinement, T>
{
var refinement = default(TRefinement);
if (refinement.CanBeRefined(value))
Expand All @@ -206,7 +206,7 @@ public static Refined<T, TRefinement> Refine<T, TRefinement>(T value)
public static Refined<TRaw, TRefined, TRefinement> Refine<TRaw, TRefined, TRefinement>(
TRaw value
)
where TRefinement : struct, IRefinementResult<TRefinement, TRefined>
where TRefinement : struct, IRefinement<TRefinement, TRaw, TRefined>
{
var refinement = default(TRefinement);
if (refinement.TryRefine(value, out var refinedValue))
Expand Down
28 changes: 0 additions & 28 deletions Tuxedo/Refinements/AbsoluteUri.cs

This file was deleted.

Loading

0 comments on commit 4c642be

Please sign in to comment.