diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/AsyncEndpoint.cs | 40 | ||||
| -rw-r--r-- | src/EndpointFinder.cs | 19 | ||||
| -rw-r--r-- | src/EndpointGenerator.cs | 255 | ||||
| -rw-r--r-- | src/Helpers.cs | 11 | ||||
| -rw-r--r-- | src/I2R.Endpoints.csproj | 15 | ||||
| -rw-r--r-- | src/SyncEndpoint.cs | 34 |
6 files changed, 374 insertions, 0 deletions
diff --git a/src/AsyncEndpoint.cs b/src/AsyncEndpoint.cs new file mode 100644 index 0000000..10998ab --- /dev/null +++ b/src/AsyncEndpoint.cs @@ -0,0 +1,40 @@ +namespace I2R.Endpoints; + +public static partial class AsyncEndpoint<BaseEndpoint> +{ + public static class Req<TRequest> + { + public abstract class Res<TResponse> + { + public abstract Task<TResponse> HandleAsync( + TRequest request, + CancellationToken cancellationToken = default + ); + } + + public abstract class NoRes + { + public abstract Task HandleAsync( + TRequest request, + CancellationToken cancellationToken = default + ); + } + } + + public static class NoReq + { + public abstract class Res<TResponse> + { + public abstract Task<TResponse> HandleAsync( + CancellationToken cancellationToken = default + ); + } + + public abstract class NoRes + { + public abstract Task HandleAsync( + CancellationToken cancellationToken = default + ); + } + } +}
\ No newline at end of file diff --git a/src/EndpointFinder.cs b/src/EndpointFinder.cs new file mode 100644 index 0000000..ce1b321 --- /dev/null +++ b/src/EndpointFinder.cs @@ -0,0 +1,19 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace I2R.Endpoints; + +public class EndpointFinder : ISyntaxReceiver +{ + public HashSet<ClassDeclarationSyntax> AsyncEndpoints { get; } = new(); + public HashSet<ClassDeclarationSyntax> SyncEndpoints { get; } = new(); + + public void OnVisitSyntaxNode(SyntaxNode syntaxNode) { + if (syntaxNode is not ClassDeclarationSyntax endpoint) return; + if (endpoint.BaseList?.Types.Any(c => c.ToString().StartsWith("AsyncEndpoint")) ?? false) { + AsyncEndpoints.Add(endpoint); + } else if (endpoint.BaseList?.Types.Any(c => c.ToString().StartsWith("SyncEndpoint")) ?? false) { + SyncEndpoints.Add(endpoint); + } + } +}
\ No newline at end of file diff --git a/src/EndpointGenerator.cs b/src/EndpointGenerator.cs new file mode 100644 index 0000000..58dbff6 --- /dev/null +++ b/src/EndpointGenerator.cs @@ -0,0 +1,255 @@ +using System.Text; +using System.Text.Json; +using System.Text.RegularExpressions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace I2R.Endpoints; + +[Generator] +public class EndpointGenerator : ISourceGenerator +{ + private const string SourceGenereatedComment = "// Generated, probably smart to leave it be"; + + public void Initialize(GeneratorInitializationContext context) { + context.RegisterForSyntaxNotifications(() => new EndpointFinder()); + } + + private Diagnostic CreateDebugDiagnostic(string message) { + var descriptor = new DiagnosticDescriptor("debug", "Debug", message, "debug", DiagnosticSeverity.Warning, true); + return Diagnostic.Create(descriptor, null, ""); + } + + public class NeededInfo + { + public string BaseEndpointClassName { get; set; } + public string BaseEnpointNamespaceName { get; set; } + } + + private NeededInfo ExtractNeededInfo(ClassDeclarationSyntax classDeclarationSyntax) { + var baseType = classDeclarationSyntax.BaseList?.Types.FirstOrDefault(); + if (baseType == default) { + return default; + } + + var fullTypeName = baseType?.Type.ToString(); + var typeNamespace = GetNamespace(baseType); + var className = Regex.Match(fullTypeName, "(!?<).+?(?=>)").Value.Replace("<", ""); + return new NeededInfo() { + BaseEndpointClassName = className, + BaseEnpointNamespaceName = typeNamespace + }; + } + + public void Execute(GeneratorExecutionContext context) { + var asyncEndpoints = ((EndpointFinder) context.SyntaxReceiver)?.AsyncEndpoints; + var syncEndpoints = ((EndpointFinder) context.SyntaxReceiver)?.SyncEndpoints; + if (asyncEndpoints == null) { + context.ReportDiagnostic(CreateDebugDiagnostic("no endpoints were found")); + return; + } + + foreach (var endpoint in syncEndpoints) { + var info = ExtractNeededInfo(endpoint); + context.AddSource(info.BaseEndpointClassName + "s.g.cs", GetSyncSource(info.BaseEndpointClassName, info.BaseEnpointNamespaceName)); + } + + foreach (var endpoint in asyncEndpoints) { + var info = ExtractNeededInfo(endpoint); + context.AddSource(info.BaseEndpointClassName + "s.g.cs", GetAsyncSource(info.BaseEndpointClassName, info.BaseEnpointNamespaceName)); + } + } + + private string GetSyncSource(string className, string namespaceName) { + return $@" +namespace {namespaceName}; +public static partial class SyncEndpoint<T{className}> +{{ + public static class Req<TRequest> + {{ + public abstract class Res<TResponse> : {className} + {{ + public abstract TResponse Handle( + TRequest request + ); + }} + + public abstract class NoRes : {className} + {{ + public abstract void Handle( + TRequest request + ); + }} + }} + + public static class NoReq + {{ + public abstract class Res<TResponse> : {className} + {{ + public abstract TResponse Handle(); + }} + + public abstract class NoRes : {className} + {{ + public abstract void Handle(); + }} + }} +}} +"; + } + + private string GetAsyncSource(string className, string namespaceName) { + return $@" +namespace {namespaceName}; +public static partial class AsyncEndpoint<T> +{{ + public static class Req<TRequest> + {{ + public abstract class Res<TResponse> : {className} + {{ + public abstract Task<TResponse> HandleAsync( + TRequest request, + CancellationToken cancellationToken = default + ); + }} + + public abstract class NoRes : {className} + {{ + public abstract Task HandleAsync( + TRequest request, + CancellationToken cancellationToken = default + ); + }} + }} + + public static class NoReq + {{ + public abstract class Res<TResponse> : {className} + {{ + public abstract Task<TResponse> HandleAsync( + CancellationToken cancellationToken = default + ); + }} + + public abstract class NoRes : {className} + {{ + public abstract Task HandleAsync( + CancellationToken cancellationToken = default + ); + }} + }} +}} +"; + } + + // determine the namespace the class/enum/struct is declared in, if any + static string GetNamespace(BaseTypeSyntax syntax) { + // If we don't have a namespace at all we'll return an empty string + // This accounts for the "default namespace" case + string nameSpace = string.Empty; + + // Get the containing syntax node for the type declaration + // (could be a nested type, for example) + SyntaxNode? potentialNamespaceParent = syntax.Parent; + + // Keep moving "out" of nested classes etc until we get to a namespace + // or until we run out of parents + while (potentialNamespaceParent != null && + potentialNamespaceParent is not NamespaceDeclarationSyntax + && potentialNamespaceParent is not FileScopedNamespaceDeclarationSyntax) { + potentialNamespaceParent = potentialNamespaceParent.Parent; + } + + // Build up the final namespace by looping until we no longer have a namespace declaration + if (potentialNamespaceParent is BaseNamespaceDeclarationSyntax namespaceParent) { + // We have a namespace. Use that as the type + nameSpace = namespaceParent.Name.ToString(); + + // Keep moving "out" of the namespace declarations until we + // run out of nested namespace declarations + while (true) { + if (namespaceParent.Parent is not NamespaceDeclarationSyntax parent) { + break; + } + + // Add the outer namespace as a prefix to the final namespace + nameSpace = $"{namespaceParent.Name}.{nameSpace}"; + namespaceParent = parent; + } + } + + // return the final namespace + return nameSpace; + } +} + + +/* + public static class Req<TRequest> : BaseEndpoint + { + public abstract class Res<TResponse> : BaseEndpoint + { + public abstract Task<TResponse> HandleAsync( + TRequest request, + CancellationToken cancellationToken = default + ); + } + + public abstract class NoRes + { + public abstract Task HandleAsync( + TRequest request, + CancellationToken cancellationToken = default + ); + } + } + + public static class NoReq + { + public abstract class Res<TResponse> + { + public abstract Task<TResponse> HandleAsync( + CancellationToken cancellationToken = default + ); + } + + public abstract class NoRes + { + public abstract Task HandleAsync( + CancellationToken cancellationToken = default + ); + } + } + */ + +/* + public static class Req<TRequest> + { + public abstract class Res<TResponse> + { + public abstract TResponse Handle( + TRequest request + ); + } + + public abstract class NoRes + { + public abstract void Handle( + TRequest request + ); + } + } + + public static class NoReq + { + public abstract class Res<TResponse> + { + public abstract TResponse Handle(); + } + + public abstract class NoRes + { + public abstract void Handle(); + } + } + */
\ No newline at end of file diff --git a/src/Helpers.cs b/src/Helpers.cs new file mode 100644 index 0000000..0159ae8 --- /dev/null +++ b/src/Helpers.cs @@ -0,0 +1,11 @@ +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace I2R.Endpoints; + +public static class Helpers +{ + public static bool Inherits(this ClassDeclarationSyntax source, string name) { + return source.BaseList?.Types.Select(baseType => baseType) + .Any(baseType => baseType.ToString() == name) ?? false; + } +}
\ No newline at end of file diff --git a/src/I2R.Endpoints.csproj b/src/I2R.Endpoints.csproj new file mode 100644 index 0000000..c4d4c67 --- /dev/null +++ b/src/I2R.Endpoints.csproj @@ -0,0 +1,15 @@ +<Project Sdk="Microsoft.NET.Sdk"> + <PropertyGroup> + <TargetFramework>net7.0</TargetFramework> + <ImplicitUsings>enable</ImplicitUsings> + + </PropertyGroup> + <ItemGroup> + <FrameworkReference Include="Microsoft.AspNetCore.App" /> + <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.3"> + <PrivateAssets>all</PrivateAssets> + <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> + </PackageReference> + <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.4.0" /> + </ItemGroup> +</Project> diff --git a/src/SyncEndpoint.cs b/src/SyncEndpoint.cs new file mode 100644 index 0000000..fc82288 --- /dev/null +++ b/src/SyncEndpoint.cs @@ -0,0 +1,34 @@ +namespace I2R.Endpoints; + +public static partial class SyncEndpoint<BaseEndpoint> +{ + public static class Req<TRequest> + { + public abstract class Res<TResponse> + { + public abstract TResponse Handle( + TRequest request + ); + } + + public abstract class NoRes + { + public abstract void Handle( + TRequest request + ); + } + } + + public static class NoReq + { + public abstract class Res<TResponse> + { + public abstract TResponse Handle(); + } + + public abstract class NoRes + { + public abstract void Handle(); + } + } +}
\ No newline at end of file |
