diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 22814bb..68d94ad 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -11,7 +11,7 @@ true true true - 1.1.0 + 1.1.1 .NotEmpty<T>() test extension false kasthack nunit xunit mstest test empty null notempty emptinness nullability diff --git a/src/kasthack.NotEmpty.Core/AssertContext.cs b/src/kasthack.NotEmpty.Core/AssertContext.cs index 105b5f1..5bd17a5 100644 --- a/src/kasthack.NotEmpty.Core/AssertContext.cs +++ b/src/kasthack.NotEmpty.Core/AssertContext.cs @@ -17,25 +17,25 @@ internal class AssertContext public string Path => "(value)" + string.Concat(this.pathSegments.Reverse()); - public bool IsArrayElement { get; set; } = false; + public ElementKind ElementKind { get; set; } = Core.ElementKind.Root; public AssertContext(AssertOptions options) => this.Options = options; - public IDisposable EnterPath(string segment, bool isArray) => new PathContext(this, segment, isArray); + public IDisposable EnterPath(string segment, ElementKind elementKind) => new PathContext(this, segment, elementKind); private struct PathContext : IDisposable { private readonly AssertContext context; - private readonly bool originalIsArrayElement; + private readonly ElementKind originalElementKind; private bool disposed = false; - public PathContext(AssertContext context, string segment, bool isArray) + public PathContext(AssertContext context, string segment, ElementKind elementKind) { this.context = context ?? throw new ArgumentNullException(nameof(context)); - this.originalIsArrayElement = this.context.IsArrayElement; + this.originalElementKind = this.context.ElementKind; this.context.pathSegments.Push(segment); - this.context.IsArrayElement = isArray; + this.context.ElementKind = elementKind; } public void Dispose() @@ -44,7 +44,7 @@ public void Dispose() { this.disposed = true; this.context.pathSegments.Pop(); - this.context.IsArrayElement = this.originalIsArrayElement; + this.context.ElementKind = this.originalElementKind; } } } diff --git a/src/kasthack.NotEmpty.Core/AssertOptions.cs b/src/kasthack.NotEmpty.Core/AssertOptions.cs index 6ffc505..d4d2d55 100644 --- a/src/kasthack.NotEmpty.Core/AssertOptions.cs +++ b/src/kasthack.NotEmpty.Core/AssertOptions.cs @@ -23,5 +23,10 @@ public class AssertOptions /// Allows empty strings but not nulls. /// public bool AllowEmptyCollections { get; set; } = false; + + /// + /// Allows bool properties to be false. + /// + public bool AllowFalseBooleanProperties { get; set; } = false; } } \ No newline at end of file diff --git a/src/kasthack.NotEmpty.Core/ElementKind.cs b/src/kasthack.NotEmpty.Core/ElementKind.cs new file mode 100644 index 0000000000..8026d9b --- /dev/null +++ b/src/kasthack.NotEmpty.Core/ElementKind.cs @@ -0,0 +1,11 @@ +namespace kasthack.NotEmpty.Core +{ + internal enum ElementKind + { + Unknown, + Root, + Property, + ArrayElement, + DictionaryElement, + } +} \ No newline at end of file diff --git a/src/kasthack.NotEmpty.Core/NotEmptyExtensionsBase.cs b/src/kasthack.NotEmpty.Core/NotEmptyExtensionsBase.cs index 904ea95..79a9ff6 100644 --- a/src/kasthack.NotEmpty.Core/NotEmptyExtensionsBase.cs +++ b/src/kasthack.NotEmpty.Core/NotEmptyExtensionsBase.cs @@ -31,14 +31,19 @@ internal void NotEmptyInternal(T? value, AssertContext context) var path = context.Path; string message = GetEmptyMessage(path); this.Assert(value is not null, message, path); // fast lane - if ( - !(context.Options.AllowZerosInNumberArrays && - context.IsArrayElement && - value is byte or sbyte or short or ushort or char or int or uint or long or ulong or float or double or decimal or BigInteger + var skipDueToBeingNumberInArrayWhenAllowedByOptions = context.ElementKind == ElementKind.ArrayElement + && context.Options.AllowZerosInNumberArrays + && value is byte or sbyte or short or ushort or char or int or uint or long or ulong or float or double or decimal or BigInteger #if NET5_0_OR_GREATER or Half or nint or nuint #endif - )) + ; + var skipDueToBeingBooleanPropertyWhenAllowedByOptions = context.ElementKind == ElementKind.Property && value is bool; + if (!( + skipDueToBeingBooleanPropertyWhenAllowedByOptions + || + skipDueToBeingNumberInArrayWhenAllowedByOptions + )) { this.Assert(!EqualityComparer.Default.Equals(default!, value!), message, path); } @@ -68,7 +73,7 @@ or TimeOnly _ foreach (var key in d.Keys) { cnt++; - using (context.EnterPath($"[{key}]", false)) + using (context.EnterPath($"[{key}]", ElementKind.DictionaryElement)) { this.NotEmptyBoxed(d[key], context); } @@ -84,7 +89,7 @@ or TimeOnly _ var index = 0; foreach (var item in e) { - using (context.EnterPath($"[{index++}]", true)) + using (context.EnterPath($"[{index++}]", ElementKind.ArrayElement)) { this.NotEmptyBoxed(item, context); } @@ -99,7 +104,7 @@ or TimeOnly _ default: foreach (var pathValue in CachedPropertyExtractor.GetProperties(value)) { - using (context.EnterPath($".{pathValue.Path}", false)) + using (context.EnterPath($".{pathValue.Path}", ElementKind.Property)) { this.NotEmptyBoxed(pathValue.Value, context); } diff --git a/src/kasthack.NotEmpty.Tests/NotEmptyTestBase.cs b/src/kasthack.NotEmpty.Tests/NotEmptyTestBase.cs index 9717abb..343c860 100644 --- a/src/kasthack.NotEmpty.Tests/NotEmptyTestBase.cs +++ b/src/kasthack.NotEmpty.Tests/NotEmptyTestBase.cs @@ -32,6 +32,7 @@ public abstract class NotEmptyTestBase #region Numbers + [Fact] public void NotDefaultPrimitiveWorks() => this.Action(new { Value = 1 }); @@ -50,6 +51,9 @@ public abstract class NotEmptyTestBase [Fact] public void BoxedDefaultPrimitiveThrows() => Assert.ThrowsAny(() => this.Action(new { Value = (object)0 })); + [Fact] + public void BoxedDefaultPrimitiveThrowsAsRoot() => Assert.ThrowsAny(() => this.Action(0)); + [Fact] public void NullNullableThrows() => Assert.ThrowsAny(() => this.Action(new { Value = new Nullable(), })); @@ -158,6 +162,24 @@ public struct InfiniteNestedStruct } #endregion + #region Booleans + + [Fact] + public void FalseThrows() => Assert.ThrowsAny(() => this.Action(false)); + + [Fact] + public void TrueWorks() => this.Action(true); + + [Fact] + public void FalseDoesntThrowWhenAllowed() => this.Action(new { Value = true }, new AssertOptions { AllowFalseBooleanProperties = true }); + + [Fact] + public void FalseThrowsWhenAllowedForDifferentKind() => Assert.ThrowsAny(() => this.Action(false, new AssertOptions { AllowFalseBooleanProperties = true })); + + [Fact] + public void FalseThrowsWhenAllowedForDifferentKindV2() => Assert.ThrowsAny(() => this.Action(new[] { false }, new AssertOptions { AllowFalseBooleanProperties = true })); + #endregion + protected void Action(object? value, AssertOptions? options = null) => this.action(value, options); } } \ No newline at end of file