diff --git a/Source/aweXpect.SourceGenerators/ExpectationGenerator.cs b/Source/aweXpect.SourceGenerators/ExpectationGenerator.cs index d210c1d39..54c224873 100644 --- a/Source/aweXpect.SourceGenerators/ExpectationGenerator.cs +++ b/Source/aweXpect.SourceGenerators/ExpectationGenerator.cs @@ -1,5 +1,6 @@ using System.Collections.Immutable; using System.Text; +using aweXpect.SourceGenerators.Helpers; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.CSharp.Syntax; using Microsoft.CodeAnalysis.Text; @@ -12,12 +13,21 @@ namespace aweXpect.SourceGenerators; [Generator] public class ExpectationGenerator : IIncrementalGenerator { + private static readonly string[] _supportedAttributes = + [ + nameof(SourceGenerationHelper.CreateExpectationOnAttribute), + nameof(SourceGenerationHelper.CreateExpectationOnNullableAttribute), + ]; + void IIncrementalGenerator.Initialize(IncrementalGeneratorInitializationContext context) { - // Add the marker attribute to the compilation + // Add the marker attributes to the compilation context.RegisterPostInitializationOutput(ctx => ctx.AddSource( "CreateExpectationOnAttribute.g.cs", SourceText.From(SourceGenerationHelper.CreateExpectationOnAttribute, Encoding.UTF8))); + context.RegisterPostInitializationOutput(ctx => ctx.AddSource( + "CreateExpectationOnNullableAttribute.g.cs", + SourceText.From(SourceGenerationHelper.CreateExpectationOnNullableAttribute, Encoding.UTF8))); HashSet files = new(); IncrementalValuesProvider expectationsToGenerate = context.SyntaxProvider @@ -50,7 +60,7 @@ private static IEnumerable GetSemanticTargetForGeneration { INamedTypeSymbol? attributeClass = attributeData.AttributeClass; if (attributeClass == null || !attributeClass.IsGenericType || - attributeClass.Name != "CreateExpectationOnAttribute") + !_supportedAttributes.Contains(attributeClass.Name)) { continue; } @@ -62,8 +72,7 @@ private static IEnumerable GetSemanticTargetForGeneration continue; } - ExpectationToGenerate? expectationToGenerate = GetExpectationToGenerate(classSymbol, - attributeData); + ExpectationToGenerate? expectationToGenerate = GetExpectationToGenerate(classSymbol, attributeData); if (expectationToGenerate != null && files.Add(expectationToGenerate.Value.FileName)) { @@ -112,27 +121,12 @@ private static void Execute(ExpectationToGenerate expectationToGenerate, SourceP return null; } - string expectationText = outcomeMethod; - string? remarks = null; - string[] usings = []; - foreach (KeyValuePair namedArgument in attributeData.NamedArguments) - { - switch (namedArgument.Key) - { - case "ExpectationText": - expectationText = namedArgument.Value.Value?.ToString() ?? expectationText; - break; - case "Remarks": - remarks = namedArgument.Value.Value?.ToString(); - break; - case "Using": - usings = - namedArgument.Value.Values.Select(x => x.Value?.ToString()).Where(x => x != null).ToArray()!; - break; - } - } - - return new ExpectationToGenerate(containingNamespace, classSymbol.Name, targetType, name, outcomeMethod, - expectationText, usings, remarks); + return new ExpectationToGenerate( + containingNamespace, + classSymbol.Name, + targetType, + name, + outcomeMethod, + attributeData); } } diff --git a/Source/aweXpect.SourceGenerators/ExpectationToGenerate.cs b/Source/aweXpect.SourceGenerators/Helpers/ExpectationToGenerate.cs similarity index 57% rename from Source/aweXpect.SourceGenerators/ExpectationToGenerate.cs rename to Source/aweXpect.SourceGenerators/Helpers/ExpectationToGenerate.cs index 2a97abcf7..b591342fc 100644 --- a/Source/aweXpect.SourceGenerators/ExpectationToGenerate.cs +++ b/Source/aweXpect.SourceGenerators/Helpers/ExpectationToGenerate.cs @@ -1,6 +1,6 @@ using Microsoft.CodeAnalysis; -namespace aweXpect.SourceGenerators; +namespace aweXpect.SourceGenerators.Helpers; internal readonly record struct ExpectationToGenerate { @@ -9,31 +9,54 @@ public ExpectationToGenerate(string @namespace, INamedTypeSymbol targetType, string name, string outcomeMethod, - string expectationText, - string[] usings, - string? remarks) + AttributeData attributeData) { Namespace = @namespace; ClassName = className; - TargetType = targetType; + TargetType = targetType.ToDisplayString(); Name = name.Replace("{Not}", ""); NegatedName = name.Replace("{Not}", "Not"); IncludeNegated = name.Contains("{Not}"); OutcomeMethod = outcomeMethod; + + string expectationText = outcomeMethod; + foreach (KeyValuePair namedArgument in attributeData.NamedArguments) + { + switch (namedArgument.Key) + { + case "ExpectationText": + expectationText = namedArgument.Value.Value?.ToString() ?? expectationText; + break; + case "Remarks": + Remarks = namedArgument.Value.Value?.ToString(); + break; + case "Using": + Usings = + namedArgument.Value.Values.Select(x => x.Value?.ToString()).Where(x => x != null).ToArray()!; + break; + } + } + + IsNullable = attributeData.AttributeClass!.Name == + nameof(SourceGenerationHelper.CreateExpectationOnNullableAttribute); + if (IsNullable) + { + TargetType += "?"; + } + ExpectationText = expectationText.Replace("{not}", "").Replace(" ", " "); NegatedExpectationText = expectationText.Replace("{not}", " not ").Replace(" ", " "); - Remarks = remarks; - Usings = usings; FileName = $"{ClassName}.{Name}.g.cs"; } - public string[] Usings { get; } + public string[] Usings { get; } = []; public string FileName { get; } public bool IncludeNegated { get; } + public bool IsNullable { get; } public string NegatedName { get; } public string Namespace { get; } public string ClassName { get; } - public INamedTypeSymbol TargetType { get; } + public string TargetType { get; } public string Name { get; } public string OutcomeMethod { get; } public string ExpectationText { get; } diff --git a/Source/aweXpect.SourceGenerators/Helpers/SourceGenerationHelper.cs b/Source/aweXpect.SourceGenerators/Helpers/SourceGenerationHelper.cs new file mode 100644 index 000000000..c27eb08fa --- /dev/null +++ b/Source/aweXpect.SourceGenerators/Helpers/SourceGenerationHelper.cs @@ -0,0 +1,175 @@ +namespace aweXpect.SourceGenerators.Helpers; + +internal static class SourceGenerationHelper +{ + public const string CreateExpectationOnAttribute = + """ + using System; + + namespace aweXpect.SourceGenerators; + + #nullable enable + /// + /// Create an assertion on the attribute. + /// + /// The target type for the assertion + [System.AttributeUsage(System.AttributeTargets.Class, AllowMultiple = true)] + internal class CreateExpectationOnAttribute : System.Attribute + { + public CreateExpectationOnAttribute(string methodName, string name) + { + TargetType = typeof(TTarget); + MethodName = methodName; + Name = name; + } + + public Type TargetType { get; } + public string MethodName { get; } + public string Name { get; set; } + public string? ExpectationText { get; set; } + public string? Remarks { get; set; } + public string[] Using { get; set; } = []; + } + #nullable disable + """; + + public const string CreateExpectationOnNullableAttribute = + """ + using System; + + namespace aweXpect.SourceGenerators; + + #nullable enable + /// + /// Create an assertion on the attribute. + /// + /// The target type for the assertion + [System.AttributeUsage(System.AttributeTargets.Class, AllowMultiple = true)] + internal class CreateExpectationOnNullableAttribute : System.Attribute + { + public CreateExpectationOnNullableAttribute(string methodName, string name) + { + TargetType = typeof(TTarget); + MethodName = methodName; + Name = name; + } + + public Type TargetType { get; } + public string MethodName { get; } + public string Name { get; set; } + public string? ExpectationText { get; set; } + public string? Remarks { get; set; } + public string[] Using { get; set; } = []; + } + #nullable disable + """; + + public static string GenerateExtensionClass(ExpectationToGenerate expectationToGenerate) + { + string result = $$""" + {{string.Join("\n", expectationToGenerate.Usings.Select(x => $"using {x};"))}} + using aweXpect.Core; + using aweXpect.Core.Constraints; + using aweXpect.Helpers; + using aweXpect.Results; + + namespace {{expectationToGenerate.Namespace}}; + + #nullable enable + public static partial class {{expectationToGenerate.ClassName}} + { + /// + /// Verifies that the subject {{expectationToGenerate.ExpectationText}}. + /// {{expectationToGenerate.AppendRemarks()}} + public static AndOrResult<{{expectationToGenerate.TargetType}}, IThat<{{expectationToGenerate.TargetType}}>> {{expectationToGenerate.Name}}(this IThat<{{expectationToGenerate.TargetType}}> source) + => new(source.Get().ExpectationBuilder.AddConstraint((it, grammars) => + new {{expectationToGenerate.Name}}Constraint(it, grammars)), + source); + + + """; + if (expectationToGenerate.IncludeNegated) + { + result += $$""" + /// + /// Verifies that the subject {{expectationToGenerate.NegatedExpectationText}}. + /// {{expectationToGenerate.AppendRemarks()}} + public static AndOrResult<{{expectationToGenerate.TargetType}}, IThat<{{expectationToGenerate.TargetType}}>> {{expectationToGenerate.NegatedName}}(this IThat<{{expectationToGenerate.TargetType}}> source) + => new(source.Get().ExpectationBuilder.AddConstraint((it, grammars) => + new {{expectationToGenerate.Name}}Constraint(it, grammars).Invert()), + source); + + + """; + } + + if (expectationToGenerate.IsNullable) + { + result += $$""" + private sealed class {{expectationToGenerate.Name}}Constraint(string it, ExpectationGrammars grammars) + : ConstraintResult.WithNotNullValue<{{expectationToGenerate.TargetType}}>(it, grammars), + IValueConstraint<{{expectationToGenerate.TargetType}}> + { + public ConstraintResult IsMetBy({{expectationToGenerate.TargetType}} actual) + { + Actual = actual; + Outcome = actual is not null && {{expectationToGenerate.OutcomeMethod.Replace("{value}", "actual.Value")}} ? Outcome.Success : Outcome.Failure; + return this; + } + + protected override void AppendNormalExpectation(StringBuilder stringBuilder, string? indentation = null) + => stringBuilder.Append("{{expectationToGenerate.ExpectationText}}"); + + protected override void AppendNormalResult(StringBuilder stringBuilder, string? indentation = null) + { + stringBuilder.Append(It).Append(" was "); + Formatter.Format(stringBuilder, Actual); + } + + protected override void AppendNegatedExpectation(StringBuilder stringBuilder, string? indentation = null) + => stringBuilder.Append("{{expectationToGenerate.NegatedExpectationText}}"); + + protected override void AppendNegatedResult(StringBuilder stringBuilder, string? indentation = null) + => AppendNormalResult(stringBuilder, indentation); + } + """; + } + else + { + result += $$""" + private sealed class {{expectationToGenerate.Name}}Constraint(string it, ExpectationGrammars grammars) + : ConstraintResult.WithValue<{{expectationToGenerate.TargetType}}>(grammars), + IValueConstraint<{{expectationToGenerate.TargetType}}> + { + public ConstraintResult IsMetBy({{expectationToGenerate.TargetType}} actual) + { + Actual = actual; + Outcome = {{expectationToGenerate.OutcomeMethod.Replace("{value}", "actual")}} ? Outcome.Success : Outcome.Failure; + return this; + } + + protected override void AppendNormalExpectation(StringBuilder stringBuilder, string? indentation = null) + => stringBuilder.Append("{{expectationToGenerate.ExpectationText}}"); + + protected override void AppendNormalResult(StringBuilder stringBuilder, string? indentation = null) + { + stringBuilder.Append(it).Append(" was "); + Formatter.Format(stringBuilder, Actual); + } + + protected override void AppendNegatedExpectation(StringBuilder stringBuilder, string? indentation = null) + => stringBuilder.Append("{{expectationToGenerate.NegatedExpectationText}}"); + + protected override void AppendNegatedResult(StringBuilder stringBuilder, string? indentation = null) + => AppendNormalResult(stringBuilder, indentation); + } + """; + } + + result += """ + } + #nullable disable + """; + return result.TrimStart(); + } +} diff --git a/Source/aweXpect.SourceGenerators/SourceGenerationHelper.cs b/Source/aweXpect.SourceGenerators/SourceGenerationHelper.cs deleted file mode 100644 index 8659a9bc6..000000000 --- a/Source/aweXpect.SourceGenerators/SourceGenerationHelper.cs +++ /dev/null @@ -1,107 +0,0 @@ -namespace aweXpect.SourceGenerators; - -internal static class SourceGenerationHelper -{ - public const string CreateExpectationOnAttribute = - """ - using System; - - namespace aweXpect.SourceGenerators; - - #nullable enable - /// - /// Create an assertion on the attribute. - /// - /// The target type for the assertion - [System.AttributeUsage(System.AttributeTargets.Class, AllowMultiple = true)] - internal class CreateExpectationOnAttribute : System.Attribute - { - public CreateExpectationOnAttribute(string methodName, string name) - { - TargetType = typeof(TTarget); - MethodName = methodName; - Name = name; - } - - public Type TargetType { get; } - public string MethodName { get; } - public string Name { get; set; } - public string? ExpectationText { get; set; } - public string? Remarks { get; set; } - public string[] Using { get; set; } = []; - } - #nullable disable - """; - - public static string GenerateExtensionClass(ExpectationToGenerate expectationToGenerate) - { - string result = $$""" - {{string.Join("\n", expectationToGenerate.Usings.Select(x => $"using {x};"))}} - using aweXpect.Core; - using aweXpect.Core.Constraints; - using aweXpect.Helpers; - using aweXpect.Results; - - namespace {{expectationToGenerate.Namespace}}; - - #nullable enable - public static partial class {{expectationToGenerate.ClassName}} - { - /// - /// Verifies that the subject {{expectationToGenerate.ExpectationText}}. - /// {{expectationToGenerate.AppendRemarks()}} - public static AndOrResult<{{expectationToGenerate.TargetType.ToDisplayString()}}, IThat<{{expectationToGenerate.TargetType.ToDisplayString()}}>> {{expectationToGenerate.Name}}(this IThat<{{expectationToGenerate.TargetType.ToDisplayString()}}> source) - => new(source.Get().ExpectationBuilder.AddConstraint((it, grammars) => - new {{expectationToGenerate.Name}}Constraint(it, grammars)), - source); - - - """; - if (expectationToGenerate.IncludeNegated) - { - result += $$""" - /// - /// Verifies that the subject {{expectationToGenerate.NegatedExpectationText}}. - /// {{expectationToGenerate.AppendRemarks()}} - public static AndOrResult<{{expectationToGenerate.TargetType.ToDisplayString()}}, IThat<{{expectationToGenerate.TargetType.ToDisplayString()}}>> {{expectationToGenerate.NegatedName}}(this IThat<{{expectationToGenerate.TargetType.ToDisplayString()}}> source) - => new(source.Get().ExpectationBuilder.AddConstraint((it, grammars) => - new {{expectationToGenerate.Name}}Constraint(it, grammars).Invert()), - source); - - - """; - } - - result += $$""" - private sealed class {{expectationToGenerate.Name}}Constraint(string it, ExpectationGrammars grammars) - : ConstraintResult.WithValue<{{expectationToGenerate.TargetType.ToDisplayString()}}>(grammars), - IValueConstraint<{{expectationToGenerate.TargetType.ToDisplayString()}}> - { - public ConstraintResult IsMetBy({{expectationToGenerate.TargetType.ToDisplayString()}} actual) - { - Actual = actual; - Outcome = {{expectationToGenerate.OutcomeMethod.Replace("{value}", "actual")}} ? Outcome.Success : Outcome.Failure; - return this; - } - - protected override void AppendNormalExpectation(StringBuilder stringBuilder, string? indentation = null) - => stringBuilder.Append("{{expectationToGenerate.ExpectationText}}"); - - protected override void AppendNormalResult(StringBuilder stringBuilder, string? indentation = null) - { - stringBuilder.Append(it).Append(" was "); - Formatter.Format(stringBuilder, Actual); - } - - protected override void AppendNegatedExpectation(StringBuilder stringBuilder, string? indentation = null) - => stringBuilder.Append("{{expectationToGenerate.NegatedExpectationText}}"); - - protected override void AppendNegatedResult(StringBuilder stringBuilder, string? indentation = null) - => AppendNormalResult(stringBuilder, indentation); - } - } - #nullable disable - """; - return result.TrimStart(); - } -} diff --git a/Source/aweXpect/That/Chars/ThatNullableChar.IsALetter.cs b/Source/aweXpect/That/Chars/ThatNullableChar.IsALetter.cs index 9bea2dc5c..2a477bd4f 100644 --- a/Source/aweXpect/That/Chars/ThatNullableChar.IsALetter.cs +++ b/Source/aweXpect/That/Chars/ThatNullableChar.IsALetter.cs @@ -2,47 +2,15 @@ using aweXpect.Core.Constraints; using aweXpect.Helpers; using aweXpect.Results; +using aweXpect.SourceGenerators; namespace aweXpect; -public static partial class ThatNullableChar -{ - /// - /// Verifies that the subject is a letter. - /// - /// - /// This means, that the specified Unicode character is categorized as a Unicode letter.
- /// - ///
- public static AndOrResult> IsALetter(this IThat source) - => new(source.Get().ExpectationBuilder.AddConstraint((it, grammars) => - new IsALetterConstraint(it, grammars)), - source); - - private sealed class IsALetterConstraint(string it, ExpectationGrammars grammars) - : ConstraintResult.WithNotNullValue(it, grammars), - IValueConstraint - { - public ConstraintResult IsMetBy(char? actual) - { - Actual = actual; - Outcome = actual is not null && char.IsLetter(actual.Value) ? Outcome.Success : Outcome.Failure; - return this; - } - - protected override void AppendNormalExpectation(StringBuilder stringBuilder, string? indentation = null) - => stringBuilder.Append("is a letter"); - - protected override void AppendNormalResult(StringBuilder stringBuilder, string? indentation = null) - { - stringBuilder.Append(It).Append(" was "); - Formatter.Format(stringBuilder, Actual); - } - - protected override void AppendNegatedExpectation(StringBuilder stringBuilder, string? indentation = null) - => stringBuilder.Append("is not a letter"); - - protected override void AppendNegatedResult(StringBuilder stringBuilder, string? indentation = null) - => AppendNormalResult(stringBuilder, indentation); - } -} +[CreateExpectationOnNullable("Is{Not}ALetter", "char.IsLetter({value})", + ExpectationText = "is {not} a letter", + Remarks = """ + This means, that the specified Unicode character is categorized as a Unicode letter.
+ + """ +)] +public static partial class ThatNullableChar; diff --git a/Source/aweXpect/That/Chars/ThatNullableChar.IsANumber.cs b/Source/aweXpect/That/Chars/ThatNullableChar.IsANumber.cs index 8080c97fc..78687201e 100644 --- a/Source/aweXpect/That/Chars/ThatNullableChar.IsANumber.cs +++ b/Source/aweXpect/That/Chars/ThatNullableChar.IsANumber.cs @@ -2,47 +2,15 @@ using aweXpect.Core.Constraints; using aweXpect.Helpers; using aweXpect.Results; +using aweXpect.SourceGenerators; namespace aweXpect; -public static partial class ThatNullableChar -{ - /// - /// Verifies that the subject is a number. - /// - /// - /// This means, that the specified Unicode character is categorized as a number.
- /// - ///
- public static AndOrResult> IsANumber(this IThat source) - => new(source.Get().ExpectationBuilder.AddConstraint((it, grammars) => - new IsANumberConstraint(it, grammars)), - source); - - private sealed class IsANumberConstraint(string it, ExpectationGrammars grammars) - : ConstraintResult.WithNotNullValue(it, grammars), - IValueConstraint - { - public ConstraintResult IsMetBy(char? actual) - { - Actual = actual; - Outcome = actual is not null && char.IsNumber(actual.Value) ? Outcome.Success : Outcome.Failure; - return this; - } - - protected override void AppendNormalExpectation(StringBuilder stringBuilder, string? indentation = null) - => stringBuilder.Append("is a number"); - - protected override void AppendNormalResult(StringBuilder stringBuilder, string? indentation = null) - { - stringBuilder.Append(It).Append(" was "); - Formatter.Format(stringBuilder, Actual); - } - - protected override void AppendNegatedExpectation(StringBuilder stringBuilder, string? indentation = null) - => stringBuilder.Append("is not a number"); - - protected override void AppendNegatedResult(StringBuilder stringBuilder, string? indentation = null) - => AppendNormalResult(stringBuilder, indentation); - } -} +[CreateExpectationOnNullable("Is{Not}ANumber", "char.IsNumber({value})", + ExpectationText = "is {not} a number", + Remarks = """ + This means, that the specified Unicode character is categorized as a number.
+ + """ +)] +public static partial class ThatNullableChar; diff --git a/Source/aweXpect/That/Chars/ThatNullableChar.IsAnAsciiLetter.cs b/Source/aweXpect/That/Chars/ThatNullableChar.IsAnAsciiLetter.cs index c159af8a7..c11ca4364 100644 --- a/Source/aweXpect/That/Chars/ThatNullableChar.IsAnAsciiLetter.cs +++ b/Source/aweXpect/That/Chars/ThatNullableChar.IsAnAsciiLetter.cs @@ -2,56 +2,21 @@ using aweXpect.Core.Constraints; using aweXpect.Helpers; using aweXpect.Results; +using aweXpect.SourceGenerators; namespace aweXpect; -public static partial class ThatNullableChar -{ #if NET8_0_OR_GREATER - /// - /// Verifies that the subject is an ASCII letter. - /// - /// - /// - /// +[CreateExpectationOnNullable("Is{Not}AnAsciiLetter", "char.IsAsciiLetter({value})", + ExpectationText = "is {not} an ASCII letter", + Remarks = """ + This means, that the specified Unicode character is categorized as an ASCII letter.
+ + """ +)] #else - /// - /// Verifies that the subject is an ASCII letter. - /// +[CreateExpectationOnNullable("Is{Not}AnAsciiLetter", "{value} is >= 'a' and <= 'z' or >= 'A' and <= 'Z'", + ExpectationText = "is {not} an ASCII letter" +)] #endif - public static AndOrResult> IsAnAsciiLetter(this IThat source) - => new(source.Get().ExpectationBuilder.AddConstraint((it, grammars) => - new IsAnAsciiLetterConstraint(it, grammars)), - source); - - private sealed class IsAnAsciiLetterConstraint(string it, ExpectationGrammars grammars) - : ConstraintResult.WithNotNullValue(it, grammars), - IValueConstraint - { - public ConstraintResult IsMetBy(char? actual) - { - Actual = actual; -#if NET8_0_OR_GREATER - Outcome = actual is not null && char.IsAsciiLetter(actual.Value) ? Outcome.Success : Outcome.Failure; -#else - Outcome = actual is >= 'a' and <= 'z' or >= 'A' and <= 'Z' ? Outcome.Success : Outcome.Failure; -#endif - return this; - } - - protected override void AppendNormalExpectation(StringBuilder stringBuilder, string? indentation = null) - => stringBuilder.Append("is an ASCII letter"); - - protected override void AppendNormalResult(StringBuilder stringBuilder, string? indentation = null) - { - stringBuilder.Append(It).Append(" was "); - Formatter.Format(stringBuilder, Actual); - } - - protected override void AppendNegatedExpectation(StringBuilder stringBuilder, string? indentation = null) - => stringBuilder.Append("is not an ASCII letter"); - - protected override void AppendNegatedResult(StringBuilder stringBuilder, string? indentation = null) - => AppendNormalResult(stringBuilder, indentation); - } -} +public static partial class ThatNullableChar; diff --git a/Source/aweXpect/That/Chars/ThatNullableChar.IsWhiteSpace.cs b/Source/aweXpect/That/Chars/ThatNullableChar.IsWhiteSpace.cs index 3fdd7792d..f98a91c2f 100644 --- a/Source/aweXpect/That/Chars/ThatNullableChar.IsWhiteSpace.cs +++ b/Source/aweXpect/That/Chars/ThatNullableChar.IsWhiteSpace.cs @@ -2,47 +2,15 @@ using aweXpect.Core.Constraints; using aweXpect.Helpers; using aweXpect.Results; +using aweXpect.SourceGenerators; namespace aweXpect; -public static partial class ThatNullableChar -{ - /// - /// Verifies that the subject is white-space. - /// - /// - /// This means, that the specified Unicode character is categorized as white-space.
- /// - ///
- public static AndOrResult> IsWhiteSpace(this IThat source) - => new(source.Get().ExpectationBuilder.AddConstraint((it, grammars) => - new IsWhiteSpaceConstraint(it, grammars)), - source); - - private sealed class IsWhiteSpaceConstraint(string it, ExpectationGrammars grammars) - : ConstraintResult.WithNotNullValue(it, grammars), - IValueConstraint - { - public ConstraintResult IsMetBy(char? actual) - { - Actual = actual; - Outcome = actual is not null && char.IsWhiteSpace(actual.Value) ? Outcome.Success : Outcome.Failure; - return this; - } - - protected override void AppendNormalExpectation(StringBuilder stringBuilder, string? indentation = null) - => stringBuilder.Append("is white-space"); - - protected override void AppendNormalResult(StringBuilder stringBuilder, string? indentation = null) - { - stringBuilder.Append(It).Append(" was "); - Formatter.Format(stringBuilder, Actual); - } - - protected override void AppendNegatedExpectation(StringBuilder stringBuilder, string? indentation = null) - => stringBuilder.Append("is not white-space"); - - protected override void AppendNegatedResult(StringBuilder stringBuilder, string? indentation = null) - => AppendNormalResult(stringBuilder, indentation); - } -} +[CreateExpectationOnNullable("Is{Not}WhiteSpace", "char.IsWhiteSpace({value})", + ExpectationText = "is {not} white-space", + Remarks = """ + This means, that the specified Unicode character is categorized as white-space.
+ + """ +)] +public static partial class ThatNullableChar; diff --git a/Source/aweXpect/That/Guids/ThatNullableGuid.IsEmpty.cs b/Source/aweXpect/That/Guids/ThatNullableGuid.IsEmpty.cs index 361a89d58..83104fb84 100644 --- a/Source/aweXpect/That/Guids/ThatNullableGuid.IsEmpty.cs +++ b/Source/aweXpect/That/Guids/ThatNullableGuid.IsEmpty.cs @@ -1,53 +1,10 @@ using System; -using aweXpect.Core; -using aweXpect.Core.Constraints; -using aweXpect.Helpers; -using aweXpect.Results; +using aweXpect.SourceGenerators; namespace aweXpect; -public static partial class ThatNullableGuid -{ - /// - /// Verifies that the subject is empty. - /// - public static AndOrResult> IsEmpty(this IThat source) - => new(source.Get().ExpectationBuilder.AddConstraint((it, grammars) => - new IsEmptyConstraint(it, grammars)), - source); - - /// - /// Verifies that the subject is not empty. - /// - public static AndOrResult> IsNotEmpty(this IThat source) - => new(source.Get().ExpectationBuilder.AddConstraint((it, grammars) => - new IsEmptyConstraint(it, grammars).Invert()), - source); - - private sealed class IsEmptyConstraint(string it, ExpectationGrammars grammars) - : ConstraintResult.WithNotNullValue(it, grammars), - IValueConstraint - { - public ConstraintResult IsMetBy(Guid? actual) - { - Actual = actual; - Outcome = actual == Guid.Empty ? Outcome.Success : Outcome.Failure; - return this; - } - - protected override void AppendNormalExpectation(StringBuilder stringBuilder, string? indentation = null) - => stringBuilder.Append("is empty"); - - protected override void AppendNormalResult(StringBuilder stringBuilder, string? indentation = null) - { - stringBuilder.Append(It).Append(" was "); - Formatter.Format(stringBuilder, Actual); - } - - protected override void AppendNegatedExpectation(StringBuilder stringBuilder, string? indentation = null) - => stringBuilder.Append("is not empty"); - - protected override void AppendNegatedResult(StringBuilder stringBuilder, string? indentation = null) - => AppendNormalResult(stringBuilder, indentation); - } -} +[CreateExpectationOnNullable("Is{Not}Empty", "{value} == Guid.Empty", + ExpectationText = "is {not} empty", + Using = ["System",] +)] +public static partial class ThatNullableGuid; diff --git a/Tests/aweXpect.Api.Tests/Expected/aweXpect_net8.0.txt b/Tests/aweXpect.Api.Tests/Expected/aweXpect_net8.0.txt index 11fc86bc9..23f05b64d 100644 --- a/Tests/aweXpect.Api.Tests/Expected/aweXpect_net8.0.txt +++ b/Tests/aweXpect.Api.Tests/Expected/aweXpect_net8.0.txt @@ -759,10 +759,14 @@ namespace aweXpect public static aweXpect.Results.AndOrResult> IsANumber(this aweXpect.Core.IThat source) { } public static aweXpect.Results.AndOrResult> IsAnAsciiLetter(this aweXpect.Core.IThat source) { } public static aweXpect.Results.AndOrResult> IsEqualTo(this aweXpect.Core.IThat source, char? expected) { } + public static aweXpect.Results.AndOrResult> IsNotALetter(this aweXpect.Core.IThat source) { } + public static aweXpect.Results.AndOrResult> IsNotANumber(this aweXpect.Core.IThat source) { } + public static aweXpect.Results.AndOrResult> IsNotAnAsciiLetter(this aweXpect.Core.IThat source) { } public static aweXpect.Results.AndOrResult> IsNotEqualTo(this aweXpect.Core.IThat source, char? unexpected) { } public static aweXpect.Results.AndOrResult> IsNotOneOf(this aweXpect.Core.IThat source, System.Collections.Generic.IEnumerable unexpected) { } public static aweXpect.Results.AndOrResult> IsNotOneOf(this aweXpect.Core.IThat source, System.Collections.Generic.IEnumerable unexpected) { } public static aweXpect.Results.AndOrResult> IsNotOneOf(this aweXpect.Core.IThat source, params char?[] unexpected) { } + public static aweXpect.Results.AndOrResult> IsNotWhiteSpace(this aweXpect.Core.IThat source) { } public static aweXpect.Results.AndOrResult> IsOneOf(this aweXpect.Core.IThat source, System.Collections.Generic.IEnumerable expected) { } public static aweXpect.Results.AndOrResult> IsOneOf(this aweXpect.Core.IThat source, System.Collections.Generic.IEnumerable expected) { } public static aweXpect.Results.AndOrResult> IsOneOf(this aweXpect.Core.IThat source, params char?[] expected) { } diff --git a/Tests/aweXpect.Api.Tests/Expected/aweXpect_netstandard2.0.txt b/Tests/aweXpect.Api.Tests/Expected/aweXpect_netstandard2.0.txt index 5c4e7c2f1..341c17493 100644 --- a/Tests/aweXpect.Api.Tests/Expected/aweXpect_netstandard2.0.txt +++ b/Tests/aweXpect.Api.Tests/Expected/aweXpect_netstandard2.0.txt @@ -522,10 +522,14 @@ namespace aweXpect public static aweXpect.Results.AndOrResult> IsANumber(this aweXpect.Core.IThat source) { } public static aweXpect.Results.AndOrResult> IsAnAsciiLetter(this aweXpect.Core.IThat source) { } public static aweXpect.Results.AndOrResult> IsEqualTo(this aweXpect.Core.IThat source, char? expected) { } + public static aweXpect.Results.AndOrResult> IsNotALetter(this aweXpect.Core.IThat source) { } + public static aweXpect.Results.AndOrResult> IsNotANumber(this aweXpect.Core.IThat source) { } + public static aweXpect.Results.AndOrResult> IsNotAnAsciiLetter(this aweXpect.Core.IThat source) { } public static aweXpect.Results.AndOrResult> IsNotEqualTo(this aweXpect.Core.IThat source, char? unexpected) { } public static aweXpect.Results.AndOrResult> IsNotOneOf(this aweXpect.Core.IThat source, System.Collections.Generic.IEnumerable unexpected) { } public static aweXpect.Results.AndOrResult> IsNotOneOf(this aweXpect.Core.IThat source, System.Collections.Generic.IEnumerable unexpected) { } public static aweXpect.Results.AndOrResult> IsNotOneOf(this aweXpect.Core.IThat source, params char?[] unexpected) { } + public static aweXpect.Results.AndOrResult> IsNotWhiteSpace(this aweXpect.Core.IThat source) { } public static aweXpect.Results.AndOrResult> IsOneOf(this aweXpect.Core.IThat source, System.Collections.Generic.IEnumerable expected) { } public static aweXpect.Results.AndOrResult> IsOneOf(this aweXpect.Core.IThat source, System.Collections.Generic.IEnumerable expected) { } public static aweXpect.Results.AndOrResult> IsOneOf(this aweXpect.Core.IThat source, params char?[] expected) { } diff --git a/Tests/aweXpect.Tests/Chars/ThatChar.Nullable.IsNotALetter.Tests.cs b/Tests/aweXpect.Tests/Chars/ThatChar.Nullable.IsNotALetter.Tests.cs new file mode 100644 index 000000000..23f894ef3 --- /dev/null +++ b/Tests/aweXpect.Tests/Chars/ThatChar.Nullable.IsNotALetter.Tests.cs @@ -0,0 +1,122 @@ +namespace aweXpect.Tests; + +public sealed partial class ThatChar +{ + public sealed partial class Nullable + { + public sealed class IsNotALetter + { + public sealed class Tests + { + [Theory] + [InlineData('\t')] + [InlineData('5')] + [InlineData('@')] + [InlineData('[')] + [InlineData(']')] + [InlineData('{')] + [InlineData('}')] + public async Task WhenSubjectIsNoLetter_ShouldSucceed(char? subject) + { + async Task Act() + => await That(subject).IsNotALetter(); + + await That(Act).DoesNotThrow(); + } + + [Theory] + [InlineData('a')] + [InlineData('d')] + [InlineData('z')] + [InlineData('A')] + [InlineData('M')] + [InlineData('Z')] + [InlineData('\u4E50')] + public async Task WhenSubjectIsNotALetter_ShouldFail(char? subject) + { + async Task Act() + => await That(subject).IsNotALetter(); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject + is not a letter, + but it was {Formatter.Format(subject)} + """); + } + + [Fact] + public async Task WhenSubjectIsNull_ShouldFail() + { + char? subject = null; + + async Task Act() + => await That(subject).IsNotALetter(); + + await That(Act).Throws() + .WithMessage(""" + Expected that subject + is not a letter, + but it was + """); + } + } + + public sealed class NegatedTests + { + [Theory] + [InlineData('\t')] + [InlineData('5')] + [InlineData('@')] + [InlineData('[')] + [InlineData(']')] + [InlineData('{')] + [InlineData('}')] + public async Task WhenSubjectIsNoLetter_ShouldFail(char? subject) + { + async Task Act() + => await That(subject).DoesNotComplyWith(it => it.IsNotALetter()); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject + is a letter, + but it was {Formatter.Format(subject)} + """); + } + + [Theory] + [InlineData('a')] + [InlineData('d')] + [InlineData('z')] + [InlineData('A')] + [InlineData('M')] + [InlineData('Z')] + [InlineData('\u4E50')] + public async Task WhenSubjectIsNotALetter_ShouldSucceed(char? subject) + { + async Task Act() + => await That(subject).DoesNotComplyWith(it => it.IsNotALetter()); + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenSubjectIsNull_ShouldFail() + { + char? subject = null; + + async Task Act() + => await That(subject).DoesNotComplyWith(it => it.IsNotALetter()); + + await That(Act).Throws() + .WithMessage(""" + Expected that subject + is a letter, + but it was + """); + } + } + } + } +} diff --git a/Tests/aweXpect.Tests/Chars/ThatChar.Nullable.IsNotANumber.Tests.cs b/Tests/aweXpect.Tests/Chars/ThatChar.Nullable.IsNotANumber.Tests.cs new file mode 100644 index 000000000..634288bb1 --- /dev/null +++ b/Tests/aweXpect.Tests/Chars/ThatChar.Nullable.IsNotANumber.Tests.cs @@ -0,0 +1,128 @@ +namespace aweXpect.Tests; + +public sealed partial class ThatChar +{ + public sealed partial class Nullable + { + public sealed class IsNotANumber + { + public sealed class Tests + { + [Theory] + [InlineData('a')] + [InlineData('d')] + [InlineData('z')] + [InlineData('A')] + [InlineData('M')] + [InlineData('Z')] + [InlineData('\u4E50')] + [InlineData('\t')] + [InlineData('@')] + [InlineData('[')] + [InlineData(']')] + [InlineData('{')] + [InlineData('}')] + public async Task WhenSubjectIsNoLetter_ShouldSucceed(char? subject) + { + async Task Act() + => await That(subject).IsNotANumber(); + + await That(Act).DoesNotThrow(); + } + + [Theory] + [InlineData('0')] + [InlineData('1')] + [InlineData('4')] + [InlineData('9')] + public async Task WhenSubjectIsNotANumber_ShouldFail(char? subject) + { + async Task Act() + => await That(subject).IsNotANumber(); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject + is not a number, + but it was {Formatter.Format(subject)} + """); + } + + [Fact] + public async Task WhenSubjectIsNull_ShouldFail() + { + char? subject = null; + + async Task Act() + => await That(subject).IsNotANumber(); + + await That(Act).Throws() + .WithMessage(""" + Expected that subject + is not a number, + but it was + """); + } + } + + public sealed class NegatedTests + { + [Theory] + [InlineData('a')] + [InlineData('d')] + [InlineData('z')] + [InlineData('A')] + [InlineData('M')] + [InlineData('Z')] + [InlineData('\u4E50')] + [InlineData('\t')] + [InlineData('@')] + [InlineData('[')] + [InlineData(']')] + [InlineData('{')] + [InlineData('}')] + public async Task WhenSubjectIsNoLetter_ShouldFail(char? subject) + { + async Task Act() + => await That(subject).DoesNotComplyWith(it => it.IsNotANumber()); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject + is a number, + but it was {Formatter.Format(subject)} + """); + } + + [Theory] + [InlineData('0')] + [InlineData('1')] + [InlineData('4')] + [InlineData('9')] + public async Task WhenSubjectIsNotANumber_ShouldSucceed(char? subject) + { + async Task Act() + => await That(subject).DoesNotComplyWith(it => it.IsNotANumber()); + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenSubjectIsNull_ShouldFail() + { + char? subject = null; + + async Task Act() + => await That(subject).DoesNotComplyWith(it => it.IsNotANumber()); + + await That(Act).Throws() + .WithMessage(""" + Expected that subject + is a number, + but it was + """); + } + } + } + } +} diff --git a/Tests/aweXpect.Tests/Chars/ThatChar.Nullable.IsNotAnAsciiLetter.Tests.cs b/Tests/aweXpect.Tests/Chars/ThatChar.Nullable.IsNotAnAsciiLetter.Tests.cs new file mode 100644 index 000000000..e0344c458 --- /dev/null +++ b/Tests/aweXpect.Tests/Chars/ThatChar.Nullable.IsNotAnAsciiLetter.Tests.cs @@ -0,0 +1,122 @@ +namespace aweXpect.Tests; + +public sealed partial class ThatChar +{ + public sealed partial class Nullable + { + public sealed class IsNotAnAsciiLetter + { + public sealed class Tests + { + [Theory] + [InlineData('\t')] + [InlineData('5')] + [InlineData('@')] + [InlineData('[')] + [InlineData(']')] + [InlineData('{')] + [InlineData('}')] + [InlineData('\u4E50')] + public async Task WhenSubjectIsNoAsciiLetter_ShouldSucceed(char? subject) + { + async Task Act() + => await That(subject).IsNotAnAsciiLetter(); + + await That(Act).DoesNotThrow(); + } + + [Theory] + [InlineData('a')] + [InlineData('d')] + [InlineData('z')] + [InlineData('A')] + [InlineData('M')] + [InlineData('Z')] + public async Task WhenSubjectIsNotAnAsciiLetter_ShouldFail(char? subject) + { + async Task Act() + => await That(subject).IsNotAnAsciiLetter(); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject + is not an ASCII letter, + but it was {Formatter.Format(subject)} + """); + } + + [Fact] + public async Task WhenSubjectIsNull_ShouldFail() + { + char? subject = null; + + async Task Act() + => await That(subject).IsNotAnAsciiLetter(); + + await That(Act).Throws() + .WithMessage(""" + Expected that subject + is not an ASCII letter, + but it was + """); + } + } + + public sealed class NegatedTests + { + [Theory] + [InlineData('\t')] + [InlineData('5')] + [InlineData('@')] + [InlineData('[')] + [InlineData(']')] + [InlineData('{')] + [InlineData('}')] + [InlineData('\u4E50')] + public async Task WhenSubjectIsNoAsciiLetter_ShouldFail(char? subject) + { + async Task Act() + => await That(subject).DoesNotComplyWith(it => it.IsNotAnAsciiLetter()); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject + is an ASCII letter, + but it was {Formatter.Format(subject)} + """); + } + + [Theory] + [InlineData('a')] + [InlineData('d')] + [InlineData('z')] + [InlineData('A')] + [InlineData('M')] + [InlineData('Z')] + public async Task WhenSubjectIsNotAnAsciiLetter_ShouldSucceed(char? subject) + { + async Task Act() + => await That(subject).DoesNotComplyWith(it => it.IsNotAnAsciiLetter()); + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenSubjectIsNull_ShouldFail() + { + char? subject = null; + + async Task Act() + => await That(subject).DoesNotComplyWith(it => it.IsNotAnAsciiLetter()); + + await That(Act).Throws() + .WithMessage(""" + Expected that subject + is an ASCII letter, + but it was + """); + } + } + } + } +} diff --git a/Tests/aweXpect.Tests/Chars/ThatChar.Nullable.IsNotWhiteSpace.Tests.cs b/Tests/aweXpect.Tests/Chars/ThatChar.Nullable.IsNotWhiteSpace.Tests.cs new file mode 100644 index 000000000..56ad37081 --- /dev/null +++ b/Tests/aweXpect.Tests/Chars/ThatChar.Nullable.IsNotWhiteSpace.Tests.cs @@ -0,0 +1,134 @@ +namespace aweXpect.Tests; + +public sealed partial class ThatChar +{ + public sealed partial class Nullable + { + public sealed class IsNotWhiteSpace + { + public sealed class Tests + { + [Theory] + [InlineData(' ')] + [InlineData('\t')] + [InlineData('\r')] + [InlineData('\n')] + public async Task WhenSubjectIsNotWhiteSpace_ShouldFail(char? subject) + { + async Task Act() + => await That(subject).IsNotWhiteSpace(); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject + is not white-space, + but it was {Formatter.Format(subject)} + """); + } + + [Theory] + [InlineData('0')] + [InlineData('1')] + [InlineData('4')] + [InlineData('9')] + [InlineData('a')] + [InlineData('d')] + [InlineData('z')] + [InlineData('A')] + [InlineData('M')] + [InlineData('Z')] + [InlineData('\u4E50')] + [InlineData('@')] + [InlineData('[')] + [InlineData(']')] + [InlineData('{')] + [InlineData('}')] + public async Task WhenSubjectIsNotWhiteSpace_ShouldSucceed(char? subject) + { + async Task Act() + => await That(subject).IsNotWhiteSpace(); + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenSubjectIsNull_ShouldFail() + { + char? subject = null; + + async Task Act() + => await That(subject).IsNotWhiteSpace(); + + await That(Act).Throws() + .WithMessage(""" + Expected that subject + is not white-space, + but it was + """); + } + } + + public sealed class NegatedTests + { + [Theory] + [InlineData('0')] + [InlineData('1')] + [InlineData('4')] + [InlineData('9')] + [InlineData('a')] + [InlineData('d')] + [InlineData('z')] + [InlineData('A')] + [InlineData('M')] + [InlineData('Z')] + [InlineData('\u4E50')] + [InlineData('@')] + [InlineData('[')] + [InlineData(']')] + [InlineData('{')] + [InlineData('}')] + public async Task WhenSubjectIsNotWhiteSpace_ShouldFail(char? subject) + { + async Task Act() + => await That(subject).DoesNotComplyWith(it => it.IsNotWhiteSpace()); + + await That(Act).Throws() + .WithMessage($""" + Expected that subject + is white-space, + but it was {Formatter.Format(subject)} + """); + } + + [Theory] + [InlineData(' ')] + [InlineData('\t')] + [InlineData('\r')] + [InlineData('\n')] + public async Task WhenSubjectIsNotWhiteSpace_ShouldSucceed(char? subject) + { + async Task Act() + => await That(subject).DoesNotComplyWith(it => it.IsNotWhiteSpace()); + + await That(Act).DoesNotThrow(); + } + + [Fact] + public async Task WhenSubjectIsNull_ShouldFail() + { + char? subject = null; + + async Task Act() + => await That(subject).DoesNotComplyWith(it => it.IsNotWhiteSpace()); + + await That(Act).Throws() + .WithMessage(""" + Expected that subject + is white-space, + but it was + """); + } + } + } + } +}