aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorivarlovlie <git@ivarlovlie.no>2022-11-19 07:52:13 +0100
committerivarlovlie <git@ivarlovlie.no>2022-11-19 07:52:13 +0100
commitf48f101eb0dd6bbdca91d5f50b4ce4194d7369ab (patch)
treed7e659690d0936bd3d08e25d4e5229d76d517a08 /src
downloaddotnet-endpoints-f48f101eb0dd6bbdca91d5f50b4ce4194d7369ab.tar.xz
dotnet-endpoints-f48f101eb0dd6bbdca91d5f50b4ce4194d7369ab.zip
feat: Initial commit
Diffstat (limited to 'src')
-rw-r--r--src/AsyncEndpoint.cs40
-rw-r--r--src/EndpointFinder.cs19
-rw-r--r--src/EndpointGenerator.cs255
-rw-r--r--src/Helpers.cs11
-rw-r--r--src/I2R.Endpoints.csproj15
-rw-r--r--src/SyncEndpoint.cs34
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