diff --git a/Source/aweXpect.SourceGenerators/ExpectationGenerator.cs b/Source/aweXpect.SourceGenerators/ExpectationGenerator.cs
new file mode 100644
index 000000000..d210c1d39
--- /dev/null
+++ b/Source/aweXpect.SourceGenerators/ExpectationGenerator.cs
@@ -0,0 +1,138 @@
+using System.Collections.Immutable;
+using System.Text;
+using Microsoft.CodeAnalysis;
+using Microsoft.CodeAnalysis.CSharp.Syntax;
+using Microsoft.CodeAnalysis.Text;
+
+namespace aweXpect.SourceGenerators;
+
+///
+/// The for simple expectations.
+///
+[Generator]
+public class ExpectationGenerator : IIncrementalGenerator
+{
+ void IIncrementalGenerator.Initialize(IncrementalGeneratorInitializationContext context)
+ {
+ // Add the marker attribute to the compilation
+ context.RegisterPostInitializationOutput(ctx => ctx.AddSource(
+ "CreateExpectationOnAttribute.g.cs",
+ SourceText.From(SourceGenerationHelper.CreateExpectationOnAttribute, Encoding.UTF8)));
+
+ HashSet files = new();
+ IncrementalValuesProvider expectationsToGenerate = context.SyntaxProvider
+ .CreateSyntaxProvider(
+ static (s, _) => IsSyntaxTargetForGeneration(s),
+ (ctx, _) => GetSemanticTargetForGeneration(ctx, files))
+ .Where(static m => m is not null)
+ .SelectMany((x, _) => x!.ToImmutableArray());
+
+ context.RegisterSourceOutput(expectationsToGenerate,
+ static (spc, source) => Execute(source, spc));
+ }
+
+ private static bool IsSyntaxTargetForGeneration(SyntaxNode node)
+ => node is ClassDeclarationSyntax { AttributeLists.Count: > 0, };
+
+ private static IEnumerable GetSemanticTargetForGeneration(GeneratorSyntaxContext context,
+ HashSet files)
+ {
+ // we know the node is a ClassDeclarationSyntax thanks to IsSyntaxTargetForGeneration
+ ClassDeclarationSyntax classDeclarationSyntax = (ClassDeclarationSyntax)context.Node;
+
+ SemanticModel semanticModel = context.SemanticModel;
+ if (semanticModel.GetDeclaredSymbol(classDeclarationSyntax) is not INamedTypeSymbol classSymbol)
+ {
+ yield break;
+ }
+
+ foreach (AttributeData? attributeData in classSymbol.GetAttributes())
+ {
+ INamedTypeSymbol? attributeClass = attributeData.AttributeClass;
+ if (attributeClass == null || !attributeClass.IsGenericType ||
+ attributeClass.Name != "CreateExpectationOnAttribute")
+ {
+ continue;
+ }
+
+ // Extract the target type from the generic type argument
+ INamedTypeSymbol? targetType = attributeClass.TypeArguments[0] as INamedTypeSymbol;
+ if (targetType == null)
+ {
+ continue;
+ }
+
+ ExpectationToGenerate? expectationToGenerate = GetExpectationToGenerate(classSymbol,
+ attributeData);
+ if (expectationToGenerate != null &&
+ files.Add(expectationToGenerate.Value.FileName))
+ {
+ yield return expectationToGenerate.Value;
+ }
+ }
+ }
+
+ private static void Execute(ExpectationToGenerate expectationToGenerate, SourceProductionContext context)
+ {
+ string result = SourceGenerationHelper.GenerateExtensionClass(expectationToGenerate);
+ // Create a separate partial class file for each enum
+ context.AddSource(expectationToGenerate.FileName, SourceText.From(result, Encoding.UTF8));
+ }
+
+ private static ExpectationToGenerate? GetExpectationToGenerate(INamedTypeSymbol classSymbol,
+ AttributeData attributeData)
+ {
+ string containingNamespace = classSymbol.ContainingNamespace.ToString();
+ if (containingNamespace is null)
+ {
+ return null;
+ }
+
+ INamedTypeSymbol? targetType = attributeData.AttributeClass?.TypeArguments[0] as INamedTypeSymbol;
+ if (targetType == null)
+ {
+ return null;
+ }
+
+ string? outcomeMethod = null;
+ string? name = null;
+ if (attributeData.ConstructorArguments.Length == 2)
+ {
+ name = attributeData.ConstructorArguments[0].Value?.ToString();
+ outcomeMethod = attributeData.ConstructorArguments[1].Value?.ToString();
+ }
+
+ if (outcomeMethod == null || name == null)
+ {
+ return null;
+ }
+
+ if (targetType.TypeKind == TypeKind.Error)
+ {
+ 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);
+ }
+}
diff --git a/Source/aweXpect.SourceGenerators/ExpectationToGenerate.cs b/Source/aweXpect.SourceGenerators/ExpectationToGenerate.cs
new file mode 100644
index 000000000..2a97abcf7
--- /dev/null
+++ b/Source/aweXpect.SourceGenerators/ExpectationToGenerate.cs
@@ -0,0 +1,57 @@
+using Microsoft.CodeAnalysis;
+
+namespace aweXpect.SourceGenerators;
+
+internal readonly record struct ExpectationToGenerate
+{
+ public ExpectationToGenerate(string @namespace,
+ string className,
+ INamedTypeSymbol targetType,
+ string name,
+ string outcomeMethod,
+ string expectationText,
+ string[] usings,
+ string? remarks)
+ {
+ Namespace = @namespace;
+ ClassName = className;
+ TargetType = targetType;
+ Name = name.Replace("{Not}", "");
+ NegatedName = name.Replace("{Not}", "Not");
+ IncludeNegated = name.Contains("{Not}");
+ OutcomeMethod = outcomeMethod;
+ 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 FileName { get; }
+ public bool IncludeNegated { get; }
+ public string NegatedName { get; }
+ public string Namespace { get; }
+ public string ClassName { get; }
+ public INamedTypeSymbol TargetType { get; }
+ public string Name { get; }
+ public string OutcomeMethod { get; }
+ public string ExpectationText { get; }
+ public string NegatedExpectationText { get; }
+ public string? Remarks { get; }
+
+ public string AppendRemarks()
+ {
+ if (string.IsNullOrEmpty(Remarks))
+ {
+ return "";
+ }
+
+ return $$"""
+
+ ///
+ /// {{Remarks!.Replace("\n", "\n/// ")}}
+ ///
+ """.Replace("\n", "\n\t");
+ }
+}
diff --git a/Source/aweXpect.SourceGenerators/Properties/launchSettings.json b/Source/aweXpect.SourceGenerators/Properties/launchSettings.json
new file mode 100644
index 000000000..93c85a58e
--- /dev/null
+++ b/Source/aweXpect.SourceGenerators/Properties/launchSettings.json
@@ -0,0 +1,9 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "Generators": {
+ "commandName": "DebugRoslynComponent",
+ "targetProject": "../aweXpect/aweXpect.csproj"
+ }
+ }
+}
diff --git a/Source/aweXpect.SourceGenerators/SourceGenerationHelper.cs b/Source/aweXpect.SourceGenerators/SourceGenerationHelper.cs
new file mode 100644
index 000000000..8659a9bc6
--- /dev/null
+++ b/Source/aweXpect.SourceGenerators/SourceGenerationHelper.cs
@@ -0,0 +1,107 @@
+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.SourceGenerators/aweXpect.SourceGenerators.csproj b/Source/aweXpect.SourceGenerators/aweXpect.SourceGenerators.csproj
new file mode 100644
index 000000000..b08d8e852
--- /dev/null
+++ b/Source/aweXpect.SourceGenerators/aweXpect.SourceGenerators.csproj
@@ -0,0 +1,22 @@
+
+
+
+ netstandard2.0
+ false
+ enable
+ true
+ Latest
+ true
+ false
+
+
+
+
+
+
+
+
+
+
+
diff --git a/Source/aweXpect/That/Chars/ThatChar.IsALetter.cs b/Source/aweXpect/That/Chars/ThatChar.IsALetter.cs
index 5942ce615..e8bd10988 100644
--- a/Source/aweXpect/That/Chars/ThatChar.IsALetter.cs
+++ b/Source/aweXpect/That/Chars/ThatChar.IsALetter.cs
@@ -1,48 +1,12 @@
-using aweXpect.Core;
-using aweXpect.Core.Constraints;
-using aweXpect.Helpers;
-using aweXpect.Results;
+using aweXpect.SourceGenerators;
namespace aweXpect;
-public static partial class ThatChar
-{
- ///
- /// 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.WithValue(grammars),
- IValueConstraint
- {
- public ConstraintResult IsMetBy(char actual)
- {
- Actual = actual;
- Outcome = char.IsLetter(actual) ? 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);
- }
-}
+[CreateExpectationOn("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 ThatChar;
diff --git a/Source/aweXpect/That/Chars/ThatChar.IsANumber.cs b/Source/aweXpect/That/Chars/ThatChar.IsANumber.cs
index fcece655a..c6fec3848 100644
--- a/Source/aweXpect/That/Chars/ThatChar.IsANumber.cs
+++ b/Source/aweXpect/That/Chars/ThatChar.IsANumber.cs
@@ -1,48 +1,12 @@
-using aweXpect.Core;
-using aweXpect.Core.Constraints;
-using aweXpect.Helpers;
-using aweXpect.Results;
+using aweXpect.SourceGenerators;
namespace aweXpect;
-public static partial class ThatChar
-{
- ///
- /// 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.WithValue(grammars),
- IValueConstraint
- {
- public ConstraintResult IsMetBy(char actual)
- {
- Actual = actual;
- Outcome = char.IsNumber(actual) ? 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);
- }
-}
+[CreateExpectationOn("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 ThatChar;
diff --git a/Source/aweXpect/That/Chars/ThatChar.IsAnAsciiLetter.cs b/Source/aweXpect/That/Chars/ThatChar.IsAnAsciiLetter.cs
index 0087941fc..35be2ba6d 100644
--- a/Source/aweXpect/That/Chars/ThatChar.IsAnAsciiLetter.cs
+++ b/Source/aweXpect/That/Chars/ThatChar.IsAnAsciiLetter.cs
@@ -1,57 +1,18 @@
-using aweXpect.Core;
-using aweXpect.Core.Constraints;
-using aweXpect.Helpers;
-using aweXpect.Results;
+using aweXpect.SourceGenerators;
namespace aweXpect;
-public static partial class ThatChar
-{
#if NET8_0_OR_GREATER
- ///
- /// Verifies that the subject is an ASCII letter.
- ///
- ///
- ///
- ///
+[CreateExpectationOn("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.
- ///
+[CreateExpectationOn("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.WithValue(grammars),
- IValueConstraint
- {
- public ConstraintResult IsMetBy(char actual)
- {
- Actual = actual;
-#if NET8_0_OR_GREATER
- Outcome = char.IsAsciiLetter(actual) ? 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 ThatChar;
diff --git a/Source/aweXpect/That/Chars/ThatChar.IsWhiteSpace.cs b/Source/aweXpect/That/Chars/ThatChar.IsWhiteSpace.cs
index 79ed47da4..6ca36cf46 100644
--- a/Source/aweXpect/That/Chars/ThatChar.IsWhiteSpace.cs
+++ b/Source/aweXpect/That/Chars/ThatChar.IsWhiteSpace.cs
@@ -1,48 +1,12 @@
-using aweXpect.Core;
-using aweXpect.Core.Constraints;
-using aweXpect.Helpers;
-using aweXpect.Results;
+using aweXpect.SourceGenerators;
namespace aweXpect;
-public static partial class ThatChar
-{
- ///
- /// 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.WithValue(grammars),
- IValueConstraint
- {
- public ConstraintResult IsMetBy(char actual)
- {
- Actual = actual;
- Outcome = char.IsWhiteSpace(actual) ? 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);
- }
-}
+[CreateExpectationOn("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 ThatChar;
diff --git a/Source/aweXpect/That/Guids/ThatGuid.IsEmpty.cs b/Source/aweXpect/That/Guids/ThatGuid.IsEmpty.cs
index e857fa892..7173bdd48 100644
--- a/Source/aweXpect/That/Guids/ThatGuid.IsEmpty.cs
+++ b/Source/aweXpect/That/Guids/ThatGuid.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 ThatGuid
-{
- ///
- /// 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);
- }
-}
+[CreateExpectationOn("Is{Not}Empty", "{value} == Guid.Empty",
+ ExpectationText = "is {not} empty",
+ Using = ["System"]
+)]
+public static partial class ThatGuid;
diff --git a/Source/aweXpect/aweXpect.csproj b/Source/aweXpect/aweXpect.csproj
index f0e5ada47..0b7dd50a9 100644
--- a/Source/aweXpect/aweXpect.csproj
+++ b/Source/aweXpect/aweXpect.csproj
@@ -20,6 +20,7 @@
+
diff --git a/Tests/aweXpect.Api.Tests/Expected/aweXpect_net8.0.txt b/Tests/aweXpect.Api.Tests/Expected/aweXpect_net8.0.txt
index ec282ac3d..11fc86bc9 100644
--- a/Tests/aweXpect.Api.Tests/Expected/aweXpect_net8.0.txt
+++ b/Tests/aweXpect.Api.Tests/Expected/aweXpect_net8.0.txt
@@ -193,10 +193,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 7b7c4406c..5c4e7c2f1 100644
--- a/Tests/aweXpect.Api.Tests/Expected/aweXpect_netstandard2.0.txt
+++ b/Tests/aweXpect.Api.Tests/Expected/aweXpect_netstandard2.0.txt
@@ -51,10 +51,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.IsNotALetter.Tests.cs b/Tests/aweXpect.Tests/Chars/ThatChar.IsNotALetter.Tests.cs
new file mode 100644
index 000000000..cbd7e0ed1
--- /dev/null
+++ b/Tests/aweXpect.Tests/Chars/ThatChar.IsNotALetter.Tests.cs
@@ -0,0 +1,87 @@
+namespace aweXpect.Tests;
+
+public sealed partial class ThatChar
+{
+ 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)}
+ """);
+ }
+ }
+
+ 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();
+ }
+ }
+ }
+}
diff --git a/Tests/aweXpect.Tests/Chars/ThatChar.IsNotANumber.Tests.cs b/Tests/aweXpect.Tests/Chars/ThatChar.IsNotANumber.Tests.cs
new file mode 100644
index 000000000..16e5b3c8b
--- /dev/null
+++ b/Tests/aweXpect.Tests/Chars/ThatChar.IsNotANumber.Tests.cs
@@ -0,0 +1,93 @@
+namespace aweXpect.Tests;
+
+public sealed partial class ThatChar
+{
+ public sealed class IsNotANumber
+ {
+ public sealed class Tests
+ {
+ [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)}
+ """);
+ }
+
+ [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();
+ }
+ }
+
+ public sealed class NegatedTests
+ {
+ [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();
+ }
+
+ [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)}
+ """);
+ }
+ }
+ }
+}
diff --git a/Tests/aweXpect.Tests/Chars/ThatChar.IsNotAnAsciiLetter.Tests.cs b/Tests/aweXpect.Tests/Chars/ThatChar.IsNotAnAsciiLetter.Tests.cs
new file mode 100644
index 000000000..7ec0597e2
--- /dev/null
+++ b/Tests/aweXpect.Tests/Chars/ThatChar.IsNotAnAsciiLetter.Tests.cs
@@ -0,0 +1,87 @@
+namespace aweXpect.Tests;
+
+public sealed partial class ThatChar
+{
+ 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)}
+ """);
+ }
+ }
+
+ 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();
+ }
+ }
+ }
+}
diff --git a/Tests/aweXpect.Tests/Chars/ThatChar.IsNotWhiteSpace.Tests.cs b/Tests/aweXpect.Tests/Chars/ThatChar.IsNotWhiteSpace.Tests.cs
new file mode 100644
index 000000000..74841780b
--- /dev/null
+++ b/Tests/aweXpect.Tests/Chars/ThatChar.IsNotWhiteSpace.Tests.cs
@@ -0,0 +1,99 @@
+namespace aweXpect.Tests;
+
+public sealed partial class ThatChar
+{
+ public sealed class IsNotWhiteSpace
+ {
+ public sealed class Tests
+ {
+ [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();
+ }
+
+ [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)}
+ """);
+ }
+ }
+
+ 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();
+ }
+ }
+ }
+}
diff --git a/aweXpect.sln b/aweXpect.sln
index 1d564c714..21085e6fe 100644
--- a/aweXpect.sln
+++ b/aweXpect.sln
@@ -93,6 +93,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "aweXpect.Analyzers.Tests",
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "aweXpect.Analyzers.CodeFixers", "Source\aweXpect.Analyzers.CodeFixers\aweXpect.Analyzers.CodeFixers.csproj", "{117FF28C-3628-47F1-8567-008265AE211F}"
EndProject
+Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "aweXpect.SourceGenerators", "Source\aweXpect.SourceGenerators\aweXpect.SourceGenerators.csproj", "{C4F90F26-54CD-447E-870D-6F1C05729155}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -177,6 +179,10 @@ Global
{117FF28C-3628-47F1-8567-008265AE211F}.Debug|Any CPU.Build.0 = Debug|Any CPU
{117FF28C-3628-47F1-8567-008265AE211F}.Release|Any CPU.ActiveCfg = Release|Any CPU
{117FF28C-3628-47F1-8567-008265AE211F}.Release|Any CPU.Build.0 = Release|Any CPU
+ {C4F90F26-54CD-447E-870D-6F1C05729155}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {C4F90F26-54CD-447E-870D-6F1C05729155}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {C4F90F26-54CD-447E-870D-6F1C05729155}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {C4F90F26-54CD-447E-870D-6F1C05729155}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(NestedProjects) = preSolution
{97D64B45-D97E-4A94-9EF0-37BF25310EBA} = {9CC57AD0-4984-4618-96EA-01FFFCCD84FA}