diff --git a/.github/workflows/cicd.yml b/.github/workflows/cicd.yml index a55f415..80b270a 100644 --- a/.github/workflows/cicd.yml +++ b/.github/workflows/cicd.yml @@ -22,10 +22,10 @@ jobs: all: if: github.run_id != 1 name: Build & Tests - uses: dailydevops/pipelines/.github/workflows/cicd-dotnet.yml@0.14.82 + uses: dailydevops/pipelines/.github/workflows/cicd-dotnet.yml@0.14.86 with: enableSonarQube: true - dotnet-logging: ${{ inputs.dotnet-logging }} + dotnet-logging: ${{ inputs.dotnet-logging || 'minimal' }} dotnet-version: ${{ vars.NE_DOTNET_TARGETFRAMEWORKS }} solution: ./FluentValue.sln secrets: inherit diff --git a/Directory.Build.props b/Directory.Build.props index 435e907..4ff7342 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -9,8 +9,8 @@ 2024 - net6.0;net8.0;net9.0 - net6.0;net7.0;net8.0;net9.0 + net8.0;net9.0 + net8.0;net9.0 diff --git a/Directory.Packages.props b/Directory.Packages.props index ea3d118..9249dfd 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -5,7 +5,7 @@ - + @@ -13,18 +13,16 @@ - - - - + + diff --git a/FluentValue.sln b/FluentValue.sln index 92cefdf..9d1f2f9 100644 --- a/FluentValue.sln +++ b/FluentValue.sln @@ -11,8 +11,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{AE6A68C3 EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NetEvolve.FluentValue.Tests.Unit", "tests\NetEvolve.FluentValue.Tests.Unit\NetEvolve.FluentValue.Tests.Unit.csproj", "{93D85E57-7EB9-4CDD-836E-301A58BF3312}" EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NetEvolve.FluentValue.Tests.Architecture", "tests\NetEvolve.FluentValue.Tests.Architecture\NetEvolve.FluentValue.Tests.Architecture.csproj", "{6C599754-9C81-494C-9136-98259711B6C8}" -EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{2FF9F0BF-A46C-4C74-BD17-25B38E8D85EE}" ProjectSection(SolutionItems) = preProject .commitlintrc = .commitlintrc @@ -46,10 +44,6 @@ Global {93D85E57-7EB9-4CDD-836E-301A58BF3312}.Debug|Any CPU.Build.0 = Debug|Any CPU {93D85E57-7EB9-4CDD-836E-301A58BF3312}.Release|Any CPU.ActiveCfg = Release|Any CPU {93D85E57-7EB9-4CDD-836E-301A58BF3312}.Release|Any CPU.Build.0 = Release|Any CPU - {6C599754-9C81-494C-9136-98259711B6C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {6C599754-9C81-494C-9136-98259711B6C8}.Debug|Any CPU.Build.0 = Debug|Any CPU - {6C599754-9C81-494C-9136-98259711B6C8}.Release|Any CPU.ActiveCfg = Release|Any CPU - {6C599754-9C81-494C-9136-98259711B6C8}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -57,6 +51,5 @@ Global GlobalSection(NestedProjects) = preSolution {7B62BC70-8970-4D45-A86C-CB6C7E64A5D9} = {BB2B11D1-1E37-4333-9E07-E6709C1796F5} {93D85E57-7EB9-4CDD-836E-301A58BF3312} = {AE6A68C3-E24A-45BC-B7A6-4D527AF3BF99} - {6C599754-9C81-494C-9136-98259711B6C8} = {AE6A68C3-E24A-45BC-B7A6-4D527AF3BF99} EndGlobalSection EndGlobal diff --git a/src/NetEvolve.FluentValue/Constraints/ConstraintBase.cs b/src/NetEvolve.FluentValue/Constraints/ConstraintBase.cs index 8176ef9..d59d957 100644 --- a/src/NetEvolve.FluentValue/Constraints/ConstraintBase.cs +++ b/src/NetEvolve.FluentValue/Constraints/ConstraintBase.cs @@ -1,9 +1,19 @@ namespace NetEvolve.FluentValue.Constraints; using System; +using System.Collections; +using System.ComponentModel; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.Text; using NetEvolve.FluentValue; +[SuppressMessage("Info Code Smell", "S1133:Deprecated code should be removed", Justification = "As designed.")] +[SuppressMessage( + "Blocker Code Smell", + "S3877:Exceptions should not be thrown from unexpected methods", + Justification = "As designed." +)] internal abstract class ConstraintBase : IConstraint { [ThreadStatic] @@ -11,10 +21,26 @@ internal abstract class ConstraintBase : IConstraint private const int DefaultCapacity = 1024; + /// public abstract bool IsSatisfiedBy(object? value); + /// public abstract void SetDescription(StringBuilder builder); + /// + [Obsolete("This is base `object` method that should not be called.", true)] + [EditorBrowsable(EditorBrowsableState.Never)] + [DebuggerHidden] + public new bool Equals(object? obj) => + throw new NotSupportedException("This is base `object` method that should not be called."); + + /// + [Obsolete("This is base `object` method that should not be called.", true)] + [EditorBrowsable(EditorBrowsableState.Never)] + [DebuggerHidden] + public new int GetHashCode() => + throw new NotSupportedException("This is base `object` method that should not be called."); + public override string ToString() { var builder = _builder ?? new StringBuilder(capacity: DefaultCapacity); diff --git a/src/NetEvolve.FluentValue/Constraints/DefaultConstraint.cs b/src/NetEvolve.FluentValue/Constraints/DefaultConstraint.cs index 9ef5a12..0619ea3 100644 --- a/src/NetEvolve.FluentValue/Constraints/DefaultConstraint.cs +++ b/src/NetEvolve.FluentValue/Constraints/DefaultConstraint.cs @@ -2,25 +2,16 @@ using System; using System.Text; +using NetEvolve.FluentValue; internal sealed class DefaultConstraint : ConstraintBase { public override bool IsSatisfiedBy(object? value) => value?.GetType() switch { - { IsValueType: true } valueType => GetDefault(valueType).Equals(value), + { IsValueType: true } valueType => TypeExtensions.GetDefault(valueType)?.Equals(value) ?? false, _ => false, }; public override void SetDescription(StringBuilder builder) => builder.Append(" is "); - - private static object GetDefault(Type value) - { - var underlying = Nullable.GetUnderlyingType(value); - if (underlying is not null) - { - return Activator.CreateInstance(underlying)!; - } - return Activator.CreateInstance(value)!; - } } diff --git a/src/NetEvolve.FluentValue/Constraints/NotDefaultConstraint.cs b/src/NetEvolve.FluentValue/Constraints/NotDefaultConstraint.cs new file mode 100644 index 0000000..1ac1059 --- /dev/null +++ b/src/NetEvolve.FluentValue/Constraints/NotDefaultConstraint.cs @@ -0,0 +1,16 @@ +namespace NetEvolve.FluentValue.Constraints; + +using System.Text; +using NetEvolve.FluentValue; + +internal sealed class NotDefaultConstraint : ConstraintBase +{ + public override bool IsSatisfiedBy(object? value) => + value?.GetType() switch + { + { IsValueType: true } valueType => !TypeExtensions.GetDefault(valueType)?.Equals(value) ?? false, + _ => true, + }; + + public override void SetDescription(StringBuilder builder) => builder.Append(" is not "); +} diff --git a/src/NetEvolve.FluentValue/Constraints/NotEmptyConstraint.cs b/src/NetEvolve.FluentValue/Constraints/NotEmptyConstraint.cs new file mode 100644 index 0000000..3741f28 --- /dev/null +++ b/src/NetEvolve.FluentValue/Constraints/NotEmptyConstraint.cs @@ -0,0 +1,20 @@ +namespace NetEvolve.FluentValue.Constraints; + +using System; +using System.Collections; +using System.Text; + +internal sealed class NotEmptyConstraint : ConstraintBase +{ + public override bool IsSatisfiedBy(object? value) => + value switch + { + string stringValue => stringValue.Length > 0, + Guid guidValue => guidValue != Guid.Empty, + ICollection collection => collection.Count > 0, + IEnumerable enumerable => enumerable.GetEnumerator().MoveNext(), + _ => false, + }; + + public override void SetDescription(StringBuilder builder) => builder.Append(" is not "); +} diff --git a/src/NetEvolve.FluentValue/Constraints/NotNullConstraint.cs b/src/NetEvolve.FluentValue/Constraints/NotNullConstraint.cs new file mode 100644 index 0000000..c5d8fcb --- /dev/null +++ b/src/NetEvolve.FluentValue/Constraints/NotNullConstraint.cs @@ -0,0 +1,10 @@ +namespace NetEvolve.FluentValue.Constraints; + +using System.Text; + +internal sealed class NotNullConstraint : ConstraintBase +{ + public override bool IsSatisfiedBy(object? value) => value is not null; + + public override void SetDescription(StringBuilder builder) => builder.Append(" is not "); +} diff --git a/src/NetEvolve.FluentValue/IOperator.cs b/src/NetEvolve.FluentValue/IOperator.cs index 8489f38..6a35e04 100644 --- a/src/NetEvolve.FluentValue/IOperator.cs +++ b/src/NetEvolve.FluentValue/IOperator.cs @@ -138,11 +138,8 @@ IConstraint EqualTo(string compareValue, StringComparison comparison = default) /// /// The current instance. /// - IConstraint Matches( -#if NET7_0_OR_GREATER - [StringSyntax(StringSyntaxAttribute.Regex)] -#endif - string pattern, RegexOptions? options = null) => SetConstraint(new MatchesConstraint(pattern, options)); + IConstraint Matches([StringSyntax(StringSyntaxAttribute.Regex)] string pattern, RegexOptions? options = null) => + SetConstraint(new MatchesConstraint(pattern, options)); /// /// Appends a negation operator. @@ -165,6 +162,21 @@ IOperator Not } } + /// + /// Appends a constraint that the value is the value. + /// + IConstraint NotDefault => SetConstraint(new NotDefaultConstraint()); + + /// + /// Appends a constraint that the value is empty. + /// + IConstraint NotEmpty => SetConstraint(new NotEmptyConstraint()); + + /// + /// Appends a constraint that the value is . + /// + IConstraint NotNull => SetConstraint(new NotNullConstraint()); + /// /// Appends a constraint that the value is . /// diff --git a/src/NetEvolve.FluentValue/Operators/AndOperator.cs b/src/NetEvolve.FluentValue/Operators/AndOperator.cs index 383e830..893e9a6 100644 --- a/src/NetEvolve.FluentValue/Operators/AndOperator.cs +++ b/src/NetEvolve.FluentValue/Operators/AndOperator.cs @@ -4,8 +4,9 @@ using System.Text; using NetEvolve.Arguments; using NetEvolve.FluentValue; +using NetEvolve.FluentValue.Constraints; -internal sealed class AndOperator : OperatorBase +internal sealed class AndOperator : ConstraintBase, IOperator { private readonly IConstraint _left; private IConstraint? _right; @@ -27,7 +28,7 @@ public override bool IsSatisfiedBy(object? value) return _left.IsSatisfiedBy(value) && _right.IsSatisfiedBy(value); } - public override IConstraint SetConstraint(IConstraint constraint) + public IConstraint SetConstraint(IConstraint constraint) { Argument.ThrowIfNull(constraint); diff --git a/src/NetEvolve.FluentValue/Operators/NotOperator.cs b/src/NetEvolve.FluentValue/Operators/NotOperator.cs index 04cf14c..582d824 100644 --- a/src/NetEvolve.FluentValue/Operators/NotOperator.cs +++ b/src/NetEvolve.FluentValue/Operators/NotOperator.cs @@ -4,8 +4,9 @@ using System.Text; using NetEvolve.Arguments; using NetEvolve.FluentValue; +using NetEvolve.FluentValue.Constraints; -internal sealed class NotOperator : OperatorBase +internal sealed class NotOperator : ConstraintBase, IOperator { internal IConstraint? _constraint; @@ -21,7 +22,7 @@ public override bool IsSatisfiedBy(object? value) return !_constraint.IsSatisfiedBy(value); } - public override IConstraint SetConstraint(IConstraint constraint) + public IConstraint SetConstraint(IConstraint constraint) { Argument.ThrowIfNull(constraint); diff --git a/src/NetEvolve.FluentValue/Operators/OperatorBase.cs b/src/NetEvolve.FluentValue/Operators/OperatorBase.cs deleted file mode 100644 index 43edaac..0000000 --- a/src/NetEvolve.FluentValue/Operators/OperatorBase.cs +++ /dev/null @@ -1,9 +0,0 @@ -namespace NetEvolve.FluentValue.Operators; - -using NetEvolve.FluentValue; -using NetEvolve.FluentValue.Constraints; - -internal abstract class OperatorBase : ConstraintBase, IOperator -{ - public abstract IConstraint SetConstraint(IConstraint constraint); -} diff --git a/src/NetEvolve.FluentValue/Operators/OrOperator.cs b/src/NetEvolve.FluentValue/Operators/OrOperator.cs index f218b0c..365b263 100644 --- a/src/NetEvolve.FluentValue/Operators/OrOperator.cs +++ b/src/NetEvolve.FluentValue/Operators/OrOperator.cs @@ -4,8 +4,9 @@ using System.Text; using NetEvolve.Arguments; using NetEvolve.FluentValue; +using NetEvolve.FluentValue.Constraints; -internal sealed class OrOperator : OperatorBase +internal sealed class OrOperator : ConstraintBase, IOperator { private readonly IConstraint _left; private IConstraint? _right; @@ -27,7 +28,7 @@ public override bool IsSatisfiedBy(object? value) return _left.IsSatisfiedBy(value) || _right.IsSatisfiedBy(value); } - public override IConstraint SetConstraint(IConstraint constraint) + public IConstraint SetConstraint(IConstraint constraint) { Argument.ThrowIfNull(constraint); diff --git a/src/NetEvolve.FluentValue/Operators/XorOperator.cs b/src/NetEvolve.FluentValue/Operators/XorOperator.cs index c7d6110..805b4bb 100644 --- a/src/NetEvolve.FluentValue/Operators/XorOperator.cs +++ b/src/NetEvolve.FluentValue/Operators/XorOperator.cs @@ -4,8 +4,9 @@ using System.Text; using NetEvolve.Arguments; using NetEvolve.FluentValue; +using NetEvolve.FluentValue.Constraints; -internal sealed class XorOperator : OperatorBase +internal sealed class XorOperator : ConstraintBase, IOperator { private readonly IConstraint _left; private IConstraint? _right; @@ -27,7 +28,7 @@ public override bool IsSatisfiedBy(object? value) return _left.IsSatisfiedBy(value) ^ _right.IsSatisfiedBy(value); } - public override IConstraint SetConstraint(IConstraint constraint) + public IConstraint SetConstraint(IConstraint constraint) { Argument.ThrowIfNull(constraint); diff --git a/src/NetEvolve.FluentValue/TypeExtensions.cs b/src/NetEvolve.FluentValue/TypeExtensions.cs new file mode 100644 index 0000000..c8184e1 --- /dev/null +++ b/src/NetEvolve.FluentValue/TypeExtensions.cs @@ -0,0 +1,21 @@ +namespace NetEvolve.FluentValue; + +using System; + +internal static class TypeExtensions +{ + internal static object? GetDefault(this Type value) + { + if (!value.IsValueType) + { + return null; + } + + var underlying = Nullable.GetUnderlyingType(value); + if (underlying is not null) + { + return null; + } + return Activator.CreateInstance(value)!; + } +} diff --git a/src/NetEvolve.FluentValue/Value.cs b/src/NetEvolve.FluentValue/Value.cs index e109220..dbe786f 100644 --- a/src/NetEvolve.FluentValue/Value.cs +++ b/src/NetEvolve.FluentValue/Value.cs @@ -1,6 +1,7 @@ namespace NetEvolve.FluentValue; using System; +using System.Diagnostics.CodeAnalysis; using System.Text.RegularExpressions; using NetEvolve.FluentValue.Constraints; using NetEvolve.FluentValue.Operators; @@ -52,7 +53,7 @@ public static IConstraint Contains(string compareValue, StringComparison compari public static IConstraint Contains(object? compareValue) => new ContainsConstraint(compareValue); /// - /// Appends a constraint that the value is the default value. + /// Appends a constraint that the value is the value. /// public static IConstraint Default => new DefaultConstraint(); @@ -126,10 +127,7 @@ public static IConstraint EqualTo(string compareValue, StringComparison comparis /// The current instance. /// public static IConstraint Matches( -#if NET7_0_OR_GREATER - [System.Diagnostics.CodeAnalysis.StringSyntax(System.Diagnostics.CodeAnalysis.StringSyntaxAttribute.Regex)] -#endif - string pattern, + [StringSyntax(StringSyntaxAttribute.Regex)] string pattern, RegexOptions? options = null ) => new MatchesConstraint(pattern, options); @@ -138,6 +136,21 @@ public static IConstraint Matches( /// public static IOperator Not => new NotOperator(); + /// + /// Appends a constraint that the value is the value. + /// + public static IConstraint NotDefault => new NotDefaultConstraint(); + + /// + /// Appends a constraint that the value is empty. + /// + public static IConstraint NotEmpty => new NotEmptyConstraint(); + + /// + /// Appends a constraint that the value is . + /// + public static IConstraint NotNull => new NotNullConstraint(); + /// /// Appends a constraint that the value is . /// diff --git a/tests/NetEvolve.FluentValue.Tests.Architecture/NetEvolve.FluentValue.Tests.Architecture.csproj b/tests/NetEvolve.FluentValue.Tests.Architecture/NetEvolve.FluentValue.Tests.Architecture.csproj deleted file mode 100644 index 512b8b5..0000000 --- a/tests/NetEvolve.FluentValue.Tests.Architecture/NetEvolve.FluentValue.Tests.Architecture.csproj +++ /dev/null @@ -1,25 +0,0 @@ - - - $(NetEvolve_TestTargetFrameworks) - - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - - - - - - - - - diff --git a/tests/NetEvolve.FluentValue.Tests.Unit/ConstraintTests.cs b/tests/NetEvolve.FluentValue.Tests.Unit/ConstraintTests.cs index 1569e43..e0e8802 100644 --- a/tests/NetEvolve.FluentValue.Tests.Unit/ConstraintTests.cs +++ b/tests/NetEvolve.FluentValue.Tests.Unit/ConstraintTests.cs @@ -4,240 +4,287 @@ using System.Collections.Generic; using System.Linq; using NetEvolve.FluentValue; -using Xunit; using static System.StringComparison; public class ConstraintTests { - [Fact] - public void Value_NotNot_NoFurtherConstraint_ThrowsInvalidOperationException() => - _ = Assert.Throws(() => Value.Not.Not); +#pragma warning disable TUnit0046 // TUnit0046: Return a `Func` rather than a `` + [Test] + [MethodDataSource(nameof(ChainedInvalidOperationsData))] + public void Value_ChainedInvalidOperations_ShouldThrowInvalidOperationException(Func funcConstraint) => + _ = Assert.Throws(() => + { + var _ = funcConstraint.Invoke(); + }); - [Fact] - public void Value_MultipleOperators_ThrowsInvalidOperationException() => - Assert.Multiple( - () => _ = Assert.Throws(() => Value.Null.And.And), - () => _ = Assert.Throws(() => Value.Null.And.Or), - () => _ = Assert.Throws(() => Value.Null.And.Xor), - () => _ = Assert.Throws(() => Value.Null.Or.And), - () => _ = Assert.Throws(() => Value.Null.Or.Or), - () => _ = Assert.Throws(() => Value.Null.Or.Xor), - () => _ = Assert.Throws(() => Value.Null.Xor.And), - () => _ = Assert.Throws(() => Value.Null.Xor.Or), - () => _ = Assert.Throws(() => Value.Null.Xor.Xor) - ); + public static Func[] ChainedInvalidOperationsData() => + [ + () => Value.Not.Not, + () => Value.Null.And.And, + () => Value.Null.And.Or, + () => Value.Null.And.Xor, + () => Value.Null.Or.And, + () => Value.Null.Or.Or, + () => Value.Null.Or.Xor, + () => Value.Null.Xor.And, + () => Value.Null.Xor.Or, + () => Value.Null.Xor.Xor, + ]; +#pragma warning restore TUnit0046 // TUnit0046: Return a `Func` rather than a `` - [Fact] - public void Value_Contains_Object_ThrowsNotSupportedException() => + [Test] + public void Value_WhenNewObject_ShouldThrowNotSupportedException() => _ = Assert.Throws(() => Value.Contains(new object()).IsSatisfiedBy(new object())); - [Theory] - [MemberData(nameof(InvalidConstraintData))] + [Test] + [MethodDataSource(nameof(InvalidConstraintData))] public void Value_InvalidConstraint_ThrowsInvalidOperationException(IConstraint constraint) => _ = Assert.Throws(() => constraint.IsSatisfiedBy(true)); - public static TheoryData InvalidConstraintData => + public static Func[] InvalidConstraintData() => [ - Value.Not, - Value.Null.And, - Value.Null.Or, - Value.Null.Xor, - Value.Not.Null.And, - Value.Not.Null.Or, - Value.Not.Null.Xor, + () => Value.Not, + () => Value.Null.And, + () => Value.Null.Or, + () => Value.Null.Xor, + () => Value.Not.Null.And, + () => Value.Not.Null.Or, + () => Value.Not.Null.Xor, ]; - [Theory] - [MemberData(nameof(ConstraintValueData))] - public void Value_Theory_Expected(bool expected, IConstraint constraint, object? value) + [Test] + [MethodDataSource(nameof(ConstraintValueData))] + public async Task Value_Theory_Expected(bool expected, IConstraint constraint, object? value) { - Assert.NotNull(constraint); + constraint = await Assert.That(constraint).IsNotNull(); var result = constraint.IsSatisfiedBy(value); - Assert.Equal(expected, result); + _ = await Assert.That(result).IsEqualTo(expected); } - public static TheoryData ConstraintValueData => - new TheoryData - { + public static Func<(bool, IConstraint, object?)>[] ConstraintValueData() => + [ // .Contains - { false, Value.Contains("Null"), null }, - { false, Value.Contains("Hallo", Ordinal), "Hello World!" }, - { true, Value.Contains("Hello"), "Hello World!" }, - { false, Value.Contains('Z', Ordinal), "Hello World!" }, - { true, Value.Contains('W'), "Hello World!" }, - { - false, - Value.Contains(2), - new Dictionary { { "Hello", "World!" } } - }, - { - true, - Value.Contains("Hello"), - new Dictionary { { "Hello", "World!" } } - }, - { - false, - Value.Contains(2), - new List { "Hello", "World!" } - }, - { - true, - Value.Contains("World!"), - new List { "Hello", "World!" } - }, - { false, Value.Contains(2), Enumerable.Range(10, 10) }, - { true, Value.Contains(11), Enumerable.Range(10, 10) }, + () => (false, Value.Contains("Null"), null), + () => (false, Value.Contains("Hallo", Ordinal), "Hello World!"), + () => (true, Value.Contains("Hello"), "Hello World!"), + () => (false, Value.Contains('Z', Ordinal), "Hello World!"), + () => (true, Value.Contains('W'), "Hello World!"), + () => (false, Value.Contains(2), new Dictionary { { "Hello", "World!" } }), + () => (true, Value.Contains("Hello"), new Dictionary { { "Hello", "World!" } }), + () => (false, Value.Contains(2), new List { "Hello", "World!" }), + () => (true, Value.Contains("World!"), new List { "Hello", "World!" }), + () => (false, Value.Contains(2), Enumerable.Range(10, 10)), + () => (true, Value.Contains(11), Enumerable.Range(10, 10)), // .Default - { false, Value.Default, 1 }, - { false, Value.Default, null }, - { false, Value.Default, (int?)3 }, - { true, Value.Default, 0 }, + () => (false, Value.Default, 1), + () => (false, Value.Default, null), + () => (false, Value.Default, (int?)3), + () => (true, Value.Default, 0), // .Empty - { false, Value.Empty, "Hello World!" }, - { true, Value.Empty, string.Empty }, - { false, Value.Empty, Guid.NewGuid() }, - { true, Value.Empty, Guid.Empty }, - { - false, - Value.Empty, - new List { "Hello", "World!" } - }, - { true, Value.Empty, new List() }, - { false, Value.Empty, Enumerable.Range(10, 10) }, - { true, Value.Empty, Enumerable.Empty() }, - { false, Value.Empty, null }, + () => (false, Value.Empty, "Hello World!"), + () => (true, Value.Empty, string.Empty), + () => (false, Value.Empty, Guid.NewGuid()), + () => (true, Value.Empty, Guid.Empty), + () => (false, Value.Empty, new List { "Hello", "World!" }), + () => (true, Value.Empty, new List()), + () => (false, Value.Empty, Enumerable.Range(10, 10)), + () => (true, Value.Empty, Enumerable.Empty()), + () => (false, Value.Empty, null), // .EndsWith - { false, Value.EndsWith("Welt!"), "Hello World!" }, - { true, Value.EndsWith("World!"), "Hello World!" }, - { false, Value.EndsWith('?'), "Hello World!" }, - { true, Value.EndsWith('!'), "Hello World!" }, - { true, Value.EndsWith("world!", OrdinalIgnoreCase), "Hello World!" }, - { false, Value.EndsWith("123"), 123 }, + () => (false, Value.EndsWith("Welt!"), "Hello World!"), + () => (true, Value.EndsWith("World!"), "Hello World!"), + () => (false, Value.EndsWith('?'), "Hello World!"), + () => (true, Value.EndsWith('!'), "Hello World!"), + () => (true, Value.EndsWith("world!", OrdinalIgnoreCase), "Hello World!"), + () => (false, Value.EndsWith("123"), 123), // .EqualTo - { false, Value.EqualTo("World!"), "Hello World!" }, - { true, Value.EqualTo("Hello World!"), "Hello World!" }, - { false, Value.EqualTo('H'), "Hello World!" }, - { true, Value.EqualTo(123456), "123456" }, - { true, Value.EqualTo("hello world!", OrdinalIgnoreCase), "Hello World!" }, - { true, Value.EqualTo(123), 123 }, - { true, Value.EqualTo(null), null }, + () => (false, Value.EqualTo("World!"), "Hello World!"), + () => (true, Value.EqualTo("Hello World!"), "Hello World!"), + () => (false, Value.EqualTo('H'), "Hello World!"), + () => (true, Value.EqualTo(123456), "123456"), + () => (true, Value.EqualTo("hello world!", OrdinalIgnoreCase), "Hello World!"), + () => (true, Value.EqualTo(123), 123), + () => (true, Value.EqualTo(null), null), // .Matches - { false, Value.Matches(@"\d+"), null }, - { true, Value.Matches(@"\d+"), 123456 }, - { false, Value.Matches(@"\d+"), "Hello World!" }, - { true, Value.Matches(@"\w+\s\w+"), "Hello World!" }, + () => (false, Value.Matches(@"\d+"), null), + () => (true, Value.Matches(@"\d+"), 123456), + () => (false, Value.Matches(@"\d+"), "Hello World!"), + () => (true, Value.Matches(@"\w+\s\w+"), "Hello World!"), // .Null - { false, Value.Null, 1 }, - { true, Value.Null, null }, + () => (false, Value.Null, 1), + () => (true, Value.Null, null), // .StartsWith - { false, Value.StartsWith("World"), "Hello World!" }, - { false, Value.StartsWith('?'), "Hello World!" }, - { true, Value.StartsWith('H'), "Hello World!" }, - { true, Value.StartsWith("hello", OrdinalIgnoreCase), "Hello World!" }, - { false, Value.StartsWith("123"), 123 }, + () => (false, Value.StartsWith("Welt!"), "Hello World!"), + () => (true, Value.StartsWith("Hello"), "Hello World!"), + () => (false, Value.StartsWith('?'), "Hello World!"), + () => (true, Value.StartsWith('H'), "Hello World!"), + () => (true, Value.StartsWith("hello", OrdinalIgnoreCase), "Hello World!"), + () => (false, Value.StartsWith("123"), 123), // .WhiteSpace - { false, Value.WhiteSpace, "Hello World!" }, - { false, Value.WhiteSpace, null }, - { true, Value.WhiteSpace, " " }, - { true, Value.WhiteSpace, "\t" }, - { true, Value.WhiteSpace, "\n" }, - { true, Value.WhiteSpace, '\t' }, - { true, Value.WhiteSpace, '\n' }, - { false, Value.WhiteSpace, 1 }, + () => (false, Value.WhiteSpace, "Hello World!"), + () => (false, Value.WhiteSpace, null), + () => (true, Value.WhiteSpace, " "), + () => (true, Value.WhiteSpace, "\t"), + () => (true, Value.WhiteSpace, "\n"), + () => (true, Value.WhiteSpace, '\t'), + () => (true, Value.WhiteSpace, '\n'), + () => (false, Value.WhiteSpace, 1), // .And Operators - { false, Value.Contains("Hello", OrdinalIgnoreCase).And.Contains("Welt!", Ordinal), "Hello World!" }, - { true, Value.Contains("Hello", OrdinalIgnoreCase).And.Contains("World!", Ordinal), "Hello World!" }, + () => (false, Value.Contains("Hello", OrdinalIgnoreCase).And.Contains("Welt!", Ordinal), "Hello World!"), + () => (true, Value.Contains("Hello", OrdinalIgnoreCase).And.Contains("World!", Ordinal), "Hello World!"), // .Or Operators - { false, Value.Null.Or.Empty, "Hello World!" }, - { true, Value.Null.Or.Empty, null }, - { true, Value.Null.Or.Empty, string.Empty }, + () => (false, Value.Null.Or.Empty, "Hello World!"), + () => (true, Value.Null.Or.Empty, null), + () => (true, Value.Null.Or.Empty, string.Empty), // .Xor Operators - { false, Value.Null.Xor.Empty, "Hello World!" }, - { true, Value.Null.Xor.Empty, string.Empty }, - { false, Value.Contains("Hello", OrdinalIgnoreCase).Xor.Contains("World!", Ordinal), "Hello World!" }, - { true, Value.Contains("Hello", OrdinalIgnoreCase).Xor.Not.Contains("World!", Ordinal), "Hello World!" }, + () => (false, Value.Null.Xor.Empty, "Hello World!"), + () => (true, Value.Null.Xor.Empty, string.Empty), + () => (false, Value.Contains("Hello", OrdinalIgnoreCase).Xor.Contains("World!", Ordinal), "Hello World!"), + () => + (true, Value.Contains("Hello", OrdinalIgnoreCase).Xor.Not.Contains("World!", Ordinal), "Hello World!"), // .Not Operators - { false, Value.Not.EqualTo(2), "2" }, - { false, Value.Not.EqualTo("Hello World!"), "Hello World!" }, - { false, Value.Not.Null, null }, - { false, Value.Not.Empty, string.Empty }, - { true, Value.Not.Empty, "Hello World!" }, - { true, Value.Not.Contains("Hallo", Ordinal), "Hello World!" }, - { false, Value.Not.StartsWith("Hello"), "Hello World!" }, - { false, Value.Not.EndsWith("World!"), "Hello World!" }, - { false, Value.Not.WhiteSpace, " " }, - { false, Value.Not.Matches(@"\d+"), 123456 }, - { true, Value.Not.Default, 1 }, - { true, Value.Not.Contains('Z'), "Hello World!" }, - { true, Value.Not.Contains(123), "Hello World!" }, - { true, Value.Not.StartsWith('W'), "Hello World!" }, - { true, Value.Not.EndsWith('W'), "Hello World!" }, + () => (false, Value.Not.EqualTo(2), "2"), + () => (false, Value.Not.EqualTo("Hello World!"), "Hello World!"), + () => (false, Value.Not.Null, null), + () => (false, Value.Not.Empty, string.Empty), + () => (true, Value.Not.Empty, "Hello World!"), + () => (true, Value.Not.Contains("Hallo", Ordinal), "Hello World!"), + () => (false, Value.Not.StartsWith("Hello"), "Hello World!"), + () => (false, Value.Not.EndsWith("World!"), "Hello World!"), + () => (false, Value.Not.WhiteSpace, " "), + () => (false, Value.Not.Matches(@"\d+"), 123456), + () => (true, Value.Not.Default, 1), + () => (true, Value.Not.Contains('Z'), "Hello World!"), + () => (true, Value.Not.Contains(123), "Hello World!"), + () => (true, Value.Not.StartsWith('W'), "Hello World!"), + () => (true, Value.Not.EndsWith('W'), "Hello World!"), // .Parenthesis - { true, Value.Not.Parenthesis(Value.Null.Or.Empty), "Hello World!" }, - { false, Value.Not.Parenthesis(Value.Null.Or.Empty), null }, - { false, Value.Not.Parenthesis(Value.Null.Or.Empty), string.Empty }, - { false, Value.Parenthesis(Value.Null.Or.Empty), "Hello World!" }, - { true, Value.Parenthesis(Value.Null.Or.Empty), null }, - { true, Value.Parenthesis(Value.Null.Or.Empty), string.Empty }, + () => (true, Value.Not.Parenthesis(Value.Null.Or.Empty), "Hello World!"), + () => (false, Value.Not.Parenthesis(Value.Null.Or.Empty), null), + () => (false, Value.Not.Parenthesis(Value.Null.Or.Empty), string.Empty), + () => (false, Value.Parenthesis(Value.Null.Or.Empty), "Hello World!"), + () => (true, Value.Parenthesis(Value.Null.Or.Empty), null), + () => (true, Value.Parenthesis(Value.Null.Or.Empty), string.Empty), // .And.Not - { true, Value.Not.Null.And.Not.Empty, "Hello World!" }, - { false, Value.Not.Null.And.Not.Empty, null }, - { false, Value.Not.Null.And.Not.Empty, string.Empty }, + () => (true, Value.Not.Null.And.Not.Empty, "Hello World!"), + () => (false, Value.Not.Null.And.Not.Empty, null), + () => (false, Value.Not.Null.And.Not.Empty, string.Empty), // .Or.Not - { false, Value.StartsWith('A').Or.Not.StartsWith('H'), "Hello World!" }, - }; + () => (false, Value.StartsWith('A').Or.Not.StartsWith('H'), "Hello World!"), + // .NotDefault + () => (false, Value.NotDefault, 0), + () => (true, Value.NotDefault, 1), + () => (true, Value.NotDefault, null), + () => (true, Value.NotDefault, "Hello World!"), + // .NotEmpty + () => (false, Value.NotEmpty, string.Empty), + () => (true, Value.NotEmpty, "Hello World!"), + () => (false, Value.NotEmpty, null), + () => (false, Value.NotEmpty, 1), + () => (false, Value.NotEmpty, new List()), + () => (true, Value.NotEmpty, new List { "Hello", "World!" }), + () => (false, Value.NotEmpty, Enumerable.Empty()), + () => (true, Value.NotEmpty, Enumerable.Range(10, 10)), + () => (true, Value.NotEmpty, Guid.NewGuid()), + () => (false, Value.NotEmpty, Guid.Empty), + // .NotNull + () => (false, Value.NotNull, null), + () => (true, Value.NotNull, "Hello World!"), + () => (true, Value.NotNull, 1), + // .Not.NotDefault + () => (true, Value.Not.NotDefault, 0), + () => (false, Value.Not.NotDefault, 1), + () => (false, Value.Not.NotDefault, null), + () => (false, Value.Not.NotDefault, "Hello World!"), + // .Not.NotEmpty + () => (true, Value.Not.NotEmpty, string.Empty), + () => (false, Value.Not.NotEmpty, "Hello World!"), + () => (true, Value.Not.NotEmpty, null), + () => (true, Value.Not.NotEmpty, 1), + () => (true, Value.Not.NotEmpty, new List()), + () => (false, Value.Not.NotEmpty, new List { "Hello", "World!" }), + () => (true, Value.Not.NotEmpty, Enumerable.Empty()), + () => (false, Value.Not.NotEmpty, Enumerable.Range(10, 10)), + // .Not.NotNull + () => (true, Value.Not.NotNull, null), + () => (false, Value.Not.NotNull, "Hello World!"), + () => (false, Value.Not.NotNull, 1), + ]; - [Theory] - [MemberData(nameof(ConstraintToStringData))] - public void ToString_Theory_Expected(string expected, IConstraint constraint) + [Test] + [MethodDataSource(nameof(ConstraintToStringData))] + public async Task ToString_Theory_Expected(string expected, IConstraint constraint) { - Assert.NotNull(expected); - Assert.NotNull(constraint); + _ = await Assert.That(expected).IsNotNull(); + constraint = await Assert.That(constraint).IsNotNull(); var result = constraint.ToString(); - Assert.Equal(expected, result); + _ = await Assert.That(result).IsEqualTo(expected); } - public static TheoryData ConstraintToStringData => - new TheoryData - { + public static Func<(string, IConstraint)>[] ConstraintToStringData() => + [ // .Contains - { "\"{Value} contains `Hello`.\"", Value.Contains("Hello") }, - { "\"{Value} contains `W`.\"", Value.Contains('W') }, - { "\"{Value} contains `2`.\"", Value.Contains(2) }, + () => ("\"{Value} contains `Hello`.\"", Value.Contains("Hello")), + () => ("\"{Value} contains `W`.\"", Value.Contains('W')), + () => ("\"{Value} contains `2`.\"", Value.Contains(2)), // .Default - { "\"{Value} is .\"", Value.Default }, + () => ("\"{Value} is .\"", Value.Default), // .Empty - { "\"{Value} is .\"", Value.Empty }, + () => ("\"{Value} is .\"", Value.Empty), // .EndsWith - { "\"{Value} ends with `World!`.\"", Value.EndsWith("World!") }, - { "\"{Value} ends with `!`.\"", Value.EndsWith('!') }, + () => ("\"{Value} ends with `World!`.\"", Value.EndsWith("World!")), + () => ("\"{Value} ends with `!`.\"", Value.EndsWith('!')), // .EqualTo - { "\"{Value} is equal to `Hello World!`.\"", Value.EqualTo("Hello World!") }, - { "\"{Value} is equal to `123456`.\"", Value.EqualTo(123456) }, + () => ("\"{Value} is equal to `Hello World!`.\"", Value.EqualTo("Hello World!")), + () => ("\"{Value} is equal to `123456`.\"", Value.EqualTo(123456)), // .Matches - { "\"{Value} matches `\\d+`.\"", Value.Matches(@"\d+") }, - { "\"{Value} matches `\\w+\\s\\w+`.\"", Value.Matches(@"\w+\s\w+") }, + () => ("\"{Value} matches `\\d+`.\"", Value.Matches(@"\d+")), + () => ("\"{Value} matches `\\w+\\s\\w+`.\"", Value.Matches(@"\w+\s\w+")), // .Null - { "\"{Value} is .\"", Value.Null }, + () => ("\"{Value} is .\"", Value.Null), // .StartsWith - { "\"{Value} starts with `Hello`.\"", Value.StartsWith("Hello") }, - { "\"{Value} starts with `H`.\"", Value.StartsWith('H') }, + () => ("\"{Value} starts with `Hello`.\"", Value.StartsWith("Hello")), + () => ("\"{Value} starts with `H`.\"", Value.StartsWith('H')), // .WhiteSpace - { "\"{Value} is .\"", Value.WhiteSpace }, + () => ("\"{Value} is .\"", Value.WhiteSpace), // .And Operators - { "\"{Value} contains `Hello` and contains `World!`.\"", Value.Contains("Hello").And.Contains("World!") }, + () => + ("\"{Value} contains `Hello` and contains `World!`.\"", Value.Contains("Hello").And.Contains("World!")), // .Or Operators - { "\"{Value} is or is .\"", Value.Null.Or.Empty }, + () => ("\"{Value} is or is .\"", Value.Null.Or.Empty), + // .Xor Operators + () => ("\"{Value} is xor is .\"", Value.Null.Xor.Empty), // .Not.Null - { "\"{Value} not is .\"", Value.Not.Null }, + () => ("\"{Value} not is .\"", Value.Not.Null), + // .Not.Empty + () => ("\"{Value} not is .\"", Value.Not.Empty), + // .Not.Contains + () => ("\"{Value} not contains `Hello`.\"", Value.Not.Contains("Hello")), + () => ("\"{Value} not contains `W`.\"", Value.Not.Contains('W')), + () => ("\"{Value} not contains `2`.\"", Value.Not.Contains(2)), + // .Parenthesis + () => ("\"{Value} not (is or is ).\"", Value.Not.Parenthesis(Value.Null.Or.Empty)), // Extreme case - { - "\"{Value} not is and (starts with `H` and ends with `!`).\"", - Value.Not.Null.And.Parenthesis(Value.StartsWith('H').And.EndsWith('!')) - }, - }; + () => + ( + "\"{Value} not is and (starts with `H` and ends with `!`).\"", + Value.Not.Null.And.Parenthesis(Value.StartsWith('H').And.EndsWith('!')) + ), + // .NotDefault + () => ("\"{Value} is not .\"", Value.NotDefault), + // .Not.NotDefault + () => ("\"{Value} not is not .\"", Value.Not.NotDefault), + // .NotEmpty + () => ("\"{Value} is not .\"", Value.NotEmpty), + // .Not.NotEmpty + () => ("\"{Value} not is not .\"", Value.Not.NotEmpty), + // .NotNull + () => ("\"{Value} is not .\"", Value.NotNull), + // .Not.NotNull + () => ("\"{Value} not is not .\"", Value.Not.NotNull), + ]; } diff --git a/tests/NetEvolve.FluentValue.Tests.Unit/NetEvolve.FluentValue.Tests.Unit.csproj b/tests/NetEvolve.FluentValue.Tests.Unit/NetEvolve.FluentValue.Tests.Unit.csproj index bfa19ce..37d80fa 100644 --- a/tests/NetEvolve.FluentValue.Tests.Unit/NetEvolve.FluentValue.Tests.Unit.csproj +++ b/tests/NetEvolve.FluentValue.Tests.Unit/NetEvolve.FluentValue.Tests.Unit.csproj @@ -1,26 +1,21 @@  + Exe $(NetEvolve_TestTargetFrameworks) - - - - - - - runtime; build; native; contentfiles; analyzers; buildtransitive - all - runtime; build; native; contentfiles; analyzers; buildtransitive all + + + - + diff --git a/tests/NetEvolve.FluentValue.Tests.Unit/TypeExtensionsTests.cs b/tests/NetEvolve.FluentValue.Tests.Unit/TypeExtensionsTests.cs new file mode 100644 index 0000000..71d5d3b --- /dev/null +++ b/tests/NetEvolve.FluentValue.Tests.Unit/TypeExtensionsTests.cs @@ -0,0 +1,29 @@ +namespace NetEvolve.FluentValue.Tests.Unit; + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +public class TypeExtensionsTests +{ + [Test] + [MethodDataSource(nameof(GetDefaultData))] + public async Task IsNullableType_ShouldReturnTrue_WhenTypeIsNullable(object? expected, Type type) + { + // Arrange & Act + var result = TypeExtensions.GetDefault(type); + // Assert + _ = await Assert.That(result).IsEqualTo(expected); + } + + public static Func<(object?, Type)>[] GetDefaultData() => + [ + () => (null, typeof(int?)), + () => (default(int), typeof(int)), + () => (null, typeof(string)), + () => (ConsoleKey.None, typeof(ConsoleKey)), + () => (null, typeof(ConsoleKey?)), + ]; +}