diff options
| -rw-r--r-- | I2R.Endpoints.sln | 4 | ||||
| -rw-r--r-- | code/lib/AsyncEndpoint.cs (renamed from src/AsyncEndpoint.cs) | 2 | ||||
| -rw-r--r-- | code/lib/I2R.Endpoints.csproj (renamed from src/I2R.Endpoints.csproj) | 20 | ||||
| -rw-r--r-- | code/lib/SyncEndpoint.cs (renamed from src/SyncEndpoint.cs) | 2 | ||||
| -rw-r--r-- | code/source-generator/EndpointFinder.cs | 17 | ||||
| -rw-r--r-- | code/source-generator/EndpointGenerator.cs | 146 | ||||
| -rw-r--r-- | code/source-generator/I2R.Endpoints.Generator.csproj | 18 | ||||
| -rw-r--r-- | src/EndpointFinder.cs | 19 | ||||
| -rw-r--r-- | src/EndpointGenerator.cs | 184 | ||||
| -rw-r--r-- | src/Helpers.cs | 11 |
10 files changed, 198 insertions, 225 deletions
diff --git a/I2R.Endpoints.sln b/I2R.Endpoints.sln index 9eeafd4..f7f4a63 100644 --- a/I2R.Endpoints.sln +++ b/I2R.Endpoints.sln @@ -3,7 +3,9 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 17 VisualStudioVersion = 17.0.31903.59 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "I2R.Endpoints", "src\I2R.Endpoints.csproj", "{7A6F787E-4FB2-42BB-B143-60D9399AB4BB}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "I2R.Endpoints", "code\lib\I2R.Endpoints.csproj", "{7A6F787E-4FB2-42BB-B143-60D9399AB4BB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "I2R.Endpoints.Generator", "code\source-generator\I2R.Endpoints.Generator.csproj", "{7A6F787E-4FB2-42BB-B143-60D9399AB4BB}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution diff --git a/src/AsyncEndpoint.cs b/code/lib/AsyncEndpoint.cs index 10998ab..505032c 100644 --- a/src/AsyncEndpoint.cs +++ b/code/lib/AsyncEndpoint.cs @@ -1,6 +1,6 @@ namespace I2R.Endpoints; -public static partial class AsyncEndpoint<BaseEndpoint> +public static partial class AsyncEndpoint<TBaseEndpoint> { public static class Req<TRequest> { diff --git a/src/I2R.Endpoints.csproj b/code/lib/I2R.Endpoints.csproj index 4948bed..90ecd90 100644 --- a/src/I2R.Endpoints.csproj +++ b/code/lib/I2R.Endpoints.csproj @@ -1,12 +1,12 @@ <Project Sdk="Microsoft.NET.Sdk"> + <PropertyGroup> <TargetFramework>net7.0</TargetFramework> <ImplicitUsings>enable</ImplicitUsings> - <PackageProjectUrl>https://git.ivar.systems/dotnet-endpoints</PackageProjectUrl> <PackageLicenseUrl>https://git.ivar.systems/dotnet-endpoints/tree/COPYING</PackageLicenseUrl> <RepositoryUrl>https://git.ivar.systems/dotnet-endpoints</RepositoryUrl> <RepositoryType>git</RepositoryType> - <PackageVersion>1.0.0</PackageVersion> + <PackageVersion>1.1.0</PackageVersion> <Title>I2R.Endpoints</Title> <Authors>Ivar Løvlie</Authors> <Description>A library that enables single file endpoints (or whatever).</Description> @@ -14,13 +14,17 @@ <PackageReleaseNotes> Initial realease. </PackageReleaseNotes> + <IsPackable>true</IsPackable> </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" /> + <ProjectReference Include="..\source-generator\I2R.Endpoints.Generator.csproj" + OutputItemType="Analyzer" + ReferenceOutputAssembly="false"/> </ItemGroup> + + <ItemGroup> + <AnalyzerReference Include="..\source-generator\I2R.Endpoints.Generator.csproj"/> + </ItemGroup> + </Project> diff --git a/src/SyncEndpoint.cs b/code/lib/SyncEndpoint.cs index fc82288..f30923a 100644 --- a/src/SyncEndpoint.cs +++ b/code/lib/SyncEndpoint.cs @@ -1,6 +1,6 @@ namespace I2R.Endpoints; -public static partial class SyncEndpoint<BaseEndpoint> +public static partial class SyncEndpoint<TBaseEndpoint> { public static class Req<TRequest> { diff --git a/code/source-generator/EndpointFinder.cs b/code/source-generator/EndpointFinder.cs new file mode 100644 index 0000000..b30451e --- /dev/null +++ b/code/source-generator/EndpointFinder.cs @@ -0,0 +1,17 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace I2R.Endpoints.Generator; + +public class EndpointFinder : ISyntaxReceiver +{ + public HashSet<ClassDeclarationSyntax> Endpoints { get; } = new(); + + public void OnVisitSyntaxNode(SyntaxNode syntaxNode) { + if (syntaxNode is not ClassDeclarationSyntax endpoint) return; + if ((endpoint.BaseList?.Types.Any(c => EndpointGenerator.IsSyncEndpoint(c.ToString())) ?? false) + || (endpoint.BaseList?.Types.Any(c => EndpointGenerator.IsAyncEndpoint(c.ToString())) ?? false)) { + Endpoints.Add(endpoint); + } + } +} diff --git a/code/source-generator/EndpointGenerator.cs b/code/source-generator/EndpointGenerator.cs new file mode 100644 index 0000000..989a1c5 --- /dev/null +++ b/code/source-generator/EndpointGenerator.cs @@ -0,0 +1,146 @@ +using System.Text.RegularExpressions; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace I2R.Endpoints.Generator; + +[Generator] +public class EndpointGenerator : ISourceGenerator +{ + private const string SourceGenereatedComment = "// Generated by I2R.Endpoints, probably smart to leave it be"; + + public void Initialize(GeneratorInitializationContext context) { + context.RegisterForSyntaxNotifications(() => new EndpointFinder()); + } + + public void Execute(GeneratorExecutionContext context) { + var endpointClasses = ((EndpointFinder) context.SyntaxReceiver)?.Endpoints; + if (endpointClasses == null) { + context.ReportDiagnostic(CreateDebugDiagnostic("no endpoints were found")); + return; + } + + foreach (var info in endpointClasses.Select(GetEndpointInfoFromClass)) { + context.AddSource(info.GeneratedFileName, info.IsSynchronous ? GetSyncSource(info) : GetAsyncSource(info)); + } + } + + private static Diagnostic CreateDebugDiagnostic(string message) { + var descriptor = new DiagnosticDescriptor("debug", "Debug", message, "debug", DiagnosticSeverity.Warning, true); + return Diagnostic.Create(descriptor, null, ""); + } + + private class EndpointInfo + { + public string BaseEndpointClassName { get; set; } + public bool IsSynchronous { get; set; } + public string GeneratedFileName { get; set; } + } + + public static bool IsSyncEndpoint(string name) { + return name.StartsWith("SyncEndpoint"); + } + + public static bool IsAyncEndpoint(string name) { + return name.StartsWith("AsyncEndpoint"); + } + + private static EndpointInfo GetEndpointInfoFromClass(ClassDeclarationSyntax classDeclarationSyntax) { + var baseType = classDeclarationSyntax.BaseList?.Types.FirstOrDefault(); + if (baseType == default) { + return default; + } + + var fullTypeName = baseType.Type.ToString(); + var className = Regex.Match(fullTypeName, "(!?<).+?(?=>)").Value.Replace("<", ""); + return new EndpointInfo() { + BaseEndpointClassName = className, + IsSynchronous = IsSyncEndpoint(baseType.ToString()), + GeneratedFileName = className + ".endpoints.g.cs" + }; + } + + private static string GetSyncSource(EndpointInfo info) { + return $@" +{SourceGenereatedComment} +namespace {typeof(EndpointGenerator).Namespace}; +public static partial class SyncEndpoint<T{info.BaseEndpointClassName}> +{{ + public static class Req<TRequest> + {{ + public abstract class Res<TResponse> : {info.BaseEndpointClassName} + {{ + public abstract TResponse Handle( + TRequest request + ); + }} + + public abstract class NoRes : {info.BaseEndpointClassName} + {{ + public abstract void Handle( + TRequest request + ); + }} + }} + + public static class NoReq + {{ + public abstract class Res<TResponse> : {info.BaseEndpointClassName} + {{ + public abstract TResponse Handle(); + }} + + public abstract class NoRes : {info.BaseEndpointClassName} + {{ + public abstract void Handle(); + }} + }} +}} +"; + } + + private static string GetAsyncSource(EndpointInfo info) { + return $@" +{SourceGenereatedComment} +namespace {typeof(EndpointGenerator).Namespace}; +public static partial class AsyncEndpoint<T> +{{ + public static class Req<TRequest> + {{ + public abstract class Res<TResponse> : {info.BaseEndpointClassName} + {{ + public abstract Task<TResponse> HandleAsync( + TRequest request, + CancellationToken cancellationToken = default + ); + }} + + public abstract class NoRes : {info.BaseEndpointClassName} + {{ + public abstract Task HandleAsync( + TRequest request, + CancellationToken cancellationToken = default + ); + }} + }} + + public static class NoReq + {{ + public abstract class Res<TResponse> : {info.BaseEndpointClassName} + {{ + public abstract Task<TResponse> HandleAsync( + CancellationToken cancellationToken = default + ); + }} + + public abstract class NoRes : {info.BaseEndpointClassName} + {{ + public abstract Task HandleAsync( + CancellationToken cancellationToken = default + ); + }} + }} +}} +"; + } +} diff --git a/code/source-generator/I2R.Endpoints.Generator.csproj b/code/source-generator/I2R.Endpoints.Generator.csproj new file mode 100644 index 0000000..f67f7d3 --- /dev/null +++ b/code/source-generator/I2R.Endpoints.Generator.csproj @@ -0,0 +1,18 @@ +<Project Sdk="Microsoft.NET.Sdk"> + <PropertyGroup> + <TargetFramework>netstandard2.0</TargetFramework> + <LangVersion>10</LangVersion> + <ImplicitUsings>true</ImplicitUsings> + <DevelopmentDependency>true</DevelopmentDependency> + </PropertyGroup> + <ItemGroup> + <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> + <ItemGroup> + <None Include="$(OutputPath)\$(AssemblyName).dll" Pack="true" PackagePath="analyzers/dotnet/cs" Visible="false"/> + </ItemGroup> +</Project> diff --git a/src/EndpointFinder.cs b/src/EndpointFinder.cs deleted file mode 100644 index ce1b321..0000000 --- a/src/EndpointFinder.cs +++ /dev/null @@ -1,19 +0,0 @@ -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 deleted file mode 100644 index c0b7c1b..0000000 --- a/src/EndpointGenerator.cs +++ /dev/null @@ -1,184 +0,0 @@ -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 $@" -{SourceGenereatedComment} -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 $@" -{SourceGenereatedComment} -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 - var nameSpace = string.Empty; - - // Get the containing syntax node for the type declaration - // (could be a nested type, for example) - var 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; - } -}
\ No newline at end of file diff --git a/src/Helpers.cs b/src/Helpers.cs deleted file mode 100644 index 0159ae8..0000000 --- a/src/Helpers.cs +++ /dev/null @@ -1,11 +0,0 @@ -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 |
